mutts 1.0.0 → 1.0.1
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/dist/chunks/{decorator-BXsign4Z.js → decorator-8qjFb7dw.js} +2 -2
- package/dist/chunks/decorator-8qjFb7dw.js.map +1 -0
- package/dist/chunks/{decorator-CPbZNnsX.esm.js → decorator-AbRkXM5O.esm.js} +2 -2
- package/dist/chunks/decorator-AbRkXM5O.esm.js.map +1 -0
- package/dist/decorator.d.ts +1 -1
- package/dist/decorator.esm.js +1 -1
- package/dist/decorator.js +1 -1
- package/dist/destroyable.esm.js +1 -1
- package/dist/destroyable.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mutts.umd.js +1 -1
- package/dist/mutts.umd.js.map +1 -1
- package/dist/mutts.umd.min.js +1 -1
- package/dist/mutts.umd.min.js.map +1 -1
- package/dist/reactive.d.ts +4 -3
- package/dist/reactive.esm.js +61 -57
- package/dist/reactive.esm.js.map +1 -1
- package/dist/reactive.js +61 -56
- package/dist/reactive.js.map +1 -1
- package/dist/std-decorators.esm.js +1 -1
- package/dist/std-decorators.js +1 -1
- package/docs/reactive.md +616 -0
- package/package.json +1 -2
- package/dist/chunks/decorator-BXsign4Z.js.map +0 -1
- package/dist/chunks/decorator-CPbZNnsX.esm.js.map +0 -1
- package/src/decorator.test.ts +0 -495
- package/src/decorator.ts +0 -205
- package/src/destroyable.test.ts +0 -155
- package/src/destroyable.ts +0 -158
- package/src/eventful.test.ts +0 -380
- package/src/eventful.ts +0 -69
- package/src/index.ts +0 -7
- package/src/indexable.test.ts +0 -388
- package/src/indexable.ts +0 -124
- package/src/promiseChain.test.ts +0 -201
- package/src/promiseChain.ts +0 -99
- package/src/reactive/array.test.ts +0 -923
- package/src/reactive/array.ts +0 -352
- package/src/reactive/core.test.ts +0 -1663
- package/src/reactive/core.ts +0 -866
- package/src/reactive/index.ts +0 -28
- package/src/reactive/interface.test.ts +0 -1477
- package/src/reactive/interface.ts +0 -231
- package/src/reactive/map.test.ts +0 -866
- package/src/reactive/map.ts +0 -162
- package/src/reactive/set.test.ts +0 -289
- package/src/reactive/set.ts +0 -142
- package/src/std-decorators.test.ts +0 -679
- package/src/std-decorators.ts +0 -182
- package/src/utils.ts +0 -52
|
@@ -1,1477 +0,0 @@
|
|
|
1
|
-
import { computed, effect, reactive, watch } from './index'
|
|
2
|
-
|
|
3
|
-
describe('computed', () => {
|
|
4
|
-
it('returns computed value and caches it', () => {
|
|
5
|
-
const state = reactive({ a: 1, b: 2 })
|
|
6
|
-
let runs = 0
|
|
7
|
-
const getter = () => {
|
|
8
|
-
runs++
|
|
9
|
-
return state.a + state.b
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const v1 = computed(getter)
|
|
13
|
-
expect(v1).toBe(3)
|
|
14
|
-
expect(runs).toBe(1)
|
|
15
|
-
|
|
16
|
-
const v2 = computed(getter)
|
|
17
|
-
expect(v2).toBe(3)
|
|
18
|
-
// still cached, no re-run
|
|
19
|
-
expect(runs).toBe(1)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('recomputes after a dependency change (at least once)', () => {
|
|
23
|
-
const state = reactive({ a: 1, b: 2 })
|
|
24
|
-
let runs = 0
|
|
25
|
-
const getter = () => {
|
|
26
|
-
runs++
|
|
27
|
-
return state.a + state.b
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// initial compute
|
|
31
|
-
expect(computed(getter)).toBe(3)
|
|
32
|
-
expect(runs).toBe(1)
|
|
33
|
-
|
|
34
|
-
// mutate dependency -> internal effect should refresh cache once
|
|
35
|
-
state.a = 5
|
|
36
|
-
expect(computed(getter)).toBe(7)
|
|
37
|
-
// getter should have run again exactly once
|
|
38
|
-
expect(runs).toBe(2)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('flows with the effects', () => {
|
|
42
|
-
const state = reactive({ a: 1, b: 2 })
|
|
43
|
-
let runs = 0
|
|
44
|
-
const getter = () => {
|
|
45
|
-
runs++
|
|
46
|
-
return state.b + 1
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
effect(() => {
|
|
50
|
-
state.a = computed(getter) + 1
|
|
51
|
-
})
|
|
52
|
-
// initial compute
|
|
53
|
-
expect(computed(getter)).toBe(3)
|
|
54
|
-
expect(runs).toBe(1)
|
|
55
|
-
|
|
56
|
-
// mutate dependency -> internal effect should refresh cache once
|
|
57
|
-
state.b = 3
|
|
58
|
-
expect(state.a).toBe(5)
|
|
59
|
-
// getter should have run again exactly once
|
|
60
|
-
expect(runs).toBe(2)
|
|
61
|
-
expect(computed(getter)).toBe(4)
|
|
62
|
-
// getter should have not run again
|
|
63
|
-
expect(runs).toBe(2)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should properly track dependencies when computed is called within an effect', () => {
|
|
67
|
-
const state = reactive({ a: 1, b: 2, c: 0 })
|
|
68
|
-
let effectRuns = 0
|
|
69
|
-
let computedRuns = 0
|
|
70
|
-
|
|
71
|
-
const getter = () => {
|
|
72
|
-
computedRuns++
|
|
73
|
-
return state.a + state.b
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
effect(() => {
|
|
77
|
-
effectRuns++
|
|
78
|
-
// This should properly track the computed's dependencies
|
|
79
|
-
state.c = computed(getter)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// Initial run
|
|
83
|
-
expect(effectRuns).toBe(1)
|
|
84
|
-
expect(computedRuns).toBe(1)
|
|
85
|
-
expect(state.c).toBe(3)
|
|
86
|
-
|
|
87
|
-
// Change a dependency of the computed
|
|
88
|
-
state.a = 5
|
|
89
|
-
// The effect should re-run because it depends on the computed
|
|
90
|
-
expect(effectRuns).toBe(2)
|
|
91
|
-
expect(computedRuns).toBe(2)
|
|
92
|
-
expect(state.c).toBe(7)
|
|
93
|
-
|
|
94
|
-
// Change another dependency
|
|
95
|
-
state.b = 10
|
|
96
|
-
expect(effectRuns).toBe(3)
|
|
97
|
-
expect(computedRuns).toBe(3)
|
|
98
|
-
expect(state.c).toBe(15)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('should properly invalidate computed cache when dependencies change within effect', () => {
|
|
102
|
-
const state = reactive({ a: 1, b: 2, result: 0 })
|
|
103
|
-
let effectRuns = 0
|
|
104
|
-
let computedRuns = 0
|
|
105
|
-
|
|
106
|
-
const getter = () => {
|
|
107
|
-
computedRuns++
|
|
108
|
-
return state.a * state.b
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
effect(() => {
|
|
112
|
-
effectRuns++
|
|
113
|
-
// The computed should be properly tracked and invalidated
|
|
114
|
-
state.result = computed(getter)
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// Initial run
|
|
118
|
-
expect(effectRuns).toBe(1)
|
|
119
|
-
expect(computedRuns).toBe(1)
|
|
120
|
-
expect(state.result).toBe(2)
|
|
121
|
-
|
|
122
|
-
// Change dependency - effect should re-run and computed should re-execute
|
|
123
|
-
state.a = 3
|
|
124
|
-
expect(effectRuns).toBe(2)
|
|
125
|
-
expect(computedRuns).toBe(2)
|
|
126
|
-
expect(state.result).toBe(6)
|
|
127
|
-
|
|
128
|
-
// Change another dependency
|
|
129
|
-
state.b = 4
|
|
130
|
-
expect(effectRuns).toBe(3)
|
|
131
|
-
expect(computedRuns).toBe(3)
|
|
132
|
-
expect(state.result).toBe(12)
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('watch', () => {
|
|
137
|
-
describe('watch with value function', () => {
|
|
138
|
-
it('should watch a specific value and trigger on changes', () => {
|
|
139
|
-
const state = reactive({ count: 0, name: 'John' })
|
|
140
|
-
let newValue: number | undefined
|
|
141
|
-
let oldValue: number | undefined
|
|
142
|
-
let callCount = 0
|
|
143
|
-
|
|
144
|
-
const stop = watch(
|
|
145
|
-
() => state.count,
|
|
146
|
-
(newVal, oldVal) => {
|
|
147
|
-
newValue = newVal
|
|
148
|
-
oldValue = oldVal
|
|
149
|
-
callCount++
|
|
150
|
-
}
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
expect(callCount).toBe(0) // Should not trigger on setup
|
|
154
|
-
|
|
155
|
-
state.count = 5
|
|
156
|
-
expect(callCount).toBe(1)
|
|
157
|
-
expect(newValue).toBe(5)
|
|
158
|
-
expect(oldValue).toBe(0)
|
|
159
|
-
|
|
160
|
-
state.count = 10
|
|
161
|
-
expect(callCount).toBe(2)
|
|
162
|
-
expect(newValue).toBe(10)
|
|
163
|
-
expect(oldValue).toBe(5)
|
|
164
|
-
|
|
165
|
-
// Changing other properties should not trigger
|
|
166
|
-
state.name = 'Jane'
|
|
167
|
-
expect(callCount).toBe(2) // Should remain 2
|
|
168
|
-
|
|
169
|
-
stop()
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('should not trigger when watching non-reactive values', () => {
|
|
173
|
-
const state = reactive({ count: 0 })
|
|
174
|
-
let callCount = 0
|
|
175
|
-
|
|
176
|
-
const stop = watch(
|
|
177
|
-
() => 42, // Non-reactive value
|
|
178
|
-
() => {
|
|
179
|
-
callCount++
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
state.count = 5
|
|
184
|
-
expect(callCount).toBe(0) // Should not trigger
|
|
185
|
-
|
|
186
|
-
stop()
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('should handle multiple watchers on the same value', () => {
|
|
190
|
-
const state = reactive({ count: 0 })
|
|
191
|
-
let watcher1Calls = 0
|
|
192
|
-
let watcher2Calls = 0
|
|
193
|
-
|
|
194
|
-
const stop1 = watch(
|
|
195
|
-
() => state.count,
|
|
196
|
-
() => {
|
|
197
|
-
watcher1Calls++
|
|
198
|
-
}
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
const stop2 = watch(
|
|
202
|
-
() => state.count,
|
|
203
|
-
() => {
|
|
204
|
-
watcher2Calls++
|
|
205
|
-
}
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
state.count = 5
|
|
209
|
-
expect(watcher1Calls).toBe(1)
|
|
210
|
-
expect(watcher2Calls).toBe(1)
|
|
211
|
-
|
|
212
|
-
state.count = 10
|
|
213
|
-
expect(watcher1Calls).toBe(2)
|
|
214
|
-
expect(watcher2Calls).toBe(2)
|
|
215
|
-
|
|
216
|
-
stop1()
|
|
217
|
-
stop2()
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('should stop watching when cleanup is called', () => {
|
|
221
|
-
const state = reactive({ count: 0 })
|
|
222
|
-
let callCount = 0
|
|
223
|
-
|
|
224
|
-
const stop = watch(
|
|
225
|
-
() => state.count,
|
|
226
|
-
() => {
|
|
227
|
-
callCount++
|
|
228
|
-
}
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
state.count = 5
|
|
232
|
-
expect(callCount).toBe(1)
|
|
233
|
-
|
|
234
|
-
stop()
|
|
235
|
-
|
|
236
|
-
state.count = 10
|
|
237
|
-
expect(callCount).toBe(1) // Should not increment after stop
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('should handle computed values in watch', () => {
|
|
241
|
-
const state = reactive({ a: 1, b: 2 })
|
|
242
|
-
let callCount = 0
|
|
243
|
-
let lastValue: number | undefined
|
|
244
|
-
|
|
245
|
-
const stop = watch(
|
|
246
|
-
() => state.a + state.b,
|
|
247
|
-
(newValue) => {
|
|
248
|
-
lastValue = newValue
|
|
249
|
-
callCount++
|
|
250
|
-
}
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
state.a = 3
|
|
254
|
-
expect(callCount).toBe(1)
|
|
255
|
-
expect(lastValue).toBe(5)
|
|
256
|
-
|
|
257
|
-
state.b = 4
|
|
258
|
-
expect(callCount).toBe(2)
|
|
259
|
-
expect(lastValue).toBe(7)
|
|
260
|
-
|
|
261
|
-
stop()
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
it('should watch computed properties that return new objects (simplified)', () => {
|
|
265
|
-
// Simplified test: watching a computed property that returns a new object each time
|
|
266
|
-
// The bug occurs when modifying existing objects, not just adding new ones
|
|
267
|
-
@reactive
|
|
268
|
-
class TestClass {
|
|
269
|
-
public slots: { item: string; count: number }[] = []
|
|
270
|
-
|
|
271
|
-
addItem(item: string) {
|
|
272
|
-
this.slots.push({ item, count: 1 })
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
incrementCount(item: string) {
|
|
276
|
-
const slot = this.slots.find((s) => s.item === item)
|
|
277
|
-
if (slot) slot.count++
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
//@computed
|
|
281
|
-
get summary(): { [k: string]: number } {
|
|
282
|
-
const result: { [k: string]: number } = {}
|
|
283
|
-
for (const slot of this.slots) {
|
|
284
|
-
result[slot.item] = slot.count
|
|
285
|
-
}
|
|
286
|
-
return result
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const obj = new TestClass()
|
|
291
|
-
let callCount = 0
|
|
292
|
-
|
|
293
|
-
const stop = watch(
|
|
294
|
-
() => obj.summary,
|
|
295
|
-
() => callCount++,
|
|
296
|
-
{ deep: true }
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
expect(callCount).toBe(0)
|
|
300
|
-
|
|
301
|
-
// First change should trigger
|
|
302
|
-
obj.addItem('wood')
|
|
303
|
-
expect(callCount).toBe(1)
|
|
304
|
-
|
|
305
|
-
// Second change should trigger
|
|
306
|
-
obj.addItem('stone')
|
|
307
|
-
expect(callCount).toBe(2)
|
|
308
|
-
|
|
309
|
-
// Third change should trigger but doesn't (this is the bug)
|
|
310
|
-
// Modifying existing object property doesn't trigger the watch
|
|
311
|
-
obj.incrementCount('wood')
|
|
312
|
-
expect(callCount).toBe(3)
|
|
313
|
-
|
|
314
|
-
stop()
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('should watch computed properties that return new objects (with computed)', () => {
|
|
318
|
-
// This test shows the same issue but with @computed for comparison
|
|
319
|
-
@reactive
|
|
320
|
-
class TestClass {
|
|
321
|
-
public slots: { item: string; count: number }[] = []
|
|
322
|
-
|
|
323
|
-
addItem(item: string) {
|
|
324
|
-
this.slots.push({ item, count: 1 })
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
incrementCount(item: string) {
|
|
328
|
-
const slot = this.slots.find((s) => s.item === item)
|
|
329
|
-
if (slot) slot.count++
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
@computed
|
|
333
|
-
get summary(): { [k: string]: number } {
|
|
334
|
-
const result: { [k: string]: number } = {}
|
|
335
|
-
for (const slot of this.slots) {
|
|
336
|
-
result[slot.item] = slot.count
|
|
337
|
-
}
|
|
338
|
-
return result
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const obj = new TestClass()
|
|
343
|
-
let callCount = 0
|
|
344
|
-
|
|
345
|
-
// Watch the computed property that returns new objects
|
|
346
|
-
const stop = watch(
|
|
347
|
-
() => obj.summary,
|
|
348
|
-
() => callCount++,
|
|
349
|
-
{ deep: true }
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
expect(callCount).toBe(0)
|
|
353
|
-
|
|
354
|
-
// First change should trigger
|
|
355
|
-
obj.addItem('wood')
|
|
356
|
-
expect(callCount).toBe(1)
|
|
357
|
-
|
|
358
|
-
// Second change should trigger
|
|
359
|
-
obj.addItem('stone')
|
|
360
|
-
expect(callCount).toBe(2)
|
|
361
|
-
|
|
362
|
-
// Third change should trigger but doesn't (this is the bug)
|
|
363
|
-
obj.incrementCount('wood')
|
|
364
|
-
expect(callCount).toBe(3)
|
|
365
|
-
|
|
366
|
-
stop()
|
|
367
|
-
})
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
describe('watch object properties', () => {
|
|
371
|
-
it('should watch any property change on a reactive object', () => {
|
|
372
|
-
const user = reactive({
|
|
373
|
-
name: 'John',
|
|
374
|
-
age: 30,
|
|
375
|
-
email: 'john@example.com',
|
|
376
|
-
})
|
|
377
|
-
let callCount = 0
|
|
378
|
-
let lastUser: any
|
|
379
|
-
|
|
380
|
-
const stop = watch(user, () => {
|
|
381
|
-
callCount++
|
|
382
|
-
lastUser = { ...user }
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
expect(callCount).toBe(0) // Should not trigger on setup
|
|
386
|
-
|
|
387
|
-
user.name = 'Jane'
|
|
388
|
-
expect(callCount).toBe(1)
|
|
389
|
-
expect(lastUser.name).toBe('Jane')
|
|
390
|
-
|
|
391
|
-
user.age = 31
|
|
392
|
-
expect(callCount).toBe(2)
|
|
393
|
-
expect(lastUser.age).toBe(31)
|
|
394
|
-
|
|
395
|
-
user.email = 'jane@example.com'
|
|
396
|
-
expect(callCount).toBe(3)
|
|
397
|
-
expect(lastUser.email).toBe('jane@example.com')
|
|
398
|
-
|
|
399
|
-
stop()
|
|
400
|
-
})
|
|
401
|
-
it('should watch nested object property changes', () => {
|
|
402
|
-
const state = reactive({
|
|
403
|
-
user: {
|
|
404
|
-
name: 'John',
|
|
405
|
-
profile: { age: 30 },
|
|
406
|
-
},
|
|
407
|
-
})
|
|
408
|
-
let callCount = 0
|
|
409
|
-
|
|
410
|
-
const stop = watch(
|
|
411
|
-
state,
|
|
412
|
-
() => {
|
|
413
|
-
callCount++
|
|
414
|
-
},
|
|
415
|
-
{ deep: true }
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
state.user.name = 'Jane'
|
|
419
|
-
expect(callCount).toBe(1)
|
|
420
|
-
|
|
421
|
-
state.user.profile.age = 31
|
|
422
|
-
expect(callCount).toBe(2)
|
|
423
|
-
|
|
424
|
-
stop()
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
it('should watch array changes when object contains arrays', () => {
|
|
428
|
-
const state = reactive({
|
|
429
|
-
items: [1, 2, 3],
|
|
430
|
-
name: 'test',
|
|
431
|
-
})
|
|
432
|
-
let callCount = 0
|
|
433
|
-
|
|
434
|
-
const stop = watch(
|
|
435
|
-
state,
|
|
436
|
-
() => {
|
|
437
|
-
callCount++
|
|
438
|
-
},
|
|
439
|
-
{ deep: true }
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
state.items.push(4)
|
|
443
|
-
expect(callCount).toBe(1)
|
|
444
|
-
|
|
445
|
-
state.items[0] = 10
|
|
446
|
-
expect(callCount).toBe(2)
|
|
447
|
-
|
|
448
|
-
state.name = 'updated'
|
|
449
|
-
expect(callCount).toBe(3)
|
|
450
|
-
|
|
451
|
-
stop()
|
|
452
|
-
})
|
|
453
|
-
it('should handle multiple watchers on the same object', () => {
|
|
454
|
-
const user = reactive({ name: 'John', age: 30 })
|
|
455
|
-
let watcher1Calls = 0
|
|
456
|
-
let watcher2Calls = 0
|
|
457
|
-
|
|
458
|
-
const stop1 = watch(user, () => {
|
|
459
|
-
watcher1Calls++
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
const stop2 = watch(user, () => {
|
|
463
|
-
watcher2Calls++
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
user.name = 'Jane'
|
|
467
|
-
expect(watcher1Calls).toBe(1)
|
|
468
|
-
expect(watcher2Calls).toBe(1)
|
|
469
|
-
|
|
470
|
-
user.age = 31
|
|
471
|
-
expect(watcher1Calls).toBe(2)
|
|
472
|
-
expect(watcher2Calls).toBe(2)
|
|
473
|
-
|
|
474
|
-
stop1()
|
|
475
|
-
stop2()
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
it('should stop watching when cleanup is called', () => {
|
|
479
|
-
const user = reactive({ name: 'John', age: 30 })
|
|
480
|
-
let callCount = 0
|
|
481
|
-
|
|
482
|
-
const stop = watch(user, () => {
|
|
483
|
-
callCount++
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
user.name = 'Jane'
|
|
487
|
-
expect(callCount).toBe(1)
|
|
488
|
-
|
|
489
|
-
stop()
|
|
490
|
-
|
|
491
|
-
user.age = 31
|
|
492
|
-
expect(callCount).toBe(1) // Should not increment after stop
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
it('should handle non-reactive objects gracefully', () => {
|
|
496
|
-
const plainObject = { name: 'John', age: 30 }
|
|
497
|
-
let callCount = 0
|
|
498
|
-
|
|
499
|
-
// This should not throw but also not trigger
|
|
500
|
-
const stop = watch(plainObject, () => {
|
|
501
|
-
callCount++
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
plainObject.name = 'Jane'
|
|
505
|
-
expect(callCount).toBe(0) // Should not trigger for non-reactive objects
|
|
506
|
-
|
|
507
|
-
stop()
|
|
508
|
-
})
|
|
509
|
-
|
|
510
|
-
it('should watch property additions and deletions', () => {
|
|
511
|
-
const state = reactive({ name: 'John' }) as any
|
|
512
|
-
let callCount = 0
|
|
513
|
-
|
|
514
|
-
const stop = watch(state, () => {
|
|
515
|
-
callCount++
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
// Add new property
|
|
519
|
-
state.age = 30
|
|
520
|
-
expect(callCount).toBe(1)
|
|
521
|
-
|
|
522
|
-
// Delete property
|
|
523
|
-
delete state.age
|
|
524
|
-
expect(callCount).toBe(2)
|
|
525
|
-
|
|
526
|
-
stop()
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('should watch reactive array mutations (currently fails)', () => {
|
|
530
|
-
const state = reactive([1, 2, 3])
|
|
531
|
-
let callCount = 0
|
|
532
|
-
|
|
533
|
-
const stop = watch(state, () => {
|
|
534
|
-
callCount++
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
// These should trigger watch but currently don't
|
|
538
|
-
state.push(4)
|
|
539
|
-
expect(callCount).toBe(1)
|
|
540
|
-
|
|
541
|
-
state[0] = 10
|
|
542
|
-
expect(callCount).toBe(2)
|
|
543
|
-
|
|
544
|
-
state[5] = 10
|
|
545
|
-
expect(callCount).toBe(3)
|
|
546
|
-
|
|
547
|
-
stop()
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
it('should watch reactive array length changes (currently fails)', () => {
|
|
551
|
-
const state = reactive([1, 2, 3])
|
|
552
|
-
let callCount = 0
|
|
553
|
-
|
|
554
|
-
const stop = watch(state, () => {
|
|
555
|
-
callCount++
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
state.length = 2
|
|
559
|
-
expect(callCount).toBe(1)
|
|
560
|
-
|
|
561
|
-
stop()
|
|
562
|
-
})
|
|
563
|
-
|
|
564
|
-
it('should watch nested object properties in arrays (simplified)', () => {
|
|
565
|
-
// This test demonstrates the core deep watch bug: nested object property changes in arrays
|
|
566
|
-
@reactive
|
|
567
|
-
class TestClass {
|
|
568
|
-
public items: { name: string; count: number }[] = []
|
|
569
|
-
|
|
570
|
-
addItem(name: string) {
|
|
571
|
-
this.items.push({ name, count: 1 })
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
incrementCount(name: string) {
|
|
575
|
-
const item = this.items.find((i) => i.name === name)
|
|
576
|
-
if (item) item.count++
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const obj = new TestClass()
|
|
581
|
-
let callCount = 0
|
|
582
|
-
|
|
583
|
-
const stop = watch(
|
|
584
|
-
() => obj.items,
|
|
585
|
-
() => callCount++,
|
|
586
|
-
{ deep: true }
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
expect(callCount).toBe(0)
|
|
590
|
-
|
|
591
|
-
// Adding new items works
|
|
592
|
-
obj.addItem('wood')
|
|
593
|
-
expect(callCount).toBe(1)
|
|
594
|
-
|
|
595
|
-
// But modifying existing object properties doesn't trigger deep watch
|
|
596
|
-
obj.incrementCount('wood')
|
|
597
|
-
expect(callCount).toBe(2) // This fails - deep watch doesn't detect nested property changes
|
|
598
|
-
|
|
599
|
-
stop()
|
|
600
|
-
})
|
|
601
|
-
it('should watch computed properties that return new objects (without computed)', () => {
|
|
602
|
-
// This test shows the same issue but with @computed for comparison
|
|
603
|
-
@reactive
|
|
604
|
-
class TestClass {
|
|
605
|
-
public slots: { item: string; count: number }[] = []
|
|
606
|
-
|
|
607
|
-
addItem(item: string) {
|
|
608
|
-
this.slots.push({ item, count: 1 })
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
incrementCount(item: string) {
|
|
612
|
-
const slot = this.slots.find((s) => s.item === item)
|
|
613
|
-
if (slot) slot.count++
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
get summary(): { [k: string]: number } {
|
|
617
|
-
const result: { [k: string]: number } = {}
|
|
618
|
-
for (const slot of this.slots) {
|
|
619
|
-
result[slot.item] = slot.count
|
|
620
|
-
}
|
|
621
|
-
return result
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const obj = new TestClass()
|
|
626
|
-
let callCount = 0
|
|
627
|
-
|
|
628
|
-
// Watch the computed property that returns new objects
|
|
629
|
-
const stop = watch(
|
|
630
|
-
() => obj.summary,
|
|
631
|
-
() => callCount++,
|
|
632
|
-
{ deep: true }
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
expect(callCount).toBe(0)
|
|
636
|
-
|
|
637
|
-
// First change should trigger
|
|
638
|
-
obj.addItem('wood')
|
|
639
|
-
expect(callCount).toBe(1)
|
|
640
|
-
|
|
641
|
-
// Second change should trigger
|
|
642
|
-
obj.addItem('stone')
|
|
643
|
-
expect(callCount).toBe(2)
|
|
644
|
-
|
|
645
|
-
// Third change should trigger but doesn't (this is the bug)
|
|
646
|
-
obj.incrementCount('wood')
|
|
647
|
-
expect(callCount).toBe(3)
|
|
648
|
-
|
|
649
|
-
stop()
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
it('should watch nested object properties in objects (not arrays)', () => {
|
|
653
|
-
// Test if the issue is specific to arrays or also affects nested objects
|
|
654
|
-
@reactive
|
|
655
|
-
class TestClass {
|
|
656
|
-
public data: { wood: { count: number }; stone: { count: number } } = {
|
|
657
|
-
wood: { count: 1 },
|
|
658
|
-
stone: { count: 1 },
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
incrementCount(item: string) {
|
|
662
|
-
if (this.data[item as keyof typeof this.data]) {
|
|
663
|
-
this.data[item as keyof typeof this.data].count++
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const obj = new TestClass()
|
|
669
|
-
let callCount = 0
|
|
670
|
-
|
|
671
|
-
const stop = watch(
|
|
672
|
-
() => obj.data,
|
|
673
|
-
() => callCount++,
|
|
674
|
-
{ deep: true }
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
expect(callCount).toBe(0)
|
|
678
|
-
|
|
679
|
-
// This should trigger the watch
|
|
680
|
-
obj.incrementCount('wood')
|
|
681
|
-
expect(callCount).toBe(1) // Does this work with nested objects?
|
|
682
|
-
|
|
683
|
-
stop()
|
|
684
|
-
})
|
|
685
|
-
|
|
686
|
-
it('should watch array property changes with direct access (not methods)', () => {
|
|
687
|
-
// Test if the issue is about method calls vs direct property access
|
|
688
|
-
@reactive
|
|
689
|
-
class TestClass {
|
|
690
|
-
public items: { name: string; count: number }[] = []
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
const obj = new TestClass()
|
|
694
|
-
let callCount = 0
|
|
695
|
-
|
|
696
|
-
const stop = watch(
|
|
697
|
-
() => obj.items,
|
|
698
|
-
() => callCount++,
|
|
699
|
-
{ deep: true }
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
expect(callCount).toBe(0)
|
|
703
|
-
|
|
704
|
-
// Add item directly (not through method)
|
|
705
|
-
obj.items.push({ name: 'wood', count: 1 })
|
|
706
|
-
expect(callCount).toBe(1)
|
|
707
|
-
|
|
708
|
-
// Modify property directly (not through method)
|
|
709
|
-
obj.items[0].count++
|
|
710
|
-
expect(callCount).toBe(2) // Does this work with direct access?
|
|
711
|
-
|
|
712
|
-
stop()
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
it('should watch array element access vs find method (hypothesis test)', () => {
|
|
716
|
-
// Test the hypothesis: find() method doesn't track individual elements
|
|
717
|
-
@reactive
|
|
718
|
-
class TestClass {
|
|
719
|
-
public items: { name: string; count: number }[] = []
|
|
720
|
-
|
|
721
|
-
addItem(name: string) {
|
|
722
|
-
this.items.push({ name, count: 1 })
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Method 1: Using find() - should fail
|
|
726
|
-
incrementWithFind(name: string) {
|
|
727
|
-
const item = this.items.find((i) => i.name === name)
|
|
728
|
-
if (item) item.count++
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Method 2: Using direct index access - should work
|
|
732
|
-
incrementWithIndex(name: string) {
|
|
733
|
-
const index = this.items.findIndex((i) => i.name === name)
|
|
734
|
-
if (index >= 0) this.items[index].count++
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const obj = new TestClass()
|
|
739
|
-
obj.addItem('wood')
|
|
740
|
-
obj.addItem('stone')
|
|
741
|
-
|
|
742
|
-
let callCount = 0
|
|
743
|
-
|
|
744
|
-
const stop = watch(
|
|
745
|
-
() => obj.items,
|
|
746
|
-
() => callCount++,
|
|
747
|
-
{ deep: true }
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
expect(callCount).toBe(0)
|
|
751
|
-
|
|
752
|
-
// This should fail (find() doesn't track individual elements)
|
|
753
|
-
obj.incrementWithFind('wood')
|
|
754
|
-
expect(callCount).toBe(1) // This will likely fail
|
|
755
|
-
|
|
756
|
-
// This should work (direct index access tracks the element)
|
|
757
|
-
obj.incrementWithIndex('stone')
|
|
758
|
-
expect(callCount).toBe(2) // This should pass
|
|
759
|
-
|
|
760
|
-
stop()
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
it('should watch new added objects', () => {
|
|
764
|
-
const state = reactive({ x: null }) as any
|
|
765
|
-
let callCount = 0
|
|
766
|
-
const stop = watch(
|
|
767
|
-
state,
|
|
768
|
-
() => {
|
|
769
|
-
callCount++
|
|
770
|
-
},
|
|
771
|
-
{ deep: true }
|
|
772
|
-
)
|
|
773
|
-
state.x = { y: 1 }
|
|
774
|
-
expect(callCount).toBe(1)
|
|
775
|
-
state.x.y = 2
|
|
776
|
-
expect(callCount).toBe(2)
|
|
777
|
-
stop()
|
|
778
|
-
})
|
|
779
|
-
})
|
|
780
|
-
|
|
781
|
-
describe('watch edge cases', () => {
|
|
782
|
-
it('should handle undefined and null values', () => {
|
|
783
|
-
const state = reactive({ value: undefined as any })
|
|
784
|
-
let callCount = 0
|
|
785
|
-
let lastValue: any
|
|
786
|
-
|
|
787
|
-
const stop = watch(
|
|
788
|
-
() => state.value,
|
|
789
|
-
(newValue) => {
|
|
790
|
-
lastValue = newValue
|
|
791
|
-
callCount++
|
|
792
|
-
}
|
|
793
|
-
)
|
|
794
|
-
|
|
795
|
-
state.value = null
|
|
796
|
-
expect(callCount).toBe(1)
|
|
797
|
-
expect(lastValue).toBe(null)
|
|
798
|
-
|
|
799
|
-
state.value = 'test'
|
|
800
|
-
expect(callCount).toBe(2)
|
|
801
|
-
expect(lastValue).toBe('test')
|
|
802
|
-
|
|
803
|
-
stop()
|
|
804
|
-
})
|
|
805
|
-
|
|
806
|
-
it('should handle circular references in object watching', () => {
|
|
807
|
-
const state = reactive({ name: 'John' }) as any
|
|
808
|
-
state.self = state // Create circular reference
|
|
809
|
-
let callCount = 0
|
|
810
|
-
|
|
811
|
-
const stop = watch(state, () => {
|
|
812
|
-
callCount++
|
|
813
|
-
})
|
|
814
|
-
|
|
815
|
-
state.name = 'Jane'
|
|
816
|
-
expect(callCount).toBe(1)
|
|
817
|
-
|
|
818
|
-
stop()
|
|
819
|
-
})
|
|
820
|
-
|
|
821
|
-
it('should handle rapid successive changes', () => {
|
|
822
|
-
const state = reactive({ count: 0 })
|
|
823
|
-
let callCount = 0
|
|
824
|
-
let lastValue: number | undefined
|
|
825
|
-
|
|
826
|
-
const stop = watch(
|
|
827
|
-
() => state.count,
|
|
828
|
-
(newValue) => {
|
|
829
|
-
lastValue = newValue
|
|
830
|
-
callCount++
|
|
831
|
-
}
|
|
832
|
-
)
|
|
833
|
-
|
|
834
|
-
// Rapid changes
|
|
835
|
-
state.count = 1
|
|
836
|
-
state.count = 2
|
|
837
|
-
state.count = 3
|
|
838
|
-
state.count = 4
|
|
839
|
-
state.count = 5
|
|
840
|
-
|
|
841
|
-
expect(callCount).toBe(5)
|
|
842
|
-
expect(lastValue).toBe(5)
|
|
843
|
-
|
|
844
|
-
stop()
|
|
845
|
-
})
|
|
846
|
-
|
|
847
|
-
it('should handle watching during effect execution', () => {
|
|
848
|
-
const state = reactive({ count: 0, multiplier: 2 })
|
|
849
|
-
let watchCalls = 0
|
|
850
|
-
let effectCalls = 0
|
|
851
|
-
|
|
852
|
-
const stopWatch = watch(
|
|
853
|
-
() => state.multiplier,
|
|
854
|
-
() => {
|
|
855
|
-
watchCalls++
|
|
856
|
-
}
|
|
857
|
-
)
|
|
858
|
-
|
|
859
|
-
const stopEffect = effect(() => {
|
|
860
|
-
effectCalls++
|
|
861
|
-
// Change watched value during effect
|
|
862
|
-
state.count = state.count + 1
|
|
863
|
-
})
|
|
864
|
-
|
|
865
|
-
expect(effectCalls).toBe(1)
|
|
866
|
-
expect(watchCalls).toBe(0) // Should trigger once during effect
|
|
867
|
-
|
|
868
|
-
state.multiplier = 3
|
|
869
|
-
expect(effectCalls).toBe(1)
|
|
870
|
-
expect(watchCalls).toBe(1) // Should trigger again
|
|
871
|
-
|
|
872
|
-
stopWatch()
|
|
873
|
-
stopEffect()
|
|
874
|
-
})
|
|
875
|
-
})
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
describe('deep watch via watch({ deep: true })', () => {
|
|
879
|
-
describe('basic deep watching functionality', () => {
|
|
880
|
-
it('should watch nested object property changes', () => {
|
|
881
|
-
const state = reactive({
|
|
882
|
-
user: {
|
|
883
|
-
name: 'John',
|
|
884
|
-
profile: { age: 30 },
|
|
885
|
-
},
|
|
886
|
-
})
|
|
887
|
-
let callCount = 0
|
|
888
|
-
|
|
889
|
-
const stop = watch(
|
|
890
|
-
state,
|
|
891
|
-
() => {
|
|
892
|
-
callCount++
|
|
893
|
-
},
|
|
894
|
-
{ deep: true }
|
|
895
|
-
)
|
|
896
|
-
|
|
897
|
-
state.user.name = 'Jane'
|
|
898
|
-
expect(callCount).toBe(1)
|
|
899
|
-
|
|
900
|
-
state.user.profile.age = 31
|
|
901
|
-
expect(callCount).toBe(2)
|
|
902
|
-
|
|
903
|
-
stop()
|
|
904
|
-
})
|
|
905
|
-
|
|
906
|
-
it('should watch array changes when object contains arrays', () => {
|
|
907
|
-
const state = reactive({
|
|
908
|
-
items: [1, 2, 3],
|
|
909
|
-
name: 'test',
|
|
910
|
-
})
|
|
911
|
-
let callCount = 0
|
|
912
|
-
|
|
913
|
-
const stop = watch(
|
|
914
|
-
state,
|
|
915
|
-
() => {
|
|
916
|
-
callCount++
|
|
917
|
-
},
|
|
918
|
-
{ deep: true }
|
|
919
|
-
)
|
|
920
|
-
|
|
921
|
-
state.items.push(4)
|
|
922
|
-
expect(callCount).toBe(1)
|
|
923
|
-
|
|
924
|
-
state.items[0] = 10
|
|
925
|
-
expect(callCount).toBe(2)
|
|
926
|
-
|
|
927
|
-
state.name = 'updated'
|
|
928
|
-
expect(callCount).toBe(3)
|
|
929
|
-
|
|
930
|
-
stop()
|
|
931
|
-
})
|
|
932
|
-
|
|
933
|
-
it('should handle object replacement correctly', () => {
|
|
934
|
-
const state = reactive({
|
|
935
|
-
user: {
|
|
936
|
-
name: 'John',
|
|
937
|
-
profile: { age: 30 },
|
|
938
|
-
},
|
|
939
|
-
})
|
|
940
|
-
let callCount = 0
|
|
941
|
-
|
|
942
|
-
const stop = watch(
|
|
943
|
-
state,
|
|
944
|
-
() => {
|
|
945
|
-
callCount++
|
|
946
|
-
},
|
|
947
|
-
{ deep: true }
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
// Replace the entire user object
|
|
951
|
-
state.user = { name: 'Jane', profile: { age: 25 } }
|
|
952
|
-
expect(callCount).toBe(1)
|
|
953
|
-
|
|
954
|
-
// Changes to the new user object should trigger
|
|
955
|
-
state.user.name = 'Bob'
|
|
956
|
-
expect(callCount).toBe(2)
|
|
957
|
-
|
|
958
|
-
state.user.profile.age = 26
|
|
959
|
-
expect(callCount).toBe(3)
|
|
960
|
-
|
|
961
|
-
stop()
|
|
962
|
-
})
|
|
963
|
-
|
|
964
|
-
it('should handle immediate option', () => {
|
|
965
|
-
const state = reactive({
|
|
966
|
-
user: {
|
|
967
|
-
name: 'John',
|
|
968
|
-
profile: { age: 30 },
|
|
969
|
-
},
|
|
970
|
-
})
|
|
971
|
-
let callCount = 0
|
|
972
|
-
|
|
973
|
-
const stop = watch(
|
|
974
|
-
state,
|
|
975
|
-
() => {
|
|
976
|
-
callCount++
|
|
977
|
-
},
|
|
978
|
-
{ immediate: true, deep: true }
|
|
979
|
-
)
|
|
980
|
-
|
|
981
|
-
// Should trigger immediately
|
|
982
|
-
expect(callCount).toBe(1)
|
|
983
|
-
|
|
984
|
-
state.user.name = 'Jane'
|
|
985
|
-
expect(callCount).toBe(2)
|
|
986
|
-
|
|
987
|
-
stop()
|
|
988
|
-
})
|
|
989
|
-
|
|
990
|
-
it('should handle multiple deep watchers on the same object', () => {
|
|
991
|
-
const state = reactive({
|
|
992
|
-
user: {
|
|
993
|
-
name: 'John',
|
|
994
|
-
profile: { age: 30 },
|
|
995
|
-
},
|
|
996
|
-
})
|
|
997
|
-
let watcher1Calls = 0
|
|
998
|
-
let watcher2Calls = 0
|
|
999
|
-
|
|
1000
|
-
const stop1 = watch(
|
|
1001
|
-
state,
|
|
1002
|
-
() => {
|
|
1003
|
-
watcher1Calls++
|
|
1004
|
-
},
|
|
1005
|
-
{ deep: true }
|
|
1006
|
-
)
|
|
1007
|
-
|
|
1008
|
-
const stop2 = watch(
|
|
1009
|
-
state,
|
|
1010
|
-
() => {
|
|
1011
|
-
watcher2Calls++
|
|
1012
|
-
},
|
|
1013
|
-
{ deep: true }
|
|
1014
|
-
)
|
|
1015
|
-
|
|
1016
|
-
state.user.name = 'Jane'
|
|
1017
|
-
expect(watcher1Calls).toBe(1)
|
|
1018
|
-
expect(watcher2Calls).toBe(1)
|
|
1019
|
-
|
|
1020
|
-
state.user.profile.age = 31
|
|
1021
|
-
expect(watcher1Calls).toBe(2)
|
|
1022
|
-
expect(watcher2Calls).toBe(2)
|
|
1023
|
-
|
|
1024
|
-
stop1()
|
|
1025
|
-
stop2()
|
|
1026
|
-
})
|
|
1027
|
-
|
|
1028
|
-
it('should stop watching when cleanup is called', () => {
|
|
1029
|
-
const state = reactive({
|
|
1030
|
-
user: {
|
|
1031
|
-
name: 'John',
|
|
1032
|
-
profile: { age: 30 },
|
|
1033
|
-
},
|
|
1034
|
-
})
|
|
1035
|
-
let callCount = 0
|
|
1036
|
-
|
|
1037
|
-
const stop = watch(
|
|
1038
|
-
state,
|
|
1039
|
-
() => {
|
|
1040
|
-
callCount++
|
|
1041
|
-
},
|
|
1042
|
-
{ deep: true }
|
|
1043
|
-
)
|
|
1044
|
-
|
|
1045
|
-
state.user.name = 'Jane'
|
|
1046
|
-
expect(callCount).toBe(1)
|
|
1047
|
-
|
|
1048
|
-
stop()
|
|
1049
|
-
|
|
1050
|
-
state.user.profile.age = 31
|
|
1051
|
-
expect(callCount).toBe(1) // Should not increment after stop
|
|
1052
|
-
})
|
|
1053
|
-
|
|
1054
|
-
it('should handle circular references', () => {
|
|
1055
|
-
const state = reactive({ name: 'John' }) as any
|
|
1056
|
-
state.self = state // Create circular reference
|
|
1057
|
-
let callCount = 0
|
|
1058
|
-
|
|
1059
|
-
const stop = watch(
|
|
1060
|
-
state,
|
|
1061
|
-
() => {
|
|
1062
|
-
callCount++
|
|
1063
|
-
},
|
|
1064
|
-
{ deep: true }
|
|
1065
|
-
)
|
|
1066
|
-
|
|
1067
|
-
state.name = 'Jane'
|
|
1068
|
-
expect(callCount).toBe(1)
|
|
1069
|
-
|
|
1070
|
-
stop()
|
|
1071
|
-
})
|
|
1072
|
-
|
|
1073
|
-
it('should handle deeply nested objects', () => {
|
|
1074
|
-
const state = reactive({
|
|
1075
|
-
level1: {
|
|
1076
|
-
level2: {
|
|
1077
|
-
level3: {
|
|
1078
|
-
level4: {
|
|
1079
|
-
value: 'deep',
|
|
1080
|
-
},
|
|
1081
|
-
},
|
|
1082
|
-
},
|
|
1083
|
-
},
|
|
1084
|
-
})
|
|
1085
|
-
let callCount = 0
|
|
1086
|
-
|
|
1087
|
-
const stop = watch(
|
|
1088
|
-
state,
|
|
1089
|
-
() => {
|
|
1090
|
-
callCount++
|
|
1091
|
-
},
|
|
1092
|
-
{ deep: true }
|
|
1093
|
-
)
|
|
1094
|
-
|
|
1095
|
-
state.level1.level2.level3.level4.value = 'deeper'
|
|
1096
|
-
expect(callCount).toBe(1)
|
|
1097
|
-
|
|
1098
|
-
stop()
|
|
1099
|
-
})
|
|
1100
|
-
})
|
|
1101
|
-
|
|
1102
|
-
describe('performance and edge cases', () => {
|
|
1103
|
-
it('should handle large object graphs efficiently', () => {
|
|
1104
|
-
// Create a large object graph
|
|
1105
|
-
const state = reactive({ items: [] as any[] })
|
|
1106
|
-
for (let i = 0; i < 100; i++) {
|
|
1107
|
-
state.items.push({ id: i, nested: { value: i * 2 } })
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
let callCount = 0
|
|
1111
|
-
const stop = watch(
|
|
1112
|
-
state,
|
|
1113
|
-
() => {
|
|
1114
|
-
callCount++
|
|
1115
|
-
},
|
|
1116
|
-
{ deep: true }
|
|
1117
|
-
)
|
|
1118
|
-
|
|
1119
|
-
// Change one item
|
|
1120
|
-
state.items[50].nested.value = 999
|
|
1121
|
-
// deep reactivity should be in touch, not `set`
|
|
1122
|
-
expect(callCount).toBe(1)
|
|
1123
|
-
|
|
1124
|
-
stop()
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
it('should trigger deep watch when pushing objects to reactive array', () => {
|
|
1128
|
-
const state = reactive({ items: [] as any[] })
|
|
1129
|
-
|
|
1130
|
-
let callCount = 0
|
|
1131
|
-
const stop = watch(
|
|
1132
|
-
state,
|
|
1133
|
-
() => {
|
|
1134
|
-
callCount++
|
|
1135
|
-
},
|
|
1136
|
-
{ deep: true }
|
|
1137
|
-
)
|
|
1138
|
-
|
|
1139
|
-
// Push an object with nested properties
|
|
1140
|
-
state.items.push({ id: 1, nested: { value: 'test' } })
|
|
1141
|
-
|
|
1142
|
-
// This should trigger deep watch because we're adding a new object to the array
|
|
1143
|
-
// If this fails, it means push() is not properly tracking deep changes
|
|
1144
|
-
expect(callCount).toBe(1)
|
|
1145
|
-
|
|
1146
|
-
state.items[0].nested.value = 'updated'
|
|
1147
|
-
expect(callCount).toBe(2)
|
|
1148
|
-
|
|
1149
|
-
stop()
|
|
1150
|
-
})
|
|
1151
|
-
|
|
1152
|
-
it('should trigger deep watch when pushing nested objects to reactive array', () => {
|
|
1153
|
-
const state = reactive({ items: [] as any[] })
|
|
1154
|
-
|
|
1155
|
-
let callCount = 0
|
|
1156
|
-
const stop = watch(
|
|
1157
|
-
state,
|
|
1158
|
-
() => {
|
|
1159
|
-
callCount++
|
|
1160
|
-
},
|
|
1161
|
-
{ deep: true }
|
|
1162
|
-
)
|
|
1163
|
-
|
|
1164
|
-
// Push a nested object
|
|
1165
|
-
state.items.push({
|
|
1166
|
-
id: 1,
|
|
1167
|
-
data: {
|
|
1168
|
-
config: {
|
|
1169
|
-
enabled: true,
|
|
1170
|
-
},
|
|
1171
|
-
},
|
|
1172
|
-
})
|
|
1173
|
-
|
|
1174
|
-
// This should trigger deep watch because we're adding a deeply nested object
|
|
1175
|
-
// If this fails, it means push() is not properly tracking deep changes for nested objects
|
|
1176
|
-
expect(callCount).toBe(1)
|
|
1177
|
-
|
|
1178
|
-
stop()
|
|
1179
|
-
})
|
|
1180
|
-
|
|
1181
|
-
it('should handle native operations', () => {
|
|
1182
|
-
// Create a large object graph
|
|
1183
|
-
const state = reactive({ items: [] as any[] })
|
|
1184
|
-
|
|
1185
|
-
let callCount = 0
|
|
1186
|
-
const stop = watch(
|
|
1187
|
-
state,
|
|
1188
|
-
() => {
|
|
1189
|
-
callCount++
|
|
1190
|
-
},
|
|
1191
|
-
{ deep: true }
|
|
1192
|
-
)
|
|
1193
|
-
|
|
1194
|
-
expect(callCount).toBe(0)
|
|
1195
|
-
state.items.push({ nested: { value: 0 } })
|
|
1196
|
-
|
|
1197
|
-
expect(callCount).toBe(1)
|
|
1198
|
-
// Change one item
|
|
1199
|
-
state.items[0].nested.value = 999
|
|
1200
|
-
// deep reactivity should be in touch, not `set`
|
|
1201
|
-
expect(callCount).toBe(2)
|
|
1202
|
-
|
|
1203
|
-
stop()
|
|
1204
|
-
})
|
|
1205
|
-
|
|
1206
|
-
it('should follow native operations', () => {
|
|
1207
|
-
// Create a large object graph
|
|
1208
|
-
const state = reactive({ items: [] as any[] })
|
|
1209
|
-
const item = reactive({ value: 0 })
|
|
1210
|
-
|
|
1211
|
-
let callCount = 0
|
|
1212
|
-
const stop = watch(
|
|
1213
|
-
state,
|
|
1214
|
-
() => {
|
|
1215
|
-
callCount++
|
|
1216
|
-
},
|
|
1217
|
-
{ deep: true }
|
|
1218
|
-
)
|
|
1219
|
-
|
|
1220
|
-
expect(callCount).toBe(0)
|
|
1221
|
-
|
|
1222
|
-
state.items.push(item)
|
|
1223
|
-
//state.items[0] = item
|
|
1224
|
-
|
|
1225
|
-
expect(callCount).toBe(1)
|
|
1226
|
-
// Change one item
|
|
1227
|
-
item.value = 999
|
|
1228
|
-
// deep reactivity should be in touch, not `set`
|
|
1229
|
-
expect(callCount).toBe(2)
|
|
1230
|
-
|
|
1231
|
-
stop()
|
|
1232
|
-
})
|
|
1233
|
-
|
|
1234
|
-
it('should handle non-reactive objects gracefully', () => {
|
|
1235
|
-
const plainObject = {
|
|
1236
|
-
user: {
|
|
1237
|
-
name: 'John',
|
|
1238
|
-
profile: { age: 30 },
|
|
1239
|
-
},
|
|
1240
|
-
}
|
|
1241
|
-
let callCount = 0
|
|
1242
|
-
|
|
1243
|
-
// This should not throw but also not trigger
|
|
1244
|
-
const stop = watch(
|
|
1245
|
-
plainObject as any,
|
|
1246
|
-
() => {
|
|
1247
|
-
callCount++
|
|
1248
|
-
},
|
|
1249
|
-
{ deep: true }
|
|
1250
|
-
)
|
|
1251
|
-
|
|
1252
|
-
plainObject.user.name = 'Jane'
|
|
1253
|
-
expect(callCount).toBe(0) // Should not trigger for non-reactive objects
|
|
1254
|
-
|
|
1255
|
-
stop()
|
|
1256
|
-
})
|
|
1257
|
-
})
|
|
1258
|
-
|
|
1259
|
-
describe('minimal deep watch failure example', () => {
|
|
1260
|
-
it('MINIMAL: watch should detect array mutations', () => {
|
|
1261
|
-
const state = reactive({
|
|
1262
|
-
items: [1, 2, 3],
|
|
1263
|
-
})
|
|
1264
|
-
|
|
1265
|
-
let callCount = 0
|
|
1266
|
-
|
|
1267
|
-
// This is the minimal failing case
|
|
1268
|
-
const stopWatch = watch(
|
|
1269
|
-
state,
|
|
1270
|
-
() => {
|
|
1271
|
-
callCount++
|
|
1272
|
-
},
|
|
1273
|
-
{ immediate: true, deep: true }
|
|
1274
|
-
)
|
|
1275
|
-
|
|
1276
|
-
expect(callCount).toBe(1) // Initial call
|
|
1277
|
-
|
|
1278
|
-
// This should trigger the watch but doesn't
|
|
1279
|
-
state.items.push(4)
|
|
1280
|
-
expect(callCount).toBe(2) // FAILS: Expected 2, Received 1
|
|
1281
|
-
|
|
1282
|
-
stopWatch()
|
|
1283
|
-
})
|
|
1284
|
-
|
|
1285
|
-
it('MINIMAL: deep watch should detect array mutations', () => {
|
|
1286
|
-
const state = reactive({
|
|
1287
|
-
items: [1, 2, 3],
|
|
1288
|
-
})
|
|
1289
|
-
|
|
1290
|
-
let callCount = 0
|
|
1291
|
-
|
|
1292
|
-
// Even with deep: true, this fails
|
|
1293
|
-
const stopWatch = watch(
|
|
1294
|
-
state,
|
|
1295
|
-
() => {
|
|
1296
|
-
callCount++
|
|
1297
|
-
},
|
|
1298
|
-
{ immediate: true, deep: true }
|
|
1299
|
-
)
|
|
1300
|
-
|
|
1301
|
-
expect(callCount).toBe(1) // Initial call
|
|
1302
|
-
|
|
1303
|
-
// This should trigger the deep watch but doesn't
|
|
1304
|
-
state.items.push(4)
|
|
1305
|
-
expect(callCount).toBe(2) // FAILS: Expected 2, Received 1
|
|
1306
|
-
|
|
1307
|
-
stopWatch()
|
|
1308
|
-
})
|
|
1309
|
-
|
|
1310
|
-
it('COMPARISON: effect with length DOES detect array mutations', () => {
|
|
1311
|
-
const state = reactive({
|
|
1312
|
-
items: [1, 2, 3],
|
|
1313
|
-
})
|
|
1314
|
-
|
|
1315
|
-
let effectCount = 0
|
|
1316
|
-
|
|
1317
|
-
// This works - effect detects array mutations when we access length
|
|
1318
|
-
const stopEffect = effect(() => {
|
|
1319
|
-
effectCount++
|
|
1320
|
-
state.items.length // Access the array length
|
|
1321
|
-
})
|
|
1322
|
-
|
|
1323
|
-
expect(effectCount).toBe(1) // Initial call
|
|
1324
|
-
|
|
1325
|
-
// This DOES trigger the effect because push changes length
|
|
1326
|
-
state.items.push(4)
|
|
1327
|
-
expect(effectCount).toBe(2) // Should PASS
|
|
1328
|
-
|
|
1329
|
-
stopEffect()
|
|
1330
|
-
})
|
|
1331
|
-
|
|
1332
|
-
it('DEBUG: what happens with just array access', () => {
|
|
1333
|
-
const state = reactive({
|
|
1334
|
-
items: [1, 2, 3],
|
|
1335
|
-
})
|
|
1336
|
-
|
|
1337
|
-
let effectCount = 0
|
|
1338
|
-
|
|
1339
|
-
// What happens when we just access the array reference?
|
|
1340
|
-
const stopEffect = effect(() => {
|
|
1341
|
-
effectCount++
|
|
1342
|
-
state.items // Just access the array reference
|
|
1343
|
-
})
|
|
1344
|
-
|
|
1345
|
-
expect(effectCount).toBe(1) // Initial call
|
|
1346
|
-
|
|
1347
|
-
// Does this trigger? It shouldn't, because we didn't access any properties
|
|
1348
|
-
state.items.push(4)
|
|
1349
|
-
expect(effectCount).toBe(1) // Should stay 1
|
|
1350
|
-
|
|
1351
|
-
stopEffect()
|
|
1352
|
-
})
|
|
1353
|
-
})
|
|
1354
|
-
|
|
1355
|
-
describe('deep watching Sets and Maps', () => {
|
|
1356
|
-
it('should detect Set mutations with deep watch', () => {
|
|
1357
|
-
const state = reactive({
|
|
1358
|
-
mySet: new Set([1, 2, 3]),
|
|
1359
|
-
})
|
|
1360
|
-
|
|
1361
|
-
let callCount = 0
|
|
1362
|
-
|
|
1363
|
-
const stopWatch = watch(
|
|
1364
|
-
state,
|
|
1365
|
-
() => {
|
|
1366
|
-
callCount++
|
|
1367
|
-
},
|
|
1368
|
-
{ immediate: true, deep: true }
|
|
1369
|
-
)
|
|
1370
|
-
|
|
1371
|
-
expect(callCount).toBe(1) // Initial call
|
|
1372
|
-
|
|
1373
|
-
// Test Set mutations
|
|
1374
|
-
state.mySet.add(4)
|
|
1375
|
-
expect(callCount).toBe(2) // Might fail
|
|
1376
|
-
|
|
1377
|
-
state.mySet.delete(1)
|
|
1378
|
-
expect(callCount).toBe(3) // Might fail
|
|
1379
|
-
|
|
1380
|
-
stopWatch()
|
|
1381
|
-
})
|
|
1382
|
-
|
|
1383
|
-
it('should detect Map mutations with deep watch', () => {
|
|
1384
|
-
const state = reactive({
|
|
1385
|
-
myMap: new Map([
|
|
1386
|
-
['a', 1],
|
|
1387
|
-
['b', 2],
|
|
1388
|
-
]),
|
|
1389
|
-
})
|
|
1390
|
-
|
|
1391
|
-
let callCount = 0
|
|
1392
|
-
|
|
1393
|
-
const stopWatch = watch(
|
|
1394
|
-
state,
|
|
1395
|
-
() => {
|
|
1396
|
-
callCount++
|
|
1397
|
-
},
|
|
1398
|
-
{ immediate: true, deep: true }
|
|
1399
|
-
)
|
|
1400
|
-
|
|
1401
|
-
expect(callCount).toBe(1) // Initial call
|
|
1402
|
-
|
|
1403
|
-
// Test Map mutations
|
|
1404
|
-
state.myMap.set('c', 3)
|
|
1405
|
-
expect(callCount).toBe(2) // Might fail
|
|
1406
|
-
|
|
1407
|
-
state.myMap.delete('a')
|
|
1408
|
-
expect(callCount).toBe(3) // Might fail
|
|
1409
|
-
|
|
1410
|
-
stopWatch()
|
|
1411
|
-
})
|
|
1412
|
-
|
|
1413
|
-
it('should detect Map value changes with deep watch', () => {
|
|
1414
|
-
const state = reactive({
|
|
1415
|
-
myMap: new Map([
|
|
1416
|
-
['a', 1],
|
|
1417
|
-
['b', 2],
|
|
1418
|
-
]),
|
|
1419
|
-
})
|
|
1420
|
-
|
|
1421
|
-
let callCount = 0
|
|
1422
|
-
|
|
1423
|
-
const stopWatch = watch(
|
|
1424
|
-
state,
|
|
1425
|
-
() => {
|
|
1426
|
-
callCount++
|
|
1427
|
-
},
|
|
1428
|
-
{ immediate: true, deep: true }
|
|
1429
|
-
)
|
|
1430
|
-
|
|
1431
|
-
expect(callCount).toBe(1) // Initial call
|
|
1432
|
-
|
|
1433
|
-
// Test Map value changes
|
|
1434
|
-
state.myMap.set('a', 10)
|
|
1435
|
-
expect(callCount).toBe(2) // Might fail
|
|
1436
|
-
|
|
1437
|
-
stopWatch()
|
|
1438
|
-
})
|
|
1439
|
-
|
|
1440
|
-
it('should detect nested Set/Map mutations with deep watch', () => {
|
|
1441
|
-
const state = reactive({
|
|
1442
|
-
container: {
|
|
1443
|
-
mySet: new Set([1, 2, 3]),
|
|
1444
|
-
myMap: new Map([
|
|
1445
|
-
['x', 1],
|
|
1446
|
-
['y', 2],
|
|
1447
|
-
]),
|
|
1448
|
-
},
|
|
1449
|
-
})
|
|
1450
|
-
|
|
1451
|
-
let callCount = 0
|
|
1452
|
-
|
|
1453
|
-
const stopWatch = watch(
|
|
1454
|
-
state,
|
|
1455
|
-
() => {
|
|
1456
|
-
callCount++
|
|
1457
|
-
},
|
|
1458
|
-
{ immediate: true, deep: true }
|
|
1459
|
-
)
|
|
1460
|
-
|
|
1461
|
-
expect(callCount).toBe(1) // Initial call
|
|
1462
|
-
|
|
1463
|
-
// Test nested Set mutations
|
|
1464
|
-
state.container.mySet.add(4)
|
|
1465
|
-
expect(callCount).toBe(2) // Might fail
|
|
1466
|
-
|
|
1467
|
-
// Test nested Map mutations
|
|
1468
|
-
state.container.myMap.set('z', 3)
|
|
1469
|
-
expect(callCount).toBe(3) // Might fail
|
|
1470
|
-
|
|
1471
|
-
stopWatch()
|
|
1472
|
-
})
|
|
1473
|
-
|
|
1474
|
-
// Note: WeakSet and WeakMap cannot be deeply reactive because they don't support iteration
|
|
1475
|
-
// They can only have shallow reactivity (tracking when the collection itself changes)
|
|
1476
|
-
})
|
|
1477
|
-
})
|