UNPKG

@nx/playwright

Version:

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

387 lines (386 loc) • 16.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodes = exports.createNodesV2 = void 0; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const devkit_1 = require("@nx/devkit"); const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs"); const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes"); const workspace_context_1 = require("nx/src/utils/workspace-context"); const minimatch_1 = require("minimatch"); const cache_directory_1 = require("nx/src/utils/cache-directory"); const js_1 = require("@nx/js"); const config_utils_1 = require("@nx/devkit/src/utils/config-utils"); const file_hasher_1 = require("nx/src/hasher/file-hasher"); 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.createNodesV2 = [ 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); } }, ]; /** * @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead. * This function will change to the v2 function in Nx 20. */ exports.createNodes = [ playwrightConfigGlob, async (configFile, options, context) => { devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'); return createNodesInternal(configFile, options, context, {}); }, ]; 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, 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 = 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) { const ciBaseTargetConfig = { ...baseTargetConfig, cache: true, inputs: [ ...('production' in namedInputs ? ['default', '^production'] : ['default', '^default']), { externalDependencies: ['@playwright/test'] }, ], outputs: getTargetOutputs(testOutput, reporterOutputs, 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: getOutputEnvVars(reporterOutputs, outputSubfolder), }, outputs: getTargetOutputs(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', }); } 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); } 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) { return { ...options, targetName: options?.targetName ?? 'e2e', ciTargetName: options?.ciTargetName ?? 'e2e-ci', }; } function getTestOutput(playwrightConfig) { const { outputDir } = playwrightConfig; if (outputDir) { return outputDir; } else { return './test-results'; } } function getReporterOutputs(playwrightConfig) { const outputs = []; const { reporter } = playwrightConfig; if (reporter) { const DEFAULT_REPORTER_OUTPUT = 'playwright-report'; if (reporter === 'html') { outputs.push([reporter, DEFAULT_REPORTER_OUTPUT]); } else if (reporter === 'json') { outputs.push([reporter, DEFAULT_REPORTER_OUTPUT]); } else if (Array.isArray(reporter)) { for (const r of reporter) { const [reporter, opts] = r; // There are a few different ways to specify an output file or directory // depending on the reporter. This is a best effort to find the output. if (opts?.outputFile) { outputs.push([reporter, opts.outputFile]); } else if (opts?.outputDir) { outputs.push([reporter, opts.outputDir]); } else if (opts?.outputFolder) { outputs.push([reporter, opts.outputFolder]); } else { outputs.push([reporter, DEFAULT_REPORTER_OUTPUT]); } } } } return outputs; } function getTargetOutputs(testOutput, reporterOutputs, workspaceRoot, projectRoot, subFolder) { const outputs = new Set(); outputs.add(normalizeOutput(addSubfolderToOutput(testOutput, subFolder), workspaceRoot, projectRoot)); for (const [, output] of reporterOutputs) { outputs.add(normalizeOutput(addSubfolderToOutput(output, subFolder), workspaceRoot, projectRoot)); } return Array.from(outputs); } function addSubfolderToOutput(output, subfolder) { if (!subfolder) return output; 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 getOutputEnvVars(reporterOutputs, outputSubfolder) { const env = {}; for (let [reporter, output] of reporterOutputs) { if (outputSubfolder) { const isFile = (0, node_path_1.parse)(output).ext !== ''; const envVarName = `PLAYWRIGHT_${reporter.toUpperCase()}_OUTPUT_${isFile ? 'FILE' : 'DIR'}`; env[envVarName] = addSubfolderToOutput(output, outputSubfolder); // 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; }