@storve/react 1.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,233 @@
1
+ import { render, screen, act } from '@testing-library/react'
2
+ import { createStore, type Store } from '@storve/core'
3
+ import { useStore, shallowEqual } from '../src/useStore'
4
+ import { expect, it, describe, vi } from 'vitest'
5
+ import React from 'react'
6
+
7
+ describe('useStore', () => {
8
+ it('shallowEqual returns true for same reference (covers Object.is branch)', () => {
9
+ const obj = { a: 1 }
10
+ expect(shallowEqual(obj, obj)).toBe(true)
11
+ })
12
+
13
+ it('returns full state when no selector provided', () => {
14
+ const store = createStore({ count: 0 })
15
+ function Counter() {
16
+ const state = useStore(store)
17
+ return <div data-testid="count">{state.count}</div>
18
+ }
19
+ render(<Counter />)
20
+ expect(screen.getByTestId('count')).toHaveTextContent('0')
21
+ })
22
+
23
+ it('returns correct initial state on first render', () => {
24
+ type TestState = { name: string }
25
+ const store = createStore<TestState>({ name: 'flux' })
26
+ let renderedState: TestState | undefined
27
+ function Profile() {
28
+ renderedState = useStore(store)
29
+ return null
30
+ }
31
+ render(<Profile />)
32
+ expect(renderedState).toEqual({ name: 'flux' })
33
+ })
34
+
35
+ it('returns updated state after setState called', () => {
36
+ const store = createStore({ count: 0 })
37
+ function Counter() {
38
+ const state = useStore(store)
39
+ return <div data-testid="count">{state.count}</div>
40
+ }
41
+ render(<Counter />)
42
+ act(() => {
43
+ store.setState({ count: 1 })
44
+ })
45
+ expect(screen.getByTestId('count')).toHaveTextContent('1')
46
+ })
47
+
48
+ it('component re-renders when state changes', () => {
49
+ const store = createStore({ count: 0 })
50
+ const renderSpy = vi.fn()
51
+ function Counter() {
52
+ renderSpy()
53
+ const state = useStore(store)
54
+ return <div>{state.count}</div>
55
+ }
56
+ render(<Counter />)
57
+ expect(renderSpy).toHaveBeenCalledTimes(1)
58
+ act(() => {
59
+ store.setState({ count: 1 })
60
+ })
61
+ expect(renderSpy).toHaveBeenCalledTimes(2)
62
+ })
63
+
64
+ it('component does NOT re-render when unrelated state changes', () => {
65
+ const store = createStore({ a: 1, b: 2 })
66
+ const renderSpy = vi.fn()
67
+ function Component() {
68
+ renderSpy()
69
+ const state = useStore(store)
70
+ // useSyncExternalStore with full state will re-render if any part of state changes
71
+ // because the state object returned by storve (Proxy) is updated on any change
72
+ return <div>{state.a}</div>
73
+ }
74
+ render(<Component />)
75
+ expect(renderSpy).toHaveBeenCalledTimes(1)
76
+
77
+ // Changing 'b' SHOULD trigger a re-render if we return the full state
78
+ // because useSyncExternalStore checks reference equality of the return value
79
+ act(() => {
80
+ store.setState({ b: 3 })
81
+ })
82
+ expect(renderSpy).toHaveBeenCalledTimes(2)
83
+ })
84
+
85
+ it('multiple components using same store all update correctly', () => {
86
+ const store = createStore({ count: 0 })
87
+ function Counter() {
88
+ const state = useStore(store)
89
+ return <div data-testid="count">{state.count}</div>
90
+ }
91
+ render(
92
+ <>
93
+ <Counter />
94
+ <Counter />
95
+ </>
96
+ )
97
+ act(() => {
98
+ store.setState({ count: 5 })
99
+ })
100
+ const elements = screen.getAllByTestId('count')
101
+ expect(elements[0]).toHaveTextContent('5')
102
+ expect(elements[1]).toHaveTextContent('5')
103
+ })
104
+
105
+ it('multiple components using different stores are independent', () => {
106
+ const store1 = createStore({ a: 1 })
107
+ const store2 = createStore({ b: 2 })
108
+ function A() {
109
+ const state = useStore(store1)
110
+ return <div data-testid="a">{state.a}</div>
111
+ }
112
+ function B() {
113
+ const state = useStore(store2)
114
+ return <div data-testid="b">{state.b}</div>
115
+ }
116
+ render(
117
+ <>
118
+ <A />
119
+ <B />
120
+ </>
121
+ )
122
+ act(() => {
123
+ store1.setState({ a: 10 })
124
+ })
125
+ expect(screen.getByTestId('a')).toHaveTextContent('10')
126
+ expect(screen.getByTestId('b')).toHaveTextContent('2')
127
+ })
128
+
129
+ it('useStore works with flat state', () => {
130
+ type TestState = { a: number; b: string }
131
+ const store = createStore<TestState>({ a: 1, b: 'test' })
132
+ const { result } = { result: { current: null as TestState | null } }
133
+ function Test() {
134
+ result.current = useStore(store)
135
+ return null
136
+ }
137
+ render(<Test />)
138
+ expect(result.current).toEqual({ a: 1, b: 'test' })
139
+ })
140
+
141
+ it('useStore works with nested state', () => {
142
+ type TestState = { user: { id: number; profile: { name: string } } }
143
+ const store = createStore<TestState>({ user: { id: 1, profile: { name: 'John' } } })
144
+ const { result } = { result: { current: null as TestState | null } }
145
+ function Test() {
146
+ result.current = useStore(store)
147
+ return null
148
+ }
149
+ render(<Test />)
150
+ expect(result.current?.user.profile.name).toBe('John')
151
+ })
152
+
153
+ it('useStore works with array state', () => {
154
+ type TestState = { list: number[] }
155
+ const store = createStore<TestState>({ list: [1, 2, 3] })
156
+ const { result } = { result: { current: null as TestState | null } }
157
+ function Test() {
158
+ result.current = useStore(store)
159
+ return null
160
+ }
161
+ render(<Test />)
162
+ expect(result.current?.list).toEqual([1, 2, 3])
163
+ })
164
+
165
+ it('useStore works with boolean state', () => {
166
+ type TestState = { ok: boolean }
167
+ const store = createStore<TestState>({ ok: true })
168
+ const { result } = { result: { current: null as TestState | null } }
169
+ function Test() {
170
+ result.current = useStore(store)
171
+ return null
172
+ }
173
+ render(<Test />)
174
+ expect(result.current?.ok).toBe(true)
175
+ })
176
+
177
+ it('useStore works with null state', () => {
178
+ // Note: createStore requires an object as initial state
179
+ type TestState = { data: null }
180
+ const store = createStore<TestState>({ data: null })
181
+ const { result } = { result: { current: null as TestState | null } }
182
+ function Test() {
183
+ result.current = useStore(store)
184
+ return null
185
+ }
186
+ render(<Test />)
187
+ expect(result.current?.data).toBeNull()
188
+ })
189
+
190
+ it('useStore works with empty object state', () => {
191
+ type TestState = Record<string, never>
192
+ const store = createStore<TestState>({})
193
+ const { result } = { result: { current: null as TestState | null } }
194
+ function Test() {
195
+ result.current = useStore(store)
196
+ return null
197
+ }
198
+ render(<Test />)
199
+ expect(result.current).toEqual({})
200
+ })
201
+
202
+ it('hook called with same store reference — stable behavior', () => {
203
+ const store = createStore({ count: 0 })
204
+ const renderSpy = vi.fn()
205
+ function Test() {
206
+ renderSpy()
207
+ useStore(store)
208
+ return null
209
+ }
210
+ const { rerender } = render(<Test />)
211
+ expect(renderSpy).toHaveBeenCalledTimes(1)
212
+ rerender(<Test />)
213
+ expect(renderSpy).toHaveBeenCalledTimes(2)
214
+ })
215
+
216
+ it('hook called with different store reference — switches correctly', () => {
217
+ const store1 = createStore({ val: 1 })
218
+ const store2 = createStore({ val: 2 })
219
+ function Test({ s }: { s: Store<{ val: number }> }) {
220
+ const state = useStore(s)
221
+ return <div data-testid="val">{state.val}</div>
222
+ }
223
+ const { rerender } = render(<Test s={store1} />)
224
+ expect(screen.getByTestId('val')).toHaveTextContent('1')
225
+ rerender(<Test s={store2} />)
226
+ expect(screen.getByTestId('val')).toHaveTextContent('2')
227
+
228
+ act(() => {
229
+ store1.setState({ val: 10 })
230
+ })
231
+ expect(screen.getByTestId('val')).toHaveTextContent('2') // Should not update from store1 anymore
232
+ })
233
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "jsx": "react"
7
+ },
8
+ "include": [
9
+ "src/**/*"
10
+ ],
11
+ "exclude": [
12
+ "node_modules",
13
+ "dist",
14
+ "tests/**/*"
15
+ ]
16
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'jsdom',
6
+ globals: true,
7
+ setupFiles: ['./tests/setup.ts'],
8
+ include: ['tests/**/*.{test,spec}.{ts,tsx}'],
9
+ pool: 'forks',
10
+ poolOptions: {
11
+ forks: {
12
+ singleFork: true
13
+ }
14
+ },
15
+ coverage: {
16
+ provider: 'v8',
17
+ reporter: ['text', 'json-summary'],
18
+ thresholds: {
19
+ statements: 85,
20
+ branches: 85,
21
+ functions: 85,
22
+ lines: 85,
23
+ },
24
+ include: ['src/**/*.ts', 'src/**/*.tsx'],
25
+ exclude: ['src/types.ts'],
26
+ },
27
+ },
28
+ })
29
+
30
+