118 lines
5.8 KiB
TypeScript
118 lines
5.8 KiB
TypeScript
// 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, 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
|
|
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");
|
|
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) {
|
|
const end = Date.now() + ms;
|
|
while (!(await chrome.eval<boolean>(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<void>][] = [
|
|
["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]);
|
|
}],
|
|
["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]);
|
|
}],
|
|
["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}`);
|