diff --git a/simulator/e2e/tests/a11y.spec.ts b/simulator/e2e/tests/a11y.spec.ts index 23962a4..3d64f65 100644 --- a/simulator/e2e/tests/a11y.spec.ts +++ b/simulator/e2e/tests/a11y.spec.ts @@ -78,3 +78,22 @@ test.describe("Task 6 — Keyboard + ARIA dial", () => { await expect(dial).toHaveAttribute("aria-valuenow", "0"); }); }); + +test.describe("Task 7 — Screen-reader narration", () => { + test("an aria-live region narrates the current scale", async ({ page }) => { + await page.goto("/"); + const live = page.locator("#sr-status"); + await expect(live).toHaveAttribute("aria-live", "polite"); + await page.locator("#dial").focus(); + await page.locator("#dial").press("Home"); + await expect + .poll(async () => (await live.textContent())?.toLowerCase()) + .toContain("cosmos"); + }); + + test("decorative overlay SVGs are hidden from assistive tech", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("#overlay")).toHaveAttribute("aria-hidden", "true"); + await expect(page.locator("#affect")).toHaveAttribute("aria-hidden", "true"); + }); +}); diff --git a/simulator/static/app.js b/simulator/static/app.js index b9bc60d..f28f487 100644 --- a/simulator/static/app.js +++ b/simulator/static/app.js @@ -593,9 +593,28 @@ function renderScaleReadout() { const scaleName = HEFi18n.pickUiString("scale." + s.id, activeLang); $("scale-name").textContent = `${scaleName} · ${member} (${ringIndex + 1}/${ring.scales.length}${poolTag})`; renderDial(); + announceState(); // narrate the new altitude (scale + clip) for screen readers refreshDevClip(); // keep the Dev Mode pool picker + clip data in sync with the landing } +// Turn the visual alteration into words for assistive tech: the current scale plus +// the clip you've landed on. Deduped so a settle on the same state stays quiet. +let lastAnnounced = ""; +function announce(text) { + const el = $("sr-status"); + if (!el || !text || text === lastAnnounced) return; + lastAnnounced = text; + el.textContent = text; +} +function announceState() { + if (!ring) return; + const s = ring.scales[ringIndex]; + const scaleName = s ? HEFi18n.pickUiString("scale." + s.id, activeLang) : ""; + const clip = activeClip(); + const title = clip && clip.title ? clip.title : ""; + announce(title ? `${scaleName}. ${title}` : scaleName); +} + function controls() { // Video (on/off) and Audio (on/off) are orthogonal toggles (audio spec §2). // Audio on = the per-altitude soundtrack (white-noise is deferred). One bipolar diff --git a/simulator/static/index.html b/simulator/static/index.html index b8e0b1d..bfe4d8c 100644 --- a/simulator/static/index.html +++ b/simulator/static/index.html @@ -24,10 +24,10 @@ - -
- - + + + +