@zeix/cause-effect 0.17.0 → 0.17.2
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 +26 -5
- package/.cursorrules +8 -3
- package/.github/copilot-instructions.md +13 -4
- package/CLAUDE.md +191 -262
- package/README.md +268 -420
- package/archive/collection.ts +23 -25
- package/archive/computed.ts +5 -4
- package/archive/list.ts +21 -28
- package/archive/memo.ts +4 -2
- package/archive/state.ts +2 -1
- package/archive/store.ts +21 -32
- package/archive/task.ts +6 -9
- package/index.dev.js +411 -220
- package/index.js +1 -1
- package/index.ts +25 -8
- package/package.json +1 -1
- package/src/classes/collection.ts +103 -77
- package/src/classes/composite.ts +28 -33
- package/src/classes/computed.ts +90 -31
- package/src/classes/list.ts +39 -33
- package/src/classes/ref.ts +96 -0
- package/src/classes/state.ts +41 -8
- package/src/classes/store.ts +47 -30
- package/src/diff.ts +2 -1
- package/src/effect.ts +19 -9
- package/src/errors.ts +31 -1
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +0 -1
- package/src/system.ts +159 -43
- package/src/util.ts +0 -10
- package/test/collection.test.ts +383 -67
- package/test/computed.test.ts +268 -11
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +249 -21
- package/test/ref.test.ts +381 -0
- package/test/state.test.ts +13 -13
- package/test/store.test.ts +473 -28
- package/types/index.d.ts +6 -5
- package/types/src/classes/collection.d.ts +27 -12
- package/types/src/classes/composite.d.ts +4 -4
- package/types/src/classes/computed.d.ts +17 -0
- package/types/src/classes/list.d.ts +6 -6
- package/types/src/classes/ref.d.ts +48 -0
- package/types/src/classes/state.d.ts +9 -0
- package/types/src/classes/store.d.ts +4 -4
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +9 -1
- package/types/src/system.d.ts +40 -24
- package/types/src/util.d.ts +1 -3
package/test/ref.test.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { expect, mock, test } from 'bun:test'
|
|
2
|
+
import { isRef, Ref } from '../src/classes/ref'
|
|
3
|
+
import { createEffect } from '../src/effect'
|
|
4
|
+
import { HOOK_WATCH } from '../src/system'
|
|
5
|
+
|
|
6
|
+
test('Ref - basic functionality', () => {
|
|
7
|
+
const obj = { name: 'test', value: 42 }
|
|
8
|
+
const ref = new Ref(obj)
|
|
9
|
+
|
|
10
|
+
expect(ref.get()).toBe(obj)
|
|
11
|
+
expect(ref[Symbol.toStringTag]).toBe('Ref')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('Ref - isRef type guard', () => {
|
|
15
|
+
const ref = new Ref({ test: true })
|
|
16
|
+
const notRef = { test: true }
|
|
17
|
+
|
|
18
|
+
expect(isRef(ref)).toBe(true)
|
|
19
|
+
expect(isRef(notRef)).toBe(false)
|
|
20
|
+
expect(isRef(null)).toBe(false)
|
|
21
|
+
expect(isRef(undefined)).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('Ref - validation with guard function', () => {
|
|
25
|
+
const isConfig = (
|
|
26
|
+
value: unknown,
|
|
27
|
+
): value is { host: string; port: number } =>
|
|
28
|
+
typeof value === 'object' &&
|
|
29
|
+
value !== null &&
|
|
30
|
+
'host' in value &&
|
|
31
|
+
'port' in value &&
|
|
32
|
+
typeof value.host === 'string' &&
|
|
33
|
+
typeof value.port === 'number'
|
|
34
|
+
|
|
35
|
+
const validConfig = { host: 'localhost', port: 3000 }
|
|
36
|
+
const invalidConfig = { host: 'localhost' } // missing port
|
|
37
|
+
|
|
38
|
+
expect(() => new Ref(validConfig, isConfig)).not.toThrow()
|
|
39
|
+
expect(() => new Ref(invalidConfig, isConfig)).toThrow()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('Ref - reactive subscriptions', () => {
|
|
43
|
+
const server = { status: 'offline', connections: 0 }
|
|
44
|
+
const ref = new Ref(server)
|
|
45
|
+
|
|
46
|
+
let effectRunCount = 0
|
|
47
|
+
let lastStatus: string = ''
|
|
48
|
+
|
|
49
|
+
createEffect(() => {
|
|
50
|
+
const current = ref.get()
|
|
51
|
+
lastStatus = current.status
|
|
52
|
+
effectRunCount++
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
expect(effectRunCount).toBe(1)
|
|
56
|
+
expect(lastStatus).toBe('offline')
|
|
57
|
+
|
|
58
|
+
// Simulate external change without going through reactive system
|
|
59
|
+
server.status = 'online'
|
|
60
|
+
server.connections = 5
|
|
61
|
+
|
|
62
|
+
// Effect shouldn't re-run yet (reference hasn't changed)
|
|
63
|
+
expect(effectRunCount).toBe(1)
|
|
64
|
+
|
|
65
|
+
// Notify that the external object has changed
|
|
66
|
+
ref.notify()
|
|
67
|
+
|
|
68
|
+
expect(effectRunCount).toBe(2)
|
|
69
|
+
expect(lastStatus).toBe('online')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('Ref - notify triggers watchers even with same reference', () => {
|
|
73
|
+
const fileObj = { path: '/test.txt', size: 100, modified: Date.now() }
|
|
74
|
+
const ref = new Ref(fileObj)
|
|
75
|
+
|
|
76
|
+
const mockCallback = mock(() => {})
|
|
77
|
+
|
|
78
|
+
createEffect(() => {
|
|
79
|
+
ref.get()
|
|
80
|
+
mockCallback()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
84
|
+
|
|
85
|
+
// Simulate file modification (same object reference, different content)
|
|
86
|
+
fileObj.size = 200
|
|
87
|
+
fileObj.modified = Date.now()
|
|
88
|
+
|
|
89
|
+
// Notify about external change
|
|
90
|
+
ref.notify()
|
|
91
|
+
|
|
92
|
+
expect(mockCallback).toHaveBeenCalledTimes(2)
|
|
93
|
+
|
|
94
|
+
// Multiple notifies should trigger multiple times
|
|
95
|
+
ref.notify()
|
|
96
|
+
expect(mockCallback).toHaveBeenCalledTimes(3)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('Ref - multiple effects with same ref', () => {
|
|
100
|
+
const database = { connected: false, queries: 0 }
|
|
101
|
+
const ref = new Ref(database)
|
|
102
|
+
|
|
103
|
+
const effect1Mock = mock(() => {})
|
|
104
|
+
const effect2Mock = mock((_connected: boolean) => {})
|
|
105
|
+
|
|
106
|
+
createEffect(() => {
|
|
107
|
+
ref.get()
|
|
108
|
+
effect1Mock()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
createEffect(() => {
|
|
112
|
+
const db = ref.get()
|
|
113
|
+
effect2Mock(db.connected)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(effect1Mock).toHaveBeenCalledTimes(1)
|
|
117
|
+
expect(effect2Mock).toHaveBeenCalledTimes(1)
|
|
118
|
+
expect(effect2Mock).toHaveBeenCalledWith(false)
|
|
119
|
+
|
|
120
|
+
// Simulate database connection change
|
|
121
|
+
database.connected = true
|
|
122
|
+
database.queries = 10
|
|
123
|
+
ref.notify()
|
|
124
|
+
|
|
125
|
+
expect(effect1Mock).toHaveBeenCalledTimes(2)
|
|
126
|
+
expect(effect2Mock).toHaveBeenCalledTimes(2)
|
|
127
|
+
expect(effect2Mock).toHaveBeenLastCalledWith(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('Ref - with Bun.file() scenario', () => {
|
|
131
|
+
// Mock a file-like object that could change externally
|
|
132
|
+
const fileRef = {
|
|
133
|
+
name: 'config.json',
|
|
134
|
+
size: 1024,
|
|
135
|
+
lastModified: Date.now(),
|
|
136
|
+
// Simulate file methods
|
|
137
|
+
exists: () => true,
|
|
138
|
+
text: () => Promise.resolve('{"version": "1.0"}'),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ref = new Ref(fileRef)
|
|
142
|
+
|
|
143
|
+
let sizeChanges = 0
|
|
144
|
+
createEffect(() => {
|
|
145
|
+
const file = ref.get()
|
|
146
|
+
if (file.size > 1000) sizeChanges++
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
expect(sizeChanges).toBe(1) // Initial run
|
|
150
|
+
|
|
151
|
+
// Simulate file growing (external change)
|
|
152
|
+
fileRef.size = 2048
|
|
153
|
+
fileRef.lastModified = Date.now()
|
|
154
|
+
ref.notify()
|
|
155
|
+
|
|
156
|
+
expect(sizeChanges).toBe(2) // Effect re-ran and condition still met
|
|
157
|
+
|
|
158
|
+
// Simulate file shrinking
|
|
159
|
+
fileRef.size = 500
|
|
160
|
+
ref.notify()
|
|
161
|
+
|
|
162
|
+
expect(sizeChanges).toBe(2) // Effect re-ran but condition no longer met
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('Ref - validation errors', () => {
|
|
166
|
+
// @ts-expect-error deliberatly provoked error
|
|
167
|
+
expect(() => new Ref(null)).toThrow()
|
|
168
|
+
// @ts-expect-error deliberatly provoked error
|
|
169
|
+
expect(() => new Ref(undefined)).toThrow()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('Ref - server config object scenario', () => {
|
|
173
|
+
const config = {
|
|
174
|
+
host: 'localhost',
|
|
175
|
+
port: 3000,
|
|
176
|
+
ssl: false,
|
|
177
|
+
maxConnections: 100,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const configRef = new Ref(config)
|
|
181
|
+
const connectionAttempts: string[] = []
|
|
182
|
+
|
|
183
|
+
createEffect(() => {
|
|
184
|
+
const cfg = configRef.get()
|
|
185
|
+
const protocol = cfg.ssl ? 'https' : 'http'
|
|
186
|
+
connectionAttempts.push(`${protocol}://${cfg.host}:${cfg.port}`)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
expect(connectionAttempts).toEqual(['http://localhost:3000'])
|
|
190
|
+
|
|
191
|
+
// Simulate config reload from file/environment
|
|
192
|
+
config.ssl = true
|
|
193
|
+
config.port = 8443
|
|
194
|
+
configRef.notify()
|
|
195
|
+
|
|
196
|
+
expect(connectionAttempts).toEqual([
|
|
197
|
+
'http://localhost:3000',
|
|
198
|
+
'https://localhost:8443',
|
|
199
|
+
])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('Ref - handles complex nested objects', () => {
|
|
203
|
+
const apiResponse = {
|
|
204
|
+
status: 200,
|
|
205
|
+
data: {
|
|
206
|
+
users: [{ id: 1, name: 'Alice' }],
|
|
207
|
+
pagination: { page: 1, total: 1 },
|
|
208
|
+
},
|
|
209
|
+
headers: { 'content-type': 'application/json' },
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ref = new Ref(apiResponse)
|
|
213
|
+
let userCount = 0
|
|
214
|
+
|
|
215
|
+
createEffect(() => {
|
|
216
|
+
const response = ref.get()
|
|
217
|
+
userCount = response.data.users.length
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
expect(userCount).toBe(1)
|
|
221
|
+
|
|
222
|
+
// Simulate API response update
|
|
223
|
+
apiResponse.data.users.push({ id: 2, name: 'Bob' })
|
|
224
|
+
apiResponse.data.pagination.total = 2
|
|
225
|
+
ref.notify()
|
|
226
|
+
|
|
227
|
+
expect(userCount).toBe(2)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('Ref - HOOK_WATCH lazy resource management', async () => {
|
|
231
|
+
// 1. Create Ref with current Date
|
|
232
|
+
const currentDate = new Date()
|
|
233
|
+
const ref = new Ref(currentDate)
|
|
234
|
+
|
|
235
|
+
let counter = 0
|
|
236
|
+
let intervalId: Timer | undefined
|
|
237
|
+
|
|
238
|
+
// 2. Add HOOK_WATCH callback that starts setInterval and returns cleanup
|
|
239
|
+
const cleanupHookCallback = ref.on(HOOK_WATCH, () => {
|
|
240
|
+
intervalId = setInterval(() => {
|
|
241
|
+
counter++
|
|
242
|
+
}, 10) // Use short interval for faster test
|
|
243
|
+
|
|
244
|
+
// Return cleanup function to clear interval
|
|
245
|
+
return () => {
|
|
246
|
+
if (intervalId) {
|
|
247
|
+
clearInterval(intervalId)
|
|
248
|
+
intervalId = undefined
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// 3. Counter should not be running yet
|
|
254
|
+
expect(counter).toBe(0)
|
|
255
|
+
|
|
256
|
+
// Wait a bit to ensure counter doesn't increment
|
|
257
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
258
|
+
expect(counter).toBe(0)
|
|
259
|
+
expect(intervalId).toBeUndefined()
|
|
260
|
+
|
|
261
|
+
// 4. Effect subscribes by .get()ting the signal value
|
|
262
|
+
const effectCleanup = createEffect(() => {
|
|
263
|
+
ref.get()
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// 5. Counter should now be running
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
268
|
+
expect(counter).toBeGreaterThan(0)
|
|
269
|
+
expect(intervalId).toBeDefined()
|
|
270
|
+
|
|
271
|
+
// 6. Call effect cleanup, which should stop internal watcher and unsubscribe
|
|
272
|
+
effectCleanup()
|
|
273
|
+
const counterAfterStop = counter
|
|
274
|
+
|
|
275
|
+
// 7. Ref signal should call #unwatch() and counter should stop incrementing
|
|
276
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
277
|
+
expect(counter).toBe(counterAfterStop) // Counter should not have incremented
|
|
278
|
+
expect(intervalId).toBeUndefined() // Interval should be cleared
|
|
279
|
+
|
|
280
|
+
// Clean up hook callback registration
|
|
281
|
+
cleanupHookCallback()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('Ref - HOOK_WATCH exception handling', async () => {
|
|
285
|
+
const ref = new Ref({ test: 'value' })
|
|
286
|
+
|
|
287
|
+
// Mock console.error to capture error logs
|
|
288
|
+
const originalError = console.error
|
|
289
|
+
const errorSpy = mock(() => {})
|
|
290
|
+
console.error = errorSpy
|
|
291
|
+
|
|
292
|
+
let successfulCallbackCalled = false
|
|
293
|
+
let throwingCallbackCalled = false
|
|
294
|
+
|
|
295
|
+
// Add callback that throws an exception
|
|
296
|
+
const cleanup1 = ref.on(HOOK_WATCH, () => {
|
|
297
|
+
throwingCallbackCalled = true
|
|
298
|
+
throw new Error('Test error in HOOK_WATCH callback')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
// Add callback that works normally
|
|
302
|
+
const cleanup2 = ref.on(HOOK_WATCH, () => {
|
|
303
|
+
successfulCallbackCalled = true
|
|
304
|
+
return () => {
|
|
305
|
+
// cleanup function
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// Subscribe to trigger HOOK_WATCH callbacks
|
|
310
|
+
const effectCleanup = createEffect(() => {
|
|
311
|
+
ref.get()
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Both callbacks should have been called despite the exception
|
|
315
|
+
expect(throwingCallbackCalled).toBe(true)
|
|
316
|
+
expect(successfulCallbackCalled).toBe(true)
|
|
317
|
+
|
|
318
|
+
// Error should have been logged
|
|
319
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
320
|
+
'Error in effect callback:',
|
|
321
|
+
expect.any(Error),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
// Cleanup
|
|
325
|
+
effectCleanup()
|
|
326
|
+
cleanup1()
|
|
327
|
+
cleanup2()
|
|
328
|
+
console.error = originalError
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('Ref - cleanup function exception handling', async () => {
|
|
332
|
+
const ref = new Ref({ test: 'value' })
|
|
333
|
+
|
|
334
|
+
// Mock console.error to capture error logs
|
|
335
|
+
const originalError = console.error
|
|
336
|
+
const errorSpy = mock(() => {})
|
|
337
|
+
console.error = errorSpy
|
|
338
|
+
|
|
339
|
+
let cleanup1Called = false
|
|
340
|
+
let cleanup2Called = false
|
|
341
|
+
|
|
342
|
+
// Add callbacks with cleanup functions, one throws
|
|
343
|
+
const hookCleanup1 = ref.on(HOOK_WATCH, () => {
|
|
344
|
+
return () => {
|
|
345
|
+
cleanup1Called = true
|
|
346
|
+
throw new Error('Test error in cleanup function')
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const hookCleanup2 = ref.on(HOOK_WATCH, () => {
|
|
351
|
+
return () => {
|
|
352
|
+
cleanup2Called = true
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Subscribe and then unsubscribe to trigger cleanup
|
|
357
|
+
const effectCleanup = createEffect(() => {
|
|
358
|
+
ref.get()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Unsubscribe to trigger cleanup functions
|
|
362
|
+
effectCleanup()
|
|
363
|
+
|
|
364
|
+
// Wait a bit for cleanup to complete
|
|
365
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
366
|
+
|
|
367
|
+
// Both cleanup functions should have been called despite the exception
|
|
368
|
+
expect(cleanup1Called).toBe(true)
|
|
369
|
+
expect(cleanup2Called).toBe(true)
|
|
370
|
+
|
|
371
|
+
// Error should have been logged
|
|
372
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
373
|
+
'Error in effect cleanup:',
|
|
374
|
+
expect.any(Error),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
// Cleanup
|
|
378
|
+
hookCleanup1()
|
|
379
|
+
hookCleanup2()
|
|
380
|
+
console.error = originalError
|
|
381
|
+
})
|
package/test/state.test.ts
CHANGED
|
@@ -125,12 +125,12 @@ describe('State', () => {
|
|
|
125
125
|
expect(() => {
|
|
126
126
|
// @ts-expect-error - Testing invalid input
|
|
127
127
|
new State(null)
|
|
128
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
128
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
129
129
|
|
|
130
130
|
expect(() => {
|
|
131
131
|
// @ts-expect-error - Testing invalid input
|
|
132
132
|
new State(undefined)
|
|
133
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
133
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
134
134
|
})
|
|
135
135
|
|
|
136
136
|
test('should throw NullishSignalValueError when newValue is nullish in set()', () => {
|
|
@@ -139,12 +139,12 @@ describe('State', () => {
|
|
|
139
139
|
expect(() => {
|
|
140
140
|
// @ts-expect-error - Testing invalid input
|
|
141
141
|
state.set(null)
|
|
142
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
142
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
143
143
|
|
|
144
144
|
expect(() => {
|
|
145
145
|
// @ts-expect-error - Testing invalid input
|
|
146
146
|
state.set(undefined)
|
|
147
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
147
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
148
148
|
})
|
|
149
149
|
|
|
150
150
|
test('should throw specific error types for nullish values', () => {
|
|
@@ -156,7 +156,7 @@ describe('State', () => {
|
|
|
156
156
|
expect(error).toBeInstanceOf(TypeError)
|
|
157
157
|
expect(error.name).toBe('NullishSignalValueError')
|
|
158
158
|
expect(error.message).toBe(
|
|
159
|
-
'Nullish signal values are not allowed in
|
|
159
|
+
'Nullish signal values are not allowed in State',
|
|
160
160
|
)
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -169,7 +169,7 @@ describe('State', () => {
|
|
|
169
169
|
expect(error).toBeInstanceOf(TypeError)
|
|
170
170
|
expect(error.name).toBe('NullishSignalValueError')
|
|
171
171
|
expect(error.message).toBe(
|
|
172
|
-
'Nullish signal values are not allowed in
|
|
172
|
+
'Nullish signal values are not allowed in State',
|
|
173
173
|
)
|
|
174
174
|
}
|
|
175
175
|
})
|
|
@@ -213,22 +213,22 @@ describe('State', () => {
|
|
|
213
213
|
expect(() => {
|
|
214
214
|
// @ts-expect-error - Testing invalid input
|
|
215
215
|
state.update(null)
|
|
216
|
-
}).toThrow('Invalid
|
|
216
|
+
}).toThrow('Invalid State update callback null')
|
|
217
217
|
|
|
218
218
|
expect(() => {
|
|
219
219
|
// @ts-expect-error - Testing invalid input
|
|
220
220
|
state.update(undefined)
|
|
221
|
-
}).toThrow('Invalid
|
|
221
|
+
}).toThrow('Invalid State update callback undefined')
|
|
222
222
|
|
|
223
223
|
expect(() => {
|
|
224
224
|
// @ts-expect-error - Testing invalid input
|
|
225
225
|
state.update('not a function')
|
|
226
|
-
}).toThrow('Invalid
|
|
226
|
+
}).toThrow('Invalid State update callback "not a function"')
|
|
227
227
|
|
|
228
228
|
expect(() => {
|
|
229
229
|
// @ts-expect-error - Testing invalid input
|
|
230
230
|
state.update(42)
|
|
231
|
-
}).toThrow('Invalid
|
|
231
|
+
}).toThrow('Invalid State update callback 42')
|
|
232
232
|
})
|
|
233
233
|
|
|
234
234
|
test('should throw specific error type for non-function updater', () => {
|
|
@@ -242,7 +242,7 @@ describe('State', () => {
|
|
|
242
242
|
expect(error).toBeInstanceOf(TypeError)
|
|
243
243
|
expect(error.name).toBe('InvalidCallbackError')
|
|
244
244
|
expect(error.message).toBe(
|
|
245
|
-
'Invalid
|
|
245
|
+
'Invalid State update callback null',
|
|
246
246
|
)
|
|
247
247
|
}
|
|
248
248
|
})
|
|
@@ -266,12 +266,12 @@ describe('State', () => {
|
|
|
266
266
|
expect(() => {
|
|
267
267
|
// @ts-expect-error - Testing invalid return value
|
|
268
268
|
state.update(() => null)
|
|
269
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
269
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
270
270
|
|
|
271
271
|
expect(() => {
|
|
272
272
|
// @ts-expect-error - Testing invalid return value
|
|
273
273
|
state.update(() => undefined)
|
|
274
|
-
}).toThrow('Nullish signal values are not allowed in
|
|
274
|
+
}).toThrow('Nullish signal values are not allowed in State')
|
|
275
275
|
|
|
276
276
|
// State should remain unchanged after error
|
|
277
277
|
expect(state.get()).toBe(42)
|