// @vitest-environment happy-dom import { act, createElement, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ save: vi.fn() })); vi.mock("@/app/admin/stygian/actions", () => ({ saveStygianRecommendationsAction: mocks.save })); vi.mock("@/components/admin/stygian-sync-button", () => ({ StygianSyncButton: () => null })); vi.mock("@/components/ui/resilient-image", () => ({ default: () => null })); vi.mock("@/components/stygian/stygian-boss-display", () => ({ StygianBossCard: ({ children }: { children: ReactNode }) => createElement("section", {}, children), StygianBossTab: ({ boss }: { boss?: { name: string } }) => createElement("span", {}, boss?.name), })); vi.mock("@/components/admin/ui/catalog-select", () => ({ CharacterSelect: ({ onValueChange }: { onValueChange: (key: string) => void }) => createElement("button", { type: "button", onClick: () => onValueChange("test-character") }, "Choose test character"), ArtifactSelect: () => null, CatalogDropdown: () => null, WeaponSelect: () => null, })); import { StygianEditor } from "./stygian-editor"; let root: Root; let container: HTMLDivElement; beforeEach(() => { vi.useFakeTimers(); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.clearAllMocks(); mocks.save.mockImplementation(async (_scheduleId, input) => ({ status: "saved", version: input.expectedVersion + 1 })); container = document.createElement("div"); document.body.append(container); root = createRoot(container); }); afterEach(async () => { await act(async () => root.unmount()); container.remove(); vi.useRealTimers(); vi.unstubAllGlobals(); }); function activePanel() { return Array.from(container.querySelectorAll('[role="tabpanel"]')).find((panel) => !panel.closest('[hidden], [inert]') && panel.querySelector(":scope > section"), )!; } async function clickButton(label: string, within: HTMLElement = activePanel()) { const button = Array.from(within.querySelectorAll("button")).find((button) => button.textContent === label)!; expect(button).toBeDefined(); await act(async () => button.click()); } async function selectBoss(position: number) { const tabs = Array.from(container.querySelectorAll('[role="tab"]')).filter((tab) => tab.getAttribute("title")?.startsWith("Test boss") && !tab.closest('[hidden]'), ); await act(async () => tabs[position].click()); await act(async () => { await vi.advanceTimersByTimeAsync(0); }); } describe("Stygian manual saving", () => { it("keeps drafts across boss tabs and saves only the active tab on a click", async () => { const bosses = [0, 1, 2].map((position) => ({ level: 105, position, name: `Test boss ${position}`, imageUrl: "/test.png", hp: 1, description: "", mechanics: [], recommendedMechanics: [], recommendedMechanicTypes: [], })); await act(async () => root.render(createElement(StygianEditor, { schedule: { scheduleId: 1, challengeName: "Test", version: 1 }, schedules: [], bosses, recommendations: [], characters: [], weapons: [], artifacts: [], }))); expect(container.querySelectorAll("section")).toHaveLength(1); await clickButton("Add team"); await clickButton("Choose test character"); await act(async () => { await vi.advanceTimersByTimeAsync(10_000); }); expect(mocks.save).not.toHaveBeenCalled(); await selectBoss(1); await clickButton("Add team"); await clickButton("Choose test character"); expect(container.querySelectorAll(".fixed")).toHaveLength(1); await clickButton("บันทึก"); expect(mocks.save).toHaveBeenCalledTimes(1); expect(mocks.save.mock.calls[0][1]).toMatchObject({ level: 105, bossPosition: 1, expectedVersion: 1 }); await selectBoss(0); expect(activePanel().querySelector(".fixed button")?.hasAttribute("disabled")).toBe(false); await clickButton("บันทึก"); expect(mocks.save).toHaveBeenCalledTimes(2); expect(mocks.save.mock.calls[1][1]).toMatchObject({ level: 105, bossPosition: 0, expectedVersion: 2 }); expect(mocks.save.mock.calls[1][1].teams[0][0].catalogKey).toBe("test-character"); }); });