jazz-tools 0.15.14 → 0.15.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +40 -44
  2. package/CHANGELOG.md +19 -0
  3. package/dist/{chunk-6CCJYSYQ.js → chunk-OSVAAVWQ.js} +107 -11
  4. package/dist/chunk-OSVAAVWQ.js.map +1 -0
  5. package/dist/index.js +357 -3
  6. package/dist/index.js.map +1 -1
  7. package/dist/testing.js +1 -1
  8. package/dist/tools/coValues/group.d.ts +1 -0
  9. package/dist/tools/coValues/group.d.ts.map +1 -1
  10. package/dist/tools/coValues/inbox.d.ts.map +1 -1
  11. package/dist/tools/coValues/interfaces.d.ts +58 -2
  12. package/dist/tools/coValues/interfaces.d.ts.map +1 -1
  13. package/dist/tools/coValues/request.d.ts +82 -0
  14. package/dist/tools/coValues/request.d.ts.map +1 -0
  15. package/dist/tools/exports.d.ts +2 -1
  16. package/dist/tools/exports.d.ts.map +1 -1
  17. package/dist/tools/lib/id.d.ts +2 -0
  18. package/dist/tools/lib/id.d.ts.map +1 -0
  19. package/dist/tools/subscribe/SubscriptionScope.d.ts +3 -2
  20. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  21. package/dist/tools/tests/exportImport.test.d.ts +2 -0
  22. package/dist/tools/tests/exportImport.test.d.ts.map +1 -0
  23. package/dist/tools/tests/request.test.d.ts +2 -0
  24. package/dist/tools/tests/request.test.d.ts.map +1 -0
  25. package/package.json +6 -5
  26. package/src/tools/coValues/group.ts +1 -0
  27. package/src/tools/coValues/inbox.ts +4 -3
  28. package/src/tools/coValues/interfaces.ts +177 -2
  29. package/src/tools/coValues/request.ts +631 -0
  30. package/src/tools/exports.ts +8 -0
  31. package/src/tools/lib/id.ts +3 -0
  32. package/src/tools/subscribe/SubscriptionScope.ts +17 -10
  33. package/src/tools/tests/coMap.test.ts +141 -0
  34. package/src/tools/tests/exportImport.test.ts +526 -0
  35. package/src/tools/tests/request.test.ts +951 -0
  36. package/tsup.config.ts +0 -2
  37. package/dist/chunk-6CCJYSYQ.js.map +0 -1
@@ -2103,3 +2103,144 @@ describe("CoMap migration", () => {
2103
2103
  });
2104
2104
  });
2105
2105
  });
