diff --git a/packages/web-shell/client/e2e/visuals/harness.spec.ts b/packages/web-shell/client/e2e/visuals/harness.spec.ts
new file mode 100644
index 00000000000..0e9e596cb4d
--- /dev/null
+++ b/packages/web-shell/client/e2e/visuals/harness.spec.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Copyright 2025 Qwen
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { expect, test } from '@playwright/test';
+import { freezeLoopingAnimations } from './harness';
+
+// `freezeLoopingAnimations` is the load-bearing step that keeps spinner-bearing
+// captures deterministic (see its docstring). It runs only implicitly via
+// `captureScreenshot`, so pin its contract explicitly here: an infinite
+// animation must be paused and rewound to time 0, while a finite one must be
+// left alone for Playwright's own `animations: 'disabled'` to settle.
+test('freezeLoopingAnimations pins infinite animations to frame 0 and leaves finite ones', async ({
+ page,
+}) => {
+ await page.setContent(`
+
+
+
+ `);
+ // Advance both animations past frame 0 first, so a freeze that did nothing
+ // would leave a non-zero currentTime and fail the assertion below.
+ await page.waitForTimeout(100);
+
+ await freezeLoopingAnimations(page);
+
+ const state = await page.evaluate(
+ /* global document */
+ () => {
+ const animOf = (id: string) => {
+ const el = document.getElementById(id);
+ if (!el) throw new Error(`element #${id} not found`);
+ return el.getAnimations()[0];
+ };
+ const loop = animOf('loop');
+ return {
+ loopPlayState: loop.playState,
+ loopCurrentTime: Number(loop.currentTime),
+ oncePlayState: animOf('once').playState,
+ };
+ },
+ );
+
+ // The infinite loop is paused at its first frame…
+ expect(state.loopPlayState).toBe('paused');
+ expect(state.loopCurrentTime).toBe(0);
+ // …while the finite animation is untouched, still running toward completion.
+ expect(state.oncePlayState).toBe('running');
+});
diff --git a/packages/web-shell/client/e2e/visuals/harness.ts b/packages/web-shell/client/e2e/visuals/harness.ts
index 21a5dcca635..a7bf5273d20 100644
--- a/packages/web-shell/client/e2e/visuals/harness.ts
+++ b/packages/web-shell/client/e2e/visuals/harness.ts
@@ -140,12 +140,48 @@ export async function captureScreenshot(
name: string,
): Promise {
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
+ await freezeLoopingAnimations(page);
await page.screenshot({
path: join(SCREENSHOTS_DIR, `${name}.png`),
animations: 'disabled',
});
}
+/**
+ * Pin looping animations to their first frame before a capture. Playwright's
+ * `animations: 'disabled'` settles finite animations and is meant to reset
+ * infinite ones, but a GPU-composited transform loop — e.g. the sidebar's
+ * rotating activity spinner — is still captured mid-rotation at a random angle.
+ * That angle differs between the base and head render passes, so the view reads
+ * as "changed" against the 0.02% before/after threshold even when nothing did.
+ * Pausing the infinite Web Animations and rewinding them to time 0 pins them to
+ * a deterministic frame (verified: sidebar-attention drops from ~0.12% of pixels
+ * differing between identical renders to 0); a two-frame wait lets the compositor
+ * commit that frame before the capture reads it.
+ *
+ * Scope: this covers WAAPI and CSS `@keyframes` animations — everything
+ * `document.getAnimations()` reports. A spinner hand-rolled on a
+ * `requestAnimationFrame` loop instead would NOT be caught, and the flake would
+ * silently return; if a spinner reimplementation ever reintroduces it, this is
+ * the function to extend. `harness.spec.ts` pins the pause/rewind contract.
+ */
+export async function freezeLoopingAnimations(page: Page): Promise {
+ await page.evaluate(
+ /* global document, requestAnimationFrame */
+ async () => {
+ for (const animation of document.getAnimations()) {
+ if (animation.effect?.getTiming().iterations === Infinity) {
+ animation.pause();
+ animation.currentTime = 0;
+ }
+ }
+ await new Promise((resolve) =>
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
+ );
+ },
+ );
+}
+
/**
* Record a continuous flow to `