UNPKG

@nx/playwright

Version:

The Nx Plugin for Playwright contains executors and generators allowing your workspace to use the powerful Playwright integration testing capabilities.

411 lines (410 loc) • 17.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodesV2 = exports.createNodes = void 0; const devkit_1 = require("@nx/devkit"); const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes"); const config_utils_1 = require("@nx/devkit/src/utils/config-utils"); const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs"); const js_1 = require("@nx/js"); const minimatch_1 = require("minimatch"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const file_hasher_1 = require("nx/src/hasher/file-hasher"); const cache_directory_1 = require("nx/src/utils/cache-directory"); const workspace_context_1 = require("nx/src/utils/workspace-context"); const reporters_1 = require("../utils/reporters"); const pmc = (0, devkit_1.getPackageManagerCommand)(); function readTargetsCache(cachePath) { try { return process.env.NX_CACHE_PROJECT_GRAPH !== 'false' ? (0, devkit_1.readJsonFile)(cachePath) : {}; } catch { return {}; } } function writeTargetsToCache(cachePath, results) { (0, devkit_1.writeJsonFile)(cachePath, results); } const playwrightConfigGlob = '**/playwright.config.{js,ts,cjs,cts,mjs,mts}'; exports.createNodes = [ playwrightConfigGlob, async (configFilePaths, options, context) => { const optionsHash = (0, file_hasher_1.hashObject)(options); const cachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `playwright-${optionsHash}.hash`); const targetsCache = readTargetsCache(cachePath); try { return await (0, devkit_1.createNodesFromFiles)((configFile, options, context) => createNodesInternal(configFile, options, context, targetsCache), configFilePaths, options, context); } finally { writeTargetsToCache(cachePath, targetsCache); } }, ]; exports.createNodesV2 = exports.createNodes; async function createNodesInternal(configFilePath, options, context, targetsCache) { const projectRoot = (0, node_path_1.dirname)(configFilePath); // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, node_fs_1.readdirSync)((0, node_path_1.join)(context.workspaceRoot, projectRoot)); if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { return {}; } const normalizedOptions = normalizeOptions(options); const hash = await (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(projectRoot, { ...normalizedOptions, CI: process.env.CI, }, context, [(0, js_1.getLockFileName)((0, devkit_1.detectPackageManager)(context.workspaceRoot))]); targetsCache[hash] ??= await buildPlaywrightTargets(configFilePath, projectRoot, normalizedOptions, context); const { targets, metadata } = targetsCache[hash]; return { projects: { [projectRoot]: { root: projectRoot, targets, metadata, }, }, }; } async function buildPlaywrightTargets(configFilePath, projectRoot, options, context) { // Playwright forbids importing the `@playwright/test` module twice. This would affect running the tests, // but we're just reading the config so let's delete the variable they are using to detect this. // See: https://github.com/microsoft/playwright/pull/11218/files delete process['__pw_initiator__']; const playwrightConfig = await (0, config_utils_1.loadConfigFile)((0, node_path_1.join)(context.workspaceRoot, configFilePath)); const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context); const targets = {}; let metadata; const testOutput = getTestOutput(playwrightConfig); const reporterOutputs = (0, reporters_1.getReporterOutputs)(playwrightConfig); const webserverCommandTasks = getWebserverCommandTasks(playwrightConfig); const baseTargetConfig = { command: 'playwright test', options: { cwd: '{projectRoot}', }, metadata: { technologies: ['playwright'], description: 'Runs Playwright Tests', help: { command: `${pmc.exec} playwright test --help`, example: { options: { workers: 1, }, }, }, }, }; if (webserverCommandTasks.length) { baseTargetConfig.dependsOn = getDependsOn(webserverCommandTasks); } else { baseTargetConfig.parallelism = false; } targets[options.targetName] = { ...baseTargetConfig, cache: true, inputs: [ ...('production' in namedInputs ? ['default', '^production'] : ['default', '^default']), { externalDependencies: ['@playwright/test'] }, ], outputs: getTargetOutputs(testOutput, reporterOutputs, context.workspaceRoot, projectRoot), }; if (options.ciTargetName) { // ensure the blob reporter output is the directory containing the blob // report files const ciReporterOutputs = reporterOutputs.map(([reporter, output]) => reporter === 'blob' && output.endsWith('.zip') ? [reporter, (0, node_path_1.dirname)(output)] : [reporter, output]); const ciBaseTargetConfig = { ...baseTargetConfig, cache: true, inputs: [ ...('production' in namedInputs ? ['default', '^production'] : ['default', '^default']), { externalDependencies: ['@playwright/test'] }, ], outputs: getTargetOutputs(testOutput, ciReporterOutputs, context.workspaceRoot, projectRoot), }; const groupName = 'E2E (CI)'; metadata = { targetGroups: { [groupName]: [] } }; const ciTargetGroup = metadata.targetGroups[groupName]; const testDir = playwrightConfig.testDir ? (0, devkit_1.joinPathFragments)(projectRoot, playwrightConfig.testDir) : projectRoot; // Playwright defaults to the following pattern. playwrightConfig.testMatch ??= '**/*.@(spec|test).?(c|m)[jt]s?(x)'; const dependsOn = []; const testFiles = await getAllTestFiles({ context, path: testDir, config: playwrightConfig, }); for (const testFile of testFiles) { const outputSubfolder = (0, node_path_1.relative)(projectRoot, testFile) .replace(/[\/\\]/g, '-') .replace(/\./g, '-'); const relativeSpecFilePath = (0, devkit_1.normalizePath)((0, node_path_1.relative)(projectRoot, testFile)); if (relativeSpecFilePath.includes('../')) { throw new Error('@nx/playwright/plugin attempted to run tests outside of the project root. This is not supported and should not happen. Please open an issue at https://github.com/nrwl/nx/issues/new/choose with the following information:\n\n' + `\n\n${JSON.stringify({ projectRoot, testFile, testFiles, context, config: playwrightConfig, }, null, 2)}`); } const targetName = `${options.ciTargetName}--${relativeSpecFilePath}`; ciTargetGroup.push(targetName); targets[targetName] = { ...ciBaseTargetConfig, options: { ...ciBaseTargetConfig.options, env: getAtomizedTaskEnvVars(reporterOutputs, outputSubfolder), }, outputs: getAtomizedTaskOutputs(testOutput, reporterOutputs, context.workspaceRoot, projectRoot, outputSubfolder), command: `${baseTargetConfig.command} ${relativeSpecFilePath} --output=${(0, node_path_1.join)(testOutput, outputSubfolder)}`, metadata: { technologies: ['playwright'], description: `Runs Playwright Tests in ${relativeSpecFilePath} in CI`, help: { command: `${pmc.exec} playwright test --help`, example: { options: { workers: 1, }, }, }, }, }; dependsOn.push({ target: targetName, projects: 'self', params: 'forward', options: 'forward', }); } targets[options.ciTargetName] ??= {}; targets[options.ciTargetName] = { executor: 'nx:noop', cache: ciBaseTargetConfig.cache, inputs: ciBaseTargetConfig.inputs, outputs: ciBaseTargetConfig.outputs, dependsOn, metadata: { technologies: ['playwright'], description: 'Runs Playwright Tests in CI', nonAtomizedTarget: options.targetName, help: { command: `${pmc.exec} playwright test --help`, example: { options: { workers: 1, }, }, }, }, }; if (!webserverCommandTasks.length) { targets[options.ciTargetName].parallelism = false; } ciTargetGroup.push(options.ciTargetName); // infer the task to merge the reports from the atomized tasks const mergeReportsTargetOutputs = new Set(); for (const [reporter, output] of reporterOutputs) { if (reporter !== 'blob' && output) { mergeReportsTargetOutputs.add(normalizeOutput(output, context.workspaceRoot, projectRoot)); } } targets[options.mergeReportsTargetName] = { executor: '@nx/playwright:merge-reports', cache: true, inputs: ciBaseTargetConfig.inputs, outputs: Array.from(mergeReportsTargetOutputs), options: { config: node_path_1.posix.relative(projectRoot, configFilePath), expectedSuites: dependsOn.length, }, metadata: { technologies: ['playwright'], description: 'Merges Playwright blob reports from atomized tasks to produce unified reports for the configured reporters.', }, }; ciTargetGroup.push(options.mergeReportsTargetName); } return { targets, metadata }; } async function getAllTestFiles(opts) { const files = await (0, workspace_context_1.getFilesInDirectoryUsingContext)(opts.context.workspaceRoot, opts.path); const matcher = createMatcher(opts.config.testMatch); const ignoredMatcher = opts.config.testIgnore ? createMatcher(opts.config.testIgnore) : () => false; return files.filter((file) => matcher(file) && !ignoredMatcher(file)); } function createMatcher(pattern) { if (Array.isArray(pattern)) { const matchers = pattern.map((p) => createMatcher(p)); return (path) => matchers.some((m) => m(path)); } else if (pattern instanceof RegExp) { return (path) => pattern.test(path); } else { return (path) => { try { return (0, minimatch_1.minimatch)(path, pattern); } catch (e) { throw new Error(`Error matching ${path} with ${pattern}: ${e.message}`); } }; } } function normalizeOptions(options) { const ciTargetName = options?.ciTargetName ?? 'e2e-ci'; return { ...options, targetName: options?.targetName ?? 'e2e', ciTargetName, mergeReportsTargetName: `${ciTargetName}--merge-reports`, }; } function getTestOutput(playwrightConfig) { const { outputDir } = playwrightConfig; if (outputDir) { return outputDir; } else { return './test-results'; } } function getTargetOutputs(testOutput, reporterOutputs, workspaceRoot, projectRoot) { const outputs = new Set(); outputs.add(normalizeOutput(testOutput, workspaceRoot, projectRoot)); for (const [, output] of reporterOutputs) { if (!output) { continue; } outputs.add(normalizeOutput(output, workspaceRoot, projectRoot)); } return Array.from(outputs); } function getAtomizedTaskOutputs(testOutput, reporterOutputs, workspaceRoot, projectRoot, subFolder) { const outputs = new Set(); outputs.add(normalizeOutput(addSubfolderToOutput(testOutput, subFolder), workspaceRoot, projectRoot)); for (const [reporter, output] of reporterOutputs) { if (!output) { continue; } if (reporter === 'blob') { const blobOutput = normalizeAtomizedTaskBlobReportOutput(output, subFolder); outputs.add(normalizeOutput(blobOutput, workspaceRoot, projectRoot)); continue; } outputs.add(normalizeOutput(addSubfolderToOutput(output, subFolder), workspaceRoot, projectRoot)); } return Array.from(outputs); } function addSubfolderToOutput(output, subfolder) { const parts = (0, node_path_1.parse)(output); if (parts.ext !== '') { return (0, node_path_1.join)(parts.dir, subfolder, parts.base); } return (0, node_path_1.join)(output, subfolder); } function getWebserverCommandTasks(playwrightConfig) { if (!playwrightConfig.webServer) { return []; } const tasks = []; const webServer = Array.isArray(playwrightConfig.webServer) ? playwrightConfig.webServer : [playwrightConfig.webServer]; for (const server of webServer) { if (!server.reuseExistingServer) { continue; } const task = parseTaskFromCommand(server.command); if (task) { tasks.push(task); } } return tasks; } function parseTaskFromCommand(command) { const nxRunRegex = /^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx run (\S+:\S+)$/; const infixRegex = /^(?:(?:npx|yarn|bun|pnpm|pnpm exec|pnpx) )?nx (\S+ \S+)$/; const nxRunMatch = command.match(nxRunRegex); if (nxRunMatch) { const [project, target] = nxRunMatch[1].split(':'); return { project, target }; } const infixMatch = command.match(infixRegex); if (infixMatch) { const [target, project] = infixMatch[1].split(' '); return { project, target }; } return null; } function getDependsOn(tasks) { const projectsPerTask = new Map(); for (const { project, target } of tasks) { if (!projectsPerTask.has(target)) { projectsPerTask.set(target, []); } projectsPerTask.get(target).push(project); } return Array.from(projectsPerTask.entries()).map(([target, projects]) => ({ projects, target, })); } function normalizeOutput(path, workspaceRoot, projectRoot) { const fullProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot); const fullPath = (0, node_path_1.resolve)(fullProjectRoot, path); const pathRelativeToProjectRoot = (0, devkit_1.normalizePath)((0, node_path_1.relative)(fullProjectRoot, fullPath)); if (pathRelativeToProjectRoot.startsWith('..')) { return (0, devkit_1.joinPathFragments)('{workspaceRoot}', (0, node_path_1.relative)(workspaceRoot, fullPath)); } return (0, devkit_1.joinPathFragments)('{projectRoot}', pathRelativeToProjectRoot); } function getAtomizedTaskEnvVars(reporterOutputs, outputSubfolder) { const env = {}; for (let [reporter, output] of reporterOutputs) { if (!output) { continue; } if (reporter === 'blob') { output = normalizeAtomizedTaskBlobReportOutput(output, outputSubfolder); } else { // add subfolder to the output to make them unique output = addSubfolderToOutput(output, outputSubfolder); } const outputExtname = (0, node_path_1.parse)(output).ext; const isFile = outputExtname !== ''; let envVarName; envVarName = `PLAYWRIGHT_${reporter.toUpperCase()}_OUTPUT_${isFile ? 'FILE' : 'DIR'}`; env[envVarName] = output; // Also set PLAYWRIGHT_HTML_REPORT for Playwright prior to 1.45.0. // HTML prior to this version did not follow the pattern of "PLAYWRIGHT_<REPORTER>_OUTPUT_<FILE|DIR>". if (reporter === 'html') { env['PLAYWRIGHT_HTML_REPORT'] = env[envVarName]; } } return env; } function normalizeAtomizedTaskBlobReportOutput(output, subfolder) { // set unique name for the blob report file return output.endsWith('.zip') ? (0, node_path_1.join)((0, node_path_1.dirname)(output), `${subfolder}.zip`) : (0, node_path_1.join)(output, `${subfolder}.zip`); }