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.
## Usage
## Getting started
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._
```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:
- **Jasmine** [https://jasmine.github.io/]
- **Jest** [https://jestjs.io/]
- **Mocha** [https://mochajs.org/]
- **Node Test Runner** _(Experimental)_ [https://nodejs.org/api/test.html]
- [**Jasmine**](https://jasmine.github.io/)
- [**Jest**](https://jestjs.io/)
- [**Mocha**](https://mochajs.org/)
- [**Node Test Runner** _(Experimental)_](https://nodejs.org/api/test.html)
With the schematics installed you can run E2E tests:
@ -26,9 +27,7 @@ With the schematics installed you can run E2E tests:
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:
@ -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` |
| `--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` |
| `--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
@ -58,6 +92,12 @@ npm run sandbox
npm run sandbox -- --build
```
To run the creating of single test schematic:
```bash
npm run sandbox:test
```
### 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

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

View File

@ -105,7 +105,7 @@ async function startServer(
const overrides = {
watch: false,
host: defaultServerOptions['host'],
port: defaultServerOptions['port'],
port: options.port ?? defaultServerOptions['port'],
} as JsonObject;
message(' Spawning test server ⚙️ ... \n', context);
@ -138,7 +138,7 @@ async function executeE2ETest(
if (error instanceof Error) {
return {success: false, error: error.message};
}
return {success: false, error: error as any};
return {success: false, error: error as string};
} finally {
if (server) {
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": {
"type": "string",
"description": "Angular target that spawns the server."
},
"port": {
"type": ["number", "null"],
"description": "Port to run the test server on."
}
},
"additionalProperties": true

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,12 @@ import {
import {addBaseFiles} from '../utils/files.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
// factory per file.
@ -34,21 +39,25 @@ export function test(options: SchematicsSpec): Rule {
};
}
function findTestingFramework([name, project]: [
string,
any
]): TestingFramework {
function findTestingOption<Property extends keyof SchematicsOptions>(
[name, project]: [string, AngularProject | undefined],
property: Property
): SchematicsOptions[Property] {
if (!project) {
throw new Error(`Project "${name}" not found.`);
}
const e2e = project.architect?.e2e;
const puppeteer = project.architect?.puppeteer;
const builder = '@puppeteer/ng-schematics:puppeteer';
if (e2e?.builder === builder) {
return e2e.options.testingFramework;
return e2e.options[property];
} 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 {
@ -57,13 +66,13 @@ function addSpecFile(options: SchematicsSpec): Rule {
const {projects} = getAngularConfig(tree);
const projectNames = Object.keys(projects) as [string, ...string[]];
const foundProject: [string, unknown] | undefined =
const foundProject: [string, AngularProject | undefined] | undefined =
projectNames.length === 1
? [projectNames[0], projects[projectNames[0]]]
: Object.entries(projects).find(([name, project]) => {
return options.project
? options.project === name
: (project as any).root === '';
: project.root === '';
});
if (!foundProject) {
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.');
@ -83,6 +96,7 @@ function addSpecFile(options: SchematicsSpec): Rule {
// Node test runner does not support glob patterns
// It looks for files `*.test.js`
ext: testingFramework === TestingFramework.Node ? 'test' : 'e2e',
port,
},
});
};

View File

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

View File

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

View File

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

View File

@ -25,6 +25,22 @@ export interface SchematicsOptions {
isDefaultTester: boolean;
exportConfig: boolean;
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 {

View File

@ -121,4 +121,20 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
['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 isBuild = process.argv.indexOf('--build') !== -1;
const isTest = process.argv.indexOf('--test') !== -1;
const commands = {
build: ['npm run build'],
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 = {
// Builds the ng-schematics before running them
@ -100,7 +109,9 @@ async function main() {
if (isBuild) {
await executeCommand(commands.build);
}
await executeCommand(commands.runSchematics);
await executeCommand(
isTest ? commands.runSchematicsTest : commands.runSchematics
);
}
}