feat: add Test command (#10443)

This commit is contained in:
Nikolay Vitkov 2023-06-23 17:23:32 +02:00 committed by GitHub
parent b6a733cdfe
commit 2d8993b45b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 242 additions and 46 deletions

View File

@ -12,8 +12,9 @@ import {JsonObject} from '@angular-devkit/core';
import {PuppeteerBuilderOptions} from './types.js';
const terminalStyles = {
blue: '\u001b[34m',
cyan: '\u001b[36;1m',
green: '\u001b[32m',
red: '\u001b[31m',
bold: '\u001b[1m',
reverse: '\u001b[7m',
clear: '\u001b[0m',
@ -21,8 +22,6 @@ const terminalStyles = {
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!'
@ -76,11 +75,22 @@ async function executeCommand(context: BuilderContext, command: string[]) {
function message(
message: string,
context: BuilderContext,
type: 'info' | 'success' = 'info'
type: 'info' | 'success' | 'error' = 'info'
): void {
const color = type === 'info' ? terminalStyles.blue : terminalStyles.green;
let style: string;
switch (type) {
case 'info':
style = terminalStyles.reverse + terminalStyles.cyan;
break;
case 'success':
style = terminalStyles.reverse + terminalStyles.green;
break;
case 'error':
style = terminalStyles.red;
break;
}
context.logger.info(
`${terminalStyles.bold}${terminalStyles.reverse}${color}${message}${terminalStyles.clear}`
`${terminalStyles.bold}${style}${message}${terminalStyles.clear}`
);
}
@ -98,7 +108,7 @@ async function startServer(
port: defaultServerOptions['port'],
} as JsonObject;
message('Spawning test server...\n', context);
message(' Spawning test server ⚙️ ... \n', context);
const server = await context.scheduleTarget(target, overrides);
const result = await server.result;
if (!result.success) {
@ -116,14 +126,15 @@ async function executeE2ETest(
try {
server = await startServer(options, context);
message('\nRunning tests...\n', context);
message('\n Running tests 🧪 ... \n', context);
for (const command of options.commands) {
await executeCommand(context, command);
}
message('\nTest ran successfully!', context, 'success');
message('\n 🚀 Test ran successfully! 🚀 ', context, 'success');
return {success: true};
} catch (error) {
message('\n 🛑 Test failed! 🛑 ', context, 'error');
if (error instanceof Error) {
return {success: false, error: error.message};
}

View File

@ -5,6 +5,11 @@
"description": "Add Puppeteer to an Angular project",
"factory": "./ng-add/index#ngAdd",
"schema": "./ng-add/schema.json"
},
"test": {
"description": "Create a single test file",
"factory": "./test/index#test",
"schema": "./test/schema.json"
}
}
}

View File

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

View File

@ -9,7 +9,9 @@ let page: puppeteer.Page;
export function setupBrowserHooks(): void {
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
beforeAll(async () => {
browser = await puppeteer.launch();
browser = await puppeteer.launch({
headless: 'new'
});
});
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
before(async () => {

View File

@ -5,22 +5,22 @@
"type": "object",
"properties": {
"isDefaultTester": {
"description": "",
"type": "boolean",
"default": true,
"alias": "d",
"x-prompt": "Use Puppeteer as default `ng e2e` command?"
},
"exportConfig": {
"description": "",
"type": "boolean",
"default": false,
"alias": "c",
"x-prompt": "Export default Puppeteer config file?"
},
"testingFramework": {
"description": "",
"type": "string",
"enum": ["jasmine", "jest", "mocha", "node"],
"default": "jasmine",
"alias": "t",
"x-prompt": {
"message": "With what Testing Library do you wish to integrate?",
"type": "list",

View File

@ -0,0 +1,15 @@
<% if(testingFramework == 'node') { %>
import * as assert from 'assert';
import {describe, it} from 'node:test';
<% } %><% if(testingFramework == 'mocha') { %>
import * as assert from 'assert';
<% } %>
import {setupBrowserHooks, getBrowserState} from './utils';
describe('<%= classify(name) %>', function () {
setupBrowserHooks();
it('', async function () {
const {page} = getBrowserState();
await page.goto('<%= baseUrl %>');
});
});

View File

@ -0,0 +1,89 @@
/**
* Copyright 2023 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 {
chain,
Rule,
SchematicContext,
SchematicsException,
Tree,
} from '@angular-devkit/schematics';
import {addBaseFiles} from '../utils/files.js';
import {getAngularConfig} from '../utils/json.js';
import {TestingFramework, SchematicsSpec} 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.
export function test(options: SchematicsSpec): Rule {
return (tree: Tree, context: SchematicContext) => {
return chain([addSpecFile(options)])(tree, context);
};
}
function findTestingFramework([name, project]: [
string,
any
]): TestingFramework {
const e2e = project.architect?.e2e;
const puppeteer = project.architect?.puppeteer;
const builder = '@puppeteer/ng-schematics:puppeteer';
if (e2e?.builder === builder) {
return e2e.options.testingFramework;
} else if (puppeteer?.builder === builder) {
return puppeteer.options.testingFramework;
}
throw new Error(`Can't find TestingFramework info for project ${name}`);
}
function addSpecFile(options: SchematicsSpec): Rule {
return async (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Spec file.');
const {projects} = getAngularConfig(tree);
const projectNames = Object.keys(projects) as [string, ...string[]];
const foundProject: [string, unknown] | 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 === '';
});
if (!foundProject) {
throw new SchematicsException(
`Project not found! Please use -p to specify in which project to run.`
);
}
const testingFramework = findTestingFramework(foundProject);
context.logger.debug('Creating Spec file.');
return addBaseFiles(tree, context, {
projects: {[foundProject[0]]: foundProject[1]},
options: {
name: options.name,
testingFramework,
// Node test runner does not support glob patterns
// It looks for files `*.test.js`
ext: testingFramework === TestingFramework.Node ? 'test' : 'e2e',
},
});
};
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "Puppeteer",
"title": "Puppeteer Spec Schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"alias": "n",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "Name for spec to be created:"
},
"project": {
"type": "string",
"alias": "p"
}
},
"required": []
}

View File

@ -32,9 +32,10 @@ import {
import {SchematicsOptions, TestingFramework} from './types.js';
export interface FilesOptions {
projects: any;
projects: Record<string, any>;
options: {
testingFramework: TestingFramework;
name?: string;
exportConfig?: boolean;
ext?: string;
};
@ -153,7 +154,7 @@ export function getScriptFromOptions(options: SchematicsOptions): string[][] {
case TestingFramework.Node:
return [
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', 'e2e/'],
['node', '--test', 'e2e/build/'],
];
}
}

View File

@ -180,6 +180,7 @@ export function updateAngularJsonScripts(
options: {
commands,
devServerTarget: `${project}:serve`,
testingFramework: options.testingFramework,
},
configurations: {
production: {

View File

@ -26,3 +26,8 @@ export interface SchematicsOptions {
exportConfig: boolean;
testingFramework: TestingFramework;
}
export interface SchematicsSpec {
name: string;
project?: string;
}

View File

@ -1,32 +1,18 @@
import https from 'https';
import expect from 'expect';
import sinon from 'sinon';
import {
buildTestingTree,
getAngularJsonScripts,
getPackageJson,
getProjectFile,
setupHttpHooks,
} from './utils.js';
describe('@puppeteer/ng-schematics: ng-add', () => {
// Stop outgoing Request for version fetching
before(() => {
const httpsGetStub = sinon.stub(https, 'get');
httpsGetStub.returns({
on: (_: any, callback: () => void) => {
callback();
},
} as any);
});
after(() => {
sinon.restore();
});
setupHttpHooks();
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree();
const tree = await buildTestingTree('ng-add');
const {devDependencies, scripts} = getPackageJson(tree);
const {builder, configurations} = getAngularJsonScripts(tree);
@ -44,7 +30,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should update create proper "ng" command for non default tester', async () => {
const tree = await buildTestingTree({
const tree = await buildTestingTree('ng-add', {
isDefaultTester: false,
});
const {scripts} = getPackageJson(tree);
@ -55,7 +41,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should create Puppeteer config', async () => {
const {files} = await buildTestingTree({
const {files} = await buildTestingTree('ng-add', {
exportConfig: true,
});
@ -63,7 +49,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree({
const {files} = await buildTestingTree('ng-add', {
exportConfig: false,
});
@ -71,7 +57,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree({
const tree = await buildTestingTree('ng-add', {
testingFramework: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
@ -89,7 +75,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree({
const tree = await buildTestingTree('ng-add', {
testingFramework: 'jest',
});
const {devDependencies} = getPackageJson(tree);
@ -103,7 +89,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree({
const tree = await buildTestingTree('ng-add', {
testingFramework: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
@ -121,8 +107,8 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
]);
});
it('should create Node files"', async () => {
const tree = await buildTestingTree({
it('should create Node files', async () => {
const tree = await buildTestingTree('ng-add', {
testingFramework: 'node',
});
const {options} = getAngularJsonScripts(tree);
@ -132,7 +118,7 @@ describe('@puppeteer/ng-schematics: ng-add', () => {
expect(tree.files).toContain(getProjectFile('e2e/tests/app.test.ts'));
expect(options['commands']).toEqual([
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', 'e2e/'],
['node', '--test', 'e2e/build/'],
]);
});
});

View File

@ -0,0 +1,28 @@
import expect from 'expect';
import {buildTestingTree, getProjectFile, setupHttpHooks} from './utils.js';
describe('@puppeteer/ng-schematics: test', () => {
setupHttpHooks();
it('should create default file', async () => {
const tree = await buildTestingTree('test', {
name: 'myTest',
});
expect(tree.files).toContain(getProjectFile('e2e/tests/my-test.e2e.ts'));
expect(tree.files).not.toContain(
getProjectFile('e2e/tests/my-test.test.ts')
);
});
it('should create Node file', async () => {
const tree = await buildTestingTree('test', {
name: 'myTest',
testingFramework: 'node',
});
expect(tree.files).not.toContain(
getProjectFile('e2e/tests/my-test.e2e.ts')
);
expect(tree.files).toContain(getProjectFile('e2e/tests/my-test.test.ts'));
});
});

View File

@ -1,3 +1,4 @@
import https from 'https';
import {join} from 'path';
import {JsonObject} from '@angular-devkit/core';
@ -5,6 +6,7 @@ import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing';
import sinon from 'sinon';
const WORKSPACE_OPTIONS = {
name: 'workspace',
@ -16,6 +18,22 @@ const APPLICATION_OPTIONS = {
name: 'sandbox',
};
export function setupHttpHooks(): void {
// Stop outgoing Request for version fetching
before(() => {
const httpsGetStub = sinon.stub(https, 'get');
httpsGetStub.returns({
on: (_: any, callback: () => void) => {
callback();
},
} as any);
});
after(() => {
sinon.restore();
});
}
export function getProjectFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`;
}
@ -49,6 +67,7 @@ export function getPackageJson(tree: UnitTestTree): {
}
export async function buildTestingTree(
command: 'ng-add' | 'test',
userOptions?: Record<string, any>
): Promise<UnitTestTree> {
const runner = new SchematicTestRunner(
@ -77,5 +96,11 @@ export async function buildTestingTree(
workingTree
);
return await runner.runSchematic('ng-add', options, workingTree);
if (command !== 'ng-add') {
// We want to create update the proper files with `ng-add`
// First else the angular.json will have wrong data
workingTree = await runner.runSchematic('ng-add', options, workingTree);
}
return await runner.runSchematic(command, options, workingTree);
}

View File

@ -34,11 +34,16 @@ const commands = {
],
};
const scripts = {
// Builds the ng-schematics before running them
'build:schematics': 'npm run --prefix ../ build',
// Deletes all files created by Puppeteer Ng-Schematics to avoid errors
'delete:file':
'rm -f .puppeteerrc.cjs && rm -f tsconfig.e2e.json && rm -R -f e2e/',
// Runs the Puppeteer Ng-Schematics against the sandbox
schematics: 'npm run delete:file && schematics ../:ng-add --dry-run=false',
schematics:
'npm run delete:file && npm run build:schematics && schematics ../:ng-add --dry-run=false',
'schematics:spec':
'npm run build:schematics && schematics ../:test --dry-run=false',
};
/**
*
@ -99,6 +104,7 @@ async function main() {
}
}
main().catch(() => {
console.log('\n');
main().catch(error => {
console.log('Something went wrong');
console.error(error);
});