@tldraw/store 5.2.0-next.ee0fa4d6244f → 5.2.0

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 (64) hide show
  1. package/DOCS.md +790 -0
  2. package/README.md +9 -1
  3. package/dist-cjs/index.js +1 -1
  4. package/dist-cjs/lib/ImmutableMap.js +0 -13
  5. package/dist-cjs/lib/ImmutableMap.js.map +2 -2
  6. package/dist-cjs/lib/RecordType.js +1 -1
  7. package/dist-cjs/lib/RecordType.js.map +2 -2
  8. package/dist-cjs/lib/RecordsDiff.js +26 -14
  9. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  10. package/dist-cjs/lib/Store.js +6 -5
  11. package/dist-cjs/lib/Store.js.map +2 -2
  12. package/dist-cjs/lib/StoreSchema.js +1 -1
  13. package/dist-cjs/lib/StoreSchema.js.map +2 -2
  14. package/dist-cjs/lib/executeQuery.js +1 -1
  15. package/dist-cjs/lib/executeQuery.js.map +2 -2
  16. package/dist-esm/index.mjs +1 -1
  17. package/dist-esm/lib/ImmutableMap.mjs +0 -13
  18. package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
  19. package/dist-esm/lib/RecordType.mjs +1 -1
  20. package/dist-esm/lib/RecordType.mjs.map +2 -2
  21. package/dist-esm/lib/RecordsDiff.mjs +26 -14
  22. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  23. package/dist-esm/lib/Store.mjs +7 -6
  24. package/dist-esm/lib/Store.mjs.map +2 -2
  25. package/dist-esm/lib/StoreSchema.mjs +1 -1
  26. package/dist-esm/lib/StoreSchema.mjs.map +2 -2
  27. package/dist-esm/lib/executeQuery.mjs +1 -1
  28. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  29. package/package.json +9 -5
  30. package/src/lib/{test/AtomMap.test.ts → AtomMap.test.ts} +77 -111
  31. package/src/lib/AtomSet.test.ts +116 -0
  32. package/src/lib/BaseRecord.test.ts +10 -22
  33. package/src/lib/ImmutableMap.test.ts +114 -71
  34. package/src/lib/ImmutableMap.ts +1 -0
  35. package/src/lib/IncrementalSetConstructor.test.ts +76 -81
  36. package/src/lib/RecordType.test.ts +216 -0
  37. package/src/lib/RecordType.ts +1 -1
  38. package/src/lib/RecordsDiff.test.ts +112 -106
  39. package/src/lib/RecordsDiff.ts +43 -18
  40. package/src/lib/Store.test.ts +570 -630
  41. package/src/lib/Store.ts +9 -10
  42. package/src/lib/StoreListeners.test.ts +462 -0
  43. package/src/lib/StoreQueries.test.ts +586 -434
  44. package/src/lib/StoreSchema.test.ts +1012 -174
  45. package/src/lib/StoreSchema.ts +1 -1
  46. package/src/lib/StoreSideEffects.test.ts +546 -158
  47. package/src/lib/devFreeze.test.ts +94 -124
  48. package/src/lib/executeQuery.test.ts +77 -31
  49. package/src/lib/executeQuery.ts +3 -1
  50. package/src/lib/migrate.test.ts +273 -296
  51. package/src/lib/setUtils.test.ts +38 -79
  52. package/src/lib/test/createMigrations.test.ts +0 -75
  53. package/src/lib/test/dependsOn.test.ts +0 -166
  54. package/src/lib/test/getMigrationsSince.test.ts +0 -121
  55. package/src/lib/test/migrate.test.ts +0 -118
  56. package/src/lib/test/migratePersistedRecord.test.ts +0 -265
  57. package/src/lib/test/migrationCaching.test.ts +0 -209
  58. package/src/lib/test/recordStore.test.ts +0 -1567
  59. package/src/lib/test/recordStoreQueries.test.ts +0 -814
  60. package/src/lib/test/recordType.test.ts +0 -19
  61. package/src/lib/test/sortMigrations.test.ts +0 -83
  62. package/src/lib/test/upgradeSchema.test.ts +0 -80
  63. package/src/lib/test/validate.test.ts +0 -178
  64. package/src/lib/test/validateMigrations.test.ts +0 -165
