Use a three-column grid (1fr / auto / 1fr) so the active combatant name stays centered while round badge and difficulty indicator are anchored in the left and right zones. Prevents layout jumps when the name changes between turns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
// @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(<TurnNavigation />, {
|
|
wrapper: ({ children }: { children: ReactNode }) => (
|
|
<AllProviders adapters={adapters}>{children}</AllProviders>
|
|
),
|
|
});
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|