feat(nextjs): add playwright as an option for e2e testing (#18281)

This commit is contained in:
Jack Hsu 2023-08-01 10:51:51 -04:00 committed by GitHub
parent 3313c7619a
commit e78575badc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 96 additions and 46 deletions

View File

@ -81,8 +81,9 @@
}, },
"e2eTestRunner": { "e2eTestRunner": {
"type": "string", "type": "string",
"enum": ["cypress", "none"], "enum": ["cypress", "playwright", "none"],
"description": "Test runner to use for end to end (E2E) tests.", "description": "Test runner to use for end to end (E2E) tests.",
"x-prompt": "Which E2E test runner would you like to use?",
"default": "cypress" "default": "cypress"
}, },
"tags": { "tags": {

View File

@ -1,5 +1,6 @@
import { import {
cleanupProject, cleanupProject,
isNotWindows,
newProject, newProject,
runCLI, runCLI,
uniq, uniq,
@ -22,7 +23,7 @@ describe('Next.js App Router', () => {
const appName = uniq('app'); const appName = uniq('app');
const jsLib = uniq('tslib'); 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`); runCLI(`generate @nx/js:lib ${jsLib} --no-interactive`);
updateFile( updateFile(
@ -42,7 +43,7 @@ describe('Next.js App Router', () => {
await checkApp(appName, { await checkApp(appName, {
checkUnitTest: false, checkUnitTest: false,
checkLint: true, checkLint: true,
checkE2E: false, checkE2E: isNotWindows(),
checkExport: false, checkExport: false,
}); });
}, 300_000); }, 300_000);

View File

@ -1,7 +1,6 @@
import { execSync } from 'child_process';
import { import {
checkFilesExist, checkFilesExist,
killPort, killPorts,
readJson, readJson,
runCLI, runCLI,
runCLIAsync, runCLIAsync,
@ -43,10 +42,10 @@ export async function checkApp(
if (opts.checkE2E && runCypressTests()) { if (opts.checkE2E && runCypressTests()) {
const e2eResults = runCLI( 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(e2eResults).toContain('Successfully ran target e2e for project');
expect(await killPort(9000)).toBeTruthy(); expect(await killPorts()).toBeTruthy();
} }
if (opts.checkExport) { if (opts.checkExport) {

View File

@ -82,6 +82,7 @@
"@nx/webpack", "@nx/webpack",
"@nx/cypress", "@nx/cypress",
"@nx/jest", "@nx/jest",
"@nx/playwright",
"typescript", "typescript",
"react", "react",
"webpack", "webpack",

View File

@ -1,6 +1,7 @@
import { import {
convertNxGenerator, convertNxGenerator,
formatFiles, formatFiles,
GeneratorCallback,
joinPathFragments, joinPathFragments,
runTasksInSerial, runTasksInSerial,
Tree, Tree,
@ -8,7 +9,7 @@ import {
import { normalizeOptions } from './lib/normalize-options'; import { normalizeOptions } from './lib/normalize-options';
import { Schema } from './schema'; import { Schema } from './schema';
import { addCypress } from './lib/add-cypress'; import { addE2e } from './lib/add-e2e';
import { addJest } from './lib/add-jest'; import { addJest } from './lib/add-jest';
import { addProject } from './lib/add-project'; import { addProject } from './lib/add-project';
import { createApplicationFiles } from './lib/create-application-files'; 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'; import { showPossibleWarnings } from './lib/show-possible-warnings';
export async function applicationGenerator(host: Tree, schema: Schema) { export async function applicationGenerator(host: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = [];
const options = normalizeOptions(host, schema); const options = normalizeOptions(host, schema);
showPossibleWarnings(host, options); showPossibleWarnings(host, options);
@ -30,17 +32,28 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
...options, ...options,
skipFormat: true, skipFormat: true,
}); });
tasks.push(nextTask);
createApplicationFiles(host, options); createApplicationFiles(host, options);
addProject(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); const jestTask = await addJest(host, options);
tasks.push(jestTask);
const lintTask = await addLinting(host, options); const lintTask = await addLinting(host, options);
updateJestConfig(host, options); tasks.push(lintTask);
updateCypressTsConfig(host, options);
const styledTask = addStyleDependencies(host, { const styledTask = addStyleDependencies(host, {
style: options.style, style: options.style,
swc: !host.exists(joinPathFragments(options.appProjectRoot, '.babelrc')), swc: !host.exists(joinPathFragments(options.appProjectRoot, '.babelrc')),
}); });
tasks.push(styledTask);
updateJestConfig(host, options);
updateCypressTsConfig(host, options);
setDefaults(host, options); setDefaults(host, options);
if (options.customServer) { if (options.customServer) {
@ -54,13 +67,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
await formatFiles(host); await formatFiles(host);
} }
return runTasksInSerial( return runTasksInSerial(...tasks);
nextTask,
cypressTask,
jestTask,
lintTask,
styledTask
);
} }
export const applicationSchematic = convertNxGenerator(applicationGenerator); export const applicationSchematic = convertNxGenerator(applicationGenerator);

View File

@ -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,
});
}

View File

@ -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 () => {};
}

View File

@ -8,7 +8,7 @@ export interface Schema {
directory?: string; directory?: string;
tags?: string; tags?: string;
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'none';
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'none';
linter?: Linter; linter?: Linter;
js?: boolean; js?: boolean;
setParserOptionsProject?: boolean; setParserOptionsProject?: boolean;

View File

@ -84,8 +84,9 @@
}, },
"e2eTestRunner": { "e2eTestRunner": {
"type": "string", "type": "string",
"enum": ["cypress", "none"], "enum": ["cypress", "playwright", "none"],
"description": "Test runner to use for end to end (E2E) tests.", "description": "Test runner to use for end to end (E2E) tests.",
"x-prompt": "Which E2E test runner would you like to use?",
"default": "cypress" "default": "cypress"
}, },
"tags": { "tags": {

View File

@ -54,14 +54,26 @@ export async function nextInitGenerator(host: Tree, schema: InitSchema) {
const jestTask = await jestInitGenerator(host, schema); const jestTask = await jestInitGenerator(host, schema);
tasks.push(jestTask); tasks.push(jestTask);
} }
if (!schema.e2eTestRunner || schema.e2eTestRunner === 'cypress') { if (schema.e2eTestRunner === 'cypress') {
const { cypressInitGenerator } = ensurePackage< const { cypressInitGenerator } = ensurePackage<
typeof import('@nx/cypress') typeof import('@nx/cypress')
>('@nx/cypress', nxVersion); >('@nx/cypress', nxVersion);
const cypressTask = await cypressInitGenerator(host, {}); const cypressTask = await cypressInitGenerator(host, {});
tasks.push(cypressTask); tasks.push(cypressTask);
} else if (schema.e2eTestRunner === 'playwright') {
const { initGenerator } = ensurePackage<typeof import('@nx/playwright')>(
'@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, { const reactTask = await reactInitGenerator(host, {
...schema, ...schema,
skipFormat: true, skipFormat: true,

View File

@ -1,6 +1,6 @@
export interface InitSchema { export interface InitSchema {
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'none';
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'playwright' | 'none';
skipFormat?: boolean; skipFormat?: boolean;
js?: boolean; js?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;