From 265096995f02cd67bf03696d006957181aefafc6 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 14 Oct 2026 19:55:38 -0500 Subject: [PATCH] test: capture corner-smoothing pages until a frame matches (#54947) The corner-smoothing tests captured the page one animation frame after showing the window. The window paints while it is still hidden and loading the page, and the frame painted for that animation frame can reach the compositor after the capture has been taken, so the capture can return a frame of a partly parsed page or a partly loaded image. Every capture saved by failing Linux ASan and Windows CI runs shows such a frame, with an average difference of 7.6 to 216 from the reference. Capture a new frame, one animation frame apart, until the page matches the reference or five seconds pass. The page is static, so the captures converge on its final rendering. The tolerance and the reference images are unchanged: a page rendered with the feature in the wrong state differs from the reference by 5.73 and still fails. Correct the tolerance comment, which gave 7.3 for that difference, and report the measured difference and the number of captures in the failure message. Co-authored-by: Claude Opus 5.5 --- spec/api-corner-smoothing.spec.ts | 54 +++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/spec/api-corner-smoothing.spec.ts b/spec/api-corner-smoothing.spec.ts index bc579439d3..711d8b69d5 100644 --- a/spec/api-corner-smoothing.spec.ts +++ b/spec/api-corner-smoothing.spec.ts @@ -16,16 +16,23 @@ const FIXTURE_PATH = path.resolve(import.meta.dirname, 'fixtures', 'api', 'corne * is the cutoff for whether two images "match" or not. * * From testing, matching images were found to have an average global difference - * up to about 1.3 and mismatched images were found to have a difference of at - * least about 7.3. + * up to about 1.3. The shape page rendered with the feature in the other state + * differs from its reference by about 5.7, so the tolerance must stay well + * below that. * - * See the documentation on `compareImages` for more information. + * See the documentation on `getAverageImageDifference` for more information. */ const COMPARISON_TOLERANCE = 2.5; /** - * Compares the average global difference of two images to determine if they - * are similar enough to "match" each other. + * How long to keep capturing new frames of the page while they do not match + * the expected image. See `pageCaptureTestRecipe`. + */ +const CAPTURE_TIMEOUT = 5000; + +/** + * Returns the average global difference of two images, which determines if + * they are similar enough to "match" each other. * * "Average global difference" is the average difference of pixel components * (RGB each) across an entire image. @@ -33,7 +40,7 @@ const COMPARISON_TOLERANCE = 2.5; * The cutoff for matching/not-matching is defined by the `COMPARISON_TOLERANCE` * constant. */ -function compareImages(img1: NativeImage, img2: NativeImage): boolean { +function getAverageImageDifference(img1: NativeImage, img2: NativeImage): number { expect( img1.getSize(), 'Cannot compare images with different sizes. Run tests with --force-device-scale-factor=1' @@ -55,8 +62,7 @@ function compareImages(img1: NativeImage, img2: NativeImage): boolean { } } - const avgDiff = totalDiff / (width * height); - return avgDiff <= COMPARISON_TOLERANCE; + return totalDiff / (width * height); } /** @@ -83,22 +89,36 @@ async function pageCaptureTestRecipe( await w.loadFile(pagePath); w.show(); - // Wait for a render frame to prepare the page. - await w.webContents.executeJavaScript('new Promise((resolve) => { requestAnimationFrame(() => resolve()); })'); - - const actualImg = await w.webContents.capturePage(); - expect(actualImg.isEmpty(), 'Failed to capture page image').toBe(false); - const expectedImg = nativeImage.createFromPath(expectedImgPath); expect(expectedImg.isEmpty(), 'Failed to read expected reference image').toBe(false); - const matches = compareImages(actualImg, expectedImg); - if (!matches) { + // The window paints while it is hidden and the page is still loading. + // One animation frame after showing the window, the renderer may not have + // submitted the frame for the loaded page yet, so a capture can still get + // a frame of a partly parsed page or a partly loaded image. Capture new + // frames until one matches the reference or time runs out. + // The page is static, so captures converge on its final rendering. + const deadline = Date.now() + CAPTURE_TIMEOUT; + let actualImg: NativeImage; + let difference: number; + let captures = 0; + do { + // Wait for a render frame to prepare the page. + await w.webContents.executeJavaScript('new Promise((resolve) => { requestAnimationFrame(() => resolve()); })'); + + actualImg = await w.webContents.capturePage(); + captures++; + expect(actualImg.isEmpty(), 'Failed to capture page image').toBe(false); + + difference = getAverageImageDifference(actualImg, expectedImg); + } while (difference > COMPARISON_TOLERANCE && Date.now() < deadline); + + if (difference > COMPARISON_TOLERANCE) { const artifactFileName = `corner-rounding-expected-${artifactName}.png`; await createArtifact(artifactFileName, actualImg.toPNG()); assert.fail( - `Actual image did not match expected reference image. Actual: "${artifactFileName}" in artifacts, Expected: "${path.relative( + `Actual image did not match expected reference image (average difference ${difference.toFixed(2)} after ${captures} capture${captures === 1 ? '' : 's'}, tolerance ${COMPARISON_TOLERANCE}). Actual: "${artifactFileName}" in artifacts, Expected: "${path.relative( path.resolve(import.meta.dirname, '..'), expectedImgPath )}" in source`