fix: port option to run dev and e2e side-by-side (#10458)

This commit is contained in:
Nikolay Vitkov 2023-06-28 10:01:59 +02:00 committed by GitHub
parent ceb6fbb365
commit a43b346bfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 150 additions and 34 deletions

View File

@ -2,9 +2,10 @@
Adds Puppeteer-based e2e tests to your Angular project. Adds Puppeteer-based e2e tests to your Angular project.
## Usage ## Getting started
Run the command below in an Angular CLI app directory and follow the prompts. Run the command below in an Angular CLI app directory and follow the prompts.
_Note this will add the schematic as a dependency to your project._ _Note this will add the schematic as a dependency to your project._
```bash ```bash
@ -15,10 +16,10 @@ Or you can use the same command followed by the [options](#options) below.
Currently, this schematic supports the following test frameworks: Currently, this schematic supports the following test frameworks:
- **Jasmine** [https://jasmine.github.io/] - [**Jasmine**](https://jasmine.github.io/)
- **Jest** [https://jestjs.io/] - [**Jest**](https://jestjs.io/)
- **Mocha** [https://mochajs.org/] - [**Mocha**](https://mochajs.org/)
- **Node Test Runner** _(Experimental)_ [https://nodejs.org/api/test.html] - [**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:
@ -26,9 +27,7 @@ With the schematics installed you can run E2E tests:
ng e2e ng e2e
``` ```
> Note: Command spawns it's own server on the same port `ng serve` does. ### Options
## Options
When adding schematics to your project you can to provide following options: When adding schematics to your project you can to provide following options:
@ -37,6 +36,41 @@ When adding schematics to your project you can to provide following options:
| `--isDefaultTester` | When true, replaces default `ng e2e` command. | `boolean` | `true` | | `--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` | | `--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` | | `--testingFramework` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` |
| `--port` | The port to spawn server for E2E. If default is used `ng serve` and `ng e2e` will not run side-by-side. | `number` | `4200` |
## Creating a single test file
Puppeteer Angular Schematic exposes a method to create a single test file.
```bash
ng generate @puppeteer/ng-schematics:test "<TestName>"
```
### Running test server and dev server at the same time
By default the E2E test will run the app on the same port as `ng start`.
To avoid this you can specify the port the an the `angular.json`
Update either `e2e` or `puppeteer` (depending on the initial setup) to:
```json
{
"e2e": {
"builder": "@puppeteer/ng-schematics:puppeteer",
"options": {
"commands": [...],
"devServerTarget": "sandbox:serve",
"testingFramework": "<TestingFramework>",
"port": 8080
},
...
}
```
Now update the E2E test file `utils.ts` baseUrl to:
```ts
const baseUrl = 'http://localhost:8080';
```
## Contributing ## Contributing
@ -58,6 +92,12 @@ npm run sandbox
npm run sandbox -- --build npm run sandbox -- --build
``` ```
To run the creating of single test schematic:
```bash
npm run sandbox:test
```
### Unit Testing ### Unit Testing
The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit: The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:

View File

@ -8,7 +8,8 @@
"dev:test": "npm run test --watch", "dev:test": "npm run test --watch",
"dev": "npm run build --watch", "dev": "npm run build --watch",
"test": "wireit", "test": "wireit",
"sandbox": "node tools/sandbox.js" "sandbox": "node tools/sandbox.js",
"sandbox:test": "node tools/sandbox.js --test"
}, },
"wireit": { "wireit": {
"build": { "build": {

View File

@ -105,7 +105,7 @@ async function startServer(
const overrides = { const overrides = {
watch: false, watch: false,
host: defaultServerOptions['host'], host: defaultServerOptions['host'],
port: defaultServerOptions['port'], port: options.port ?? defaultServerOptions['port'],
} as JsonObject; } as JsonObject;
message(' Spawning test server ⚙️ ... \n', context); message(' Spawning test server ⚙️ ... \n', context);
@ -138,7 +138,7 @@ async function executeE2ETest(
if (error instanceof Error) { if (error instanceof Error) {
return {success: false, error: error.message}; return {success: false, error: error.message};
} }
return {success: false, error: error as any}; return {success: false, error: error as string};
} finally { } finally {
if (server) { if (server) {
await server.stop(); await server.stop();
@ -146,4 +146,4 @@ async function executeE2ETest(
} }
} }
export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest) as any; export default createBuilder<PuppeteerBuilderOptions>(executeE2ETest);

View File

@ -16,6 +16,10 @@
"devServerTarget": { "devServerTarget": {
"type": "string", "type": "string",
"description": "Angular target that spawns the server." "description": "Angular target that spawns the server."
},
"port": {
"type": ["number", "null"],
"description": "Port to run the test server on."
} }
}, },
"additionalProperties": true "additionalProperties": true

View File

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

View File

@ -10,7 +10,6 @@ describe('App test', function () {
setupBrowserHooks(); setupBrowserHooks();
it('is running', async function () { it('is running', async function () {
const {page} = getBrowserState(); const {page} = getBrowserState();
await page.goto('<%= baseUrl %>');
const element = await page.waitForSelector('text/sandbox app is running!'); const element = await page.waitForSelector('text/sandbox app is running!');
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %> <% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>

View File

@ -3,6 +3,7 @@ import {before, beforeEach, after, afterEach} from 'node:test';
<% } %> <% } %>
import * as puppeteer from 'puppeteer'; import * as puppeteer from 'puppeteer';
const baseUrl = '<%= baseUrl %>';
let browser: puppeteer.Browser; let browser: puppeteer.Browser;
let page: puppeteer.Page; let page: puppeteer.Page;
@ -21,6 +22,7 @@ export function setupBrowserHooks(): void {
beforeEach(async () => { beforeEach(async () => {
page = await browser.newPage(); page = await browser.newPage();
await page.goto(baseUrl);
}); });
afterEach(async () => { afterEach(async () => {
@ -41,6 +43,7 @@ export function setupBrowserHooks(): void {
export function getBrowserState(): { export function getBrowserState(): {
browser: puppeteer.Browser; browser: puppeteer.Browser;
page: puppeteer.Page; page: puppeteer.Page;
baseUrl: string;
} { } {
if (!browser) { if (!browser) {
throw new Error( throw new Error(
@ -50,5 +53,6 @@ export function getBrowserState(): {
return { return {
browser, browser,
page, page,
baseUrl,
}; };
} }

View File

@ -43,6 +43,12 @@
} }
] ]
} }
},
"port": {
"type": ["number"],
"default": 4200,
"alias": "p",
"x-prompt": "On which port to spawn test server on?"
} }
}, },
"required": [] "required": []

View File

@ -10,6 +10,5 @@ describe('<%= classify(name) %>', function () {
setupBrowserHooks(); setupBrowserHooks();
it('', async function () { it('', async function () {
const {page} = getBrowserState(); const {page} = getBrowserState();
await page.goto('<%= baseUrl %>');
}); });
}); });

View File

@ -24,7 +24,12 @@ import {
import {addBaseFiles} from '../utils/files.js'; import {addBaseFiles} from '../utils/files.js';
import {getAngularConfig} from '../utils/json.js'; import {getAngularConfig} from '../utils/json.js';
import {TestingFramework, SchematicsSpec} from '../utils/types.js'; import {
TestingFramework,
SchematicsSpec,
SchematicsOptions,
AngularProject,
} from '../utils/types.js';
// You don't have to export the function as default. You can also have more than one rule // You don't have to export the function as default. You can also have more than one rule
// factory per file. // factory per file.
@ -34,21 +39,25 @@ export function test(options: SchematicsSpec): Rule {
}; };
} }
function findTestingFramework([name, project]: [ function findTestingOption<Property extends keyof SchematicsOptions>(
string, [name, project]: [string, AngularProject | undefined],
any property: Property
]): TestingFramework { ): SchematicsOptions[Property] {
if (!project) {
throw new Error(`Project "${name}" not found.`);
}
const e2e = project.architect?.e2e; const e2e = project.architect?.e2e;
const puppeteer = project.architect?.puppeteer; const puppeteer = project.architect?.puppeteer;
const builder = '@puppeteer/ng-schematics:puppeteer'; const builder = '@puppeteer/ng-schematics:puppeteer';
if (e2e?.builder === builder) { if (e2e?.builder === builder) {
return e2e.options.testingFramework; return e2e.options[property];
} else if (puppeteer?.builder === builder) { } else if (puppeteer?.builder === builder) {
return puppeteer.options.testingFramework; return puppeteer.options[property];
} }
throw new Error(`Can't find TestingFramework info for project ${name}`); throw new Error(`Can't find property "${property}" for project "${name}".`);
} }
function addSpecFile(options: SchematicsSpec): Rule { function addSpecFile(options: SchematicsSpec): Rule {
@ -57,13 +66,13 @@ function addSpecFile(options: SchematicsSpec): Rule {
const {projects} = getAngularConfig(tree); const {projects} = getAngularConfig(tree);
const projectNames = Object.keys(projects) as [string, ...string[]]; const projectNames = Object.keys(projects) as [string, ...string[]];
const foundProject: [string, unknown] | undefined = const foundProject: [string, AngularProject | undefined] | undefined =
projectNames.length === 1 projectNames.length === 1
? [projectNames[0], projects[projectNames[0]]] ? [projectNames[0], projects[projectNames[0]]]
: Object.entries(projects).find(([name, project]) => { : Object.entries(projects).find(([name, project]) => {
return options.project return options.project
? options.project === name ? options.project === name
: (project as any).root === ''; : project.root === '';
}); });
if (!foundProject) { if (!foundProject) {
throw new SchematicsException( throw new SchematicsException(
@ -71,7 +80,11 @@ function addSpecFile(options: SchematicsSpec): Rule {
); );
} }
const testingFramework = findTestingFramework(foundProject); const testingFramework = findTestingOption(
foundProject,
'testingFramework'
);
const port = findTestingOption(foundProject, 'port');
context.logger.debug('Creating Spec file.'); context.logger.debug('Creating Spec file.');
@ -83,6 +96,7 @@ function addSpecFile(options: SchematicsSpec): Rule {
// Node test runner does not support glob patterns // Node test runner does not support glob patterns
// It looks for files `*.test.js` // It looks for files `*.test.js`
ext: testingFramework === TestingFramework.Node ? 'test' : 'e2e', ext: testingFramework === TestingFramework.Node ? 'test' : 'e2e',
port,
}, },
}); });
}; };

View File

@ -35,6 +35,7 @@ export interface FilesOptions {
projects: Record<string, any>; projects: Record<string, any>;
options: { options: {
testingFramework: TestingFramework; testingFramework: TestingFramework;
port: number;
name?: string; name?: string;
exportConfig?: boolean; exportConfig?: boolean;
ext?: string; ext?: string;
@ -70,7 +71,7 @@ export function addFiles(
workspacePath workspacePath
); );
const baseUrl = getProjectBaseUrl(project); const baseUrl = getProjectBaseUrl(project, options.port);
return mergeWith( return mergeWith(
apply(url(applyPath), [ apply(url(applyPath), [
@ -95,13 +96,13 @@ export function addFiles(
)(tree, context); )(tree, context);
} }
function getProjectBaseUrl(project: any): string { function getProjectBaseUrl(project: any, port: number): string {
let options = {protocol: 'http', port: 4200, host: 'localhost'}; let options = {protocol: 'http', port, host: 'localhost'};
if (project.architect?.serve?.options) { if (project.architect?.serve?.options) {
const projectOptions = project.architect?.serve?.options; const projectOptions = project.architect?.serve?.options;
const projectPort = port !== 4200 ? port : projectOptions?.port ?? port;
options = {...options, ...projectOptions}; options = {...options, ...projectOptions, port: projectPort};
options.protocol = projectOptions.ssl ? 'https' : 'http'; options.protocol = projectOptions.ssl ? 'https' : 'http';
} }

View File

@ -16,6 +16,8 @@
import {SchematicsException, Tree} from '@angular-devkit/schematics'; import {SchematicsException, Tree} from '@angular-devkit/schematics';
import {AngularJson} from './types.js';
export function getJsonFileAsObject( export function getJsonFileAsObject(
tree: Tree, tree: Tree,
path: string path: string
@ -33,6 +35,6 @@ export function getObjectAsJson(object: Record<string, any>): string {
return JSON.stringify(object, null, 2); return JSON.stringify(object, null, 2);
} }
export function getAngularConfig(tree: Tree): Record<string, any> { export function getAngularConfig(tree: Tree): AngularJson {
return getJsonFileAsObject(tree, './angular.json'); return getJsonFileAsObject(tree, './angular.json') as AngularJson;
} }

View File

@ -170,6 +170,7 @@ export function updateAngularJsonScripts(
const angularJson = getAngularConfig(tree); const angularJson = getAngularConfig(tree);
const commands = getScriptFromOptions(options); const commands = getScriptFromOptions(options);
const name = getNgCommandName(options); const name = getNgCommandName(options);
const port = options.port !== 4200 ? Number(options.port) : undefined;
Object.keys(angularJson['projects']).forEach(project => { Object.keys(angularJson['projects']).forEach(project => {
const e2eScript = [ const e2eScript = [
@ -181,6 +182,7 @@ export function updateAngularJsonScripts(
commands, commands,
devServerTarget: `${project}:serve`, devServerTarget: `${project}:serve`,
testingFramework: options.testingFramework, testingFramework: options.testingFramework,
port,
}, },
configurations: { configurations: {
production: { production: {
@ -192,7 +194,7 @@ export function updateAngularJsonScripts(
]; ];
updateJsonValues( updateJsonValues(
angularJson['projects'][project], angularJson['projects'][project]!,
'architect', 'architect',
e2eScript, e2eScript,
overwrite overwrite

View File

@ -25,6 +25,22 @@ export interface SchematicsOptions {
isDefaultTester: boolean; isDefaultTester: boolean;
exportConfig: boolean; exportConfig: boolean;
testingFramework: TestingFramework; testingFramework: TestingFramework;
port: number;
}
export interface PuppeteerSchematicsConfig {
builder: string;
options: SchematicsOptions;
}
export interface AngularProject {
root: string;
architect: {
e2e?: PuppeteerSchematicsConfig;
puppeteer?: PuppeteerSchematicsConfig;
};
}
export interface AngularJson {
projects: Record<string, AngularProject>;
} }
export interface SchematicsSpec { export interface SchematicsSpec {

View File

@ -121,4 +121,20 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
['node', '--test', 'e2e/build/'], ['node', '--test', 'e2e/build/'],
]); ]);
}); });
it('should not create port option', async () => {
const tree = await buildTestingTree('ng-add');
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBeUndefined();
});
it('should create port option when specified', async () => {
const port = 8080;
const tree = await buildTestingTree('ng-add', {
port,
});
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBe(port);
});
}); });

View File

@ -21,6 +21,7 @@ const {cwd} = require('process');
const isInit = process.argv.indexOf('--init') !== -1; const isInit = process.argv.indexOf('--init') !== -1;
const isBuild = process.argv.indexOf('--build') !== -1; const isBuild = process.argv.indexOf('--build') !== -1;
const isTest = process.argv.indexOf('--test') !== -1;
const commands = { const commands = {
build: ['npm run build'], build: ['npm run build'],
createSandbox: ['npx ng new sandbox --defaults'], createSandbox: ['npx ng new sandbox --defaults'],
@ -32,6 +33,14 @@ const commands = {
}, },
}, },
], ],
runSchematicsTest: [
{
command: 'npm run schematics:test',
options: {
cwd: join(cwd(), '/sandbox/'),
},
},
],
}; };
const scripts = { const scripts = {
// Builds the ng-schematics before running them // Builds the ng-schematics before running them
@ -100,7 +109,9 @@ async function main() {
if (isBuild) { if (isBuild) {
await executeCommand(commands.build); await executeCommand(commands.build);
} }
await executeCommand(commands.runSchematics); await executeCommand(
isTest ? commands.runSchematicsTest : commands.runSchematics
);
} }
} }