chore(ng-schematics): Update ng e2e to custom builder (#9300)

**What kind of change does this PR introduce?**

This replaces the default `ng e2e` with our custom builder. In the
feature it seem possible to remove the necessity of the user running the
server separately and run it from the builder - that will improve the
easy of use and CI of this schematic.

**Did you add tests for your changes?**

**If relevant, did you update the documentation?**

Yes - Updated `@puppeteer/ng-schematics` README.md

**Summary**

We want to not see the default `ng e2e` and we want to make it easier
for the user to run commands.
Angular Developer are likely to also use its' CLI.

**Does this PR introduce a breaking change?**

Yes. Users need to delete the default and initialize the schematics
again.

**Other information**
This commit is contained in:
Nikolay Vitkov 2022-11-22 14:02:24 +01:00 committed by Alex
parent 0107ad8f08
commit 16784fc8cb
14 changed files with 340 additions and 56 deletions

105
package-lock.json generated
View File

@ -80,6 +80,58 @@
"zod": "3.19.1"
}
},
"node_modules/@angular-devkit/architect": {
"version": "0.1402.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.10.tgz",
"integrity": "sha512-/6YmPrgataj1jD2Uqd1ED+CG4DaZGacoeZd/89hH7hF76Nno8K18DrSOqJAEmDnOWegpSRGVLd0qP09IHmaG5w==",
"dependencies": {
"@angular-devkit/core": "14.2.10",
"rxjs": "6.6.7"
},
"engines": {
"node": "^14.15.0 || >=16.10.0",
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
"yarn": ">= 1.13.0"
}
},
"node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": {
"version": "14.2.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.10.tgz",
"integrity": "sha512-K4AO7mROTdbhQ7chtyQd6oPwmuL+BPUh+wn6Aq1qrmYJK4UZYFOPp8fi/Ehs8meCEeywtrssOPfrOE4Gsre9dg==",
"dependencies": {
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"jsonc-parser": "3.1.0",
"rxjs": "6.6.7",
"source-map": "0.7.4"
},
"engines": {
"node": "^14.15.0 || >=16.10.0",
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
"yarn": ">= 1.13.0"
},
"peerDependencies": {
"chokidar": "^3.5.2"
},
"peerDependenciesMeta": {
"chokidar": {
"optional": true
}
}
},
"node_modules/@angular-devkit/architect/node_modules/jsonc-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz",
"integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg=="
},
"node_modules/@angular-devkit/architect/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/@angular-devkit/core": {
"version": "14.2.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.7.tgz",
@ -8703,6 +8755,7 @@
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
"@angular-devkit/architect": "^0.1402.10",
"@angular-devkit/core": "^14.2.6",
"@angular-devkit/schematics": "^14.2.6"
},
@ -8755,24 +8808,6 @@
"node": ">=14.1.0"
}
},
"packages/puppeteer-schematics": {
"version": "0.0.0",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"@angular-devkit/architect": "0.1402.7",
"@angular-devkit/core": "^14.2.6",
"@angular-devkit/schematics": "^14.2.6",
"@schematics/angular": "^14.2.8",
"typescript": "~4.7.2"
},
"devDependencies": {
"@types/node": "^14.15.0"
},
"engines": {
"node": ">=14.1.0"
}
},
"packages/testserver": {
"name": "@pptr/testserver",
"version": "0.6.0",
@ -8799,6 +8834,39 @@
}
},
"dependencies": {
"@angular-devkit/architect": {
"version": "0.1402.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.10.tgz",
"integrity": "sha512-/6YmPrgataj1jD2Uqd1ED+CG4DaZGacoeZd/89hH7hF76Nno8K18DrSOqJAEmDnOWegpSRGVLd0qP09IHmaG5w==",
"requires": {
"@angular-devkit/core": "14.2.10",
"rxjs": "6.6.7"
},
"dependencies": {
"@angular-devkit/core": {
"version": "14.2.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.10.tgz",
"integrity": "sha512-K4AO7mROTdbhQ7chtyQd6oPwmuL+BPUh+wn6Aq1qrmYJK4UZYFOPp8fi/Ehs8meCEeywtrssOPfrOE4Gsre9dg==",
"requires": {
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"jsonc-parser": "3.1.0",
"rxjs": "6.6.7",
"source-map": "0.7.4"
}
},
"jsonc-parser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz",
"integrity": "sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg=="
},
"source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
}
}
},
"@angular-devkit/core": {
"version": "14.2.7",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.7.tgz",
@ -9876,6 +9944,7 @@
"@puppeteer/ng-schematics": {
"version": "file:packages/ng-schematics",
"requires": {
"@angular-devkit/architect": "^0.1402.10",
"@angular-devkit/core": "^14.2.6",
"@angular-devkit/schematics": "^14.2.6",
"@schematics/angular": "^14.2.8",

View File

@ -20,13 +20,14 @@ Currently, this schematic supports the following test frameworks:
- **Mocha** [https://mochajs.org/]
- **Node Test Runner** _(Experimental)_ [https://nodejs.org/api/test.html]
With the schematics installed, you can run E2E tests:
With the schematics installed you can run E2E tests:
```bash
npm run e2e
# or yarn e2e
ng e2e
```
> Note: Server must be running before executing the command.
## Options
When adding schematics to your project you can to provide following options:

View File

@ -26,6 +26,7 @@
"node": ">=14.1.0"
},
"dependencies": {
"@angular-devkit/architect": "^0.1402.10",
"@angular-devkit/core": "^14.2.6",
"@angular-devkit/schematics": "^14.2.6"
},
@ -40,5 +41,6 @@
"ng-add": {
"save": "devDependencies"
},
"schematics": "./lib/schematics/collection.json"
"schematics": "./lib/schematics/collection.json",
"builders": "./lib/builders/builders.json"
}

View File

@ -0,0 +1,10 @@
{
"$schema": "../../../../node_modules/@angular-devkit/architect/src/builders-schema.json",
"builders": {
"puppeteer": {
"implementation": "./puppeteer",
"schema": "./puppeteer/schema.json",
"description": "Run e2e test with Puppeteer"
}
}
}

View File

@ -0,0 +1,82 @@
import {
createBuilder,
BuilderContext,
BuilderOutput,
} from '@angular-devkit/architect';
import {spawn} from 'child_process';
import {PuppeteerBuilderOptions} from './types.js';
function getError(executable: string, args: string[]) {
return (
`Puppeteer E2E tests failed!` +
'\n' +
`Error running '${executable}' with arguments '${args.join(' ')}'.` +
`\n` +
'Please look at the output above to determine the issue!'
);
}
function getExecutable(command: string[]) {
const executable = command.shift()!;
const error = getError(executable, command);
if (executable === 'node') {
return {
executable: executable,
args: command,
error,
};
}
return {
executable: `./node_modules/.bin/${executable}`,
args: command,
error,
};
}
async function executeCommand(context: BuilderContext, command: string[]) {
await new Promise((resolve, reject) => {
const {executable, args, error} = getExecutable(command);
const child = spawn(executable, args, {
cwd: context.workspaceRoot,
stdio: 'inherit',
});
child.on('error', message => {
console.log(message);
reject(error);
});
child.on('exit', code => {
if (code === 0) {
resolve(true);
} else {
reject(error);
}
});
});
}
async function executeE2ETest(
options: PuppeteerBuilderOptions,
context: BuilderContext
): Promise<BuilderOutput> {
context.logger.debug('Running commands for E2E test.');
try {
for (const command of options.commands) {
await executeCommand(context, command);
}
return {success: true};
} catch (error) {
if (error instanceof Error) {
return {success: false, error: error.message};
}
return {success: false, error: error as any};
}
}
export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest) as any;

View File

@ -0,0 +1,18 @@
{
"title": "Puppeteer",
"description": "Options for Puppeteer Angular Schematics",
"type": "object",
"properties": {
"commands": {
"type": "array",
"items": {
"type": "array",
"item": {
"type": "string"
}
},
"description": "Commands to execute in the repo. Commands prefixed with `./node_modules/bin` (Exception: 'node')."
}
},
"additionalProperties": true
}

View File

@ -0,0 +1,23 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {JsonObject} from '@angular-devkit/core';
type Command = [string, ...string[]];
export interface PuppeteerBuilderOptions extends JsonObject {
commands: Command[];
}

View File

@ -7,7 +7,7 @@
<% } %><% if(testingFramework == 'node') { %>
"module": "CommonJS",
"rootDir": "tests/",
"outDir": "out-tsc/",
"outDir": "test/",
<% } %>
"types": ["<%= testingFramework %>"]
},

