@argos-ci/playwright
Version:
Playwright SDK for visual testing with Argos.
439 lines (433 loc) • 12.9 kB
JavaScript
// src/screenshot.ts
import { mkdir } from "fs/promises";
import { resolve, dirname } from "path";
import {
resolveViewport,
getGlobalScript
} from "@argos-ci/browser";
import {
getMetadataPath,
getScreenshotName,
validateThreshold,
writeMetadata
} from "@argos-ci/util";
// src/attachment.ts
function getAttachmentName(name, type) {
return `argos/${type}___${name}`;
}
// 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();
function setMetadataConfig(metadata) {
metadataConfigStorage.enterWith(metadata);
}
var DEFAULT_PLAYWRIGHT_LIBRARIES = [
"@playwright/test",
"playwright",
"playwright-core"
];
function getMetadataOverrides() {
return metadataConfigStorage.getStore();
}
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
};
}
function resolveTestFilePath(filepath, repositoryPath) {
if (!repositoryPath) {
return filepath;
}
return relative(repositoryPath, filepath);
}
async function getTestMetadata(testInfo) {
const repositoryPath = await getGitRepositoryPath();
const metadataConfig = metadataConfigStorage.getStore();
if (metadataConfig?.test) {
return {
...metadataConfig.test,
location: metadataConfig.test?.location ? {
file: resolveTestFilePath(
metadataConfig.test.location.file,
repositoryPath
),
line: metadataConfig.test.location.line,
column: metadataConfig.test.location.column
} : void 0
};
}
if (!testInfo) {
return null;
}
const testMetadata = {
id: testInfo.testId,
title: testInfo.title,
titlePath: testInfo.titlePath,
retry: testInfo.retry,
retries: testInfo.project.retries,
repeat: testInfo.repeatEachIndex,
location: {
file: resolveTestFilePath(testInfo.file, repositoryPath),
line: testInfo.line,
column: testInfo.column
},
annotations: testInfo.annotations
};
return testMetadata;
}
// src/util.ts
import { createRequire as createRequire2 } from "module";
var require3 = createRequire2(import.meta.url);
function checkIsUsingArgosReporter(testInfo) {
const reporterPath = require3.resolve("@argos-ci/playwright/reporter");
return testInfo.config.reporter.some(
(reporter) => reporter[0].includes("@argos-ci/playwright/reporter") || reporter[0] === reporterPath
);
}
// src/screenshot.ts
var DEFAULT_SCREENSHOT_ROOT = "./screenshots";
async function injectArgos(handler) {
const injected = await handler.evaluate(
() => typeof window.__ARGOS__ !== "undefined"
);
if (!injected) {
await handler.addScriptTag({ content: getGlobalScript() });
}
}
async function getTestInfo() {
try {
const { test } = await import("@playwright/test");
return test.info();
} catch {
return null;
}
}
function checkIsFrame(handler) {
return "page" in handler && typeof handler.page === "function";
}
function getPage(handler) {
if (checkIsFrame(handler)) {
return handler.page();
}
return handler;
}
function getViewportSize(page) {
const viewportSize = page.viewportSize();
if (!viewportSize) {
throw new Error("Can't take screenshots without a viewport.");
}
return viewportSize;
}
async function setViewportSize(page, viewportSize) {
await page.setViewportSize(viewportSize);
await page.waitForFunction(
({ width, height }) => window.innerWidth === width && window.innerHeight === height,
{ width: viewportSize.width, height: viewportSize.height }
);
}
function getStabilizationContext(options) {
const { fullPage, argosCSS, stabilize, viewports } = options;
return {
fullPage,
argosCSS,
viewports,
options: stabilize
};
}
async function beforeAll(handler, options) {
const { disableHover = true } = options;
const context = getStabilizationContext(options);
await handler.evaluate(
(context2) => window.__ARGOS__.beforeAll(context2),
context
);
if (disableHover) {
await getPage(handler).mouse.move(0, 0);
}
return async () => {
await handler.evaluate(
() => window.__ARGOS__.afterAll()
);
};
}
async function beforeEach(handler, options) {
const context = getStabilizationContext(options);
await handler.evaluate(
(context2) => window.__ARGOS__.beforeEach(context2),
context
);
return async () => {
await handler.evaluate(
() => window.__ARGOS__.afterEach()
);
};
}
async function increaseTimeout() {
const testInfo = await getTestInfo();
if (testInfo) {
const { timeout } = testInfo;
testInfo.setTimeout(timeout * 3);
return {
value: timeout,
reset: () => {
testInfo.setTimeout(timeout);
}
};
}
return null;
}
async function waitForReadiness(handler, options) {
const context = getStabilizationContext(options);
const timeout = await increaseTimeout();
try {
await handler.waitForFunction(
(context2) => {
const argos = window.__ARGOS__;
return argos.waitFor(context2);
},
context,
timeout ? { timeout: timeout.value } : void 0
);
timeout?.reset();
} catch (error) {
const reasons = await handler.evaluate(
(context2) => window.__ARGOS__.getWaitFailureExplanations(
context2
),
context
);
throw new Error(
`
Failed to stabilize screenshot, found the following issues:
${reasons.map((reason) => `- ${reason}`).join("\n")}
`.trim(),
{ cause: error }
);
}
}
function getScreenshotNames(name, testInfo) {
if (testInfo) {
const projectName = `${testInfo.project.name}/${name}`;
if (testInfo.repeatEachIndex > 0) {
return {
name: `${projectName} repeat-${testInfo.repeatEachIndex}`,
baseName: projectName
};
}
return { name: projectName, baseName: null };
}
return { name, baseName: null };
}
async function argosScreenshot(handler, name, options = {}) {
const {
element,
has,
hasText,
viewports,
argosCSS: _argosCSS,
root = DEFAULT_SCREENSHOT_ROOT,
...playwrightOptions
} = options;
if (!handler) {
throw new Error("A Playwright `handler` object is required.");
}
if (!name) {
throw new Error("The `name` argument is required.");
}
const screenshotTarget = typeof element === "string" ? handler.locator(element, { has, hasText }) : element ?? (checkIsFrame(handler) ? handler.locator("body") : handler);
const testInfo = await getTestInfo();
const useArgosReporter = Boolean(
testInfo && checkIsUsingArgosReporter(testInfo)
);
await Promise.all([
// Create the screenshot folder if it doesn't exist
useArgosReporter ? null : mkdir(root, { recursive: true }),
// Inject Argos script into the page
injectArgos(handler)
]);
const originalViewportSize = checkIsFrame(handler) ? null : getViewportSize(handler);
const fullPage = options.fullPage !== void 0 ? options.fullPage : screenshotTarget === handler;
const afterAll = await beforeAll(handler, options);
const collectMetadata = async (testInfo2) => {
const overrides = getMetadataOverrides();
const [colorScheme, mediaType, libMetadata, testMetadata] = await Promise.all([
handler.evaluate(
() => window.__ARGOS__.getColorScheme()
),
handler.evaluate(
() => window.__ARGOS__.getMediaType()
),
getLibraryMetadata(),
getTestMetadata(testInfo2)
]);
const viewportSize = checkIsFrame(handler) ? null : getViewportSize(handler);
const browser = getPage(handler).context().browser();
if (!browser) {
throw new Error("Can't take screenshots without a browser.");
}
const browserName = browser.browserType().name();
const browserVersion = browser.version();
const url = overrides?.url ?? handler.url();
const metadata = {
url,
colorScheme,
mediaType,
test: testMetadata,
browser: {
name: browserName,
version: browserVersion
},
...libMetadata
};
const viewport = viewportSize ?? getMetadataOverrides()?.viewport;
if (viewport) {
metadata.viewport = viewport;
}
return metadata;
};
const stabilizeAndScreenshot = async (name2) => {
const names = getScreenshotNames(name2, testInfo);
const metadata = await collectMetadata(testInfo);
metadata.transient = {};
if (options.threshold !== void 0) {
validateThreshold(options.threshold);
metadata.transient.threshold = options.threshold;
}
if (names.baseName) {
metadata.transient.baseName = `${names.baseName}.png`;
}
const screenshotPath = useArgosReporter && testInfo ? testInfo.outputPath("argos", `${names.name}.png`) : resolve(root, `${names.name}.png`);
const dir = dirname(screenshotPath);
if (dir !== root) {
await mkdir(dirname(screenshotPath), { recursive: true });
}
await options.beforeScreenshot?.({
runStabilization: (stabilizationOptions) => waitForReadiness(handler, {
...options,
stabilize: stabilizationOptions ?? options.stabilize
})
});
await waitForReadiness(handler, options);
const afterEach = await beforeEach(handler, options);
await waitForReadiness(handler, options);
await Promise.all([
screenshotTarget.screenshot({
path: screenshotPath,
type: "png",
fullPage,
mask: [handler.locator('[data-visual-test="blackout"]')],
animations: "disabled",
...playwrightOptions
}),
writeMetadata(screenshotPath, metadata)
]);
const attachments = [
{
name: getAttachmentName(names.name, "screenshot"),
contentType: "image/png",
path: screenshotPath
},
{
name: getAttachmentName(names.name, "metadata"),
contentType: "application/json",
path: getMetadataPath(screenshotPath)
}
];
if (useArgosReporter && testInfo) {
await Promise.all(
attachments.map(
(attachment) => testInfo.attach(attachment.name, {
path: attachment.path,
contentType: attachment.contentType
})
)
);
}
await afterEach();
await options.afterScreenshot?.();
return attachments;
};
const allAttachments = [];
if (viewports) {
if (checkIsFrame(handler)) {
throw new Error(`viewports option is not supported with an iframe`);
}
for (const viewport of viewports) {
const viewportSize = resolveViewport(viewport);
await setViewportSize(handler, viewportSize);
const attachments = await stabilizeAndScreenshot(
getScreenshotName(name, { viewportWidth: viewportSize.width })
);
allAttachments.push(...attachments);
}
if (!originalViewportSize) {
throw new Error(`Invariant: viewport size must be saved`);
}
await setViewportSize(handler, originalViewportSize);
} else {
const attachments = await stabilizeAndScreenshot(name);
allAttachments.push(...attachments);
}
await afterAll();
return allAttachments;
}
// src/csp.ts
import { getGlobalScript as getGlobalScript2 } from "@argos-ci/browser";
import { createHash } from "crypto";
function getCSPScriptHash() {
const hash = createHash("sha256").update(getGlobalScript2()).digest("base64");
return `'sha256-${hash}'`;
}
export {
setMetadataConfig as DO_NOT_USE_setMetadataConfig,
argosScreenshot,
getCSPScriptHash
};