// Phase 26 turn-navigator component tests. The navigator owns three // affordances: arrows that step ±1 through history, a clickable // `turn N` button that opens the full popover, and the popover rows // themselves. The store under test is a real `GameStateStore` // instance seeded into Svelte context — the navigator never calls // the network in tests because we override `viewTurn` / // `returnToCurrent` with `vi.fn` spies. import "@testing-library/jest-dom/vitest"; import { fireEvent, render } from "@testing-library/svelte"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import TurnNavigator from "../src/lib/header/turn-navigator.svelte"; import { GAME_STATE_CONTEXT_KEY, GameStateStore, } from "../src/lib/game-state.svelte"; function buildStore(opts: { currentTurn: number; viewedTurn: number; ready?: boolean; }): GameStateStore { const store = new GameStateStore(); store.currentTurn = opts.currentTurn; store.viewedTurn = opts.viewedTurn; store.status = opts.ready === false ? "loading" : "ready"; return store; } beforeEach(() => { i18n.resetForTests("en"); }); describe("TurnNavigator", () => { test("renders `turn ?` when the store is not ready yet", () => { const store = buildStore({ currentTurn: 0, viewedTurn: 0, ready: false }); const ui = render(TurnNavigator, { context: new Map([[GAME_STATE_CONTEXT_KEY, store]]), }); expect(ui.getByTestId("turn-navigator-trigger")).toHaveTextContent( "turn ?", ); expect(ui.getByTestId("turn-navigator-trigger")).toBeDisabled(); }); test("prev arrow disabled at viewedTurn = 0", () => { const ui = render(TurnNavigator, { context: new Map([ [ GAME_STATE_CONTEXT_KEY, buildStore({ currentTurn: 4, viewedTurn: 0 }), ], ]), }); expect(ui.getByTestId("turn-navigator-prev")).toBeDisabled(); expect(ui.getByTestId("turn-navigator-next")).not.toBeDisabled(); }); test("next arrow disabled at viewedTurn = currentTurn", () => { const ui = render(TurnNavigator, { context: new Map([ [ GAME_STATE_CONTEXT_KEY, buildStore({ currentTurn: 4, viewedTurn: 4 }), ], ]), }); expect(ui.getByTestId("turn-navigator-prev")).not.toBeDisabled(); expect(ui.getByTestId("turn-navigator-next")).toBeDisabled(); }); test("prev arrow steps to viewedTurn - 1 via viewTurn", async () => { const store = buildStore({ currentTurn: 4, viewedTurn: 4 }); const viewTurn = vi .spyOn(store, "viewTurn") .mockResolvedValue(undefined); const returnToCurrent = vi .spyOn(store, "returnToCurrent") .mockResolvedValue(undefined); const ui = render(TurnNavigator, { context: new Map([[GAME_STATE_CONTEXT_KEY, store]]), }); await fireEvent.click(ui.getByTestId("turn-navigator-prev")); expect(viewTurn).toHaveBeenCalledWith(3); expect(returnToCurrent).not.toHaveBeenCalled(); }); test("next arrow at one-step-from-current routes through returnToCurrent", async () => { const store = buildStore({ currentTurn: 4, viewedTurn: 3 }); const viewTurn = vi .spyOn(store, "viewTurn") .mockResolvedValue(undefined); const returnToCurrent = vi .spyOn(store, "returnToCurrent") .mockResolvedValue(undefined); const ui = render(TurnNavigator, { context: new Map([[GAME_STATE_CONTEXT_KEY, store]]), }); await fireEvent.click(ui.getByTestId("turn-navigator-next")); expect(returnToCurrent).toHaveBeenCalledTimes(1); expect(viewTurn).not.toHaveBeenCalled(); }); test("trigger opens the popover with every turn in descending order", async () => { const store = buildStore({ currentTurn: 3, viewedTurn: 1 }); const ui = render(TurnNavigator, { context: new Map([[GAME_STATE_CONTEXT_KEY, store]]), }); await fireEvent.click(ui.getByTestId("turn-navigator-trigger")); const list = ui.getByTestId("turn-navigator-list"); expect(list).toBeInTheDocument(); const rows = list.querySelectorAll("button[role='menuitem']"); expect(rows.length).toBe(4); expect(rows[0]).toHaveAttribute( "data-testid", "turn-navigator-item-3", ); expect(rows[3]).toHaveAttribute( "data-testid", "turn-navigator-item-0", ); // Current-turn row carries the badge. const currentRow = ui.getByTestId("turn-navigator-item-3"); expect(currentRow.querySelector("[data-testid='turn-navigator-current-badge']")) .not.toBeNull(); // Other rows do not carry a badge. const otherRow = ui.getByTestId("turn-navigator-item-2"); expect(otherRow.querySelector("[data-testid='turn-navigator-current-badge']")) .toBeNull(); }); test("selecting a past row delegates to viewTurn(N)", async () => { const store = buildStore({ currentTurn: 3, viewedTurn: 3 }); const viewTurn = vi .spyOn(store, "viewTurn") .mockResolvedValue(undefined); const returnToCurrent = vi .spyOn(store, "returnToCurrent") .mockResolvedValue(undefined); const ui = render(TurnNavigator, { context: new Map([[GAME_STATE_CONTEXT_KEY, store]]), }); await fireEvent.click(ui.getByTestId("turn-navigator-trigger")); await fireEvent.click(ui.getByTestId("turn-navigator-item-1")); expect(viewTurn).toHaveBeenCalledWith(1); expect(returnToCurrent).not.toHaveBeenCalled(); }); test("selecting the current row delegates to returnToCurrent", async () => { const store = buildStore({ currentTurn: 3, viewedTurn: 1 }); const viewTurn = vi .spyOn(store, "viewTurn") .mockResolvedValue(undefined); const returnToCurrent = vi .spyOn(store, "returnToCurrent") .mockResolvedValue(undefined); const ui = render(TurnNavigator, { context: new Map([[GAME_STATE_CONTEXT_KEY, store]]), }); await fireEvent.click(ui.getByTestId("turn-navigator-trigger")); await fireEvent.click(ui.getByTestId("turn-navigator-item-3")); expect(returnToCurrent).toHaveBeenCalledTimes(1); expect(viewTurn).not.toHaveBeenCalled(); }); });