// @vitest-environment jsdom import "@testing-library/jest-dom/vitest"; import { combatantId } from "@initiative/domain"; import { cleanup, render, screen } from "@testing-library/react"; import type { ReactNode } from "react"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { createTestAdapters } from "../../__tests__/adapters/in-memory-adapters.js"; import { buildCombatant, buildEncounter, } from "../../__tests__/factories/index.js"; import { AllProviders } from "../../__tests__/test-providers.js"; import { TurnNavigation } from "../turn-navigation.js"; beforeAll(() => { Object.defineProperty(globalThis, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }); }); afterEach(cleanup); function renderNav(encounter = buildEncounter()) { const adapters = createTestAdapters({ encounter }); return render(, { wrapper: ({ children }: { children: ReactNode }) => ( {children} ), }); } describe("TurnNavigation", () => { describe("US1: Round badge and combatant name", () => { it("renders the round badge with correct round number", () => { renderNav( buildEncounter({ combatants: [buildCombatant({ name: "Goblin" })], roundNumber: 3, }), ); expect(screen.getByText("R3")).toBeInTheDocument(); }); it("renders the combatant name separately from the round badge", () => { renderNav( buildEncounter({ combatants: [ buildCombatant({ id: combatantId("c-1"), name: "Goblin" }), buildCombatant({ id: combatantId("c-2"), name: "Conjurer" }), ], activeIndex: 0, roundNumber: 1, }), ); const badge = screen.getByText("R1"); const name = screen.getByText("Goblin"); expect(badge).toBeInTheDocument(); expect(name).toBeInTheDocument(); expect(badge).not.toBe(name); expect(badge.closest("[class]")).not.toBe(name.closest("[class]")); }); it("does not render an em dash between round and name", () => { const { container } = renderNav( buildEncounter({ combatants: [buildCombatant({ name: "Goblin" })], }), ); expect(container.textContent).not.toContain("\u2014"); }); it("round badge is in the left zone and name is in the center zone", () => { renderNav( buildEncounter({ combatants: [buildCombatant({ name: "Goblin" })], }), ); const badge = screen.getByText("R1"); const name = screen.getByText("Goblin"); // Badge and name are in separate grid cells to prevent layout shifts expect(badge.parentElement).not.toBe(name.parentElement); }); }); describe("US2: Layout robustness", () => { it("applies truncation styles to long combatant names", () => { const longName = "Ancient Red Dragon Wyrm of the Northern Wastes and Beyond"; renderNav( buildEncounter({ combatants: [buildCombatant({ name: longName })], }), ); const nameEl = screen.getByText(longName); expect(nameEl.className).toContain("truncate"); }); it("renders three-zone layout with a single-character name", () => { renderNav( buildEncounter({ combatants: [buildCombatant({ name: "O" })], }), ); expect(screen.getByText("R1")).toBeInTheDocument(); expect(screen.getByText("O")).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Previous turn" }), ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Next turn" }), ).toBeInTheDocument(); }); it("keeps all action buttons accessible regardless of name length", () => { const longName = "A".repeat(60); renderNav( buildEncounter({ combatants: [buildCombatant({ name: longName })], }), ); expect( screen.getByRole("button", { name: "Previous turn" }), ).toBeInTheDocument(); expect( screen.getByRole("button", { name: "Next turn" }), ).toBeInTheDocument(); }); it("renders a 40-character name without truncation class issues", () => { const name40 = "A".repeat(40); renderNav( buildEncounter({ combatants: [buildCombatant({ name: name40 })], }), ); const nameEl = screen.getByText(name40); expect(nameEl).toBeInTheDocument(); expect(nameEl.className).toContain("truncate"); }); }); describe("US3: No combatants state", () => { it("shows the round badge when there are no combatants", () => { renderNav(buildEncounter({ combatants: [], roundNumber: 1 })); expect(screen.getByText("R1")).toBeInTheDocument(); }); it("shows 'No combatants' placeholder text", () => { renderNav(buildEncounter({ combatants: [] })); expect(screen.getByText("No combatants")).toBeInTheDocument(); }); it("disables navigation buttons when there are no combatants", () => { renderNav(buildEncounter({ combatants: [] })); expect( screen.getByRole("button", { name: "Previous turn" }), ).toBeDisabled(); expect(screen.getByRole("button", { name: "Next turn" })).toBeDisabled(); }); }); });