2106
+
2107
+ describe("Updating a nested reference", () => {
2108
+ test("should assign a resolved optional reference and expect value is not null", async () => {
2109
+ // Define the schema similar to the server-worker-http example
2110
+ const PlaySelection = co.map({
2111
+ value: z.literal(["rock", "paper", "scissors"]),
2112
+ group: Group,
2113
+ });
2114
+
2115
+ const Player = co.map({
2116
+ account: co.account(),
2117
+ playSelection: PlaySelection.optional(),
2118
+ });
2119
+
2120
+ const Game = co.map({
2121
+ player1: Player,
2122
+ player2: Player,
2123
+ outcome: z.literal(["player1", "player2", "draw"]).optional(),
2124
+ player1Score: z.number(),
2125
+ player2Score: z.number(),
2126
+ });
2127
+
2128
+ // Create accounts for the players
2129
+ const player1Account = await createJazzTestAccount({
2130
+ creationProps: { name: "Player 1" },
2131
+ });
2132
+ const player2Account = await createJazzTestAccount({
2133
+ creationProps: { name: "Player 2" },
2134
+ });
2135
+
2136
+ // Create a game
2137
+ const game = Game.create({
2138
+ player1: Player.create({
2139
+ account: player1Account,
2140
+ }),
2141
+ player2: Player.create({
2142
+ account: player2Account,
2143
+ }),
2144
+ player1Score: 0,
2145
+ player2Score: 0,
2146
+ });
2147
+
2148
+ // Create a group for the play selection (similar to the route logic)
2149
+ const group = Group.create({ owner: Account.getMe() });
2150
+ group.addMember(player1Account, "reader");
2151
+
2152
+ // Load the game to verify the assignment worked
2153
+ const loadedGame = await Game.load(game.id, {
2154
+ resolve: {
2155
+ player1: {
2156
+ account: true,
2157
+ playSelection: true,
2158
+ },
2159
+ player2: {
2160
+ account: true,
2161
+ playSelection: true,
2162
+ },
2163
+ },
2164
+ });
2165
+
2166
+ assert(loadedGame);
2167
+
2168
+ // Create a play selection
2169
+ const playSelection = PlaySelection.create({ value: "rock", group }, group);
2170
+
2171
+ // Assign the play selection to player1 (similar to the route logic)
2172
+ loadedGame.player1.playSelection = playSelection;
2173
+
2174
+ // Verify that the playSelection is not null and has the expected value
2175
+ expect(loadedGame.player1.playSelection).not.toBeNull();
2176
+ expect(loadedGame.player1.playSelection).toBeDefined();
2177
+ });
2178
+
2179
+ test("should assign a resolved reference and expect value to update", async () => {
2180
+ // Define the schema similar to the server-worker-http example
2181
+ const PlaySelection = co.map({
2182
+ value: z.literal(["rock", "paper", "scissors"]),
2183
+ });
2184
+
2185
+ const Player = co.map({
2186
+ account: co.account(),
2187
+ playSelection: PlaySelection,
2188
+ });
2189
+
2190
+ const Game = co.map({
2191
+ player1: Player,
2192
+ player2: Player,
2193
+ outcome: z.literal(["player1", "player2", "draw"]).optional(),
2194
+ player1Score: z.number(),
2195
+ player2Score: z.number(),
2196
+ });
2197
+
2198
+ // Create accounts for the players
2199
+ const player1Account = await createJazzTestAccount({
2200
+ creationProps: { name: "Player 1" },
2201
+ });
2202
+ const player2Account = await createJazzTestAccount({
2203
+ creationProps: { name: "Player 2" },
2204
+ });
2205
+
2206
+ // Create a game
2207
+ const game = Game.create({
2208
+ player1: Player.create({
2209
+ account: player1Account,
2210
+ playSelection: PlaySelection.create({ value: "rock" }),
2211
+ }),
2212
+ player2: Player.create({
2213
+ account: player2Account,
2214
+ playSelection: PlaySelection.create({ value: "paper" }),
2215
+ }),
2216
+ player1Score: 0,
2217
+ player2Score: 0,
2218
+ });
2219
+
2220
+ // Load the game to verify the assignment worked
2221
+ const loadedGame = await Game.load(game.id, {
2222
+ resolve: {
2223
+ player1: {
2224
+ account: true,
2225
+ playSelection: true,
2226
+ },
2227
+ player2: {
2228
+ account: true,
2229
+ playSelection: true,
2230
+ },
2231
+ },
2232
+ });
2233
+
2234
+ assert(loadedGame);
2235
+
2236
+ // Create a play selection
2237
+ const playSelection = PlaySelection.create({ value: "scissors" });
2238
+
2239
+ // Assign the play selection to player1 (similar to the route logic)
2240
+ loadedGame.player1.playSelection = playSelection;
2241
+
2242
+ // Verify that the playSelection is not null and has the expected value
2243
+ expect(loadedGame.player1.playSelection.id).toBe(playSelection.id);
2244
+ expect(loadedGame.player1.playSelection.value).toEqual("scissors");
2245
+ });
2246
+ });
@@ -0,0 +1,526 @@
1
+ import { cojsonInternals } from "cojson";
2
+ import { assert, beforeEach, describe, expect, test } from "vitest";
3
+ import { exportCoValue, importContentPieces } from "../coValues/interfaces.js";
4
+ import { Account, CoPlainText, Group, co, z } from "../exports.js";
5
+ import {
6
+ createJazzTestAccount,
7
+ createJazzTestGuest,
8
+ setupJazzTestSync,
9
+ } from "../testing.js";
10
+
11
+ cojsonInternals.CO_VALUE_LOADING_CONFIG.RETRY_DELAY = 10;
12
+
13
+ beforeEach(async () => {
14
+ await setupJazzTestSync();
15
+ await createJazzTestAccount({
16
+ isCurrentActiveAccount: true,
17
+ });
18
+ });
19
+
20
+ describe("exportCoValue", () => {
21
+ test("exports a simple CoMap", async () => {
22
+ const Person = co.map({
23
+ name: z.string(),
24
+ age: z.number(),
25
+ });
26
+
27
+ const group = Group.create();
28
+ const person = Person.create({ name: "John", age: 30 }, group);
29
+ group.addMember("everyone", "reader");
30
+
31
+ const alice = await createJazzTestAccount();
32
+
33
+ const exported = await exportCoValue(Person, person.id, {
34
+ loadAs: alice,
35
+ });
36
+
37
+ expect(exported).not.toBeNull();
38
+ expect(exported).toBeInstanceOf(Array);
39
+ expect(exported!.length).toBeGreaterThan(0);
40
+
41
+ // Verify the exported content contains the person data
42
+ const hasPersonContent = exported!.some((piece) => piece.id === person.id);
43
+ expect(hasPersonContent).toBe(true);
44
+ });
45
+
46
+ test("exports a CoMap with nested references", async () => {
47
+ const Address = co.map({
48
+ street: z.string(),
49
+ city: z.string(),
50
+ });
51
+
52
+ const Person = co.map({
53
+ name: z.string(),
54
+ address: Address,
55
+ });
56
+
57
+ const group = Group.create();
58
+ const address = Address.create(
59
+ { street: "123 Main St", city: "New York" },
60
+ group,
61
+ );
62
+ const person = Person.create({ name: "John", address }, group);
63
+ group.addMember("everyone", "reader");
64
+
65
+ const alice = await createJazzTestAccount();
66
+
67
+ const exported = await exportCoValue(Person, person.id, {
68
+ resolve: { address: true },
69
+ loadAs: alice,
70
+ });
71
+
72
+ expect(exported).not.toBeNull();
73
+ expect(exported).toBeInstanceOf(Array);
74
+ expect(exported!.length).toBeGreaterThan(0);
75
+
76
+ // Verify both person and address content are exported
77
+ const personContent = exported!.filter((piece) => piece.id === person.id);
78
+ const addressContent = exported!.filter((piece) => piece.id === address.id);
79
+
80
+ expect(personContent.length).toBeGreaterThan(0);
81
+ expect(addressContent.length).toBeGreaterThan(0);
82
+ });
83
+
84
+ test("exports a CoList", async () => {
85
+ const TodoList = co.list(z.string());
86
+
87
+ const group = Group.create();
88
+ const todos = TodoList.create([], group);
89
+ todos.push("Buy groceries");
90
+ todos.push("Walk the dog");
91
+ group.addMember("everyone", "reader");
92
+
93
+ const alice = await createJazzTestAccount();
94
+
95
+ const exported = await exportCoValue(TodoList, todos.id, {
96
+ loadAs: alice,
97
+ });
98
+
99
+ expect(exported).not.toBeNull();
100
+ expect(exported).toBeInstanceOf(Array);
101
+ expect(exported!.length).toBeGreaterThan(0);
102
+
103
+ const hasTodoContent = exported!.some((piece) => piece.id === todos.id);
104
+ expect(hasTodoContent).toBe(true);
105
+ });
106
+
107
+ test("exports a CoStream", async () => {
108
+ const ChatStream = co.feed(z.string());
109
+
110
+ const group = Group.create();
111
+ const chat = ChatStream.create([], group);
112
+ chat.push("Hello");
113
+ chat.push("World");
114
+ group.addMember("everyone", "reader");
115
+
116
+ const alice = await createJazzTestAccount();
117
+
118
+ const exported = await exportCoValue(ChatStream, chat.id, {
119
+ loadAs: alice,
120
+ });
121
+
122
+ expect(exported).not.toBeNull();
123
+ expect(exported).toBeInstanceOf(Array);
124
+ expect(exported!.length).toBeGreaterThan(0);
125
+
126
+ const hasChatContent = exported!.some((piece) => piece.id === chat.id);
127
+ expect(hasChatContent).toBe(true);
128
+ });
129
+
130
+ test("returns null for unauthorized CoValue", async () => {
131
+ const Person = co.map({
132
+ name: z.string(),
133
+ });
134
+
135
+ const group = Group.create();
136
+ const person = Person.create({ name: "John" }, group);
137
+ // Don't add any members, so it's private
138
+
139
+ const alice = await createJazzTestAccount();
140
+
141
+ const exported = await exportCoValue(Person, person.id, {
142
+ loadAs: alice,
143
+ });
144
+
145
+ expect(exported).toBeNull();
146
+ });
147
+
148
+ test("exports with custom resolve options", async () => {
149
+ const Address = co.map({
150
+ street: z.string(),
151
+ city: z.string(),
152
+ });
153
+
154
+ const Person = co.map({
155
+ name: z.string(),
156
+ address: Address,
157
+ });
158
+
159
+ const group = Group.create();
160
+ const address = Address.create(
161
+ { street: "123 Main St", city: "New York" },
162
+ group,
163
+ );
164
+ const person = Person.create({ name: "John", address }, group);
165
+ group.addMember("everyone", "reader");
166
+
167
+ const alice = await createJazzTestAccount();
168
+
169
+ // Export without resolving nested references
170
+ const exportedWithoutResolve = await exportCoValue(Person, person.id, {
171
+ resolve: { address: false },
172
+ loadAs: alice,
173
+ });
174
+
175
+ // Export with resolving nested references
176
+ const exportedWithResolve = await exportCoValue(Person, person.id, {
177
+ resolve: { address: true },
178
+ loadAs: alice,
179
+ });
180
+
181
+ expect(exportedWithoutResolve).not.toBeNull();
182
+ expect(exportedWithResolve).not.toBeNull();
183
+
184
+ // The version with resolve should have more content pieces
185
+ expect(exportedWithResolve!.length).toBeGreaterThanOrEqual(
186
+ exportedWithoutResolve!.length,
187
+ );
188
+ });
189
+
190
+ test("exports should handle errors on child covalues gracefully", async () => {
191
+ const Address = co.map({
192
+ street: co.plainText(),
193
+ city: co.plainText(),
194
+ });
195
+
196
+ const Person = co.map({
197
+ name: z.string(),
198
+ address: Address,
199
+ });
200
+
201
+ const group = Group.create();
202
+ const address = Address.create(
203
+ {
204
+ street: CoPlainText.create("123 Main St"),
205
+ city: CoPlainText.create("New York"),
206
+ },
207
+ group,
208
+ );
209
+ const person = Person.create({ name: "John", address }, group);
210
+
211
+ // Only add the person to the group, not the address
212
+ // This makes the address unauthorized for other accounts
213
+ group.addMember("everyone", "reader");
214
+
215
+ const alice = await createJazzTestAccount();
216
+
217
+ // Export from alice's perspective with resolve: true
218
+ // This should attempt to resolve the address but handle the error gracefully
219
+ const exported = await exportCoValue(Person, person.id, {
220
+ resolve: { address: { street: true, city: true } },
221
+ loadAs: alice,
222
+ bestEffortResolution: true,
223
+ });
224
+
225
+ assert(exported);
226
+
227
+ // Verify the person content is exported
228
+ const personContent = exported.filter((piece) => piece.id === person.id);
229
+ expect(personContent.length).toBeGreaterThan(0);
230
+
231
+ const addressContent = exported.filter((piece) => piece.id === address.id);
232
+ expect(addressContent.length).toBeGreaterThan(0);
233
+
234
+ const streetContent = exported.filter(
235
+ (piece) => piece.id === address.street.id,
236
+ );
237
+ expect(streetContent).toHaveLength(0);
238
+
239
+ const cityContent = exported.filter(
240
+ (piece) => piece.id === address.city.id,
241
+ );
242
+ expect(cityContent).toHaveLength(0);
243
+ });
244
+ });
245
+
246
+ describe("importContentPieces", () => {
247
+ test("imports content pieces successfully", async () => {
248
+ const Person = co.map({
249
+ name: z.string(),
250
+ age: z.number(),
251
+ });
252
+
253
+ const group = Group.create();
254
+ const person = Person.create({ name: "John", age: 30 }, group);
255
+ group.addMember("everyone", "reader");
256
+
257
+ const alice = await createJazzTestAccount();
258
+ const bob = await createJazzTestAccount();
259
+
260
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
261
+ peer.gracefulShutdown();
262
+ });
263
+
264
+ // Export from alice's perspective
265
+ const exported = await exportCoValue(Person, person.id, {
266
+ loadAs: alice,
267
+ });
268
+
269
+ expect(exported).not.toBeNull();
270
+
271
+ // Import to bob's node
272
+ importContentPieces(exported!, bob);
273
+
274
+ // Verify bob can now access the person
275
+ const importedPerson = await Person.load(person.id, { loadAs: bob });
276
+ expect(importedPerson).not.toBeNull();
277
+ expect(importedPerson?.name).toBe("John");
278
+ expect(importedPerson?.age).toBe(30);
279
+ });
280
+
281
+ test("imports content pieces with nested references", async () => {
282
+ const Address = co.map({
283
+ street: z.string(),
284
+ city: z.string(),
285
+ });
286
+
287
+ const Person = co.map({
288
+ name: z.string(),
289
+ address: Address,
290
+ });
291
+
292
+ const group = Group.create();
293
+ const address = Address.create(
294
+ { street: "123 Main St", city: "New York" },
295
+ group,
296
+ );
297
+ const person = Person.create({ name: "John", address }, group);
298
+ group.addMember("everyone", "reader");
299
+
300
+ const alice = await createJazzTestAccount();
301
+ const bob = await createJazzTestAccount();
302
+
303
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
304
+ peer.gracefulShutdown();
305
+ });
306
+
307
+ // Export with resolved references
308
+ const exported = await exportCoValue(Person, person.id, {
309
+ resolve: { address: true },
310
+ loadAs: alice,
311
+ });
312
+
313
+ expect(exported).not.toBeNull();
314
+
315
+ // Import to bob's node
316
+ importContentPieces(exported!, bob);
317
+
318
+ // Verify bob can access both person and address
319
+ const importedPerson = await Person.load(person.id, {
320
+ resolve: { address: true },
321
+ loadAs: bob,
322
+ });
323
+
324
+ expect(importedPerson).not.toBeNull();
325
+ expect(importedPerson?.name).toBe("John");
326
+ expect(importedPerson?.address).not.toBeNull();
327
+ expect(importedPerson?.address.street).toBe("123 Main St");
328
+ expect(importedPerson?.address.city).toBe("New York");
329
+ });
330
+
331
+ test("imports content pieces to anonymous agent", async () => {
332
+ const Person = co.map({
333
+ name: z.string(),
334
+ });
335
+
336
+ const group = Group.create();
337
+ const person = Person.create({ name: "John" }, group);
338
+ group.addMember("everyone", "reader");
339
+
340
+ const alice = await createJazzTestAccount();
341
+ const { guest } = await createJazzTestGuest();
342
+
343
+ guest.node.syncManager.getPeers().forEach((peer) => {
344
+ peer.gracefulShutdown();
345
+ });
346
+
347
+ // Export from alice's perspective
348
+ const exported = await exportCoValue(Person, person.id, {
349
+ loadAs: alice,
350
+ });
351
+
352
+ expect(exported).not.toBeNull();
353
+
354
+ // Import to anonymous agent
355
+ importContentPieces(exported!, guest);
356
+
357
+ // Verify anonymous agent can access the person
358
+ const importedPerson = await Person.load(person.id, { loadAs: guest });
359
+ expect(importedPerson).not.toBeNull();
360
+ expect(importedPerson?.name).toBe("John");
361
+ });
362
+
363
+ test("imports content pieces without specifying loadAs (uses current account)", async () => {
364
+ const Person = co.map({
365
+ name: z.string(),
366
+ });
367
+
368
+ const group = Group.create();
369
+ const person = Person.create({ name: "John" }, group);
370
+ group.addMember("everyone", "reader");
371
+
372
+ const alice = await createJazzTestAccount();
373
+ const bob = await createJazzTestAccount({
374
+ isCurrentActiveAccount: true,
375
+ });
376
+
377
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
378
+ peer.gracefulShutdown();
379
+ });
380
+
381
+ // Export from alice's perspective
382
+ const exported = await exportCoValue(Person, person.id, {
383
+ loadAs: alice,
384
+ });
385
+
386
+ expect(exported).not.toBeNull();
387
+
388
+ // Import without specifying loadAs (should use current account)
389
+ importContentPieces(exported!);
390
+
391
+ // Verify bob can access the person
392
+ const importedPerson = await Person.load(person.id, { loadAs: bob });
393
+ expect(importedPerson).not.toBeNull();
394
+ expect(importedPerson?.name).toBe("John");
395
+ });
396
+
397
+ test("handles empty content pieces array", async () => {
398
+ const alice = await createJazzTestAccount();
399
+
400
+ // Should not throw when importing empty array
401
+ expect(() => {
402
+ importContentPieces([], alice);
403
+ }).not.toThrow();
404
+ });
405
+
406
+ test("handles duplicate content pieces", async () => {
407
+ const Person = co.map({
408
+ name: z.string(),
409
+ });
410
+
411
+ const group = Group.create();
412
+ const person = Person.create({ name: "John" }, group);
413
+ group.addMember("everyone", "reader");
414
+
415
+ const alice = await createJazzTestAccount();
416
+ const bob = await createJazzTestAccount();
417
+
418
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
419
+ peer.gracefulShutdown();
420
+ });
421
+
422
+ // Export from alice's perspective
423
+ const exported = await exportCoValue(Person, person.id, {
424
+ loadAs: alice,
425
+ });
426
+
427
+ expect(exported).not.toBeNull();
428
+
429
+ // Import the same content pieces twice
430
+ importContentPieces(exported!, bob);
431
+ importContentPieces(exported!, bob);
432
+
433
+ // Should still work correctly
434
+ const importedPerson = await Person.load(person.id, { loadAs: bob });
435
+ expect(importedPerson).not.toBeNull();
436
+ expect(importedPerson?.name).toBe("John");
437
+ });
438
+
439
+ test("imports content pieces with complex nested structure", async () => {
440
+ const Comment = co.map({
441
+ text: z.string(),
442
+ author: z.string(),
443
+ });
444
+
445
+ const Post = co.map({
446
+ title: z.string(),
447
+ content: z.string(),
448
+ comments: co.list(Comment),
449
+ });
450
+
451
+ const Blog = co.map({
452
+ name: z.string(),
453
+ posts: co.list(Post),
454
+ });
455
+
456
+ const group = Group.create();
457
+ const comment1 = Comment.create(
458
+ { text: "Great post!", author: "Alice" },
459
+ group,
460
+ );
461
+ const comment2 = Comment.create({ text: "Thanks!", author: "Bob" }, group);
462
+
463
+ const post = Post.create(
464
+ {
465
+ title: "My First Post",
466
+ content: "Hello World",
467
+ comments: Post.def.shape.comments.create([comment1, comment2], group),
468
+ },
469
+ group,
470
+ );
471
+
472
+ const blog = Blog.create(
473
+ { name: "My Blog", posts: Blog.def.shape.posts.create([post], group) },
474
+ group,
475
+ );
476
+
477
+ group.addMember("everyone", "reader");
478
+
479
+ const alice = await createJazzTestAccount();
480
+ const bob = await createJazzTestAccount();
481
+
482
+ bob._raw.core.node.syncManager.getPeers().forEach((peer) => {
483
+ peer.gracefulShutdown();
484
+ });
485
+
486
+ // Export with all nested references resolved
487
+ const exported = await exportCoValue(Blog, blog.id, {
488
+ resolve: {
489
+ posts: {
490
+ $each: {
491
+ comments: true,
492
+ },
493
+ },
494
+ },
495
+ loadAs: alice,
496
+ });
497
+
498
+ expect(exported).not.toBeNull();
499
+
500
+ // Import to bob's node
501
+ importContentPieces(exported!, bob);
502
+
503
+ // Verify bob can access the entire structure
504
+ const importedBlog = await Blog.load(blog.id, {
505
+ resolve: {
506
+ posts: {
507
+ $each: {
508
+ comments: true,
509
+ },
510
+ },
511
+ },
512
+ loadAs: bob,
513
+ });
514
+
515
+ expect(importedBlog).not.toBeNull();
516
+ expect(importedBlog?.name).toBe("My Blog");
517
+ expect(importedBlog?.posts.length).toBe(1);
518
+ expect(importedBlog?.posts[0]?.title).toBe("My First Post");
519
+ expect(importedBlog?.posts[0]?.content).toBe("Hello World");
520
+ expect(importedBlog?.posts[0]?.comments.length).toBe(2);
521
+ expect(importedBlog?.posts[0]?.comments[0]?.text).toBe("Great post!");
522
+ expect(importedBlog?.posts[0]?.comments[0]?.author).toBe("Alice");
523
+ expect(importedBlog?.posts[0]?.comments[1]?.text).toBe("Thanks!");
524
+ expect(importedBlog?.posts[0]?.comments[1]?.author).toBe("Bob");
525
+ });
526
+ });