// @vitest-environment jsdom import { act, createElement, type ComponentProps } from "react"; import { createRoot, type Root } from "react-dom/client"; import { RouterContext } from "next/dist/shared/lib/router-context.shared-runtime"; import type { NextRouter } from "next/router"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import AppLink from "@/components/app-link"; import { useAppNavigation, useAppRouter } from "./use-app-router"; import { clearPendingNavigation } from "./pending-navigation"; const state = vi.hoisted(() => ({ reuseDestination: vi.fn(() => true), hasSession: true, router: { push: vi.fn(), replace: vi.fn(), prefetch: vi.fn(), refresh: vi.fn(), back: vi.fn(), forward: vi.fn() }, })); vi.mock("next/navigation", () => ({ useRouter: () => state.router })); vi.mock("./app-tabs-context", () => ({ useOptionalAppTabSession: () => state.hasSession ? { reuseDestination: state.reuseDestination } : null, })); vi.mock("./app-tab-session-context", () => ({ useOptionalAppTabSession: () => state.hasSession ? { reuseDestination: state.reuseDestination } : null, })); let root: Root; let container: HTMLDivElement; beforeEach(() => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.clearAllMocks(); state.hasSession = true; state.reuseDestination.mockReturnValue(true); container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(async () => { await act(() => root.unmount()); container.remove(); // Desktop navigations start a pending destination and its timers. clearPendingNavigation(); vi.unstubAllGlobals(); }); async function renderLink(props: ComponentProps) { const linkRouter = { ...state.router, pathname: "/home", asPath: "/home", query: {}, isReady: true } as unknown as NextRouter; await act(() => root.render(createElement(RouterContext.Provider, { value: linkRouter }, createElement(AppLink, { ...props, prefetch: false }, "Open")))); return container.querySelector("a")!; } async function click(anchor: HTMLAnchorElement, options: MouseEventInit = {}) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, button: 0, ...options }); // Suppress jsdom's unimplemented browser navigation after React handles it. const cancelBrowser = (received: Event) => received.preventDefault(); document.addEventListener("click", cancelBrowser, { once: true }); await act(() => { anchor.dispatchEvent(event); }); } describe("application navigation entry points", () => { it("reuses programmatic opening while preserving fallback options and local replacements", async () => { let router!: ReturnType; let open!: ReturnType; function Caller() { router = useAppRouter(); open = useAppNavigation(); return null; } await act(() => root.render(createElement(Caller))); router.push("/pull-requests?pr=a", { scroll: false }); expect(state.reuseDestination).toHaveBeenCalledWith("/pull-requests?pr=a"); expect(state.router.push).not.toHaveBeenCalled(); state.reuseDestination.mockReturnValue(false); router.push("/pull-requests?pr=b", { scroll: false }); expect(state.router.push).toHaveBeenCalledWith("/pull-requests?pr=b", { scroll: false }); const history = vi.fn(); open("/projects/p/pages/a", history); expect(history).toHaveBeenCalledOnce(); state.reuseDestination.mockClear(); router.replace("/pull-requests", { scroll: false }); expect(state.router.replace).toHaveBeenCalledWith("/pull-requests", { scroll: false }); expect(state.reuseDestination).not.toHaveBeenCalled(); }); it("preserves ordinary routing without an application tab provider", async () => { state.hasSession = false; let router!: ReturnType; function Caller() { router = useAppRouter(); return null; } await act(() => root.render(createElement(Caller))); router.push("/home"); expect(state.router.push).toHaveBeenCalledWith("/home"); }); it("reuses an ordinary link using the rendered object-form destination", async () => { const onClick = vi.fn(); const onNavigate = vi.fn(); const anchor = await renderLink({ href: { pathname: "/pull-requests", query: { pr: "a" } }, onClick, onNavigate }); await click(anchor); expect(onClick).toHaveBeenCalledOnce(); expect(onNavigate).toHaveBeenCalledOnce(); expect(state.reuseDestination).toHaveBeenCalledExactlyOnceWith("/pull-requests?pr=a"); }); it("keeps Next link navigation when no destination matches", async () => { state.reuseDestination.mockReturnValue(false); await click(await renderLink({ href: "/all", scroll: false })); expect(state.router.push).toHaveBeenCalledWith("/all", { scroll: false }); }); it.each([{ metaKey: true }, { ctrlKey: true }, { shiftKey: true }, { altKey: true }])( "preserves modified link opening: %o", async (options) => { await click(await renderLink({ href: "/all" }), options); expect(state.reuseDestination).not.toHaveBeenCalled(); }, ); it.each([{ target: "_blank" }, { download: true }, { href: "https://example.com/all" }])( "preserves explicit browser destinations: %o", async (props) => { await click(await renderLink({ href: "/all", ...props })); expect(state.reuseDestination).not.toHaveBeenCalled(); }, ); it("honors custom click and navigation cancellation", async () => { await click(await renderLink({ href: "/all", onClick: (event) => event.preventDefault() })); expect(state.reuseDestination).not.toHaveBeenCalled(); await click(await renderLink({ href: "/all", onNavigate: (event) => event.preventDefault() })); expect(state.reuseDestination).not.toHaveBeenCalled(); }); });