dev-react-microstore 4.0.1 → 6.0.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.
@@ -0,0 +1,271 @@
1
+ import React from 'react'
2
+ import { describe, it, expect, vi } from 'vitest'
3
+ import { renderHook, act } from '@testing-library/react'
4
+ import {
5
+ createStoreState,
6
+ useStoreSelector,
7
+ createSelectorHook,
8
+ } from './index'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // useStoreSelector
12
+ // ---------------------------------------------------------------------------
13
+ describe('useStoreSelector', () => {
14
+ it('returns selected keys from the store', () => {
15
+ const store = createStoreState({ a: 1, b: 2, c: 3 })
16
+ const { result } = renderHook(() => useStoreSelector(store, ['a', 'c']))
17
+
18
+ expect(result.current).toEqual({ a: 1, c: 3 })
19
+ })
20
+
21
+ it('re-renders when a subscribed key changes', () => {
22
+ const store = createStoreState({ count: 0 })
23
+ const { result } = renderHook(() => useStoreSelector(store, ['count']))
24
+
25
+ expect(result.current.count).toBe(0)
26
+
27
+ act(() => store.set({ count: 5 }))
28
+
29
+ expect(result.current.count).toBe(5)
30
+ })
31
+
32
+ it('does not re-render when an unrelated key changes', () => {
33
+ const store = createStoreState({ a: 1, b: 2 })
34
+ const renderCount = vi.fn()
35
+
36
+ renderHook(() => {
37
+ renderCount()
38
+ return useStoreSelector(store, ['a'])
39
+ })
40
+
41
+ const callsAfterMount = renderCount.mock.calls.length
42
+
43
+ act(() => store.set({ b: 99 }))
44
+
45
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
46
+ })
47
+
48
+ it('does not re-render when set to the same value', () => {
49
+ const store = createStoreState({ count: 0 })
50
+ const renderCount = vi.fn()
51
+
52
+ renderHook(() => {
53
+ renderCount()
54
+ return useStoreSelector(store, ['count'])
55
+ })
56
+
57
+ const callsAfterMount = renderCount.mock.calls.length
58
+
59
+ act(() => store.set({ count: 0 }))
60
+
61
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
62
+ })
63
+
64
+ it('handles multiple keys and only re-renders for actual changes', () => {
65
+ const store = createStoreState({ x: 1, y: 2, z: 3 })
66
+ const { result } = renderHook(() => useStoreSelector(store, ['x', 'y']))
67
+
68
+ act(() => store.set({ x: 10 }))
69
+ expect(result.current).toEqual({ x: 10, y: 2 })
70
+
71
+ act(() => store.set({ y: 20 }))
72
+ expect(result.current).toEqual({ x: 10, y: 20 })
73
+ })
74
+
75
+ it('works with custom comparison functions', () => {
76
+ const store = createStoreState({
77
+ items: [1, 2, 3],
78
+ })
79
+ const renderCount = vi.fn()
80
+
81
+ const { result } = renderHook(() => {
82
+ renderCount()
83
+ return useStoreSelector(store, [
84
+ { items: (prev: number[], next: number[]) => prev.length === next.length },
85
+ ])
86
+ })
87
+
88
+ const callsAfterMount = renderCount.mock.calls.length
89
+
90
+ // Same length array — custom compare returns true (equal), so no re-render
91
+ act(() => store.set({ items: [4, 5, 6] }))
92
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
93
+
94
+ // Different length — custom compare returns false (not equal), triggers re-render
95
+ act(() => store.set({ items: [1, 2, 3, 4] }))
96
+ expect(result.current.items).toEqual([1, 2, 3, 4])
97
+ })
98
+
99
+ it('returns referentially stable result when nothing changed', () => {
100
+ const store = createStoreState({ a: 1, b: 2 })
101
+ const { result, rerender } = renderHook(() => useStoreSelector(store, ['a']))
102
+
103
+ const first = result.current
104
+ rerender()
105
+ const second = result.current
106
+
107
+ expect(first).toBe(second)
108
+ })
109
+
110
+ it('handles rapid sequential updates correctly', () => {
111
+ const store = createStoreState({ v: 0 })
112
+ const { result } = renderHook(() => useStoreSelector(store, ['v']))
113
+
114
+ act(() => {
115
+ store.set({ v: 1 })
116
+ store.set({ v: 2 })
117
+ store.set({ v: 3 })
118
+ })
119
+
120
+ expect(result.current.v).toBe(3)
121
+ })
122
+
123
+ it('works with object values', () => {
124
+ const store = createStoreState({ user: { name: 'Alice', age: 30 } })
125
+ const { result } = renderHook(() => useStoreSelector(store, ['user']))
126
+
127
+ expect(result.current.user).toEqual({ name: 'Alice', age: 30 })
128
+
129
+ const newUser = { name: 'Bob', age: 25 }
130
+ act(() => store.set({ user: newUser }))
131
+ expect(result.current.user).toBe(newUser)
132
+ })
133
+
134
+ it('works with null and undefined values', () => {
135
+ const store = createStoreState<{ v: string | null }>({ v: null })
136
+ const { result } = renderHook(() => useStoreSelector(store, ['v']))
137
+
138
+ expect(result.current.v).toBeNull()
139
+
140
+ act(() => store.set({ v: 'hello' }))
141
+ expect(result.current.v).toBe('hello')
142
+
143
+ act(() => store.set({ v: null }))
144
+ expect(result.current.v).toBeNull()
145
+ })
146
+ })
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // createSelectorHook
150
+ // ---------------------------------------------------------------------------
151
+ describe('createSelectorHook', () => {
152
+ it('returns a hook that works like useStoreSelector', () => {
153
+ const store = createStoreState({ count: 0, name: 'Alice' })
154
+ const useStore = createSelectorHook(store)
155
+
156
+ const { result } = renderHook(() => useStore(['count', 'name']))
157
+ expect(result.current).toEqual({ count: 0, name: 'Alice' })
158
+ })
159
+
160
+ it('re-renders on subscribed key change', () => {
161
+ const store = createStoreState({ count: 0 })
162
+ const useStore = createSelectorHook(store)
163
+
164
+ const { result } = renderHook(() => useStore(['count']))
165
+
166
+ act(() => store.set({ count: 42 }))
167
+ expect(result.current.count).toBe(42)
168
+ })
169
+
170
+ it('does not re-render on unrelated key change', () => {
171
+ const store = createStoreState({ a: 1, b: 2 })
172
+ const useStore = createSelectorHook(store)
173
+ const renderCount = vi.fn()
174
+
175
+ renderHook(() => {
176
+ renderCount()
177
+ return useStore(['a'])
178
+ })
179
+
180
+ const callsAfterMount = renderCount.mock.calls.length
181
+
182
+ act(() => store.set({ b: 99 }))
183
+
184
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
185
+ })
186
+
187
+ it('supports custom comparison functions', () => {
188
+ const store = createStoreState({ data: [1, 2] })
189
+ const useStore = createSelectorHook(store)
190
+ const renderCount = vi.fn()
191
+
192
+ renderHook(() => {
193
+ renderCount()
194
+ return useStore([
195
+ { data: (prev: number[], next: number[]) => prev.length === next.length },
196
+ ])
197
+ })
198
+
199
+ const callsAfterMount = renderCount.mock.calls.length
200
+
201
+ act(() => store.set({ data: [3, 4] })) // same length
202
+ expect(renderCount.mock.calls.length).toBe(callsAfterMount)
203
+
204
+ act(() => store.set({ data: [1] })) // different length
205
+ expect(renderCount.mock.calls.length).toBeGreaterThan(callsAfterMount)
206
+ })
207
+
208
+ it('multiple hooks on the same store work independently', () => {
209
+ const store = createStoreState({ a: 1, b: 2 })
210
+ const useStore = createSelectorHook(store)
211
+
212
+ const { result: r1 } = renderHook(() => useStore(['a']))
213
+ const { result: r2 } = renderHook(() => useStore(['b']))
214
+
215
+ act(() => store.set({ a: 10 }))
216
+
217
+ expect(r1.current.a).toBe(10)
218
+ expect(r2.current.b).toBe(2)
219
+ })
220
+ })
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Edge cases
224
+ // ---------------------------------------------------------------------------
225
+ describe('edge cases', () => {
226
+ it('useStoreSelector with middleware that blocks — hook reflects blocked state', () => {
227
+ const store = createStoreState({ count: 0 })
228
+ store.addMiddleware((_state, update, next) => {
229
+ if (update.count !== undefined && update.count < 0) return // block negatives
230
+ next()
231
+ })
232
+
233
+ const { result } = renderHook(() => useStoreSelector(store, ['count']))
234
+
235
+ act(() => store.set({ count: 5 }))
236
+ expect(result.current.count).toBe(5)
237
+
238
+ act(() => store.set({ count: -1 })) // blocked
239
+ expect(result.current.count).toBe(5)
240
+ })
241
+
242
+ it('useStoreSelector with middleware that transforms', () => {
243
+ const store = createStoreState({ name: '' })
244
+ store.addMiddleware((_state, update, next) => {
245
+ if (update.name) next({ name: update.name.trim() })
246
+ else next()
247
+ })
248
+
249
+ const { result } = renderHook(() => useStoreSelector(store, ['name']))
250
+
251
+ act(() => store.set({ name: ' hello ' }))
252
+ expect(result.current.name).toBe('hello')
253
+ })
254
+
255
+ it('store works correctly after many subscribe/unsubscribe cycles', () => {
256
+ const store = createStoreState({ v: 0 })
257
+ const unsubs: (() => void)[] = []
258
+
259
+ for (let i = 0; i < 100; i++) {
260
+ unsubs.push(store.subscribe(['v'], () => {}))
261
+ }
262
+ for (const unsub of unsubs) {
263
+ unsub()
264
+ }
265
+
266
+ const listener = vi.fn()
267
+ store.subscribe(['v'], listener)
268
+ store.set({ v: 1 })
269
+ expect(listener).toHaveBeenCalledTimes(1)
270
+ })
271
+ })