test(web): reduced motion disables all motion

Also turns off the fold height transition, which the universal selector does not reach (::details-content).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 20:55:32 +01:00
co-authored by Claude Opus 5.5
parent 370d381ff4
commit 6bb7681306
3 changed files with 74 additions and 0 deletions
+70
View File
@@ -249,4 +249,74 @@ run("navigation motion (headless Chrome)", () => {
await chrome.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
expect(await exists(".lr-menu")).toBe(false);
}, 40_000);
test("prefers-reduced-motion: no view transition, animation or transition anywhere, and everything still works", async () => {
await setReducedMotion(true);
const still = async (what: string) => {
await chrome.eval(afterFrame);
await Bun.sleep(250);
const r = await m();
expect([what, r.vt, r.anims, r.trans, await running()]).toEqual([what, 0, [], [], []]);
await reset();
};
try {
// library to course
await open("/", 'a[href="/c/sample"]');
await click('a[href="/c/sample"]');
await until(() => exists(".lr-main > article h1"), "course page");
await still("library to course");
// course to lesson, then Next: the old lesson is still held while the next loads, then swapped plainly
await click('.lr-les[href="/c/sample/m1-intro"]');
await until(async () => (await text(".lr-main .lr-lesson h1.title")) === "Introduction", "intro");
await still("course to lesson");
await chrome.eval("window.scrollTo(0, 900)");
await chrome.eval("window.__delay = 400");
await click(".stc-dock .stc-pill--primary");
await Bun.sleep(120);
expect(await text(".lr-main .lr-lesson h1.title")).toBe("Introduction");
await until(async () => (await text(".lr-main .lr-lesson h1.title")) === "Watch the clip", "next lesson");
await chrome.eval("window.__delay = 0");
expect(await chrome.eval<number>("scrollY")).toBe(0);
await still("lesson next");
// module open
await chrome.eval(`[...document.querySelectorAll(".lr-mod-head")][1].click(), true`);
await still("module open");
// fold: straight from closed to open
const heights = await chrome.eval<number[]>(`(() => {
const d = document.querySelector("details.fold.transcript");
d.querySelector("summary").click();
const out = [], t0 = performance.now();
return new Promise((r) => { const f = () => { out.push(parseFloat(getComputedStyle(d, "::details-content").height)); if (performance.now() - t0 < 300) requestAnimationFrame(f); else r(out); }; f(); });
})()`);
expect(new Set(heights).size).toBe(1);
await still("fold");
// slide change: flips without animating
await until(() => chrome.eval<boolean>(`document.querySelector(".stage canvas[data-front]")?.dataset.page === "1"`), "deck");
await click('.deckbar [aria-label="Next slide"]');
await until(() => chrome.eval<boolean>(`document.querySelector(".stage canvas[data-front]").dataset.page === "2"`), "slide 2");
await still("slide");
// mark done (progress fills), menu, toast
await click(".stc-dock button[aria-pressed]");
await Bun.sleep(400);
await still("progress");
await click(".lr-switch-btn");
await until(() => exists(".lr-menu"), "menu");
await still("menu");
await chrome.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
await chrome.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
await open("/c/sample/m2-ex-add", ".stc-seg button");
await chrome.eval(`[...document.querySelectorAll(".stc-seg button")].find((b) => b.textContent === "Reading").click(), true`);
await until(() => exists(".lr-toast"), "toast");
await still("toast");
await until(async () => !(await exists(".lr-toast")), "toast removed", 4000);
expect((await m()).anims).toEqual([]);
} finally {
await setReducedMotion(false);
}
}, 60_000);
});
+2
View File
@@ -97,4 +97,6 @@ code, pre { font-family: var(--mono); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
/* separate rule: browsers without ::details-content drop only this one */
*::details-content { transition: none !important; }
}
+2
View File
@@ -88,6 +88,8 @@ test("prefers-reduced-motion turns off every animation, transition and view tran
expect(block).toMatch(/::view-transition-group\(\*\)/);
expect(block).toMatch(/::view-transition-old\(\*\)/);
expect(block).toMatch(/::view-transition-new\(\*\)/);
// `*` does not reach ::details-content (fold heights), so it has its own rule.
expect(block).toMatch(/\*::details-content\s*\{\s*transition:\s*none !important/);
});
test("view transitions never take pointer input and keep the header and sidebar still", () => {