// @vitest-environment happy-dom // Mirror the IS_REACT_ACT_ENVIRONMENT setup from useDrawerUrlState.test.ts // so React doesn't log act() warnings about the createRoot render/unmount // and the popstate-driven state updates. (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; import React, { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { useProviderDrawerUrlState } from "./useProviderDrawerUrlState"; /** * Minimal renderHook shim — same pattern as the other hook tests. */ function renderHook(setup: () => TResult): { result: { current: TResult | undefined }; unmount: () => void; } { const result: { current: TResult | undefined } = { current: undefined }; const container = document.createElement("div"); document.body.appendChild(container); function Probe() { result.current = setup(); return null; } const root: Root = createRoot(container); act(() => { root.render(React.createElement(Probe)); }); return { result, unmount: () => { act(() => root.unmount()); container.remove(); }, }; } /** * Point happy-dom's URL at a known value. happy-dom v20 doesn't expose a * writable `window.location.search`, but `window.happyDOM.setURL` updates * the URL the window reports without triggering a navigation — exactly * what we want for mounting the hook at `/providers?provider=NPI` etc. */ function setLocation(url: string): void { (window as unknown as { happyDOM: { setURL: (u: string) => void } }).happyDOM.setURL(url); } describe("useProviderDrawerUrlState", () => { type PushState = (state: unknown, unused: string, url?: string | URL | null) => void; let pushStateMock: ReturnType>; let replaceStateMock: ReturnType>; beforeEach(() => { pushStateMock = vi.fn(); replaceStateMock = vi.fn(); vi.stubGlobal("history", { pushState: pushStateMock, replaceState: replaceStateMock, state: null, }); }); afterEach(() => { vi.unstubAllGlobals(); setLocation("http://localhost/"); }); it("reads the ?provider= param from window.location.search on mount", () => { setLocation("http://localhost/providers?provider=1881068062"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); expect(result.current?.providerNpi).toBe("1881068062"); expect(typeof result.current?.open).toBe("function"); expect(typeof result.current?.close).toBe("function"); expect(typeof result.current?.setProviderNpi).toBe("function"); unmount(); }); it("returns null providerNpi when no ?provider= param is set", () => { setLocation("http://localhost/providers"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); expect(result.current?.providerNpi).toBeNull(); unmount(); }); it("returns null providerNpi when ?provider= is present but empty", () => { setLocation("http://localhost/providers?provider="); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); expect(result.current?.providerNpi).toBeNull(); unmount(); }); it("open(npi) pushes a new history entry containing ?provider=npi", () => { setLocation("http://localhost/providers"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); act(() => { result.current?.open("1881068062"); }); expect(pushStateMock).toHaveBeenCalledTimes(1); expect(replaceStateMock).not.toHaveBeenCalled(); const urlArg = pushStateMock.mock.calls[0][2] as string; expect(urlArg).toContain("?provider=1881068062"); unmount(); }); it("setProviderNpi(npi) replaces the current history entry (no new entry) and does NOT pushState", () => { setLocation("http://localhost/providers?provider=1881068062"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); act(() => { result.current?.setProviderNpi("1881068063"); }); expect(replaceStateMock).toHaveBeenCalledTimes(1); expect(pushStateMock).not.toHaveBeenCalled(); const urlArg = replaceStateMock.mock.calls[0][2] as string; expect(urlArg).toContain("?provider=1881068063"); unmount(); }); it("open() and close() preserve other query params (only ?provider= is touched)", () => { setLocation("http://localhost/providers?sort=name&provider=1881068062"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); act(() => { result.current?.open("1881068063"); }); const openUrl = pushStateMock.mock.calls[0][2] as string; expect(openUrl).toContain("sort=name"); expect(openUrl).toMatch(/[?&]provider=1881068063/); act(() => { result.current?.close(); }); const closeUrl = pushStateMock.mock.calls[1][2] as string; expect(closeUrl).toContain("sort=name"); expect(closeUrl).not.toContain("provider="); unmount(); }); it("close() pushes a new history entry with the ?provider= param stripped", () => { setLocation("http://localhost/providers?provider=1881068062"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); act(() => { result.current?.close(); }); expect(pushStateMock).toHaveBeenCalledTimes(1); expect(replaceStateMock).not.toHaveBeenCalled(); const urlArg = pushStateMock.mock.calls[0][2] as string; expect(urlArg).not.toContain("?provider="); expect(result.current?.providerNpi).toBeNull(); unmount(); }); it("updates providerNpi in response to popstate (browser back/forward)", async () => { setLocation("http://localhost/providers?provider=1881068062"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); expect(result.current?.providerNpi).toBe("1881068062"); setLocation("http://localhost/providers?provider=1881068063"); await act(async () => { window.dispatchEvent(new PopStateEvent("popstate")); await Promise.resolve(); }); expect(result.current?.providerNpi).toBe("1881068063"); unmount(); }); it("does not collide with the existing ?claim= or ?remit= params (orthogonal keys)", () => { // The providers drawer is independent of the claims and remits // drawers — all three can be open simultaneously in the future, and // the hooks must not stomp each other's URL state. Opening a // provider must leave ?claim= and ?remit= intact. setLocation("http://localhost/?claim=CLM-1&remit=REM-1"); const { result, unmount } = renderHook(() => useProviderDrawerUrlState()); act(() => { result.current?.open("1881068062"); }); const openUrl = pushStateMock.mock.calls[0][2] as string; expect(openUrl).toContain("claim=CLM-1"); expect(openUrl).toContain("remit=REM-1"); expect(openUrl).toMatch(/[?&]provider=1881068062/); unmount(); }); });