import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, fireEvent } from "@testing-library/svelte"; // Controllable store shims + spies, declared via vi.hoisted so they exist when // the hoisted vi.mock factories run. A tiny writable shim avoids importing // svelte inside the hoisted block. const h = vi.hoisted(() => { function shim(initial: T) { let value = initial; const subs = new Set<(v: T) => void>(); return { set(v: T) { value = v; subs.forEach((fn) => fn(value)); }, subscribe(fn: (v: T) => void) { subs.add(fn); fn(value); return () => subs.delete(fn); }, }; } return { currentUserStore: shim<{ name: string } | null>({ name: "Ada" }), serverNameStore: shim("Home Server"), serverUrlStore: shim("https://media.example.com"), logout: vi.fn(async () => {}), reset: vi.fn(), goto: vi.fn(), }; }); vi.mock("$lib/stores/auth", () => ({ auth: { logout: h.logout }, currentUser: { subscribe: h.currentUserStore.subscribe }, serverName: { subscribe: h.serverNameStore.subscribe }, serverUrl: { subscribe: h.serverUrlStore.subscribe }, })); vi.mock("$lib/stores/library", () => ({ library: { reset: h.reset }, })); vi.mock("$app/navigation", () => ({ goto: h.goto })); import AccountMenu from "./AccountMenu.svelte"; function openMenu() { const trigger = screen.getByRole("button", { name: "Account menu" }); fireEvent.click(trigger); return trigger; } describe("AccountMenu", () => { beforeEach(() => { vi.clearAllMocks(); h.currentUserStore.set({ name: "Ada" }); h.serverNameStore.set("Home Server"); h.serverUrlStore.set("https://media.example.com"); }); it("trigger toggles aria-expanded", async () => { render(AccountMenu); const trigger = screen.getByRole("button", { name: "Account menu" }); expect(trigger.getAttribute("aria-expanded")).toBe("false"); await fireEvent.click(trigger); expect(trigger.getAttribute("aria-expanded")).toBe("true"); await fireEvent.click(trigger); expect(trigger.getAttribute("aria-expanded")).toBe("false"); }); it("renders the documented items in order", async () => { render(AccountMenu); openMenu(); const items = screen.getAllByRole("menuitem").map((el) => el.textContent?.trim()); expect(items).toEqual(["Downloads", "Settings", "Display", "Sign out"]); }); it("shows the identity block with name and server host", async () => { render(AccountMenu); openMenu(); expect(screen.getByText("Signed in as")).toBeTruthy(); // "Ada" appears in both the trigger label and the identity block. expect(screen.getAllByText("Ada").length).toBeGreaterThanOrEqual(1); expect(screen.getByText("Home Server")).toBeTruthy(); }); it("falls back to the URL host when no server name is set", async () => { h.serverNameStore.set(null); render(AccountMenu); openMenu(); expect(screen.getByText("media.example.com")).toBeTruthy(); }); it("Escape closes the menu", async () => { render(AccountMenu); const trigger = openMenu(); expect(trigger.getAttribute("aria-expanded")).toBe("true"); await fireEvent.keyDown(window, { key: "Escape" }); expect(trigger.getAttribute("aria-expanded")).toBe("false"); }); it("backdrop click closes the menu", async () => { render(AccountMenu); const trigger = openMenu(); const backdrop = screen.getByRole("button", { name: "Close account menu" }); await fireEvent.click(backdrop); expect(trigger.getAttribute("aria-expanded")).toBe("false"); }); it("Sign out logs out, resets library state, and redirects home", async () => { render(AccountMenu); openMenu(); const signOut = screen.getByRole("menuitem", { name: "Sign out" }); await fireEvent.click(signOut); expect(h.logout).toHaveBeenCalledOnce(); expect(h.reset).toHaveBeenCalledOnce(); expect(h.goto).toHaveBeenCalledWith("/"); }); it("Sign out is the last item, after the routine navigation", async () => { render(AccountMenu); openMenu(); const items = screen.getAllByRole("menuitem").map((el) => el.textContent?.trim()); expect(items[items.length - 1]).toBe("Sign out"); }); });