@@ -1,6 +1,9 @@
1
1
  import { react, transaction } from '@tldraw/state'
2
- import { vi } from 'vitest'
3
- import { AtomMap } from '../AtomMap'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { AtomMap } from './AtomMap'
4
+
5
+ // Tests for SPEC.md §22 (AtomMap).
6
+ // Rule IDs like [AM1] in test names refer to that document.
4
7
 
5
8
  describe('AtomMap', () => {
6
9
  let cleanupFns: (() => void)[] = []
@@ -19,7 +22,7 @@ describe('AtomMap', () => {
19
22
  }
20
23
 
21
24
  describe('get', () => {
22
- it('should return the value or undefined', () => {
25
+ it('[AM3] returns the value or undefined', () => {
23
26
  const map = new AtomMap('test')
24
27
  expect(map.get('a')).toBe(undefined)
25
28
 
@@ -30,7 +33,7 @@ describe('AtomMap', () => {
30
33
  expect(map.get('a')).toBe(undefined)
31
34
  })
32
35
 
33
- it('should react to creating, updating, and deleting values', () => {
36
+ it('[AM1] [AM2] reacts to creating, updating, and deleting the read key', () => {
34
37
  const map = new AtomMap('test', [['a', 1]])
35
38
  const reactor = testReactor('test', () => map.get('b'))
36
39
  expect(reactor).toHaveBeenCalledTimes(1)
@@ -49,34 +52,28 @@ describe('AtomMap', () => {
49
52
  expect(reactor).toHaveLastReturnedWith(undefined)
50
53
  })
51
54
 
52
- it('should not react to changes in other keys', () => {
55
+ it('[AM1] does not react to changes in other keys', () => {
53
56
  const map = new AtomMap('test', [['a', 1]])
54
57
  const aReactor = testReactor('a-reactor', () => map.get('a'))
55
58
  expect(aReactor).toHaveBeenCalledTimes(1)
56
59
 
57
- // Setting other keys shouldn't trigger a reaction
60
+ // setting, updating, and deleting other keys does not trigger a reaction
58
61
  map.set('b', 1)
59
62
  map.set('c', 2)
60
- expect(aReactor).toHaveBeenCalledTimes(1)
61
-
62
- // Updating other keys shouldn't trigger a reaction
63
63
  map.set('b', 3)
64
64
  map.set('c', 4)
65
- expect(aReactor).toHaveBeenCalledTimes(1)
66
-
67
- // Deleting other keys shouldn't trigger a reaction
68
65
  map.delete('b')
69
66
  map.delete('c')
70
67
  expect(aReactor).toHaveBeenCalledTimes(1)
71
68
 
72
- // But setting the watched key should trigger a reaction
69
+ // but setting the watched key does
73
70
  map.set('a', 5)
74
71
  expect(aReactor).toHaveBeenCalledTimes(2)
75
72
  })
76
73
  })
77
74
 
78
75
  describe('has', () => {
79
- it('should return true if the key exists with a value', () => {
76
+ it('[AM3] reports whether the key has a value', () => {
80
77
  const map = new AtomMap('test')
81
78
  expect(map.has('a')).toBe(false)
82
79
 
@@ -87,7 +84,7 @@ describe('AtomMap', () => {
87
84
  expect(map.has('a')).toBe(false)
88
85
  })
89
86
 
90
- it('should react to creating, updating, and deleting values', () => {
87
+ it('[AM1] [AM2] reacts to creating, updating, and deleting the checked key', () => {
91
88
  const map = new AtomMap('test')
92
89
  const reactor = testReactor('test', () => map.has('a'))
93
90
  expect(reactor).toHaveBeenCalledTimes(1)
@@ -106,48 +103,51 @@ describe('AtomMap', () => {
106
103
  expect(reactor).toHaveLastReturnedWith(false)
107
104
 
108
105
  map.set('a', 2)
109
- expect(reactor).toHaveBeenCalledTimes(5) // reinstating a previously deleted atom works
106
+ expect(reactor).toHaveBeenCalledTimes(5) // reinstating a previously deleted key works
110
107
  expect(reactor).toHaveLastReturnedWith(true)
111
108
  })
112
109
 
113
- it('should not react to changes in other keys', () => {
110
+ it('[AM1] does not react to changes in other keys', () => {
114
111
  const map = new AtomMap('test', [['a', 1]])
115
112
  const aReactor = testReactor('a-reactor', () => map.has('a'))
116
113
  expect(aReactor).toHaveBeenCalledTimes(1)
117
114
 
118
- // Setting other keys shouldn't trigger a reaction
119
115
  map.set('b', 1)
120
116
  map.set('c', 2)
121
- expect(aReactor).toHaveBeenCalledTimes(1)
122
-
123
- // Updating other keys shouldn't trigger a reaction
124
117
  map.set('b', 3)
125
118
  map.set('c', 4)
126
- expect(aReactor).toHaveBeenCalledTimes(1)
127
-
128
- // Deleting other keys shouldn't trigger a reaction
129
119
  map.delete('b')
130
120
  map.delete('c')
131
121
  expect(aReactor).toHaveBeenCalledTimes(1)
132
122
 
133
- // But setting the watched key should trigger a reaction
134
123
  map.set('a', 5)
135
124
  expect(aReactor).toHaveBeenCalledTimes(2)
136
125
  })
137
126
  })
138
127
 
139
- describe('set', () => {
140
- it('should set values and return the map', () => {
128
+ describe('set and update', () => {
129
+ it('[AM3] set adds or updates and returns the map', () => {
141
130
  const map = new AtomMap('test')
142
131
  expect(map.set('a', 1)).toBe(map)
143
132
  expect(map.get('a')).toBe(1)
144
133
  map.set('a', 2)
145
134
  expect(map.get('a')).toBe(2)
146
135
  })
136
+
137
+ it('[AM3] update replaces an existing value', () => {
138
+ const map = new AtomMap('test', [['count', 5]])
139
+ map.update('count', (n) => n + 1)
140
+ expect(map.get('count')).toBe(6)
141
+ })
142
+
143
+ it('[AM3] update throws for a missing key', () => {
144
+ const map = new AtomMap<string, number>('test')
145
+ expect(() => map.update('missing', (n) => n + 1)).toThrow('AtomMap: key missing not found')
146
+ })
147
147
  })
148
148
 
149
149
  describe('delete', () => {
150
- it('should delete values and return true if value existed', () => {
150
+ it('[AM4] deletes values and returns whether the key existed', () => {
151
151
  const map = new AtomMap('test')
152
152
  expect(map.delete('a')).toBe(false)
153
153
 
@@ -159,7 +159,7 @@ describe('AtomMap', () => {
159
159
  })
160
160
 
161
161
  describe('deleteMany', () => {
162
- it('should delete multiple values at once', () => {
162
+ it('[AM4] deletes multiple values at once', () => {
163
163
  const map = new AtomMap('test', [
164
164
  ['a', 1],
165
165
  ['b', 2],
@@ -184,7 +184,7 @@ describe('AtomMap', () => {
184
184
  })
185
185
  })
186
186
 
187
- it('should trigger a single reaction for multiple deletions', () => {
187
+ it('[AM4] triggers a single reaction for multiple deletions', () => {
188
188
  const map = new AtomMap('test', [
189
189
  ['a', 1],
190
190
  ['b', 2],
@@ -216,7 +216,7 @@ describe('AtomMap', () => {
216
216
  expect(bValueReactor).toHaveLastReturnedWith(undefined)
217
217
  })
218
218
 
219
- it('should do nothing if no keys are provided', () => {
219
+ it('[AM4] does nothing if no keys are provided', () => {
220
220
  const map = new AtomMap('test', [
221
221
  ['a', 1],
222
222
  ['b', 2],
@@ -230,7 +230,7 @@ describe('AtomMap', () => {
230
230
  expect(map.size).toBe(2)
231
231
  })
232
232
 
233
- it('should do nothing if none of the keys exist', () => {
233
+ it('[AM4] does nothing if none of the keys exist', () => {
234
234
  const map = new AtomMap('test', [
235
235
  ['a', 1],
236
236
  ['b', 2],
@@ -244,7 +244,7 @@ describe('AtomMap', () => {
244
244
  expect(map.size).toBe(2)
245
245
  })
246
246
 
247
- it('should return only the entries that were actually deleted', () => {
247
+ it('[AM4] returns only the entries that were actually deleted', () => {
248
248
  const map = new AtomMap('test', [
249
249
  ['a', 1],
250
250
  ['b', 2],
@@ -260,7 +260,7 @@ describe('AtomMap', () => {
260
260
  expect(Array.from(map.entries())).toEqual([['c', 3]])
261
261
  })
262
262
 
263
- it('should not include entries that were already empty', () => {
263
+ it('[AM4] does not include entries that were already deleted', () => {
264
264
  const map = new AtomMap('test', [
265
265
  ['a', 1],
266
266
  ['b', 2],
@@ -270,7 +270,7 @@ describe('AtomMap', () => {
270
270
  expect(deleted).toEqual([['b', 2]])
271
271
  })
272
272
 
273
- it('should not trigger reactions for unaffected keys', () => {
273
+ it('[AM1] [AM4] does not trigger reactions for unaffected keys', () => {
274
274
  const map = new AtomMap('test', [
275
275
  ['a', 1],
276
276
  ['b', 2],
@@ -303,7 +303,7 @@ describe('AtomMap', () => {
303
303
  })
304
304
 
305
305
  describe('clear', () => {
306
- it('should remove all values', () => {
306
+ it('[AM5] removes all values', () => {
307
307
  const map = new AtomMap('test', [
308
308
  ['a', 1],
309
309
  ['b', 2],
@@ -315,7 +315,7 @@ describe('AtomMap', () => {
315
315
  expect(map.has('b')).toBe(false)
316
316
  })
317
317
 
318
- it('should trigger reactions', () => {
318
+ it('[AM5] triggers reactions', () => {
319
319
  const map = new AtomMap('test', [
320
320
  ['a', 1],
321
321
  ['b', 2],
@@ -325,35 +325,26 @@ describe('AtomMap', () => {
325
325
  expect(sizeReactor).toHaveLastReturnedWith(2)
326
326
 
327
327
  map.clear()
328
- // clear triggers multiple reactions since it clears multiple values
329
328
  expect(sizeReactor).toHaveLastReturnedWith(0)
330
329
  })
331
330
  })
332
331
 
333
332
  describe('entries', () => {
334
- it('should iterate over key-value pairs', () => {
333
+ it('[AM6] iterates over key-value pairs, excluding deleted entries', () => {
335
334
  const map = new AtomMap('test', [
336
335
  ['a', 1],
337
336
  ['b', 2],
338
337
  ])
339
- const entries = Array.from(map.entries())
340
- expect(entries).toEqual([
338
+ expect(Array.from(map.entries())).toEqual([
341
339
  ['a', 1],
342
340
  ['b', 2],
343
341
  ])
344
- })
345
342
 
346
- it('should skip empty values', () => {
347
- const map = new AtomMap('test', [
348
- ['a', 1],
349
- ['b', 2],
350
- ])
351
343
  map.delete('a')
352
- const entries = Array.from(map.entries())
353
- expect(entries).toEqual([['b', 2]])
344
+ expect(Array.from(map.entries())).toEqual([['b', 2]])
354
345
  })
355
346
 
356
- it('should react to changes', () => {
347
+ it('[AM6] reacts to changes', () => {
357
348
  const map = new AtomMap('test', [['a', 1]])
358
349
  const reactor = testReactor('test', () => Array.from(map.entries()))
359
350
  expect(reactor).toHaveBeenCalledTimes(1)
@@ -377,29 +368,29 @@ describe('AtomMap', () => {
377
368
  ['a', 3],
378
369
  ])
379
370
  })
380
- })
381
371
 
382
- describe('keys', () => {
383
- it('should iterate over keys', () => {
372
+ it('[AM6] iterating the map itself is the same as entries', () => {
384
373
  const map = new AtomMap('test', [
385
374
  ['a', 1],
386
375
  ['b', 2],
387
376
  ])
388
- const keys = Array.from(map.keys())
389
- expect(keys).toEqual(['a', 'b'])
377
+ expect(Array.from(map)).toEqual(Array.from(map.entries()))
390
378
  })
379
+ })
391
380
 
392
- it('should skip keys with empty values', () => {
381
+ describe('keys', () => {
382
+ it('[AM6] iterates over keys, excluding deleted entries', () => {
393
383
  const map = new AtomMap('test', [
394
384
  ['a', 1],
395
385
  ['b', 2],
396
386
  ])
387
+ expect(Array.from(map.keys())).toEqual(['a', 'b'])
388
+
397
389
  map.delete('a')
398
- const keys = Array.from(map.keys())
399
- expect(keys).toEqual(['b'])
390
+ expect(Array.from(map.keys())).toEqual(['b'])
400
391
  })
401
392
 
402
- it('should react to changes', () => {
393
+ it('[AM6] reacts to changes', () => {
403
394
  const map = new AtomMap('test', [['a', 1]])
404
395
  const reactor = testReactor('test', () => Array.from(map.keys()))
405
396
  expect(reactor).toHaveBeenCalledTimes(1)
@@ -420,26 +411,18 @@ describe('AtomMap', () => {
420
411
  })
421
412
 
422
413
  describe('values', () => {
423
- it('should iterate over values', () => {
414
+ it('[AM6] iterates over values, excluding deleted entries', () => {
424
415
  const map = new AtomMap('test', [
425
416
  ['a', 1],
426
417
  ['b', 2],
427
418
  ])
428
- const values = Array.from(map.values())
429
- expect(values).toEqual([1, 2])
430
- })
419
+ expect(Array.from(map.values())).toEqual([1, 2])
431
420
 
432
- it('should skip empty values', () => {
433
- const map = new AtomMap('test', [
434
- ['a', 1],
435
- ['b', 2],
436
- ])
437
421
  map.delete('a')
438
- const values = Array.from(map.values())
439
- expect(values).toEqual([2])
422
+ expect(Array.from(map.values())).toEqual([2])
440
423
  })
441
424
 
442
- it('should react to changes', () => {
425
+ it('[AM6] reacts to changes', () => {
443
426
  const map = new AtomMap('test', [['a', 1]])
444
427
  const reactor = testReactor('test', () => Array.from(map.values()))
445
428
  expect(reactor).toHaveBeenCalledTimes(1)
@@ -460,13 +443,11 @@ describe('AtomMap', () => {
460
443
  })
461
444
 
462
445
  describe('size', () => {
463
- it('should return the number of non-empty values', () => {
446
+ it('[AM6] counts the live entries', () => {
464
447
  const map = new AtomMap('test')
465
- const size = map.size
466
- expect(size).toBe(0)
448
+ expect(map.size).toBe(0)
467
449
 
468
450
  map.set('a', 1)
469
- expect(Array.from(map.entries()).length).toBe(1) // verify entries shows 1 item
470
451
  expect(map.size).toBe(1)
471
452
 
472
453
  map.set('b', 2)
@@ -479,17 +460,16 @@ describe('AtomMap', () => {
479
460
  expect(map.size).toBe(0)
480
461
  })
481
462
 
482
- it('should react to changes', () => {
463
+ it('[AM6] reacts to changes', () => {
483
464
  const map = new AtomMap('test')
484
465
  const reactor = testReactor('test', () => map.size)
485
466
  expect(reactor).toHaveBeenCalledTimes(1)
486
467
  expect(reactor).toHaveLastReturnedWith(0)
487
468
 
488
469
  map.set('a', 1)
489
- // setting a value triggers a reaction for both the value and size
490
470
  expect(reactor).toHaveLastReturnedWith(1)
491
471
 
492
- map.set('a', 2) // updating should trigger a reaction but keep same size
472
+ map.set('a', 2) // updating keeps the same size
493
473
  expect(reactor).toHaveLastReturnedWith(1)
494
474
 
495
475
  map.delete('a')
@@ -498,35 +478,26 @@ describe('AtomMap', () => {
498
478
  })
499
479
 
500
480
  describe('forEach', () => {
501
- it('should iterate over all entries', () => {
481
+ it('[AM6] iterates over live entries', () => {
502
482
  const map = new AtomMap('test', [
503
483
  ['a', 1],
504
484
  ['b', 2],
505
485
  ])
486
+ map.delete('a')
487
+ map.set('c', 3)
488
+
506
489
  const results: Array<[string, number]> = []
507
- map.forEach((value, key) => {
490
+ map.forEach((value, key, theMap) => {
491
+ expect(theMap).toBe(map)
508
492
  results.push([key, value])
509
493
  })
510
494
  expect(results).toEqual([
511
- ['a', 1],
512
- ['b', 2],
513
- ])
514
- })
515
-
516
- it('should skip empty values', () => {
517
- const map = new AtomMap('test', [
518
- ['a', 1],
519
495
  ['b', 2],
496
+ ['c', 3],
520
497
  ])
521
- map.delete('a')
522
- const results: Array<[string, number]> = []
523
- map.forEach((value, key) => {
524
- results.push([key, value])
525
- })
526
- expect(results).toEqual([['b', 2]])
527
498
  })
528
499
 
529
- it('should react to changes', () => {
500
+ it('[AM6] reacts to changes', () => {
530
501
  const map = new AtomMap('test', [['a', 1]])
531
502
  const reactor = testReactor('test', () => {
532
503
  const current: Array<[string, number]> = []
@@ -540,24 +511,13 @@ describe('AtomMap', () => {
540
511
 
541
512
  map.set('b', 2)
542
513
  expect(reactor).toHaveBeenCalledTimes(2)
543
- expect(reactor).toHaveLastReturnedWith([
544
- ['a', 1],
545
- ['b', 2],
546
- ])
547
514
 
548
515
  map.delete('a')
549
516
  expect(reactor).toHaveBeenCalledTimes(3)
550
517
  expect(reactor).toHaveLastReturnedWith([['b', 2]])
551
-
552
- map.set('a', 3)
553
- expect(reactor).toHaveBeenCalledTimes(4)
554
- expect(reactor).toHaveLastReturnedWith([
555
- ['b', 2],
556
- ['a', 3],
557
- ])
558
518
  })
559
519
 
560
- it('should use provided thisArg', () => {
520
+ it('[AM6] uses the provided thisArg', () => {
561
521
  const map = new AtomMap('test', [['a', 1]])
562
522
  const thisArg = { test: true }
563
523
  map.forEach(function (this: any) {
@@ -567,7 +527,7 @@ describe('AtomMap', () => {
567
527
  })
568
528
 
569
529
  describe('transaction rollbacks', () => {
570
- it('works for additions', () => {
530
+ it('[AM7] rolls back additions', () => {
571
531
  const map = new AtomMap('test', [
572
532
  ['a', 1],
573
533
  ['b', 2],
@@ -578,6 +538,7 @@ describe('AtomMap', () => {
578
538
  })
579
539
  expect(map.size).toBe(2)
580
540
  expect(map.has('c')).toBe(false)
541
+
581
542
  transaction(() => {
582
543
  map.set('c', 3)
583
544
  })
@@ -585,7 +546,7 @@ describe('AtomMap', () => {
585
546
  expect(map.get('c')).toBe(3)
586
547
  })
587
548
 
588
- it('works for updates', () => {
549
+ it('[AM7] rolls back updates', () => {
589
550
  const map = new AtomMap('test', [
590
551
  ['a', 1],
591
552
  ['b', 2],
@@ -602,12 +563,11 @@ describe('AtomMap', () => {
602
563
  map.set('a', 3)
603
564
  map.set('b', 4)
604
565
  })
605
-
606
566
  expect(map.get('a')).toBe(3)
607
567
  expect(map.get('b')).toBe(4)
608
568
  })
609
569
 
610
- it('works for deletions', () => {
570
+ it('[AM7] rolls back deletions', () => {
611
571
  const map = new AtomMap('test', [
612
572
  ['a', 1],
613
573
  ['b', 2],
@@ -618,6 +578,7 @@ describe('AtomMap', () => {
618
578
  })
619
579
  expect(map.has('a')).toBe(true)
620
580
  expect(map.size).toBe(2)
581
+
621
582
  transaction(() => {
622
583
  map.delete('a')
623
584
  })
@@ -625,4 +586,9 @@ describe('AtomMap', () => {
625
586
  expect(map.size).toBe(1)
626
587
  })
627
588
  })
589
+
590
+ it('[AM8] has the AtomMap string tag', () => {
591
+ const map = new AtomMap('test')
592
+ expect(Object.prototype.toString.call(map)).toBe('[object AtomMap]')
593
+ })
628
594
  })
@@ -0,0 +1,116 @@
1
+ import { react } from '@tldraw/state'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import { AtomSet } from './AtomSet'
4
+
5
+ // Tests for SPEC.md §23 (AtomSet).
6
+ // Rule IDs like [AS1] in test names refer to that document.
7
+
8
+ describe('AtomSet', () => {
9
+ let cleanupFns: (() => void)[] = []
10
+ afterEach(() => {
11
+ for (const fn of cleanupFns) {
12
+ fn()
13
+ }
14
+ cleanupFns = []
15
+ })
16
+
17
+ function testReactor(name: string, fn: () => any) {
18
+ const cb = vi.fn(fn)
19
+ const cleanup = react(name, cb)
20
+ cleanupFns.push(() => cleanup())
21
+ return cb
22
+ }
23
+
24
+ it('[AS1] add returns the set; has reports membership', () => {
25
+ const set = new AtomSet<string>('test')
26
+ expect(set.has('a')).toBe(false)
27
+
28
+ expect(set.add('a')).toBe(set)
29
+ expect(set.has('a')).toBe(true)
30
+
31
+ // adding again is a no-op
32
+ set.add('a')
33
+ expect(set.size).toBe(1)
34
+ })
35
+
36
+ it('[AS1] delete returns whether the value was present', () => {
37
+ const set = new AtomSet('test', ['a', 'b'])
38
+ expect(set.delete('a')).toBe(true)
39
+ expect(set.delete('a')).toBe(false)
40
+ expect(set.delete('missing')).toBe(false)
41
+ expect(set.has('a')).toBe(false)
42
+ expect(set.has('b')).toBe(true)
43
+ })
44
+
45
+ it('[AS1] clear empties the set', () => {
46
+ const set = new AtomSet('test', ['a', 'b'])
47
+ set.clear()
48
+ expect(set.size).toBe(0)
49
+ expect(set.has('a')).toBe(false)
50
+ })
51
+
52
+ it('[AS1] size counts the elements', () => {
53
+ const set = new AtomSet('test', ['a', 'b'])
54
+ expect(set.size).toBe(2)
55
+ set.add('c')
56
+ expect(set.size).toBe(3)
57
+ set.delete('a')
58
+ expect(set.size).toBe(2)
59
+ })
60
+
61
+ it('[AS1] keys, values, and iteration yield the elements; entries yields [value, value] pairs', () => {
62
+ const set = new AtomSet('test', ['a', 'b'])
63
+
64
+ expect(Array.from(set.keys())).toEqual(['a', 'b'])
65
+ expect(Array.from(set.values())).toEqual(['a', 'b'])
66
+ expect(Array.from(set)).toEqual(['a', 'b'])
67
+ expect(Array.from(set.entries())).toEqual([
68
+ ['a', 'a'],
69
+ ['b', 'b'],
70
+ ])
71
+ })
72
+
73
+ it('[AS1] forEach passes the value twice and the set, and honors thisArg', () => {
74
+ const set = new AtomSet('test', ['a', 'b'])
75
+ const seen: Array<[string, string]> = []
76
+ const thisArg = { marker: true }
77
+
78
+ set.forEach(function (this: any, value, value2, theSet) {
79
+ expect(this).toBe(thisArg)
80
+ expect(theSet).toBe(set)
81
+ seen.push([value, value2])
82
+ }, thisArg)
83
+
84
+ expect(seen).toEqual([
85
+ ['a', 'a'],
86
+ ['b', 'b'],
87
+ ])
88
+ })
89
+
90
+ it('[AS2] membership reads are reactive', () => {
91
+ const set = new AtomSet<string>('test', ['a'])
92
+ const reactor = testReactor('has-a', () => set.has('a'))
93
+ expect(reactor).toHaveBeenCalledTimes(1)
94
+ expect(reactor).toHaveLastReturnedWith(true)
95
+
96
+ set.delete('a')
97
+ expect(reactor).toHaveBeenCalledTimes(2)
98
+ expect(reactor).toHaveLastReturnedWith(false)
99
+
100
+ set.add('a')
101
+ expect(reactor).toHaveBeenCalledTimes(3)
102
+ expect(reactor).toHaveLastReturnedWith(true)
103
+ })
104
+
105
+ it('[AS2] iteration is reactive', () => {
106
+ const set = new AtomSet<string>('test', ['a'])
107
+ const reactor = testReactor('all', () => Array.from(set))
108
+ expect(reactor).toHaveLastReturnedWith(['a'])
109
+
110
+ set.add('b')
111
+ expect(reactor).toHaveLastReturnedWith(['a', 'b'])
112
+
113
+ set.delete('a')
114
+ expect(reactor).toHaveLastReturnedWith(['b'])
115
+ })
116
+ })
@@ -1,44 +1,32 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { isRecord } from './BaseRecord'
3
3
 
4
- describe('isRecord function', () => {
5
- it('should return true for valid records', () => {
6
- const validRecord = {
7
- id: 'book:123',
8
- typeName: 'book',
9
- title: '1984',
10
- }
4
+ // Tests for SPEC.md §2 (records).
5
+ // Rule IDs like [R1] in test names refer to that document.
11
6
 
12
- expect(isRecord(validRecord)).toBe(true)
13
- })
7
+ describe('records (R)', () => {
8
+ it('[R1] isRecord is true exactly for objects with an id and a typeName', () => {
9
+ expect(isRecord({ id: 'book:123', typeName: 'book', title: '1984' })).toBe(true)
10
+ expect(isRecord({ id: 'book:123', typeName: 'book' })).toBe(true)
14
11
 
15
- it('should return false for null and undefined', () => {
16
12
  expect(isRecord(null)).toBe(false)
17
13
  expect(isRecord(undefined)).toBe(false)
18
- })
19
-
20
- it('should return false for primitive types', () => {
21
14
  expect(isRecord('string')).toBe(false)
22
15
  expect(isRecord(42)).toBe(false)
23
16
  expect(isRecord(true)).toBe(false)
24
- })
25
-
26
- it('should return false for objects missing required properties', () => {
27
17
  expect(isRecord({})).toBe(false)
28
18
  expect(isRecord({ id: 'test' })).toBe(false)
29
19
  expect(isRecord({ typeName: 'test' })).toBe(false)
30
20
  })
31
21
 
32
- it('should work as type guard', () => {
33
- const unknownValue: unknown = {
34
- id: 'book:123',
35
- typeName: 'book',
36
- title: '1984',
37
- }
22
+ it('[R1] isRecord narrows the type of its argument', () => {
23
+ const unknownValue: unknown = { id: 'book:123', typeName: 'book', title: '1984' }
38
24
 
39
25
  if (isRecord(unknownValue)) {
40
26
  expect(unknownValue.id).toBe('book:123')
41
27
  expect(unknownValue.typeName).toBe('book')
28
+ } else {
29
+ throw new Error('expected a record')
42
30
  }
43
31
  })
44
32
  })