@nx/playwright
Version:
411 lines (410 loc) • 17.2 kB
JavaScript
;
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`);
}