@tldraw/store 5.2.0-next.e2b8d10bf10e → 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.
- package/DOCS.md +790 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ImmutableMap.js +0 -13
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +1 -1
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js +26 -14
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +6 -5
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +1 -1
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js +1 -1
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ImmutableMap.mjs +0 -13
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +1 -1
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs +26 -14
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +7 -6
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +1 -1
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs +1 -1
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/package.json +9 -5
- package/src/lib/{test/AtomMap.test.ts → AtomMap.test.ts} +77 -111
- package/src/lib/AtomSet.test.ts +116 -0
- package/src/lib/BaseRecord.test.ts +10 -22
- package/src/lib/ImmutableMap.test.ts +114 -71
- package/src/lib/ImmutableMap.ts +1 -0
- package/src/lib/IncrementalSetConstructor.test.ts +76 -81
- package/src/lib/RecordType.test.ts +216 -0
- package/src/lib/RecordType.ts +1 -1
- package/src/lib/RecordsDiff.test.ts +112 -106
- package/src/lib/RecordsDiff.ts +43 -18
- package/src/lib/Store.test.ts +570 -630
- package/src/lib/Store.ts +9 -10
- package/src/lib/StoreListeners.test.ts +462 -0
- package/src/lib/StoreQueries.test.ts +586 -434
- package/src/lib/StoreSchema.test.ts +1012 -174
- package/src/lib/StoreSchema.ts +1 -1
- package/src/lib/StoreSideEffects.test.ts +546 -158
- package/src/lib/devFreeze.test.ts +94 -124
- package/src/lib/executeQuery.test.ts +77 -31
- package/src/lib/executeQuery.ts +3 -1
- package/src/lib/migrate.test.ts +273 -296
- package/src/lib/setUtils.test.ts +38 -79
- package/src/lib/test/createMigrations.test.ts +0 -75
- package/src/lib/test/dependsOn.test.ts +0 -166
- package/src/lib/test/getMigrationsSince.test.ts +0 -121
- package/src/lib/test/migrate.test.ts +0 -118
- package/src/lib/test/migratePersistedRecord.test.ts +0 -265
- package/src/lib/test/migrationCaching.test.ts +0 -209
- package/src/lib/test/recordStore.test.ts +0 -1567
- package/src/lib/test/recordStoreQueries.test.ts +0 -814
- package/src/lib/test/recordType.test.ts +0 -19
- package/src/lib/test/sortMigrations.test.ts +0 -83
- package/src/lib/test/upgradeSchema.test.ts +0 -80
- package/src/lib/test/validate.test.ts +0 -178
- 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 '
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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('
|
|
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('
|
|
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
|
|
106
|
+
expect(reactor).toHaveBeenCalledTimes(5) // reinstating a previously deleted key works
|
|
110
107
|
expect(reactor).toHaveLastReturnedWith(true)
|
|
111
108
|
})
|
|
112
109
|
|
|
113
|
-
it('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
expect(entries).toEqual([['b', 2]])
|
|
344
|
+
expect(Array.from(map.entries())).toEqual([['b', 2]])
|
|
354
345
|
})
|
|
355
346
|
|
|
356
|
-
it('
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
expect(keys).toEqual(['a', 'b'])
|
|
377
|
+
expect(Array.from(map)).toEqual(Array.from(map.entries()))
|
|
390
378
|
})
|
|
379
|
+
})
|
|
391
380
|
|
|
392
|
-
|
|
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
|
-
|
|
399
|
-
expect(keys).toEqual(['b'])
|
|
390
|
+
expect(Array.from(map.keys())).toEqual(['b'])
|
|
400
391
|
})
|
|
401
392
|
|
|
402
|
-
it('
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
expect(values).toEqual([2])
|
|
422
|
+
expect(Array.from(map.values())).toEqual([2])
|
|
440
423
|
})
|
|
441
424
|
|
|
442
|
-
it('
|
|
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('
|
|
446
|
+
it('[AM6] counts the live entries', () => {
|
|
464
447
|
const map = new AtomMap('test')
|
|
465
|
-
|
|
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('
|
|
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
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
})
|