diff --git a/packages/api-client/tsup.config.ts b/packages/api-client/tsup.config.ts index f260be5..f0ac238 100644 --- a/packages/api-client/tsup.config.ts +++ b/packages/api-client/tsup.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], dts: true, - clean: true, format: ["esm"], }); diff --git a/packages/browser/src/global/index.ts b/packages/browser/src/global/index.ts index 55f82ba..7278260 100644 --- a/packages/browser/src/global/index.ts +++ b/packages/browser/src/global/index.ts @@ -1,14 +1,19 @@ import { - waitForStability, + checkIsStable, setup, teardown, SetupOptions, TeardownOptions, + StabilizationOptions, + getStabilityFailureReasons, } from "./stabilization"; import { getColorScheme, getMediaType } from "./media"; const ArgosGlobal = { - waitForStability: () => waitForStability(document), + checkIsStable: (options?: StabilizationOptions) => + checkIsStable(document, options), + getStabilityFailureReasons: (options?: StabilizationOptions) => + getStabilityFailureReasons(document, options), setup: (options: SetupOptions = {}) => setup(document, options), teardown: (options: TeardownOptions = {}) => teardown(document, options), getColorScheme: () => getColorScheme(window), diff --git a/packages/browser/src/global/stabilization.ts b/packages/browser/src/global/stabilization.ts index 22831fb..c1e48b9 100644 --- a/packages/browser/src/global/stabilization.ts +++ b/packages/browser/src/global/stabilization.ts @@ -255,14 +255,68 @@ function waitForNoBusy(document: Document) { return elements.every((element) => !checkIsVisible(element)); } +export type StabilizationOptions = { + /** + * Wait for [aria-busy="true"] elements to be invisible. + * @default true + */ + ariaBusy?: boolean; + /** + * Wait for images to be loaded. + * @default true + */ + images?: boolean; + /** + * Wait for fonts to be loaded. + * @default true + */ + fonts?: boolean; +}; + +/** + * Get the stabilization state of the document. + */ +function getStabilityState(document: Document, options?: StabilizationOptions) { + const { ariaBusy = true, images = true, fonts = true } = options ?? {}; + return { + ariaBusy: ariaBusy ? waitForNoBusy(document) : true, + images: images ? waitForImagesToLoad(document) : true, + fonts: fonts ? waitForFontsToLoad(document) : true, + }; +} + +const VALIDATION_ERRORS: Record = { + ariaBusy: "Some elements still have `aria-busy='true'`", + images: "Some images are still loading", + fonts: "Some fonts are still loading", +}; + /** - * Wait for the document to be stable. + * Get the stability failure reasons. */ -export function waitForStability(document: Document) { - const results = [ - waitForNoBusy(document), - waitForImagesToLoad(document), - waitForFontsToLoad(document), - ]; - return results.every(Boolean); +export function getStabilityFailureReasons( + document: Document, + options?: StabilizationOptions, +) { + const stabilityState = getStabilityState(document, options); + return Object.entries(stabilityState).reduce( + (reasons, [key, value]) => { + if (!value) { + reasons.push(VALIDATION_ERRORS[key as keyof typeof VALIDATION_ERRORS]); + } + return reasons; + }, + [], + ); +} + +/** + * Check if the document is stable. + */ +export function checkIsStable( + document: Document, + options?: StabilizationOptions, +) { + const stabilityState = getStabilityState(document, options); + return Object.values(stabilityState).every(Boolean); } diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index cd7cf15..1288f2e 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,2 +1,3 @@ export * from "./viewport"; export * from "./script"; +export type { StabilizationOptions } from "./global/stabilization"; diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts index c69e3e6..4940a63 100644 --- a/packages/cli/tsup.config.ts +++ b/packages/cli/tsup.config.ts @@ -2,6 +2,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], - clean: true, format: ["esm"], }); diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index f260be5..f0ac238 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], dts: true, - clean: true, format: ["esm"], }); diff --git a/packages/cypress/docs/index.mdx b/packages/cypress/docs/index.mdx index 835b768..ad80fcd 100644 --- a/packages/cypress/docs/index.mdx +++ b/packages/cypress/docs/index.mdx @@ -31,6 +31,10 @@ Please refer to our [Quickstart guide](/quickstart/cypress) to get started with - `options.viewports`: Define specific viewports for capturing screenshots. More on [viewports configuration](/viewports). - `options.argosCSS`: Specific CSS applied during the screenshot process. More on [injecting CSS](/injecting-css) - `options.threshold`: Sensitivity threshold between 0 and 1. The higher the threshold, the less sensitive the diff will be. Default to `0.5`. +- `options.stabilize`: Wait for the UI to stabilize before taking the screenshot. Set to `false` to disable stabilization. Pass an object to customize the stabilization. Default to `true`. +- `options.stabilize.ariaBusy`: Wait for the `aria-busy` attribute to be removed from the document. Default to `true`. +- `options.stabilize.fonts`: Wait for fonts to be loaded. Default to `true`. +- `options.stabilize.images`: Wait for images to be loaded. Default to `true`. ## Helper Attributes for Visual Testing diff --git a/packages/cypress/src/support.ts b/packages/cypress/src/support.ts index cbdfd18..0aa3bf3 100644 --- a/packages/cypress/src/support.ts +++ b/packages/cypress/src/support.ts @@ -2,6 +2,7 @@ import "cypress-wait-until"; import { ArgosGlobal, resolveViewport, + StabilizationOptions, type ViewportOption, } from "@argos-ci/browser"; import { getGlobalScript } from "@argos-ci/browser"; @@ -21,16 +22,26 @@ type ArgosScreenshotOptions = Partial< * Viewports to take screenshots of. */ viewports?: ViewportOption[]; + /** * Custom CSS evaluated during the screenshot process. */ argosCSS?: string; + /** * Sensitivity threshold between 0 and 1. * The higher the threshold, the less sensitive the diff will be. * @default 0.5 */ threshold?: number; + + /** + * Wait for the UI to stabilize before taking the screenshot. + * Set to `false` to disable stabilization. + * Pass an object to customize the stabilization. + * @default true + */ + stabilize?: boolean | StabilizationOptions; }; declare global { @@ -88,7 +99,12 @@ Cypress.Commands.add( "argosScreenshot", { prevSubject: ["optional", "element", "window", "document"] }, (subject, name, options = {}) => { - const { viewports, argosCSS, ...cypressOptions } = options; + const { + viewports, + argosCSS, + stabilize = true, + ...cypressOptions + } = options; if (!name) { throw new Error("The `name` argument is required."); } @@ -104,13 +120,32 @@ Cypress.Commands.add( const teardown = setup(options); function stabilizeAndScreenshot(name: string) { - cy.waitUntil(() => - cy - .window({ log: false }) - .then((window) => - ((window as any).__ARGOS__ as ArgosGlobal).waitForStability(), - ), - ); + if (stabilize) { + const stabilizationOptions = + typeof stabilize === "object" ? stabilize : {}; + + cy.waitUntil(() => + cy.window({ log: false }).then((window) => { + const isStable = ( + (window as any).__ARGOS__ as ArgosGlobal + ).checkIsStable(stabilizationOptions); + + if (isStable) { + return true; + } + + const failureReasons = ( + (window as any).__ARGOS__ as ArgosGlobal + ).getStabilityFailureReasons(stabilizationOptions); + + failureReasons.forEach((reason) => { + cy.log(`[argos] stability: ${reason}`); + }); + + return false; + }), + ); + } let ref: any = {}; diff --git a/packages/cypress/tsup.config.ts b/packages/cypress/tsup.config.ts index ec87c00..6c40951 100644 --- a/packages/cypress/tsup.config.ts +++ b/packages/cypress/tsup.config.ts @@ -4,13 +4,11 @@ export default defineConfig([ { entry: ["src/support.ts"], dts: true, - clean: true, format: ["esm"], }, { entry: ["src/task.ts"], dts: true, - clean: true, format: ["esm", "cjs"], }, ]); diff --git a/packages/gitlab/tsup.config.ts b/packages/gitlab/tsup.config.ts index 78fa43b..9a03083 100644 --- a/packages/gitlab/tsup.config.ts +++ b/packages/gitlab/tsup.config.ts @@ -4,13 +4,11 @@ export default defineConfig([ { entry: ["src/index.ts"], dts: true, - clean: true, format: ["esm"], }, { entry: ["src/cli.ts"], dts: false, - clean: true, format: ["esm"], }, ]); diff --git a/packages/playwright/docs/index.mdx b/packages/playwright/docs/index.mdx index 339f08c..db53a9a 100644 --- a/packages/playwright/docs/index.mdx +++ b/packages/playwright/docs/index.mdx @@ -211,6 +211,10 @@ export default defineConfig({ - `options.disableHover`: Disable hover effects by moving the mouse to the top-left corner of the page. Default to `true`. - `options.threshold`: Sensitivity threshold between 0 and 1. The higher the threshold, the less sensitive the diff will be. Default to `0.5`. - `options.root`: Folder where the screenshots will be saved if not using the Argos reporter. Default to `./screenshots`. +- `options.stabilize`: Wait for the UI to stabilize before taking the screenshot. Set to `false` to disable stabilization. Pass an object to customize the stabilization. Default to `true`. +- `options.stabilize.ariaBusy`: Wait for the `aria-busy` attribute to be removed from the document. Default to `true`. +- `options.stabilize.fonts`: Wait for fonts to be loaded. Default to `true`. +- `options.stabilize.images`: Wait for images to be loaded. Default to `true`. Unlike [Playwright's `screenshot` method](https://playwright.dev/docs/api/class-page#page-screenshot), set `fullPage` option to `true` by default. Feel free to override this option if you prefer partial screenshots of your pages. diff --git a/packages/playwright/src/screenshot.ts b/packages/playwright/src/screenshot.ts index b2fbbd6..ea4ff7d 100644 --- a/packages/playwright/src/screenshot.ts +++ b/packages/playwright/src/screenshot.ts @@ -15,6 +15,7 @@ import { resolveViewport, ArgosGlobal, getGlobalScript, + StabilizationOptions, } from "@argos-ci/browser"; import { getMetadataPath, @@ -70,6 +71,14 @@ export type ArgosScreenshotOptions = { * @default "./screenshots" */ root?: string; + + /** + * Wait for the UI to stabilize before taking the screenshot. + * Set to `false` to disable stabilization. + * Pass an object to customize the stabilization. + * @default true + */ + stabilize?: boolean | StabilizationOptions; } & LocatorOptions & ScreenshotOptions & ScreenshotOptions; @@ -196,6 +205,7 @@ export async function argosScreenshot( hasText, viewports, argosCSS, + stabilize = true, root = DEFAULT_SCREENSHOT_ROOT, ...playwrightOptions } = options; @@ -272,9 +282,32 @@ export async function argosScreenshot( }; const stabilizeAndScreenshot = async (name: string) => { - await page.waitForFunction(() => - ((window as any).__ARGOS__ as ArgosGlobal).waitForStability(), - ); + if (stabilize) { + const stabilizationOptions = + typeof stabilize === "object" ? stabilize : {}; + try { + await page.waitForFunction( + (options) => + ((window as any).__ARGOS__ as ArgosGlobal).checkIsStable(options), + stabilizationOptions, + ); + } catch (error) { + const reasons = await page.evaluate( + (options) => + ( + (window as any).__ARGOS__ as ArgosGlobal + ).getStabilityFailureReasons(options), + stabilizationOptions, + ); + throw new Error( + ` +Failed to stabilize screenshot, found the following issues: +${reasons.map((reason) => `- ${reason}`).join("\n")} + `.trim(), + { cause: error }, + ); + } + } const names = getScreenshotNames(name, testInfo); diff --git a/packages/playwright/tsup.config.ts b/packages/playwright/tsup.config.ts index fcfd33e..e0f3cc8 100644 --- a/packages/playwright/tsup.config.ts +++ b/packages/playwright/tsup.config.ts @@ -4,14 +4,12 @@ export default defineConfig([ { entry: ["src/index.ts"], dts: true, - clean: true, external: ["@playwright/test"], format: ["esm"], }, { entry: ["src/reporter.ts"], dts: true, - clean: true, format: ["esm"], }, ]); diff --git a/packages/puppeteer/docs/index.mdx b/packages/puppeteer/docs/index.mdx index 5246dbd..2de6920 100644 --- a/packages/puppeteer/docs/index.mdx +++ b/packages/puppeteer/docs/index.mdx @@ -58,6 +58,10 @@ Screenshots are stored in `screenshots/argos` folder, relative to current direct - `options.argosCSS`: Specific CSS applied during the screenshot process. More on [injecting CSS](/injecting-css) - `options.disableHover`: Disable hover effects by moving the mouse to the top-left corner of the page. Default to `true`. - `options.threshold`: Sensitivity threshold between 0 and 1. The higher the threshold, the less sensitive the diff will be. Default to `0.5`. +- `options.stabilize`: Wait for the UI to stabilize before taking the screenshot. Set to `false` to disable stabilization. Pass an object to customize the stabilization. Default to `true`. +- `options.stabilize.ariaBusy`: Wait for the `aria-busy` attribute to be removed from the document. Default to `true`. +- `options.stabilize.fonts`: Wait for fonts to be loaded. Default to `true`. +- `options.stabilize.images`: Wait for images to be loaded. Default to `true`. Unlike [Puppeteer's `screenshot` method](https://playwright.dev/docs/api/class-page#page-screenshot), `argosScreenshot` set `fullPage` option to `true` by default. Feel free to override this option if you prefer partial screenshots of your pages. diff --git a/packages/puppeteer/src/index.ts b/packages/puppeteer/src/index.ts index 73faf1a..4a090e4 100644 --- a/packages/puppeteer/src/index.ts +++ b/packages/puppeteer/src/index.ts @@ -8,6 +8,7 @@ import { ArgosGlobal, getGlobalScript, ViewportSize, + StabilizationOptions, } from "@argos-ci/browser"; import { ScreenshotMetadata, @@ -41,25 +42,37 @@ export type ArgosScreenshotOptions = Omit< * ElementHandle or string selector of the element to take a screenshot of. */ element?: string | ElementHandle; + /** * Viewports to take screenshots of. */ viewports?: ViewportOption[]; + /** * Custom CSS evaluated during the screenshot process. */ argosCSS?: string; + /** * Disable hover effects by moving the mouse to the top-left corner of the page. * @default true */ disableHover?: boolean; + /** * Sensitivity threshold between 0 and 1. * The higher the threshold, the less sensitive the diff will be. * @default 0.5 */ threshold?: number; + + /** + * Wait for the UI to stabilize before taking the screenshot. + * Set to `false` to disable stabilization. + * Pass an object to customize the stabilization. + * @default true + */ + stabilize?: boolean | StabilizationOptions; }; async function getPuppeteerVersion(): Promise { @@ -166,7 +179,13 @@ export async function argosScreenshot( */ options: ArgosScreenshotOptions = {}, ) { - const { element, viewports, argosCSS, ...puppeteerOptions } = options; + const { + element, + viewports, + argosCSS, + stabilize = true, + ...puppeteerOptions + } = options; if (!page) { throw new Error("A Puppeteer `page` object is required."); } @@ -238,9 +257,33 @@ export async function argosScreenshot( } async function stabilizeAndScreenshot(name: string) { - await page.waitForFunction(() => - ((window as any).__ARGOS__ as ArgosGlobal).waitForStability(), - ); + if (stabilize) { + const stabilizationOptions = + typeof stabilize === "object" ? stabilize : {}; + try { + await page.waitForFunction( + (options) => + ((window as any).__ARGOS__ as ArgosGlobal).checkIsStable(options), + undefined, + stabilizationOptions, + ); + } catch (error) { + const reasons = await page.evaluate( + (options) => + ( + (window as any).__ARGOS__ as ArgosGlobal + ).getStabilityFailureReasons(options), + stabilizationOptions, + ); + throw new Error( + ` +Failed to stabilize screenshot, found the following issues: +${reasons.map((reason) => `- ${reason}`).join("\n")} + `.trim(), + { cause: error }, + ); + } + } const [screenshotPath, metadata] = await Promise.all([ getScreenshotPath(name), diff --git a/packages/puppeteer/tsup.config.ts b/packages/puppeteer/tsup.config.ts index f260be5..f0ac238 100644 --- a/packages/puppeteer/tsup.config.ts +++ b/packages/puppeteer/tsup.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], dts: true, - clean: true, format: ["esm"], }); diff --git a/packages/storybook/src/index.ts b/packages/storybook/src/index.ts index 4fb2248..449a37d 100644 --- a/packages/storybook/src/index.ts +++ b/packages/storybook/src/index.ts @@ -81,6 +81,13 @@ export async function argosScreenshot( await argosPlaywrightScreenshot(page, join(context.title, context.name), { ...screenshotOptions, + // Disable aria-busy stabilization by default + stabilize: screenshotOptions.stabilize ?? { + ariaBusy: false, + ...(typeof screenshotOptions.stabilize === "object" + ? screenshotOptions.stabilize + : {}), + }, ...fitToContentOptions, }); } diff --git a/packages/storybook/tsup.config.ts b/packages/storybook/tsup.config.ts index f260be5..f0ac238 100644 --- a/packages/storybook/tsup.config.ts +++ b/packages/storybook/tsup.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], dts: true, - clean: true, format: ["esm"], }); diff --git a/packages/util/tsup.config.ts b/packages/util/tsup.config.ts index 7d30b3c..4a77597 100644 --- a/packages/util/tsup.config.ts +++ b/packages/util/tsup.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts", "src/browser.ts"], dts: true, - clean: true, format: ["esm"], }); diff --git a/packages/webdriverio/tsup.config.ts b/packages/webdriverio/tsup.config.ts index f260be5..f0ac238 100644 --- a/packages/webdriverio/tsup.config.ts +++ b/packages/webdriverio/tsup.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], dts: true, - clean: true, format: ["esm"], });