// @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();
});
});
});