View File

@ -1,3 +1,3 @@
# Compiled e2e tests output
# Compiled e2e tests output Node auto resolves files in folders named 'test'
/out-tsc
test/

View File

@ -31,6 +31,7 @@ import {
getPackageLatestNpmVersion,
DependencyType,
type NodePackage,
updateAngularJsonScripts,
} from '../utils/packages.js';
import {type SchematicsOptions} from '../utils/types.js';
@ -45,6 +46,7 @@ export function ngAdd(options: SchematicsOptions): Rule {
addPuppeteerFiles(options),
addOtherFiles(options),
updateScripts(options),
updateAngularConfig(options),
])(tree, context);
};
}
@ -73,15 +75,14 @@ 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 script = getScriptFromOptions(options);
return addPackageJsonScripts(tree, [
{
name: 'e2e',
script,
script: 'ng e2e',
},
]);
};
@ -89,7 +90,7 @@ function updateScripts(options: SchematicsOptions): Rule {
function addPuppeteerFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer base files');
context.logger.debug('Adding Puppeteer base files.');
const {projects} = getAngularConfig(tree);
return addBaseFiles(tree, context, {
@ -101,7 +102,7 @@ function addPuppeteerFiles(options: SchematicsOptions): Rule {
function addOtherFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer additional files');
context.logger.debug('Adding Puppeteer additional files.');
const {projects} = getAngularConfig(tree);
return addFrameworkFiles(tree, context, {
@ -110,3 +111,12 @@ 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);
};
}

View File

@ -1,7 +1,22 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {getSystemPath, normalize, strings} from '@angular-devkit/core';
import {
SchematicContext,
SchematicsException,
Tree,
apply,
applyTemplates,
@ -121,17 +136,18 @@ export function addFrameworkFiles(
return addFiles(tree, context, options);
}
export function getScriptFromOptions(options: SchematicsOptions): string {
export function getScriptFromOptions(options: SchematicsOptions): string[][] {
switch (options.testingFramework) {
case TestingFramework.Jasmine:
return 'jasmine --config=./e2e/support/jasmine.json';
return [[`jasmine`, '--config=./e2e/support/jasmine.json']];
case TestingFramework.Jest:
return 'jest -c e2e/jest.config.js';
return [[`jest`, '-c', 'e2e/jest.config.js']];
case TestingFramework.Mocha:
return 'mocha --config=./e2e/.mocharc.js';
return [[`mocha`, '--config=./e2e/.mocharc.js']];
case TestingFramework.Node:
return 'tsc -p e2e/tsconfig.json && node --test e2e/out-tsc/**.js';
default:
throw new SchematicsException('Testing framework not supported.');
return [
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', 'e2e/'],
];
}
}

View File

@ -14,10 +14,14 @@
* limitations under the License.
*/
import {SchematicsException, Tree} from '@angular-devkit/schematics';
import {Tree} from '@angular-devkit/schematics';
import {get} from 'https';
import {SchematicsOptions, TestingFramework} from './types.js';
import {getJsonFileAsObject, getObjectAsJson} from './json.js';
import {
getAngularConfig,
getJsonFileAsObject,
getObjectAsJson,
} from './json.js';
export interface NodePackage {
name: string;
version: string;
@ -68,7 +72,7 @@ export function getPackageLatestNpmVersion(name: string): Promise<NodePackage> {
function updateJsonValues(
json: Record<string, any>,
target: string,
updates: Array<{name: string; value: string}>,
updates: Array<{name: string; value: any}>,
overwrite = false
) {
updates.forEach(({name, value}) => {
@ -128,8 +132,6 @@ export function getDependenciesFromOptions(
case TestingFramework.Node:
dependencies.push('@types/node');
break;
default:
throw new SchematicsException(`Testing framework not supported.`);
}
return dependencies;
@ -156,3 +158,36 @@ export function addPackageJsonScripts(
return tree;
}
export function updateAngularJsonScripts(
tree: Tree,
commands: string[][],
overwrite = true
): Tree {
const angularJson = getAngularConfig(tree);
const e2eScript = [
{
name: 'e2e',
value: {
builder: '@puppeteer/ng-schematics:puppeteer',
options: {
commands,
},
},
},
];
Object.keys(angularJson['projects']).forEach(project => {
updateJsonValues(
angularJson['projects'][project],
'architect',
e2eScript,
overwrite
);
});
tree.overwrite('./angular.json', getObjectAsJson(angularJson));
return tree;
}

View File

@ -22,6 +22,11 @@ function getProjectFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`;
}
function getAngularJsonScripts(tree: UnitTestTree): Record<string, any> {
const angularJson = tree.readJson('angular.json') as any;
return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'];
}
function getPackageJson(tree: UnitTestTree): {
scripts: Record<string, string>;
devDependencies: string[];
@ -87,11 +92,14 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree();
const {devDependencies} = getPackageJson(tree);
const {devDependencies, scripts} = getPackageJson(tree);
const {e2e} = 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');
});
it('should create Puppeteer config', async () => {
@ -114,55 +122,65 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
const tree = await buildTestingTree({
testingFramework: 'jasmine',
});
const {scripts, devDependencies} = getPackageJson(tree);
const {devDependencies} = getPackageJson(tree);
const {e2e} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/support/jasmine.json'));
expect(tree.files).toContain(getProjectFile('e2e/helpers/babel.js'));
expect(scripts['e2e']).toBe('jasmine --config=./e2e/support/jasmine.json');
expect(devDependencies).toContain('jasmine');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(e2e.options.commands).toEqual([
[`jasmine`, '--config=./e2e/support/jasmine.json'],
]);
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree({
testingFramework: 'jest',
});
const {scripts, devDependencies} = getPackageJson(tree);
const {devDependencies} = getPackageJson(tree);
const {e2e} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/jest.config.js'));
expect(scripts['e2e']).toBe('jest -c 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'],
]);
});
it('should create Jasmine files and update "package.json"', async () => {
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree({
testingFramework: 'mocha',
});
const {scripts, devDependencies} = getPackageJson(tree);
const {devDependencies} = getPackageJson(tree);
const {e2e} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/.mocharc.js'));
expect(tree.files).toContain(getProjectFile('e2e/babel.js'));
expect(scripts['e2e']).toBe('mocha --config=./e2e/.mocharc.js');
expect(devDependencies).toContain('mocha');
expect(devDependencies).toContain('@types/mocha');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(e2e.options.commands).toEqual([
[`mocha`, '--config=./e2e/.mocharc.js'],
]);
});
it('should create Node files"', async () => {
const tree = await buildTestingTree({
testingFramework: 'node',
});
const {scripts} = getPackageJson(tree);
const {e2e} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/.gitignore'));
expect(scripts['e2e']).toBe(
'tsc -p e2e/tsconfig.json && node --test e2e/out-tsc/**.js'
);
expect(e2e.options.commands).toEqual([
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', 'e2e/'],
]);
});
});

View File

@ -2,7 +2,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "tsconfig",
"lib": ["es2018", "dom"],
"lib": ["ES2018"],
"module": "CommonJS",
"noEmitOnError": true,
"rootDir": "src/",