| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { createMemoryRouter, redirect } from "react-router"; |
| 4 | import { pageOwnsNavigation, pendingNavigationOwns, sidebarLocation } from "./navigation-owner.ts"; |
| 5 | |
| 6 | test("an old page loses navigation ownership as soon as a newer click starts", async () => { |
| 7 | let finish!: (value: Response) => void; |
| 8 | const slow = new Promise<Response>((resolve) => { finish = resolve; }); |
| 9 | const router = createMemoryRouter([ |
| 10 | { path: "/home" }, |
| 11 | { path: "/artifacts", loader: () => slow }, |
| 12 | { path: "/code", loader: () => "latest" }, |
| 13 | { path: "/old-redirect" }, |
| 14 | ], { initialEntries: ["/home"] }); |
| 15 | try { |
| 16 | const homeKey = router.state.location.key; |
| 17 | assert.equal(pageOwnsNavigation(router.state, homeKey), true); |
| 18 | const first = router.navigate("/artifacts"); |
| 19 | const firstKey = router.state.navigation.location?.key; |
| 20 | assert.equal(sidebarLocation(router.state).pathname, "/artifacts", "sidebar follows the pending click immediately"); |
| 21 | assert.equal(pageOwnsNavigation(router.state, homeKey), false, "old effects must not override a pending click"); |
| 22 | assert.equal(pendingNavigationOwns(router.state.navigation, firstKey), true); |
| 23 | await router.navigate("/code"); |
| 24 | assert.equal(sidebarLocation(router.state).pathname, "/code"); |
| 25 | finish(redirect("/old-redirect")); |
| 26 | await first; |
| 27 | assert.equal(router.state.location.pathname, "/code"); |
| 28 | assert.equal(sidebarLocation(router.state).pathname, "/code", "late redirects cannot move the sidebar back"); |
| 29 | assert.equal(pageOwnsNavigation(router.state, homeKey), false); |
| 30 | assert.equal(pendingNavigationOwns(router.state.navigation, firstKey), false, "an old recovery timer must not fire"); |
| 31 | await router.navigate("/home"); |
| 32 | assert.equal(pageOwnsNavigation(router.state, homeKey), false, "returning to the same URL is a new visit"); |
| 33 | } finally { router.dispose(); } |
| 34 | }); |
| 35 | |
| 36 | test("repeated clicks to the same path have different recovery owners", () => { |
| 37 | assert.equal(pendingNavigationOwns({ state: "loading", location: { key: "second-click" } }, "first-click"), false); |
| 38 | assert.equal(pendingNavigationOwns({ state: "submitting", location: { key: "first-click" } }, "first-click"), false); |
| 39 | }); |
| 40 | |
| 41 | for (const [firstPath, latestPath] of [["/artifacts/one", "/artifacts/two"], ["/artifacts/one", "/code"]]) { |
| 42 | test(`latest sidebar destination wins: ${firstPath} then ${latestPath}`, async () => { |
| 43 | let finishFirst!: (value: string) => void; |
| 44 | let finishLatest!: (value: string) => void; |
| 45 | const firstLoad = new Promise<string>((resolve) => { finishFirst = resolve; }); |
| 46 | const latestLoad = new Promise<string>((resolve) => { finishLatest = resolve; }); |
| 47 | const router = createMemoryRouter([ |
| 48 | { path: "/home" }, |
| 49 | { path: firstPath, loader: () => firstLoad }, |
| 50 | { path: latestPath, loader: () => latestLoad }, |
| 51 | ], { initialEntries: ["/home"] }); |
| 52 | const seen: string[] = []; |
| 53 | const unsubscribe = router.subscribe((state) => seen.push(sidebarLocation(state).pathname)); |
| 54 | try { |
| 55 | const first = router.navigate(firstPath); |
| 56 | const latest = router.navigate(latestPath); |
| 57 | assert.equal(sidebarLocation(router.state).pathname, latestPath); |
| 58 | finishFirst("stale"); |
| 59 | await first; |
| 60 | assert.equal(router.state.location.pathname, "/home", "stale page must not commit while the latest page loads"); |
| 61 | assert.equal(sidebarLocation(router.state).pathname, latestPath); |
| 62 | finishLatest("latest"); |
| 63 | await latest; |
| 64 | assert.equal(router.state.location.pathname, latestPath); |
| 65 | assert.deepEqual(seen, [firstPath, latestPath, latestPath], "subscribers never see the stale destination again"); |
| 66 | } finally { unsubscribe(); router.dispose(); } |
| 67 | }); |
| 68 | } |