// 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 // 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 import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { Chrome } from "../packages/feature-learner/test/chrome"; const [base = "http://localhost:3491", out = "motion-frames"] = process.argv.slice(2); const SLOW = 0.1; mkdirSync(out, { recursive: true }); 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"); const js = (s: string) => JSON.stringify(s); async function until(cond: string, ms = 8000) { const end = Date.now() + ms; while (!(await chrome.eval(cond).catch(() => false))) { if (Date.now() > end) throw new Error(`timed out: ${cond}`); await Bun.sleep(30); } } async function shot(name: string) { const r = await chrome.send("Page.captureScreenshot", { format: "png" }); writeFileSync(join(dir, `${name}.png`), Buffer.from(r.result.data, "base64")); } async function open(path: string, ready: string) { await chrome.goto(`${base}${path}`, 100); await until(`location.pathname === ${js(path)} && !!document.querySelector(${js(ready)})`); await Bun.sleep(500); } /** Runs `action` (page JS), then captures frames at the given real-time offsets (ms). */ async function frames(name: string, action: string, at: number[]) { await shot(`${name}-0-before`); const t0 = Date.now(); await chrome.eval(`(${action}, true)`); for (const [i, t] of at.entries()) { await Bun.sleep(Math.max(0, t - (Date.now() - t0))); await shot(`${name}-${i + 1}-${t}ms`); } } const flows: [string, () => Promise][] = [ ["01-library-to-course", async () => { await open("/", 'a[href="/c/sample"]'); await frames("01-library-to-course", `document.querySelector('a[href="/c/sample"]').click()`, [300, 700, 2500]); }], ["02-course-to-lesson", async () => { await open("/c/sample", ".lr-main > article h1"); await frames("02-course-to-lesson", `document.querySelector('.lr-les[href="/c/sample/m1-code"]').click()`, [300, 700, 2500]); }], ["03-lesson-next", async () => { 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]); }], ["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]); }], ["05-fold-open", async () => { await open("/c/sample/m1-video", "details.fold.transcript"); await chrome.eval(`document.querySelector("details.fold.transcript").scrollIntoView({ block: "center" })`); // ::details-content transitions are not slowed by the Animation domain; sample real time instead. await frames("05-fold-open", `document.querySelector("details.fold.transcript > summary").click()`, [80, 140, 1000]); }], ["06-slide-next", async () => { await open("/c/sample/m1-video", '.stage canvas[data-page="1"]'); await chrome.eval(`document.querySelector(".deck").scrollIntoView({ block: "center" })`); await Bun.sleep(300); await frames("06-slide-next", `document.querySelector('.deckbar [aria-label="Next slide"]').click()`, [600, 2500]); }], ["07-menu", async () => { await open("/c/sample/m1-intro", ".lr-switch-btn"); await frames("07-menu", `document.querySelector(".lr-switch-btn").click()`, [500, 2000]); }], ["08-toast", async () => { await open("/c/sample/m2-ex-add", ".stc-seg button"); await frames("08-toast", `[...document.querySelectorAll(".stc-seg button")].find((b) => b.textContent === "Reading").click()`, [500, 2000]); }], ]; try { await chrome.send("Animation.setPlaybackRate", { playbackRate: SLOW }); for (const [, run] of flows) await run(); // The same moments with reduced motion: the end state at once. await chrome.send("Animation.setPlaybackRate", { playbackRate: 1 }); await chrome.send("Emulation.setEmulatedMedia", { features: [{ name: "prefers-reduced-motion", value: "reduce" }] }); dir = join(out, "reduced"); mkdirSync(dir, { recursive: true }); for (const [name, run] of flows) await run().catch((e) => console.error(name, e)); } finally { await chrome.close(); } console.log(`frames written to ${out}`);