UNPKG

@argos-ci/playwright

Version:

Playwright SDK for visual testing with Argos.

356 lines (350 loc) 10.4 kB
// src/screenshot.ts import { mkdir } from "node:fs/promises"; import { resolve, dirname } from "node: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 "node:path"; import { createRequire } from "node:module"; import { AsyncLocalStorage } from "node: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" ]; 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 getTestMetadataFromTestInfo(testInfo) { const repositoryPath = await getGitRepositoryPath(); const testMetadata = { id: testInfo.testId, title: testInfo.title, titlePath: testInfo.titlePath, retry: testInfo.retry, retries: testInfo.project.retries, repeat: testInfo.repeatEachIndex, location: { file: repositoryPath ? relative(repositoryPath, testInfo.file) : testInfo.file, line: testInfo.line, column: testInfo.column } }; return testMetadata; } // src/util.ts import { createRequire as createRequire2 } from "node: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(page) { const injected = await page.evaluate( () => typeof window.__ARGOS__ !== "undefined" ); if (!injected) { await page.addScriptTag({ content: getGlobalScript() }); } } async function getTestInfo() { try { const { test } = await import("@playwright/test"); return test.info(); } catch { return null; } } 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 } = options; return { fullPage, argosCSS, options: stabilize }; } async function beforeAll(page, options) { const { disableHover = true } = options; const context = getStabilizationContext(options); await page.evaluate( (context2) => window.__ARGOS__.beforeAll(context2), context ); if (disableHover) { await page.mouse.move(0, 0); } return async () => { await page.evaluate( () => window.__ARGOS__.afterAll() ); }; } async function beforeEach(page, options) { const context = getStabilizationContext(options); await page.evaluate( (context2) => window.__ARGOS__.beforeEach(context2), context ); return async () => { await page.evaluate( () => window.__ARGOS__.afterEach() ); }; } async function waitForReadiness(page, options) { const context = getStabilizationContext(options); try { await page.waitForFunction( (context2) => window.__ARGOS__.waitFor(context2), context ); } catch (error) { const reasons = await page.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(page, name, options = {}) { const { element, has, hasText, viewports, argosCSS: _argosCSS, root = DEFAULT_SCREENSHOT_ROOT, ...playwrightOptions } = options; if (!page) { throw new Error("A Playwright `page` object is required."); } if (!name) { throw new Error("The `name` argument is required."); } const handle = typeof element === "string" ? page.locator(element, { has, hasText }) : element ?? page; 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(page) ]); const originalViewportSize = getViewportSize(page); const fullPage = options.fullPage !== void 0 ? options.fullPage : handle === page; const afterAll = await beforeAll(page, options); const collectMetadata = async (testInfo2) => { const [colorScheme, mediaType, libMetadata, testMetadata] = await Promise.all([ page.evaluate( () => window.__ARGOS__.getColorScheme() ), page.evaluate( () => window.__ARGOS__.getMediaType() ), getLibraryMetadata(), testInfo2 ? getTestMetadataFromTestInfo(testInfo2) : null ]); const viewportSize = getViewportSize(page); const browser = page.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 = page.url(); const metadata = { url, viewport: viewportSize, colorScheme, mediaType, test: testMetadata, browser: { name: browserName, version: browserVersion }, ...libMetadata }; return metadata; }; const stabilizeAndScreenshot = async (name2) => { await options.beforeScreenshot?.({ runStabilization: (stabilizationOptions) => waitForReadiness(page, { ...options, stabilize: stabilizationOptions ?? options.stabilize }) }); await waitForReadiness(page, options); const afterEach = await beforeEach(page, options); 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 Promise.all([ handle.screenshot({ path: screenshotPath, type: "png", fullPage, mask: [page.locator('[data-visual-test="blackout"]')], animations: "disabled", ...playwrightOptions }), writeMetadata(screenshotPath, metadata) ]); if (useArgosReporter && testInfo) { await Promise.all([ testInfo.attach(getAttachmentName(names.name, "metadata"), { path: getMetadataPath(screenshotPath), contentType: "application/json" }), testInfo.attach(getAttachmentName(names.name, "screenshot"), { path: screenshotPath, contentType: "image/png" }) ]); } await afterEach(); await options.afterScreenshot?.(); }; if (viewports) { for (const viewport of viewports) { const viewportSize = resolveViewport(viewport); await setViewportSize(page, viewportSize); await stabilizeAndScreenshot( getScreenshotName(name, { viewportWidth: viewportSize.width }) ); } await setViewportSize(page, originalViewportSize); } else { await stabilizeAndScreenshot(name); } await afterAll(); } // src/csp.ts import { getGlobalScript as getGlobalScript2 } from "@argos-ci/browser"; import { createHash } from "node:crypto"; function getCSPScriptHash() { const hash = createHash("sha256").update(getGlobalScript2()).digest("base64"); return `'sha256-${hash}'`; } export { setMetadataConfig as DO_NOT_USE_setMetadataConfig, argosScreenshot, getCSPScriptHash };