|
|
@@ -0,0 +1,124 @@
|
|
|
+import { describe, it, expect, afterEach } from "vitest";
|
|
|
+import { render, cleanup } from "@testing-library/react";
|
|
|
+import { RollAnnouncer } from "@/components/dice/roll-announcer";
|
|
|
+import type { Movie } from "@/types/movie";
|
|
|
+
|
|
|
+afterEach(cleanup);
|
|
|
+
|
|
|
+function makeMovie(overrides: Partial<Movie> = {}): Movie {
|
|
|
+ return {
|
|
|
+ id: "movie-1",
|
|
|
+ group_id: "group-1",
|
|
|
+ tmdb_id: 1,
|
|
|
+ title: "Test Movie",
|
|
|
+ year: 2024,
|
|
|
+ poster_path: null,
|
|
|
+ genres: [],
|
|
|
+ trailer_url: null,
|
|
|
+ trailer_url_refreshed_at: null,
|
|
|
+ metadata_refreshed_at: null,
|
|
|
+ added_by: null,
|
|
|
+ watched: false,
|
|
|
+ watched_at: null,
|
|
|
+ added_at: "2024-01-01T00:00:00Z",
|
|
|
+ ...overrides,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+describe("RollAnnouncer", () => {
|
|
|
+ it("renders 'Rolling…' when state is 'rolling'", () => {
|
|
|
+ const { container } = render(
|
|
|
+ <RollAnnouncer state="rolling" winner={null} />,
|
|
|
+ );
|
|
|
+ expect(container.textContent).toBe("Rolling…");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("renders 'Rolled <title> (<year>)' when state is 'complete' with a winner", () => {
|
|
|
+ const winner = makeMovie({ title: "Inception", year: 2010 });
|
|
|
+ const { container } = render(
|
|
|
+ <RollAnnouncer state="complete" winner={winner} />,
|
|
|
+ );
|
|
|
+ expect(container.textContent).toBe("Rolled Inception (2010)");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("renders empty when state is 'idle'", () => {
|
|
|
+ const { container } = render(<RollAnnouncer state="idle" winner={null} />);
|
|
|
+ expect(container.textContent).toBe("");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("renders empty when state is 'complete' but no winner", () => {
|
|
|
+ const { container } = render(
|
|
|
+ <RollAnnouncer state="complete" winner={null} />,
|
|
|
+ );
|
|
|
+ expect(container.textContent).toBe("");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("uses an sr-only live region with aria-live='polite' and aria-atomic='true'", () => {
|
|
|
+ const { container } = render(
|
|
|
+ <RollAnnouncer state="rolling" winner={null} />,
|
|
|
+ );
|
|
|
+ const region = container.querySelector("[aria-live]");
|
|
|
+ expect(region).not.toBeNull();
|
|
|
+ expect(region?.getAttribute("aria-live")).toBe("polite");
|
|
|
+ expect(region?.getAttribute("aria-atomic")).toBe("true");
|
|
|
+ expect(region?.className).toContain("sr-only");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("increments the announce-id counter on each transition into 'complete' (forces re-announce)", () => {
|
|
|
+ const first = makeMovie({ id: "a", title: "First", year: 2001 });
|
|
|
+ const second = makeMovie({ id: "b", title: "Second", year: 2002 });
|
|
|
+
|
|
|
+ const { container, rerender } = render(
|
|
|
+ <RollAnnouncer state="idle" winner={null} />,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Idle: counter starts at 0
|
|
|
+ let announceId = container
|
|
|
+ .querySelector("[data-announce-id]")
|
|
|
+ ?.getAttribute("data-announce-id");
|
|
|
+ expect(announceId).toBe("0");
|
|
|
+
|
|
|
+ // First roll: idle -> rolling -> complete (counter -> 1)
|
|
|
+ rerender(<RollAnnouncer state="rolling" winner={null} />);
|
|
|
+ rerender(<RollAnnouncer state="complete" winner={first} />);
|
|
|
+
|
|
|
+ announceId = container
|
|
|
+ .querySelector("[data-announce-id]")
|
|
|
+ ?.getAttribute("data-announce-id");
|
|
|
+ expect(announceId).toBe("1");
|
|
|
+ expect(container.textContent).toBe("Rolled First (2001)");
|
|
|
+
|
|
|
+ // Re-roll: complete -> rolling -> complete (counter -> 2)
|
|
|
+ rerender(<RollAnnouncer state="rolling" winner={null} />);
|
|
|
+ rerender(<RollAnnouncer state="complete" winner={second} />);
|
|
|
+
|
|
|
+ announceId = container
|
|
|
+ .querySelector("[data-announce-id]")
|
|
|
+ ?.getAttribute("data-announce-id");
|
|
|
+ expect(announceId).toBe("2");
|
|
|
+ expect(container.textContent).toBe("Rolled Second (2002)");
|
|
|
+ });
|
|
|
+
|
|
|
+ it("renders movie title as text only — no HTML injection (XSS guard)", () => {
|
|
|
+ const evil = makeMovie({
|
|
|
+ title: "<script>alert(1)</script>",
|
|
|
+ year: 2024,
|
|
|
+ });
|
|
|
+ const { container } = render(
|
|
|
+ <RollAnnouncer state="complete" winner={evil} />,
|
|
|
+ );
|
|
|
+
|
|
|
+ // No <script> element should ever land in the DOM.
|
|
|
+ expect(container.querySelector("script")).toBeNull();
|
|
|
+
|
|
|
+ // The raw string is rendered as text content.
|
|
|
+ expect(container.textContent).toBe(
|
|
|
+ "Rolled <script>alert(1)</script> (2024)",
|
|
|
+ );
|
|
|
+
|
|
|
+ // Confirm it lives in a text node, not parsed HTML.
|
|
|
+ const span = container.querySelector("[data-announce-id]");
|
|
|
+ expect(span?.children.length).toBe(0);
|
|
|
+ expect(span?.textContent).toBe("Rolled <script>alert(1)</script> (2024)");
|
|
|
+ });
|
|
|
+});
|