From 558097ed70002ce63d0966dbd99f4ed7725e855e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 29 Oct 2026 14:34:07 +0000 Subject: [PATCH] fix(cli): check no longer calls a redrawn WebGL canvas a dead stretch (#6749) * fix(cli): check no longer calls a redrawn WebGL canvas a dead stretch * fix(cli): keep the seek comment under the line limit --- packages/cli/src/commands/check.test.ts | 14 +++++++++++++ .../src/utils/checkBrowser.chromium.test.ts | 20 ++++++++++++++++++- packages/cli/src/utils/checkBrowser.ts | 7 +++++-- packages/cli/src/utils/checkPipeline.ts | 4 ++-- packages/cli/src/utils/checkTypes.ts | 4 ++-- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/check.test.ts b/packages/cli/src/commands/check.test.ts index 1cbf482ea2..970fdb65a1 100644 --- a/packages/cli/src/commands/check.test.ts +++ b/packages/cli/src/commands/check.test.ts @@ -2841,6 +2841,20 @@ describe("check pipeline", () => { expect(checkExitCode(strict.report)).toBe(0); }); + it("seeks each craft frame with WebGL draws on, so a redrawn canvas reads as moving", async () => { + const driver = craftDriver(); + await runScenario(driver); + const painted = new Set( + vi + .mocked(driver.seekGeometry) + .mock.calls.flatMap(([time, seek]) => (seek?.draws ? [time] : [])), + ); + const sampled = vi.mocked(driver.collectCraftFrame!).mock.calls.map(([time]) => time); + + expect(sampled.length).toBeGreaterThan(0); + expect(sampled.every((time) => painted.has(time))).toBe(true); + }); + it("re-measures a short line between dense samples before judging it", async () => { const driver = craftDriver(); const { report } = await runScenario(driver); diff --git a/packages/cli/src/utils/checkBrowser.chromium.test.ts b/packages/cli/src/utils/checkBrowser.chromium.test.ts index b218ea1648..144380970b 100644 --- a/packages/cli/src/utils/checkBrowser.chromium.test.ts +++ b/packages/cli/src/utils/checkBrowser.chromium.test.ts @@ -3,7 +3,7 @@ import puppeteer, { type Browser, type Page } from "puppeteer-core"; import { createRequire } from "node:module"; import { resolve } from "node:path"; import { measuredInMotion, readContrastMotion } from "./contrastMotion.js"; -import { collectSeekClock } from "./checkBrowser.js"; +import { collectSeekClock, createPageDriver } from "./checkBrowser.js"; const executablePath = process.env.PUPPETEER_EXECUTABLE_PATH; @@ -119,6 +119,24 @@ describe.runIf(executablePath)("collectSeekClock in Chromium", () => { await page.close(); }); + it("skips a seek's WebGL draws unless the caller asks to paint them", async () => { + page = await browser.newPage(); + await page.setContent(``); + const driver = createPageDriver(page); + const drawn = () => page.evaluate(() => Reflect.get(window, "draws")); + + await driver.seekGeometry(1); + expect(await drawn()).toBe(0); + await driver.seekGeometry(2, { draws: true }); + expect(await drawn()).toBe(1); + await page.close(); + }); + it("reports nothing on a page with no animations", async () => { page = await browser.newPage(); await page.setContent("

Hello

"); diff --git a/packages/cli/src/utils/checkBrowser.ts b/packages/cli/src/utils/checkBrowser.ts index 924a976c8e..98ee3b7adc 100644 --- a/packages/cli/src/utils/checkBrowser.ts +++ b/packages/cli/src/utils/checkBrowser.ts @@ -969,9 +969,12 @@ export function createPageDriver( screenshotFollows ? SCREENSHOT_SEEK_OPTIONS : AUDIT_SEEK_OPTIONS, ); }, - seekGeometry: async (time) => { + seekGeometry: async (time, { draws = false } = {}) => { clock.at(time); - await seekCompositionTimeline(page, time, DENSE_GEOMETRY_SEEK_OPTIONS); + const options = draws + ? { ...DENSE_GEOMETRY_SEEK_OPTIONS, skipWebglDraws: false } + : DENSE_GEOMETRY_SEEK_OPTIONS; + await seekCompositionTimeline(page, time, options); }, collectLayout: (time, tolerance, layout) => collectLayout(page, time, tolerance, layout), collectOverlap: (time) => collectOverlap(page, time), diff --git a/packages/cli/src/utils/checkPipeline.ts b/packages/cli/src/utils/checkPipeline.ts index 1730884676..7ee085a0b2 100644 --- a/packages/cli/src/utils/checkPipeline.ts +++ b/packages/cli/src/utils/checkPipeline.ts @@ -759,7 +759,7 @@ async function collectMotionOverlapSamples( const craftTimes = buildCraftSampleTimes(grid.duration).filter((time) => time < grid.duration); for (const [index, time] of craftTimes.entries()) { progress(`craft frame at ${time}s`); - await driver.seekGeometry(time); + await driver.seekGeometry(time, { draws: true }); collected.craftFrames.push(...(await collectCraftFrame(driver, time))); progress(); report("craft", index + 1, craftTimes.length); @@ -1583,7 +1583,7 @@ export async function runAuditGrid( noTimeline || sweepFindings.length > 0 || wholeCompositionKeepsMoving(plan.assertions), }, async (time) => { - await driver.seekGeometry(time); + await driver.seekGeometry(time, { draws: true }); if (!driver.collectCraftFrame) throw new Error("craft sampling unavailable"); return driver.collectCraftFrame(time); }, diff --git a/packages/cli/src/utils/checkTypes.ts b/packages/cli/src/utils/checkTypes.ts index a01956b3b3..33ef1c8141 100644 --- a/packages/cli/src/utils/checkTypes.ts +++ b/packages/cli/src/utils/checkTypes.ts @@ -262,8 +262,8 @@ export interface CheckAuditDriver { getCanvas(): Promise; findAmbiguousSelectors(selectors: string[]): Promise; seek(time: number, screenshotFollows?: boolean): Promise; - /** Settle-free seek for the geometry-only dense content_overlap pass; only collectOverlap consumes it, and getBoundingClientRect is valid synchronously after setTime. */ - seekGeometry(time: number): Promise; + /** Settle-free seek for the dense passes; WebGL draws are skipped unless `draws` (a pass reading canvas pixels). */ + seekGeometry(time: number, options?: { draws?: boolean }): Promise; collectLayout( time: number, tolerance: number,