@zeix/cause-effect 0.17.2 → 0.18.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/.ai-context.md +163 -226
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/.zed/settings.json +3 -0
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +197 -202
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +241 -220
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1326 -1174
- package/index.js +1 -1
- package/index.ts +58 -85
- package/package.json +9 -6
- package/src/errors.ts +118 -70
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -0
- package/src/signal.ts +112 -64
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -69
- package/test/benchmark.test.ts +473 -485
- package/test/collection.test.ts +455 -955
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +332 -857
- package/test/memo.test.ts +380 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -271
- package/test/store.test.ts +346 -898
- package/test/task.test.ts +395 -0
- package/test/untrack.test.ts +167 -0
- package/test/util/dependency-graph.ts +2 -2
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +5 -7
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -19
- package/types/src/graph.d.ts +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +73 -0
- package/types/src/signal.d.ts +43 -28
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -688
- package/archive/collection.ts +0 -310
- package/archive/computed.ts +0 -198
- package/archive/list.ts +0 -544
- package/archive/memo.ts +0 -140
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -357
- package/archive/task.ts +0 -191
- package/src/classes/collection.ts +0 -298
- package/src/classes/composite.ts +0 -171
- package/src/classes/computed.ts +0 -392
- package/src/classes/list.ts +0 -310
- package/src/classes/ref.ts +0 -96
- package/src/classes/state.ts +0 -131
- package/src/classes/store.ts +0 -227
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -96
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -275
- package/test/computed.test.ts +0 -1126
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -381
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -47
- package/types/src/classes/composite.d.ts +0 -15
- package/types/src/classes/computed.d.ts +0 -114
- package/types/src/classes/list.d.ts +0 -41
- package/types/src/classes/ref.d.ts +0 -48
- package/types/src/classes/state.d.ts +0 -61
- package/types/src/classes/store.d.ts +0 -51
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -81
package/test/state.test.ts
CHANGED
|
@@ -1,343 +1,289 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { createEffect, createState, isMemo, isState } from '../index.ts'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('State', () => {
|
|
7
|
-
describe('
|
|
8
|
-
test('
|
|
9
|
-
const count =
|
|
10
|
-
expect(
|
|
11
|
-
expect(isComputed(count)).toBe(false)
|
|
12
|
-
})
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
describe('Boolean cause', () => {
|
|
16
|
-
test('should be boolean', () => {
|
|
17
|
-
const cause = new State(false)
|
|
18
|
-
expect(typeof cause.get()).toBe('boolean')
|
|
7
|
+
describe('createState', () => {
|
|
8
|
+
test('should return initial value from get()', () => {
|
|
9
|
+
const count = createState(0)
|
|
10
|
+
expect(count.get()).toBe(0)
|
|
19
11
|
})
|
|
20
12
|
|
|
21
|
-
test('should
|
|
22
|
-
|
|
23
|
-
expect(
|
|
13
|
+
test('should work with different value types', () => {
|
|
14
|
+
expect(createState(false).get()).toBe(false)
|
|
15
|
+
expect(createState('foo').get()).toBe('foo')
|
|
16
|
+
expect(createState([1, 2, 3]).get()).toEqual([1, 2, 3])
|
|
17
|
+
expect(createState({ a: 1 }).get()).toEqual({ a: 1 })
|
|
24
18
|
})
|
|
25
19
|
|
|
26
|
-
test('should
|
|
27
|
-
const
|
|
28
|
-
expect(
|
|
20
|
+
test('should have Symbol.toStringTag of "State"', () => {
|
|
21
|
+
const state = createState(0)
|
|
22
|
+
expect(state[Symbol.toStringTag]).toBe('State')
|
|
29
23
|
})
|
|
24
|
+
})
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expect(cause.get()).toBe(true)
|
|
26
|
+
describe('isState', () => {
|
|
27
|
+
test('should identify state signals', () => {
|
|
28
|
+
expect(isState(createState(0))).toBe(true)
|
|
35
29
|
})
|
|
36
30
|
|
|
37
|
-
test('should
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
31
|
+
test('should return false for non-state values', () => {
|
|
32
|
+
expect(isState(42)).toBe(false)
|
|
33
|
+
expect(isState(null)).toBe(false)
|
|
34
|
+
expect(isState({})).toBe(false)
|
|
35
|
+
expect(isMemo(createState(0))).toBe(false)
|
|
41
36
|
})
|
|
42
37
|
})
|
|
43
38
|
|
|
44
|
-
describe('
|
|
45
|
-
test('should
|
|
46
|
-
const
|
|
47
|
-
|
|
39
|
+
describe('set', () => {
|
|
40
|
+
test('should update value', () => {
|
|
41
|
+
const state = createState(0)
|
|
42
|
+
state.set(42)
|
|
43
|
+
expect(state.get()).toBe(42)
|
|
48
44
|
})
|
|
49
45
|
|
|
50
|
-
test('should
|
|
51
|
-
const
|
|
52
|
-
|
|
46
|
+
test('should replace value entirely for objects', () => {
|
|
47
|
+
const state = createState<Record<string, unknown>>({ a: 1 })
|
|
48
|
+
state.set({ b: 2 })
|
|
49
|
+
expect(state.get()).toEqual({ b: 2 })
|
|
53
50
|
})
|
|
54
51
|
|
|
55
|
-
test('should
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
expect(
|
|
52
|
+
test('should replace value entirely for arrays', () => {
|
|
53
|
+
const state = createState([1, 2, 3])
|
|
54
|
+
state.set([4, 5, 6])
|
|
55
|
+
expect(state.get()).toEqual([4, 5, 6])
|
|
59
56
|
})
|
|
60
57
|
|
|
61
|
-
test('should
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
test('should skip update when value is equal by reference', () => {
|
|
59
|
+
const obj = { a: 1 }
|
|
60
|
+
const state = createState(obj)
|
|
61
|
+
let effectCount = 0
|
|
62
|
+
createEffect(() => {
|
|
63
|
+
state.get()
|
|
64
|
+
effectCount++
|
|
65
|
+
})
|
|
66
|
+
expect(effectCount).toBe(1)
|
|
67
|
+
state.set(obj) // same reference
|
|
68
|
+
expect(effectCount).toBe(1)
|
|
65
69
|
})
|
|
66
70
|
})
|
|
67
71
|
|
|
68
|
-
describe('
|
|
69
|
-
test('should
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
+
describe('update', () => {
|
|
73
|
+
test('should update value via callback', () => {
|
|
74
|
+
const state = createState(0)
|
|
75
|
+
state.update(v => v + 1)
|
|
76
|
+
expect(state.get()).toBe(1)
|
|
72
77
|
})
|
|
73
78
|
|
|
74
|
-
test('should
|
|
75
|
-
const
|
|
76
|
-
|
|
79
|
+
test('should pass current value to callback', () => {
|
|
80
|
+
const state = createState('hello')
|
|
81
|
+
state.update(v => v.toUpperCase())
|
|
82
|
+
expect(state.get()).toBe('HELLO')
|
|
77
83
|
})
|
|
78
84
|
|
|
79
|
-
test('should
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
expect(
|
|
85
|
+
test('should work with arrays', () => {
|
|
86
|
+
const state = createState([1, 2, 3])
|
|
87
|
+
state.update(arr => [...arr, 4])
|
|
88
|
+
expect(state.get()).toEqual([1, 2, 3, 4])
|
|
83
89
|
})
|
|
84
90
|
|
|
85
|
-
test('should
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
expect(
|
|
91
|
+
test('should work with objects', () => {
|
|
92
|
+
const state = createState({ count: 0 })
|
|
93
|
+
state.update(obj => ({ ...obj, count: obj.count + 1 }))
|
|
94
|
+
expect(state.get()).toEqual({ count: 1 })
|
|
89
95
|
})
|
|
90
96
|
})
|
|
91
97
|
|
|
92
|
-
describe('
|
|
93
|
-
test('should
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
describe('options.equals', () => {
|
|
99
|
+
test('should use custom equality function to skip updates', () => {
|
|
100
|
+
const state = createState(
|
|
101
|
+
{ x: 1 },
|
|
102
|
+
{ equals: (a, b) => a.x === b.x },
|
|
103
|
+
)
|
|
104
|
+
let effectCount = 0
|
|
105
|
+
createEffect(() => {
|
|
106
|
+
state.get()
|
|
107
|
+
effectCount++
|
|
108
|
+
})
|
|
109
|
+
expect(effectCount).toBe(1)
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect(cause.get()).toEqual([1, 2, 3])
|
|
101
|
-
})
|
|
111
|
+
state.set({ x: 1 }) // structurally equal
|
|
112
|
+
expect(effectCount).toBe(1)
|
|
102
113
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
cause.set([4, 5, 6])
|
|
106
|
-
expect(cause.get()).toEqual([4, 5, 6])
|
|
114
|
+
state.set({ x: 2 }) // different
|
|
115
|
+
expect(effectCount).toBe(2)
|
|
107
116
|
})
|
|
108
117
|
|
|
109
|
-
test('should
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
test('should default to reference equality', () => {
|
|
119
|
+
const state = createState({ x: 1 })
|
|
120
|
+
let effectCount = 0
|
|
121
|
+
createEffect(() => {
|
|
122
|
+
state.get()
|
|
123
|
+
effectCount++
|
|
124
|
+
})
|
|
125
|
+
expect(effectCount).toBe(1)
|
|
126
|
+
|
|
127
|
+
state.set({ x: 1 }) // new reference, same shape
|
|
128
|
+
expect(effectCount).toBe(2)
|
|
114
129
|
})
|
|
130
|
+
})
|
|
115
131
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
describe('options.guard', () => {
|
|
133
|
+
test('should validate initial value against guard', () => {
|
|
134
|
+
expect(() => {
|
|
135
|
+
createState(0, {
|
|
136
|
+
guard: (v): v is number => typeof v === 'number' && v > 0,
|
|
137
|
+
})
|
|
138
|
+
}).toThrow('[State] Signal value 0 is invalid')
|
|
121
139
|
})
|
|
122
140
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// @ts-expect-error - Testing invalid input
|
|
127
|
-
new State(null)
|
|
128
|
-
}).toThrow('Nullish signal values are not allowed in State')
|
|
129
|
-
|
|
130
|
-
expect(() => {
|
|
131
|
-
// @ts-expect-error - Testing invalid input
|
|
132
|
-
new State(undefined)
|
|
133
|
-
}).toThrow('Nullish signal values are not allowed in State')
|
|
141
|
+
test('should validate set() values against guard', () => {
|
|
142
|
+
const state = createState(1, {
|
|
143
|
+
guard: (v): v is number => typeof v === 'number' && v > 0,
|
|
134
144
|
})
|
|
145
|
+
expect(() => state.set(0)).toThrow(
|
|
146
|
+
'[State] Signal value 0 is invalid',
|
|
147
|
+
)
|
|
148
|
+
expect(state.get()).toBe(1) // unchanged
|
|
149
|
+
})
|
|
135
150
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
expect(() => {
|
|
140
|
-
// @ts-expect-error - Testing invalid input
|
|
141
|
-
state.set(null)
|
|
142
|
-
}).toThrow('Nullish signal values are not allowed in State')
|
|
143
|
-
|
|
144
|
-
expect(() => {
|
|
145
|
-
// @ts-expect-error - Testing invalid input
|
|
146
|
-
state.set(undefined)
|
|
147
|
-
}).toThrow('Nullish signal values are not allowed in State')
|
|
151
|
+
test('should validate update() return values against guard', () => {
|
|
152
|
+
const state = createState(1, {
|
|
153
|
+
guard: (v): v is number => typeof v === 'number' && v > 0,
|
|
148
154
|
})
|
|
155
|
+
expect(() => state.update(() => 0)).toThrow(
|
|
156
|
+
'[State] Signal value 0 is invalid',
|
|
157
|
+
)
|
|
158
|
+
expect(state.get()).toBe(1) // unchanged
|
|
159
|
+
})
|
|
149
160
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
new State(null)
|
|
154
|
-
expect(true).toBe(false) // Should not reach here
|
|
155
|
-
} catch (error) {
|
|
156
|
-
expect(error).toBeInstanceOf(TypeError)
|
|
157
|
-
expect(error.name).toBe('NullishSignalValueError')
|
|
158
|
-
expect(error.message).toBe(
|
|
159
|
-
'Nullish signal values are not allowed in State',
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const state = new State(42)
|
|
164
|
-
try {
|
|
165
|
-
// @ts-expect-error - Testing invalid input
|
|
166
|
-
state.set(null)
|
|
167
|
-
expect(true).toBe(false) // Should not reach here
|
|
168
|
-
} catch (error) {
|
|
169
|
-
expect(error).toBeInstanceOf(TypeError)
|
|
170
|
-
expect(error.name).toBe('NullishSignalValueError')
|
|
171
|
-
expect(error.message).toBe(
|
|
172
|
-
'Nullish signal values are not allowed in State',
|
|
173
|
-
)
|
|
174
|
-
}
|
|
161
|
+
test('should allow values that pass the guard', () => {
|
|
162
|
+
const state = createState(1, {
|
|
163
|
+
guard: (v): v is number => typeof v === 'number' && v > 0,
|
|
175
164
|
})
|
|
165
|
+
state.set(5)
|
|
166
|
+
expect(state.get()).toBe(5)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
176
169
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
new State('')
|
|
185
|
-
}).not.toThrow()
|
|
186
|
-
|
|
187
|
-
expect(() => {
|
|
188
|
-
new State(false)
|
|
189
|
-
}).not.toThrow()
|
|
190
|
-
|
|
191
|
-
expect(() => {
|
|
192
|
-
new State({})
|
|
193
|
-
}).not.toThrow()
|
|
194
|
-
|
|
195
|
-
expect(() => {
|
|
196
|
-
new State([])
|
|
197
|
-
}).not.toThrow()
|
|
198
|
-
|
|
199
|
-
const state = new State(42)
|
|
200
|
-
expect(() => {
|
|
201
|
-
state.set(0)
|
|
202
|
-
}).not.toThrow()
|
|
203
|
-
|
|
204
|
-
expect(() => {
|
|
205
|
-
// @ts-expect-error - Testing valid input of invalid type
|
|
206
|
-
state.set('')
|
|
207
|
-
}).not.toThrow()
|
|
170
|
+
describe('Edge cases: NaN and special numbers', () => {
|
|
171
|
+
test('should propagate on every set(NaN) since NaN !== NaN', () => {
|
|
172
|
+
const state = createState(NaN)
|
|
173
|
+
let effectCount = 0
|
|
174
|
+
createEffect(() => {
|
|
175
|
+
state.get()
|
|
176
|
+
effectCount++
|
|
208
177
|
})
|
|
178
|
+
expect(effectCount).toBe(1)
|
|
209
179
|
|
|
210
|
-
|
|
211
|
-
|
|
180
|
+
state.set(NaN)
|
|
181
|
+
expect(effectCount).toBe(2) // NaN !== NaN, so it propagates
|
|
212
182
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}).toThrow('Invalid State update callback null')
|
|
217
|
-
|
|
218
|
-
expect(() => {
|
|
219
|
-
// @ts-expect-error - Testing invalid input
|
|
220
|
-
state.update(undefined)
|
|
221
|
-
}).toThrow('Invalid State update callback undefined')
|
|
222
|
-
|
|
223
|
-
expect(() => {
|
|
224
|
-
// @ts-expect-error - Testing invalid input
|
|
225
|
-
state.update('not a function')
|
|
226
|
-
}).toThrow('Invalid State update callback "not a function"')
|
|
227
|
-
|
|
228
|
-
expect(() => {
|
|
229
|
-
// @ts-expect-error - Testing invalid input
|
|
230
|
-
state.update(42)
|
|
231
|
-
}).toThrow('Invalid State update callback 42')
|
|
232
|
-
})
|
|
183
|
+
state.set(NaN)
|
|
184
|
+
expect(effectCount).toBe(3)
|
|
185
|
+
})
|
|
233
186
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
// @ts-expect-error - Testing invalid input
|
|
239
|
-
state.update(null)
|
|
240
|
-
expect(true).toBe(false) // Should not reach here
|
|
241
|
-
} catch (error) {
|
|
242
|
-
expect(error).toBeInstanceOf(TypeError)
|
|
243
|
-
expect(error.name).toBe('InvalidCallbackError')
|
|
244
|
-
expect(error.message).toBe(
|
|
245
|
-
'Invalid State update callback null',
|
|
246
|
-
)
|
|
247
|
-
}
|
|
187
|
+
test('should reject NaN with a Number.isFinite guard', () => {
|
|
188
|
+
const state = createState(1, {
|
|
189
|
+
guard: (v): v is number => Number.isFinite(v),
|
|
248
190
|
})
|
|
191
|
+
expect(() => state.set(NaN)).toThrow(
|
|
192
|
+
'[State] Signal value NaN is invalid',
|
|
193
|
+
)
|
|
194
|
+
expect(state.get()).toBe(1)
|
|
195
|
+
})
|
|
249
196
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
expect(() => {
|
|
254
|
-
state.update(() => {
|
|
255
|
-
throw new Error('Updater error')
|
|
256
|
-
})
|
|
257
|
-
}).toThrow('Updater error')
|
|
258
|
-
|
|
259
|
-
// State should remain unchanged after error
|
|
260
|
-
expect(state.get()).toBe(42)
|
|
197
|
+
test('should reject Infinity with a Number.isFinite guard', () => {
|
|
198
|
+
const state = createState(1, {
|
|
199
|
+
guard: (v): v is number => Number.isFinite(v),
|
|
261
200
|
})
|
|
201
|
+
expect(() => state.set(Infinity)).toThrow(
|
|
202
|
+
'[State] Signal value Infinity is invalid',
|
|
203
|
+
)
|
|
204
|
+
expect(() => state.set(-Infinity)).toThrow(
|
|
205
|
+
'[State] Signal value -Infinity is invalid',
|
|
206
|
+
)
|
|
207
|
+
expect(state.get()).toBe(1)
|
|
208
|
+
})
|
|
262
209
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}).toThrow('Nullish signal values are not allowed in State')
|
|
270
|
-
|
|
271
|
-
expect(() => {
|
|
272
|
-
// @ts-expect-error - Testing invalid return value
|
|
273
|
-
state.update(() => undefined)
|
|
274
|
-
}).toThrow('Nullish signal values are not allowed in State')
|
|
275
|
-
|
|
276
|
-
// State should remain unchanged after error
|
|
277
|
-
expect(state.get()).toBe(42)
|
|
210
|
+
test('should treat +0 and -0 as equal by default (===)', () => {
|
|
211
|
+
const state = createState(0)
|
|
212
|
+
let effectCount = 0
|
|
213
|
+
createEffect(() => {
|
|
214
|
+
state.get()
|
|
215
|
+
effectCount++
|
|
278
216
|
})
|
|
217
|
+
expect(effectCount).toBe(1)
|
|
279
218
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
expect(() => {
|
|
283
|
-
numberState.update(x => x + 5)
|
|
284
|
-
}).not.toThrow()
|
|
285
|
-
expect(numberState.get()).toBe(15)
|
|
286
|
-
|
|
287
|
-
const stringState = new State('hello')
|
|
288
|
-
expect(() => {
|
|
289
|
-
stringState.update(x => x.toUpperCase())
|
|
290
|
-
}).not.toThrow()
|
|
291
|
-
expect(stringState.get()).toBe('HELLO')
|
|
292
|
-
|
|
293
|
-
const arrayState = new State([1, 2, 3])
|
|
294
|
-
expect(() => {
|
|
295
|
-
arrayState.update(arr => [...arr, 4])
|
|
296
|
-
}).not.toThrow()
|
|
297
|
-
expect(arrayState.get()).toEqual([1, 2, 3, 4])
|
|
298
|
-
|
|
299
|
-
const objectState = new State({ count: 0 })
|
|
300
|
-
expect(() => {
|
|
301
|
-
objectState.update(obj => ({
|
|
302
|
-
...obj,
|
|
303
|
-
count: obj.count + 1,
|
|
304
|
-
}))
|
|
305
|
-
}).not.toThrow()
|
|
306
|
-
expect(objectState.get()).toEqual({ count: 1 })
|
|
307
|
-
})
|
|
219
|
+
state.set(-0) // +0 === -0 is true
|
|
220
|
+
expect(effectCount).toBe(1) // no propagation
|
|
308
221
|
})
|
|
309
222
|
})
|
|
310
223
|
|
|
311
|
-
describe('
|
|
312
|
-
test('should
|
|
313
|
-
|
|
314
|
-
|
|
224
|
+
describe('Input Validation', () => {
|
|
225
|
+
test('should throw NullishSignalValueError for null or undefined initial value', () => {
|
|
226
|
+
expect(() => {
|
|
227
|
+
// @ts-expect-error - Testing invalid input
|
|
228
|
+
createState(null)
|
|
229
|
+
}).toThrow('[State] Signal value cannot be null or undefined')
|
|
230
|
+
|
|
231
|
+
expect(() => {
|
|
232
|
+
// @ts-expect-error - Testing invalid input
|
|
233
|
+
createState(undefined)
|
|
234
|
+
}).toThrow('[State] Signal value cannot be null or undefined')
|
|
315
235
|
})
|
|
316
236
|
|
|
317
|
-
test('should
|
|
318
|
-
const
|
|
319
|
-
expect(
|
|
237
|
+
test('should throw NullishSignalValueError for null or undefined in set()', () => {
|
|
238
|
+
const state = createState(42)
|
|
239
|
+
expect(() => {
|
|
240
|
+
// @ts-expect-error - Testing invalid input
|
|
241
|
+
state.set(null)
|
|
242
|
+
}).toThrow('[State] Signal value cannot be null or undefined')
|
|
243
|
+
|
|
244
|
+
expect(() => {
|
|
245
|
+
// @ts-expect-error - Testing invalid input
|
|
246
|
+
state.set(undefined)
|
|
247
|
+
}).toThrow('[State] Signal value cannot be null or undefined')
|
|
320
248
|
})
|
|
321
249
|
|
|
322
|
-
test('should
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
250
|
+
test('should throw NullishSignalValueError for nullish return from update()', () => {
|
|
251
|
+
const state = createState(42)
|
|
252
|
+
expect(() => {
|
|
253
|
+
// @ts-expect-error - Testing invalid return value
|
|
254
|
+
state.update(() => null)
|
|
255
|
+
}).toThrow('[State] Signal value cannot be null or undefined')
|
|
256
|
+
|
|
257
|
+
expect(() => {
|
|
258
|
+
// @ts-expect-error - Testing invalid return value
|
|
259
|
+
state.update(() => undefined)
|
|
260
|
+
}).toThrow('[State] Signal value cannot be null or undefined')
|
|
261
|
+
|
|
262
|
+
expect(state.get()).toBe(42) // unchanged
|
|
326
263
|
})
|
|
327
264
|
|
|
328
|
-
test('should
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
265
|
+
test('should throw InvalidCallbackError for non-function in update()', () => {
|
|
266
|
+
const state = createState(42)
|
|
267
|
+
expect(() => {
|
|
268
|
+
// @ts-expect-error - Testing invalid input
|
|
269
|
+
state.update(null)
|
|
270
|
+
}).toThrow('[State] Callback null is invalid')
|
|
271
|
+
|
|
272
|
+
expect(() => {
|
|
273
|
+
// @ts-expect-error - Testing invalid input
|
|
274
|
+
state.update('not a function')
|
|
275
|
+
}).toThrow('[State] Callback "not a function" is invalid')
|
|
334
276
|
})
|
|
335
277
|
|
|
336
|
-
test('should
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
278
|
+
test('should propagate errors thrown by update callback', () => {
|
|
279
|
+
const state = createState(42)
|
|
280
|
+
expect(() => {
|
|
281
|
+
state.update(() => {
|
|
282
|
+
throw new Error('Updater error')
|
|
283
|
+
})
|
|
284
|
+
}).toThrow('Updater error')
|
|
285
|
+
|
|
286
|
+
expect(state.get()).toBe(42) // unchanged
|
|
341
287
|
})
|
|
342
288
|
})
|
|
343
289
|
})
|