@zeix/cause-effect 0.14.2 → 0.15.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.
@@ -0,0 +1,746 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ computed,
4
+ effect,
5
+ isStore,
6
+ type StoreAddEvent,
7
+ type StoreChangeEvent,
8
+ type StoreRemoveEvent,
9
+ state,
10
+ store,
11
+ toSignal,
12
+ UNSET,
13
+ } from '..'
14
+
15
+ describe('store', () => {
16
+ describe('creation and basic operations', () => {
17
+ test('creates a store with initial values', () => {
18
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
19
+
20
+ expect(user.name.get()).toBe('Hannah')
21
+ expect(user.email.get()).toBe('hannah@example.com')
22
+ })
23
+
24
+ test('has Symbol.toStringTag of Store', () => {
25
+ const s = store({ a: 1 })
26
+ expect(s[Symbol.toStringTag]).toBe('Store')
27
+ })
28
+
29
+ test('isStore identifies store instances correctly', () => {
30
+ const s = store({ a: 1 })
31
+ const st = state(1)
32
+ const c = computed(() => 1)
33
+
34
+ expect(isStore(s)).toBe(true)
35
+ expect(isStore(st)).toBe(false)
36
+ expect(isStore(c)).toBe(false)
37
+ expect(isStore({})).toBe(false)
38
+ expect(isStore(null)).toBe(false)
39
+ })
40
+
41
+ test('get() returns the complete store value', () => {
42
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
43
+
44
+ expect(user.get()).toEqual({
45
+ name: 'Hannah',
46
+ email: 'hannah@example.com',
47
+ })
48
+
49
+ /**
50
+ * store() only accepts object map types for arrays
51
+ */
52
+ const participants = store<{
53
+ [x: number]: { name: string; tags: string[] }
54
+ }>([
55
+ { name: 'Alice', tags: ['friends', 'mates'] },
56
+ { name: 'Bob', tags: ['friends'] },
57
+ ])
58
+ expect(participants.get()).toEqual([
59
+ { name: 'Alice', tags: ['friends', 'mates'] },
60
+ { name: 'Bob', tags: ['friends'] },
61
+ ])
62
+
63
+ /**
64
+ * toSignal() converts arrays to object map types when creating stores
65
+ */
66
+ const participants2 = toSignal<{ name: string; tags: string[] }[]>([
67
+ { name: 'Alice', tags: ['friends', 'mates'] },
68
+ { name: 'Bob', tags: ['friends'] },
69
+ ])
70
+ expect(participants2.get()).toEqual([
71
+ { name: 'Alice', tags: ['friends', 'mates'] },
72
+ { name: 'Bob', tags: ['friends'] },
73
+ ])
74
+ })
75
+ })
76
+
77
+ describe('proxy data access and modification', () => {
78
+ test('properties can be accessed and modified via signals', () => {
79
+ const user = store({ name: 'Hannah', age: 25 })
80
+
81
+ // Get signals from store proxy
82
+ expect(user.name.get()).toBe('Hannah')
83
+ expect(user.age.get()).toBe(25)
84
+
85
+ // Set values via signals
86
+ user.name.set('Alice')
87
+ user.age.set(30)
88
+
89
+ expect(user.name.get()).toBe('Alice')
90
+ expect(user.age.get()).toBe(30)
91
+ })
92
+
93
+ test('returns undefined for non-existent properties', () => {
94
+ const user = store({ name: 'Hannah' })
95
+
96
+ // @ts-expect-error accessing non-existent property
97
+ expect(user.nonExistent).toBeUndefined()
98
+ })
99
+
100
+ test('supports numeric key access', () => {
101
+ const items = store({ '0': 'first', '1': 'second' })
102
+
103
+ expect(items[0].get()).toBe('first')
104
+ expect(items['0'].get()).toBe('first')
105
+ expect(items[1].get()).toBe('second')
106
+ expect(items['1'].get()).toBe('second')
107
+ })
108
+
109
+ test('can add new properties via add method', () => {
110
+ const user = store<{ name: string; email?: string }>({
111
+ name: 'Hannah',
112
+ })
113
+
114
+ user.add('email', 'hannah@example.com')
115
+
116
+ expect(user.email?.get()).toBe('hannah@example.com')
117
+ expect(user.get()).toEqual({
118
+ name: 'Hannah',
119
+ email: 'hannah@example.com',
120
+ })
121
+ })
122
+
123
+ test('can remove existing properties via remove method', () => {
124
+ const user = store<{ name: string; email?: string }>({
125
+ name: 'Hannah',
126
+ email: 'hannah@example.com',
127
+ })
128
+
129
+ expect(user.email?.get()).toBe('hannah@example.com')
130
+
131
+ user.remove('email')
132
+
133
+ expect(user.email).toBeUndefined()
134
+ expect(user.get()).toEqual({
135
+ name: 'Hannah',
136
+ })
137
+ })
138
+ })
139
+
140
+ describe('nested stores', () => {
141
+ test('creates nested stores for object properties', () => {
142
+ const user = store({
143
+ name: 'Hannah',
144
+ preferences: {
145
+ theme: 'dark',
146
+ notifications: true,
147
+ },
148
+ })
149
+
150
+ expect(isStore(user.preferences)).toBe(true)
151
+ expect(user.preferences.theme?.get()).toBe('dark')
152
+ expect(user.preferences.notifications?.get()).toBe(true)
153
+ })
154
+
155
+ test('nested properties are reactive', () => {
156
+ const user = store({
157
+ preferences: {
158
+ theme: 'dark',
159
+ },
160
+ })
161
+
162
+ user.preferences.theme.set('light')
163
+ expect(user.preferences.theme.get()).toBe('light')
164
+ expect(user.get().preferences.theme).toBe('light')
165
+ })
166
+
167
+ test('deeply nested stores work correctly', () => {
168
+ const config = store({
169
+ ui: {
170
+ theme: {
171
+ colors: {
172
+ primary: 'blue',
173
+ },
174
+ },
175
+ },
176
+ })
177
+
178
+ expect(config.ui.theme.colors.primary.get()).toBe('blue')
179
+ config.ui.theme.colors.primary.set('red')
180
+ expect(config.ui.theme.colors.primary.get()).toBe('red')
181
+ })
182
+ })
183
+
184
+ describe('set() and update() methods', () => {
185
+ test('set() replaces entire store value', () => {
186
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
187
+
188
+ user.set({ name: 'Alice', email: 'alice@example.com' })
189
+
190
+ expect(user.get()).toEqual({
191
+ name: 'Alice',
192
+ email: 'alice@example.com',
193
+ })
194
+ })
195
+
196
+ test('update() modifies store using function', () => {
197
+ const user = store({ name: 'Hannah', age: 25 })
198
+
199
+ user.update(prev => ({ ...prev, age: prev.age + 1 }))
200
+
201
+ expect(user.get()).toEqual({
202
+ name: 'Hannah',
203
+ age: 26,
204
+ })
205
+ })
206
+ })
207
+
208
+ describe('iterator protocol', () => {
209
+ test('supports for...of iteration', () => {
210
+ const user = store({ name: 'Hannah', age: 25 })
211
+ const entries: Array<[string, unknown & {}]> = []
212
+
213
+ for (const [key, signal] of user) {
214
+ entries.push([key, signal.get()])
215
+ }
216
+
217
+ expect(entries).toContainEqual(['name', 'Hannah'])
218
+ expect(entries).toContainEqual(['age', 25])
219
+ })
220
+ })
221
+
222
+ describe('change tracking', () => {
223
+ test('tracks size changes', () => {
224
+ const user = store<{ name: string; email?: string }>({
225
+ name: 'Hannah',
226
+ })
227
+
228
+ expect(user.size.get()).toBe(1)
229
+
230
+ user.add('email', 'hannah@example.com')
231
+ expect(user.size.get()).toBe(2)
232
+
233
+ user.remove('email')
234
+ expect(user.size.get()).toBe(1)
235
+ })
236
+
237
+ test('dispatches store-add event on initial creation', async () => {
238
+ let addEvent: StoreAddEvent<{ name: string }> | null = null
239
+ const user = store({ name: 'Hannah' })
240
+
241
+ user.addEventListener('store-add', event => {
242
+ addEvent = event
243
+ })
244
+
245
+ // Wait for the async initial event
246
+ await new Promise(resolve => setTimeout(resolve, 10))
247
+
248
+ expect(addEvent).toBeTruthy()
249
+ // biome-ignore lint/style/noNonNullAssertion: test
250
+ expect(addEvent!.detail).toEqual({ name: 'Hannah' })
251
+ })
252
+
253
+ test('dispatches store-add event for new properties', () => {
254
+ const user = store<{ name: string; email?: string }>({
255
+ name: 'Hannah',
256
+ })
257
+
258
+ let addEvent: StoreAddEvent<{
259
+ name: string
260
+ email?: string
261
+ }> | null = null
262
+ user.addEventListener('store-add', event => {
263
+ addEvent = event
264
+ })
265
+
266
+ user.update(v => ({ ...v, email: 'hannah@example.com' }))
267
+
268
+ expect(addEvent).toBeTruthy()
269
+ // biome-ignore lint/style/noNonNullAssertion: test
270
+ expect(addEvent!.detail).toEqual({
271
+ email: 'hannah@example.com',
272
+ })
273
+ })
274
+
275
+ test('dispatches store-change event for property changes', () => {
276
+ const user = store({ name: 'Hannah' })
277
+
278
+ let changeEvent: StoreChangeEvent<{ name: string }> | null = null
279
+ user.addEventListener('store-change', event => {
280
+ changeEvent = event
281
+ })
282
+
283
+ user.set({ name: 'Alice' })
284
+
285
+ expect(changeEvent).toBeTruthy()
286
+ // biome-ignore lint/style/noNonNullAssertion: test
287
+ expect(changeEvent!.detail).toEqual({
288
+ name: 'Alice',
289
+ })
290
+ })
291
+
292
+ test('dispatches store-change event for signal changes', () => {
293
+ const user = store({ name: 'Hannah' })
294
+
295
+ let changeEvent: StoreChangeEvent<{ name: string }> | null = null
296
+ user.addEventListener('store-change', event => {
297
+ changeEvent = event
298
+ })
299
+
300
+ user.name.set('Bob')
301
+
302
+ expect(changeEvent).toBeTruthy()
303
+ // biome-ignore lint/style/noNonNullAssertion: test
304
+ expect(changeEvent!.detail).toEqual({
305
+ name: 'Bob',
306
+ })
307
+ })
308
+
309
+ test('dispatches store-remove event for removed properties', () => {
310
+ const user = store<{ name: string; email?: string }>({
311
+ name: 'Hannah',
312
+ email: 'hannah@example.com',
313
+ })
314
+
315
+ let removeEvent: StoreRemoveEvent<{
316
+ name: string
317
+ email?: string
318
+ }> | null = null
319
+ user.addEventListener('store-remove', event => {
320
+ removeEvent = event
321
+ })
322
+
323
+ user.remove('email')
324
+
325
+ expect(removeEvent).toBeTruthy()
326
+ // biome-ignore lint/style/noNonNullAssertion: test
327
+ expect(removeEvent!.detail.email).toBe(UNSET)
328
+ })
329
+
330
+ test('dispatches store-add event when using add method', () => {
331
+ const user = store<{ name: string; email?: string }>({
332
+ name: 'Hannah',
333
+ })
334
+
335
+ let addEvent: StoreAddEvent<{
336
+ name: string
337
+ email?: string
338
+ }> | null = null
339
+ user.addEventListener('store-add', event => {
340
+ addEvent = event
341
+ })
342
+
343
+ user.add('email', 'hannah@example.com')
344
+
345
+ expect(addEvent).toBeTruthy()
346
+ // biome-ignore lint/style/noNonNullAssertion: test
347
+ expect(addEvent!.detail).toEqual({
348
+ email: 'hannah@example.com',
349
+ })
350
+ })
351
+
352
+ test('can remove event listeners', () => {
353
+ const user = store({ name: 'Hannah' })
354
+
355
+ let eventCount = 0
356
+ const listener = () => {
357
+ eventCount++
358
+ }
359
+
360
+ user.addEventListener('store-change', listener)
361
+ user.name.set('Alice')
362
+ expect(eventCount).toBe(1)
363
+
364
+ user.removeEventListener('store-change', listener)
365
+ user.name.set('Bob')
366
+ expect(eventCount).toBe(1) // Should not increment
367
+ })
368
+
369
+ test('supports multiple event listeners for the same event', () => {
370
+ const user = store({ name: 'Hannah' })
371
+
372
+ let listener1Called = false
373
+ let listener2Called = false
374
+
375
+ user.addEventListener('store-change', () => {
376
+ listener1Called = true
377
+ })
378
+
379
+ user.addEventListener('store-change', () => {
380
+ listener2Called = true
381
+ })
382
+
383
+ user.name.set('Alice')
384
+
385
+ expect(listener1Called).toBe(true)
386
+ expect(listener2Called).toBe(true)
387
+ })
388
+ })
389
+
390
+ describe('reactivity', () => {
391
+ test('store-level get() is reactive', () => {
392
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
393
+ let lastValue = { name: '', email: '' }
394
+
395
+ effect(() => {
396
+ lastValue = user.get()
397
+ })
398
+
399
+ user.name.set('Alice')
400
+
401
+ expect(lastValue).toEqual({
402
+ name: 'Alice',
403
+ email: 'hannah@example.com',
404
+ })
405
+ })
406
+
407
+ test('individual signal reactivity works', () => {
408
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
409
+ let lastName = ''
410
+ let nameEffectRuns = 0
411
+
412
+ // Get signal for name property directly
413
+ const nameSignal = user.name
414
+
415
+ effect(() => {
416
+ lastName = nameSignal.get()
417
+ nameEffectRuns++
418
+ })
419
+
420
+ // Change name should trigger effect
421
+ user.name.set('Alice')
422
+ expect(lastName).toBe('Alice')
423
+ expect(nameEffectRuns).toBe(2) // Initial + update
424
+ })
425
+
426
+ test('nested store changes propagate to parent', () => {
427
+ const user = store({
428
+ preferences: {
429
+ theme: 'dark',
430
+ },
431
+ })
432
+ let effectRuns = 0
433
+
434
+ effect(() => {
435
+ user.get() // Watch entire store
436
+ effectRuns++
437
+ })
438
+
439
+ user.preferences.theme.set('light')
440
+ expect(effectRuns).toBe(2) // Initial + nested change
441
+ })
442
+
443
+ test('updates are reactive', () => {
444
+ const user = store<{ name: string; email?: string }>({
445
+ name: 'Hannah',
446
+ })
447
+ let lastValue = {}
448
+ let effectRuns = 0
449
+
450
+ effect(() => {
451
+ lastValue = user.get()
452
+ effectRuns++
453
+ })
454
+
455
+ user.add('email', 'hannah@example.com')
456
+ expect(lastValue).toEqual({
457
+ name: 'Hannah',
458
+ email: 'hannah@example.com',
459
+ })
460
+ expect(effectRuns).toBe(2)
461
+ })
462
+
463
+ test('remove method is reactive', () => {
464
+ const user = store<{ name: string; email?: string }>({
465
+ name: 'Hannah',
466
+ email: 'hannah@example.com',
467
+ })
468
+ let lastValue = {}
469
+ let effectRuns = 0
470
+
471
+ effect(() => {
472
+ lastValue = user.get()
473
+ effectRuns++
474
+ })
475
+
476
+ expect(effectRuns).toBe(1)
477
+
478
+ user.remove('email')
479
+ expect(lastValue).toEqual({
480
+ name: 'Hannah',
481
+ })
482
+ expect(effectRuns).toBe(2)
483
+ })
484
+
485
+ test('add method does not overwrite existing properties', () => {
486
+ const user = store<{ name: string; email?: string }>({
487
+ name: 'Hannah',
488
+ email: 'original@example.com',
489
+ })
490
+
491
+ const originalSize = user.size.get()
492
+ user.add('email', 'new@example.com')
493
+
494
+ expect(user.email?.get()).toBe('original@example.com')
495
+ expect(user.size.get()).toBe(originalSize)
496
+ })
497
+
498
+ test('remove method has no effect on non-existent properties', () => {
499
+ const user = store<{ name: string; email?: string }>({
500
+ name: 'Hannah',
501
+ })
502
+
503
+ const originalSize = user.size.get()
504
+ user.remove('email')
505
+
506
+ expect(user.size.get()).toBe(originalSize)
507
+ })
508
+ })
509
+
510
+ describe('computed integration', () => {
511
+ test('works with computed signals', () => {
512
+ const user = store({ firstName: 'Hannah', lastName: 'Smith' })
513
+
514
+ const fullName = computed(() => {
515
+ return `${user.firstName.get()} ${user.lastName.get()}`
516
+ })
517
+
518
+ expect(fullName.get()).toBe('Hannah Smith')
519
+
520
+ user.firstName.set('Alice')
521
+ expect(fullName.get()).toBe('Alice Smith')
522
+ })
523
+
524
+ test('computed reacts to nested store changes', () => {
525
+ const config = store({
526
+ ui: {
527
+ theme: 'dark',
528
+ },
529
+ })
530
+
531
+ const themeDisplay = computed(() => {
532
+ return `Theme: ${config.ui.theme.get()}`
533
+ })
534
+
535
+ expect(themeDisplay.get()).toBe('Theme: dark')
536
+
537
+ config.ui.theme.set('light')
538
+ expect(themeDisplay.get()).toBe('Theme: light')
539
+ })
540
+ })
541
+
542
+ describe('arrays and edge cases', () => {
543
+ test('handles arrays as store values', () => {
544
+ const data = store({ items: [1, 2, 3] })
545
+
546
+ // Arrays become stores with string indices
547
+ expect(isStore(data.items)).toBe(true)
548
+ expect(data.items['0'].get()).toBe(1)
549
+ expect(data.items['1'].get()).toBe(2)
550
+ expect(data.items['2'].get()).toBe(3)
551
+ })
552
+
553
+ test('array-derived nested stores have correct type inference', () => {
554
+ const todoApp = store({
555
+ todos: ['Buy milk', 'Walk the dog', 'Write code'],
556
+ users: [
557
+ { name: 'Alice', active: true },
558
+ { name: 'Bob', active: false },
559
+ ],
560
+ numbers: [1, 2, 3, 4, 5],
561
+ })
562
+
563
+ // Arrays should become stores
564
+ expect(isStore(todoApp.todos)).toBe(true)
565
+ expect(isStore(todoApp.users)).toBe(true)
566
+ expect(isStore(todoApp.numbers)).toBe(true)
567
+
568
+ // String array elements should be State<string>
569
+ expect(todoApp.todos['0'].get()).toBe('Buy milk')
570
+ expect(todoApp.todos['1'].get()).toBe('Walk the dog')
571
+ expect(todoApp.todos['2'].get()).toBe('Write code')
572
+
573
+ // Should be able to modify string elements
574
+ todoApp.todos['0'].set('Buy groceries')
575
+ expect(todoApp.todos['0'].get()).toBe('Buy groceries')
576
+
577
+ // Object array elements should be Store<T>
578
+ expect(isStore(todoApp.users[0])).toBe(true)
579
+ expect(isStore(todoApp.users[1])).toBe(true)
580
+
581
+ // Should be able to access nested properties in object array elements
582
+ expect(todoApp.users[0].name.get()).toBe('Alice')
583
+ expect(todoApp.users[0].active.get()).toBe(true)
584
+ expect(todoApp.users[1].name.get()).toBe('Bob')
585
+ expect(todoApp.users[1].active.get()).toBe(false)
586
+
587
+ // Should be able to modify nested properties
588
+ todoApp.users[0].name.set('Alice Smith')
589
+ todoApp.users[0].active.set(false)
590
+ expect(todoApp.users[0].name.get()).toBe('Alice Smith')
591
+ expect(todoApp.users[0].active.get()).toBe(false)
592
+
593
+ // Number array elements should be State<number>
594
+ expect(todoApp.numbers[0].get()).toBe(1)
595
+ expect(todoApp.numbers[4].get()).toBe(5)
596
+
597
+ // Should be able to modify number elements
598
+ todoApp.numbers[0].set(10)
599
+ todoApp.numbers[4].set(50)
600
+ expect(todoApp.numbers[0].get()).toBe(10)
601
+ expect(todoApp.numbers[4].get()).toBe(50)
602
+
603
+ // Store-level access should reflect all changes
604
+ const currentState = todoApp.get()
605
+ expect(currentState.todos[0]).toBe('Buy groceries')
606
+ expect(currentState.users[0].name).toBe('Alice Smith')
607
+ expect(currentState.users[0].active).toBe(false)
608
+ expect(currentState.numbers[0]).toBe(10)
609
+ expect(currentState.numbers[4]).toBe(50)
610
+ })
611
+
612
+ test('handles UNSET values', () => {
613
+ const data = store({ value: UNSET as string })
614
+
615
+ expect(data.value.get()).toBe(UNSET)
616
+ data.value.set('some string')
617
+ expect(data.value.get()).toBe('some string')
618
+ })
619
+
620
+ test('handles primitive values', () => {
621
+ const data = store({
622
+ str: 'hello',
623
+ num: 42,
624
+ bool: true,
625
+ })
626
+
627
+ expect(data.str.get()).toBe('hello')
628
+ expect(data.num.get()).toBe(42)
629
+ expect(data.bool.get()).toBe(true)
630
+ })
631
+ })
632
+
633
+ describe('proxy behavior', () => {
634
+ test('Object.keys returns property keys', () => {
635
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
636
+
637
+ expect(Object.keys(user)).toEqual(['name', 'email'])
638
+ })
639
+
640
+ test('property enumeration works', () => {
641
+ const user = store({ name: 'Hannah', email: 'hannah@example.com' })
642
+ const keys: string[] = []
643
+
644
+ for (const key in user) {
645
+ keys.push(key)
646
+ }
647
+
648
+ expect(keys).toEqual(['name', 'email'])
649
+ })
650
+
651
+ test('in operator works', () => {
652
+ const user = store({ name: 'Hannah' })
653
+
654
+ expect('name' in user).toBe(true)
655
+ expect('email' in user).toBe(false)
656
+ })
657
+
658
+ test('Object.getOwnPropertyDescriptor works', () => {
659
+ const user = store({ name: 'Hannah' })
660
+
661
+ const descriptor = Object.getOwnPropertyDescriptor(user, 'name')
662
+ expect(descriptor).toEqual({
663
+ enumerable: true,
664
+ configurable: true,
665
+ writable: true,
666
+ value: user.name,
667
+ })
668
+ })
669
+ })
670
+
671
+ describe('type conversion via toSignal', () => {
672
+ test('arrays are converted to stores', () => {
673
+ const fruits = store({ items: ['apple', 'banana', 'cherry'] })
674
+
675
+ expect(isStore(fruits.items)).toBe(true)
676
+ expect(fruits.items['0'].get()).toBe('apple')
677
+ expect(fruits.items['1'].get()).toBe('banana')
678
+ expect(fruits.items['2'].get()).toBe('cherry')
679
+ })
680
+
681
+ test('nested objects become nested stores', () => {
682
+ const config = store({
683
+ database: {
684
+ host: 'localhost',
685
+ port: 5432,
686
+ },
687
+ })
688
+
689
+ expect(isStore(config.database)).toBe(true)
690
+ expect(config.database.host.get()).toBe('localhost')
691
+ expect(config.database.port.get()).toBe(5432)
692
+ })
693
+ })
694
+
695
+ describe('spread operator behavior', () => {
696
+ test('spreading store spreads individual signals', () => {
697
+ const user = store({ name: 'Hannah', age: 25, active: true })
698
+
699
+ // Spread the store - should get individual signals
700
+ const spread = { ...user }
701
+
702
+ // Check that we get the signals themselves
703
+ expect('name' in spread).toBe(true)
704
+ expect('age' in spread).toBe(true)
705
+ expect('active' in spread).toBe(true)
706
+
707
+ // The spread should contain signals that can be called with .get()
708
+ expect(typeof spread.name?.get).toBe('function')
709
+ expect(typeof spread.age?.get).toBe('function')
710
+ expect(typeof spread.active?.get).toBe('function')
711
+
712
+ // The signals should return the correct values
713
+ expect(spread.name?.get()).toBe('Hannah')
714
+ expect(spread.age?.get()).toBe(25)
715
+ expect(spread.active?.get()).toBe(true)
716
+
717
+ // Modifying the original store should be reflected in the spread signals
718
+ user.name.set('Alice')
719
+ user.age.set(30)
720
+
721
+ expect(spread.name?.get()).toBe('Alice')
722
+ expect(spread.age?.get()).toBe(30)
723
+ })
724
+
725
+ test('spreading nested store works correctly', () => {
726
+ const config = store({
727
+ app: { name: 'MyApp', version: '1.0' },
728
+ settings: { theme: 'dark', debug: false },
729
+ })
730
+
731
+ const spread = { ...config }
732
+
733
+ // Should get nested store signals
734
+ expect(isStore(spread.app)).toBe(true)
735
+ expect(isStore(spread.settings)).toBe(true)
736
+
737
+ // Should be able to access nested properties
738
+ expect(spread.app.name.get()).toBe('MyApp')
739
+ expect(spread.settings.theme.get()).toBe('dark')
740
+
741
+ // Modifications should be reflected
742
+ config.app.name.set('UpdatedApp')
743
+ expect(spread.app.name.get()).toBe('UpdatedApp')
744
+ })
745
+ })
746
+ })