From 3e2426187caca9b06e2f3e445d7e367f9cb135c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 11 Oct 2026 13:59:59 +0200 Subject: [PATCH] test(checkups): pin that a linked visit older than the list's newest hundred is read and opened --- .../__tests__/deep-linked-record.test.ts | 96 +++++++++++++++++++ src/hooks/use-deep-linked-record.ts | 66 +++++++++---- 2 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 src/hooks/__tests__/deep-linked-record.test.ts diff --git a/src/hooks/__tests__/deep-linked-record.test.ts b/src/hooks/__tests__/deep-linked-record.test.ts new file mode 100644 index 000000000..a8b0aa48c --- /dev/null +++ b/src/hooks/__tests__/deep-linked-record.test.ts @@ -0,0 +1,96 @@ +/** + * `/checkups?visit=` from a search hit. The visits list reads the newest + * hundred of each arm, and a search reaches back further than that, so a hit + * can name a visit the list does not hold. That visit is read on its own and + * opened like a listed one. + */ +import { describe, expect, it, vi } from "vitest"; + +import { openDeepLinked } from "../use-deep-linked-record"; + +type Visit = { id: string; occurredAt: string }; + +/** The newest hundred visits, the way the list holds them. */ +const newestHundred: Visit[] = Array.from({ length: 100 }, (_, i) => ({ + id: `v${i}`, + occurredAt: `2026-01-01T00:00:00Z`, +})); +const older: Visit = { id: "v-older", occurredAt: "2014-05-06T09:00:00Z" }; + +describe("a link to a visit older than the newest hundred", () => { + it("reads it on its own and opens it", async () => { + const resolve = vi.fn(async (id: string) => + id === older.id ? older : null, + ); + const open = vi.fn(); + const scrollTo = vi.fn(); + + const outcome = await openDeepLinked({ + wanted: older.id, + records: newestHundred, + open, + resolve, + stillWanted: () => true, + scrollTo, + }); + + expect(outcome).toBe("resolved"); + expect(resolve).toHaveBeenCalledWith("v-older"); + expect(open).toHaveBeenCalledWith(older); + // Not on the page, so there is nothing to scroll to. + expect(scrollTo).not.toHaveBeenCalled(); + }); + + it("opens a listed visit without a second read", async () => { + const resolve = vi.fn(); + const open = vi.fn(); + const scrollTo = vi.fn(); + const outcome = await openDeepLinked({ + wanted: "v42", + records: newestHundred, + open, + resolve, + stillWanted: () => true, + scrollTo, + }); + expect(outcome).toBe("listed"); + expect(resolve).not.toHaveBeenCalled(); + expect(scrollTo).toHaveBeenCalledWith("v42"); + expect(open).toHaveBeenCalledWith(newestHundred[42]); + }); + + it("leaves the page alone when the visit is gone or refused", async () => { + const open = vi.fn(); + for (const resolve of [ + async () => null, + async () => { + throw new Error("403"); + }, + ]) { + const outcome = await openDeepLinked({ + wanted: "v-gone", + records: newestHundred, + open, + resolve, + stillWanted: () => true, + scrollTo: () => {}, + }); + expect(outcome).toBe("none"); + } + expect(open).not.toHaveBeenCalled(); + }); + + it("does not open a late answer for a link that is no longer wanted", async () => { + const open = vi.fn(); + const outcome = await openDeepLinked({ + wanted: older.id, + records: newestHundred, + open, + resolve: async () => older, + stillWanted: () => false, + scrollTo: () => {}, + }); + expect(outcome).toBe("none"); + expect(open).not.toHaveBeenCalled(); + }); +}); diff --git a/src/hooks/use-deep-linked-record.ts b/src/hooks/use-deep-linked-record.ts index 56582669d..5cfe09d3b 100644 --- a/src/hooks/use-deep-linked-record.ts +++ b/src/hooks/use-deep-linked-record.ts @@ -56,24 +56,52 @@ export function useDeepLinkedRecord({ useEffect(() => { if (!wanted || !records || handled.current === wanted) return; handled.current = wanted; - const record = records.find((entry) => entry.id === wanted); - if (!record) { - if (!resolve || !open) return; - void resolve(wanted) - .then((found) => { - // Still on the page, and still the link it was asked for. - if (found && mounted.current && handled.current === wanted) { - open(found); - } - }) - .catch(() => { - // Gone or refused: the page stays as it would without the link. - }); - return; - } - document - .querySelector(`[${anchorAttribute}="${CSS.escape(wanted)}"]`) - ?.scrollIntoView({ block: "center" }); - open?.(record); + void openDeepLinked({ + wanted, + records, + open, + resolve, + // Still on the page, and still the link it was asked for. + stillWanted: () => mounted.current && handled.current === wanted, + scrollTo: (id) => + document + .querySelector(`[${anchorAttribute}="${CSS.escape(id)}"]`) + ?.scrollIntoView({ block: "center" }), + }); }, [wanted, records, anchorAttribute, open, resolve]); } + +/** + * What a link does once the list has loaded, apart from React: a record the + * list holds is scrolled to and opened; one it does not hold (older than the + * bounded read, say the newest hundred visits) is read on its own through + * `resolve` and opened when it comes back and the link is still wanted. + * Resolves to which of the three happened. + */ +export async function openDeepLinked(args: { + wanted: string; + records: readonly T[]; + open?: (record: T) => void; + resolve?: (id: string) => Promise; + stillWanted: () => boolean; + scrollTo: (id: string) => void; +}): Promise<"listed" | "resolved" | "none"> { + const { wanted, records, open, resolve } = args; + const record = records.find((entry) => entry.id === wanted); + if (record) { + args.scrollTo(wanted); + open?.(record); + return "listed"; + } + if (!resolve || !open) return "none"; + let found: T | null; + try { + found = await resolve(wanted); + } catch { + // Gone or refused: the page stays as it would without the link. + return "none"; + } + if (!found || !args.stillWanted()) return "none"; + open(found); + return "resolved"; +}