@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.
- package/benchmarks/week3.ts +102 -0
- package/coverage/coverage-summary.json +5 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.js +9 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +7 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.mjs +94 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/useDevtools.d.ts +23 -0
- package/dist/useDevtools.d.ts.map +1 -0
- package/dist/useStore.d.ts +5 -0
- package/dist/useStore.d.ts.map +1 -0
- package/package.json +40 -0
- package/rollup.config.mjs +25 -0
- package/src/index.ts +4 -0
- package/src/types.ts +16 -0
- package/src/useDevtools.ts +74 -0
- package/src/useStore.ts +83 -0
- package/test_output.txt +234 -0
- package/tests/computed.react.test.tsx +71 -0
- package/tests/concurrent.test.tsx +101 -0
- package/tests/index.test.tsx +29 -0
- package/tests/integration.test.tsx +135 -0
- package/tests/lifecycle.test.tsx +148 -0
- package/tests/selector.test.tsx +288 -0
- package/tests/setup.ts +7 -0
- package/tests/useDevtools.test.tsx +80 -0
- package/tests/useStore.test.tsx +233 -0
- package/tsconfig.json +16 -0
- package/vitest.config.mts +30 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { render, act } from '@testing-library/react'
|
|
2
|
+
import { createStore } from '@storve/core'
|
|
3
|
+
import { useStore } from '../src/useStore'
|
|
4
|
+
import { expect, it, describe, vi } from 'vitest'
|
|
5
|
+
import React, { useEffect } from 'react'
|
|
6
|
+
|
|
7
|
+
describe('lifecycle', () => {
|
|
8
|
+
it('subscription created on component mount', () => {
|
|
9
|
+
const store = createStore({ count: 0 })
|
|
10
|
+
const subscribeSpy = vi.spyOn(store, 'subscribe')
|
|
11
|
+
function Test() {
|
|
12
|
+
useStore(store)
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
render(<Test />)
|
|
16
|
+
expect(subscribeSpy).toHaveBeenCalled()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('subscription cleaned up on component unmount', () => {
|
|
20
|
+
const store = createStore({ count: 0 })
|
|
21
|
+
const unsubscribeSpy = vi.fn()
|
|
22
|
+
vi.spyOn(store, 'subscribe').mockReturnValue(unsubscribeSpy)
|
|
23
|
+
|
|
24
|
+
function Test() {
|
|
25
|
+
useStore(store)
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
const { unmount } = render(<Test />)
|
|
29
|
+
unmount()
|
|
30
|
+
expect(unsubscribeSpy).toHaveBeenCalled()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('no memory leak after 100 mount/unmount cycles', () => {
|
|
34
|
+
const store = createStore({ count: 0 })
|
|
35
|
+
const subscribeSpy = vi.spyOn(store, 'subscribe')
|
|
36
|
+
const unsubscribeSpy = vi.fn()
|
|
37
|
+
subscribeSpy.mockReturnValue(unsubscribeSpy)
|
|
38
|
+
|
|
39
|
+
function Test() {
|
|
40
|
+
useStore(store)
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < 100; i++) {
|
|
45
|
+
const { unmount } = render(<Test />)
|
|
46
|
+
unmount()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
expect(subscribeSpy).toHaveBeenCalledTimes(100)
|
|
50
|
+
expect(unsubscribeSpy).toHaveBeenCalledTimes(100)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('unmounted component does not receive state updates', () => {
|
|
54
|
+
const store = createStore({ count: 0 })
|
|
55
|
+
const renderSpy = vi.fn()
|
|
56
|
+
function Test() {
|
|
57
|
+
renderSpy()
|
|
58
|
+
useStore(store)
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
const { unmount } = render(<Test />)
|
|
62
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
63
|
+
unmount()
|
|
64
|
+
|
|
65
|
+
act(() => {
|
|
66
|
+
store.setState({ count: 1 })
|
|
67
|
+
})
|
|
68
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('re-mounted component resubscribes correctly', () => {
|
|
72
|
+
const store = createStore({ count: 0 })
|
|
73
|
+
const subscribeSpy = vi.spyOn(store, 'subscribe')
|
|
74
|
+
function Test() {
|
|
75
|
+
useStore(store)
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
const { unmount } = render(<Test />)
|
|
79
|
+
unmount()
|
|
80
|
+
render(<Test />)
|
|
81
|
+
expect(subscribeSpy).toHaveBeenCalledTimes(2)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('multiple mounts of same component — each has own subscription', () => {
|
|
85
|
+
const store = createStore({ count: 0 })
|
|
86
|
+
const subscribeSpy = vi.spyOn(store, 'subscribe')
|
|
87
|
+
function Test() {
|
|
88
|
+
useStore(store)
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
render(
|
|
92
|
+
<>
|
|
93
|
+
<Test />
|
|
94
|
+
<Test />
|
|
95
|
+
</>
|
|
96
|
+
)
|
|
97
|
+
expect(subscribeSpy).toHaveBeenCalledTimes(2)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('component unmount during state update — no crash', () => {
|
|
101
|
+
const store = createStore({ count: 0 })
|
|
102
|
+
function Test() {
|
|
103
|
+
useStore(store)
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
// Trigger update on mount then unmount
|
|
106
|
+
// This is a bit tricky to simulate exactly but we can try
|
|
107
|
+
}, [])
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
const { unmount } = render(<Test />)
|
|
111
|
+
act(() => {
|
|
112
|
+
store.setState({ count: 1 })
|
|
113
|
+
unmount()
|
|
114
|
+
})
|
|
115
|
+
// Should not crash
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('setState after all consumers unmounted — no crash', () => {
|
|
119
|
+
const store = createStore({ count: 0 })
|
|
120
|
+
function Test() {
|
|
121
|
+
useStore(store)
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
const { unmount } = render(<Test />)
|
|
125
|
+
unmount()
|
|
126
|
+
act(() => {
|
|
127
|
+
store.setState({ count: 1 })
|
|
128
|
+
})
|
|
129
|
+
// Should not crash
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('parent unmount cleans up child subscriptions', () => {
|
|
133
|
+
const store = createStore({ count: 0 })
|
|
134
|
+
const unsubscribeSpy = vi.fn()
|
|
135
|
+
vi.spyOn(store, 'subscribe').mockReturnValue(unsubscribeSpy)
|
|
136
|
+
|
|
137
|
+
function Child() {
|
|
138
|
+
useStore(store)
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
function Parent() {
|
|
142
|
+
return <Child />
|
|
143
|
+
}
|
|
144
|
+
const { unmount } = render(<Parent />)
|
|
145
|
+
unmount()
|
|
146
|
+
expect(unsubscribeSpy).toHaveBeenCalled()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { render, screen, act } from '@testing-library/react'
|
|
2
|
+
import { createStore } from '@storve/core'
|
|
3
|
+
import { useStore } from '../src/useStore'
|
|
4
|
+
import { expect, it, describe, vi } from 'vitest'
|
|
5
|
+
import React, { useCallback } from 'react'
|
|
6
|
+
|
|
7
|
+
describe('useStore with selector', () => {
|
|
8
|
+
it('selector receives full state as argument', () => {
|
|
9
|
+
const store = createStore({ a: 1, b: 2 })
|
|
10
|
+
let receivedState: unknown
|
|
11
|
+
function Test() {
|
|
12
|
+
useStore(store, (s: unknown) => {
|
|
13
|
+
receivedState = s
|
|
14
|
+
return (s as { a: number; b: number }).a
|
|
15
|
+
})
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
render(<Test />)
|
|
19
|
+
expect(receivedState).toEqual({ a: 1, b: 2 })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('selector return value is what component receives', () => {
|
|
23
|
+
const store = createStore({ count: 10 })
|
|
24
|
+
function Test() {
|
|
25
|
+
const doubled = useStore(store, (s) => s.count * 2)
|
|
26
|
+
return <div data-testid="val">{doubled}</div>
|
|
27
|
+
}
|
|
28
|
+
render(<Test />)
|
|
29
|
+
expect(screen.getByTestId('val')).toHaveTextContent('20')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('component re-renders when selector return value changes', () => {
|
|
33
|
+
const store = createStore({ count: 0, unrelated: 0 })
|
|
34
|
+
const renderSpy = vi.fn()
|
|
35
|
+
function Test() {
|
|
36
|
+
renderSpy()
|
|
37
|
+
const count = useStore(store, (s) => s.count)
|
|
38
|
+
return <div>{count}</div>
|
|
39
|
+
}
|
|
40
|
+
render(<Test />)
|
|
41
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
42
|
+
act(() => {
|
|
43
|
+
store.setState({ count: 1 })
|
|
44
|
+
})
|
|
45
|
+
expect(renderSpy).toHaveBeenCalledTimes(2)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('component does NOT re-render when selector return value unchanged', () => {
|
|
49
|
+
const store = createStore({ count: 0, unrelated: 0 })
|
|
50
|
+
const renderSpy = vi.fn()
|
|
51
|
+
function Test() {
|
|
52
|
+
renderSpy()
|
|
53
|
+
const isPositive = useStore(store, (s) => s.count > 0)
|
|
54
|
+
return <div>{String(isPositive)}</div>
|
|
55
|
+
}
|
|
56
|
+
render(<Test />)
|
|
57
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
58
|
+
|
|
59
|
+
// Unrelated state change
|
|
60
|
+
act(() => {
|
|
61
|
+
store.setState({ unrelated: 1 })
|
|
62
|
+
})
|
|
63
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
64
|
+
|
|
65
|
+
// Related state change but result remains same (0 -> -1, both NOT > 0)
|
|
66
|
+
act(() => {
|
|
67
|
+
store.setState({ count: -1 })
|
|
68
|
+
})
|
|
69
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('selector returning primitive re-renders correctly', () => {
|
|
73
|
+
const store = createStore({ val: 'a' })
|
|
74
|
+
function Test() {
|
|
75
|
+
const val = useStore(store, (s) => s.val)
|
|
76
|
+
return <div data-testid="val">{val}</div>
|
|
77
|
+
}
|
|
78
|
+
render(<Test />)
|
|
79
|
+
act(() => {
|
|
80
|
+
store.setState({ val: 'b' })
|
|
81
|
+
})
|
|
82
|
+
expect(screen.getByTestId('val')).toHaveTextContent('b')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('selector returning object re-renders correctly', () => {
|
|
86
|
+
const store = createStore({ a: 1, b: 2 })
|
|
87
|
+
const renderSpy = vi.fn()
|
|
88
|
+
function Test() {
|
|
89
|
+
renderSpy()
|
|
90
|
+
const sub = useStore(store, (s) => ({ combined: s.a + s.b }))
|
|
91
|
+
return <div>{sub.combined}</div>
|
|
92
|
+
}
|
|
93
|
+
render(<Test />)
|
|
94
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
95
|
+
|
|
96
|
+
// Even if values are same, returning a new object literal triggers re-render
|
|
97
|
+
// because useSyncExternalStore uses Object.is comparison
|
|
98
|
+
act(() => {
|
|
99
|
+
store.setState({ a: 1 }) // No actual change, but store.subscribe might fire
|
|
100
|
+
})
|
|
101
|
+
// Storve only notifies if state actually changed.
|
|
102
|
+
// If we change 'a' to '1' (same), Storve might not notify.
|
|
103
|
+
|
|
104
|
+
act(() => {
|
|
105
|
+
store.setState({ a: 10 })
|
|
106
|
+
})
|
|
107
|
+
expect(renderSpy).toHaveBeenCalledTimes(2)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('selector returning array re-renders correctly', () => {
|
|
111
|
+
const store = createStore({ items: [1, 2] })
|
|
112
|
+
const renderSpy = vi.fn()
|
|
113
|
+
function Test() {
|
|
114
|
+
renderSpy()
|
|
115
|
+
const first = useStore(store, (s) => [s.items[0]])
|
|
116
|
+
return <div>{first[0]}</div>
|
|
117
|
+
}
|
|
118
|
+
render(<Test />)
|
|
119
|
+
act(() => {
|
|
120
|
+
store.setState({ items: [10, 2] })
|
|
121
|
+
})
|
|
122
|
+
expect(renderSpy).toHaveBeenCalledTimes(2)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('selector returning derived number re-renders correctly', () => {
|
|
126
|
+
const store = createStore({ a: 1, b: 2 })
|
|
127
|
+
function Test() {
|
|
128
|
+
const sum = useStore(store, (s) => s.a + s.b)
|
|
129
|
+
return <div data-testid="sum">{sum}</div>
|
|
130
|
+
}
|
|
131
|
+
render(<Test />)
|
|
132
|
+
act(() => {
|
|
133
|
+
store.setState({ a: 5 })
|
|
134
|
+
})
|
|
135
|
+
expect(screen.getByTestId('sum')).toHaveTextContent('7')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('selector returning derived boolean re-renders correctly', () => {
|
|
139
|
+
const store = createStore({ count: 0 })
|
|
140
|
+
function Test() {
|
|
141
|
+
const isZero = useStore(store, (s) => s.count === 0)
|
|
142
|
+
return <div data-testid="val">{String(isZero)}</div>
|
|
143
|
+
}
|
|
144
|
+
render(<Test />)
|
|
145
|
+
act(() => {
|
|
146
|
+
store.setState({ count: 1 })
|
|
147
|
+
})
|
|
148
|
+
expect(screen.getByTestId('val')).toHaveTextContent('false')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('selector returning null does not crash', () => {
|
|
152
|
+
const store = createStore<{ data: { name: string } | null }>({ data: { name: 'test' } })
|
|
153
|
+
function Test() {
|
|
154
|
+
const name = useStore(store, (s: unknown) => {
|
|
155
|
+
const state = s as { data: { name: string } | null }
|
|
156
|
+
return state.data ? state.data.name : null
|
|
157
|
+
})
|
|
158
|
+
return <div data-testid="val">{name === null ? 'null' : name}</div>
|
|
159
|
+
}
|
|
160
|
+
render(<Test />)
|
|
161
|
+
act(() => {
|
|
162
|
+
store.setState({ data: null })
|
|
163
|
+
})
|
|
164
|
+
expect(screen.getByTestId('val')).toHaveTextContent('null')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('selector returning undefined does not crash', () => {
|
|
168
|
+
const store = createStore({ val: 1 } as { val?: number })
|
|
169
|
+
function Test() {
|
|
170
|
+
const val = useStore(store, (s) => s.val)
|
|
171
|
+
return <div data-testid="val">{val === undefined ? 'undefined' : val}</div>
|
|
172
|
+
}
|
|
173
|
+
render(<Test />)
|
|
174
|
+
act(() => {
|
|
175
|
+
store.setState({ val: undefined })
|
|
176
|
+
})
|
|
177
|
+
expect(screen.getByTestId('val')).toHaveTextContent('undefined')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('selector with multiple state dependencies updates correctly', () => {
|
|
181
|
+
const store = createStore({ a: 1, b: 2, c: 3 })
|
|
182
|
+
function Test() {
|
|
183
|
+
const total = useStore(store, (s) => s.a + s.b + s.c)
|
|
184
|
+
return <div data-testid="total">{total}</div>
|
|
185
|
+
}
|
|
186
|
+
render(<Test />)
|
|
187
|
+
act(() => {
|
|
188
|
+
store.setState({ a: 10, b: 20 })
|
|
189
|
+
})
|
|
190
|
+
expect(screen.getByTestId('total')).toHaveTextContent('33')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('inline selector function — stable behavior', () => {
|
|
194
|
+
const store = createStore({ count: 0 })
|
|
195
|
+
const renderSpy = vi.fn()
|
|
196
|
+
function Test() {
|
|
197
|
+
renderSpy()
|
|
198
|
+
const count = useStore(store, (s) => s.count)
|
|
199
|
+
return <div>{count}</div>
|
|
200
|
+
}
|
|
201
|
+
const { rerender } = render(<Test />)
|
|
202
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
203
|
+
rerender(<Test />)
|
|
204
|
+
expect(renderSpy).toHaveBeenCalledTimes(2)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('memoized selector via useCallback — no extra re-renders', () => {
|
|
208
|
+
const store = createStore({ count: 0 })
|
|
209
|
+
const renderSpy = vi.fn()
|
|
210
|
+
function Test() {
|
|
211
|
+
renderSpy()
|
|
212
|
+
const selector = useCallback((s: unknown) => (s as { count: number }).count, [])
|
|
213
|
+
const count = useStore(store, selector)
|
|
214
|
+
return <div>{count}</div>
|
|
215
|
+
}
|
|
216
|
+
const { rerender } = render(<Test />)
|
|
217
|
+
expect(renderSpy).toHaveBeenCalledTimes(1)
|
|
218
|
+
rerender(<Test />)
|
|
219
|
+
// Even if selector is memoized, Component itself re-renders because rerender() was called
|
|
220
|
+
expect(renderSpy).toHaveBeenCalledTimes(2)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('changing selector between renders — returns correct value', () => {
|
|
224
|
+
const store = createStore({ a: 1, b: 2 })
|
|
225
|
+
function Test({ mode }: { mode: 'a' | 'b' }) {
|
|
226
|
+
const val = useStore(store, mode === 'a' ? (s) => s.a : (s) => s.b)
|
|
227
|
+
return <div data-testid="val">{val}</div>
|
|
228
|
+
}
|
|
229
|
+
const { rerender } = render(<Test mode="a" />)
|
|
230
|
+
expect(screen.getByTestId('val')).toHaveTextContent('1')
|
|
231
|
+
rerender(<Test mode="b" />)
|
|
232
|
+
expect(screen.getByTestId('val')).toHaveTextContent('2')
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('selector returning primitive then object — shallowEqual with type mismatch', () => {
|
|
236
|
+
const store = createStore({ mode: 'num' as 'num' | 'obj' })
|
|
237
|
+
const renderSpy = vi.fn()
|
|
238
|
+
function Test() {
|
|
239
|
+
renderSpy()
|
|
240
|
+
const val = useStore(
|
|
241
|
+
store,
|
|
242
|
+
(s) => (s.mode === 'num' ? 42 : { value: 42 })
|
|
243
|
+
)
|
|
244
|
+
return (
|
|
245
|
+
<div data-testid="val">
|
|
246
|
+
{typeof val === 'object' ? val.value : val}
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
render(<Test />)
|
|
251
|
+
expect(screen.getByTestId('val')).toHaveTextContent('42')
|
|
252
|
+
act(() => store.setState({ mode: 'obj' }))
|
|
253
|
+
expect(screen.getByTestId('val')).toHaveTextContent('42')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('selector returning null then object — shallowEqual with null', () => {
|
|
257
|
+
const store = createStore<{ data: { x: number } | null }>({ data: null })
|
|
258
|
+
const renderSpy = vi.fn()
|
|
259
|
+
function Test() {
|
|
260
|
+
renderSpy()
|
|
261
|
+
const data = useStore(store, (s) => s.data)
|
|
262
|
+
return (
|
|
263
|
+
<div data-testid="val">
|
|
264
|
+
{data === null ? 'null' : data.x}
|
|
265
|
+
</div>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
render(<Test />)
|
|
269
|
+
expect(screen.getByTestId('val')).toHaveTextContent('null')
|
|
270
|
+
act(() => store.setState({ data: { x: 1 } }))
|
|
271
|
+
expect(screen.getByTestId('val')).toHaveTextContent('1')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('selector returning same object reference — Object.is early return', () => {
|
|
275
|
+
const nested = { x: 1 }
|
|
276
|
+
const store = createStore({ nested, other: 0 })
|
|
277
|
+
const renderSpy = vi.fn()
|
|
278
|
+
function Test() {
|
|
279
|
+
renderSpy()
|
|
280
|
+
const sub = useStore(store, (s) => s.nested)
|
|
281
|
+
return <div data-testid="val">{sub.x}</div>
|
|
282
|
+
}
|
|
283
|
+
render(<Test />)
|
|
284
|
+
expect(screen.getByTestId('val')).toHaveTextContent('1')
|
|
285
|
+
act(() => store.setState({ other: 1 }))
|
|
286
|
+
expect(screen.getByTestId('val')).toHaveTextContent('1')
|
|
287
|
+
})
|
|
288
|
+
})
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen, act } from '@testing-library/react';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import type { Store } from '../../storve/src/types';
|
|
5
|
+
import { createStore } from '../../storve/src/store';
|
|
6
|
+
import { withDevtools } from '../../storve/src/devtools/withDevtools';
|
|
7
|
+
import { useDevtools } from '../src/useDevtools';
|
|
8
|
+
|
|
9
|
+
/** @internal */
|
|
10
|
+
type DevtoolsStore<S extends object> = Store<S> & {
|
|
11
|
+
undo: () => void;
|
|
12
|
+
redo: () => void;
|
|
13
|
+
snapshot: (name: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
function TestComponent({ store }: { store: Store<object> }) {
|
|
18
|
+
const { canUndo, canRedo, history, snapshots } = useDevtools(store);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<div data-testid="can-undo">{String(canUndo)}</div>
|
|
23
|
+
<div data-testid="can-redo">{String(canRedo)}</div>
|
|
24
|
+
<div data-testid="history-count">{history.length}</div>
|
|
25
|
+
<div data-testid="snapshots">{snapshots.join(',')}</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('useDevtools hook', () => {
|
|
31
|
+
it('canRedo becomes true after undo', () => {
|
|
32
|
+
const store = createStore(withDevtools({ count: 0 }, { name: 'test' }));
|
|
33
|
+
|
|
34
|
+
render(<TestComponent store={store} />);
|
|
35
|
+
expect(screen.getByTestId('can-redo').textContent).toBe('false');
|
|
36
|
+
expect(screen.getByTestId('history-count').textContent).toBe('0'); // empty until first state change
|
|
37
|
+
|
|
38
|
+
act(() => {
|
|
39
|
+
store.setState({ count: 1 });
|
|
40
|
+
store.setState({ count: 2 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
act(() => {
|
|
44
|
+
store.undo();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(screen.getByTestId('can-redo').textContent).toBe('true');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('re-renders when undo is called', () => {
|
|
51
|
+
const store = createStore(withDevtools({ count: 0 }, { name: 'test' }));
|
|
52
|
+
act(() => {
|
|
53
|
+
store.setState({ count: 1 });
|
|
54
|
+
store.setState({ count: 2 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
render(<TestComponent store={store} />);
|
|
58
|
+
expect(screen.getByTestId('history-count').textContent).toBe('2');
|
|
59
|
+
|
|
60
|
+
act(() => {
|
|
61
|
+
store.undo();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(screen.getByTestId('can-undo').textContent).toBe('false');
|
|
65
|
+
expect(screen.getByTestId('can-redo').textContent).toBe('true');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('snapshot list updates reactively after snapshot()', () => {
|
|
69
|
+
const store = createStore(withDevtools({ count: 0 }, { name: 'test' }));
|
|
70
|
+
render(<TestComponent store={store} />);
|
|
71
|
+
|
|
72
|
+
expect(screen.getByTestId('snapshots').textContent).toBe('');
|
|
73
|
+
|
|
74
|
+
act(() => {
|
|
75
|
+
(store as unknown as DevtoolsStore<object>).snapshot('s1');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(screen.getByTestId('snapshots').textContent).toBe('s1');
|
|
79
|
+
});
|
|
80
|
+
});
|