From 689a084251b138ae1a68c3c805cdf18373f63651 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com> Date: Wed, 23 Nov 2022 13:10:03 +0100 Subject: [PATCH] chore(ng-schematics): Spawn server when running `ng e2e` (#9306) **What kind of change does this PR introduce?** Spawn own server when running `ng e2e`. Give user option to not replace `ng e2e`. **Did you add tests for your changes?** Yes. **If relevant, did you update the documentation?** Yes, `ng-schematics` README.md updated. **Summary** When running `ng-schematics`'s `ng e2e` command spawns it's own server. This way we remove the need of developers to run `ng server` separately thus increasing ease of use in development and CI. We want to support Protractor migration so we give the user the option to opt out of replacing `ng e2e` so they can have a gradual migration. (Note: There may be issues with folder conflicts, to be address in a PR for adding better Migration support) **Does this PR introduce a breaking change?** Yes, as we don't check if required options are there before spawning the server. **Other information** Co-authored-by: Alex Rudenko --- packages/ng-schematics/README.md | 7 ++- .../src/builders/puppeteer/index.ts | 57 ++++++++++++++++++- .../src/builders/puppeteer/schema.json | 4 ++ .../src/builders/puppeteer/types.ts | 1 + .../src/schematics/ng-add/index.ts | 26 +++++---- .../src/schematics/ng-add/schema.json | 8 ++- .../src/schematics/utils/files.ts | 7 +++ .../src/schematics/utils/packages.ts | 31 ++++++---- .../src/schematics/utils/types.ts | 3 +- packages/ng-schematics/test/src/index.spec.ts | 53 ++++++++++++----- 10 files changed, 158 insertions(+), 39 deletions(-) diff --git a/packages/ng-schematics/README.md b/packages/ng-schematics/README.md index 2c25e28a4ae4d..fb37403913829 100644 --- a/packages/ng-schematics/README.md +++ b/packages/ng-schematics/README.md @@ -26,7 +26,7 @@ With the schematics installed you can run E2E tests: ng e2e ``` -> Note: Server must be running before executing the command. +> Note: Command spawns it's own server on the same port `ng serve` does. ## Options @@ -34,9 +34,14 @@ When adding schematics to your project you can to provide following options: | Option | Description | Value | Required | | -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- | +| `--isDefaultTester` | When true, replaces default `ng e2e` command. | `boolean` | `true` | | `--exportConfig` | When true, creates an empty [Puppeteer configuration](https://pptr.dev/guides/configuration) file. (`.puppeteerrc.cjs`) | `boolean` | `true` | | `--testingFramework` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` | +## Contributing + +Check out our [contributing guide](https://pptr.dev/contributing) to get an overview of what you need to develop in the Puppeteer repo. + ### Unit Testing The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit: diff --git a/packages/ng-schematics/src/builders/puppeteer/index.ts b/packages/ng-schematics/src/builders/puppeteer/index.ts index 8822776e03928..fe73e5bcd797f 100644 --- a/packages/ng-schematics/src/builders/puppeteer/index.ts +++ b/packages/ng-schematics/src/builders/puppeteer/index.ts @@ -2,11 +2,22 @@ import { createBuilder, BuilderContext, BuilderOutput, + targetFromTargetString, + BuilderRun, } from '@angular-devkit/architect'; +import {JsonObject} from '@angular-devkit/core'; import {spawn} from 'child_process'; import {PuppeteerBuilderOptions} from './types.js'; +const terminalStyles = { + blue: '\u001b[34m', + green: '\u001b[32m', + bold: '\u001b[1m', + reverse: '\u001b[7m', + clear: '\u001b[0m', +}; + function getError(executable: string, args: string[]) { return ( `Puppeteer E2E tests failed!` + @@ -38,6 +49,7 @@ function getExecutable(command: string[]) { async function executeCommand(context: BuilderContext, command: string[]) { await new Promise((resolve, reject) => { + context.logger.debug(`Trying to execute command - ${command.join(' ')}.`); const {executable, args, error} = getExecutable(command); const child = spawn(executable, args, { @@ -60,22 +72,65 @@ async function executeCommand(context: BuilderContext, command: string[]) { }); } +function message( + message: string, + context: BuilderContext, + type: 'info' | 'success' = 'info' +): void { + const color = type === 'info' ? terminalStyles.blue : terminalStyles.green; + context.logger.info( + `${terminalStyles.bold}${terminalStyles.reverse}${color}${message}${terminalStyles.clear}` + ); +} + +async function startServer( + options: PuppeteerBuilderOptions, + context: BuilderContext +): Promise { + context.logger.debug('Trying to start server.'); + const target = targetFromTargetString(options.devServerTarget); + const defaultServerOptions = await context.getTargetOptions(target); + + const overrides = { + watch: false, + host: defaultServerOptions['host'], + port: defaultServerOptions['port'], + } as JsonObject; + + message('Spawning test server...\n', context); + const server = await context.scheduleTarget(target, overrides); + const result = await server.result; + if (!result.success) { + throw new Error('Failed to spawn server! Stopping tests...'); + } + + return server; +} + async function executeE2ETest( options: PuppeteerBuilderOptions, context: BuilderContext ): Promise { - context.logger.debug('Running commands for E2E test.'); + let server: BuilderRun | null = null; try { + server = await startServer(options, context); + + message('\nRunning tests...\n', context); for (const command of options.commands) { await executeCommand(context, command); } + message('\nTest ran successfully!', context, 'success'); return {success: true}; } catch (error) { if (error instanceof Error) { return {success: false, error: error.message}; } return {success: false, error: error as any}; + } finally { + if (server) { + await server.stop(); + } } } diff --git a/packages/ng-schematics/src/builders/puppeteer/schema.json b/packages/ng-schematics/src/builders/puppeteer/schema.json index 739b98bfb1afd..42e80f46d0542 100644 --- a/packages/ng-schematics/src/builders/puppeteer/schema.json +++ b/packages/ng-schematics/src/builders/puppeteer/schema.json @@ -12,6 +12,10 @@ } }, "description": "Commands to execute in the repo. Commands prefixed with `./node_modules/bin` (Exception: 'node')." + }, + "devServerTarget": { + "type": "string", + "description": "Angular target that spawns the server." } }, "additionalProperties": true diff --git a/packages/ng-schematics/src/builders/puppeteer/types.ts b/packages/ng-schematics/src/builders/puppeteer/types.ts index cc8db1392abfa..b25d51b5f1258 100644 --- a/packages/ng-schematics/src/builders/puppeteer/types.ts +++ b/packages/ng-schematics/src/builders/puppeteer/types.ts @@ -20,4 +20,5 @@ type Command = [string, ...string[]]; export interface PuppeteerBuilderOptions extends JsonObject { commands: Command[]; + devServerTarget: string; } diff --git a/packages/ng-schematics/src/schematics/ng-add/index.ts b/packages/ng-schematics/src/schematics/ng-add/index.ts index dacdbacf449e6..386066211bf7a 100644 --- a/packages/ng-schematics/src/schematics/ng-add/index.ts +++ b/packages/ng-schematics/src/schematics/ng-add/index.ts @@ -22,7 +22,7 @@ import {of} from 'rxjs'; import { addBaseFiles, addFrameworkFiles, - getScriptFromOptions, + getNgCommandName, } from '../utils/files.js'; import { addPackageJsonDependencies, @@ -75,16 +75,23 @@ function addDependencies(options: SchematicsOptions): Rule { }; } -function updateScripts(_options: SchematicsOptions): Rule { +function updateScripts(options: SchematicsOptions): Rule { return (tree: Tree, context: SchematicContext): Tree => { context.logger.debug('Updating "package.json" scripts'); + const angularJson = getAngularConfig(tree); + const projects = Object.keys(angularJson['projects']); - return addPackageJsonScripts(tree, [ - { - name: 'e2e', - script: 'ng e2e', - }, - ]); + if (projects.length === 1) { + const name = getNgCommandName(options); + const prefix = options.isDefaultTester ? '' : `run ${projects[0]}:`; + return addPackageJsonScripts(tree, [ + { + name, + script: `ng ${prefix}${name}`, + }, + ]); + } + return tree; }; } @@ -115,8 +122,7 @@ function addOtherFiles(options: SchematicsOptions): Rule { function updateAngularConfig(options: SchematicsOptions): Rule { return (tree: Tree, context: SchematicContext): Tree => { context.logger.debug('Updating "angular.json".'); - const script = getScriptFromOptions(options); - return updateAngularJsonScripts(tree, script); + return updateAngularJsonScripts(tree, options); }; } diff --git a/packages/ng-schematics/src/schematics/ng-add/schema.json b/packages/ng-schematics/src/schematics/ng-add/schema.json index 0d7c1a4da2c6c..77cb5bdd4808d 100644 --- a/packages/ng-schematics/src/schematics/ng-add/schema.json +++ b/packages/ng-schematics/src/schematics/ng-add/schema.json @@ -4,11 +4,17 @@ "title": "Puppeteer Install Schema", "type": "object", "properties": { + "isDefaultTester": { + "description": "", + "type": "boolean", + "default": true, + "x-prompt": "Use Puppeteer as default `ng e2e` command?" + }, "exportConfig": { "description": "", "type": "boolean", "default": false, - "x-prompt": "Do you wish to export default Puppeteer config file?" + "x-prompt": "Export default Puppeteer config file?" }, "testingFramework": { "description": "", diff --git a/packages/ng-schematics/src/schematics/utils/files.ts b/packages/ng-schematics/src/schematics/utils/files.ts index 07fe4e03bd80e..443a45e63c7db 100644 --- a/packages/ng-schematics/src/schematics/utils/files.ts +++ b/packages/ng-schematics/src/schematics/utils/files.ts @@ -151,3 +151,10 @@ export function getScriptFromOptions(options: SchematicsOptions): string[][] { ]; } } + +export function getNgCommandName(options: SchematicsOptions): string { + if (options.isDefaultTester) { + return 'e2e'; + } + return 'puppeteer'; +} diff --git a/packages/ng-schematics/src/schematics/utils/packages.ts b/packages/ng-schematics/src/schematics/utils/packages.ts index ab0b615a6942e..b2fa494b123b9 100644 --- a/packages/ng-schematics/src/schematics/utils/packages.ts +++ b/packages/ng-schematics/src/schematics/utils/packages.ts @@ -22,6 +22,7 @@ import { getJsonFileAsObject, getObjectAsJson, } from './json.js'; +import {getNgCommandName, getScriptFromOptions} from './files.js'; export interface NodePackage { name: string; version: string; @@ -161,24 +162,32 @@ export function addPackageJsonScripts( export function updateAngularJsonScripts( tree: Tree, - commands: string[][], + options: SchematicsOptions, overwrite = true ): Tree { const angularJson = getAngularConfig(tree); + const commands = getScriptFromOptions(options); + const name = getNgCommandName(options); - const e2eScript = [ - { - name: 'e2e', - value: { - builder: '@puppeteer/ng-schematics:puppeteer', - options: { - commands, + Object.keys(angularJson['projects']).forEach(project => { + const e2eScript = [ + { + name, + value: { + builder: '@puppeteer/ng-schematics:puppeteer', + options: { + commands, + devServerTarget: `${project}:serve`, + }, + configurations: { + production: { + devServerTarget: `${project}:serve:production`, + }, + }, }, }, - }, - ]; + ]; - Object.keys(angularJson['projects']).forEach(project => { updateJsonValues( angularJson['projects'][project], 'architect', diff --git a/packages/ng-schematics/src/schematics/utils/types.ts b/packages/ng-schematics/src/schematics/utils/types.ts index 80ea621387af1..c59f90e69da17 100644 --- a/packages/ng-schematics/src/schematics/utils/types.ts +++ b/packages/ng-schematics/src/schematics/utils/types.ts @@ -22,6 +22,7 @@ export enum TestingFramework { } export interface SchematicsOptions { + isDefaultTester: boolean; + exportConfig: boolean; testingFramework: TestingFramework; - exportConfig?: boolean; } diff --git a/packages/ng-schematics/test/src/index.spec.ts b/packages/ng-schematics/test/src/index.spec.ts index bb8516f9a9db2..4eebecc044858 100644 --- a/packages/ng-schematics/test/src/index.spec.ts +++ b/packages/ng-schematics/test/src/index.spec.ts @@ -22,9 +22,19 @@ function getProjectFile(file: string): string { return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`; } -function getAngularJsonScripts(tree: UnitTestTree): Record { +function getAngularJsonScripts( + tree: UnitTestTree, + isDefault = true +): { + builder: string; + configurations: Record; + options: Record; +} { const angularJson = tree.readJson('angular.json') as any; - return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect']; + const e2eScript = isDefault ? 'e2e' : 'puppeteer'; + return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'][ + e2eScript + ]; } function getPackageJson(tree: UnitTestTree): { @@ -46,6 +56,7 @@ async function buildTestingTree(userOptions?: Record) { join(__dirname, '../../lib/schematics/collection.json') ); const options = { + isDefaultTester: true, exportConfig: false, testingFramework: 'jasmine', ...userOptions, @@ -93,13 +104,29 @@ describe('@puppeteer/ng-schematics: ng-add', () => { it('should create base files and update to "package.json"', async () => { const tree = await buildTestingTree(); const {devDependencies, scripts} = getPackageJson(tree); - const {e2e} = getAngularJsonScripts(tree); + const {builder, configurations} = getAngularJsonScripts(tree); expect(tree.files).toContain(getProjectFile('e2e/tsconfig.json')); expect(tree.files).toContain(getProjectFile('e2e/tests/app.e2e.ts')); expect(devDependencies).toContain('puppeteer'); expect(scripts['e2e']).toBe('ng e2e'); - expect(e2e.builder).toBe('@puppeteer/ng-schematics:puppeteer'); + expect(builder).toBe('@puppeteer/ng-schematics:puppeteer'); + expect(configurations).toEqual({ + production: { + devServerTarget: 'sandbox:serve:production', + }, + }); + }); + + it('should update create proper "ng" command for non default tester', async () => { + const tree = await buildTestingTree({ + isDefaultTester: false, + }); + const {scripts} = getPackageJson(tree); + const {builder} = getAngularJsonScripts(tree, false); + + expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer'); + expect(builder).toBe('@puppeteer/ng-schematics:puppeteer'); }); it('should create Puppeteer config', async () => { @@ -123,7 +150,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => { testingFramework: 'jasmine', }); const {devDependencies} = getPackageJson(tree); - const {e2e} = getAngularJsonScripts(tree); + const {options} = getAngularJsonScripts(tree); expect(tree.files).toContain(getProjectFile('e2e/support/jasmine.json')); expect(tree.files).toContain(getProjectFile('e2e/helpers/babel.js')); @@ -131,7 +158,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => { expect(devDependencies).toContain('@babel/core'); expect(devDependencies).toContain('@babel/register'); expect(devDependencies).toContain('@babel/preset-typescript'); - expect(e2e.options.commands).toEqual([ + expect(options['commands']).toEqual([ [`jasmine`, '--config=./e2e/support/jasmine.json'], ]); }); @@ -141,15 +168,13 @@ describe('@puppeteer/ng-schematics: ng-add', () => { testingFramework: 'jest', }); const {devDependencies} = getPackageJson(tree); - const {e2e} = getAngularJsonScripts(tree); + const {options} = getAngularJsonScripts(tree); expect(tree.files).toContain(getProjectFile('e2e/jest.config.js')); expect(devDependencies).toContain('jest'); expect(devDependencies).toContain('@types/jest'); expect(devDependencies).toContain('ts-jest'); - expect(e2e.options.commands).toEqual([ - [`jest`, '-c', 'e2e/jest.config.js'], - ]); + expect(options['commands']).toEqual([[`jest`, '-c', 'e2e/jest.config.js']]); }); it('should create Mocha files and update "package.json"', async () => { @@ -157,7 +182,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => { testingFramework: 'mocha', }); const {devDependencies} = getPackageJson(tree); - const {e2e} = getAngularJsonScripts(tree); + const {options} = getAngularJsonScripts(tree); expect(tree.files).toContain(getProjectFile('e2e/.mocharc.js')); expect(tree.files).toContain(getProjectFile('e2e/babel.js')); @@ -166,7 +191,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => { expect(devDependencies).toContain('@babel/core'); expect(devDependencies).toContain('@babel/register'); expect(devDependencies).toContain('@babel/preset-typescript'); - expect(e2e.options.commands).toEqual([ + expect(options['commands']).toEqual([ [`mocha`, '--config=./e2e/.mocharc.js'], ]); }); @@ -175,10 +200,10 @@ describe('@puppeteer/ng-schematics: ng-add', () => { const tree = await buildTestingTree({ testingFramework: 'node', }); - const {e2e} = getAngularJsonScripts(tree); + const {options} = getAngularJsonScripts(tree); expect(tree.files).toContain(getProjectFile('e2e/.gitignore')); - expect(e2e.options.commands).toEqual([ + expect(options['commands']).toEqual([ [`tsc`, '-p', 'e2e/tsconfig.json'], ['node', '--test', 'e2e/'], ]);