@tldraw/state 5.1.0 → 5.2.0-canary.019da1aa690a
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 +563 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/Computed.js +1 -1
- package/dist-cjs/lib/Computed.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/Computed.mjs +1 -1
- package/dist-esm/lib/Computed.mjs.map +2 -2
- package/package.json +8 -4
- package/src/lib/Computed.ts +1 -1
- package/src/lib/__tests__/{arraySet.test.ts → ArraySet.test.ts} +83 -0
- package/src/lib/__tests__/EffectScheduler.test.ts +355 -34
- package/src/lib/__tests__/HistoryBuffer.test.ts +19 -2
- package/src/lib/__tests__/atom.test.ts +132 -128
- package/src/lib/__tests__/capture.test.ts +203 -84
- package/src/lib/__tests__/computed.test.ts +163 -438
- package/src/lib/__tests__/debug.test.ts +84 -0
- package/src/lib/__tests__/deferAsyncEffects.test.ts +232 -0
- package/src/lib/__tests__/errors.test.ts +75 -47
- package/src/lib/__tests__/fuzz.tlstate.test.ts +1 -1
- package/src/lib/__tests__/guards.test.ts +49 -0
- package/src/lib/__tests__/helpers.test.ts +46 -58
- package/src/lib/__tests__/history.test.ts +524 -0
- package/src/lib/__tests__/localStorageAtom.test.ts +91 -11
- package/src/lib/__tests__/propagation.test.ts +279 -0
- package/src/lib/__tests__/transactions.test.ts +49 -435
- package/src/lib/__tests__/reactor.test.ts +0 -197
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { localStorageAtom } from '../localStorageAtom'
|
|
2
2
|
import { getGlobalEpoch } from '../transactions'
|
|
3
3
|
|
|
4
|
+
// Tests for SPEC.md §15 (localStorageAtom).
|
|
5
|
+
// Rule IDs like [LS2] in test names refer to that document.
|
|
6
|
+
|
|
4
7
|
// Mock localStorage
|
|
5
8
|
const mockLocalStorage = (() => {
|
|
6
9
|
let store: Record<string, string> = {}
|
|
@@ -41,14 +44,14 @@ describe('localStorageAtom', () => {
|
|
|
41
44
|
})
|
|
42
45
|
|
|
43
46
|
describe('initialization', () => {
|
|
44
|
-
it('should create atom with initial value when localStorage is empty', () => {
|
|
47
|
+
it('[LS1] should create atom with initial value when localStorage is empty', () => {
|
|
45
48
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial-value')
|
|
46
49
|
|
|
47
50
|
expect(atom.get()).toBe('initial-value')
|
|
48
51
|
cleanup()
|
|
49
52
|
})
|
|
50
53
|
|
|
51
|
-
it('should restore value from localStorage when it exists', () => {
|
|
54
|
+
it('[LS1] should restore value from localStorage when it exists', () => {
|
|
52
55
|
mockLocalStorage.setItem('test-key', JSON.stringify('stored-value'))
|
|
53
56
|
|
|
54
57
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial-value')
|
|
@@ -59,7 +62,7 @@ describe('localStorageAtom', () => {
|
|
|
59
62
|
})
|
|
60
63
|
|
|
61
64
|
describe('corrupted localStorage handling', () => {
|
|
62
|
-
it('should use initial value and delete corrupted localStorage entry', () => {
|
|
65
|
+
it('[LS2] should use initial value and delete corrupted localStorage entry', () => {
|
|
63
66
|
mockLocalStorage.setItem('test-key', 'invalid-json')
|
|
64
67
|
|
|
65
68
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial-value')
|
|
@@ -69,7 +72,7 @@ describe('localStorageAtom', () => {
|
|
|
69
72
|
cleanup()
|
|
70
73
|
})
|
|
71
74
|
|
|
72
|
-
it('should handle empty string in localStorage', () => {
|
|
75
|
+
it('[LS2] should handle empty string in localStorage', () => {
|
|
73
76
|
mockLocalStorage.setItem('test-key', '')
|
|
74
77
|
|
|
75
78
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial-value')
|
|
@@ -81,7 +84,7 @@ describe('localStorageAtom', () => {
|
|
|
81
84
|
})
|
|
82
85
|
|
|
83
86
|
describe('localStorage synchronization', () => {
|
|
84
|
-
it('should save to localStorage when atom value changes', () => {
|
|
87
|
+
it('[LS3] should save to localStorage when atom value changes', () => {
|
|
85
88
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
86
89
|
|
|
87
90
|
atom.set('new-value')
|
|
@@ -90,7 +93,7 @@ describe('localStorageAtom', () => {
|
|
|
90
93
|
cleanup()
|
|
91
94
|
})
|
|
92
95
|
|
|
93
|
-
it('should update localStorage on multiple changes', () => {
|
|
96
|
+
it('[LS3] should update localStorage on multiple changes', () => {
|
|
94
97
|
const [atom, cleanup] = localStorageAtom('counter', 0)
|
|
95
98
|
|
|
96
99
|
// Clear initial call from atom creation
|
|
@@ -108,8 +111,70 @@ describe('localStorageAtom', () => {
|
|
|
108
111
|
})
|
|
109
112
|
})
|
|
110
113
|
|
|
114
|
+
describe('cross-tab synchronization', () => {
|
|
115
|
+
it('[LS4] updates the atom when a storage event for its key arrives', () => {
|
|
116
|
+
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
117
|
+
|
|
118
|
+
const event = new Event('storage')
|
|
119
|
+
Object.defineProperties(event, {
|
|
120
|
+
key: { value: 'test-key' },
|
|
121
|
+
newValue: { value: JSON.stringify('from-other-tab') },
|
|
122
|
+
})
|
|
123
|
+
window.dispatchEvent(event as StorageEvent)
|
|
124
|
+
|
|
125
|
+
expect(atom.get()).toBe('from-other-tab')
|
|
126
|
+
cleanup()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('[LS4] resets the atom to the initial value when the key is deleted in another tab', () => {
|
|
130
|
+
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
131
|
+
|
|
132
|
+
atom.set('changed')
|
|
133
|
+
|
|
134
|
+
const event = new Event('storage')
|
|
135
|
+
Object.defineProperties(event, {
|
|
136
|
+
key: { value: 'test-key' },
|
|
137
|
+
newValue: { value: null },
|
|
138
|
+
})
|
|
139
|
+
window.dispatchEvent(event as StorageEvent)
|
|
140
|
+
|
|
141
|
+
expect(atom.get()).toBe('initial')
|
|
142
|
+
cleanup()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('[LS4] ignores storage events for other keys', () => {
|
|
146
|
+
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
147
|
+
|
|
148
|
+
const event = new Event('storage')
|
|
149
|
+
Object.defineProperties(event, {
|
|
150
|
+
key: { value: 'other-key' },
|
|
151
|
+
newValue: { value: JSON.stringify('other-value') },
|
|
152
|
+
})
|
|
153
|
+
window.dispatchEvent(event as StorageEvent)
|
|
154
|
+
|
|
155
|
+
expect(atom.get()).toBe('initial')
|
|
156
|
+
cleanup()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('[LS4] ignores storage events with unparseable values', () => {
|
|
160
|
+
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
161
|
+
|
|
162
|
+
atom.set('current')
|
|
163
|
+
|
|
164
|
+
const event = new Event('storage')
|
|
165
|
+
Object.defineProperties(event, {
|
|
166
|
+
key: { value: 'test-key' },
|
|
167
|
+
newValue: { value: 'not json' },
|
|
168
|
+
})
|
|
169
|
+
window.dispatchEvent(event as StorageEvent)
|
|
170
|
+
|
|
171
|
+
expect(atom.get()).toBe('current')
|
|
172
|
+
cleanup()
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
111
176
|
describe('cleanup functionality', () => {
|
|
112
|
-
it('should stop syncing to localStorage after cleanup', () => {
|
|
177
|
+
it('[LS5] should stop syncing to localStorage after cleanup', () => {
|
|
113
178
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
114
179
|
|
|
115
180
|
// Change value before cleanup - should sync
|
|
@@ -128,7 +193,22 @@ describe('localStorageAtom', () => {
|
|
|
128
193
|
expect(mockLocalStorage.setItem).not.toHaveBeenCalled()
|
|
129
194
|
})
|
|
130
195
|
|
|
131
|
-
it('should
|
|
196
|
+
it('[LS5] should stop handling storage events after cleanup', () => {
|
|
197
|
+
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
198
|
+
|
|
199
|
+
cleanup()
|
|
200
|
+
|
|
201
|
+
const event = new Event('storage')
|
|
202
|
+
Object.defineProperties(event, {
|
|
203
|
+
key: { value: 'test-key' },
|
|
204
|
+
newValue: { value: JSON.stringify('from-other-tab') },
|
|
205
|
+
})
|
|
206
|
+
window.dispatchEvent(event as StorageEvent)
|
|
207
|
+
|
|
208
|
+
expect(atom.get()).toBe('initial')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('[LS5] should allow atom to continue functioning after cleanup', () => {
|
|
132
212
|
const [atom, cleanup] = localStorageAtom('test-key', 'initial')
|
|
133
213
|
|
|
134
214
|
cleanup()
|
|
@@ -139,7 +219,7 @@ describe('localStorageAtom', () => {
|
|
|
139
219
|
})
|
|
140
220
|
|
|
141
221
|
describe('atom options', () => {
|
|
142
|
-
it('should pass through atom options', () => {
|
|
222
|
+
it('[LS5] should pass through atom options', () => {
|
|
143
223
|
const isEqual = (a: string, b: string) => a.toLowerCase() === b.toLowerCase()
|
|
144
224
|
const [atom, cleanup] = localStorageAtom('test-key', 'Hello', { isEqual })
|
|
145
225
|
|
|
@@ -148,7 +228,7 @@ describe('localStorageAtom', () => {
|
|
|
148
228
|
cleanup()
|
|
149
229
|
})
|
|
150
230
|
|
|
151
|
-
it('should work with history options', () => {
|
|
231
|
+
it('[LS5] should work with history options', () => {
|
|
152
232
|
const [atom, cleanup] = localStorageAtom('test-key', 0, {
|
|
153
233
|
historyLength: 3,
|
|
154
234
|
computeDiff: (a, b) => b - a,
|
|
@@ -166,7 +246,7 @@ describe('localStorageAtom', () => {
|
|
|
166
246
|
})
|
|
167
247
|
|
|
168
248
|
describe('multiple instances', () => {
|
|
169
|
-
it('should handle multiple atoms with different keys', () => {
|
|
249
|
+
it('[LS3] should handle multiple atoms with different keys', () => {
|
|
170
250
|
const [atom1, cleanup1] = localStorageAtom('key1', 'value1')
|
|
171
251
|
const [atom2, cleanup2] = localStorageAtom('key2', 'value2')
|
|
172
252
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
import { atom } from '../Atom'
|
|
3
|
+
import { computed } from '../Computed'
|
|
4
|
+
import { react, reactor } from '../EffectScheduler'
|
|
5
|
+
import { transact, transaction } from '../transactions'
|
|
6
|
+
|
|
7
|
+
// Tests for SPEC.md §10 (change propagation and the reaction phase).
|
|
8
|
+
// Rule IDs like [P4] in test names refer to that document.
|
|
9
|
+
|
|
10
|
+
describe('change propagation (P)', () => {
|
|
11
|
+
it('[P1] runs effects synchronously, before set returns', () => {
|
|
12
|
+
const a = atom('', 1)
|
|
13
|
+
let observed = 0
|
|
14
|
+
|
|
15
|
+
react('', () => {
|
|
16
|
+
observed = a.get()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
a.set(2)
|
|
20
|
+
expect(observed).toBe(2)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('[P2] reaches effects through chains of computeds, but only along listening edges', () => {
|
|
24
|
+
const a = atom('', 1)
|
|
25
|
+
const double = vi.fn(() => a.get() * 2)
|
|
26
|
+
const c1 = computed('', double)
|
|
27
|
+
const c2 = computed('', () => c1.get() + 1)
|
|
28
|
+
|
|
29
|
+
let last = 0
|
|
30
|
+
const stop = react('', () => {
|
|
31
|
+
last = c2.get()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(last).toBe(3)
|
|
35
|
+
|
|
36
|
+
a.set(2)
|
|
37
|
+
expect(last).toBe(5)
|
|
38
|
+
|
|
39
|
+
stop()
|
|
40
|
+
|
|
41
|
+
// with nothing listening, changes do not propagate (and computeds stay lazy)
|
|
42
|
+
a.set(3)
|
|
43
|
+
expect(last).toBe(5)
|
|
44
|
+
expect(double).toHaveBeenCalledTimes(2)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('[P3] runs an effect once per change in a diamond-shaped graph', () => {
|
|
48
|
+
const a = atom('', 1)
|
|
49
|
+
const left = computed('', () => a.get() + 1)
|
|
50
|
+
const right = computed('', () => a.get() * 2)
|
|
51
|
+
|
|
52
|
+
const effect = vi.fn(() => {
|
|
53
|
+
left.get()
|
|
54
|
+
right.get()
|
|
55
|
+
})
|
|
56
|
+
react('', effect)
|
|
57
|
+
|
|
58
|
+
expect(effect).toHaveBeenCalledTimes(1)
|
|
59
|
+
|
|
60
|
+
a.set(2)
|
|
61
|
+
|
|
62
|
+
expect(effect).toHaveBeenCalledTimes(2)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('setting atoms during the reaction phase (P)', () => {
|
|
67
|
+
it('[P4] works', () => {
|
|
68
|
+
const a = atom('', 0)
|
|
69
|
+
const b = atom('', 0)
|
|
70
|
+
|
|
71
|
+
react('', () => {
|
|
72
|
+
b.set(a.get() + 1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(a.get()).toBe(0)
|
|
76
|
+
expect(b.get()).toBe(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('[P5] throws an error if it gets into a loop', () => {
|
|
80
|
+
expect(() => {
|
|
81
|
+
const a = atom('', 0)
|
|
82
|
+
|
|
83
|
+
react('', () => {
|
|
84
|
+
a.set(a.get() + 1)
|
|
85
|
+
})
|
|
86
|
+
}).toThrowErrorMatchingInlineSnapshot(`[Error: Reaction update depth limit exceeded]`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('[P5] throws when a reactor can not stop setting atom values', () => {
|
|
90
|
+
const a = atom('', 1)
|
|
91
|
+
const r = reactor('', () => {
|
|
92
|
+
if (a.get() < +Infinity) {
|
|
93
|
+
a.update((a) => a + 1)
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
expect(() => r.start()).toThrowErrorMatchingInlineSnapshot(
|
|
97
|
+
`[Error: Reaction update depth limit exceeded]`
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('[P4][P6] works with a transaction running', () => {
|
|
102
|
+
const a = atom('', 0)
|
|
103
|
+
|
|
104
|
+
react('', () => {
|
|
105
|
+
transact(() => {
|
|
106
|
+
if (a.get() < 10) {
|
|
107
|
+
a.set(a.get() + 1)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
expect(a.get()).toBe(10)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('[P7][regression 1] should allow computeds to be updated properly', () => {
|
|
116
|
+
const a = atom('', 0)
|
|
117
|
+
const b = atom('', 0)
|
|
118
|
+
const c = computed('', () => b.get() * 2)
|
|
119
|
+
|
|
120
|
+
let cValue = 0
|
|
121
|
+
|
|
122
|
+
react('', () => {
|
|
123
|
+
b.set(a.get() + 1)
|
|
124
|
+
cValue = c.get()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(a.get()).toBe(0)
|
|
128
|
+
expect(b.get()).toBe(1)
|
|
129
|
+
expect(cValue).toBe(2)
|
|
130
|
+
|
|
131
|
+
transact(() => {
|
|
132
|
+
a.set(1)
|
|
133
|
+
})
|
|
134
|
+
expect(cValue).toBe(4)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('[P7][regression 2] should allow computeds to be updated properly', () => {
|
|
138
|
+
const a = atom('', 0)
|
|
139
|
+
const b = atom('', 1)
|
|
140
|
+
const c = atom('', 0)
|
|
141
|
+
const d = computed('', () => a.get() * 2)
|
|
142
|
+
|
|
143
|
+
let dValue = 0
|
|
144
|
+
react('', () => {
|
|
145
|
+
// update a, causes a and d to be traversed (but not updated)
|
|
146
|
+
a.set(b.get())
|
|
147
|
+
// update c
|
|
148
|
+
c.set(a.get())
|
|
149
|
+
// make sure that when we get d, it is updated properly
|
|
150
|
+
dValue = d.get()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
expect(a.get()).toBe(1)
|
|
154
|
+
expect(b.get()).toBe(1)
|
|
155
|
+
expect(c.get()).toBe(1)
|
|
156
|
+
|
|
157
|
+
expect(dValue).toBe(2)
|
|
158
|
+
|
|
159
|
+
transact(() => {
|
|
160
|
+
b.set(2)
|
|
161
|
+
})
|
|
162
|
+
expect(dValue).toBe(4)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('transactions during the reaction phase (P6)', () => {
|
|
167
|
+
it('[P6] it should be possible to run a transaction during a reaction', () => {
|
|
168
|
+
const a = atom('', 0)
|
|
169
|
+
const b = atom('', 0)
|
|
170
|
+
|
|
171
|
+
react('', () => {
|
|
172
|
+
transaction(() => {
|
|
173
|
+
b.set(a.get() + 1)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
expect(a.get()).toBe(0)
|
|
178
|
+
expect(b.get()).toBe(1)
|
|
179
|
+
|
|
180
|
+
a.set(1)
|
|
181
|
+
|
|
182
|
+
expect(b.get()).toBe(2)
|
|
183
|
+
|
|
184
|
+
transaction(() => {
|
|
185
|
+
a.set(2)
|
|
186
|
+
expect(b.get()).toBe(2)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
expect(b.get()).toBe(3)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('[P6] it should be possible to abort a transaction during a reaction', () => {
|
|
193
|
+
const a = atom('', 0)
|
|
194
|
+
const b = atom('', 0)
|
|
195
|
+
|
|
196
|
+
const unsub = react('', () => {
|
|
197
|
+
transaction((rollback) => {
|
|
198
|
+
b.set(a.get() + 1)
|
|
199
|
+
rollback()
|
|
200
|
+
})
|
|
201
|
+
expect(b.get()).toBe(0)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
expect(a.get()).toBe(0)
|
|
205
|
+
expect(b.get()).toBe(0)
|
|
206
|
+
|
|
207
|
+
unsub()
|
|
208
|
+
|
|
209
|
+
react('', () => {
|
|
210
|
+
transaction(() => {
|
|
211
|
+
b.set(3)
|
|
212
|
+
try {
|
|
213
|
+
transaction(() => {
|
|
214
|
+
b.set(a.get() + 1)
|
|
215
|
+
throw new Error('oops')
|
|
216
|
+
})
|
|
217
|
+
} catch (e: any) {
|
|
218
|
+
expect(e.message).toBe('oops')
|
|
219
|
+
} finally {
|
|
220
|
+
expect(b.get()).toBe(3)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
expect(b.get()).toBe(3)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(a.get()).toBe(0)
|
|
227
|
+
expect(b.get()).toBe(3)
|
|
228
|
+
|
|
229
|
+
expect.assertions(8)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('[P6] defers all side effects until the end of the outer reaction pass', () => {
|
|
233
|
+
const a = atom('', 0)
|
|
234
|
+
const b = atom('', 0)
|
|
235
|
+
const c = atom('', 0)
|
|
236
|
+
|
|
237
|
+
const aChanged = vi.fn()
|
|
238
|
+
const bChanged = vi.fn()
|
|
239
|
+
const cChanged = vi.fn()
|
|
240
|
+
|
|
241
|
+
react('', () => {
|
|
242
|
+
a.get()
|
|
243
|
+
aChanged()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
react('', () => {
|
|
247
|
+
transaction(() => {
|
|
248
|
+
a.set(b.get() + 1)
|
|
249
|
+
})
|
|
250
|
+
bChanged()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
react('', () => {
|
|
254
|
+
transaction(() => {
|
|
255
|
+
b.set(c.get() + 1)
|
|
256
|
+
})
|
|
257
|
+
cChanged()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
expect(aChanged).toHaveBeenCalledTimes(3)
|
|
261
|
+
expect(bChanged).toHaveBeenCalledTimes(2)
|
|
262
|
+
expect(cChanged).toHaveBeenCalledTimes(1)
|
|
263
|
+
|
|
264
|
+
expect(a.__unsafe__getWithoutCapture()).toBe(2)
|
|
265
|
+
|
|
266
|
+
cChanged.mockImplementationOnce(() => {
|
|
267
|
+
// b was .set() during c's reaction
|
|
268
|
+
expect(b.__unsafe__getWithoutCapture()).toBe(2)
|
|
269
|
+
// a was not yet set because the effect was deferred
|
|
270
|
+
// until the end of the reaction
|
|
271
|
+
expect(a.__unsafe__getWithoutCapture()).toBe(2)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
c.set(1)
|
|
275
|
+
|
|
276
|
+
expect(a.__unsafe__getWithoutCapture()).toBe(3)
|
|
277
|
+
expect(cChanged).toHaveBeenCalledTimes(2)
|
|
278
|
+
})
|
|
279
|
+
})
|