Skip to content
File

Blob: tests/client/hooks/use-shared-inbox.test.ts

typescript112 lines
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2import { createFreshDb, deleteDb } from "@tests/client/util/idb";
3import type { BlandDatabase } from "@/client/stores/db/bland-db";
4import type { SharedPagesResponse, SharedWithMeItem } from "@/shared/types";
5 
6const sharedWithMeMock = vi.fn();
7 
8let db: BlandDatabase;
9let fetchSharedInbox: typeof import("@/client/hooks/use-shared-inbox").fetchSharedInbox;
10 
11const ITEM: SharedWithMeItem = {
12 page_id: "p1",
13 title: "Shared Page",
14 icon: null,
15 cover_url: null,
16 workspace: { id: "ws-1", name: "WS", slug: "ws", icon: null, role: null },
17 permission: "view",
18 shared_by: "user-2",
19 shared_by_name: "Alice",
20 shared_at: "2026-04-01T00:00:00.000Z",
21};
22 
23function response(items: SharedWithMeItem[]): SharedPagesResponse {
24 return { items, workspace_summaries: [] };
25}
26 
27beforeEach(async () => {
28 vi.resetModules();
29 sharedWithMeMock.mockReset();
30 db = createFreshDb();
31 vi.doMock("@/client/stores/db/bland-db", async () => {
32 const actual = await vi.importActual<typeof import("@/client/stores/db/bland-db")>("@/client/stores/db/bland-db");
33 return { ...actual, db };
34 });
35 vi.doMock("@/client/lib/api", () => ({
36 api: {
37 shares: {
38 sharedWithMe: sharedWithMeMock,
39 },
40 },
41 }));
42 
43 fetchSharedInbox = (await import("@/client/hooks/use-shared-inbox")).fetchSharedInbox;
44});
45 
46afterEach(async () => {
47 await deleteDb(db);
48 vi.restoreAllMocks();
49});
50 
51describe("fetchSharedInbox", () => {
52 it("dedupes concurrent calls into a single API request", async () => {
53 let resolveApi!: (payload: SharedPagesResponse) => void;
54 sharedWithMeMock.mockImplementationOnce(
55 () =>
56 new Promise<SharedPagesResponse>((resolve) => {
57 resolveApi = resolve;
58 }),
59 );
60 
61 const first = fetchSharedInbox();
62 const second = fetchSharedInbox();
63 
64 expect(sharedWithMeMock).toHaveBeenCalledTimes(1);
65 
66 resolveApi(response([ITEM]));
67 
68 const [firstResponse, secondResponse] = await Promise.all([first, second]);
69 
70 expect(firstResponse.items).toEqual([ITEM]);
71 expect(secondResponse.items).toEqual([ITEM]);
72 const rows = await db.sharedInboxItems.toArray();
73 expect(rows.map((r) => r.item)).toEqual([ITEM]);
74 expect(await db.sharedInboxWorkspaceSummaries.count()).toBe(0);
75 });
76 
77 it("starts a fresh fetch after a previous call resolves", async () => {
78 sharedWithMeMock.mockResolvedValueOnce(response([ITEM]));
79 await fetchSharedInbox();
80 expect(sharedWithMeMock).toHaveBeenCalledTimes(1);
81 
82 sharedWithMeMock.mockResolvedValueOnce(response([{ ...ITEM, page_id: "p2", title: "Second" }]));
83 const result = await fetchSharedInbox();
84 expect(sharedWithMeMock).toHaveBeenCalledTimes(2);
85 expect(result.items[0]?.page_id).toBe("p2");
86 });
87 
88 it("clears the in-flight slot when the API rejects, allowing a retry", async () => {
89 sharedWithMeMock.mockRejectedValueOnce(new Error("network"));
90 await expect(fetchSharedInbox()).rejects.toThrow("network");
91 
92 sharedWithMeMock.mockResolvedValueOnce(response([ITEM]));
93 const result = await fetchSharedInbox();
94 expect(result.items).toEqual([ITEM]);
95 expect(sharedWithMeMock).toHaveBeenCalledTimes(2);
96 });
97 
98 it("writes both cross-workspace items and same-workspace summaries through sharedInboxCommands", async () => {
99 sharedWithMeMock.mockResolvedValueOnce({
100 items: [ITEM],
101 workspace_summaries: [{ workspace: { id: "ws-2", name: "Home", slug: "home", icon: null }, count: 3 }],
102 });
103 
104 const result = await fetchSharedInbox();
105 expect(result.workspace_summaries).toHaveLength(1);
106 const summaryRows = await db.sharedInboxWorkspaceSummaries.toArray();
107 expect(summaryRows.map((r) => r.summary)).toEqual([
108 { workspace: { id: "ws-2", name: "Home", slug: "home", icon: null }, count: 3 },
109 ]);
110 });
111});