From 89083ac7900599bc2df1d0af378df9241aa6b053 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Thu, 20 Mar 2025 09:22:09 +0100 Subject: [PATCH] Stabilize visual tests (#3001) --- packages/gitbook/e2e/util.ts | 82 +++++++++++++++++-- packages/gitbook/playwright.config.ts | 3 + .../src/components/Search/SearchButton.tsx | 12 ++- packages/gitbook/tailwind.config.ts | 4 +- 4 files changed, 89 insertions(+), 12 deletions(-) diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts index c75eae4e8..a486b0468 100644 --- a/packages/gitbook/e2e/util.ts +++ b/packages/gitbook/e2e/util.ts @@ -191,21 +191,20 @@ export function runTestCases(testCases: TestsCase[]) { .intercom-lightweight-app { display: none !important; } - - /* Switch image rendering to pixelated */ - img { - image-rendering: pixelated; - } `, threshold: screenshotOptions?.threshold ?? undefined, fullPage: testEntry.fullPage ?? false, beforeScreenshot: async ({ runStabilization }) => { await runStabilization(); await waitForIcons(page); + await roundImageSizes(page); if (screenshotOptions?.waitForTOCScrolling !== false) { await waitForTOCScrolling(page); } }, + afterScreenshot: async () => { + await restoreImageSizes(page); + }, }); } }); @@ -326,7 +325,14 @@ async function waitForIcons(page: Page) { function loadImage(src: string) { return new Promise((resolve, reject) => { const img = new Image(); - img.onload = () => resolve(img); + img.onload = () => { + // Wait two frames to ensure the image has been rendered + requestAnimationFrame(() => { + requestAnimationFrame(() => { + resolve(true); + }); + }); + }; img.onerror = (_error) => reject(new Error(`Failed to load image: ${src}`)); img.src = src; }); @@ -348,6 +354,70 @@ async function waitForIcons(page: Page) { }); } +/** + * Take all images, measure them and set their width and height to rounded values. + */ +async function roundImageSizes(page: Page) { + await page.waitForFunction(async () => { + const images = Array.from(document.querySelectorAll('img')); + await Promise.all( + images.map(async (img) => { + return new Promise((resolve) => { + const setDimensions = () => { + // Mark it as stabilized + img.dataset.stabilized = 'true'; + // Preserve the original width and height + img.dataset.originalWidth = img.style.width ?? ''; + img.dataset.originalHeight = img.style.height ?? ''; + const rect = img.getBoundingClientRect(); + img.style.width = `${Math.round(rect.width)}px`; + img.style.height = `${Math.round(rect.height)}px`; + resolve(); + }; + + if (img.complete) { + setDimensions(); + } else { + const cleanup = () => { + img.removeEventListener('load', handleLoad); + img.removeEventListener('error', handleError); + }; + const handleError = () => { + cleanup(); + resolve(); + }; + const handleLoad = () => { + cleanup(); + setDimensions(); + }; + img.addEventListener('load', handleLoad); + img.addEventListener('error', handleError); + } + }); + }) + ); + return true; + }); +} + +/** + * Restore images to their original size. + */ +async function restoreImageSizes(page: Page) { + await page.evaluate(() => { + const images = Array.from(document.querySelectorAll('img[data-stabilized]')); + images.forEach((img) => { + if (img instanceof HTMLImageElement) { + img.style.width = img.dataset.originalWidth ?? ''; + img.style.height = img.dataset.originalHeight ?? ''; + delete img.dataset.originalWidth; + delete img.dataset.originalHeight; + delete img.dataset.stabilized; + } + }); + }); +} + /** * Wait for TOC to be correctly scrolled into view. */ diff --git a/packages/gitbook/playwright.config.ts b/packages/gitbook/playwright.config.ts index 1f79e89b8..849d9538f 100644 --- a/packages/gitbook/playwright.config.ts +++ b/packages/gitbook/playwright.config.ts @@ -18,5 +18,8 @@ export default defineConfig({ use: { trace: 'on-first-retry', screenshot: 'only-on-failure', + contextOptions: { + reducedMotion: 'reduce', + }, }, }); diff --git a/packages/gitbook/src/components/Search/SearchButton.tsx b/packages/gitbook/src/components/Search/SearchButton.tsx index 5244dbb60..ab8516d05 100644 --- a/packages/gitbook/src/components/Search/SearchButton.tsx +++ b/packages/gitbook/src/components/Search/SearchButton.tsx @@ -115,9 +115,15 @@ function Shortcut() { setOperatingSystem(getOperatingSystem()); }, []); - return operatingSystem ? ( + return ( - ) : ( - ); } diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts index 340978ab4..3efc2f510 100644 --- a/packages/gitbook/tailwind.config.ts +++ b/packages/gitbook/tailwind.config.ts @@ -297,8 +297,8 @@ const config: Config = { present: 'present .5s ease-out both', scaleIn: 'scaleIn 200ms ease', scaleOut: 'scaleOut 200ms ease', - fadeIn: 'fadeIn 200ms ease', - fadeOut: 'fadeOut 200ms ease', + fadeIn: 'fadeIn 200ms ease forwards', + fadeOut: 'fadeOut 200ms ease forwards', enterFromLeft: 'enterFromLeft 250ms ease', enterFromRight: 'enterFromRight 250ms ease', exitToLeft: 'exitToLeft 250ms ease',