import { act, renderHook } from "@testing-library/react"; import { expect, it, vi } from "vitest"; import type { ColumnRegistry } from "../decks/column-tools"; import type { DeckColumn, DeckView, Workspace } from "../decks/model"; import type { ResearchCitation } from "./model"; import { useCitationNavigation } from "./use-citation-navigation"; vi.mock("../navigation/use-url-state", () => ({ useUrlState: () => [{ citation: "twitter:123" }, vi.fn<() => void>()], })); const column: DeckColumn = { id: "column", title: "Research", connectionId: "twitter", source: { platform: "twitter", kind: "search", query: "WebMCP", product: "Latest", following: false, }, }; const citation: ResearchCitation = { column, post: { key: "twitter:123", nativeId: "123", platform: "twitter", url: "https://x.com/alice/status/123", text: "Snapshot", author: { name: "Alice", handle: "alice" }, }, }; const deck: DeckView = { id: "saved", title: "Saved", columns: [column], persisted: true, revision: 1, }; function setup(decks: DeckView[], registry: ColumnRegistry = new Map()) { const workspace: Workspace = { decks, activeDeckId: decks[0]?.id ?? "" }; const select = vi.fn[0]["select"]>(); const createTemporary = vi.fn[0]["createTemporary"]>( (title: string, columns: DeckColumn[]): DeckView => ({ id: "created", title, columns, persisted: false, }), ); return { ...renderHook(() => useCitationNavigation({ registry, getWorkspace: () => workspace, select, createTemporary, }), ), select, createTemporary, }; } it("prefers a live loaded card over its old research snapshot", () => { const live = { ...citation.post, text: "Latest live post" }; const registry = new Map([ [column.id, { current: { column, query: { data: { pages: [{ posts: [live] }] } } } }], ]) as ColumnRegistry; const { result, select, createTemporary } = setup([deck], registry); act(() => { expect(result.current.navigate("https://mobile.twitter.com/alice/status/123", [citation])).toBe( true, ); }); expect(result.current.target?.post.text).toBe("Latest live post"); expect(select).not.toHaveBeenCalled(); expect(createTemporary).not.toHaveBeenCalled(); }); it("selects another deck with the same source and connection without changing saved data", () => { const otherColumn = { ...column, id: "other-column" }; const otherDeck = { ...deck, id: "other", columns: [otherColumn] }; const { result, select, createTemporary } = setup([{ ...deck, columns: [] }, otherDeck]); act(() => { result.current.navigate(citation.post.url, [citation]); }); expect(select).toHaveBeenCalledWith("other", false); expect(result.current.target).toMatchObject({ deckId: "other", column: otherColumn, post: citation.post, }); expect(otherDeck.columns).toEqual([otherColumn]); expect(createTemporary).not.toHaveBeenCalled(); }); it.each([ { ...column, connectionId: "different-account" }, { ...column, source: { ...column.source, kind: "search" as const, platform: "twitter" as const, query: "Changed", product: "Latest" as const, following: false, }, }, ])( "creates a one-column temporary deck when an old column ID now means another source", (changed) => { const { result, createTemporary } = setup([{ ...deck, columns: [changed] }]); act(() => { expect(result.current.navigate(citation.post.url, [citation])).toBe(true); }); expect(createTemporary).toHaveBeenCalledWith("Cited posts", [column], false); expect(result.current.target?.deckId).toBe("created"); }, ); it("leaves an unknown URL to the normal external link handler", () => { const { result, select, createTemporary } = setup([deck]); act(() => { expect(result.current.navigate("https://outside.invalid/post", [citation])).toBe(false); }); expect(result.current.target).toBeUndefined(); expect(select).not.toHaveBeenCalled(); expect(createTemporary).not.toHaveBeenCalled(); });