@pyreon/reactivity 0.24.5 → 0.24.6
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/package.json +1 -4
- package/src/batch.ts +0 -196
- package/src/cell.ts +0 -72
- package/src/computed.ts +0 -313
- package/src/createSelector.ts +0 -109
- package/src/debug.ts +0 -134
- package/src/effect.ts +0 -467
- package/src/env.d.ts +0 -6
- package/src/index.ts +0 -60
- package/src/lpih.ts +0 -227
- package/src/manifest.ts +0 -660
- package/src/reactive-devtools.ts +0 -494
- package/src/reactive-trace.ts +0 -142
- package/src/reconcile.ts +0 -118
- package/src/resource.ts +0 -84
- package/src/scope.ts +0 -123
- package/src/signal.ts +0 -261
- package/src/store.ts +0 -250
- package/src/tests/batch.test.ts +0 -751
- package/src/tests/bind.test.ts +0 -84
- package/src/tests/branches.test.ts +0 -343
- package/src/tests/cell.test.ts +0 -159
- package/src/tests/computed.test.ts +0 -436
- package/src/tests/coverage-hardening.test.ts +0 -471
- package/src/tests/createSelector.test.ts +0 -291
- package/src/tests/debug.test.ts +0 -196
- package/src/tests/effect.test.ts +0 -464
- package/src/tests/fanout-repro.test.ts +0 -179
- package/src/tests/lpih-source-location.test.ts +0 -277
- package/src/tests/lpih.test.ts +0 -351
- package/src/tests/manifest-snapshot.test.ts +0 -96
- package/src/tests/reactive-devtools-treeshake.test.ts +0 -48
- package/src/tests/reactive-devtools.test.ts +0 -296
- package/src/tests/reactive-trace.test.ts +0 -102
- package/src/tests/reconcile-security.test.ts +0 -45
- package/src/tests/resource.test.ts +0 -326
- package/src/tests/scope.test.ts +0 -231
- package/src/tests/signal.test.ts +0 -368
- package/src/tests/store.test.ts +0 -286
- package/src/tests/tracking.test.ts +0 -158
- package/src/tests/vue-parity.test.ts +0 -191
- package/src/tests/watch.test.ts +0 -246
- package/src/tracking.ts +0 -139
- package/src/watch.ts +0 -68
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { batch } from '../batch'
|
|
2
|
-
import { effect, renderEffect } from '../effect'
|
|
3
|
-
import { signal } from '../signal'
|
|
4
|
-
import { runUntracked } from '../tracking'
|
|
5
|
-
|
|
6
|
-
describe('tracking', () => {
|
|
7
|
-
describe('notifySubscribers', () => {
|
|
8
|
-
test('multi-subscriber notification without batching (snapshot path)', () => {
|
|
9
|
-
const s = signal(0)
|
|
10
|
-
let runs1 = 0
|
|
11
|
-
let runs2 = 0
|
|
12
|
-
|
|
13
|
-
effect(() => {
|
|
14
|
-
s()
|
|
15
|
-
runs1++
|
|
16
|
-
})
|
|
17
|
-
effect(() => {
|
|
18
|
-
s()
|
|
19
|
-
runs2++
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
expect(runs1).toBe(1)
|
|
23
|
-
expect(runs2).toBe(1)
|
|
24
|
-
|
|
25
|
-
// Triggers non-batched multi-subscriber path (snapshot via [...subscribers])
|
|
26
|
-
s.set(1)
|
|
27
|
-
expect(runs1).toBe(2)
|
|
28
|
-
expect(runs2).toBe(2)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('multi-subscriber notification during batching', () => {
|
|
32
|
-
const s = signal(0)
|
|
33
|
-
let runs1 = 0
|
|
34
|
-
let runs2 = 0
|
|
35
|
-
|
|
36
|
-
effect(() => {
|
|
37
|
-
s()
|
|
38
|
-
runs1++
|
|
39
|
-
})
|
|
40
|
-
effect(() => {
|
|
41
|
-
s()
|
|
42
|
-
runs2++
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
batch(() => {
|
|
46
|
-
s.set(1)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
expect(runs1).toBe(2)
|
|
50
|
-
expect(runs2).toBe(2)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('single subscriber batching path', () => {
|
|
54
|
-
const s = signal(0)
|
|
55
|
-
let runs = 0
|
|
56
|
-
|
|
57
|
-
effect(() => {
|
|
58
|
-
s()
|
|
59
|
-
runs++
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
batch(() => {
|
|
63
|
-
s.set(1)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
expect(runs).toBe(2)
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('runUntracked', () => {
|
|
71
|
-
test('signal reads inside runUntracked do not create dependencies', () => {
|
|
72
|
-
const s = signal(0)
|
|
73
|
-
let runs = 0
|
|
74
|
-
|
|
75
|
-
effect(() => {
|
|
76
|
-
runUntracked(() => s())
|
|
77
|
-
runs++
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
expect(runs).toBe(1)
|
|
81
|
-
s.set(1)
|
|
82
|
-
expect(runs).toBe(1) // not re-run
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('restores tracking context after runUntracked', () => {
|
|
86
|
-
const tracked = signal(0)
|
|
87
|
-
const untracked = signal(0)
|
|
88
|
-
let runs = 0
|
|
89
|
-
|
|
90
|
-
effect(() => {
|
|
91
|
-
tracked()
|
|
92
|
-
runUntracked(() => untracked())
|
|
93
|
-
runs++
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
expect(runs).toBe(1)
|
|
97
|
-
|
|
98
|
-
// tracked signal should still trigger re-run
|
|
99
|
-
tracked.set(1)
|
|
100
|
-
expect(runs).toBe(2)
|
|
101
|
-
|
|
102
|
-
// untracked signal should not
|
|
103
|
-
untracked.set(1)
|
|
104
|
-
expect(runs).toBe(2)
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
describe('trackSubscriber with depsCollector', () => {
|
|
109
|
-
test('renderEffect uses fast deps collector path', () => {
|
|
110
|
-
const s = signal(0)
|
|
111
|
-
let runs = 0
|
|
112
|
-
|
|
113
|
-
const dispose = renderEffect(() => {
|
|
114
|
-
s()
|
|
115
|
-
runs++
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
s.set(1)
|
|
119
|
-
expect(runs).toBe(2)
|
|
120
|
-
|
|
121
|
-
dispose()
|
|
122
|
-
s.set(2)
|
|
123
|
-
expect(runs).toBe(2)
|
|
124
|
-
})
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
describe('cleanupEffect', () => {
|
|
128
|
-
test('effect dynamically tracks/untracks deps on re-run', () => {
|
|
129
|
-
const cond = signal(true)
|
|
130
|
-
const a = signal(0)
|
|
131
|
-
const b = signal(0)
|
|
132
|
-
let runs = 0
|
|
133
|
-
|
|
134
|
-
effect(() => {
|
|
135
|
-
if (cond()) {
|
|
136
|
-
a()
|
|
137
|
-
} else {
|
|
138
|
-
b()
|
|
139
|
-
}
|
|
140
|
-
runs++
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
expect(runs).toBe(1)
|
|
144
|
-
|
|
145
|
-
a.set(1) // tracked
|
|
146
|
-
expect(runs).toBe(2)
|
|
147
|
-
|
|
148
|
-
cond.set(false) // switch branch
|
|
149
|
-
expect(runs).toBe(3)
|
|
150
|
-
|
|
151
|
-
a.set(2) // no longer tracked
|
|
152
|
-
expect(runs).toBe(3)
|
|
153
|
-
|
|
154
|
-
b.set(1) // now tracked
|
|
155
|
-
expect(runs).toBe(4)
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
})
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
// Tests for the Vue-parity APIs added in M4: markRaw, shallowReactive,
|
|
2
|
-
// onScopeDispose. Each surface is independent; grouped here for legibility
|
|
3
|
-
// rather than spreading across signal/store/scope test files.
|
|
4
|
-
|
|
5
|
-
import { effect } from '../effect'
|
|
6
|
-
import { effectScope, getCurrentScope, onScopeDispose } from '../scope'
|
|
7
|
-
import { signal } from '../signal'
|
|
8
|
-
import { createStore, markRaw, shallowReactive } from '../store'
|
|
9
|
-
|
|
10
|
-
describe('markRaw', () => {
|
|
11
|
-
test('marks an object as raw — createStore returns it unwrapped', () => {
|
|
12
|
-
const raw = markRaw({ x: 1 })
|
|
13
|
-
const store = createStore({ inner: raw })
|
|
14
|
-
// Reading store.inner returns the raw reference, not a proxy.
|
|
15
|
-
expect(store.inner).toBe(raw)
|
|
16
|
-
// Mutating raw does NOT trigger reactivity (it's not proxied).
|
|
17
|
-
let runs = 0
|
|
18
|
-
effect(() => {
|
|
19
|
-
void store.inner.x
|
|
20
|
-
runs++
|
|
21
|
-
})
|
|
22
|
-
expect(runs).toBe(1)
|
|
23
|
-
raw.x = 99
|
|
24
|
-
expect(runs).toBe(1) // not reactive — raw bypasses proxy
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('also opts out of shallowReactive wrapping', () => {
|
|
28
|
-
const raw = markRaw({ y: 2 })
|
|
29
|
-
const store = shallowReactive({ inner: raw })
|
|
30
|
-
expect(store.inner).toBe(raw)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('class instances marked raw are usable without proxy quirks', () => {
|
|
34
|
-
class Editor {
|
|
35
|
-
cursor = 0
|
|
36
|
-
moveTo(pos: number): void {
|
|
37
|
-
this.cursor = pos
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const ed = markRaw(new Editor())
|
|
41
|
-
const store = createStore({ editor: ed })
|
|
42
|
-
expect(() => store.editor.moveTo(5)).not.toThrow()
|
|
43
|
-
expect(store.editor.cursor).toBe(5)
|
|
44
|
-
expect(store.editor).toBe(ed) // identity preserved
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
test('markRaw is idempotent', () => {
|
|
48
|
-
const obj = { x: 1 }
|
|
49
|
-
expect(() => {
|
|
50
|
-
markRaw(obj)
|
|
51
|
-
markRaw(obj)
|
|
52
|
-
}).not.toThrow()
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe('shallowReactive', () => {
|
|
57
|
-
test('top-level mutations trigger reactivity', () => {
|
|
58
|
-
const store = shallowReactive({ count: 0 })
|
|
59
|
-
let seen = -1
|
|
60
|
-
effect(() => {
|
|
61
|
-
seen = store.count
|
|
62
|
-
})
|
|
63
|
-
expect(seen).toBe(0)
|
|
64
|
-
store.count = 5
|
|
65
|
-
expect(seen).toBe(5)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('nested mutations do NOT trigger reactivity (shallow)', () => {
|
|
69
|
-
const store = shallowReactive({ user: { name: 'Alice' } })
|
|
70
|
-
let runs = 0
|
|
71
|
-
effect(() => {
|
|
72
|
-
void store.user.name
|
|
73
|
-
runs++
|
|
74
|
-
})
|
|
75
|
-
expect(runs).toBe(1)
|
|
76
|
-
// Nested mutation — should NOT re-run because nested object is raw.
|
|
77
|
-
store.user.name = 'Bob'
|
|
78
|
-
expect(runs).toBe(1)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('replacing a top-level reference DOES trigger reactivity', () => {
|
|
82
|
-
const store = shallowReactive({ user: { name: 'Alice' } })
|
|
83
|
-
const names: string[] = []
|
|
84
|
-
effect(() => {
|
|
85
|
-
names.push(store.user.name)
|
|
86
|
-
})
|
|
87
|
-
expect(names).toEqual(['Alice'])
|
|
88
|
-
store.user = { name: 'Bob' }
|
|
89
|
-
expect(names).toEqual(['Alice', 'Bob'])
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('nested reads return raw references, not proxies', () => {
|
|
93
|
-
const inner = { x: 1 }
|
|
94
|
-
const store = shallowReactive({ inner })
|
|
95
|
-
expect(store.inner).toBe(inner)
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
test('separate cache from createStore — same raw can be both shallow and deep', () => {
|
|
99
|
-
const raw = { x: 1 }
|
|
100
|
-
const deep = createStore(raw)
|
|
101
|
-
const shallow = shallowReactive({ wrapper: raw })
|
|
102
|
-
// The shallow store returns raw nested, not the deep proxy.
|
|
103
|
-
expect(shallow.wrapper).toBe(raw)
|
|
104
|
-
// The deep store wraps raw.
|
|
105
|
-
expect(deep).not.toBe(raw)
|
|
106
|
-
})
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe('onScopeDispose', () => {
|
|
110
|
-
test('callback fires when scope stops', () => {
|
|
111
|
-
const scope = effectScope()
|
|
112
|
-
let disposed = 0
|
|
113
|
-
scope.runInScope(() => {
|
|
114
|
-
onScopeDispose(() => {
|
|
115
|
-
disposed++
|
|
116
|
-
})
|
|
117
|
-
})
|
|
118
|
-
expect(disposed).toBe(0)
|
|
119
|
-
scope.stop()
|
|
120
|
-
expect(disposed).toBe(1)
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test('multiple callbacks all fire, in registration order', () => {
|
|
124
|
-
const scope = effectScope()
|
|
125
|
-
const order: string[] = []
|
|
126
|
-
scope.runInScope(() => {
|
|
127
|
-
onScopeDispose(() => order.push('first'))
|
|
128
|
-
onScopeDispose(() => order.push('second'))
|
|
129
|
-
onScopeDispose(() => order.push('third'))
|
|
130
|
-
})
|
|
131
|
-
scope.stop()
|
|
132
|
-
expect(order).toEqual(['first', 'second', 'third'])
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
test('warns when called outside any scope (dev mode)', () => {
|
|
136
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
137
|
-
let called = false
|
|
138
|
-
onScopeDispose(() => {
|
|
139
|
-
called = true
|
|
140
|
-
})
|
|
141
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
142
|
-
expect.stringContaining('without an active EffectScope'),
|
|
143
|
-
)
|
|
144
|
-
expect(called).toBe(false)
|
|
145
|
-
warnSpy.mockRestore()
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
test('captures the SCOPE active at registration time', () => {
|
|
149
|
-
const scopeA = effectScope()
|
|
150
|
-
const scopeB = effectScope()
|
|
151
|
-
const log: string[] = []
|
|
152
|
-
scopeA.runInScope(() => {
|
|
153
|
-
onScopeDispose(() => log.push('A'))
|
|
154
|
-
})
|
|
155
|
-
scopeB.runInScope(() => {
|
|
156
|
-
onScopeDispose(() => log.push('B'))
|
|
157
|
-
})
|
|
158
|
-
scopeA.stop()
|
|
159
|
-
expect(log).toEqual(['A'])
|
|
160
|
-
scopeB.stop()
|
|
161
|
-
expect(log).toEqual(['A', 'B'])
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('integrates with effect lifecycle — disposes alongside scope effects', () => {
|
|
165
|
-
const s = signal(0)
|
|
166
|
-
const scope = effectScope()
|
|
167
|
-
const events: string[] = []
|
|
168
|
-
scope.runInScope(() => {
|
|
169
|
-
effect(() => {
|
|
170
|
-
events.push(`effect:${s()}`)
|
|
171
|
-
})
|
|
172
|
-
onScopeDispose(() => events.push('disposed'))
|
|
173
|
-
})
|
|
174
|
-
s.set(1)
|
|
175
|
-
expect(events).toEqual(['effect:0', 'effect:1'])
|
|
176
|
-
scope.stop()
|
|
177
|
-
expect(events).toEqual(['effect:0', 'effect:1', 'disposed'])
|
|
178
|
-
s.set(2)
|
|
179
|
-
// Both effect and dispose callback are inactive after stop.
|
|
180
|
-
expect(events).toEqual(['effect:0', 'effect:1', 'disposed'])
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
test('captures null-scope correctly — no-op without throwing', () => {
|
|
184
|
-
expect(getCurrentScope()).toBeNull()
|
|
185
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
186
|
-
expect(() => {
|
|
187
|
-
onScopeDispose(() => {})
|
|
188
|
-
}).not.toThrow()
|
|
189
|
-
warnSpy.mockRestore()
|
|
190
|
-
})
|
|
191
|
-
})
|
package/src/tests/watch.test.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { signal } from '../signal'
|
|
2
|
-
import { watch } from '../watch'
|
|
3
|
-
|
|
4
|
-
describe('watch', () => {
|
|
5
|
-
test('calls callback when source changes', () => {
|
|
6
|
-
const s = signal(1)
|
|
7
|
-
const calls: [number, number | undefined][] = []
|
|
8
|
-
|
|
9
|
-
watch(
|
|
10
|
-
() => s(),
|
|
11
|
-
(newVal, oldVal) => {
|
|
12
|
-
calls.push([newVal, oldVal])
|
|
13
|
-
},
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
expect(calls.length).toBe(0) // not called on first run without immediate
|
|
17
|
-
|
|
18
|
-
s.set(2)
|
|
19
|
-
expect(calls).toEqual([[2, 1]])
|
|
20
|
-
|
|
21
|
-
s.set(3)
|
|
22
|
-
expect(calls).toEqual([
|
|
23
|
-
[2, 1],
|
|
24
|
-
[3, 2],
|
|
25
|
-
])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('immediate option calls callback on first run', () => {
|
|
29
|
-
const s = signal(1)
|
|
30
|
-
const calls: [number, number | undefined][] = []
|
|
31
|
-
|
|
32
|
-
watch(
|
|
33
|
-
() => s(),
|
|
34
|
-
(newVal, oldVal) => {
|
|
35
|
-
calls.push([newVal, oldVal])
|
|
36
|
-
},
|
|
37
|
-
{ immediate: true },
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
expect(calls).toEqual([[1, undefined]])
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('stop function disposes the watcher', () => {
|
|
44
|
-
const s = signal(1)
|
|
45
|
-
let callCount = 0
|
|
46
|
-
|
|
47
|
-
const stop = watch(
|
|
48
|
-
() => s(),
|
|
49
|
-
() => {
|
|
50
|
-
callCount++
|
|
51
|
-
},
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
s.set(2)
|
|
55
|
-
expect(callCount).toBe(1)
|
|
56
|
-
|
|
57
|
-
stop()
|
|
58
|
-
|
|
59
|
-
s.set(3)
|
|
60
|
-
expect(callCount).toBe(1) // no more calls
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('cleanup function is called before each re-run', () => {
|
|
64
|
-
const s = signal(1)
|
|
65
|
-
const log: string[] = []
|
|
66
|
-
|
|
67
|
-
watch(
|
|
68
|
-
() => s(),
|
|
69
|
-
(newVal) => {
|
|
70
|
-
log.push(`run-${newVal}`)
|
|
71
|
-
return () => log.push(`cleanup-${newVal}`)
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
s.set(2)
|
|
76
|
-
expect(log).toEqual(['run-2'])
|
|
77
|
-
|
|
78
|
-
s.set(3)
|
|
79
|
-
expect(log).toEqual(['run-2', 'cleanup-2', 'run-3'])
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('cleanup function from immediate is called on next change', () => {
|
|
83
|
-
const s = signal(1)
|
|
84
|
-
const log: string[] = []
|
|
85
|
-
|
|
86
|
-
watch(
|
|
87
|
-
() => s(),
|
|
88
|
-
(newVal) => {
|
|
89
|
-
log.push(`run-${newVal}`)
|
|
90
|
-
return () => log.push(`cleanup-${newVal}`)
|
|
91
|
-
},
|
|
92
|
-
{ immediate: true },
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
expect(log).toEqual(['run-1'])
|
|
96
|
-
|
|
97
|
-
s.set(2)
|
|
98
|
-
expect(log).toEqual(['run-1', 'cleanup-1', 'run-2'])
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('cleanup is called on stop', () => {
|
|
102
|
-
const s = signal(1)
|
|
103
|
-
const log: string[] = []
|
|
104
|
-
|
|
105
|
-
const stop = watch(
|
|
106
|
-
() => s(),
|
|
107
|
-
(newVal) => {
|
|
108
|
-
log.push(`run-${newVal}`)
|
|
109
|
-
return () => log.push(`cleanup-${newVal}`)
|
|
110
|
-
},
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
s.set(2)
|
|
114
|
-
expect(log).toEqual(['run-2'])
|
|
115
|
-
|
|
116
|
-
stop()
|
|
117
|
-
expect(log).toEqual(['run-2', 'cleanup-2'])
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
test('callback returning non-function does not set cleanup', () => {
|
|
121
|
-
const s = signal(1)
|
|
122
|
-
let callCount = 0
|
|
123
|
-
|
|
124
|
-
watch(
|
|
125
|
-
() => s(),
|
|
126
|
-
() => {
|
|
127
|
-
callCount++
|
|
128
|
-
// returns void, not a function
|
|
129
|
-
},
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
s.set(2)
|
|
133
|
-
s.set(3)
|
|
134
|
-
expect(callCount).toBe(2)
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
test('stop without cleanup does not throw', () => {
|
|
138
|
-
const s = signal(1)
|
|
139
|
-
const stop = watch(
|
|
140
|
-
() => s(),
|
|
141
|
-
() => {},
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
stop() // no cleanup function was set, should not throw
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test('oldValue tracks previous value across multiple changes', () => {
|
|
148
|
-
const s = signal('a')
|
|
149
|
-
const history: [string, string | undefined][] = []
|
|
150
|
-
|
|
151
|
-
watch(
|
|
152
|
-
() => s(),
|
|
153
|
-
(newVal, oldVal) => {
|
|
154
|
-
history.push([newVal, oldVal])
|
|
155
|
-
},
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
s.set('b')
|
|
159
|
-
s.set('c')
|
|
160
|
-
s.set('d')
|
|
161
|
-
|
|
162
|
-
expect(history).toEqual([
|
|
163
|
-
['b', 'a'],
|
|
164
|
-
['c', 'b'],
|
|
165
|
-
['d', 'c'],
|
|
166
|
-
])
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('oldValue is undefined on immediate first call', () => {
|
|
170
|
-
const s = signal(42)
|
|
171
|
-
let receivedOld: number | undefined = -1
|
|
172
|
-
|
|
173
|
-
watch(
|
|
174
|
-
() => s(),
|
|
175
|
-
(_newVal, oldVal) => {
|
|
176
|
-
receivedOld = oldVal
|
|
177
|
-
},
|
|
178
|
-
{ immediate: true },
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
expect(receivedOld).toBeUndefined()
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('watch with derived source (computed-like)', () => {
|
|
185
|
-
const a = signal(1)
|
|
186
|
-
const b = signal(10)
|
|
187
|
-
const calls: [number, number | undefined][] = []
|
|
188
|
-
|
|
189
|
-
watch(
|
|
190
|
-
() => a() + b(),
|
|
191
|
-
(newVal, oldVal) => {
|
|
192
|
-
calls.push([newVal, oldVal])
|
|
193
|
-
},
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
a.set(2) // 2 + 10 = 12
|
|
197
|
-
expect(calls).toEqual([[12, 11]])
|
|
198
|
-
|
|
199
|
-
b.set(20) // 2 + 20 = 22
|
|
200
|
-
expect(calls).toEqual([
|
|
201
|
-
[12, 11],
|
|
202
|
-
[22, 12],
|
|
203
|
-
])
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
test('stop prevents cleanup from running on future changes', () => {
|
|
207
|
-
const s = signal(1)
|
|
208
|
-
const log: string[] = []
|
|
209
|
-
|
|
210
|
-
const stop = watch(
|
|
211
|
-
() => s(),
|
|
212
|
-
(newVal) => {
|
|
213
|
-
log.push(`run-${newVal}`)
|
|
214
|
-
return () => log.push(`cleanup-${newVal}`)
|
|
215
|
-
},
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
s.set(2)
|
|
219
|
-
expect(log).toEqual(['run-2'])
|
|
220
|
-
|
|
221
|
-
stop()
|
|
222
|
-
expect(log).toEqual(['run-2', 'cleanup-2'])
|
|
223
|
-
|
|
224
|
-
// Further changes should not trigger anything
|
|
225
|
-
s.set(3)
|
|
226
|
-
expect(log).toEqual(['run-2', 'cleanup-2'])
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
test('watch does not fire when value stays the same', () => {
|
|
230
|
-
const s = signal(1)
|
|
231
|
-
let callCount = 0
|
|
232
|
-
|
|
233
|
-
watch(
|
|
234
|
-
() => s(),
|
|
235
|
-
() => {
|
|
236
|
-
callCount++
|
|
237
|
-
},
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
s.set(1) // same value — signal doesn't notify
|
|
241
|
-
expect(callCount).toBe(0)
|
|
242
|
-
|
|
243
|
-
s.set(2)
|
|
244
|
-
expect(callCount).toBe(1)
|
|
245
|
-
})
|
|
246
|
-
})
|