From e78575badc15560278a3cb208f4dbc5afd9017c3 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Tue, 1 Aug 2023 10:51:51 -0400 Subject: [PATCH] feat(nextjs): add playwright as an option for e2e testing (#18281) --- .../packages/next/generators/application.json | 3 +- e2e/next/src/next-appdir.test.ts | 5 +- e2e/next/src/utils.ts | 9 ++-- packages/next/.eslintrc.json | 1 + .../src/generators/application/application.ts | 29 +++++++---- .../generators/application/lib/add-cypress.ts | 23 --------- .../src/generators/application/lib/add-e2e.ts | 51 +++++++++++++++++++ .../src/generators/application/schema.d.ts | 2 +- .../src/generators/application/schema.json | 3 +- packages/next/src/generators/init/init.ts | 14 ++++- packages/next/src/generators/init/schema.d.ts | 2 +- 11 files changed, 96 insertions(+), 46 deletions(-) delete mode 100644 packages/next/src/generators/application/lib/add-cypress.ts create mode 100644 packages/next/src/generators/application/lib/add-e2e.ts diff --git a/docs/generated/packages/next/generators/application.json b/docs/generated/packages/next/generators/application.json index f5531bc6db6ab..594c4d7df9eb6 100644 --- a/docs/generated/packages/next/generators/application.json +++ b/docs/generated/packages/next/generators/application.json @@ -81,8 +81,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/next/src/next-appdir.test.ts b/e2e/next/src/next-appdir.test.ts index bc21eb5f3d641..33b833da65cb1 100644 --- a/e2e/next/src/next-appdir.test.ts +++ b/e2e/next/src/next-appdir.test.ts @@ -1,5 +1,6 @@ import { cleanupProject, + isNotWindows, newProject, runCLI, uniq, @@ -22,7 +23,7 @@ describe('Next.js App Router', () => { const appName = uniq('app'); const jsLib = uniq('tslib'); - runCLI(`generate @nx/next:app ${appName}`); + runCLI(`generate @nx/next:app ${appName} --e2eTestRunner=playwright`); runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`); updateFile( @@ -42,7 +43,7 @@ describe('Next.js App Router', () => { await checkApp(appName, { checkUnitTest: false, checkLint: true, - checkE2E: false, + checkE2E: isNotWindows(), checkExport: false, }); }, 300_000); diff --git a/e2e/next/src/utils.ts b/e2e/next/src/utils.ts index 90307fae2852c..343f6116e8775 100644 --- a/e2e/next/src/utils.ts +++ b/e2e/next/src/utils.ts @@ -1,7 +1,6 @@ -import { execSync } from 'child_process'; import { checkFilesExist, - killPort, + killPorts, readJson, runCLI, runCLIAsync, @@ -43,10 +42,10 @@ export async function checkApp( if (opts.checkE2E && runCypressTests()) { const e2eResults = runCLI( - `e2e ${appName}-e2e --no-watch --configuration=production --port=9000` + `e2e ${appName}-e2e --no-watch --configuration=production` ); - expect(e2eResults).toContain('All specs passed!'); - expect(await killPort(9000)).toBeTruthy(); + expect(e2eResults).toContain('Successfully ran target e2e for project'); + expect(await killPorts()).toBeTruthy(); } if (opts.checkExport) { diff --git a/packages/next/.eslintrc.json b/packages/next/.eslintrc.json index b7fa759b7ce04..163c42aec47b7 100644 --- a/packages/next/.eslintrc.json +++ b/packages/next/.eslintrc.json @@ -82,6 +82,7 @@ "@nx/webpack", "@nx/cypress", "@nx/jest", + "@nx/playwright", "typescript", "react", "webpack", diff --git a/packages/next/src/generators/application/application.ts b/packages/next/src/generators/application/application.ts index 5bb9aad0c79ce..0a1c99f07663b 100644 --- a/packages/next/src/generators/application/application.ts +++ b/packages/next/src/generators/application/application.ts @@ -1,6 +1,7 @@ import { convertNxGenerator, formatFiles, + GeneratorCallback, joinPathFragments, runTasksInSerial, Tree, @@ -8,7 +9,7 @@ import { import { normalizeOptions } from './lib/normalize-options'; import { Schema } from './schema'; -import { addCypress } from './lib/add-cypress'; +import { addE2e } from './lib/add-e2e'; import { addJest } from './lib/add-jest'; import { addProject } from './lib/add-project'; import { createApplicationFiles } from './lib/create-application-files'; @@ -22,6 +23,7 @@ import { updateCypressTsConfig } from './lib/update-cypress-tsconfig'; import { showPossibleWarnings } from './lib/show-possible-warnings'; export async function applicationGenerator(host: Tree, schema: Schema) { + const tasks: GeneratorCallback[] = []; const options = normalizeOptions(host, schema); showPossibleWarnings(host, options); @@ -30,17 +32,28 @@ export async function applicationGenerator(host: Tree, schema: Schema) { ...options, skipFormat: true, }); + tasks.push(nextTask); + createApplicationFiles(host, options); addProject(host, options); - const cypressTask = await addCypress(host, options); + + const e2eTask = await addE2e(host, options); + tasks.push(e2eTask); + const jestTask = await addJest(host, options); + tasks.push(jestTask); + const lintTask = await addLinting(host, options); - updateJestConfig(host, options); - updateCypressTsConfig(host, options); + tasks.push(lintTask); + const styledTask = addStyleDependencies(host, { style: options.style, swc: !host.exists(joinPathFragments(options.appProjectRoot, '.babelrc')), }); + tasks.push(styledTask); + + updateJestConfig(host, options); + updateCypressTsConfig(host, options); setDefaults(host, options); if (options.customServer) { @@ -54,13 +67,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) { await formatFiles(host); } - return runTasksInSerial( - nextTask, - cypressTask, - jestTask, - lintTask, - styledTask - ); + return runTasksInSerial(...tasks); } export const applicationSchematic = convertNxGenerator(applicationGenerator); diff --git a/packages/next/src/generators/application/lib/add-cypress.ts b/packages/next/src/generators/application/lib/add-cypress.ts deleted file mode 100644 index fea217778dea6..0000000000000 --- a/packages/next/src/generators/application/lib/add-cypress.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ensurePackage, Tree } from '@nx/devkit'; -import { Linter } from '@nx/linter'; - -import { nxVersion } from '../../../utils/versions'; -import { NormalizedSchema } from './normalize-options'; - -export async function addCypress(host: Tree, options: NormalizedSchema) { - if (options.e2eTestRunner !== 'cypress') { - return () => {}; - } - - const { cypressProjectGenerator } = ensurePackage< - typeof import('@nx/cypress') - >('@nx/cypress', nxVersion); - return cypressProjectGenerator(host, { - ...options, - linter: Linter.EsLint, - name: options.e2eProjectName, - directory: options.directory, - project: options.projectName, - skipFormat: true, - }); -} diff --git a/packages/next/src/generators/application/lib/add-e2e.ts b/packages/next/src/generators/application/lib/add-e2e.ts new file mode 100644 index 0000000000000..dc12788f8b468 --- /dev/null +++ b/packages/next/src/generators/application/lib/add-e2e.ts @@ -0,0 +1,51 @@ +import { + addProjectConfiguration, + ensurePackage, + getPackageManagerCommand, + joinPathFragments, + Tree, +} from '@nx/devkit'; +import { Linter } from '@nx/linter'; + +import { nxVersion } from '../../../utils/versions'; +import { NormalizedSchema } from './normalize-options'; + +export async function addE2e(host: Tree, options: NormalizedSchema) { + if (options.e2eTestRunner === 'cypress') { + const { cypressProjectGenerator } = ensurePackage< + typeof import('@nx/cypress') + >('@nx/cypress', nxVersion); + return cypressProjectGenerator(host, { + ...options, + linter: Linter.EsLint, + name: options.e2eProjectName, + directory: options.directory, + project: options.projectName, + skipFormat: true, + }); + } else if (options.e2eTestRunner === 'playwright') { + const { configurationGenerator } = ensurePackage< + typeof import('@nx/playwright') + >('@nx/playwright', nxVersion); + addProjectConfiguration(host, options.e2eProjectName, { + root: options.e2eProjectRoot, + sourceRoot: joinPathFragments(options.e2eProjectRoot, ''), + targets: {}, + implicitDependencies: [options.projectName], + }); + return configurationGenerator(host, { + project: options.e2eProjectName, + skipFormat: true, + skipPackageJson: options.skipPackageJson, + directory: 'src', + js: false, + linter: options.linter, + setParserOptionsProject: options.setParserOptionsProject, + webServerAddress: 'http://127.0.0.1:4200', + webServerCommand: `${getPackageManagerCommand().exec} nx serve ${ + options.name + }`, + }); + } + return () => {}; +} diff --git a/packages/next/src/generators/application/schema.d.ts b/packages/next/src/generators/application/schema.d.ts index fc9ff0fdbb309..f45cb9e1554b7 100644 --- a/packages/next/src/generators/application/schema.d.ts +++ b/packages/next/src/generators/application/schema.d.ts @@ -8,7 +8,7 @@ export interface Schema { directory?: string; tags?: string; unitTestRunner?: 'jest' | 'none'; - e2eTestRunner?: 'cypress' | 'none'; + e2eTestRunner?: 'cypress' | 'playwright' | 'none'; linter?: Linter; js?: boolean; setParserOptionsProject?: boolean; diff --git a/packages/next/src/generators/application/schema.json b/packages/next/src/generators/application/schema.json index 469b72598a9dc..fc9ddf32a0170 100644 --- a/packages/next/src/generators/application/schema.json +++ b/packages/next/src/generators/application/schema.json @@ -84,8 +84,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/next/src/generators/init/init.ts b/packages/next/src/generators/init/init.ts index 777d809465714..ed69a3ffc4430 100644 --- a/packages/next/src/generators/init/init.ts +++ b/packages/next/src/generators/init/init.ts @@ -54,14 +54,26 @@ export async function nextInitGenerator(host: Tree, schema: InitSchema) { const jestTask = await jestInitGenerator(host, schema); tasks.push(jestTask); } - if (!schema.e2eTestRunner || schema.e2eTestRunner === 'cypress') { + if (schema.e2eTestRunner === 'cypress') { const { cypressInitGenerator } = ensurePackage< typeof import('@nx/cypress') >('@nx/cypress', nxVersion); const cypressTask = await cypressInitGenerator(host, {}); tasks.push(cypressTask); + } else if (schema.e2eTestRunner === 'playwright') { + const { initGenerator } = ensurePackage( + '@nx/playwright', + nxVersion + ); + const playwrightTask = await initGenerator(host, { + skipFormat: true, + skipPackageJson: schema.skipPackageJson, + }); + tasks.push(playwrightTask); } + // @ts-ignore + // TODO(jack): remove once the React Playwright PR lands first const reactTask = await reactInitGenerator(host, { ...schema, skipFormat: true, diff --git a/packages/next/src/generators/init/schema.d.ts b/packages/next/src/generators/init/schema.d.ts index 43169ac817dad..662b69228d18d 100644 --- a/packages/next/src/generators/init/schema.d.ts +++ b/packages/next/src/generators/init/schema.d.ts @@ -1,6 +1,6 @@ export interface InitSchema { unitTestRunner?: 'jest' | 'none'; - e2eTestRunner?: 'cypress' | 'none'; + e2eTestRunner?: 'cypress' | 'playwright' | 'none'; skipFormat?: boolean; js?: boolean; skipPackageJson?: boolean;