diff --git a/packages/ng-schematics/src/builders/puppeteer/index.ts b/packages/ng-schematics/src/builders/puppeteer/index.ts index 45aec951523..a85e997c6bb 100644 --- a/packages/ng-schematics/src/builders/puppeteer/index.ts +++ b/packages/ng-schematics/src/builders/puppeteer/index.ts @@ -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}; } diff --git a/packages/ng-schematics/src/schematics/collection.json b/packages/ng-schematics/src/schematics/collection.json index 0cf38b799b6..ca3b2997364 100644 --- a/packages/ng-schematics/src/schematics/collection.json +++ b/packages/ng-schematics/src/schematics/collection.json @@ -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" } } } diff --git a/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.__ext@dasherize__.ts.template b/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.__ext@dasherize__.ts.template index edf7f7b083c..6ead9dac252 100644 --- a/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.__ext@dasherize__.ts.template +++ b/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/app.__ext@dasherize__.ts.template @@ -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') { %> diff --git a/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/utils.ts.template b/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/utils.ts.template index e9bb3eea35b..2ba5264fc90 100644 --- a/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/utils.ts.template +++ b/packages/ng-schematics/src/schematics/ng-add/files/base/e2e/tests/utils.ts.template @@ -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 () => { diff --git a/packages/ng-schematics/src/schematics/ng-add/schema.json b/packages/ng-schematics/src/schematics/ng-add/schema.json index 77cb5bdd480..971053ddb06 100644 --- a/packages/ng-schematics/src/schematics/ng-add/schema.json +++ b/packages/ng-schematics/src/schematics/ng-add/schema.json @@ -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", diff --git a/packages/ng-schematics/src/schematics/test/files/base/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template b/packages/ng-schematics/src/schematics/test/files/base/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template new file mode 100644 index 00000000000..183f93726f2 --- /dev/null +++ b/packages/ng-schematics/src/schematics/test/files/base/e2e/tests/__name@dasherize__.__ext@dasherize__.ts.template @@ -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 %>'); + }); +}); diff --git a/packages/ng-schematics/src/schematics/test/index.ts b/packages/ng-schematics/src/schematics/test/index.ts new file mode 100644 index 00000000000..dfff018ed66 --- /dev/null +++ b/packages/ng-schematics/src/schematics/test/index.ts @@ -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', + }, + }); + }; +} diff --git a/packages/ng-schematics/src/schematics/test/schema.json b/packages/ng-schematics/src/schematics/test/schema.json new file mode 100644 index 00000000000..d637db707b4 --- /dev/null +++ b/packages/ng-schematics/src/schematics/test/schema.json @@ -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": [] +} diff --git a/packages/ng-schematics/src/schematics/utils/files.ts b/packages/ng-schematics/src/schematics/utils/files.ts index 3230c410289..df463c76222 100644 --- a/packages/ng-schematics/src/schematics/utils/files.ts +++ b/packages/ng-schematics/src/schematics/utils/files.ts @@ -32,9 +32,10 @@ import { import {SchematicsOptions, TestingFramework} from './types.js'; export interface FilesOptions { - projects: any; + projects: Record; 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/'], ]; } } diff --git a/packages/ng-schematics/src/schematics/utils/packages.ts b/packages/ng-schematics/src/schematics/utils/packages.ts index b3724be90fc..be4d43ec535 100644 --- a/packages/ng-schematics/src/schematics/utils/packages.ts +++ b/packages/ng-schematics/src/schematics/utils/packages.ts @@ -180,6 +180,7 @@ export function updateAngularJsonScripts( options: { commands, devServerTarget: `${project}:serve`, + testingFramework: options.testingFramework, }, configurations: { production: { diff --git a/packages/ng-schematics/src/schematics/utils/types.ts b/packages/ng-schematics/src/schematics/utils/types.ts index c59f90e69da..0873a1e2d40 100644 --- a/packages/ng-schematics/src/schematics/utils/types.ts +++ b/packages/ng-schematics/src/schematics/utils/types.ts @@ -26,3 +26,8 @@ export interface SchematicsOptions { exportConfig: boolean; testingFramework: TestingFramework; } + +export interface SchematicsSpec { + name: string; + project?: string; +} diff --git a/packages/ng-schematics/test/src/ng-add.spec.ts b/packages/ng-schematics/test/src/ng-add.spec.ts index d6bf1dabb12..2abd2d559d6 100644 --- a/packages/ng-schematics/test/src/ng-add.spec.ts +++ b/packages/ng-schematics/test/src/ng-add.spec.ts @@ -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/'], ]); }); }); diff --git a/packages/ng-schematics/test/src/test.spec.ts b/packages/ng-schematics/test/src/test.spec.ts new file mode 100644 index 00000000000..f3e1a317d03 --- /dev/null +++ b/packages/ng-schematics/test/src/test.spec.ts @@ -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')); + }); +}); diff --git a/packages/ng-schematics/test/src/utils.ts b/packages/ng-schematics/test/src/utils.ts index 5349a5c930b..03a82ab8d44 100644 --- a/packages/ng-schematics/test/src/utils.ts +++ b/packages/ng-schematics/test/src/utils.ts @@ -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 ): Promise { 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); } diff --git a/packages/ng-schematics/tools/sandbox.js b/packages/ng-schematics/tools/sandbox.js index 44f131bfacf..96f918cc5fc 100644 --- a/packages/ng-schematics/tools/sandbox.js +++ b/packages/ng-schematics/tools/sandbox.js @@ -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); });