learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1import "fake-indexeddb/auto";
2import { afterEach, beforeEach, describe, expect, it } from "vitest";
3import { db, generateLocalId, isLocalId, type LocalDeck, type LocalNote } from "../db";
4
5describe("db", () => {
6 beforeEach(async () => {
7 await db.decks.clear();
8 await db.cards.clear();
9 await db.notes.clear();
10 await db.syncQueue.clear();
11 });
12
13 afterEach(async () => {
14 await db.decks.clear();
15 await db.cards.clear();
16 await db.notes.clear();
17 await db.syncQueue.clear();
18 });
19
20 describe("generateLocalId", () => {
21 it("should generate unique IDs with local_ prefix", () => {
22 const id1 = generateLocalId();
23 const id2 = generateLocalId();
24
25 expect(id1).toMatch(/^local_\d+_[a-z0-9]+$/);
26 expect(id2).toMatch(/^local_\d+_[a-z0-9]+$/);
27 expect(id1).not.toBe(id2);
28 });
29 });
30
31 describe("isLocalId", () => {
32 it("should return true for local IDs", () => {
33 expect(isLocalId("local_123_abc")).toBe(true);
34 expect(isLocalId("local_")).toBe(true);
35 });
36
37 it("should return false for server IDs", () => {
38 expect(isLocalId("deck-123")).toBe(false);
39 expect(isLocalId("uuid-abc-def")).toBe(false);
40 });
41 });
42
43 describe("decks table", () => {
44 it("should insert and retrieve decks", async () => {
45 const deck: LocalDeck = {
46 id: "deck-1",
47 ownerDid: "did:plc:test",
48 title: "Test Deck",
49 description: "A test deck",
50 tags: ["test"],
51 visibility: { type: "Private" },
52 syncStatus: "local_only",
53 localVersion: 1,
54 updatedAt: new Date().toISOString(),
55 };
56
57 await db.decks.put(deck);
58 const retrieved = await db.decks.get("deck-1");
59
60 expect(retrieved).toBeDefined();
61 expect(retrieved?.title).toBe("Test Deck");
62 expect(retrieved?.syncStatus).toBe("local_only");
63 });
64
65 it("should query decks by ownerDid", async () => {
66 await db.decks.bulkPut([{
67 id: "deck-1",
68 ownerDid: "did:alice",
69 title: "Alice Deck 1",
70 description: "",
71 tags: [],
72 visibility: { type: "Private" },
73 syncStatus: "synced",
74 localVersion: 1,
75 updatedAt: new Date().toISOString(),
76 }, {
77 id: "deck-2",
78 ownerDid: "did:alice",
79 title: "Alice Deck 2",
80 description: "",
81 tags: [],
82 visibility: { type: "Public" },
83 syncStatus: "pending_push",
84 localVersion: 2,
85 updatedAt: new Date().toISOString(),
86 }, {
87 id: "deck-3",
88 ownerDid: "did:bob",
89 title: "Bob Deck",
90 description: "",
91 tags: [],
92 visibility: { type: "Private" },
93 syncStatus: "local_only",
94 localVersion: 1,
95 updatedAt: new Date().toISOString(),
96 }]);
97
98 const aliceDecks = await db.decks.where("ownerDid").equals("did:alice").toArray();
99 expect(aliceDecks).toHaveLength(2);
100 expect(aliceDecks.map((d) => d.title)).toContain("Alice Deck 1");
101 expect(aliceDecks.map((d) => d.title)).toContain("Alice Deck 2");
102 });
103
104 it("should query decks by syncStatus", async () => {
105 await db.decks.bulkPut([{
106 id: "deck-1",
107 ownerDid: "did:test",
108 title: "Synced",
109 description: "",
110 tags: [],
111 visibility: { type: "Private" },
112 syncStatus: "synced",
113 localVersion: 1,
114 updatedAt: new Date().toISOString(),
115 }, {
116 id: "deck-2",
117 ownerDid: "did:test",
118 title: "Conflict",
119 description: "",
120 tags: [],
121 visibility: { type: "Private" },
122 syncStatus: "conflict",
123 localVersion: 1,
124 updatedAt: new Date().toISOString(),
125 }]);
126
127 const conflicts = await db.decks.where("syncStatus").equals("conflict").toArray();
128 expect(conflicts).toHaveLength(1);
129 expect(conflicts[0].title).toBe("Conflict");
130 });
131 });
132
133 describe("notes table", () => {
134 it("should insert and retrieve notes", async () => {
135 const note: LocalNote = {
136 id: "note-1",
137 ownerDid: "did:plc:test",
138 title: "Test Note",
139 body: "This is a test note",
140 tags: ["test"],
141 visibility: { type: "Private" },
142 links: [],
143 syncStatus: "local_only",
144 localVersion: 1,
145 updatedAt: new Date().toISOString(),
146 };
147
148 await db.notes.put(note);
149 const retrieved = await db.notes.get("note-1");
150
151 expect(retrieved).toBeDefined();
152 expect(retrieved?.title).toBe("Test Note");
153 expect(retrieved?.body).toBe("This is a test note");
154 });
155 });
156
157 describe("syncQueue table", () => {
158 it("should auto-increment IDs", async () => {
159 const id1 = await db.syncQueue.add({
160 entityType: "deck",
161 entityId: "deck-1",
162 operation: "push",
163 createdAt: new Date().toISOString(),
164 retryCount: 0,
165 });
166
167 const id2 = await db.syncQueue.add({
168 entityType: "note",
169 entityId: "note-1",
170 operation: "push",
171 createdAt: new Date().toISOString(),
172 retryCount: 0,
173 });
174
175 expect(id2).toBeGreaterThan(id1!);
176 });
177
178 it("should count pending items", async () => {
179 await db.syncQueue.bulkAdd([{
180 entityType: "deck",
181 entityId: "deck-1",
182 operation: "push",
183 createdAt: new Date().toISOString(),
184 retryCount: 0,
185 }, {
186 entityType: "deck",
187 entityId: "deck-2",
188 operation: "push",
189 createdAt: new Date().toISOString(),
190 retryCount: 0,
191 }, {
192 entityType: "note",
193 entityId: "note-1",
194 operation: "push",
195 createdAt: new Date().toISOString(),
196 retryCount: 0,
197 }]);
198
199 const count = await db.syncQueue.count();
200 expect(count).toBe(3);
201 });
202 });
203});