import { afterEach, describe, expect, it } from "vitest"; import { ANNOUNCER_ROOT_ID, announce, ensureAnnouncerRegions, } from "./liveAnnouncer"; const NBSP = String.fromCharCode(0xa0); const root = () => document.getElementById(ANNOUNCER_ROOT_ID); const polite = () => document.getElementById(`${ANNOUNCER_ROOT_ID}-polite`); const assertive = () => document.getElementById(`${ANNOUNCER_ROOT_ID}-assertive`); afterEach(() => { root()?.remove(); }); describe("ensureAnnouncerRegions", () => { it("mounts both regions empty so assistive tech registers them before any message", () => { ensureAnnouncerRegions(); expect(polite()?.textContent).toBe(""); expect(assertive()?.textContent).toBe(""); }); it("is idempotent", () => { const first = ensureAnnouncerRegions(); const second = ensureAnnouncerRegions(); expect(first).toBe(second); expect(document.querySelectorAll(`#${ANNOUNCER_ROOT_ID}`)).toHaveLength(1); }); it("fixes politeness per region instead of swapping it on one node", () => { ensureAnnouncerRegions(); expect(polite()?.getAttribute("role")).toBe("status"); expect(polite()?.getAttribute("aria-live")).toBe("polite"); expect(assertive()?.getAttribute("role")).toBe("alert"); expect(assertive()?.getAttribute("aria-live")).toBe("assertive"); }); }); describe("announce", () => { it("writes polite messages to the polite region by default", () => { announce("Settings saved."); expect(polite()?.textContent).toBe("Settings saved."); expect(assertive()?.textContent).toBe(""); }); it("routes assertive messages to the assertive region and clears the polite one", () => { announce("Settings saved."); announce("Could not save settings.", "assertive"); expect(assertive()?.textContent).toBe("Could not save settings."); expect(polite()?.textContent).toBe(""); }); it("re-announces an identical consecutive message by alternating a trailing NBSP", () => { // A repeated "Settings saved." must still mutate the region"s text, or the // screen reader stays silent on every save after the first. announce("Settings saved."); const first = polite()?.textContent; announce("Settings saved."); const second = polite()?.textContent; announce("Settings saved."); const third = polite()?.textContent; expect(first).toBe("Settings saved."); expect(second).toBe(`Settings saved.${NBSP}`); expect(third).toBe("Settings saved."); expect(second).not.toBe(first); expect(third).not.toBe(second); }); it("alternates across regions too, so a repeated error still announces", () => { announce("Could not save settings.", "assertive"); const first = assertive()?.textContent; announce("Could not save settings.", "assertive"); const second = assertive()?.textContent; expect(first).toBe("Could not save settings."); expect(second).toBe(`Could not save settings.${NBSP}`); }); it("clears both regions on an empty message and forgets the last message", () => { announce("Settings saved."); announce(""); expect(polite()?.textContent).toBe(""); expect(assertive()?.textContent).toBe(""); announce("Settings saved."); expect(polite()?.textContent).toBe("Settings saved."); }); it("creates the regions on demand when nothing mounted them first", () => { expect(root()).toBeNull(); announce("Settings saved."); expect(root()).not.toBeNull(); expect(polite()?.textContent).toBe("Settings saved."); }); });