feat(testing): add @nx/playwright plugin (#17975)

This commit is contained in:
Caleb Ukle 2023-07-11 15:47:56 -05:00 committed by GitHub
parent 460e469617
commit 2128f8e3e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 709 additions and 1 deletions

View File

@ -99,6 +99,8 @@ pnpm-lock.yaml @nrwl/nx-pipelines-reviewers
/e2e/cypress/** @nrwl/nx-testing-tools-reviewers /e2e/cypress/** @nrwl/nx-testing-tools-reviewers
/packages/jest/** @nrwl/nx-testing-tools-reviewers /packages/jest/** @nrwl/nx-testing-tools-reviewers
/e2e/jest/** @nrwl/nx-testing-tools-reviewers /e2e/jest/** @nrwl/nx-testing-tools-reviewers
/packages/playwright/** @nrwl/nx-testing-tools-reviewers
/e2e/playwright/** @nrwl/nx-testing-tools-reviewers
# Linter # Linter
/docs/generated/packages/eslint-plugin/** @nrwl/nx-linter-reviewers @nrwl/nx-docs-reviewers /docs/generated/packages/eslint-plugin/** @nrwl/nx-linter-reviewers @nrwl/nx-docs-reviewers

View File

@ -0,0 +1,13 @@
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
maxWorkers: 1,
globals: {},
globalSetup: '../utils/global-setup.ts',
globalTeardown: '../utils/global-teardown.ts',
displayName: 'e2e-playwright',
preset: '../../jest.preset.js',
};

View File

@ -0,0 +1,11 @@
{
"name": "e2e-playwright",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/playwright",
"projectType": "application",
"targets": {
"e2e": {},
"run-e2e-tests": {}
},
"implicitDependencies": ["playwright"]
}

View File

@ -0,0 +1,171 @@
import {
cleanupProject,
newProject,
uniq,
createFile,
runCLI,
packageInstall,
runCommand,
} from '@nx/e2e/utils';
const TEN_MINS_MS = 600_000;
describe('Playwright E2E Test runner', () => {
beforeAll(() => {
newProject({ name: uniq('playwright') });
packageInstall('@playwright/test', undefined, '^1.30.0', 'dev');
runCommand('npx playwright install --with-deps');
});
afterAll(() => cleanupProject());
it(
'should test example app',
() => {
//TODO: remove when generators are setup.Need to have basic workspace deps setup
runCLI(`g @nx/js:init`);
addSampleProject();
// NOTE: playwright throws errors if it detects running inside jest process. tmp remove and restore the env var for playwright to run
const results = runCLI(`e2e demo-e2e`);
expect(results).toContain('6 passed');
expect(results).toContain('Successfully ran target e2e for project');
},
TEN_MINS_MS
);
});
// TODO: remove this when there are project generators
function addSampleProject() {
createFile(
'apps/demo-e2e/src/example.spec.ts',
`
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects the URL to contain intro.
await expect(page).toHaveURL(/.*intro/);
});
`
);
createFile(
'apps/demo-e2e/playwright.config.ts',
`
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './src',
outputDir: '../../dist/playwright/apps/demo-e2e/output',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
[
'html',
{
outputFolder:
'../../dist/playwright/apps/demo-e2e/playwright-report',
},
],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like await page.goto('/'). */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
});
`
);
createFile(
'apps/demo-e2e/project.json',
JSON.stringify(
{
name: 'demo-e2e',
root: 'apps/demo-e2e',
sourceRoot: 'apps/demo-e2e/src',
targets: {
e2e: {
executor: '@nx/playwright:playwright',
options: {},
},
},
},
null,
2
)
);
}

View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}

View File

@ -72,6 +72,7 @@ export function newProject({
`@nx/next`, `@nx/next`,
`@nx/node`, `@nx/node`,
`@nx/plugin`, `@nx/plugin`,
`@nx/playwright`,
`@nx/rollup`, `@nx/rollup`,
`@nx/react`, `@nx/react`,
`@nx/storybook`, `@nx/storybook`,

View File

@ -122,6 +122,10 @@ export function getStrippedEnvironmentVariables() {
return false; return false;
} }
if (key === 'JEST_WORKER_ID') {
return false;
}
return true; return true;
}) })
); );

View File

@ -1,4 +1,4 @@
const { getJestProjects } = require('@nx/jest'); import { getJestProjects } from '@nx/jest';
export default { export default {
projects: getJestProjects(), projects: getJestProjects(),

View File

@ -124,6 +124,7 @@
"@nrwl/next", "@nrwl/next",
"@nx/node", "@nx/node",
"@nrwl/node", "@nrwl/node",
"@nx/playwright",
"@nx/plugin", "@nx/plugin",
"@nrwl/nx-plugin", "@nrwl/nx-plugin",
"@nx/react", "@nx/react",

View File

@ -0,0 +1,25 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["./package.json", "./executors.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nx/nx-plugin-checks": "error"
}
}
]
}

View File

@ -0,0 +1,11 @@
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
{{links}}
<hr>
# Nx: Smart, Fast and Extensible Build System
Nx is a next generation build system with first class monorepo support and powerful integrations.
{{content}}

View File

@ -0,0 +1,16 @@
{
"builders": {
"playwright": {
"implementation": "./src/executors/playwright/compat",
"schema": "./src/executors/playwright/schema.json",
"description": "Run Playwright tests."
}
},
"executors": {
"playwright": {
"implementation": "./src/executors/playwright/playwright",
"schema": "./src/executors/playwright/schema.json",
"description": "Run Playwright tests."
}
}
}

View File

@ -0,0 +1,10 @@
/* eslint-disable */
export default {
displayName: 'playwright',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/playwright',
};

View File

@ -0,0 +1,43 @@
{
"name": "@nx/playwright",
"version": "0.0.1",
"type": "commonjs",
"homepage": "https://nx.dev",
"private": true,
"description": "The Nx Plugin for Playwright contains executors and generators allowing your workspace to use the powerful Playwright integration testing capabilities.",
"keywords": [
"Monorepo",
"Angular",
"React",
"Web",
"Node",
"Nest",
"Jest",
"Playwright",
"CLI"
],
"main": "./index",
"typings": "./index.d.ts",
"author": "Victor Savkin",
"license": "MIT",
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
"directory": "packages/playwright"
},
"dependencies": {
"@nx/devkit": "file:../devkit"
},
"peerDependencies": {
"@playwright/test": "^1.30.0"
},
"peerDependenciesMeta": {
"@playwright/test": {
"optional": true
}
},
"executors": "./executors.json"
}

View File

@ -0,0 +1,50 @@
{
"name": "playwright",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/playwright/src",
"projectType": "library",
"targets": {
"build": {
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/build/packages/playwright"],
"options": {
"commands": ["node ./scripts/copy-readme.js playwright"]
}
},
"build-base": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "build/packages/playwright",
"main": "packages/playwright/src/index.ts",
"tsConfig": "packages/playwright/tsconfig.lib.json",
"assets": [
"packages/playwright/*.md",
{
"input": "./packages/playwright/src",
"glob": "**/!(*.ts)",
"output": "./src"
},
{
"input": "./packages/playwright/src",
"glob": "**/*.d.ts",
"output": "./src"
},
{
"input": "./packages/playwright",
"glob": "generators.json",
"output": "."
},
{
"input": "./packages/playwright",
"glob": "executors.json",
"output": "."
}
]
}
},
"lint": {},
"test": {}
},
"tags": []
}

View File

@ -0,0 +1,4 @@
import { convertNxExecutor } from '@nx/devkit';
import { playwrightExecutor } from './playwright';
export default convertNxExecutor(playwrightExecutor);

View File

@ -0,0 +1,113 @@
import { fork } from 'child_process';
import { ExecutorContext, names } from '@nx/devkit';
import { join } from 'path';
export interface PlaywrightExecutorSchema {
/*
* if 'projects' is configured then that name needs to be provided instead of
* all, chromium, firefox, webkit
**/
browser?: 'all' | 'chromium' | 'firefox' | 'webkit' | string;
config?: string;
debug?: boolean;
forbidOnly?: boolean;
fullyParallel?: boolean;
grep?: string;
globalTimeout?: number;
grepInvert?: string;
headed?: boolean;
ignoreSnapshots?: boolean;
workers?: string;
list?: boolean;
maxFailures?: number | boolean;
noDeps?: boolean;
output?: string;
passWithNoTests?: boolean;
project?: string[];
quiet?: boolean;
repeatEach?: number;
reporter?:
| 'list'
| 'line'
| 'dot'
| 'json'
| 'junit'
| 'null'
| 'github'
| 'html'
| 'blob';
retries?: number;
shard?: string;
timeout?: number;
trace?:
| 'on'
| 'off'
| 'on-first-retry'
| 'on-all-retries'
| 'retain-on-failure';
updateSnapshots?: boolean;
ui?: boolean;
uiHost?: string;
uiPort?: string;
}
export async function playwrightExecutor(
options: PlaywrightExecutorSchema,
context: ExecutorContext
) {
const projectRoot =
context.projectGraph?.nodes?.[context?.projectName]?.data?.root;
if (!projectRoot) {
throw new Error(
`Unable to find the Project Root for ${context.projectName}. Is it set in the project.json?`
);
}
const args = createArgs(options);
const p = runPlaywright(args, join(context.root, projectRoot));
return new Promise<{ success: boolean }>((resolve) => {
p.on('close', (code) => {
resolve({ success: code === 0 });
});
});
}
function createArgs(opts: PlaywrightExecutorSchema): string[] {
const args: string[] = [];
for (const key in opts) {
const value = opts[key];
// NOTE: playwright doesn't accept pascalCase args, only kebab-case
const arg = names(key).fileName;
if (Array.isArray(value)) {
args.push(`--${arg}=${value.map((v) => v.trim()).join(',')}`);
} else if (typeof value === 'boolean') {
// NOTE: playwright don't accept --arg=false, instead just don't pass the arg.
if (value) {
args.push(`--${arg}`);
}
} else {
args.push(`--${arg}=${value}`);
}
}
return args;
}
function runPlaywright(args: string[], cwd: string) {
try {
const cli = require.resolve('@playwright/test/cli');
return fork(cli, ['test', ...args], {
stdio: 'inherit',
cwd,
});
} catch (e) {
console.error(e);
throw new Error('Unable to run playwright. Is @playwright/test installed?');
}
}
export default playwrightExecutor;

View File

@ -0,0 +1,155 @@
{
"$schema": "http://json-schema.org/schema",
"version": 2,
"title": "Playwright executor",
"description": "Run Playwright tests.",
"type": "object",
"properties": {
"browser": {
"type": "string",
"description": "Browser to use for tests, one of 'all', 'chromium', 'firefox' or 'webkit'. If a playwright config is provided/discovered then the browserName value is expected from the configured 'projects'",
"x-priority": "important"
},
"config": {
"type": "string",
"description": "Configuration file, or a test directory with optional",
"x-completion-type": "file",
"x-completion-glob": "playwright?(*)@(.js|.cjs|.mjs|.ts|.cts|.mtx)",
"x-priority": "important"
},
"debug": {
"type": "boolean",
"description": "Run tests with Playwright Inspector. Shortcut for 'PWDEBUG=1' environment variable and '--timeout=0',--max-failures=1 --headed --workers=1' options"
},
"forbidOnly": {
"type": "boolean",
"description": "Fail if test.only is called"
},
"fullyParallel": {
"type": "boolean",
"description": "Run all tests in parallel"
},
"grep": {
"alias": "g",
"type": "string",
"description": "Only run tests matching this regular expression"
},
"globalTimeout": {
"type": "number",
"description": "Maximum time this test suite can run in milliseconds"
},
"grepInvert": {
"alias": "gv",
"type": "string",
"description": "Only run tests that do not match this regular expression"
},
"headed": {
"type": "boolean",
"description": "Run tests in headed browsers",
"x-priority": "important"
},
"ignoreSnapshots": {
"type": "boolean",
"description": "Ignore screenshot and snapshot expectations"
},
"workers": {
"alias": "j",
"type": "string",
"description": "Number of concurrent workers or percentage of logical CPU cores, use 1 to run in a single worker"
},
"list": {
"type": "boolean",
"description": "Collect all the tests and report them, but do not run"
},
"maxFailures": {
"alias": "x",
"oneOf": [{ "type": "number" }, { "type": "boolean" }],
"description": "Stop after the first N failures"
},
"noDeps": {
"type": "boolean",
"description": "Do not run project dependencies"
},
"output": {
"type": "string",
"description": "Folder for output artifacts"
},
"passWithNoTests": {
"type": "boolean",
"description": "Makes test run succeed even if no tests were found",
"default": true
},
"project": {
"description": "Only run tests from the specified list of projects",
"type": "array",
"items": {
"type": "string"
}
},
"quiet": {
"alias": "q",
"type": "boolean",
"description": "Suppress stdio"
},
"repeatEach": {
"type": "number",
"description": "Run each test N times"
},
"reporter": {
"type": "string",
"enum": [
"list",
"line",
"dot",
"json",
"junit",
"null",
"github",
"html",
"blob"
],
"description": "Reporter to use, comma-separated, can be 'list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob'. To configure reporter options, use the playwright configuration."
},
"retries": {
"type": "number",
"description": "Maximum retry count for flaky tests, zero for no retries"
},
"shard": {
"type": "string",
"description": "Shard tests and execute only the selected shard, specify in the form 'current/all', 1-based, for example '3/5'"
},
"timeout": {
"type": "number",
"description": "Specify test timeout threshold in milliseconds, zero for unlimited"
},
"trace": {
"type": "string",
"enum": [
"on",
"off",
"on-first-retry",
"on-all-retries",
"retain-on-failure"
],
"description": "Force tracing mode, can be 'on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure'"
},
"updateSnapshots": {
"alias": "u",
"type": "boolean",
"description": "Update snapshots with actual results. Snapshots will be created if missing."
},
"ui": {
"type": "boolean",
"description": "Run tests in interactive UI mode"
},
"uiHost": {
"type": "string",
"description": "Host to serve UI on; specifying this option opens UI in a browser tab"
},
"uiPort": {
"type": "string",
"description": "Port to serve UI on, 0 for any free port; specifying this option opens UI in a browser tab"
}
},
"required": []
}

View File

@ -0,0 +1,4 @@
export {
playwrightExecutor,
PlaywrightExecutorSchema,
} from './executors/playwright/playwright';

View File

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -83,6 +83,7 @@
"@nx/nx-dev/ui-references": ["nx-dev/ui-references/src/index.ts"], "@nx/nx-dev/ui-references": ["nx-dev/ui-references/src/index.ts"],
"@nx/nx-dev/ui-sponsor-card": ["nx-dev/ui-sponsor-card/src/index.ts"], "@nx/nx-dev/ui-sponsor-card": ["nx-dev/ui-sponsor-card/src/index.ts"],
"@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"], "@nx/nx-dev/ui-theme": ["nx-dev/ui-theme/src/index.ts"],
"@nx/playwright": ["packages/playwright/src/index.ts"],
"@nx/plugin": ["packages/plugin"], "@nx/plugin": ["packages/plugin"],
"@nx/plugin/*": ["packages/plugin/*"], "@nx/plugin/*": ["packages/plugin/*"],
"@nx/react": ["packages/react"], "@nx/react": ["packages/react"],