// @vitest-environment happy-dom import { act, cleanup, render, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { useHashRouter, type Route, type UseHashRouterResult } from "./useHashRouter.js"; let controls: UseHashRouterResult; function Probe() { controls = useHashRouter(); return
{controls.route.kind === "attach" ? `attach:${controls.route.sessionId}` : "catalog"}
; } beforeEach(() => { location.hash = ""; }); afterEach(cleanup); describe("useHashRouter", () => { it("starts on catalog for an empty hash", () => { const view = render(); expect(view.container.textContent).toBe("catalog"); }); it("resolves #/attach/:id to the attach route", () => { location.hash = "#/attach/s1"; const view = render(); expect(view.container.textContent).toBe("attach:s1"); }); it("decodes an encoded session id", () => { location.hash = `#/attach/${encodeURIComponent("weird id/x")}`; const view = render(); expect(view.container.textContent).toBe("attach:weird id/x"); }); it("falls back to catalog for an unknown route", () => { location.hash = "#/nonsense"; const view = render(); expect(view.container.textContent).toBe("catalog"); }); it("follows a hashchange fired outside the hook", async () => { const view = render(); // happy-dom dispatches `hashchange` on a real timer, like a browser — not // synchronously with the assignment. location.hash = "#/attach/s9"; await waitFor(() => expect(view.container.textContent).toBe("attach:s9")); }); it("navigate updates the hash and the route", async () => { const view = render(); act(() => controls.navigate({ kind: "attach", sessionId: "s2" } satisfies Route)); expect(location.hash).toBe("#/attach/s2"); await waitFor(() => expect(view.container.textContent).toBe("attach:s2")); }); it("navigate back to catalog sets the root hash", async () => { location.hash = "#/attach/s1"; const view = render(); act(() => controls.navigate({ kind: "catalog" })); expect(location.hash).toBe("#/"); await waitFor(() => expect(view.container.textContent).toBe("catalog")); }); it("navigating to the already-current route still updates state", () => { location.hash = "#/attach/s1"; const view = render(); act(() => controls.navigate({ kind: "attach", sessionId: "s1" } satisfies Route)); expect(view.container.textContent).toBe("attach:s1"); }); it("stops listening after unmount", async () => { const view = render(); view.unmount(); location.hash = "#/attach/s3"; // No assertion beyond "doesn't throw" — a leaked listener would only ever // surface as noise in a later test's act() warnings. await new Promise((r) => setTimeout(r, 0)); }); });