@argos-ci/playwright
Version:
Playwright SDK for visual testing with Argos.
379 lines (372 loc) • 12.2 kB
JavaScript
// src/reporter.ts
import chalk from "chalk";
import { readConfig, upload } from "@argos-ci/core";
import { copyFile, readdir, writeFile } from "fs/promises";
import { dirname as dirname2, join } from "path";
// src/util.ts
import { createRequire as createRequire2 } from "module";
// src/metadata.ts
import {
getGitRepositoryPath,
readVersionFromPackage
} from "@argos-ci/util";
import { relative } from "path";
import { createRequire } from "module";
import { AsyncLocalStorage } from "async_hooks";
var require2 = createRequire(import.meta.url);
function tryResolve(pkg) {
try {
return require2.resolve(pkg);
} catch {
return null;
}
}
var metadataConfigStorage = new AsyncLocalStorage();
var DEFAULT_PLAYWRIGHT_LIBRARIES = [
"@playwright/test",
"playwright",
"playwright-core"
];
async function getAutomationLibraryMetadata() {
const metadataConfig = metadataConfigStorage.getStore();
const libraries = metadataConfig?.playwrightLibraries ?? DEFAULT_PLAYWRIGHT_LIBRARIES;
for (const name of libraries) {
const pkgPath = tryResolve(`${name}/package.json`);
if (pkgPath) {
const version = await readVersionFromPackage(pkgPath);
return { version, name };
}
}
throw new Error(
`Unable to find any of the following packages: ${libraries.join(", ")}`
);
}
async function getArgosPlaywrightVersion() {
const pkgPath = require2.resolve("@argos-ci/playwright/package.json");
return readVersionFromPackage(pkgPath);
}
async function getSdkMetadata() {
const metadataConfig = metadataConfigStorage.getStore();
if (metadataConfig) {
return metadataConfig.sdk;
}
const argosPlaywrightVersion = await getArgosPlaywrightVersion();
return {
name: "@argos-ci/playwright",
version: argosPlaywrightVersion
};
}
async function getLibraryMetadata() {
const [automationLibrary, sdk] = await Promise.all([
getAutomationLibraryMetadata(),
getSdkMetadata()
]);
return {
automationLibrary,
sdk
};
}
async function getTestMetadataFromTestCase(testCase, testResult) {
const repositoryPath = await getGitRepositoryPath();
const testMetadata = {
title: testCase.title,
titlePath: testCase.titlePath(),
retry: testResult.retry,
retries: testCase.retries,
repeat: testCase.repeatEachIndex,
location: {
file: repositoryPath ? relative(repositoryPath, testCase.location.file) : testCase.location.file,
line: testCase.location.line,
column: testCase.location.column
},
annotations: testCase.annotations
};
return testMetadata;
}
async function getMetadataFromTestCase(testCase, testResult) {
const [libMetadata, testMetadata] = await Promise.all([
getLibraryMetadata(),
getTestMetadataFromTestCase(testCase, testResult)
]);
const metadata = {
test: testMetadata,
...libMetadata
};
return metadata;
}
// src/util.ts
import {
getGlobalScript
} from "@argos-ci/browser";
import { dirname, resolve } from "path";
import { mkdir } from "fs/promises";
var require3 = createRequire2(import.meta.url);
var PNG_EXTENSION = `.png`;
var ARIA_EXTENSION = `.aria.yml`;
var METADATA_EXTENSION = `.argos.json`;
var MAX_NAME_LENGTH = 255 - PNG_EXTENSION.length - METADATA_EXTENSION.length;
function truncate(text, length) {
if (text.length <= length) {
return text;
}
return text.slice(0, length - 1) + "\u2026";
}
function getAutomaticScreenshotName(test, result) {
const name = test.titlePath().join(" ");
let suffix = "";
suffix += result.retry > 0 ? ` #${result.retry + 1}` : "";
suffix += result.status === "failed" || result.status === "timedOut" ? " (failed)" : "";
const maxNameLength = MAX_NAME_LENGTH - suffix.length;
if (name.length > maxNameLength) {
return `${truncate(`${test.id} - ${test.title}`, maxNameLength)}${suffix}`;
}
return `${name}${suffix}`;
}
// src/attachment.ts
function parseAttachmentName(name) {
const match = name.match(/^argos\/(screenshot|aria)(\/metadata)?___(.*)$/);
if (!match) {
return null;
}
const [, mainType, metadataPart, originalName] = match;
if (!originalName) {
throw new Error(`Invalid attachment name: ${name}`);
}
const type = metadataPart ? `${mainType}/metadata` : mainType;
return { type, originalName };
}
function getAttachmentFilename(attachment) {
const parsed = parseAttachmentName(attachment.name);
if (!parsed) {
throw new Error(`Invalid attachment name: ${attachment.name}`);
}
const { type, originalName } = parsed;
const extension = {
screenshot: PNG_EXTENSION,
aria: ARIA_EXTENSION,
"screenshot/metadata": `${PNG_EXTENSION}${METADATA_EXTENSION}`,
"aria/metadata": `${ARIA_EXTENSION}${METADATA_EXTENSION}`
}[type];
return `${originalName}${extension}`;
}
function checkIsTrace(attachment) {
return attachment.name === "trace" && attachment.contentType === "application/zip" && Boolean(attachment.path);
}
function checkIsArgosSnapshot(attachment) {
const parsed = parseAttachmentName(attachment.name);
if (!parsed) {
return false;
}
return parsed.type === "aria" || parsed.type === "screenshot";
}
function checkIsArgosMetadata(attachment) {
const parsed = parseAttachmentName(attachment.name);
if (!parsed) {
return false;
}
return parsed.type === "aria/metadata" || parsed.type === "screenshot/metadata";
}
function checkIsAutomaticScreenshot(attachment) {
return attachment.name === "screenshot" && attachment.contentType === "image/png" && Boolean(attachment.path);
}
// src/debug.ts
import createDebug from "debug";
var KEY = "@argos-ci/playwright";
var debug = createDebug(KEY);
// src/reporter.ts
import { createDirectory, createTemporaryDirectory } from "@argos-ci/util";
function checkIsDynamicBuildName(buildName) {
return Boolean(typeof buildName === "object" && buildName);
}
function createArgosReporterOptions(options) {
return options;
}
async function getParallelFromConfig(config) {
if (!config.shard) {
return null;
}
if (config.shard.total === 1) {
return null;
}
const argosConfig = await readConfig();
if (!argosConfig.parallelNonce) {
throw new Error(
"Playwright shard mode detected. Please specify ARGOS_PARALLEL_NONCE env variable. Read /parallel-testing"
);
}
return {
total: argosConfig.parallelTotal ?? config.shard.total,
nonce: argosConfig.parallelNonce,
index: argosConfig.parallelIndex ?? config.shard.current
};
}
var ArgosReporter = class {
rootUploadDirectoryPromise;
uploadDirectoryPromises;
config;
playwrightConfig;
uploadToArgos;
constructor(config) {
this.config = config;
this.uploadToArgos = config.uploadToArgos ?? true;
this.rootUploadDirectoryPromise = null;
this.uploadDirectoryPromises = /* @__PURE__ */ new Map();
}
/**
* Write a file to the temporary directory.
*/
async writeFile(path, body) {
await createDirectory(dirname2(path));
debug(`Writing file to ${path}`);
await writeFile(path, body);
debug(`File written to ${path}`);
}
/**
* Copy a file to the temporary directory.
*/
async copyFile(from, to) {
await createDirectory(dirname2(to));
debug(`Copying file from ${from} to ${to}`);
await copyFile(from, to);
debug(`File copied from ${from} to ${to}`);
}
/**
* Copy the trace file if found in the result.
*/
async copyTraceIfFound(result, path) {
const trace = result.attachments.find(checkIsTrace) ?? null;
if (trace) {
await this.copyFile(trace.path, path + ".pw-trace.zip");
}
}
/**
* Get the root upload directory (cached).
*/
getRootUploadDirectory() {
if (!this.rootUploadDirectoryPromise) {
this.rootUploadDirectoryPromise = createTemporaryDirectory();
}
return this.rootUploadDirectoryPromise;
}
onBegin(config) {
debug("ArgosReporter:onBegin");
this.playwrightConfig = config;
}
async onTestEnd(test, result) {
const buildName = checkIsDynamicBuildName(this.config.buildName) ? this.config.buildName.get(test) : this.config.buildName;
if (buildName === "") {
throw new Error('Argos "buildName" cannot be an empty string.');
}
const rootUploadDir = await this.getRootUploadDirectory();
const uploadDir = buildName ? join(rootUploadDir, buildName) : rootUploadDir;
debug("ArgosReporter:onTestEnd");
await Promise.all(
result.attachments.map(async (attachment) => {
if (checkIsArgosSnapshot(attachment) || checkIsArgosMetadata(attachment)) {
const path = join(uploadDir, getAttachmentFilename(attachment));
await Promise.all([
this.copyFile(attachment.path, path),
this.copyTraceIfFound(result, path)
]);
return;
}
if (checkIsAutomaticScreenshot(attachment)) {
const metadata = await getMetadataFromTestCase(test, result);
const name = getAutomaticScreenshotName(test, result);
const path = join(uploadDir, `${name}${PNG_EXTENSION}`);
await Promise.all([
this.writeFile(path + METADATA_EXTENSION, JSON.stringify(metadata)),
this.copyFile(attachment.path, path),
this.copyTraceIfFound(result, path)
]);
return;
}
})
);
}
async onEnd(result) {
debug("ArgosReporter:onEnd");
const rootUploadDir = await this.getRootUploadDirectory();
if (!this.uploadToArgos) {
debug("Not uploading to Argos because uploadToArgos is false.");
debug(`Upload directory: ${rootUploadDir}`);
return;
}
debug("Getting parallel from config");
const parallel = await getParallelFromConfig(this.playwrightConfig);
if (parallel) {
debug(
`Using parallel config \u2014 total: ${parallel.total}, nonce: "${parallel.nonce}"`
);
} else {
debug("Non-parallel mode");
}
const buildNameConfig = this.config.buildName;
const uploadOptions = {
files: ["**/*.png", "**/*.aria.yml"],
parallel: parallel ?? void 0,
...this.config,
buildName: void 0,
// We will set it later
metadata: {
testReport: {
status: result.status,
stats: {
startTime: result.startTime.toISOString(),
duration: result.duration
}
}
}
};
try {
if (checkIsDynamicBuildName(buildNameConfig)) {
debug(
`Dynamic build names, uploading to Argos for each build name: ${buildNameConfig.values.join()}`
);
const directories = await readdir(rootUploadDir);
if (directories.some((dir) => !buildNameConfig.values.includes(dir))) {
throw new Error(
`The \`buildName.values\` (${buildNameConfig.values.join(", ")}) are inconsistent with the \`buildName.get\` returns values (${directories.join(", ")}). Please fix the configuration.`
);
}
const iteratesOnBuildNames = parallel ? buildNameConfig.values : directories;
for (const buildName of iteratesOnBuildNames) {
const uploadDir = join(rootUploadDir, buildName);
await createDirectory(uploadDir);
debug(`Uploading to Argos for build: ${buildName}`);
const res = await upload({
...uploadOptions,
root: uploadDir,
buildName
});
console.log(
chalk.green(
`\u2705 Argos "${buildName}" build created: ${res.build.url}`
)
);
}
} else {
debug("Uploading to Argos");
const uploadDir = buildNameConfig ? join(rootUploadDir, buildNameConfig) : rootUploadDir;
const res = await upload({
...uploadOptions,
root: uploadDir,
buildName: buildNameConfig ?? void 0
});
console.log(chalk.green(`\u2705 Argos build created: ${res.build.url}`));
}
} catch (error) {
console.error(error);
return { status: "failed" };
}
return;
}
printsToStdio() {
return false;
}
};
var reporter_default = ArgosReporter;
export {
createArgosReporterOptions,
reporter_default as default
};