/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import { Architect } from '@angular-devkit/architect'; import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core'; import express from 'express'; // eslint-disable-line import/no-extraneous-dependencies import * as http from 'http'; import { AddressInfo } from 'net'; import { createArchitect, host } from '../test-utils'; describe('AppShell Builder', () => { const target = { project: 'app', target: 'app-shell' }; let architect: Architect; beforeEach(async () => { await host.initialize().toPromise(); architect = (await createArchitect(host.root())).architect; }); afterEach(async () => host.restore().toPromise()); const appShellRouteFiles = { 'src/styles.css': ` p { color: #000 } `, 'src/app/app-shell/app-shell.component.html': `

app-shell works!

`, 'src/app/app-shell/app-shell.component.ts': ` import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-app-shell', templateUrl: './app-shell.component.html', }) export class AppShellComponent implements OnInit { constructor() { } ngOnInit() { } } `, 'src/app/app.module.ts': ` import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { environment } from '../environments/environment'; import { RouterModule } from '@angular/router'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), AppRoutingModule, RouterModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } `, 'src/app/app.server.module.ts': ` import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { Routes, RouterModule } from '@angular/router'; import { AppShellComponent } from './app-shell/app-shell.component'; const routes: Routes = [ { path: 'shell', component: AppShellComponent }]; @NgModule({ imports: [ AppModule, ServerModule, RouterModule.forRoot(routes), ], bootstrap: [AppComponent], declarations: [AppShellComponent], }) export class AppServerModule {} `, 'src/main.ts': ` import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } document.addEventListener('DOMContentLoaded', () => { platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.log(err)); }); `, 'src/app/app-routing.module.ts': ` import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = []; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } `, 'src/app/app.component.html': ` `, }; it('works (basic)', async () => { host.replaceInFile( 'src/app/app.module.ts', / {4}BrowserModule/, ` BrowserModule.withServerTransition({ appId: 'some-app' }) `, ); const run = await architect.scheduleTarget(target); const output = await run.result; await run.stop(); expect(output.success).toBe(true); const fileName = 'dist/index.html'; const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); expect(content).toMatch(/Welcome to app!/); }); it('works with route', async () => { host.writeMultipleFiles(appShellRouteFiles); const overrides = { route: 'shell' }; const run = await architect.scheduleTarget(target, overrides); const output = await run.result; await run.stop(); expect(output.success).toBe(true); const fileName = 'dist/index.html'; const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); expect(content).toContain('app-shell works!'); }); it('works with route and service-worker', async () => { host.writeMultipleFiles(appShellRouteFiles); host.writeMultipleFiles({ 'src/ngsw-config.json': ` { "index": "/index.html", "assetGroups": [{ "name": "app", "installMode": "prefetch", "resources": { "files": [ "/favicon.ico", "/index.html", "/*.css", "/*.js" ] } }, { "name": "assets", "installMode": "lazy", "updateMode": "prefetch", "resources": { "files": [ "/assets/**" ] } }] } `, 'src/app/app.module.ts': ` import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; import { RouterModule } from '@angular/router'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), AppRoutingModule, ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production }), RouterModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } `, 'e2e/app.e2e-spec.ts': ` import { browser, by, element } from 'protractor'; it('should have ngsw in normal state', () => { browser.get('/'); // Wait for service worker to load. browser.sleep(2000); browser.waitForAngularEnabled(false); browser.get('/ngsw/state'); // Should have updated, and be in normal state. expect(element(by.css('pre')).getText()).not.toContain('Last update check: never'); expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL'); }); `, }); // This should match the browser target prod config. host.replaceInFile( 'angular.json', '"buildOptimizer": true', '"buildOptimizer": true, "serviceWorker": true', ); // We're changing the workspace file so we need to recreate the Architect instance. architect = (await createArchitect(host.root())).architect; const overrides = { route: 'shell' }; const run = await architect.scheduleTarget( { ...target, configuration: 'production' }, overrides, ); const output = await run.result; await run.stop(); expect(output.success).toBe(true); // Make sure the index is pre-rendering the route. const fileName = 'dist/index.html'; const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); expect(content).toContain('app-shell works!'); // Serve the app using a simple static server. const app = express(); app.use('/', express.static(getSystemPath(join(host.root(), 'dist')) + '/')); const server = await new Promise((resolve) => { const innerServer = app.listen(0, 'localhost', () => resolve(innerServer)); }); try { const serverPort = (server.address() as AddressInfo).port; // Load app in protractor, then check service worker status. const protractorRun = await architect.scheduleTarget( { project: 'app-e2e', target: 'e2e' }, { baseUrl: `http://localhost:${serverPort}/`, devServerTarget: '' }, ); const protractorOutput = await protractorRun.result; await protractorRun.stop(); expect(protractorOutput.success).toBe(true); } finally { // Close the express server. await new Promise((resolve) => server.close(() => resolve())); } }); it('critical CSS is inlined', async () => { host.writeMultipleFiles(appShellRouteFiles); const overrides = { route: 'shell', browserTarget: 'app:build:production,inline-critical-css', }; const run = await architect.scheduleTarget(target, overrides); const output = await run.result; await run.stop(); expect(output.success).toBe(true); const fileName = 'dist/index.html'; const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); expect(content).toContain('app-shell works!'); expect(content).toContain('p{color:#000;}'); expect(content).toMatch( //, ); }); });