@argos-ci/browser
Version:
Browser utilities to stabilize visual testing with Argos.
86 lines (84 loc) • 16.5 kB
JavaScript
// src/viewport.ts
var 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-11": { width: 1366, height: 768 },
"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": { 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-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;
}
// src/script.ts
function getGlobalScript() {
return '"use strict";\n(() => {\n // src/global/media.ts\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 function getMediaType() {\n return window.matchMedia("print").matches ? "print" : "screen";\n }\n\n // src/global/stabilization/plugins/addArgosClass.ts\n var plugin = {\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\n // src/global/stabilization/util.ts\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 style2 = document.getElementById(`argos-${id}`);\n if (style2) {\n style2.remove();\n }\n };\n }\n\n // src/global/stabilization/plugins/addArgosCSS.ts\n var plugin2 = {\n name: "addArgosCSS",\n beforeAll(options) {\n if (options.argosCSS) {\n return injectGlobalStyles(options.argosCSS, "custom-css");\n }\n return void 0;\n }\n };\n\n // src/global/stabilization/plugins/argosHelpers.ts\n var plugin3 = {\n name: "argosHelpers",\n beforeAll() {\n const styles = `\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 `;\n return injectGlobalStyles(styles, "argos-helpers");\n }\n };\n\n // src/global/stabilization/plugins/disableSpellCheck.ts\n var UNSET = "--unset";\n var BACKUP_ATTRIBUTE = "data-argos-bck-spellcheck";\n var plugin4 = {\n name: "disableSpellcheck",\n beforeAll() {\n document.querySelectorAll(\n "[contenteditable]:not([contenteditable=false]), input, textarea"\n ).forEach((element) => {\n const spellcheck = element.getAttribute("spellcheck");\n if (spellcheck === "false") {\n return;\n }\n element.setAttribute(BACKUP_ATTRIBUTE, spellcheck ?? UNSET);\n element.setAttribute("spellcheck", "false");\n });\n return () => {\n document.querySelectorAll(`[${BACKUP_ATTRIBUTE}]`).forEach((input) => {\n const bckSpellcheck = input.getAttribute(BACKUP_ATTRIBUTE);\n if (bckSpellcheck === UNSET) {\n input.removeAttribute("spellcheck");\n } else if (bckSpellcheck) {\n input.setAttribute("spellcheck", bckSpellcheck);\n }\n input.removeAttribute(BACKUP_ATTRIBUTE);\n });\n };\n }\n };\n\n // src/global/stabilization/plugins/fontAntialiasing.ts\n var plugin5 = {\n name: "fontAntialiasing",\n beforeAll() {\n return injectGlobalStyles(\n `* { -webkit-font-smoothing: antialiased !important; }`,\n "font-antialiasing"\n );\n }\n };\n\n // src/global/stabilization/plugins/hideCarets.ts\n var plugin6 = {\n name: "hideCarets",\n beforeAll() {\n return injectGlobalStyles(\n `* { caret-color: transparent !important; }`,\n "hide-carets"\n );\n }\n };\n\n // src/global/stabilization/plugins/hideScrollbars.ts\n var plugin7 = {\n name: "hideScrollbars",\n beforeAll() {\n return injectGlobalStyles(\n `::-webkit-scrollbar { display: none !important; }`,\n "hide-scrollbars"\n );\n }\n };\n\n // src/global/stabilization/plugins/loadImageSrcset.ts\n var plugin8 = {\n name: "loadImageSrcset",\n beforeEach(options) {\n if (!options.viewports || options.viewports.length === 0) {\n return void 0;\n }\n function getLargestSrcFromSrcset(srcset) {\n const sources = srcset.split(",").map((item) => {\n const [url, size] = item.trim().split(/\\s+/);\n if (!url) {\n return null;\n }\n const widthMatch = size && size.match(/^(\\d+)w$/);\n if (!widthMatch) {\n return { url, width: 0 };\n }\n const width = parseInt(widthMatch[1], 10);\n return { url, width };\n }).filter((x) => x !== null);\n if (sources.length === 0) {\n return srcset;\n }\n const largest = sources.reduce(\n (max, curr) => curr.width > max.width ? curr : max\n );\n return largest.url;\n }\n function forceSrcsetReload(img) {\n const srcset = img.getAttribute("srcset");\n if (!srcset) {\n return;\n }\n img.setAttribute("srcset", getLargestSrcFromSrcset(srcset));\n }\n Array.from(document.querySelectorAll("img,source")).forEach(\n forceSrcsetReload\n );\n }\n };\n\n // src/global/stabilization/plugins/roundImageSize.ts\n var BACKUP_ATTRIBUTE_WIDTH = "data-argos-bck-width";\n var BACKUP_ATTRIBUTE_HEIGHT = "data-argos-bck-height";\n var plugin9 = {\n name: "roundImageSize",\n beforeEach() {\n Array.from(document.images).forEach((img) => {\n if (!img.complete) {\n return;\n }\n img.setAttribute(BACKUP_ATTRIBUTE_WIDTH, img.style.width);\n img.setAttribute(BACKUP_ATTRIBUTE_HEIGHT, img.style.height);\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 const bckWidth = img.getAttribute(BACKUP_ATTRIBUTE_WIDTH);\n const bckHeight = img.getAttribute(BACKUP_ATTRIBUTE_HEIGHT);\n if (bckWidth === null && bckHeight === null) {\n return;\n }\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\n // src/global/stabilization/plugins/stabilizeSticky.ts\n var BACKUP_ATTRIBUTE2 = "data-argos-bck-position";\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_ATTRIBUTE2, previousPosition ?? "unset");\n }\n var plugin10 = {\n name: "stabilizeSticky",\n beforeAll(options) {\n if (!options.fullPage) {\n return void 0;\n }\n document.querySelectorAll("*").forEach((element) => {\n if (!(element instanceof HTMLElement)) {\n return;\n }\n if (element.tagName === "IFRAME") {\n return;\n }\n const style = window.getComputedStyle(element);\n const { position } = style;\n if (position === "fixed") {\n setAndBackupPosition(element, "absolute");\n } else if (position === "sticky") {\n setAndBackupPosition(element, "relative");\n }\n });\n return () => {\n document.querySelectorAll(`[${BACKUP_ATTRIBUTE2}]`).forEach((element) => {\n if (!(element instanceof HTMLElement)) {\n return;\n }\n const position = element.getAttribute(BACKUP_ATTRIBUTE2);\n if (!position) {\n return;\n }\n if (position === "unset") {\n element.style.removeProperty("position");\n } else {\n element.style.position = position;\n }\n element.removeAttribute(BACKUP_ATTRIBUTE2);\n });\n };\n }\n };\n\n // src/global/stabilization/plugins/waitForAriaBusy.ts\n function checkIsElementVisible(element) {\n if (element instanceof HTMLElement && (element.offsetHeight !== 0 || element.offsetWidth !== 0)) {\n return true;\n }\n return element.getClientRects().length > 0;\n }\n var plugin11 = {\n name: "waitForAriaBusy",\n wait: {\n for: () => {\n return Array.from(document.querySelectorAll(\'[aria-busy="true"]\')).every(\n (element) => !checkIsElementVisible(element)\n );\n },\n failureExplanation: "Some elements still have `aria-busy=\'true\'`"\n }\n };\n\n // src/global/stabilization/plugins/waitForFonts.ts\n var plugin12 = {\n name: "waitForFonts",\n wait: {\n for: () => {\n return document.fonts.status === "loaded";\n },\n failureExplanation: "Some fonts have not been loaded"\n }\n };\n\n // src/global/stabilization/plugins/waitForImages.ts\n var plugin13 = {\n name: "waitForImages",\n beforeEach() {\n Array.from(document.images).every((img) => {\n if (img.decoding !== "sync") {\n img.decoding = "sync";\n }\n if (img.loading !== "eager") {\n img.loading = "eager";\n }\n });\n return void 0;\n },\n wait: {\n for: () => {\n const images = Array.from(document.images);\n const results = images.map((img) => {\n if (img.decoding !== "sync") {\n img.decoding = "sync";\n }\n if (img.loading !== "eager") {\n img.loading = "eager";\n }\n return img.complete;\n });\n return results.every((x) => x);\n },\n failureExplanation: "Some images have not been loaded"\n }\n };\n\n // src/global/stabilization/plugins/index.ts\n var corePlugins = [plugin, plugin2, plugin3];\n var plugins = [\n plugin4,\n plugin5,\n plugin6,\n plugin7,\n plugin8,\n plugin9,\n plugin10,\n plugin11,\n plugin12,\n plugin13\n ];\n\n // src/global/stabilization/index.ts\n var beforeAllCleanups = /* @__PURE__ */ new Set();\n var beforeEachCleanups = /* @__PURE__ */ new Set();\n function getPlugins(context) {\n const enabledPlugins = plugins.filter((plugin14) => {\n if (context.options === false) {\n return false;\n }\n if (typeof context.options === "object") {\n const pluginEnabled = context.options[plugin14.name];\n if (pluginEnabled === false) {\n return false;\n }\n }\n return true;\n });\n return [...corePlugins, ...enabledPlugins];\n }\n function getPluginByName(name) {\n const plugin14 = plugins.find((p) => p.name === name);\n if (!plugin14) {\n throw new Error(`Invariant: plugin ${name} not found`);\n }\n return plugin14;\n }\n function beforeAll(context = {}) {\n getPlugins(context).forEach((plugin14) => {\n if (plugin14.beforeAll) {\n const cleanup = plugin14.beforeAll(context);\n if (cleanup) {\n beforeAllCleanups.add(cleanup);\n }\n }\n });\n }\n function afterAll() {\n beforeAllCleanups.forEach((cleanup) => {\n cleanup();\n });\n beforeAllCleanups.clear();\n }\n function beforeEach(context = {}) {\n getPlugins(context).forEach((plugin14) => {\n if (plugin14.beforeEach) {\n const cleanup = plugin14.beforeEach(context);\n if (cleanup) {\n beforeEachCleanups.add(cleanup);\n }\n }\n });\n }\n function afterEach() {\n beforeEachCleanups.forEach((cleanup) => {\n cleanup();\n });\n beforeEachCleanups.clear();\n }\n function getStabilityState(context) {\n const stabilityState = {};\n getPlugins(context).forEach((plugin14) => {\n if (plugin14.wait) {\n stabilityState[plugin14.name] = plugin14.wait.for(context);\n }\n });\n return stabilityState;\n }\n function waitFor(context) {\n const stabilityState = getStabilityState(context);\n return Object.values(stabilityState).every(Boolean);\n }\n function getWaitFailureExplanations(options) {\n const stabilityState = getStabilityState(options);\n const failedPlugins = Object.entries(stabilityState).filter(\n ([, value]) => !value\n );\n return failedPlugins.map(([name]) => {\n const plugin14 = getPluginByName(name);\n if (!plugin14.wait) {\n throw new Error(\n `Invariant: plugin ${name} does not have a wait function`\n );\n }\n return plugin14.wait.failureExplanation;\n });\n }\n\n // src/global/index.ts\n var ArgosGlobal = {\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n waitFor,\n getWaitFailureExplanations,\n getColorScheme: () => getColorScheme(),\n getMediaType: () => getMediaType()\n };\n window.__ARGOS__ = ArgosGlobal;\n})();\n';
}
export {
getGlobalScript,
resolveViewport
};