UNPKG

@argos-ci/browser

Version:
280 lines (279 loc) 25.7 kB
//#region src/viewport.ts const viewportPresets = { "pro-display": { width: 3008, height: 1962 }, "studio-display": { width: 2560, height: 1440 }, "imac-24": { width: 2240, height: 1260 }, "macbook-16": { width: 1536, height: 960 }, "macbook-15": { width: 1440, height: 900 }, "macbook-13": { width: 1280, height: 800 }, "macbook-neo": { width: 1204, height: 753 }, "macbook-11": { width: 1366, height: 768 }, "ipad-13-pro": { width: 1032, height: 1376 }, "ipad-11-pro-m5": { width: 834, height: 1210 }, "ipad-13-air": { width: 1024, height: 1366 }, "ipad-11-air": { width: 820, height: 1180 }, "ipad-11": { width: 820, height: 1180 }, "ipad-12-pro": { width: 1024, height: 1366 }, "ipad-11-pro": { width: 834, height: 1194 }, "ipad-10": { width: 810, height: 1080 }, "ipad-10-pro": { width: 834, height: 1112 }, "ipad-9-pro": { width: 768, height: 1024 }, "ipad-2": { width: 768, height: 1024 }, "ipad-mini-a17": { width: 744, height: 1133 }, "ipad-mini": { width: 768, height: 1024 }, "iphone-air": { width: 420, height: 912 }, "iphone-17": { width: 402, height: 874 }, "iphone-17-pro": { width: 402, height: 873 }, "iphone-17-pro-max": { width: 440, height: 956 }, "iphone-17e": { width: 390, height: 844 }, "iphone-16": { width: 393, height: 852 }, "iphone-16e": { width: 390, height: 844 }, "iphone-16-plus": { width: 430, height: 932 }, "iphone-16-pro": { width: 402, height: 874 }, "iphone-16-pro-max": { width: 440, height: 956 }, "iphone-15": { width: 393, height: 852 }, "iphone-15-plus": { width: 430, height: 932 }, "iphone-15-pro": { width: 393, height: 852 }, "iphone-15-pro-max": { width: 430, height: 932 }, "iphone-14": { width: 390, height: 844 }, "iphone-14-plus": { width: 428, height: 926 }, "iphone-14-pro": { width: 393, height: 852 }, "iphone-14-pro-max": { width: 490, height: 932 }, "iphone-13": { width: 390, height: 844 }, "iphone-13-mini": { width: 360, height: 780 }, "iphone-13-pro": { width: 390, height: 844 }, "iphone-13-pro-max": { width: 428, height: 926 }, "iphone-12": { width: 390, height: 844 }, "iphone-12-mini": { width: 360, height: 780 }, "iphone-12-pro": { width: 390, height: 844 }, "iphone-12-pro-max": { width: 428, height: 926 }, "iphone-11": { width: 414, height: 896 }, "iphone-11-pro": { width: 375, height: 812 }, "iphone-11-pro-max": { width: 414, height: 896 }, "iphone-xr": { width: 414, height: 896 }, "iphone-x": { width: 375, height: 812 }, "iphone-6+": { width: 414, height: 736 }, "iphone-se2": { width: 375, height: 667 }, "iphone-8": { width: 375, height: 667 }, "iphone-7": { width: 375, height: 667 }, "iphone-6": { width: 375, height: 667 }, "iphone-5": { width: 320, height: 568 }, "iphone-4": { width: 320, height: 480 }, "iphone-3": { width: 320, height: 480 }, "samsung-s10": { width: 360, height: 760 }, "samsung-note9": { width: 414, height: 846 } }; function resolveViewportPreset(preset, orientation) { const { width, height } = viewportPresets[preset]; return orientation === "portrait" ? { width, height } : { width: height, height: width }; } function checkIsViewportPresetOption(value) { return typeof value === "object" && value !== null && "preset" in value; } function resolveViewport(viewportOption) { if (checkIsViewportPresetOption(viewportOption)) return resolveViewportPreset(viewportOption.preset, viewportOption.orientation ?? "portrait"); if (typeof viewportOption === "string") return resolveViewportPreset(viewportOption, "portrait"); return viewportOption; } //#endregion //#region src/script.ts /** * Read the global script and return it as a string. */ function getGlobalScript() { return "(function() {\n //#region src/global/media.ts\n /**\n * Get the current color scheme of the user.\n */\n function getColorScheme() {\n const { colorScheme } = window.getComputedStyle(document.body);\n return colorScheme === \"dark\" || colorScheme === \"dark only\" || window.matchMedia(\"(prefers-color-scheme: dark)\").matches ? \"dark\" : \"light\";\n }\n /**\n * Get the current media type of the user.\n */\n function getMediaType() {\n return window.matchMedia(\"print\").matches ? \"print\" : \"screen\";\n }\n //#endregion\n //#region src/global/stabilization/plugins/addArgosClass.ts\n /**\n * Add a global class to the document element.\n */\n const plugin$14 = {\n name: \"addArgosClass\",\n beforeAll() {\n const className = \"__argos__\";\n document.documentElement.classList.add(className);\n return () => {\n document.documentElement.classList.remove(className);\n };\n }\n };\n //#endregion\n //#region src/global/stabilization/util.ts\n /**\n * Inject global styles in the DOM.\n */\n function injectGlobalStyles(css, id) {\n const style = document.createElement(\"style\");\n style.textContent = css.trim();\n style.id = `argos-${id}`;\n document.head.appendChild(style);\n return () => {\n const style = document.getElementById(`argos-${id}`);\n if (style) style.remove();\n };\n }\n //#endregion\n //#region src/global/stabilization/plugins/addArgosCSS.ts\n /**\n * Inject custom CSS into the document.\n */\n const plugin$13 = {\n name: \"addArgosCSS\",\n beforeAll(options) {\n if (options.argosCSS) return injectGlobalStyles(options.argosCSS, \"custom-css\");\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/argosHelpers.ts\n /**\n * Inject CSS for Argos helpers.\n */\n const plugin$12 = {\n name: \"argosHelpers\",\n beforeAll() {\n return injectGlobalStyles(`\n/* Make the element transparent */\n[data-visual-test=\"transparent\"] {\n color: transparent !important;\n font-family: monospace !important;\n opacity: 0 !important;\n}\n\n/* Remove the element */\n[data-visual-test=\"removed\"] {\n display: none !important;\n}\n\n/* Disable radius */\n[data-visual-test-no-radius]:not([data-visual-test-no-radius=\"false\"]) {\n border-radius: 0 !important;\n}\n `, \"argos-helpers\");\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/disableSpellCheck.ts\n const UNSET = \"--unset\";\n const BACKUP_ATTRIBUTE$2 = \"data-argos-bck-spellcheck\";\n /**\n * Disable spellcheck to avoid displaying markers.\n */\n const plugin$11 = {\n name: \"disableSpellcheck\",\n beforeAll() {\n document.querySelectorAll(\"[contenteditable]:not([contenteditable=false]), input, textarea\").forEach((element) => {\n const spellcheck = element.getAttribute(\"spellcheck\");\n if (spellcheck === \"false\") return;\n element.setAttribute(BACKUP_ATTRIBUTE$2, spellcheck ?? UNSET);\n element.setAttribute(\"spellcheck\", \"false\");\n });\n return () => {\n document.querySelectorAll(`[${BACKUP_ATTRIBUTE$2}]`).forEach((input) => {\n const bckSpellcheck = input.getAttribute(BACKUP_ATTRIBUTE$2);\n if (bckSpellcheck === UNSET) input.removeAttribute(\"spellcheck\");\n else if (bckSpellcheck) input.setAttribute(\"spellcheck\", bckSpellcheck);\n input.removeAttribute(BACKUP_ATTRIBUTE$2);\n });\n };\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/fontAntialiasing.ts\n /**\n * Enable antialiasing for fonts.\n */\n const plugin$10 = {\n name: \"fontAntialiasing\",\n beforeAll() {\n return injectGlobalStyles(`* { -webkit-font-smoothing: antialiased !important; }`, \"font-antialiasing\");\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/hideCarets.ts\n /**\n * Hide carets.\n */\n const plugin$9 = {\n name: \"hideCarets\",\n beforeAll() {\n return injectGlobalStyles(`* { caret-color: transparent !important; }`, \"hide-carets\");\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/hideScrollbars.ts\n /**\n * Hide scrollbars.\n */\n const plugin$8 = {\n name: \"hideScrollbars\",\n beforeAll() {\n return injectGlobalStyles(`::-webkit-scrollbar { display: none !important; }`, \"hide-scrollbars\");\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/loadImageSrcset.ts\n /**\n * Force srcset to resolve to the biggest candidate for the current run.\n * This collapses srcset to a single candidate while staying consistent with\n * descriptor rules from the spec.\n */\n const plugin$7 = {\n name: \"loadImageSrcset\",\n beforeEach(options) {\n if (!options.viewports || options.viewports.length === 0) return;\n function parseSrcset(srcset) {\n return srcset.split(\",\").reduce((candidates, rawPart) => {\n const part = rawPart.trim();\n if (!part) return candidates;\n const tokens = part.split(/\\s+/);\n const maybeDescriptor = tokens.length > 1 ? tokens[tokens.length - 1] : null;\n if (maybeDescriptor && /^\\d+w$/.test(maybeDescriptor)) {\n const url = tokens.slice(0, -1).join(\" \");\n if (url) candidates.push({\n url,\n kind: \"w\",\n value: Number.parseInt(maybeDescriptor.slice(0, -1), 10)\n });\n return candidates;\n }\n if (maybeDescriptor && /^\\d+\\.?\\d*x$/.test(maybeDescriptor)) {\n const url = tokens.slice(0, -1).join(\" \");\n if (url) candidates.push({\n url,\n kind: \"x\",\n value: Number.parseFloat(maybeDescriptor.slice(0, -1))\n });\n return candidates;\n }\n const url = tokens[0] ?? \"\";\n if (url) candidates.push({\n url,\n kind: \"none\",\n value: 1\n });\n return candidates;\n }, []);\n }\n function pickLargestCandidate(candidates) {\n let winner = null;\n let kind = null;\n for (const candidate of candidates) {\n if (kind !== null && candidate.kind !== kind) return null;\n kind ??= candidate.kind;\n if (winner === null || candidate.value > winner.value) winner = candidate;\n }\n return winner;\n }\n function candidateToSingleSrcset(candidate, requireW) {\n if (candidate.kind === \"none\") return requireW ? `${candidate.url} 1w` : candidate.url;\n return `${candidate.url} ${candidate.value}${candidate.kind}`;\n }\n function forceSrcsetReload(el) {\n const srcset = el.getAttribute(\"srcset\");\n if (!srcset) return;\n const candidates = parseSrcset(srcset);\n const chosen = pickLargestCandidate(candidates);\n if (!chosen) return;\n const requireW = el.hasAttribute(\"sizes\") || chosen.kind === \"w\" || candidates.some((c) => c.kind === \"w\");\n el.setAttribute(\"srcset\", \"\");\n if (el instanceof HTMLImageElement) el.src = chosen.url;\n el.clientWidth;\n el.setAttribute(\"srcset\", candidateToSingleSrcset(chosen, requireW));\n }\n Array.from(document.querySelectorAll(\"img,source\")).forEach(forceSrcsetReload);\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/pauseGifs.ts\n const BACKUP_ATTRIBUTE$1 = \"data-argos-gif-src\";\n /**\n * Attribute authors can set to flag an image as a GIF explicitly, e.g.\n * `<img data-image-type=\"gif\">`.\n */\n const IMAGE_TYPE_ATTRIBUTE = \"data-image-type\";\n /**\n * GIFs currently being frozen for this screenshot cycle.\n * Freezing is asynchronous (we load a fresh copy to grab its first frame), so\n * `wait.for` polls this set to keep stabilization blocked until every GIF has\n * been paused.\n */\n const pendingFreezes = /* @__PURE__ */ new Set();\n /**\n * Detect animated GIFs from their resolved source. There is no cheap way to\n * inspect the decoded bytes, so we rely on the URL (file extension or the\n * `data:image/gif` MIME type). Authors can also opt an image in explicitly with\n * `data-image-type=\"gif\"`, which is the only way to catch GIFs served from URLs\n * that carry no `.gif` extension (e.g. a CDN endpoint).\n */\n function isGif(img) {\n if (img.getAttribute(IMAGE_TYPE_ATTRIBUTE) === \"gif\") return true;\n const src = img.currentSrc || img.src;\n return /^data:image\\/gif[;,]/i.test(src) || /\\.gif(?:$|[?#])/i.test(src);\n }\n /**\n * Pause animated GIFs by replacing them with a static snapshot of their first\n * frame. A playing GIF renders a non-deterministic frame depending on when the\n * screenshot is taken, which causes flaky visual diffs.\n *\n * The current frame of an already-rendered `<img>` is unpredictable (it has\n * been animating since it loaded), so we load a fresh copy of the image and\n * draw it to a canvas the moment it decodes — that always yields the first\n * frame — then swap the frozen data URL back into the original element.\n */\n const plugin$6 = {\n name: \"pauseGifs\",\n beforeEach() {\n Array.from(document.images).forEach((img) => {\n if (img.hasAttribute(BACKUP_ATTRIBUTE$1) || !isGif(img)) return;\n const originalSrc = img.src;\n const frame = new Image();\n frame.crossOrigin = \"anonymous\";\n pendingFreezes.add(img);\n const done = () => {\n pendingFreezes.delete(img);\n };\n frame.onload = () => {\n const canvas = document.createElement(\"canvas\");\n canvas.width = frame.naturalWidth;\n canvas.height = frame.naturalHeight;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx || canvas.width === 0 || canvas.height === 0) {\n done();\n return;\n }\n ctx.drawImage(frame, 0, 0);\n try {\n const frozenSrc = canvas.toDataURL(\"image/png\");\n img.setAttribute(BACKUP_ATTRIBUTE$1, originalSrc);\n img.src = frozenSrc;\n } catch {}\n done();\n };\n frame.onerror = done;\n frame.src = originalSrc;\n });\n return () => {\n pendingFreezes.clear();\n Array.from(document.images).forEach((img) => {\n const originalSrc = img.getAttribute(BACKUP_ATTRIBUTE$1);\n if (originalSrc === null) return;\n img.src = originalSrc;\n img.removeAttribute(BACKUP_ATTRIBUTE$1);\n });\n };\n },\n wait: {\n for: () => pendingFreezes.size === 0,\n failureExplanation: \"Some GIFs are still being paused\"\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/roundImageSize.ts\n const BACKUP_ATTRIBUTE_WIDTH = \"data-argos-bck-width\";\n const BACKUP_ATTRIBUTE_HEIGHT = \"data-argos-bck-height\";\n /**\n * Round all image sizes to stabilize images rendering.\n */\n const plugin$5 = {\n name: \"roundImageSize\",\n beforeEach() {\n Array.from(document.images).forEach((img) => {\n if (!img.complete || img.naturalWidth === 0) return;\n if (!img.hasAttribute(BACKUP_ATTRIBUTE_WIDTH)) {\n img.setAttribute(BACKUP_ATTRIBUTE_WIDTH, img.style.width);\n img.setAttribute(BACKUP_ATTRIBUTE_HEIGHT, img.style.height);\n }\n img.style.width = `${Math.round(img.offsetWidth)}px`;\n img.style.height = `${Math.round(img.offsetHeight)}px`;\n });\n return () => {\n Array.from(document.images).forEach((img) => {\n if (!img.hasAttribute(BACKUP_ATTRIBUTE_WIDTH)) return;\n const bckWidth = img.getAttribute(BACKUP_ATTRIBUTE_WIDTH);\n const bckHeight = img.getAttribute(BACKUP_ATTRIBUTE_HEIGHT);\n img.style.width = bckWidth ?? \"\";\n img.style.height = bckHeight ?? \"\";\n img.removeAttribute(BACKUP_ATTRIBUTE_WIDTH);\n img.removeAttribute(BACKUP_ATTRIBUTE_HEIGHT);\n });\n };\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/stabilizeSticky.ts\n const BACKUP_ATTRIBUTE = \"data-argos-bck-position\";\n /**\n * Set the position of an element and backup the previous one.\n */\n function setAndBackupPosition(element, position) {\n const previousPosition = element.style.position;\n const previousRect = element.getBoundingClientRect();\n element.style.position = position;\n const currentRect = element.getBoundingClientRect();\n if (previousRect.x !== currentRect.x || previousRect.y !== currentRect.y) {\n element.style.position = previousPosition;\n return;\n }\n element.setAttribute(BACKUP_ATTRIBUTE, previousPosition ?? \"unset\");\n }\n /**\n * Stabilize sticky and fixed elements.\n */\n const plugin$4 = {\n name: \"stabilizeSticky\",\n beforeAll(options) {\n if (!options.fullPage) return;\n document.querySelectorAll(\"*\").forEach((element) => {\n if (!(element instanceof HTMLElement)) return;\n if (element.tagName === \"IFRAME\") return;\n const { position } = window.getComputedStyle(element);\n if (position === \"fixed\") setAndBackupPosition(element, \"absolute\");\n else if (position === \"sticky\") setAndBackupPosition(element, \"relative\");\n });\n return () => {\n document.querySelectorAll(`[${BACKUP_ATTRIBUTE}]`).forEach((element) => {\n if (!(element instanceof HTMLElement)) return;\n const position = element.getAttribute(BACKUP_ATTRIBUTE);\n if (!position) return;\n if (position === \"unset\") element.style.removeProperty(\"position\");\n else element.style.position = position;\n element.removeAttribute(BACKUP_ATTRIBUTE);\n });\n };\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/waitForAriaBusy.ts\n /**\n * Check if an element is visible.\n */\n function checkIsElementVisible(element) {\n if (element instanceof HTMLElement && (element.offsetHeight !== 0 || element.offsetWidth !== 0)) return true;\n return element.getClientRects().length > 0;\n }\n /**\n * Wait for [aria-busy=\"true\"] elements to be invisible.\n */\n const plugin$3 = {\n name: \"waitForAriaBusy\",\n wait: {\n for: () => {\n return Array.from(document.querySelectorAll(\"[aria-busy=\\\"true\\\"]\")).every((element) => !checkIsElementVisible(element));\n },\n failureExplanation: \"Some elements still have `aria-busy='true'`\"\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/waitForBackgroundImages.ts\n /**\n * Images preloaded for the current screenshot cycle.\n * Populated by a single DOM scan in `beforeEach` and polled cheaply in\n * `wait.for` (we only read `img.complete`, never re-scan the DOM).\n */\n const preloadedImages = /* @__PURE__ */ new Set();\n const URL_REGEX = /url\\((['\"]?)([^'\")]+)\\1\\)/g;\n /**\n * By default, only elements opted in with the `data-visual-test-wait-bg-img`\n * attribute (and their descendants) are scanned. This keeps the otherwise\n * expensive `getComputedStyle` sweep cheap while still letting authors flag the\n * regions whose background images must be loaded before the screenshot.\n */\n const DEFAULT_SELECTOR = \"[data-visual-test-wait-bg-img], [data-visual-test-wait-bg-img] *\";\n /**\n * Extract non-data background-image URLs from a computed `background-image`\n * value (which may contain several layered backgrounds and gradients).\n */\n function collectUrls(value, urls) {\n if (!value || value === \"none\") return;\n URL_REGEX.lastIndex = 0;\n let match;\n while ((match = URL_REGEX.exec(value)) !== null) {\n const url = match[2];\n if (url && !url.startsWith(\"data:\")) urls.add(url);\n }\n }\n /**\n * Resolve the selector scoping the scan. Defaults to the opt-in attribute\n * selector; `true` widens the scan to the whole document, and an explicit\n * `selector` overrides both.\n */\n function resolveSelector(options) {\n if (options === true) return \"*\";\n if (options && typeof options === \"object\") {\n const { selector } = options;\n if (typeof selector === \"string\" && selector.trim() !== \"\") return selector;\n }\n return DEFAULT_SELECTOR;\n }\n /**\n * Wait for CSS background images to be loaded.\n *\n * There is no native load event for CSS background images, so URLs are\n * discovered with `getComputedStyle` and preloaded through `Image` objects.\n *\n * A full-document `getComputedStyle` sweep is expensive, so by default the scan\n * is limited to elements opted in with the `data-visual-test-wait-bg-img`\n * attribute (and their descendants). Widen it to the whole document with\n * `stabilize: { waitForBackgroundImages: true }`, or target a custom selector\n * with `stabilize: { waitForBackgroundImages: { selector: \".hero, [data-bg]\" } }`.\n *\n * The scan runs only once per viewport in `beforeEach`; `wait.for` just polls\n * the preloaded images.\n */\n const plugin$2 = {\n name: \"waitForBackgroundImages\",\n beforeEach(_context, options) {\n const selector = resolveSelector(options);\n const urls = /* @__PURE__ */ new Set();\n document.querySelectorAll(selector).forEach((element) => {\n collectUrls(window.getComputedStyle(element).backgroundImage, urls);\n collectUrls(window.getComputedStyle(element, \"::before\").backgroundImage, urls);\n collectUrls(window.getComputedStyle(element, \"::after\").backgroundImage, urls);\n });\n urls.forEach((url) => {\n const img = new Image();\n img.decoding = \"sync\";\n img.src = url;\n preloadedImages.add(img);\n });\n return () => {\n preloadedImages.clear();\n };\n },\n wait: {\n for: () => {\n return Array.from(preloadedImages).every((img) => img.complete);\n },\n failureExplanation: \"Some background images are still loading\"\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/waitForFonts.ts\n /**\n * Wait for fonts to be loaded.\n */\n const plugin$1 = {\n name: \"waitForFonts\",\n wait: {\n for: () => {\n return document.fonts.status === \"loaded\";\n },\n failureExplanation: \"Some fonts are still loading\"\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/waitForImages.ts\n /**\n * Wait for images to be loaded.\n */\n const plugin = {\n name: \"waitForImages\",\n beforeEach() {\n Array.from(document.images).forEach((img) => {\n if (img.decoding !== \"sync\") img.decoding = \"sync\";\n if (img.loading !== \"eager\") img.loading = \"eager\";\n });\n },\n wait: {\n for: () => {\n return Array.from(document.images).every((img) => {\n if (img.decoding !== \"sync\") img.decoding = \"sync\";\n if (img.loading !== \"eager\") img.loading = \"eager\";\n return img.complete;\n });\n },\n failureExplanation: \"Some images are still loading\"\n }\n };\n //#endregion\n //#region src/global/stabilization/plugins/index.ts\n const corePlugins = [\n plugin$14,\n plugin$13,\n plugin$12\n ];\n const plugins = [\n plugin$11,\n plugin$10,\n plugin$9,\n plugin$8,\n plugin$7,\n plugin$6,\n plugin$5,\n plugin$4,\n plugin$3,\n plugin$2,\n plugin$1,\n plugin\n ];\n //#endregion\n //#region src/global/stabilization/index.ts\n const beforeAllCleanups = /* @__PURE__ */ new Set();\n const beforeEachCleanups = /* @__PURE__ */ new Set();\n /**\n * Get the option value passed for a specific plugin.\n */\n function getPluginOptions(context, name) {\n if (typeof context.options === \"object\" && context.options) return context.options[name];\n }\n /**\n * Get the list of plugins to run based on the options.\n */\n function getPlugins(context) {\n const enabledPlugins = plugins.filter((plugin) => {\n if (context.options === false) return false;\n if (getPluginOptions(context, plugin.name) === false) return false;\n return true;\n });\n return [...corePlugins, ...enabledPlugins];\n }\n function getPluginByName(name) {\n const plugin = plugins.find((p) => p.name === name);\n if (!plugin) throw new Error(`Invariant: plugin ${name} not found`);\n return plugin;\n }\n /**\n * Run before taking all screenshots.\n */\n function beforeAll(context = {}) {\n getPlugins(context).forEach((plugin) => {\n if (plugin.beforeAll) {\n const cleanup = plugin.beforeAll(context, getPluginOptions(context, plugin.name));\n if (cleanup) beforeAllCleanups.add(cleanup);\n }\n });\n }\n /**\n * Run after taking all screenshots.\n */\n function afterAll() {\n beforeAllCleanups.forEach((cleanup) => {\n cleanup();\n });\n beforeAllCleanups.clear();\n }\n /**\n * Run before taking each screenshot (between viewport changes).\n */\n function beforeEach(context = {}) {\n getPlugins(context).forEach((plugin) => {\n if (plugin.beforeEach) {\n const cleanup = plugin.beforeEach(context, getPluginOptions(context, plugin.name));\n if (cleanup) beforeEachCleanups.add(cleanup);\n }\n });\n }\n /**\n * Run after taking each screenshot (between viewport changes).\n */\n function afterEach() {\n beforeEachCleanups.forEach((cleanup) => {\n cleanup();\n });\n beforeEachCleanups.clear();\n }\n /**\n * Get the stabilization state of the document.\n */\n function getStabilityState(context) {\n const stabilityState = {};\n getPlugins(context).forEach((plugin) => {\n if (plugin.wait) stabilityState[plugin.name] = plugin.wait.for(context, getPluginOptions(context, plugin.name));\n });\n return stabilityState;\n }\n /**\n * Wait for a condition to be met before taking a screenshot.\n */\n function waitFor(context) {\n const stabilityState = getStabilityState(context);\n return Object.values(stabilityState).every(Boolean);\n }\n /**\n * Get the error message to display if the condition is not met.\n */\n function getWaitFailureExplanations(options) {\n const stabilityState = getStabilityState(options);\n return Object.entries(stabilityState).filter(([, value]) => !value).map(([name]) => {\n const plugin = getPluginByName(name);\n if (!plugin.wait) throw new Error(`Invariant: plugin ${name} does not have a wait function`);\n return plugin.wait.failureExplanation;\n });\n }\n //#endregion\n //#region src/global/index.ts\n window.__ARGOS__ = {\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n waitFor,\n getWaitFailureExplanations,\n getColorScheme: () => getColorScheme(),\n getMediaType: () => getMediaType()\n };\n //#endregion\n})();\n"; } //#endregion export { getGlobalScript, resolveViewport };