jazz-tools 0.19.1 → 0.19.3

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 (67) hide show
  1. package/.turbo/turbo-build.log +63 -63
  2. package/CHANGELOG.md +21 -0
  3. package/dist/{chunk-NCNM6UDZ.js → chunk-JPWM4CS2.js} +4 -2
  4. package/dist/{chunk-NCNM6UDZ.js.map → chunk-JPWM4CS2.js.map} +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/inspector/{custom-element-QESCMFY7.js → custom-element-3JAYHXWQ.js} +1134 -464
  7. package/dist/inspector/custom-element-3JAYHXWQ.js.map +1 -0
  8. package/dist/inspector/index.js +1104 -434
  9. package/dist/inspector/index.js.map +1 -1
  10. package/dist/inspector/register-custom-element.js +1 -1
  11. package/dist/inspector/tests/utils/history.test.d.ts +2 -0
  12. package/dist/inspector/tests/utils/history.test.d.ts.map +1 -0
  13. package/dist/inspector/tests/viewer/co-value-editor.test.d.ts +2 -0
  14. package/dist/inspector/tests/viewer/co-value-editor.test.d.ts.map +1 -0
  15. package/dist/inspector/tests/viewer/comap-view.test.d.ts +2 -0
  16. package/dist/inspector/tests/viewer/comap-view.test.d.ts.map +1 -0
  17. package/dist/inspector/ui/icon.d.ts +6 -0
  18. package/dist/inspector/ui/icon.d.ts.map +1 -1
  19. package/dist/inspector/ui/icons/add-icon.d.ts +2 -0
  20. package/dist/inspector/ui/icons/add-icon.d.ts.map +1 -0
  21. package/dist/inspector/ui/icons/edit-icon.d.ts +2 -0
  22. package/dist/inspector/ui/icons/edit-icon.d.ts.map +1 -0
  23. package/dist/inspector/ui/icons/history.d.ts +2 -0
  24. package/dist/inspector/ui/icons/history.d.ts.map +1 -0
  25. package/dist/inspector/utils/history.d.ts +3 -0
  26. package/dist/inspector/utils/history.d.ts.map +1 -0
  27. package/dist/inspector/utils/permissions.d.ts +3 -0
  28. package/dist/inspector/utils/permissions.d.ts.map +1 -0
  29. package/dist/inspector/utils/transactions-changes.d.ts +38 -0
  30. package/dist/inspector/utils/transactions-changes.d.ts.map +1 -0
  31. package/dist/inspector/viewer/co-map-view.d.ts +9 -0
  32. package/dist/inspector/viewer/co-map-view.d.ts.map +1 -0
  33. package/dist/inspector/viewer/co-value-editor.d.ts +10 -0
  34. package/dist/inspector/viewer/co-value-editor.d.ts.map +1 -0
  35. package/dist/inspector/viewer/grid-view.d.ts +3 -2
  36. package/dist/inspector/viewer/grid-view.d.ts.map +1 -1
  37. package/dist/inspector/viewer/history-view.d.ts.map +1 -1
  38. package/dist/inspector/viewer/page.d.ts.map +1 -1
  39. package/dist/testing.js +1 -1
  40. package/dist/tools/coValues/CoFieldInit.d.ts +2 -1
  41. package/dist/tools/coValues/CoFieldInit.d.ts.map +1 -1
  42. package/dist/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.d.ts +3 -2
  43. package/dist/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.d.ts.map +1 -1
  44. package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
  45. package/package.json +4 -4
  46. package/src/inspector/tests/utils/history.test.ts +401 -0
  47. package/src/inspector/tests/viewer/co-value-editor.test.tsx +903 -0
  48. package/src/inspector/tests/viewer/comap-view.test.tsx +889 -0
  49. package/src/inspector/ui/icon.tsx +6 -0
  50. package/src/inspector/ui/icons/add-icon.tsx +21 -0
  51. package/src/inspector/ui/icons/edit-icon.tsx +17 -0
  52. package/src/inspector/ui/icons/history.tsx +28 -0
  53. package/src/inspector/ui/modal.tsx +3 -3
  54. package/src/inspector/utils/history.ts +49 -0
  55. package/src/inspector/utils/permissions.ts +10 -0
  56. package/src/inspector/utils/transactions-changes.ts +98 -0
  57. package/src/inspector/viewer/co-map-view.tsx +324 -0
  58. package/src/inspector/viewer/co-value-editor.tsx +164 -0
  59. package/src/inspector/viewer/grid-view.tsx +140 -10
  60. package/src/inspector/viewer/history-view.tsx +19 -119
  61. package/src/inspector/viewer/page.tsx +13 -0
  62. package/src/react-core/tests/usePassPhraseAuth.test.ts +1 -1
  63. package/src/tools/coValues/CoFieldInit.ts +6 -3
  64. package/src/tools/implementation/zodSchema/typeConverters/CoFieldSchemaInit.ts +12 -7
  65. package/src/tools/implementation/zodSchema/unionUtils.ts +3 -4
  66. package/src/tools/tests/coVector.test.ts +43 -0
  67. package/dist/inspector/custom-element-QESCMFY7.js.map +0 -1
