test(web): deterministic motion e2e (fetch gate, condition waits) and new frame capture

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 21:49:49 +01:00
co-authored by Claude Opus 5.5
parent b5e00af4f7
commit 4379d77b9c
2 changed files with 172 additions and 133 deletions
+20 -2
View File
@@ -1,6 +1,6 @@
// Captures before / mid / after frames of each navigation motion from a running server, for visual review.
// Animations are slowed 10x through the DevTools Animation domain (view transitions included), so a screenshot
// taken a few hundred ms in lands mid-transition. A second pass with prefers-reduced-motion shows the same
// Animations are slowed 10x through the DevTools Animation domain, so a screenshot taken a few hundred ms in
// lands mid-entrance. Lesson fetches can be held (window.__gate) to show the loading line. A second pass with prefers-reduced-motion shows the same
// moment with no motion. Uses the sample course; sign-up creates a throwaway account on that server.
//
// bun scripts/motion-frames.ts http://localhost:3491 /tmp/frames
@@ -16,6 +16,15 @@ let dir = out;
const chrome = await Chrome.launch();
await chrome.send("Emulation.setDeviceMetricsOverride", { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false });
await chrome.send("Animation.enable");
await chrome.send("Page.addScriptToEvaluateOnNewDocument", { source: `
const waiting = [];
window.__release = () => { window.__gate = false; waiting.splice(0).forEach((r) => r()); };
const f = window.fetch;
window.fetch = function (input, init) {
const u = String((input && input.url) || input);
if (window.__gate && u.includes("/lessons/")) return new Promise((r) => waiting.push(r)).then(() => f.call(this, input, init));
return f.call(this, input, init);
};` });
const js = (s: string) => JSON.stringify(s);
async function until(cond: string, ms = 8000) {
@@ -58,6 +67,15 @@ const flows: [string, () => Promise<void>][] = [
await open("/c/sample/m1-code", ".lr-main .lr-lesson h1.title");
await frames("03-lesson-next", `document.querySelector(".stc-dock .stc-pill--primary").click()`, [300, 700, 2500]);
}],
["03b-lesson-loading", async () => {
// The next lesson held back: the column is empty at once and the loading line shows after 150 ms.
await open("/c/sample/m1-intro", ".lr-main .lr-lesson h1.title");
await chrome.eval("window.__gate = true");
await frames("03b-lesson-loading", `document.querySelector(".stc-dock .stc-pill--primary").click()`, [80, 600]);
await chrome.eval("window.__release()");
await Bun.sleep(400);
await shot("03b-lesson-loading-3-released-400ms");
}],
["04-module-open", async () => {
await open("/c/sample/m1-intro", ".lr-main .lr-lesson h1.title");
await frames("04-module-open", `[...document.querySelectorAll(".lr-mod-head")][1].click()`, [500, 2500]);