feat(welcome): reduce-motion toggle for morph transitions; decouple loop from it
Per operator: Speed only controls the resting loop, so reduce-motion is still needed to turn off the morph/transition videos. Re-add a visible 'Reduce motion' toggle on BOTH the welcome screen and the panel (mirrored, default from OS prefers-reduced-motion, remembers the choice). When ON, altitude changes are instant cuts instead of an animated morph tween (autoScrub's reduced branch); the resting loop is unaffected — it follows the Speed slider (0x freezes it). Decouples the loop from reduce-motion (was: main froze both videos + disabled the slider under OS-reduced). playLoop()/landing/applyPlaySpeed no longer check isReduced(); isReduced() now only gates the morph tween. Welcome controls are now Language / Speed / Audio / Reduce motion. i18n: restore rm.label (4 langs), reword warn.body to point at the on-screen control. Tests: a11y Task 5 reworked (speed independent of reduce-motion; new instant-cut behavior test); welcome defaults assert reduce-motion off. node 35, e2e 32, static 2, pytest 301 (3 pre-existing ffmpeg/cv2 env fails). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,12 +48,11 @@ test.describe("Task 4 — Photosensitivity notice", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => {
|
||||
test("the Reduce motion toggle is gone, replaced by a 0–4x speed slider", async ({ page }) => {
|
||||
test.describe("Task 5 — Playback speed slider + Reduce motion", () => {
|
||||
test("Speed is a 0–4x forward slider; the loop follows it (default 1x)", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.goto("/");
|
||||
await enter(page);
|
||||
await expect(page.locator("#reduce-motion")).toHaveCount(0);
|
||||
const slider = page.locator("#play-speed");
|
||||
await expect(slider).toBeVisible();
|
||||
await expect(slider).toHaveValue("1"); // default = normal speed
|
||||
@@ -81,14 +80,28 @@ test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () =>
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => {
|
||||
test("Reduce motion defaults from the OS setting and does NOT disable Speed", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto("/");
|
||||
// The welcome toggle reflects the OS preference before launch.
|
||||
await expect(page.locator("#welcome-reduce-motion")).toBeChecked();
|
||||
await enter(page);
|
||||
await expect(page.locator("#play-speed")).toBeDisabled();
|
||||
await expect
|
||||
.poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused))
|
||||
.toBe(true); // OS prefers-reduced-motion → frozen regardless of slider
|
||||
await expect(page.locator("#reduce-motion")).toBeChecked(); // panel mirrors it
|
||||
await expect(page.locator("#play-speed")).toBeEnabled(); // Speed is independent of reduce-motion
|
||||
});
|
||||
|
||||
test("Reduce motion turns altitude changes into instant cuts (no morph tween)", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.goto("/");
|
||||
await enter(page);
|
||||
await page.locator('label[for="reduce-motion"]').click(); // hidden checkbox → toggle via label
|
||||
await expect(page.locator("#reduce-motion")).toBeChecked();
|
||||
const start = (await page.locator("#scale-name").textContent())!;
|
||||
const t0 = await page.evaluate(() => performance.now());
|
||||
await page.locator("#stage").dispatchEvent("wheel", { deltaY: 60 }); // descend one altitude
|
||||
await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 5000 });
|
||||
const dt = (await page.evaluate(() => performance.now())) - t0;
|
||||
expect(dt).toBeLessThan(600); // instant cut, not the ~1200ms animated morph tween
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -25,12 +25,13 @@ test("welcome screen shows on load with the expected defaults; the panel is hidd
|
||||
await expect(page.locator("#welcome")).toBeVisible();
|
||||
await expect(page.locator("#welcome")).toHaveClass(/welcoming/);
|
||||
|
||||
// Defaults: Language English, Speed 1, Audio 2.
|
||||
// Defaults: Language English, Speed 1, Audio 2, Reduce motion off (no OS pref).
|
||||
await expect(page.locator("#welcome-lang")).toHaveValue("en");
|
||||
await expect(page.locator("#welcome-play-speed")).toHaveValue("1");
|
||||
await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×");
|
||||
await expect(page.locator("#welcome-audio")).toHaveValue("2");
|
||||
await expect(page.locator("#welcome-audio-val")).toHaveText("2");
|
||||
await expect(page.locator("#welcome-reduce-motion")).not.toBeChecked();
|
||||
|
||||
// Heads-up + Launch button present; "Loading Universe" progress present.
|
||||
await expect(page.locator("#welcome-title")).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user