File
Blob: tests/client/stores/workspace-lifecycle.test.ts
| 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 2 | import { createFreshDb, deleteDb } from "@tests/client/util/idb"; |
| 3 | import { createMember, createMembershipSummary, createPage, createWorkspace } from "@tests/client/util/fixtures"; |
| 4 | import type { BlandDatabase } from "@/client/stores/db/bland-db"; |
| 5 | |
| 6 | let db: BlandDatabase; |
| 7 | let replicaCommands: typeof import("@/client/stores/db/workspace-replica").replicaCommands; |
| 8 | let directoryCommands: typeof import("@/client/stores/db/workspace-directory").directoryCommands; |
| 9 | let navigationCommands: typeof import("@/client/stores/db/workspace-navigation").navigationCommands; |
| 10 | let workspaceLifecycleCommands: typeof import("@/client/stores/db/workspace-lifecycle").workspaceLifecycleCommands; |
| 11 | |
| 12 | beforeEach(async () => { |
| 13 | vi.resetModules(); |
| 14 | db = createFreshDb(); |
| 15 | vi.doMock("@/client/stores/db/bland-db", async () => { |
| 16 | const actual = await vi.importActual<typeof import("@/client/stores/db/bland-db")>("@/client/stores/db/bland-db"); |
| 17 | return { ...actual, db }; |
| 18 | }); |
| 19 | replicaCommands = (await import("@/client/stores/db/workspace-replica")).replicaCommands; |
| 20 | directoryCommands = (await import("@/client/stores/db/workspace-directory")).directoryCommands; |
| 21 | navigationCommands = (await import("@/client/stores/db/workspace-navigation")).navigationCommands; |
| 22 | workspaceLifecycleCommands = (await import("@/client/stores/db/workspace-lifecycle")).workspaceLifecycleCommands; |
| 23 | }); |
| 24 | |
| 25 | afterEach(async () => { |
| 26 | await deleteDb(db); |
| 27 | vi.restoreAllMocks(); |
| 28 | }); |
| 29 | |
| 30 | describe("workspaceLifecycleCommands.removeWorkspace", () => { |
| 31 | async function seedTwoWorkspaces() { |
| 32 | const target = createMembershipSummary({ id: "ws-1", slug: "one" }); |
| 33 | const other = createMembershipSummary({ id: "ws-2", slug: "two" }); |
| 34 | await directoryCommands.replaceAll([target, other]); |
| 35 | |
| 36 | await replicaCommands.replaceWorkspace({ |
| 37 | workspace: createWorkspace({ id: "ws-1" }), |
| 38 | accessMode: "member", |
| 39 | workspaceRole: "member", |
| 40 | pages: [createPage({ id: "p1", workspace_id: "ws-1" }), createPage({ id: "p2", workspace_id: "ws-1" })], |
| 41 | members: [createMember({ workspace_id: "ws-1" })], |
| 42 | }); |
| 43 | await replicaCommands.replaceWorkspace({ |
| 44 | workspace: createWorkspace({ id: "ws-2" }), |
| 45 | accessMode: "member", |
| 46 | workspaceRole: "member", |
| 47 | pages: [createPage({ id: "p3", workspace_id: "ws-2" })], |
| 48 | members: [createMember({ workspace_id: "ws-2", user_id: "u-other" })], |
| 49 | }); |
| 50 | await replicaCommands.upsertPageAccess("p1", "edit"); |
| 51 | await replicaCommands.upsertPageAccess("p2", "view"); |
| 52 | await replicaCommands.upsertPageAccess("p3", "edit"); |
| 53 | |
| 54 | await navigationCommands.setLastVisitedWorkspaceId("ws-1"); |
| 55 | await navigationCommands.setLastVisitedPage("ws-1", "p1"); |
| 56 | await navigationCommands.setLastVisitedPage("ws-2", "p3"); |
| 57 | } |
| 58 | |
| 59 | it("cascades across directory, replica, pages, members, pageAccess, and lastVisitedPages", async () => { |
| 60 | await seedTwoWorkspaces(); |
| 61 | |
| 62 | await workspaceLifecycleCommands.removeWorkspace("ws-1"); |
| 63 | |
| 64 | expect(await db.memberWorkspaces.get("ws-1")).toBeUndefined(); |
| 65 | expect(await db.workspaceReplicas.get("ws-1")).toBeUndefined(); |
| 66 | expect(await db.workspacePages.where("workspace_id").equals("ws-1").count()).toBe(0); |
| 67 | expect(await db.workspaceMembers.where("workspace_id").equals("ws-1").count()).toBe(0); |
| 68 | expect(await db.pageAccess.get("p1")).toBeUndefined(); |
| 69 | expect(await db.pageAccess.get("p2")).toBeUndefined(); |
| 70 | expect(await db.lastVisitedPages.get("ws-1")).toBeUndefined(); |
| 71 | }); |
| 72 | |
| 73 | it("clears lastVisitedWorkspaceId when it matches the removed workspace", async () => { |
| 74 | await seedTwoWorkspaces(); |
| 75 | await workspaceLifecycleCommands.removeWorkspace("ws-1"); |
| 76 | const lv = await db.workspaceMeta.get("lastVisitedWorkspaceId"); |
| 77 | expect(lv?.value).toBeNull(); |
| 78 | }); |
| 79 | |
| 80 | it("preserves lastVisitedWorkspaceId when another workspace was the target", async () => { |
| 81 | await seedTwoWorkspaces(); |
| 82 | await workspaceLifecycleCommands.removeWorkspace("ws-2"); |
| 83 | const lv = await db.workspaceMeta.get("lastVisitedWorkspaceId"); |
| 84 | expect(lv?.value).toBe("ws-1"); |
| 85 | }); |
| 86 | |
| 87 | it("leaves other workspaces' pages, members, and pageAccess intact", async () => { |
| 88 | await seedTwoWorkspaces(); |
| 89 | await workspaceLifecycleCommands.removeWorkspace("ws-1"); |
| 90 | expect(await db.workspaceReplicas.get("ws-2")).toBeDefined(); |
| 91 | expect(await db.workspacePages.get("p3")).toBeDefined(); |
| 92 | expect((await db.workspaceMembers.where("workspace_id").equals("ws-2").toArray()).length).toBe(1); |
| 93 | expect((await db.pageAccess.get("p3"))?.mode).toBe("edit"); |
| 94 | expect(await db.lastVisitedPages.get("ws-2")).toBeDefined(); |
| 95 | }); |
| 96 | }); |
| 97 | |
| 98 | describe("workspaceLifecycleCommands.clearAllLocal", () => { |
| 99 | it("wipes every workspace-scoped table and resets lastVisitedWorkspaceId but keeps the meta row structure", async () => { |
| 100 | await directoryCommands.replaceAll([createMembershipSummary({ id: "ws-1" })]); |
| 101 | await replicaCommands.replaceWorkspace({ |
| 102 | workspace: createWorkspace({ id: "ws-1" }), |
| 103 | accessMode: "member", |
| 104 | workspaceRole: "member", |
| 105 | pages: [createPage({ id: "p1", workspace_id: "ws-1" })], |
| 106 | members: [createMember({ workspace_id: "ws-1" })], |
| 107 | }); |
| 108 | await replicaCommands.upsertPageAccess("p1", "edit"); |
| 109 | await navigationCommands.setLastVisitedWorkspaceId("ws-1"); |
| 110 | await navigationCommands.setLastVisitedPage("ws-1", "p1"); |
| 111 | await db.workspaceMeta.put({ key: "owner", value: "user-1" }); |
| 112 | |
| 113 | await workspaceLifecycleCommands.clearAllLocal(); |
| 114 | |
| 115 | expect(await db.memberWorkspaces.count()).toBe(0); |
| 116 | expect(await db.workspaceReplicas.count()).toBe(0); |
| 117 | expect(await db.workspacePages.count()).toBe(0); |
| 118 | expect(await db.workspaceMembers.count()).toBe(0); |
| 119 | expect(await db.pageAccess.count()).toBe(0); |
| 120 | expect(await db.sharedInboxItems.count()).toBe(0); |
| 121 | expect(await db.lastVisitedPages.count()).toBe(0); |
| 122 | // owner row is NOT touched by clearAllLocal; the caller |
| 123 | // (ensureWorkspaceLocalOwner / resetWorkspaceLocalOwner) writes the new |
| 124 | // owner value separately. |
| 125 | expect((await db.workspaceMeta.get("owner"))?.value).toBe("user-1"); |
| 126 | expect((await db.workspaceMeta.get("lastVisitedWorkspaceId"))?.value).toBeNull(); |
| 127 | }); |
| 128 | }); |