// @vitest-environment happy-dom import { useEffect, useState } from "react"; import { cleanup, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionId } from "@shared/ids"; import { SessionProvider, useSessionService } from "@work/index"; import { SessionMenu } from "./SessionMenu"; import { parseRoute } from "./routing"; function HashSync() { // Mirrors the production AppShell effect: hash changes drive // SessionService.setActive so useActiveSession() resolves correctly. const service = useSessionService(); const [tick, setTick] = useState(0); useEffect(() => { const onHash = () => setTick((t) => t + 1); window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash); }, []); useEffect(() => { const route = parseRoute(window.location.hash); if (route.sessionId && service.get(route.sessionId as SessionId)) { service.setActive(route.sessionId as SessionId); } else { service.setActive(null); } }, [tick, service]); return null; } function Wrap({ children }: { children: React.ReactNode }) { return ( {children} ); } function CurrentHash() { return {window.location.hash || "(empty)"}; } function SeedTwo() { const service = useSessionService(); if (service.list().length === 0) { service.create({ name: "Alpha" }); service.create({ name: "Beta" }); } return null; } beforeEach(() => { globalThis.localStorage?.clear(); history.replaceState(null, "", window.location.pathname); }); afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe("SessionMenu", () => { it("creating a new session navigates the hash to /s/", async () => { render( , ); const user = userEvent.setup(); await user.click(screen.getByTestId("session-menu-toggle")); await user.click(screen.getByTestId("session-menu-new")); await user.type(screen.getByTestId("session-new-input"), "Demo"); await user.click(screen.getByTestId("session-new-confirm")); await waitFor(() => { const route = parseRoute(window.location.hash); expect(route.sessionId).toMatch(/^sess_/); expect(route.mode).toBe("review"); }); }); it("switching sessions writes the chosen id into the hash", async () => { render( , ); const user = userEvent.setup(); await user.click(screen.getByTestId("session-menu-toggle")); const alphaBtn = await screen.findByText(/Alpha/); await user.click(alphaBtn); await waitFor(() => { const route = parseRoute(window.location.hash); expect(route.sessionId).not.toBeNull(); expect(route.mode).toBe("review"); }); }); it("rename rejects a duplicate name with an inline error", async () => { render( , ); const user = userEvent.setup(); // Switch to Alpha first so it becomes active and rename becomes available. await user.click(screen.getByTestId("session-menu-toggle")); const alphaBtn = await screen.findByText(/Alpha/); await user.click(alphaBtn); // Re-open menu (it closed after switch) and try rename → Beta (taken). await user.click(screen.getByTestId("session-menu-toggle")); await user.click(screen.getByTestId("session-menu-rename")); const input = screen.getByTestId("session-rename-input") as HTMLInputElement; // Clear existing value and type new await user.clear(input); await user.type(input, "Beta"); await user.click(screen.getByTestId("session-rename-confirm")); const error = await screen.findByTestId("session-menu-error"); expect(error.textContent).toMatch(/already exists/); }); });