muya 1.1.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/src/shallow.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { isArray, isMap, isSet } from './is'
2
+
3
+ // eslint-disable-next-line sonarjs/cognitive-complexity
4
+ export function shallow<T>(valueA: T, valueB: T): boolean {
5
+ if (valueA == valueB) {
6
+ return true
7
+ }
8
+ if (Object.is(valueA, valueB)) {
9
+ return true
10
+ }
11
+
12
+ if (typeof valueA !== 'object' || valueA == null || typeof valueB !== 'object' || valueB == null) {
13
+ return false
14
+ }
15
+
16
+ if (isMap(valueA) && isMap(valueB)) {
17
+ if (valueA.size !== valueB.size) return false
18
+ for (const [key, value] of valueA) {
19
+ if (!Object.is(value, valueB.get(key))) {
20
+ return false
21
+ }
22
+ }
23
+ return true
24
+ }
25
+
26
+ if (isSet(valueA) && isSet(valueB)) {
27
+ if (valueA.size !== valueB.size) return false
28
+ for (const value of valueA) {
29
+ if (!valueB.has(value)) {
30
+ return false
31
+ }
32
+ }
33
+ return true
34
+ }
35
+
36
+ if (isArray(valueA) && isArray(valueB)) {
37
+ if (valueA.length !== valueB.length) return false
38
+ for (const [index, element] of valueA.entries()) {
39
+ if (!Object.is(element, valueB[index])) {
40
+ return false
41
+ }
42
+ }
43
+ return true
44
+ }
45
+
46
+ const keysA = Object.keys(valueA as Record<string, unknown>)
47
+ const keysB = Object.keys(valueB as Record<string, unknown>)
48
+ if (keysA.length !== keysB.length) return false
49
+ for (const key of keysA) {
50
+ if (
51
+ !Object.prototype.hasOwnProperty.call(valueB, key) ||
52
+ !Object.is((valueA as Record<string, unknown>)[key], (valueB as Record<string, unknown>)[key])
53
+ ) {
54
+ return false
55
+ }
56
+ }
57
+ return true
58
+ }
@@ -0,0 +1,647 @@
1
+ /* eslint-disable @typescript-eslint/no-shadow */
2
+ /* eslint-disable no-shadow */
3
+ import { Suspense } from 'react'
4
+ import { create } from './create'
5
+ import { renderHook, act, waitFor, render, screen } from '@testing-library/react'
6
+ import { shallow } from './shallow'
7
+ describe('state', () => {
8
+ it('should test state', () => {
9
+ const appState = create({ count: 0 })
10
+ expect(appState.getState()).toEqual({ count: 0 })
11
+ })
12
+
13
+ it('should render state with promise hook', async () => {
14
+ const promise = Promise.resolve({ count: 100 })
15
+ const appState = create(promise)
16
+ const renderCount = { current: 0 }
17
+ // const
18
+
19
+ const result = renderHook(() => {
20
+ renderCount.current++
21
+ return appState()
22
+ })
23
+ // wait for the promise to be resolved
24
+ await waitFor(() => {})
25
+ expect(result.result.current).toEqual({ count: 100 })
26
+ // count rendered
27
+ expect(renderCount.current).toEqual(2)
28
+ expect(appState.getState()).toEqual({ count: 100 })
29
+ })
30
+
31
+ it('should render state with get promise hook', async () => {
32
+ // eslint-disable-next-line unicorn/consistent-function-scoping
33
+ const getPromise = () => Promise.resolve({ count: 100 })
34
+ const appState = create(getPromise)
35
+ const renderCount = { current: 0 }
36
+ // const
37
+
38
+ const result = renderHook(() => {
39
+ renderCount.current++
40
+ return appState()
41
+ })
42
+ // wait for the promise to be resolved
43
+ await waitFor(() => {})
44
+ act(() => {
45
+ appState.setState({ count: 15 })
46
+ })
47
+ expect(result.result.current).toEqual({ count: 15 })
48
+ // count rendered
49
+ expect(renderCount.current).toEqual(3)
50
+ expect(appState.getState()).toEqual({ count: 15 })
51
+ })
52
+
53
+ it('should render state with get promise check default', async () => {
54
+ // eslint-disable-next-line unicorn/consistent-function-scoping
55
+ const getPromise = () => Promise.resolve({ count: 100 })
56
+ const appState = create(getPromise)
57
+ // const
58
+
59
+ // wait for the promise to be resolved
60
+ await waitFor(() => {})
61
+ act(() => {
62
+ appState.setState({ count: 15 })
63
+ })
64
+ expect(appState.getState()).toEqual({ count: 15 })
65
+ // count rendered
66
+ act(() => {
67
+ appState.reset()
68
+ })
69
+ expect(appState.getState()).toEqual({ count: 15 })
70
+ })
71
+
72
+ it('should render state with get hook', async () => {
73
+ // eslint-disable-next-line unicorn/consistent-function-scoping
74
+ const get = () => ({ count: 100 })
75
+ const appState = create(get)
76
+ const renderCount = { current: 0 }
77
+ // const
78
+
79
+ const result = renderHook(() => {
80
+ renderCount.current++
81
+ return appState()
82
+ })
83
+ // wait for the promise to be resolved
84
+ await waitFor(() => {})
85
+ act(() => {
86
+ appState.setState({ count: 15 })
87
+ })
88
+ expect(result.result.current).toEqual({ count: 15 })
89
+ // count rendered
90
+ expect(renderCount.current).toEqual(2)
91
+ expect(appState.getState()).toEqual({ count: 15 })
92
+
93
+ act(() => {
94
+ appState.reset()
95
+ })
96
+ expect(result.result.current).toEqual({ count: 100 })
97
+ })
98
+
99
+ it('should render state with get', async () => {
100
+ let wasCalled = false
101
+ const get = () => {
102
+ wasCalled = true
103
+ return { count: 100 }
104
+ }
105
+ const appState = create(get)
106
+ expect(wasCalled).toEqual(false)
107
+ appState.getState()
108
+ expect(wasCalled).toEqual(true)
109
+ })
110
+ it('should render state with get hook', async () => {
111
+ let wasCalled = false
112
+ const get = () => {
113
+ wasCalled = true
114
+ return { count: 100 }
115
+ }
116
+ const appState = create(get)
117
+ expect(wasCalled).toEqual(false)
118
+ renderHook(() => {
119
+ appState()
120
+ })
121
+ expect(wasCalled).toEqual(true)
122
+ })
123
+
124
+ it('should render state with promise with suspense', async () => {
125
+ const promise = Promise.resolve({ count: 100 })
126
+ const appState = create(promise)
127
+ const renderCount = { current: 0 }
128
+
129
+ const MockedComponent = jest.fn(() => <div>loading</div>)
130
+ const MockedComponentAfterSuspense = jest.fn(() => <div>loaded</div>)
131
+ // const
132
+ function Component() {
133
+ renderCount.current++
134
+ return (
135
+ <div>
136
+ {appState().count}
137
+ <MockedComponentAfterSuspense />
138
+ </div>
139
+ )
140
+ }
141
+ render(
142
+ <Suspense fallback={<MockedComponent />}>
143
+ <Component />
144
+ </Suspense>,
145
+ )
146
+ expect(MockedComponent).toHaveBeenCalledTimes(1)
147
+ expect(MockedComponentAfterSuspense).toHaveBeenCalledTimes(0)
148
+ await waitFor(() => {
149
+ return screen.getByText('100')
150
+ })
151
+ expect(MockedComponent).toHaveBeenCalledTimes(1)
152
+ expect(MockedComponentAfterSuspense).toHaveBeenCalledTimes(1)
153
+ })
154
+
155
+ it('should render state', () => {
156
+ const appState = create({ count: 0 })
157
+ const renderCount = { current: 0 }
158
+ // const
159
+
160
+ const result = renderHook(() => {
161
+ renderCount.current++
162
+ return appState()
163
+ })
164
+ expect(result.result.current).toEqual({ count: 0 })
165
+ // count rendered
166
+ expect(renderCount.current).toEqual(1)
167
+ })
168
+
169
+ it('should render state', () => {
170
+ const appState = create({ count: 0 })
171
+ const slice = appState.select((slice) => slice.count)
172
+ const renderCount = { current: 0 }
173
+ // const
174
+
175
+ const result = renderHook(() => {
176
+ renderCount.current++
177
+ return slice()
178
+ })
179
+ expect(result.result.current).toEqual(0)
180
+ // count rendered
181
+ expect(renderCount.current).toEqual(1)
182
+ })
183
+
184
+ it('should render state with change', () => {
185
+ const appState = create({ count: 0 })
186
+ const renderCount = { current: 0 }
187
+ // const
188
+
189
+ const result = renderHook(() => {
190
+ renderCount.current++
191
+ return appState((slice) => slice)
192
+ })
193
+
194
+ act(() => {
195
+ appState.setState({ count: 1 })
196
+ })
197
+ expect(result.result.current).toEqual({ count: 1 })
198
+ expect(renderCount.current).toEqual(2)
199
+ })
200
+
201
+ it('should render state with slice change', () => {
202
+ const appState = create({ count: { nested: 0, array: [0] } })
203
+ const renderCount = { current: 0 }
204
+ const useNestedSlice = appState.select((slice) => slice.count)
205
+ const useNestedSliceArray = appState.select((slice) => slice.count.array.length)
206
+ const result = renderHook(() => {
207
+ return appState()
208
+ })
209
+ const sliceResult = renderHook(() => {
210
+ renderCount.current++
211
+ return useNestedSlice()
212
+ })
213
+ const sliceArrayResult = renderHook(() => {
214
+ return useNestedSliceArray()
215
+ })
216
+ expect(sliceArrayResult.result.current).toEqual(1)
217
+ expect(sliceResult.result.current).toEqual({ nested: 0, array: [0] })
218
+ act(() => {
219
+ appState.setState({ count: { nested: 2, array: [0] } })
220
+ })
221
+
222
+ expect(result.result.current).toEqual({ count: { nested: 2, array: [0] } })
223
+ expect(sliceResult.result.current).toEqual({ nested: 2, array: [0] })
224
+
225
+ act(() => {
226
+ appState.setState({ count: { nested: 2, array: [1, 2, 4] } })
227
+ })
228
+ expect(sliceArrayResult.result.current).toEqual(3)
229
+ })
230
+
231
+ it('should render multiple state', () => {
232
+ const mainState = create({ count: { nestedCount: 2 } })
233
+ const slice1 = mainState.select((slice) => slice.count)
234
+ const slice2FromSlice1 = slice1.select((slice) => slice.nestedCount)
235
+
236
+ const slice2FromSlice1Result = renderHook(() => slice2FromSlice1())
237
+ expect(slice2FromSlice1Result.result.current).toEqual(2)
238
+
239
+ act(() => {
240
+ mainState.setState({ count: { nestedCount: 3 } })
241
+ })
242
+ expect(slice2FromSlice1Result.result.current).toEqual(3)
243
+ })
244
+
245
+ it('should render multiple state with change', () => {
246
+ const appState = create({ count: 0 })
247
+ const renderCount1 = { current: 0 }
248
+ const renderCount2 = { current: 0 }
249
+ // const
250
+
251
+ const result1 = renderHook(() => {
252
+ renderCount1.current++
253
+ return appState()
254
+ })
255
+ const result2 = renderHook(() => {
256
+ renderCount2.current++
257
+ return appState((slice) => slice.count)
258
+ })
259
+ act(() => {
260
+ appState.setState({ count: 1 })
261
+ })
262
+ expect(result1.result.current).toEqual({ count: 1 })
263
+ expect(result2.result.current).toEqual(1)
264
+ expect(renderCount1.current).toEqual(2)
265
+ expect(renderCount2.current).toEqual(2)
266
+ })
267
+
268
+ it('should test initial state', () => {
269
+ const appState = create({ count: 0 })
270
+ expect(appState.getState()).toEqual({ count: 0 })
271
+ })
272
+
273
+ it('should render initial state', () => {
274
+ const appState = create({ count: 0 })
275
+ const renderCount = { current: 0 }
276
+
277
+ const result = renderHook(() => {
278
+ renderCount.current++
279
+ return appState()
280
+ })
281
+ expect(result.result.current).toEqual({ count: 0 })
282
+ expect(renderCount.current).toEqual(1)
283
+ })
284
+
285
+ it('should render state after change', () => {
286
+ const appState = create({ count: 0 })
287
+ const renderCount = { current: 0 }
288
+
289
+ const result = renderHook(() => {
290
+ renderCount.current++
291
+ return appState((slice) => slice)
292
+ })
293
+
294
+ act(() => {
295
+ appState.setState({ count: 1 })
296
+ })
297
+ expect(result.result.current).toEqual({ count: 1 })
298
+ expect(renderCount.current).toEqual(2)
299
+ })
300
+
301
+ it('should render state with nested slice change', () => {
302
+ const appState = create({ count: { nested: 0, array: [0] } })
303
+ const renderCount = { current: 0 }
304
+ const useNestedSlice = appState.select((slice) => slice.count)
305
+ const useNestedSliceArray = appState.select((slice) => slice.count.array.length)
306
+
307
+ const result = renderHook(() => appState())
308
+ const sliceResult = renderHook(() => {
309
+ renderCount.current++
310
+ return useNestedSlice()
311
+ })
312
+ const sliceArrayResult = renderHook(() => useNestedSliceArray())
313
+
314
+ expect(sliceArrayResult.result.current).toEqual(1)
315
+ expect(sliceResult.result.current).toEqual({ nested: 0, array: [0] })
316
+
317
+ act(() => {
318
+ appState.setState({ count: { nested: 2, array: [0] } })
319
+ })
320
+ expect(result.result.current).toEqual({ count: { nested: 2, array: [0] } })
321
+ expect(sliceResult.result.current).toEqual({ nested: 2, array: [0] })
322
+
323
+ act(() => {
324
+ appState.setState({ count: { nested: 2, array: [1, 2, 4] } })
325
+ })
326
+ expect(sliceArrayResult.result.current).toEqual(3)
327
+ })
328
+
329
+ it('should render multiple state slices with updates', () => {
330
+ const mainState = create({ count: { nestedCount: 2 } })
331
+ const slice1 = mainState.select((slice) => slice.count)
332
+ const slice2FromSlice1 = slice1.select((slice) => slice.nestedCount)
333
+
334
+ const slice2FromSlice1Result = renderHook(() => slice2FromSlice1())
335
+ expect(slice2FromSlice1Result.result.current).toEqual(2)
336
+
337
+ act(() => {
338
+ mainState.setState({ count: { nestedCount: 3 } })
339
+ })
340
+ expect(slice2FromSlice1Result.result.current).toEqual(3)
341
+ })
342
+
343
+ it('should render multiple components observing the same state', () => {
344
+ const appState = create({ count: 0 })
345
+ const renderCount1 = { current: 0 }
346
+ const renderCount2 = { current: 0 }
347
+
348
+ const result1 = renderHook(() => {
349
+ renderCount1.current++
350
+ return appState()
351
+ })
352
+ const result2 = renderHook(() => {
353
+ renderCount2.current++
354
+ return appState((slice) => slice.count)
355
+ })
356
+
357
+ act(() => {
358
+ appState.setState({ count: 1 })
359
+ })
360
+ expect(result1.result.current).toEqual({ count: 1 })
361
+ expect(result2.result.current).toEqual(1)
362
+ expect(renderCount1.current).toEqual(2)
363
+ expect(renderCount2.current).toEqual(2)
364
+ })
365
+
366
+ it('should reset state to default value', () => {
367
+ const appState = create({ count: 0 })
368
+ act(() => {
369
+ appState.setState({ count: 10 })
370
+ })
371
+ expect(appState.getState()).toEqual({ count: 10 })
372
+
373
+ act(() => {
374
+ appState.reset()
375
+ })
376
+ expect(appState.getState()).toEqual({ count: 0 })
377
+ })
378
+
379
+ it('should handle updates with deep nesting in state', () => {
380
+ const appState = create({ data: { nested: { value: 1 } } })
381
+ const nestedSlice = appState.select((s) => s.data.nested.value)
382
+
383
+ const result = renderHook(() => nestedSlice())
384
+ expect(result.result.current).toEqual(1)
385
+
386
+ act(() => {
387
+ appState.setState({ data: { nested: { value: 2 } } })
388
+ })
389
+ expect(result.result.current).toEqual(2)
390
+ })
391
+
392
+ it('should not re-render for unrelated slice changes', () => {
393
+ const appState = create({ count: 0, unrelated: 5 })
394
+ const renderCount = { current: 0 }
395
+
396
+ const countSlice = appState.select((state) => state.count)
397
+ const unrelatedSlice = appState.select((state) => state.unrelated)
398
+
399
+ renderHook(() => {
400
+ renderCount.current++
401
+ return countSlice()
402
+ })
403
+
404
+ const unrelatedResult = renderHook(() => unrelatedSlice())
405
+ expect(renderCount.current).toEqual(1)
406
+
407
+ act(() => {
408
+ return appState.setState({ unrelated: 10 } as never)
409
+ })
410
+
411
+ expect(unrelatedResult.result.current).toEqual(10)
412
+ expect(renderCount.current).toEqual(2) // No re-render for count slice
413
+ })
414
+
415
+ it('should not re-render where isEqual return true on state', () => {
416
+ const appState = create({ count: 0 }, () => true)
417
+ const renderCount = { current: 0 }
418
+
419
+ renderHook(() => {
420
+ renderCount.current++
421
+ return appState()
422
+ })
423
+
424
+ act(() => {
425
+ appState.setState({ count: 10 })
426
+ })
427
+
428
+ expect(renderCount.current).toEqual(1)
429
+ })
430
+
431
+ it('should not re-render where isEqual return true hook slice', () => {
432
+ const appState = create({ count: 0 })
433
+ const renderCount = { current: 0 }
434
+
435
+ renderHook(() => {
436
+ renderCount.current++
437
+ return appState(
438
+ (slice) => slice,
439
+ () => true,
440
+ )
441
+ })
442
+
443
+ act(() => {
444
+ appState.setState({ count: 10 })
445
+ })
446
+
447
+ expect(renderCount.current).toEqual(1)
448
+ })
449
+
450
+ it('should not re-render where isEqual return true on slice', () => {
451
+ const appState = create({ count: 0 })
452
+ const appStateSlice = appState.select(
453
+ (slice) => slice.count,
454
+ () => true,
455
+ )
456
+ const renderCount = { current: 0 }
457
+
458
+ renderHook(() => {
459
+ renderCount.current++
460
+ return appStateSlice()
461
+ })
462
+
463
+ act(() => {
464
+ appState.setState({ count: 10 })
465
+ })
466
+ expect(renderCount.current).toEqual(1)
467
+ })
468
+
469
+ it('should not re-render where isEqual return true on nested slice', () => {
470
+ const appState = create({ count: { nested: { count: 0 } } })
471
+ const appStateSlice = appState.select((slice) => slice.count)
472
+ const nestedAppSlice = appStateSlice.select(
473
+ (slice) => slice.nested.count,
474
+ () => true,
475
+ )
476
+ const renderCount = { current: 0 }
477
+
478
+ renderHook(() => {
479
+ renderCount.current++
480
+ return nestedAppSlice()
481
+ })
482
+
483
+ act(() => {
484
+ appState.setState({ count: { nested: { count: 10 } } })
485
+ })
486
+ expect(renderCount.current).toEqual(1)
487
+ })
488
+ it('should use merge states', () => {
489
+ const state1 = create(3)
490
+ const state2 = create(2)
491
+ const mergedState = state1.merge(state2, (s1, s2) => s1 + s2)
492
+ const result = renderHook(() => mergedState())
493
+ expect(result.result.current).toEqual(5)
494
+ act(() => {
495
+ state1.setState(5)
496
+ })
497
+ expect(result.result.current).toEqual(7)
498
+ act(() => {
499
+ state2.setState(3)
500
+ })
501
+ expect(result.result.current).toEqual(8)
502
+ })
503
+
504
+ it('should use merge states nested', () => {
505
+ const useName = create(() => 'John')
506
+ const useAge = create(() => 30)
507
+ const useUser = useName.merge(useAge, (name, age) => ({ name, age }), shallow)
508
+ const result = renderHook(() => useUser())
509
+ expect(result.result.current).toEqual({ name: 'John', age: 30 })
510
+
511
+ act(() => {
512
+ useName.setState('Jane')
513
+ })
514
+ expect(result.result.current).toEqual({ name: 'Jane', age: 30 })
515
+ })
516
+
517
+ it('should use slice with new reference', () => {
518
+ const useName = create(() => 'John')
519
+ const useDifferentName = useName.select(
520
+ (name) => ({
521
+ name,
522
+ }),
523
+ shallow,
524
+ )
525
+ const result = renderHook(() => useDifferentName())
526
+ expect(result.result.current).toEqual({ name: 'John' })
527
+ act(() => {
528
+ useName.setState('Jane')
529
+ })
530
+ expect(result.result.current).toEqual({ name: 'Jane' })
531
+ })
532
+ it('should check if subscribe works', () => {
533
+ const appState = create({ count: 0 })
534
+ let count = 0
535
+ const unsubscribe = appState.subscribe((state) => {
536
+ expect(state).toEqual({ count })
537
+ count++
538
+ })
539
+
540
+ act(() => {
541
+ appState.setState({ count: 1 })
542
+ })
543
+ expect(count).toEqual(2)
544
+
545
+ unsubscribe()
546
+ act(() => {
547
+ appState.setState({ count: 2 })
548
+ })
549
+ expect(count).toEqual(2)
550
+ })
551
+
552
+ it('should handle rapid consecutive state updates', () => {
553
+ const appState = create({ count: 0 })
554
+ const renderCount = { current: 0 }
555
+
556
+ const result = renderHook(() => {
557
+ renderCount.current++
558
+ return appState()
559
+ })
560
+
561
+ act(() => {
562
+ // batch updates
563
+ appState.setState({ count: 1 })
564
+ appState.setState({ count: 2 })
565
+ appState.setState({ count: 3 })
566
+ })
567
+
568
+ expect(result.result.current).toEqual({ count: 3 })
569
+ expect(renderCount.current).toEqual(2) // it's batch
570
+ })
571
+
572
+ it('should handle setting state to the same value', () => {
573
+ const appState = create({ count: 0 })
574
+ const renderCount = { current: 0 }
575
+
576
+ const result = renderHook(() => {
577
+ renderCount.current++
578
+ return appState()
579
+ })
580
+
581
+ act(() => {
582
+ appState.setState((previous) => previous)
583
+ })
584
+
585
+ expect(result.result.current).toEqual({ count: 0 })
586
+ expect(renderCount.current).toEqual(1)
587
+ })
588
+
589
+ it('should handle setting state with partial updates', () => {
590
+ const appState = create({ count: 0, name: 'John' })
591
+ const renderCount = { current: 0 }
592
+
593
+ const result = renderHook(() => {
594
+ renderCount.current++
595
+ return appState()
596
+ })
597
+
598
+ act(() => {
599
+ appState.updateState({ count: 1 })
600
+ })
601
+
602
+ expect(result.result.current).toEqual({ count: 1, name: 'John' })
603
+ expect(renderCount.current).toEqual(2)
604
+ })
605
+
606
+ it('should handle resetting state after multiple updates', () => {
607
+ const appState = create({ count: 0 })
608
+ const renderCount = { current: 0 }
609
+
610
+ const result = renderHook(() => {
611
+ renderCount.current++
612
+ return appState()
613
+ })
614
+
615
+ act(() => {
616
+ appState.setState({ count: 1 })
617
+ appState.setState({ count: 2 })
618
+ appState.reset()
619
+ })
620
+
621
+ expect(result.result.current).toEqual({ count: 0 })
622
+ expect(renderCount.current).toEqual(2)
623
+ })
624
+
625
+ it('should handle concurrent asynchronous state updates', async () => {
626
+ const appState = create({ count: 0 })
627
+ const renderCount = { current: 0 }
628
+
629
+ const result = renderHook(() => {
630
+ renderCount.current++
631
+ return appState()
632
+ })
633
+
634
+ act(() => {
635
+ // Simulate concurrent asynchronous updates
636
+ appState.setState({ count: 1 })
637
+ appState.setState({ count: 2 })
638
+ appState.setState({ count: 3 })
639
+ })
640
+
641
+ await waitFor(() => {
642
+ expect(result.result.current).toEqual({ count: 3 })
643
+ })
644
+
645
+ expect(renderCount.current).toBe(2)
646
+ })
647
+ })