// @vitest-environment jsdom import "@testing-library/jest-dom/vitest"; import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { AllProviders } from "../../__tests__/test-providers.js"; import { ActionBar } from "../action-bar.js"; // Mock persistence — no localStorage interaction vi.mock("../../persistence/encounter-storage.js", () => ({ loadEncounter: () => null, saveEncounter: () => {}, })); vi.mock("../../persistence/player-character-storage.js", () => ({ loadPlayerCharacters: () => [], savePlayerCharacters: () => {}, })); // Mock bestiary — no IndexedDB or JSON index vi.mock("../../adapters/bestiary-cache.js", () => ({ loadAllCachedCreatures: () => Promise.resolve(new Map()), isSourceCached: () => Promise.resolve(false), cacheSource: () => Promise.resolve(), getCachedSources: () => Promise.resolve([]), clearSource: () => Promise.resolve(), clearAll: () => Promise.resolve(), })); vi.mock("../../adapters/bestiary-index-adapter.js", () => ({ loadBestiaryIndex: () => ({ sources: {}, creatures: [] }), getAllSourceCodes: () => [], getDefaultFetchUrl: () => "", getSourceDisplayName: (code: string) => code, })); // DOM API stubs — jsdom doesn't implement these 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 renderBar(props: Partial[0]> = {}) { return render(, { wrapper: AllProviders }); } describe("ActionBar", () => { it("renders input with placeholder '+ Add combatants'", () => { renderBar(); expect(screen.getByPlaceholderText("+ Add combatants")).toBeInTheDocument(); }); it("submitting with a name adds a combatant", async () => { const user = userEvent.setup(); renderBar(); const input = screen.getByPlaceholderText("+ Add combatants"); await user.type(input, "Goblin"); // The Add button appears when name >= 2 chars and no suggestions const addButton = screen.getByRole("button", { name: "Add" }); await user.click(addButton); // Input is cleared after adding (context handles the state) expect(input).toHaveValue(""); }); it("submitting with empty name does nothing", async () => { const user = userEvent.setup(); renderBar(); // Submit the form directly (Enter on empty input) const input = screen.getByPlaceholderText("+ Add combatants"); await user.type(input, "{Enter}"); // Input stays empty, no error expect(input).toHaveValue(""); }); it("shows custom fields (Init, AC, MaxHP) when name >= 2 chars and no bestiary suggestions", async () => { const user = userEvent.setup(); renderBar(); const input = screen.getByPlaceholderText("+ Add combatants"); await user.type(input, "Go"); expect(screen.getByPlaceholderText("Init")).toBeInTheDocument(); expect(screen.getByPlaceholderText("AC")).toBeInTheDocument(); expect(screen.getByPlaceholderText("MaxHP")).toBeInTheDocument(); }); it("shows Add button when name >= 2 chars and no suggestions", async () => { const user = userEvent.setup(); renderBar(); const input = screen.getByPlaceholderText("+ Add combatants"); await user.type(input, "Go"); expect(screen.getByRole("button", { name: "Add" })).toBeInTheDocument(); }); it("does not show roll all initiative button when no creature combatants", () => { renderBar(); expect( screen.queryByRole("button", { name: "Roll all initiative" }), ).not.toBeInTheDocument(); }); it("shows overflow menu items", () => { renderBar({ onManagePlayers: vi.fn() }); // The overflow menu should be present (it contains Player Characters etc.) expect( screen.getByRole("button", { name: "More actions" }), ).toBeInTheDocument(); }); });