@@ -0,0 +1,889 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, assert, beforeAll, describe, expect, it } from "vitest";
3
+ import { createJazzTestAccount, setupJazzTestSync } from "jazz-tools/testing";
4
+ import { co, z } from "jazz-tools";
5
+ import {
6
+ cleanup,
7
+ fireEvent,
8
+ render,
9
+ screen,
10
+ waitFor,
11
+ } from "@testing-library/react";
12
+ import { CoMapView } from "../../viewer/co-map-view";
13
+ import { setup } from "goober";
14
+ import React from "react";
15
+ import { JsonObject } from "cojson";
16
+
17
+ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
18
+
19
+ describe("CoMapView", async () => {
20
+ const account = await setupJazzTestSync();
21
+
22
+ beforeAll(() => {
23
+ setup(React.createElement);
24
+ });
25
+
26
+ afterEach(() => {
27
+ cleanup();
28
+ });
29
+
30
+ describe("Basic Rendering", () => {
31
+ it("should render GridView with data", async () => {
32
+ const value = co
33
+ .map({
34
+ pet: z.string(),
35
+ age: z.number(),
36
+ })
37
+ .create({ pet: "dog", age: 10 }, account);
38
+
39
+ const data = value.$jazz.raw.toJSON() as JsonObject;
40
+
41
+ render(
42
+ <CoMapView
43
+ coValue={value.$jazz.raw}
44
+ data={data}
45
+ node={account.$jazz.localNode}
46
+ onNavigate={() => {}}
47
+ />,
48
+ );
49
+
50
+ expect(screen.getByText("pet")).toBeDefined();
51
+ expect(screen.getByText("age")).toBeDefined();
52
+ expect(screen.getByText("dog")).toBeDefined();
53
+ expect(screen.getByText("10")).toBeDefined();
54
+ });
55
+
56
+ it("should render restore button", async () => {
57
+ const value = co
58
+ .map({
59
+ foo: z.string(),
60
+ })
61
+ .create({ foo: "bar" }, account);
62
+
63
+ const data = value.$jazz.raw.toJSON() as JsonObject;
64
+
65
+ render(
66
+ <CoMapView
67
+ coValue={value.$jazz.raw}
68
+ data={data}
69
+ node={account.$jazz.localNode}
70
+ onNavigate={() => {}}
71
+ />,
72
+ );
73
+
74
+ const restoreButton = screen.getByTitle("Timeline");
75
+ expect(restoreButton).toBeDefined();
76
+ });
77
+ });
78
+
79
+ describe("Timestamp Selection", () => {
80
+ it("should display timestamps and allow selection", async () => {
81
+ const value = co
82
+ .map({
83
+ pet: z.string(),
84
+ })
85
+ .create({ pet: "dog" }, account);
86
+
87
+ await sleep(2);
88
+ value.$jazz.set("pet", "cat");
89
+ await sleep(2);
90
+ value.$jazz.set("pet", "bird");
91
+
92
+ const data = value.$jazz.raw.toJSON() as JsonObject;
93
+
94
+ render(
95
+ <CoMapView
96
+ coValue={value.$jazz.raw}
97
+ data={data}
98
+ node={account.$jazz.localNode}
99
+ onNavigate={() => {}}
100
+ />,
101
+ );
102
+
103
+ const restoreButton = screen.getByTitle("Timeline");
104
+ fireEvent.click(restoreButton);
105
+
106
+ const slider = screen.getByRole("slider") as HTMLInputElement;
107
+ expect(slider).toBeDefined();
108
+ expect(slider.max).toBe("2");
109
+
110
+ fireEvent.change(slider, { target: { value: "0" } });
111
+ expect(slider.value).toBe("0");
112
+
113
+ fireEvent.change(slider, { target: { value: "1" } });
114
+ expect(slider.value).toBe("1");
115
+ });
116
+
117
+ it("should show timestamp in ISO format", async () => {
118
+ const value = co
119
+ .map({
120
+ foo: z.string(),
121
+ })
122
+ .create({ foo: "bar" }, account);
123
+
124
+ value.$jazz.set("foo", "baz");
125
+
126
+ const data = value.$jazz.raw.toJSON() as JsonObject;
127
+
128
+ render(
129
+ <CoMapView
130
+ coValue={value.$jazz.raw}
131
+ data={data}
132
+ node={account.$jazz.localNode}
133
+ onNavigate={() => {}}
134
+ />,
135
+ );
136
+
137
+ const restoreButton = screen.getByTitle("Timeline");
138
+ fireEvent.click(restoreButton);
139
+
140
+ const timestampDisplay = screen.getAllByText(/\d{4}-\d{2}-\d{2}T/)[0];
141
+ expect(timestampDisplay).toBeDefined();
142
+ });
143
+
144
+ it("should update preview when timestamp changes", async () => {
145
+ const value = co
146
+ .map({
147
+ pet: z.string(),
148
+ })
149
+ .create({ pet: "dog" }, account);
150
+
151
+ // wait to have different timestamps in transactions
152
+ await sleep(2);
153
+ value.$jazz.set("pet", "cat");
154
+ await sleep(2);
155
+ value.$jazz.set("pet", "bird");
156
+
157
+ const data = value.$jazz.raw.toJSON() as JsonObject;
158
+
159
+ render(
160
+ <CoMapView
161
+ coValue={value.$jazz.raw}
162
+ data={data}
163
+ node={account.$jazz.localNode}
164
+ onNavigate={() => {}}
165
+ />,
166
+ );
167
+
168
+ const restoreButton = screen.getByTitle("Timeline");
169
+ fireEvent.click(restoreButton);
170
+
171
+ const slider = screen.getByRole("slider") as HTMLInputElement;
172
+ const preview = screen.getByText(/State at that time:/);
173
+ expect(preview).toBeDefined();
174
+
175
+ // Modal starts at the most recent timestamp (last index)
176
+ await waitFor(() => {
177
+ const previewPre = preview.parentElement?.querySelector("pre");
178
+ expect(previewPre?.textContent).toContain("bird");
179
+ });
180
+
181
+ fireEvent.change(slider, { target: { value: 0 } });
182
+ await waitFor(() => {
183
+ const previewPre = preview.parentElement?.querySelector("pre");
184
+ expect(previewPre?.textContent).toContain("dog");
185
+ });
186
+ });
187
+ });
188
+
189
+ describe("Remove Unknown Properties Checkbox", () => {
190
+ it("should toggle checkbox state", async () => {
191
+ const value = co
192
+ .map({
193
+ foo: z.string(),
194
+ })
195
+ .create({ foo: "bar" }, account);
196
+
197
+ value.$jazz.set("foo", "baz");
198
+
199
+ const data = value.$jazz.raw.toJSON() as JsonObject;
200
+
201
+ render(
202
+ <CoMapView
203
+ coValue={value.$jazz.raw}
204
+ data={data}
205
+ node={account.$jazz.localNode}
206
+ onNavigate={() => {}}
207
+ />,
208
+ );
209
+
210
+ const restoreButton = screen.getByTitle("Timeline");
211
+ fireEvent.click(restoreButton);
212
+
213
+ const checkbox = screen.getByRole("checkbox") as HTMLInputElement;
214
+ expect(checkbox.checked).toBe(false);
215
+
216
+ fireEvent.click(checkbox);
217
+ expect(checkbox.checked).toBe(true);
218
+
219
+ fireEvent.click(checkbox);
220
+ expect(checkbox.checked).toBe(false);
221
+ });
222
+ });
223
+
224
+ describe("Restore Functionality", () => {
225
+ it("should show preview of state to restore", async () => {
226
+ const value = co
227
+ .map({
228
+ pet: z.string(),
229
+ age: z.number(),
230
+ })
231
+ .create({ pet: "dog", age: 10 }, account);
232
+
233
+ await sleep(2);
234
+ value.$jazz.set("pet", "cat");
235
+ value.$jazz.set("age", 20);
236
+
237
+ const data = value.$jazz.raw.toJSON() as JsonObject;
238
+
239
+ render(
240
+ <CoMapView
241
+ coValue={value.$jazz.raw}
242
+ data={data}
243
+ node={account.$jazz.localNode}
244
+ onNavigate={() => {}}
245
+ />,
246
+ );
247
+
248
+ const restoreButton = screen.getByTitle("Timeline");
249
+ fireEvent.click(restoreButton);
250
+
251
+ const slider = screen.getByRole("slider") as HTMLInputElement;
252
+ fireEvent.change(slider, { target: { value: 0 } });
253
+
254
+ await waitFor(() => {
255
+ const preview = screen.getByText(/State at that time:/);
256
+ const previewPre = preview.parentElement?.querySelector("pre");
257
+ expect(previewPre?.textContent).toContain("dog");
258
+ expect(previewPre?.textContent).toContain("10");
259
+ });
260
+ });
261
+
262
+ it("should close modal when restore is clicked", async () => {
263
+ const value = co
264
+ .map({
265
+ pet: z.string(),
266
+ age: z.number(),
267
+ })
268
+ .create({ pet: "dog", age: 10 }, account);
269
+
270
+ await sleep(2);
271
+ value.$jazz.set("pet", "cat");
272
+
273
+ const data = value.$jazz.raw.toJSON() as JsonObject;
274
+
275
+ render(
276
+ <CoMapView
277
+ coValue={value.$jazz.raw}
278
+ data={data}
279
+ node={account.$jazz.localNode}
280
+ onNavigate={() => {}}
281
+ />,
282
+ );
283
+
284
+ const restoreButton = screen.getByTitle("Timeline");
285
+ fireEvent.click(restoreButton);
286
+
287
+ expect(screen.getByText("Select Timestamp")).toBeDefined();
288
+
289
+ const slider = screen.getByRole("slider") as HTMLInputElement;
290
+ fireEvent.change(slider, { target: { value: 0 } });
291
+
292
+ const restoreActionButton = screen.getByText("Restore");
293
+ fireEvent.click(restoreActionButton);
294
+
295
+ await waitFor(() => {
296
+ expect(screen.queryByText("Select Timestamp")).toBeNull();
297
+ });
298
+ });
299
+
300
+ it("should allow selecting timestamp and checking remove properties option", async () => {
301
+ const value = co
302
+ .map({
303
+ pet: z.string(),
304
+ age: z.number().optional(),
305
+ })
306
+ .create({ pet: "dog" }, account);
307
+
308
+ await sleep(2);
309
+ value.$jazz.set("age", 10);
310
+ await sleep(2);
311
+ value.$jazz.set("pet", "cat");
312
+
313
+ const data = value.$jazz.raw.toJSON() as JsonObject;
314
+
315
+ render(
316
+ <CoMapView
317
+ coValue={value.$jazz.raw}
318
+ data={data}
319
+ node={account.$jazz.localNode}
320
+ onNavigate={() => {}}
321
+ />,
322
+ );
323
+
324
+ const restoreButton = screen.getByTitle("Timeline");
325
+ fireEvent.click(restoreButton);
326
+
327
+ const checkbox = screen.getByRole("checkbox") as HTMLInputElement;
328
+ expect(checkbox.checked).toBe(false);
329
+
330
+ fireEvent.click(checkbox);
331
+ expect(checkbox.checked).toBe(true);
332
+
333
+ // Change to earlier timestamp
334
+ const slider = screen.getByRole("slider") as HTMLInputElement;
335
+ fireEvent.change(slider, { target: { value: "0" } });
336
+
337
+ await waitFor(() => {
338
+ const preview = screen.getByText(/State at that time:/);
339
+ const previewPre = preview.parentElement?.querySelector("pre");
340
+ expect(previewPre?.textContent).toContain("dog");
341
+ });
342
+
343
+ const restoreActionButton = screen.getByText(
344
+ "Restore",
345
+ ) as HTMLButtonElement;
346
+ expect(restoreActionButton.disabled).toBe(false);
347
+ });
348
+ });
349
+
350
+ describe("Edge Cases", () => {
351
+ it("should handle empty CoMap", async () => {
352
+ const value = co.map({}).create({}, account);
353
+
354
+ const data = value.$jazz.raw.toJSON() as JsonObject;
355
+
356
+ render(
357
+ <CoMapView
358
+ coValue={value.$jazz.raw}
359
+ data={data}
360
+ node={account.$jazz.localNode}
361
+ onNavigate={() => {}}
362
+ />,
363
+ );
364
+
365
+ expect(screen.getByTitle("Timeline")).toBeDefined();
366
+ });
367
+
368
+ it("should handle complex data types", async () => {
369
+ const value = co
370
+ .map({
371
+ obj: z.object({
372
+ name: z.string(),
373
+ count: z.number(),
374
+ }),
375
+ date: z.date(),
376
+ bool: z.boolean(),
377
+ })
378
+ .create(
379
+ {
380
+ obj: { name: "test", count: 42 },
381
+ date: new Date("2024-01-01"),
382
+ bool: true,
383
+ },
384
+ account,
385
+ );
386
+
387
+ const data = value.$jazz.raw.toJSON() as JsonObject;
388
+
389
+ render(
390
+ <CoMapView
391
+ coValue={value.$jazz.raw}
392
+ data={data}
393
+ node={account.$jazz.localNode}
394
+ onNavigate={() => {}}
395
+ />,
396
+ );
397
+
398
+ expect(screen.getByText("obj")).toBeDefined();
399
+ expect(screen.getByText("date")).toBeDefined();
400
+ expect(screen.getByText("bool")).toBeDefined();
401
+ });
402
+
403
+ it("should not allow restoring to same state (no changes)", async () => {
404
+ const value = co
405
+ .map({
406
+ pet: z.string(),
407
+ })
408
+ .create({ pet: "dog" }, account);
409
+
410
+ const data = value.$jazz.raw.toJSON() as JsonObject;
411
+
412
+ render(
413
+ <CoMapView
414
+ coValue={value.$jazz.raw}
415
+ data={data}
416
+ node={account.$jazz.localNode}
417
+ onNavigate={() => {}}
418
+ />,
419
+ );
420
+
421
+ const restoreButton = screen.getByTitle("Timeline");
422
+ fireEvent.click(restoreButton);
423
+
424
+ expect(screen.queryByRole("slider")).toBeNull();
425
+ });
426
+
427
+ it("should handle multiple property changes at different times", async () => {
428
+ const value = co
429
+ .map({
430
+ a: z.string(),
431
+ b: z.string(),
432
+ c: z.string(),
433
+ })
434
+ .create({ a: "1", b: "2", c: "3" }, account);
435
+
436
+ await sleep(2);
437
+ value.$jazz.set("a", "4");
438
+ await sleep(2);
439
+ value.$jazz.set("b", "5");
440
+ await sleep(2);
441
+ value.$jazz.set("c", "6");
442
+
443
+ const data = value.$jazz.raw.toJSON() as JsonObject;
444
+
445
+ render(
446
+ <CoMapView
447
+ coValue={value.$jazz.raw}
448
+ data={data}
449
+ node={account.$jazz.localNode}
450
+ onNavigate={() => {}}
451
+ />,
452
+ );
453
+
454
+ const restoreButton = screen.getByTitle("Timeline");
455
+ fireEvent.click(restoreButton);
456
+
457
+ const slider = screen.getByRole("slider") as HTMLInputElement;
458
+
459
+ // Verify we can navigate to initial state
460
+ fireEvent.change(slider, { target: { value: 0 } });
461
+ await waitFor(() => {
462
+ const preview = screen.getByText(/State at that time:/);
463
+ const previewPre = preview.parentElement?.querySelector("pre");
464
+ expect(previewPre?.textContent).toContain("1");
465
+ expect(previewPre?.textContent).toContain("2");
466
+ expect(previewPre?.textContent).toContain("3");
467
+ });
468
+
469
+ // Verify we can navigate to latest state
470
+ fireEvent.change(slider, { target: { value: slider.max } });
471
+ await waitFor(() => {
472
+ const preview = screen.getByText(/State at that time:/);
473
+ const previewPre = preview.parentElement?.querySelector("pre");
474
+ expect(previewPre?.textContent).toContain("4");
475
+ expect(previewPre?.textContent).toContain("5");
476
+ expect(previewPre?.textContent).toContain("6");
477
+ });
478
+ });
479
+ });
480
+
481
+ describe("Preview Display", () => {
482
+ it("should show JSON preview of selected state", async () => {
483
+ const value = co
484
+ .map({
485
+ pet: z.string(),
486
+ age: z.number(),
487
+ })
488
+ .create({ pet: "dog", age: 10 }, account);
489
+
490
+ // wait to have different timestamps in transactions
491
+ await sleep(2);
492
+
493
+ value.$jazz.set("pet", "cat");
494
+
495
+ const data = value.$jazz.raw.toJSON() as JsonObject;
496
+
497
+ render(
498
+ <CoMapView
499
+ coValue={value.$jazz.raw}
500
+ data={data}
501
+ node={account.$jazz.localNode}
502
+ onNavigate={() => {}}
503
+ />,
504
+ );
505
+
506
+ const restoreButton = screen.getByTitle("Timeline");
507
+ fireEvent.click(restoreButton);
508
+
509
+ // Modal starts at most recent timestamp
510
+ const preview = screen.getByText(/State at that time:/);
511
+ await waitFor(() => {
512
+ const previewPre = preview.parentElement?.querySelector("pre");
513
+ expect(previewPre?.textContent).toContain("cat");
514
+ });
515
+
516
+ const slider = screen.getByRole("slider") as HTMLInputElement;
517
+ fireEvent.change(slider, { target: { value: "0" } });
518
+
519
+ await waitFor(
520
+ () => {
521
+ const previewPre = preview.parentElement?.querySelector("pre");
522
+ expect(previewPre?.textContent).toContain("dog");
523
+ },
524
+ {
525
+ timeout: 1000,
526
+ },
527
+ );
528
+ });
529
+
530
+ it("should update preview when slider moves", async () => {
531
+ const value = co
532
+ .map({
533
+ counter: z.number(),
534
+ })
535
+ .create({ counter: 1 }, account);
536
+
537
+ // wait to have different timestamps in transactions
538
+ await sleep(2);
539
+ value.$jazz.set("counter", 2);
540
+ await sleep(2);
541
+ value.$jazz.set("counter", 3);
542
+ await sleep(2);
543
+ value.$jazz.set("counter", 4);
544
+
545
+ const data = value.$jazz.raw.toJSON() as JsonObject;
546
+
547
+ render(
548
+ <CoMapView
549
+ coValue={value.$jazz.raw}
550
+ data={data}
551
+ node={account.$jazz.localNode}
552
+ onNavigate={() => {}}
553
+ />,
554
+ );
555
+
556
+ const restoreButton = screen.getByTitle("Timeline");
557
+ fireEvent.click(restoreButton);
558
+
559
+ const slider = screen.getByRole("slider") as HTMLInputElement;
560
+ const preview = screen.getByText(/State at that time:/);
561
+
562
+ // Modal starts at most recent timestamp (counter: 4)
563
+ await waitFor(() => {
564
+ const previewPre = preview.parentElement?.querySelector("pre");
565
+ expect(previewPre?.textContent).toContain('"counter": 4');
566
+ });
567
+
568
+ fireEvent.change(slider, { target: { value: "0" } });
569
+ await waitFor(() => {
570
+ const previewPre = preview.parentElement?.querySelector("pre");
571
+ expect(previewPre?.textContent).toContain('"counter": 1');
572
+ });
573
+
574
+ fireEvent.change(slider, { target: { value: "2" } });
575
+ await waitFor(() => {
576
+ const previewPre = preview.parentElement?.querySelector("pre");
577
+ expect(previewPre?.textContent).toContain('"counter": 3');
578
+ });
579
+ });
580
+ });
581
+
582
+ describe("Permissions", () => {
583
+ it("should disable Add Property button for reader account", async () => {
584
+ const reader = await createJazzTestAccount();
585
+ const group = co.group().create({ owner: account });
586
+ group.addMember(reader, "reader");
587
+
588
+ const schema = co.map({
589
+ pet: z.string(),
590
+ });
591
+
592
+ const value = schema.create({ pet: "dog" }, group);
593
+
594
+ const valueOnReader = await schema.load(value.$jazz.id, {
595
+ loadAs: reader,
596
+ });
597
+ assert(valueOnReader.$isLoaded);
598
+ const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
599
+
600
+ render(
601
+ <CoMapView
602
+ coValue={valueOnReader.$jazz.raw}
603
+ data={data}
604
+ node={reader.$jazz.localNode}
605
+ onNavigate={() => {}}
606
+ />,
607
+ );
608
+
609
+ const addButton = screen.getByTitle("Add Property");
610
+ expect(addButton).toBeDefined();
611
+ expect((addButton as HTMLButtonElement).disabled).toBe(true);
612
+ });
613
+
614
+ it("should enable Add Property button for writer account", async () => {
615
+ const writer = await createJazzTestAccount();
616
+ const group = co.group().create({ owner: account });
617
+ group.addMember(writer, "writer");
618
+
619
+ const schema = co.map({
620
+ pet: z.string(),
621
+ });
622
+
623
+ const value = schema.create({ pet: "dog" }, group);
624
+
625
+ const valueOnWriter = await schema.load(value.$jazz.id, {
626
+ loadAs: writer,
627
+ });
628
+ assert(valueOnWriter.$isLoaded);
629
+ const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
630
+
631
+ render(
632
+ <CoMapView
633
+ coValue={valueOnWriter.$jazz.raw}
634
+ data={data}
635
+ node={writer.$jazz.localNode}
636
+ onNavigate={() => {}}
637
+ />,
638
+ );
639
+
640
+ const addButton = screen.getByTitle("Add Property");
641
+ expect(addButton).toBeDefined();
642
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
643
+ });
644
+
645
+ it("should hide restore buttons for reader account when multiple timestamps exist", async () => {
646
+ const reader = await createJazzTestAccount();
647
+ const group = co.group().create({ owner: account });
648
+ group.addMember(reader, "reader");
649
+
650
+ const schema = co.map({
651
+ pet: z.string(),
652
+ });
653
+
654
+ const value = schema.create({ pet: "dog" }, group);
655
+ await sleep(2);
656
+ value.$jazz.set("pet", "cat");
657
+
658
+ const valueOnReader = await schema.load(value.$jazz.id, {
659
+ loadAs: reader,
660
+ });
661
+ assert(valueOnReader.$isLoaded);
662
+ const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
663
+
664
+ render(
665
+ <CoMapView
666
+ coValue={valueOnReader.$jazz.raw}
667
+ data={data}
668
+ node={reader.$jazz.localNode}
669
+ onNavigate={() => {}}
670
+ />,
671
+ );
672
+
673
+ const restoreButton = screen.getByTitle("Timeline");
674
+ fireEvent.click(restoreButton);
675
+
676
+ await waitFor(() => {
677
+ expect(screen.getByText("Select Timestamp")).toBeDefined();
678
+ });
679
+
680
+ expect(screen.queryByText("Restore")).toBeNull();
681
+ expect(screen.queryByRole("checkbox")).toBeNull();
682
+ });
683
+
684
+ it("should show restore buttons for writer account when multiple timestamps exist", async () => {
685
+ const writer = await createJazzTestAccount();
686
+ const group = co.group().create({ owner: account });
687
+ group.addMember(writer, "writer");
688
+
689
+ const schema = co.map({
690
+ pet: z.string(),
691
+ });
692
+
693
+ const value = schema.create({ pet: "dog" }, group);
694
+ await sleep(2);
695
+ value.$jazz.set("pet", "cat");
696
+
697
+ const valueOnWriter = await schema.load(value.$jazz.id, {
698
+ loadAs: writer,
699
+ });
700
+ assert(valueOnWriter.$isLoaded);
701
+ const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
702
+
703
+ render(
704
+ <CoMapView
705
+ coValue={valueOnWriter.$jazz.raw}
706
+ data={data}
707
+ node={writer.$jazz.localNode}
708
+ onNavigate={() => {}}
709
+ />,
710
+ );
711
+
712
+ const restoreButton = screen.getByTitle("Timeline");
713
+ fireEvent.click(restoreButton);
714
+
715
+ await waitFor(() => {
716
+ expect(screen.getByText("Restore")).toBeDefined();
717
+ });
718
+
719
+ expect(screen.getByRole("checkbox")).toBeDefined();
720
+ });
721
+
722
+ it("should hide edit buttons in GridView for reader account", async () => {
723
+ const reader = await createJazzTestAccount();
724
+ const group = co.group().create({ owner: account });
725
+ group.addMember(reader, "reader");
726
+
727
+ const schema = co.map({
728
+ pet: z.string(),
729
+ age: z.number(),
730
+ });
731
+
732
+ const value = schema.create({ pet: "dog", age: 10 }, group);
733
+
734
+ const valueOnReader = await schema.load(value.$jazz.id, {
735
+ loadAs: reader,
736
+ });
737
+ assert(valueOnReader.$isLoaded);
738
+ const data = valueOnReader.$jazz.raw.toJSON() as JsonObject;
739
+
740
+ render(
741
+ <CoMapView
742
+ coValue={valueOnReader.$jazz.raw}
743
+ data={data}
744
+ node={reader.$jazz.localNode}
745
+ onNavigate={() => {}}
746
+ />,
747
+ );
748
+
749
+ expect(screen.getByText("pet")).toBeDefined();
750
+ expect(screen.getByText("age")).toBeDefined();
751
+
752
+ const editButtons = screen.queryAllByLabelText("Edit");
753
+ const deleteButtons = screen.queryAllByLabelText("Delete");
754
+
755
+ expect(editButtons).toHaveLength(0);
756
+ expect(deleteButtons).toHaveLength(0);
757
+ });
758
+
759
+ it("should show edit buttons in GridView for writer account", async () => {
760
+ const writer = await createJazzTestAccount();
761
+ const group = co.group().create({ owner: account });
762
+ group.addMember(writer, "writer");
763
+
764
+ const schema = co.map({
765
+ pet: z.string(),
766
+ age: z.number(),
767
+ });
768
+
769
+ const value = schema.create({ pet: "dog", age: 10 }, group);
770
+
771
+ const valueOnWriter = await schema.load(value.$jazz.id, {
772
+ loadAs: writer,
773
+ });
774
+ assert(valueOnWriter.$isLoaded);
775
+ const data = valueOnWriter.$jazz.raw.toJSON() as JsonObject;
776
+
777
+ render(
778
+ <CoMapView
779
+ coValue={valueOnWriter.$jazz.raw}
780
+ data={data}
781
+ node={writer.$jazz.localNode}
782
+ onNavigate={() => {}}
783
+ />,
784
+ );
785
+
786
+ expect(screen.getByText("pet")).toBeDefined();
787
+ expect(screen.getByText("age")).toBeDefined();
788
+
789
+ const editButtons = screen.queryAllByLabelText("Edit");
790
+ const deleteButtons = screen.queryAllByLabelText("Delete");
791
+
792
+ expect(editButtons.length).toBeGreaterThan(0);
793
+ expect(deleteButtons.length).toBeGreaterThan(0);
794
+ });
795
+
796
+ it("should enable Add Property button for admin account", async () => {
797
+ const admin = await createJazzTestAccount();
798
+ const group = co.group().create({ owner: account });
799
+ group.addMember(admin, "admin");
800
+
801
+ const schema = co.map({
802
+ pet: z.string(),
803
+ });
804
+
805
+ const value = schema.create({ pet: "dog" }, group);
806
+
807
+ const valueOnAdmin = await schema.load(value.$jazz.id, {
808
+ loadAs: admin,
809
+ });
810
+ assert(valueOnAdmin.$isLoaded);
811
+ const data = valueOnAdmin.$jazz.raw.toJSON() as JsonObject;
812
+
813
+ render(
814
+ <CoMapView
815
+ coValue={valueOnAdmin.$jazz.raw}
816
+ data={data}
817
+ node={admin.$jazz.localNode}
818
+ onNavigate={() => {}}
819
+ />,
820
+ );
821
+
822
+ const addButton = screen.getByTitle("Add Property");
823
+ expect(addButton).toBeDefined();
824
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
825
+ });
826
+
827
+ it("should enable Add Property button for manager account", async () => {
828
+ const manager = await createJazzTestAccount();
829
+ const group = co.group().create({ owner: account });
830
+ group.addMember(manager, "manager");
831
+
832
+ const schema = co.map({
833
+ pet: z.string(),
834
+ });
835
+
836
+ const value = schema.create({ pet: "dog" }, group);
837
+
838
+ const valueOnManager = await schema.load(value.$jazz.id, {
839
+ loadAs: manager,
840
+ });
841
+ assert(valueOnManager.$isLoaded);
842
+ const data = valueOnManager.$jazz.raw.toJSON() as JsonObject;
843
+
844
+ render(
845
+ <CoMapView
846
+ coValue={valueOnManager.$jazz.raw}
847
+ data={data}
848
+ node={manager.$jazz.localNode}
849
+ onNavigate={() => {}}
850
+ />,
851
+ );
852
+
853
+ const addButton = screen.getByTitle("Add Property");
854
+ expect(addButton).toBeDefined();
855
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
856
+ });
857
+
858
+ it("should enable Add Property button for writeOnly account", async () => {
859
+ const writeOnly = await createJazzTestAccount();
860
+ const group = co.group().create({ owner: account });
861
+ group.addMember(writeOnly, "writeOnly");
862
+
863
+ const schema = co.map({
864
+ pet: z.string(),
865
+ });
866
+
867
+ const value = schema.create({ pet: "dog" }, group);
868
+
869
+ const valueOnWriteOnly = await schema.load(value.$jazz.id, {
870
+ loadAs: writeOnly,
871
+ });
872
+ assert(valueOnWriteOnly.$isLoaded);
873
+ const data = valueOnWriteOnly.$jazz.raw.toJSON() as JsonObject;
874
+
875
+ render(
876
+ <CoMapView
877
+ coValue={valueOnWriteOnly.$jazz.raw}
878
+ data={data}
879
+ node={writeOnly.$jazz.localNode}
880
+ onNavigate={() => {}}
881
+ />,
882
+ );
883
+
884
+ const addButton = screen.getByTitle("Add Property");
885
+ expect(addButton).toBeDefined();
886
+ expect((addButton as HTMLButtonElement).disabled).toBe(false);
887
+ });
888
+ });
889
+ });