// @vitest-environment jsdom import "@testing-library/jest-dom/vitest"; import { combatantId } from "@initiative/domain"; import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; import { CombatantRow } from "../combatant-row"; import { PLAYER_COLOR_HEX } from "../player-icon-map"; afterEach(cleanup); const defaultProps = { onRename: vi.fn(), onSetInitiative: vi.fn(), onRemove: vi.fn(), onSetHp: vi.fn(), onAdjustHp: vi.fn(), onSetAc: vi.fn(), onToggleCondition: vi.fn(), onToggleConcentration: vi.fn(), }; function renderRow( overrides: Partial<{ combatant: Parameters[0]["combatant"]; isActive: boolean; onRollInitiative: (id: ReturnType) => void; onRemove: (id: ReturnType) => void; onShowStatBlock: () => void; }> = {}, ) { const combatant = overrides.combatant ?? { id: combatantId("1"), name: "Goblin", initiative: 15, maxHp: 10, currentHp: 10, ac: 13, }; const props = { ...defaultProps, combatant, isActive: overrides.isActive ?? false, onRollInitiative: overrides.onRollInitiative, onShowStatBlock: overrides.onShowStatBlock, onRemove: overrides.onRemove ?? defaultProps.onRemove, }; return render(); } describe("CombatantRow", () => { it("renders combatant name", () => { renderRow(); expect(screen.getByText("Goblin")).toBeInTheDocument(); }); it("renders initiative value", () => { renderRow(); expect(screen.getByText("15")).toBeInTheDocument(); }); it("renders current HP", () => { renderRow({ combatant: { id: combatantId("1"), name: "Goblin", maxHp: 10, currentHp: 7, }, }); expect(screen.getByText("7")).toBeInTheDocument(); }); it("active combatant gets active border styling", () => { const { container } = renderRow({ isActive: true }); const row = container.firstElementChild; expect(row?.className).toContain("border-active-row-border"); }); it("unconscious combatant (currentHp === 0) gets dimmed styling", () => { renderRow({ combatant: { id: combatantId("1"), name: "Goblin", maxHp: 10, currentHp: 0, }, }); // The name area should have opacity-50 const nameEl = screen.getByText("Goblin"); const nameContainer = nameEl.closest(".opacity-50"); expect(nameContainer).not.toBeNull(); }); it("shows '--' for current HP when no maxHp is set", () => { renderRow({ combatant: { id: combatantId("1"), name: "Goblin", }, }); expect(screen.getByLabelText("No HP set")).toBeInTheDocument(); }); it("shows concentration icon when isConcentrating is true", () => { renderRow({ combatant: { id: combatantId("1"), name: "Goblin", isConcentrating: true, }, }); const concButton = screen.getByRole("button", { name: "Toggle concentration", }); expect(concButton.className).toContain("text-purple-400"); }); it("shows player character icon and color when set", () => { const { container } = renderRow({ combatant: { id: combatantId("1"), name: "Aragorn", color: "red", icon: "sword", }, }); // The icon should be rendered with the player color const svgIcon = container.querySelector("svg[style]"); expect(svgIcon).not.toBeNull(); expect(svgIcon).toHaveStyle({ color: PLAYER_COLOR_HEX.red }); }); it("remove button calls onRemove after confirmation", async () => { const user = userEvent.setup(); const onRemove = vi.fn(); renderRow({ onRemove }); const removeBtn = screen.getByRole("button", { name: "Remove combatant", }); // First click enters confirm state await user.click(removeBtn); // Second click confirms const confirmBtn = screen.getByRole("button", { name: "Confirm remove combatant", }); await user.click(confirmBtn); expect(onRemove).toHaveBeenCalledWith(combatantId("1")); }); it("shows d20 roll button when initiative is undefined and onRollInitiative is provided", () => { renderRow({ combatant: { id: combatantId("1"), name: "Goblin", }, onRollInitiative: vi.fn(), }); expect( screen.getByRole("button", { name: "Roll initiative" }), ).toBeInTheDocument(); }); });