// Component tests for the in-game shell header. The header composes // the headline strip (` @ , turn N`, falling back to `?` // while the lobby / report calls are in flight), the view-menu, and // the account-menu. The tests assert the headline copy, that every // view-menu entry dispatches `goto` with the right URL, and that the // Logout entry of the account-menu calls `session.signOut("user")`. import "@testing-library/jest-dom/vitest"; import { fireEvent, render } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test, vi, } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import { session } from "../src/lib/session-store.svelte"; import Header from "../src/lib/header/header.svelte"; import { GAME_STATE_CONTEXT_KEY, GameStateStore, } from "../src/lib/game-state.svelte"; import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups"; function withGameState(opts: { gameName?: string; race?: string; turn?: number; } = {}): Map { const store = new GameStateStore(); store.gameName = opts.gameName ?? ""; if (opts.race !== undefined || opts.turn !== undefined) { store.report = { turn: opts.turn ?? 0, mapWidth: 1000, mapHeight: 1000, planetCount: 0, planets: [], race: opts.race ?? "", localShipClass: [], routes: [], localPlayerDrive: 0, localPlayerWeapons: 0, localPlayerShields: 0, localPlayerCargo: 0, ...EMPTY_SHIP_GROUPS, }; store.status = "ready"; } return new Map([[GAME_STATE_CONTEXT_KEY, store]]); } const gotoSpy = vi.fn(async (..._args: unknown[]) => {}); vi.mock("$app/navigation", () => ({ goto: (...args: unknown[]) => gotoSpy(...args), })); beforeEach(() => { i18n.resetForTests("en"); gotoSpy.mockReset(); vi.spyOn(session, "signOut").mockResolvedValue(undefined); }); afterEach(() => { vi.restoreAllMocks(); }); describe("game-shell header", () => { test("renders fall-back placeholders before the lobby / report data lands", () => { const onToggleSidebar = vi.fn(); const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar }, context: withGameState(), }); expect(ui.getByTestId("game-shell-headline")).toHaveTextContent( "? @ ?, turn ?", ); expect(ui.getByTestId("view-menu-trigger")).toBeInTheDocument(); expect(ui.getByTestId("account-menu-trigger")).toBeInTheDocument(); }); test("renders the live race / game / turn from GameStateStore", () => { const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} }, context: withGameState({ gameName: "Phase 14", race: "Federation", turn: 7, }), }); expect(ui.getByTestId("game-shell-headline")).toHaveTextContent( "Federation @ Phase 14, turn 7", ); }); test("partial data still falls back gracefully (race known, game unknown)", () => { const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} }, context: withGameState({ race: "Federation", turn: 3 }), }); expect(ui.getByTestId("game-shell-headline")).toHaveTextContent( "Federation @ ?, turn 3", ); }); test("clicking the sidebar toggle invokes the prop callback", async () => { const onToggleSidebar = vi.fn(); const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar }, }); await fireEvent.click(ui.getByTestId("sidebar-toggle")); expect(onToggleSidebar).toHaveBeenCalledTimes(1); }); test("view-menu navigates to every IA destination", async () => { const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} }, }); const destinations: Array<[string, string]> = [ ["view-menu-item-map", "/games/g1/map"], ["view-menu-item-report", "/games/g1/report"], ["view-menu-item-battle", "/games/g1/battle"], ["view-menu-item-mail", "/games/g1/mail"], [ "view-menu-item-designer-ship-class", "/games/g1/designer/ship-class", ], [ "view-menu-item-designer-science", "/games/g1/designer/science", ], ]; for (const [testId, href] of destinations) { await fireEvent.click(ui.getByTestId("view-menu-trigger")); await fireEvent.click(ui.getByTestId(testId)); expect(gotoSpy).toHaveBeenLastCalledWith(href); } }); test("view-menu Tables sub-list navigates to every entity", async () => { const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} }, }); const tableEntities: Array<[string, string]> = [ ["view-menu-item-table-planets", "/games/g1/table/planets"], [ "view-menu-item-table-ship-classes", "/games/g1/table/ship-classes", ], [ "view-menu-item-table-ship-groups", "/games/g1/table/ship-groups", ], ["view-menu-item-table-fleets", "/games/g1/table/fleets"], ["view-menu-item-table-sciences", "/games/g1/table/sciences"], ["view-menu-item-table-races", "/games/g1/table/races"], ]; for (const [testId, href] of tableEntities) { await fireEvent.click(ui.getByTestId("view-menu-trigger")); // Open the Tables sub-disclosure each iteration; the menu // closes on every navigation. const summary = ui .getByTestId("view-menu-tables") .querySelector("summary"); if (summary !== null) { await fireEvent.click(summary); } await fireEvent.click(ui.getByTestId(testId)); expect(gotoSpy).toHaveBeenLastCalledWith(href); } }); test("account-menu Logout triggers session.signOut('user')", async () => { const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} }, }); await fireEvent.click(ui.getByTestId("account-menu-trigger")); await fireEvent.click(ui.getByTestId("account-menu-logout")); expect(session.signOut).toHaveBeenCalledWith("user"); }); test("account-menu language picker switches the i18n locale", async () => { const ui = render(Header, { props: { gameId: "g1", sidebarOpen: false, onToggleSidebar: () => {} }, }); await fireEvent.click(ui.getByTestId("account-menu-trigger")); const select = ui.getByTestId("account-menu-language-select"); await fireEvent.change(select, { target: { value: "ru" } }); expect(i18n.locale).toBe("ru"); }); });