import { act, useState } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ADMIN_SHELL_CONTRACT, AdminShell, GearIcon, STACKED_SHELL_QUERY, SwatchIcon, } from "./AdminShell"; let root: Root | null = null; function ShellHarness({ onSubmit, onFocusResults, }: { onSubmit: () => void; onFocusResults?: () => void; }) { const [search, setSearch] = useState("tooltip"); const [section, setSection] = useState<"general" | "chips">("general"); return ( Pro} topbarActions={[ { id: "save", label: "Save", onClick: vi.fn(), variant: "primary", }, ]} search={{ value: search, label: "Search settings", placeholder: "Search...", help: "Press Enter to open.", clearLabel: "Clear search", clearText: "Clear", onChange: setSearch, onSubmit, onFocusResults, }} navigationLabel="Settings sections" navItems={[ { id: "general", label: "General", Icon: GearIcon }, { id: "chips", label: "Chips", Icon: SwatchIcon }, ]} activeSection={section} onSelectSection={setSection} preview={{ title: "Preview", meta: Click an option, content: Preview content, footer: "Updates live.", }} sidebarFooter={Sidebar footer} mainContent={
Main content
} mainActions={[ { id: "reset", label: "Reset", onClick: vi.fn(), variant: "secondary", }, ]} toast={{ show: true, type: "success", message: "Saved" }} /> ); } describe("AdminShell", () => { beforeEach(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean; } ).IS_REACT_ACT_ENVIRONMENT = true; vi.stubGlobal( "requestAnimationFrame", vi.fn((callback: FrameRequestCallback) => { callback(0); return 1; }), ); }); afterEach(() => { act(() => root?.unmount()); root = null; document.body.innerHTML = ""; vi.unstubAllGlobals(); vi.clearAllMocks(); }); it("owns the shared landmarks and keyboard/navigation behavior", async () => { const onSubmit = vi.fn(); const onFocusResults = vi.fn(); const container = document.createElement("div"); document.body.append(container); root = createRoot(container); await act(async () => root?.render( , ), ); const shell = container.querySelector( `[data-admin-shell-contract="${ADMIN_SHELL_CONTRACT}"]`, ); expect(shell).toBeTruthy(); expect(shell?.querySelector("header")).toBeTruthy(); expect(shell?.querySelector("aside")).toBeTruthy(); expect(shell?.querySelector("nav")?.getAttribute("aria-label")).toBe( "Settings sections", ); expect(shell?.querySelector("main")?.textContent).toContain("Main content"); // The toast is presentational: the owning app announces the message through // the persistent live regions in `liveAnnouncer`. const toast = shell?.querySelector(".b3-wvs-shell-toast"); expect(toast?.textContent).toContain("Saved"); expect(toast?.getAttribute("aria-hidden")).toBe("true"); expect(toast?.getAttribute("role")).toBeNull(); expect(toast?.getAttribute("aria-live")).toBeNull(); const search = shell?.querySelector( 'input[type="search"]', ); await act(async () => { search?.dispatchEvent( new KeyboardEvent("keydown", { key: "Enter", bubbles: true }), ); }); expect(onSubmit).toHaveBeenCalledOnce(); await act(async () => { search?.dispatchEvent( new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }), ); }); expect(onFocusResults).toHaveBeenCalledOnce(); expect(search?.maxLength).toBe(120); await act(async () => { search?.dispatchEvent( new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), ); }); expect(search?.value).toBe(""); const chips = Array.from(shell?.querySelectorAll("nav button") ?? []).find( (button) => button.textContent?.includes("Chips"), ); await act(async () => { (chips as HTMLButtonElement | undefined)?.click(); }); expect(chips?.getAttribute("aria-current")).toBe("page"); }); it("marks the shell while searching so stacked layouts can lift results", async () => { const container = document.createElement("div"); document.body.append(container); root = createRoot(container); await act(async () => root?.render()); const shell = container.querySelector(".b3-wvs-shell-app"); expect(shell?.classList.contains("is-searching")).toBe(true); const search = shell?.querySelector( 'input[type="search"]', ); await act(async () => { search?.dispatchEvent( new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), ); }); expect(shell?.classList.contains("is-searching")).toBe(false); }); it.each([ { stacked: true, expectScroll: true }, { stacked: false, expectScroll: false }, ])( "reveals the main panel after a nav choice only on the stacked layout (stacked: $stacked)", async ({ stacked, expectScroll }) => { vi.stubGlobal( "matchMedia", vi.fn((query: string) => ({ matches: query === STACKED_SHELL_QUERY ? stacked : false, media: query, addEventListener: vi.fn(), removeEventListener: vi.fn(), })), ); const scrollTo = vi.fn(); vi.stubGlobal("scrollTo", scrollTo); const container = document.createElement("div"); document.body.append(container); root = createRoot(container); await act(async () => root?.render()); const main = container.querySelector("main"); vi.spyOn(main as HTMLElement, "getBoundingClientRect").mockReturnValue({ top: 900, } as DOMRect); const chips = Array.from(container.querySelectorAll("nav button")).find( (button) => button.textContent?.includes("Chips"), ) as HTMLButtonElement; await act(async () => chips.click()); if (expectScroll) { expect(scrollTo).toHaveBeenCalledOnce(); const [{ top, behavior }] = scrollTo.mock.calls[0]; expect(top).toBeGreaterThan(0); expect(top).toBeLessThan(900); expect(behavior).toBe("smooth"); expect(document.activeElement).toBe(main); } else { expect(scrollTo).not.toHaveBeenCalled(); expect(document.activeElement).not.toBe(main); } }, ); });