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 <OrKoN@users.noreply.github.com>
This commit is contained in:
Nikolay Vitkov 2022-11-23 13:10:03 +01:00 committed by GitHub
parent 181b20fedf
commit 689a084251
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 41 deletions

View File

@ -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:

View File

@ -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<BuilderRun> {
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<BuilderOutput> {
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();
}
}
}

View File

@ -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

View File

@ -20,4 +20,5 @@ type Command = [string, ...string[]];
export interface PuppeteerBuilderOptions extends JsonObject {
commands: Command[];
devServerTarget: string;
}

View File

@ -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);
};
}

View File

@ -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": "",

View File

@ -151,3 +151,10 @@ export function getScriptFromOptions(options: SchematicsOptions): string[][] {
];
}
}
export function getNgCommandName(options: SchematicsOptions): string {
if (options.isDefaultTester) {
return 'e2e';
}
return 'puppeteer';
}

View File

@ -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 e2eScript = [
{
name: 'e2e',
value: {
builder: '@puppeteer/ng-schematics:puppeteer',
options: {
commands,
},
},
},
];
const commands = getScriptFromOptions(options);
const name = getNgCommandName(options);
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`,
},
},
},
},
];
updateJsonValues(
angularJson['projects'][project],
'architect',

View File

@ -22,6 +22,7 @@ export enum TestingFramework {
}
export interface SchematicsOptions {
isDefaultTester: boolean;
exportConfig: boolean;
testingFramework: TestingFramework;
exportConfig?: boolean;
}

View File

@ -22,9 +22,19 @@ function getProjectFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`;
}
function getAngularJsonScripts(tree: UnitTestTree): Record<string, any> {
function getAngularJsonScripts(
tree: UnitTestTree,
isDefault = true
): {
builder: string;
configurations: Record<string, any>;
options: Record<string, any>;
} {
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<string, any>) {
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/'],
]);