Top bar stripped to turn navigation only (Prev, round badge, Clear, Next). Roll All Initiative, Manage Sources, and Bulk Import moved to a new overflow menu in the bottom bar. Player Characters also moved there. Browse stat blocks is now an Eye/EyeOff toggle inside the search input that switches between add mode and browse mode. Add button only appears when entering a custom creature name. Roll All Initiative button shows conditionally — only when bestiary creatures lack initiative values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
5.7 KiB
TypeScript
200 lines
5.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import "@testing-library/jest-dom/vitest";
|
|
|
|
import type { Encounter } from "@initiative/domain";
|
|
import { combatantId } from "@initiative/domain";
|
|
import { cleanup, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { TurnNavigation } from "../turn-navigation";
|
|
|
|
afterEach(cleanup);
|
|
|
|
function renderNav(overrides: Partial<Encounter> = {}) {
|
|
const encounter: Encounter = {
|
|
combatants: [
|
|
{ id: combatantId("1"), name: "Goblin" },
|
|
{ id: combatantId("2"), name: "Conjurer" },
|
|
],
|
|
activeIndex: 0,
|
|
roundNumber: 1,
|
|
...overrides,
|
|
};
|
|
|
|
return render(
|
|
<TurnNavigation
|
|
encounter={encounter}
|
|
onAdvanceTurn={vi.fn()}
|
|
onRetreatTurn={vi.fn()}
|
|
onClearEncounter={vi.fn()}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe("TurnNavigation", () => {
|
|
describe("US1: Round badge and combatant name", () => {
|
|
it("renders the round badge with correct round number", () => {
|
|
renderNav({ roundNumber: 3 });
|
|
expect(screen.getByText("R3")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the combatant name separately from the round badge", () => {
|
|
renderNav();
|
|
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();
|
|
expect(container.textContent).not.toContain("—");
|
|
});
|
|
|
|
it("round badge and combatant name are siblings in the center area", () => {
|
|
renderNav();
|
|
const badge = screen.getByText("R1");
|
|
const name = screen.getByText("Goblin");
|
|
expect(badge.parentElement).toBe(name.parentElement);
|
|
});
|
|
|
|
it("updates the round badge when round changes", () => {
|
|
const { rerender } = render(
|
|
<TurnNavigation
|
|
encounter={{
|
|
combatants: [{ id: combatantId("1"), name: "Goblin" }],
|
|
activeIndex: 0,
|
|
roundNumber: 2,
|
|
}}
|
|
onAdvanceTurn={vi.fn()}
|
|
onRetreatTurn={vi.fn()}
|
|
onClearEncounter={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("R2")).toBeInTheDocument();
|
|
|
|
rerender(
|
|
<TurnNavigation
|
|
encounter={{
|
|
combatants: [{ id: combatantId("1"), name: "Goblin" }],
|
|
activeIndex: 0,
|
|
roundNumber: 3,
|
|
}}
|
|
onAdvanceTurn={vi.fn()}
|
|
onRetreatTurn={vi.fn()}
|
|
onClearEncounter={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("R3")).toBeInTheDocument();
|
|
expect(screen.queryByText("R2")).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("renders the next combatant name when turn advances", () => {
|
|
const { rerender } = render(
|
|
<TurnNavigation
|
|
encounter={{
|
|
combatants: [
|
|
{ id: combatantId("1"), name: "Goblin" },
|
|
{ id: combatantId("2"), name: "Conjurer" },
|
|
],
|
|
activeIndex: 0,
|
|
roundNumber: 1,
|
|
}}
|
|
onAdvanceTurn={vi.fn()}
|
|
onRetreatTurn={vi.fn()}
|
|
onClearEncounter={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("Goblin")).toBeInTheDocument();
|
|
|
|
rerender(
|
|
<TurnNavigation
|
|
encounter={{
|
|
combatants: [
|
|
{ id: combatantId("1"), name: "Goblin" },
|
|
{ id: combatantId("2"), name: "Conjurer" },
|
|
],
|
|
activeIndex: 1,
|
|
roundNumber: 1,
|
|
}}
|
|
onAdvanceTurn={vi.fn()}
|
|
onRetreatTurn={vi.fn()}
|
|
onClearEncounter={vi.fn()}
|
|
/>,
|
|
);
|
|
expect(screen.getByText("Conjurer")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
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({
|
|
combatants: [{ id: combatantId("1"), name: longName }],
|
|
});
|
|
const nameEl = screen.getByText(longName);
|
|
expect(nameEl.className).toContain("truncate");
|
|
});
|
|
|
|
it("renders three-zone layout with a single-character name", () => {
|
|
renderNav({
|
|
combatants: [{ id: combatantId("1"), 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({
|
|
combatants: [{ id: combatantId("1"), 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({
|
|
combatants: [{ id: combatantId("1"), name: name40 }],
|
|
});
|
|
const nameEl = screen.getByText(name40);
|
|
expect(nameEl).toBeInTheDocument();
|
|
// The truncate class is applied but CSS only visually truncates if content overflows
|
|
expect(nameEl.className).toContain("truncate");
|
|
});
|
|
});
|
|
|
|
describe("US3: No combatants state", () => {
|
|
it("shows the round badge when there are no combatants", () => {
|
|
renderNav({ combatants: [], roundNumber: 1 });
|
|
expect(screen.getByText("R1")).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows 'No combatants' placeholder text", () => {
|
|
renderNav({ combatants: [] });
|
|
expect(screen.getByText("No combatants")).toBeInTheDocument();
|
|
});
|
|
|
|
it("disables navigation buttons when there are no combatants", () => {
|
|
renderNav({ combatants: [] });
|
|
expect(
|
|
screen.getByRole("button", { name: "Previous turn" }),
|
|
).toBeDisabled();
|
|
expect(screen.getByRole("button", { name: "Next turn" })).toBeDisabled();
|
|
});
|
|
});
|
|
});
|