diff --git a/docs/generated/packages/angular/generators/application.json b/docs/generated/packages/angular/generators/application.json index e9fc7ebb95c75..54d91e2ae48c6 100644 --- a/docs/generated/packages/angular/generators/application.json +++ b/docs/generated/packages/angular/generators/application.json @@ -102,8 +102,9 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], "description": "Test runner to use for end to end (E2E) tests.", + "x-prompt": "Which E2E test runner would you like to use?", "default": "cypress" }, "tags": { diff --git a/docs/generated/packages/angular/generators/host.json b/docs/generated/packages/angular/generators/host.json index c22fff90516d1..ac8cc9d76cf7a 100644 --- a/docs/generated/packages/angular/generators/host.json +++ b/docs/generated/packages/angular/generators/host.json @@ -109,8 +109,9 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], "description": "Test runner to use for end to end (E2E) tests.", + "x-prompt": "Which E2E test runner would you like to use?", "default": "cypress" }, "tags": { diff --git a/docs/generated/packages/angular/generators/init.json b/docs/generated/packages/angular/generators/init.json index bd4e1ac196393..31d6b5d39b657 100644 --- a/docs/generated/packages/angular/generators/init.json +++ b/docs/generated/packages/angular/generators/init.json @@ -24,7 +24,8 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], + "x-prompt": "Which E2E test runner would you like to use?", "description": "Test runner to use for end to end (e2e) tests.", "default": "cypress", "x-priority": "important" diff --git a/docs/generated/packages/angular/generators/remote.json b/docs/generated/packages/angular/generators/remote.json index e94bd1b698147..1c56c9cc45af4 100644 --- a/docs/generated/packages/angular/generators/remote.json +++ b/docs/generated/packages/angular/generators/remote.json @@ -103,8 +103,9 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], "description": "Test runner to use for end to end (E2E) tests.", + "x-prompt": "Which E2E test runner would you like to use?", "default": "cypress" }, "tags": { diff --git a/e2e/angular-core/src/projects.test.ts b/e2e/angular-core/src/projects.test.ts index 3a42a5dc736ab..b734a729c9f3c 100644 --- a/e2e/angular-core/src/projects.test.ts +++ b/e2e/angular-core/src/projects.test.ts @@ -2,6 +2,7 @@ import { names } from '@nx/devkit'; import { checkFilesExist, cleanupProject, + ensurePlaywrightBrowsersInstallation, getSize, killPorts, killProcessAndPorts, @@ -123,6 +124,23 @@ describe('Angular Projects', () => { await killProcessAndPorts(esbProcess.pid, appPort); }, 1000000); + it.only('should successfully work with playwright for e2e tests', async () => { + const app = uniq('app'); + + runCLI( + `generate @nx/angular:app ${app} --e2eTestRunner=playwright --no-interactive` + ); + + if (runCypressTests()) { + ensurePlaywrightBrowsersInstallation(); + const e2eResults = runCLI(`e2e ${app}-e2e --no-watch`); + expect(e2eResults).toContain( + `Successfully ran target e2e for project ${app}-e2e` + ); + expect(await killPorts()).toBeTruthy(); + } + }); + it('should lint correctly with eslint and handle external HTML files and inline templates', async () => { // check apps and lib pass linting for initial generated code runCLI(`run-many --target lint --projects=${app1},${lib1} --parallel`); diff --git a/packages/angular/.eslintrc.json b/packages/angular/.eslintrc.json index f9eb90627c72b..bd0857693476b 100644 --- a/packages/angular/.eslintrc.json +++ b/packages/angular/.eslintrc.json @@ -49,6 +49,7 @@ "semver", // These are installed by ensurePackage so missing in package.json "@nx/cypress", + "@nx/playwright", "@nx/jest", "@nx/rollup", "@nx/storybook", diff --git a/packages/angular/package.json b/packages/angular/package.json index f08a5ca817c85..e11e7a72fe8cb 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -57,7 +57,6 @@ "webpack": "^5.80.0", "webpack-merge": "^5.8.0", "enquirer": "^2.3.6", - "@nx/cypress": "file:../cypress", "@nx/devkit": "file:../devkit", "@nx/jest": "file:../jest", "@nx/js": "file:../js", diff --git a/packages/angular/project.json b/packages/angular/project.json index 558b5af6e273c..d9dac4070d8b7 100644 --- a/packages/angular/project.json +++ b/packages/angular/project.json @@ -74,5 +74,5 @@ }, "lint": {} }, - "implicitDependencies": ["workspace", "cypress", "jest"] + "implicitDependencies": ["workspace", "playwright", "cypress", "jest"] } diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index aa9de783ae8dd..6a43831d2833c 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -26,6 +26,13 @@ import type { Schema } from './schema'; // which is v9 while we are testing for the new v10 version jest.mock('@nx/cypress/src/utils/cypress-version'); jest.mock('enquirer'); +jest.mock('@nx/devkit', () => { + const original = jest.requireActual('@nx/devkit'); + return { + ...original, + ensurePackage: (pkg: string) => jest.requireActual(pkg), + }; +}); describe('app', () => { let appTree: Tree; @@ -128,6 +135,23 @@ describe('app', () => { expect(tsconfigE2E).toMatchSnapshot('e2e tsconfig.json'); }); + it('should setup playwright', async () => { + await generateApp(appTree, 'playwright-app', { + e2eTestRunner: E2eTestRunner.Playwright, + }); + + expect( + appTree.exists('apps/playwright-app-e2e/playwright.config.ts') + ).toBeTruthy(); + expect( + appTree.exists('apps/playwright-app-e2e/src/example.spec.ts') + ).toBeTruthy(); + expect( + readProjectConfiguration(appTree, 'playwright-app-e2e')?.targets?.e2e + ?.executor + ).toEqual('@nx/playwright:playwright'); + }); + it('should setup jest with serializers', async () => { await generateApp(appTree); @@ -869,6 +893,18 @@ describe('app', () => { const project = readProjectConfiguration(appTree, 'my-app'); expect(project.targets.build.options['outputPath']).toBe('dist/my-app'); }); + + it('should generate playwright with root project', async () => { + await generateApp(appTree, 'root-app', { + e2eTestRunner: E2eTestRunner.Playwright, + rootProject: true, + }); + expect( + readProjectConfiguration(appTree, 'e2e').targets.e2e.executor + ).toEqual('@nx/playwright:playwright'); + expect(appTree.exists('e2e/playwright.config.ts')).toBeTruthy(); + expect(appTree.exists('e2e/src/example.spec.ts')).toBeTruthy(); + }); }); it('should error correctly when Angular version does not support standalone', async () => { diff --git a/packages/angular/src/generators/application/lib/add-e2e.ts b/packages/angular/src/generators/application/lib/add-e2e.ts index 7e60957d8eb54..51eb32037e1f3 100644 --- a/packages/angular/src/generators/application/lib/add-e2e.ts +++ b/packages/angular/src/generators/application/lib/add-e2e.ts @@ -1,7 +1,10 @@ -import { cypressProjectGenerator } from '@nx/cypress'; import type { Tree } from '@nx/devkit'; import { addDependenciesToPackageJson, + addProjectConfiguration, + ensurePackage, + getPackageManagerCommand, + joinPathFragments, readProjectConfiguration, updateProjectConfiguration, } from '@nx/devkit'; @@ -13,6 +16,9 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) { removeScaffoldedE2e(tree, options, options.ngCliSchematicE2ERoot); if (options.e2eTestRunner === 'cypress') { + const { cypressProjectGenerator } = ensurePackage< + typeof import('@nx/cypress') + >('@nx/cypress', nxVersion); // TODO: This can call `@nx/web:static-config` generator when ready addFileServerTarget(tree, options, 'serve-static'); @@ -25,6 +31,31 @@ export async function addE2e(tree: Tree, options: NormalizedSchema) { skipPackageJson: options.skipPackageJson, skipFormat: true, }); + } else if (options.e2eTestRunner === 'playwright') { + const { configurationGenerator: playwrightConfigurationGenerator } = + ensurePackage( + '@nx/playwright', + nxVersion + ); + addProjectConfiguration(tree, options.e2eProjectName, { + root: options.e2eProjectRoot, + sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'), + targets: {}, + implicitDependencies: [options.name], + }); + await playwrightConfigurationGenerator(tree, { + project: options.e2eProjectName, + skipFormat: true, + skipPackageJson: options.skipPackageJson, + directory: 'src', + js: false, + linter: options.linter, + setParserOptionsProject: options.setParserOptionsProject, + webServerCommand: `${getPackageManagerCommand().exec} nx serve ${ + options.name + }`, + webServerAddress: `http://localhost:${options.port ?? 4200}`, + }); } } diff --git a/packages/angular/src/generators/application/schema.json b/packages/angular/src/generators/application/schema.json index 0d287467aa3c5..d92bf5021f006 100644 --- a/packages/angular/src/generators/application/schema.json +++ b/packages/angular/src/generators/application/schema.json @@ -105,8 +105,9 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], "description": "Test runner to use for end to end (E2E) tests.", + "x-prompt": "Which E2E test runner would you like to use?", "default": "cypress" }, "tags": { diff --git a/packages/angular/src/generators/host/schema.json b/packages/angular/src/generators/host/schema.json index c1d21991c0c34..b37009f314a2d 100644 --- a/packages/angular/src/generators/host/schema.json +++ b/packages/angular/src/generators/host/schema.json @@ -112,8 +112,9 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], "description": "Test runner to use for end to end (E2E) tests.", + "x-prompt": "Which E2E test runner would you like to use?", "default": "cypress" }, "tags": { diff --git a/packages/angular/src/generators/init/init.spec.ts b/packages/angular/src/generators/init/init.spec.ts index 7c4906b4cdcfc..dfc3ea4a2a90e 100644 --- a/packages/angular/src/generators/init/init.spec.ts +++ b/packages/angular/src/generators/init/init.spec.ts @@ -2,9 +2,13 @@ jest.mock('@nx/devkit', () => ({ ...jest.requireActual('@nx/devkit'), // need to mock so it doesn't resolve what the workspace has installed // and be able to test with different versions - ensurePackage: jest.fn(), + ensurePackage: jest.fn(() => ({ + cypressInitGenerator: jest.fn(), + initGenerator: jest.fn(), + })), })); import { + ensurePackage, NxJsonConfiguration, readJson, readNxJson, @@ -171,21 +175,50 @@ describe('init', () => { }); describe('--e2e-test-runner', () => { - describe('cypress', () => { - it('should add cypress dependencies', async () => { + describe('playwright', () => { + it('should call @nx/playwright:init', async () => { // ACT await init(tree, { unitTestRunner: UnitTestRunner.None, - e2eTestRunner: E2eTestRunner.Cypress, + e2eTestRunner: E2eTestRunner.Playwright, linter: Linter.EsLint, skipFormat: false, }); - const { devDependencies } = readJson(tree, 'package.json'); + expect(ensurePackage).toHaveBeenLastCalledWith( + '@nx/playwright', + '0.0.1' + ); + }); + + it('should set defaults', async () => { + // ACT + await init(tree, { + unitTestRunner: UnitTestRunner.None, + e2eTestRunner: E2eTestRunner.Playwright, + linter: Linter.EsLint, + skipFormat: false, + }); + + const { generators } = readJson(tree, 'nx.json'); // ASSERT - expect(devDependencies['@nx/cypress']).toBeDefined(); - expect(devDependencies['cypress']).toBeDefined(); + expect(generators['@nx/angular:application'].e2eTestRunner).toEqual( + 'playwright' + ); + }); + }); + describe('cypress', () => { + it('should call @nx/cypress:init', async () => { + // ACT + await init(tree, { + unitTestRunner: UnitTestRunner.None, + e2eTestRunner: E2eTestRunner.Cypress, + linter: Linter.EsLint, + skipFormat: false, + }); + + expect(ensurePackage).toHaveBeenLastCalledWith('@nx/cypress', '0.0.1'); }); it('should set defaults', async () => { @@ -523,6 +556,38 @@ bar }); describe('--e2e-test-runner', () => { + it('should call @nx/playwright:init', async () => { + // ACT + await init(tree, { + unitTestRunner: UnitTestRunner.None, + e2eTestRunner: E2eTestRunner.Playwright, + linter: Linter.EsLint, + skipFormat: false, + }); + + expect(ensurePackage).toHaveBeenLastCalledWith( + '@nx/playwright', + '0.0.1' + ); + }); + + it('should set defaults', async () => { + // ACT + await init(tree, { + unitTestRunner: UnitTestRunner.None, + e2eTestRunner: E2eTestRunner.Playwright, + linter: Linter.EsLint, + skipFormat: false, + }); + + const { generators } = readJson(tree, 'nx.json'); + + // ASSERT + expect(generators['@nx/angular:application'].e2eTestRunner).toEqual( + 'playwright' + ); + }); + describe('cypress', () => { it('should add cypress dependencies', async () => { // ACT @@ -532,12 +597,11 @@ bar linter: Linter.EsLint, skipFormat: false, }); - - const { devDependencies } = readJson(tree, 'package.json'); - // ASSERT - expect(devDependencies['@nx/cypress']).toBeDefined(); - expect(devDependencies['cypress']).toBeDefined(); + expect(ensurePackage).toHaveBeenLastCalledWith( + '@nx/cypress', + '0.0.1' + ); }); it('should set defaults', async () => { diff --git a/packages/angular/src/generators/init/init.ts b/packages/angular/src/generators/init/init.ts index f89ae4219c7c1..8747a85dbaf4b 100755 --- a/packages/angular/src/generators/init/init.ts +++ b/packages/angular/src/generators/init/init.ts @@ -1,4 +1,3 @@ -import { cypressInitGenerator } from '@nx/cypress'; import { addDependenciesToPackageJson, ensurePackage, @@ -21,6 +20,7 @@ import { } from '../utils/version-utils'; import type { PackageVersions } from '../../utils/backward-compatible-versions'; import { Schema } from './schema'; +import { nxVersion } from '../../utils/versions'; export async function angularInitGenerator( tree: Tree, @@ -198,9 +198,20 @@ async function addE2ETestRunner( ): Promise { switch (options.e2eTestRunner) { case E2eTestRunner.Cypress: + const { cypressInitGenerator } = ensurePackage< + typeof import('@nx/cypress') + >('@nx/cypress', nxVersion); return cypressInitGenerator(tree, { skipPackageJson: options.skipPackageJson, }); + case E2eTestRunner.Playwright: + const { initGenerator: playwrightInitGenerator } = ensurePackage< + typeof import('@nx/playwright') + >('@nx/playwright', nxVersion); + return playwrightInitGenerator(tree, { + skipFormat: true, + skipPackageJson: options.skipPackageJson, + }); default: return () => {}; } diff --git a/packages/angular/src/generators/init/schema.json b/packages/angular/src/generators/init/schema.json index 4d3a13fdb4bdb..34a1dde95e259 100644 --- a/packages/angular/src/generators/init/schema.json +++ b/packages/angular/src/generators/init/schema.json @@ -21,7 +21,8 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], + "x-prompt": "Which E2E test runner would you like to use?", "description": "Test runner to use for end to end (e2e) tests.", "default": "cypress", "x-priority": "important" diff --git a/packages/angular/src/generators/remote/schema.json b/packages/angular/src/generators/remote/schema.json index 42e2532ecd112..9c72063146039 100644 --- a/packages/angular/src/generators/remote/schema.json +++ b/packages/angular/src/generators/remote/schema.json @@ -106,8 +106,9 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["cypress", "none"], + "enum": ["cypress", "playwright", "none"], "description": "Test runner to use for end to end (E2E) tests.", + "x-prompt": "Which E2E test runner would you like to use?", "default": "cypress" }, "tags": { diff --git a/packages/angular/src/utils/test-runners.ts b/packages/angular/src/utils/test-runners.ts index f0950951f3ef1..642a233683ef5 100644 --- a/packages/angular/src/utils/test-runners.ts +++ b/packages/angular/src/utils/test-runners.ts @@ -5,5 +5,6 @@ export enum UnitTestRunner { export enum E2eTestRunner { Cypress = 'cypress', + Playwright = 'playwright', None = 'none', }