@storve/core 1.0.1 → 1.0.3
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/LICENSE +21 -0
- package/README.md +993 -26
- package/dist/adapters/indexedDB.cjs +0 -1
- package/dist/adapters/indexedDB.mjs +0 -1
- package/dist/adapters/localStorage.cjs +0 -1
- package/dist/adapters/localStorage.mjs +0 -1
- package/dist/adapters/memory.cjs +0 -1
- package/dist/adapters/memory.mjs +0 -1
- package/dist/adapters/sessionStorage.cjs +0 -1
- package/dist/adapters/sessionStorage.mjs +0 -1
- package/dist/async-entry.d.ts +0 -1
- package/dist/async.cjs +0 -1
- package/dist/async.d.ts +0 -1
- package/dist/async.mjs +0 -1
- package/dist/batch.d.ts +0 -1
- package/dist/compose.d.ts +0 -1
- package/dist/computed-entry.d.ts +0 -1
- package/dist/computed.cjs +0 -1
- package/dist/computed.d.ts +0 -1
- package/dist/computed.mjs +0 -1
- package/dist/devtools/history.d.ts +0 -1
- package/dist/devtools/index.d.ts +0 -1
- package/dist/devtools/redux-bridge.d.ts +0 -1
- package/dist/devtools/snapshots.d.ts +0 -1
- package/dist/devtools/withDevtools.d.ts +0 -1
- package/dist/devtools.cjs +0 -1
- package/dist/devtools.mjs +0 -1
- package/dist/extensions/noop.d.ts +0 -1
- package/dist/index.cjs +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -1
- package/dist/persist/adapters/indexedDB.d.ts +0 -1
- package/dist/persist/adapters/localStorage.d.ts +0 -1
- package/dist/persist/adapters/memory.d.ts +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts +0 -1
- package/dist/persist/debounce.d.ts +0 -1
- package/dist/persist/hydrate.d.ts +0 -1
- package/dist/persist/index.d.ts +0 -1
- package/dist/persist/serialize.d.ts +0 -1
- package/dist/persist.cjs +0 -1
- package/dist/persist.mjs +0 -1
- package/dist/proxy.d.ts +0 -1
- package/dist/registry-qtr1UpFU.js +0 -1
- package/dist/registry-zaKZ1P-s.js +0 -1
- package/dist/registry.d.ts +0 -1
- package/dist/signals/createSignal.d.ts +0 -1
- package/dist/signals/index.d.ts +0 -1
- package/dist/signals/useSignal.d.ts +0 -1
- package/dist/signals.cjs +0 -1
- package/dist/signals.mjs +0 -1
- package/dist/store.d.ts +0 -1
- package/dist/sync/channel.d.ts +0 -1
- package/dist/sync/index.d.ts +0 -1
- package/dist/sync/protocol.d.ts +0 -1
- package/dist/sync/withSync.d.ts +0 -1
- package/dist/sync.cjs +0 -1
- package/dist/sync.mjs +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +9 -3
- package/CHANGELOG.md +0 -151
- package/benchmarks/run.ts +0 -102
- package/benchmarks/week2.md +0 -9
- package/benchmarks/week2.ts +0 -64
- package/benchmarks/week4.md +0 -13
- package/benchmarks/week4.ts +0 -178
- package/benchmarks/week5.md +0 -15
- package/benchmarks/week5.ts +0 -184
- package/coverage/coverage-summary.json +0 -31
- package/dist/adapters/indexedDB.cjs.map +0 -1
- package/dist/adapters/indexedDB.mjs.map +0 -1
- package/dist/adapters/localStorage.cjs.map +0 -1
- package/dist/adapters/localStorage.mjs.map +0 -1
- package/dist/adapters/memory.cjs.map +0 -1
- package/dist/adapters/memory.mjs.map +0 -1
- package/dist/adapters/sessionStorage.cjs.map +0 -1
- package/dist/adapters/sessionStorage.mjs.map +0 -1
- package/dist/async-entry.d.ts.map +0 -1
- package/dist/async.cjs.map +0 -1
- package/dist/async.d.ts.map +0 -1
- package/dist/async.mjs.map +0 -1
- package/dist/batch.d.ts.map +0 -1
- package/dist/compose.d.ts.map +0 -1
- package/dist/computed-entry.d.ts.map +0 -1
- package/dist/computed.cjs.map +0 -1
- package/dist/computed.d.ts.map +0 -1
- package/dist/computed.mjs.map +0 -1
- package/dist/devtools/history.d.ts.map +0 -1
- package/dist/devtools/index.d.ts.map +0 -1
- package/dist/devtools/redux-bridge.d.ts.map +0 -1
- package/dist/devtools/snapshots.d.ts.map +0 -1
- package/dist/devtools/withDevtools.d.ts.map +0 -1
- package/dist/devtools.cjs.map +0 -1
- package/dist/devtools.mjs.map +0 -1
- package/dist/extensions/noop.d.ts.map +0 -1
- package/dist/index.cjs.js +0 -118
- package/dist/index.cjs.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.esm.js +0 -116
- package/dist/index.esm.js.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/persist/adapters/indexedDB.d.ts.map +0 -1
- package/dist/persist/adapters/localStorage.d.ts.map +0 -1
- package/dist/persist/adapters/memory.d.ts.map +0 -1
- package/dist/persist/adapters/sessionStorage.d.ts.map +0 -1
- package/dist/persist/debounce.d.ts.map +0 -1
- package/dist/persist/hydrate.d.ts.map +0 -1
- package/dist/persist/index.d.ts.map +0 -1
- package/dist/persist/serialize.d.ts.map +0 -1
- package/dist/persist.cjs.map +0 -1
- package/dist/persist.mjs.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/registry-D3X0HSbl.js +0 -26
- package/dist/registry-D3X0HSbl.js.map +0 -1
- package/dist/registry-RDjbeJdx.js +0 -29
- package/dist/registry-RDjbeJdx.js.map +0 -1
- package/dist/registry-qtr1UpFU.js.map +0 -1
- package/dist/registry-zaKZ1P-s.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/signals/createSignal.d.ts.map +0 -1
- package/dist/signals/index.d.ts.map +0 -1
- package/dist/signals/useSignal.d.ts.map +0 -1
- package/dist/signals.cjs.map +0 -1
- package/dist/signals.mjs.map +0 -1
- package/dist/stats.html +0 -4949
- package/dist/store.d.ts.map +0 -1
- package/dist/sync/channel.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/sync/protocol.d.ts.map +0 -1
- package/dist/sync/withSync.d.ts.map +0 -1
- package/dist/sync.cjs.map +0 -1
- package/dist/sync.mjs.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/rollup.config.mjs +0 -44
- package/src/async-entry.ts +0 -6
- package/src/async.ts +0 -240
- package/src/batch.ts +0 -33
- package/src/compose.ts +0 -50
- package/src/computed-entry.ts +0 -6
- package/src/computed.ts +0 -187
- package/src/devtools/history.ts +0 -103
- package/src/devtools/index.ts +0 -5
- package/src/devtools/redux-bridge.ts +0 -70
- package/src/devtools/snapshots.ts +0 -54
- package/src/devtools/withDevtools.ts +0 -196
- package/src/extensions/noop.ts +0 -12
- package/src/index.ts +0 -4
- package/src/persist/adapters/indexedDB.ts +0 -114
- package/src/persist/adapters/localStorage.ts +0 -28
- package/src/persist/adapters/memory.ts +0 -26
- package/src/persist/adapters/sessionStorage.ts +0 -28
- package/src/persist/debounce.ts +0 -28
- package/src/persist/hydrate.ts +0 -60
- package/src/persist/index.ts +0 -141
- package/src/persist/serialize.ts +0 -60
- package/src/proxy.ts +0 -87
- package/src/registry.ts +0 -67
- package/src/signals/createSignal.ts +0 -81
- package/src/signals/index.ts +0 -20
- package/src/signals/useSignal.ts +0 -18
- package/src/store.ts +0 -250
- package/src/sync/channel.ts +0 -15
- package/src/sync/index.ts +0 -3
- package/src/sync/protocol.ts +0 -18
- package/src/sync/withSync.ts +0 -147
- package/src/types.ts +0 -159
- package/tests/async.test.ts +0 -1100
- package/tests/batch.test.ts +0 -41
- package/tests/compose.test.ts +0 -209
- package/tests/computed.test.ts +0 -867
- package/tests/devtools.test.ts +0 -1039
- package/tests/integration/persist.integration.test.ts +0 -258
- package/tests/integration/signals.integration.test.ts +0 -309
- package/tests/integration.test.ts +0 -278
- package/tests/persist/adapters/indexedDB.adapter.test.ts +0 -185
- package/tests/persist/adapters/localStorage.adapter.test.ts +0 -105
- package/tests/persist/adapters/memory.adapter.test.ts +0 -112
- package/tests/persist/adapters/sessionStorage.adapter.test.ts +0 -128
- package/tests/persist/debounce.test.ts +0 -121
- package/tests/persist/hydrate.test.ts +0 -120
- package/tests/persist/migrate.test.ts +0 -208
- package/tests/persist/persist.test.ts +0 -357
- package/tests/persist/serialize.test.ts +0 -128
- package/tests/proxy.test.ts +0 -473
- package/tests/registry.test.ts +0 -67
- package/tests/signals/derived.test.ts +0 -244
- package/tests/signals/inference.test.ts +0 -108
- package/tests/signals/signal.test.ts +0 -348
- package/tests/signals/useSignal.test.tsx +0 -275
- package/tests/store.test.ts +0 -482
- package/tests/stress.test.ts +0 -268
- package/tests/sync.test.ts +0 -576
- package/tests/types.test.ts +0 -32
- package/tests/v0.3.test.ts +0 -813
- package/tree-shake-test.js +0 -1
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -22
- package/vitest_play.ts +0 -7
package/tests/v0.3.test.ts
DELETED
|
@@ -1,813 +0,0 @@
|
|
|
1
|
-
// packages/storve/tests/v0.3.test.ts
|
|
2
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
-
import { createStore } from '../src'
|
|
4
|
-
|
|
5
|
-
// ─────────────────────────────────────────────
|
|
6
|
-
// ACTIONS
|
|
7
|
-
// ─────────────────────────────────────────────
|
|
8
|
-
describe('Actions', () => {
|
|
9
|
-
|
|
10
|
-
describe('State isolation', () => {
|
|
11
|
-
it('actions key is absent from getState()', () => {
|
|
12
|
-
const store = createStore({
|
|
13
|
-
count: 0,
|
|
14
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
15
|
-
})
|
|
16
|
-
expect('actions' in store.getState()).toBe(false)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('individual action names are absent from getState()', () => {
|
|
20
|
-
const store = createStore({
|
|
21
|
-
count: 0,
|
|
22
|
-
actions: {
|
|
23
|
-
increment() {},
|
|
24
|
-
decrement() {},
|
|
25
|
-
reset() {},
|
|
26
|
-
}
|
|
27
|
-
})
|
|
28
|
-
const state = store.getState()
|
|
29
|
-
expect('increment' in state).toBe(false)
|
|
30
|
-
expect('decrement' in state).toBe(false)
|
|
31
|
-
expect('reset' in state).toBe(false)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('getState() returns only data keys', () => {
|
|
35
|
-
const store = createStore({
|
|
36
|
-
count: 0,
|
|
37
|
-
name: 'test',
|
|
38
|
-
actions: { doSomething() {} }
|
|
39
|
-
})
|
|
40
|
-
expect(Object.keys(store.getState()).sort()).toEqual(['count', 'name'].sort())
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('subscribe listener never receives actions in payload', () => {
|
|
44
|
-
const store = createStore({
|
|
45
|
-
count: 0,
|
|
46
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
47
|
-
})
|
|
48
|
-
let received: Record<string, unknown> = {}
|
|
49
|
-
store.subscribe(s => { received = s as Record<string, unknown> })
|
|
50
|
-
store.increment()
|
|
51
|
-
expect('increment' in received).toBe(false)
|
|
52
|
-
expect('actions' in received).toBe(false)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('setState updater fn never receives actions in state arg', () => {
|
|
56
|
-
const store = createStore({
|
|
57
|
-
count: 0,
|
|
58
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
59
|
-
})
|
|
60
|
-
let capturedKeys: string[] = []
|
|
61
|
-
store.setState(s => {
|
|
62
|
-
capturedKeys = Object.keys(s)
|
|
63
|
-
return s
|
|
64
|
-
})
|
|
65
|
-
expect(capturedKeys).not.toContain('actions')
|
|
66
|
-
expect(capturedKeys).not.toContain('increment')
|
|
67
|
-
})
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
describe('Callable behaviour', () => {
|
|
71
|
-
it('action is callable directly on store', () => {
|
|
72
|
-
const store = createStore({
|
|
73
|
-
count: 0,
|
|
74
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
75
|
-
})
|
|
76
|
-
store.increment()
|
|
77
|
-
expect(store.getState().count).toBe(1)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('action updates state correctly', () => {
|
|
81
|
-
const store = createStore({
|
|
82
|
-
count: 0,
|
|
83
|
-
actions: {
|
|
84
|
-
increment() { store.setState(s => ({ count: s.count + 1 })) },
|
|
85
|
-
decrement() { store.setState(s => ({ count: s.count - 1 })) },
|
|
86
|
-
reset() { store.setState({ count: 0 }) },
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
store.increment()
|
|
90
|
-
store.increment()
|
|
91
|
-
store.increment()
|
|
92
|
-
expect(store.getState().count).toBe(3)
|
|
93
|
-
store.decrement()
|
|
94
|
-
expect(store.getState().count).toBe(2)
|
|
95
|
-
store.reset()
|
|
96
|
-
expect(store.getState().count).toBe(0)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('action with single argument works', () => {
|
|
100
|
-
const store = createStore({
|
|
101
|
-
count: 0,
|
|
102
|
-
actions: {
|
|
103
|
-
incrementBy(n: number) { store.setState(s => ({ count: s.count + n })) }
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
store.incrementBy(5)
|
|
107
|
-
expect(store.getState().count).toBe(5)
|
|
108
|
-
store.incrementBy(10)
|
|
109
|
-
expect(store.getState().count).toBe(15)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('action with multiple arguments works', () => {
|
|
113
|
-
const store = createStore({
|
|
114
|
-
items: [] as string[],
|
|
115
|
-
actions: {
|
|
116
|
-
insert(item: string, atStart: boolean) {
|
|
117
|
-
store.setState(s => ({
|
|
118
|
-
items: atStart ? [item, ...s.items] : [...s.items, item]
|
|
119
|
-
}))
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
})
|
|
123
|
-
store.insert('b', false)
|
|
124
|
-
store.insert('a', true)
|
|
125
|
-
expect(store.getState().items).toEqual(['a', 'b'])
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('action with no arguments works', () => {
|
|
129
|
-
const store = createStore({
|
|
130
|
-
toggled: false,
|
|
131
|
-
actions: {
|
|
132
|
-
toggle() { store.setState(s => ({ toggled: !s.toggled })) }
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
store.toggle()
|
|
136
|
-
expect(store.getState().toggled).toBe(true)
|
|
137
|
-
store.toggle()
|
|
138
|
-
expect(store.getState().toggled).toBe(false)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('10 rapid sequential action calls produce correct state', () => {
|
|
142
|
-
const store = createStore({
|
|
143
|
-
count: 0,
|
|
144
|
-
actions: { inc() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
145
|
-
})
|
|
146
|
-
for (let i = 0; i < 10; i++) store.inc()
|
|
147
|
-
expect(store.getState().count).toBe(10)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
describe('Auto-binding', () => {
|
|
152
|
-
it('destructured action works without .bind()', () => {
|
|
153
|
-
const store = createStore({
|
|
154
|
-
count: 0,
|
|
155
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
156
|
-
})
|
|
157
|
-
const { increment } = store
|
|
158
|
-
increment()
|
|
159
|
-
expect(store.getState().count).toBe(1)
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('action passed as callback reference works', () => {
|
|
163
|
-
const store = createStore({
|
|
164
|
-
count: 0,
|
|
165
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
166
|
-
})
|
|
167
|
-
const fn = store.increment
|
|
168
|
-
;[1, 2, 3].forEach(() => fn())
|
|
169
|
-
expect(store.getState().count).toBe(3)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('action assigned to variable and called later works', () => {
|
|
173
|
-
const store = createStore({
|
|
174
|
-
count: 0,
|
|
175
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
176
|
-
})
|
|
177
|
-
const saved = store.increment
|
|
178
|
-
store.setState({ count: 99 })
|
|
179
|
-
saved()
|
|
180
|
-
expect(store.getState().count).toBe(100)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('store.actions object is stable across multiple getState calls', () => {
|
|
184
|
-
const store = createStore({
|
|
185
|
-
count: 0,
|
|
186
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
187
|
-
})
|
|
188
|
-
const a1 = store.actions
|
|
189
|
-
store.increment()
|
|
190
|
-
store.increment()
|
|
191
|
-
const a2 = store.actions
|
|
192
|
-
expect(a1).toBe(a2) // same reference — not recreated
|
|
193
|
-
})
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
describe('Async actions', () => {
|
|
197
|
-
it('async action updates state after resolution', async () => {
|
|
198
|
-
const store = createStore({
|
|
199
|
-
count: 0,
|
|
200
|
-
actions: {
|
|
201
|
-
async incrementAsync() {
|
|
202
|
-
await Promise.resolve()
|
|
203
|
-
store.setState(s => ({ count: s.count + 1 }))
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
await store.incrementAsync()
|
|
208
|
-
expect(store.getState().count).toBe(1)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
it('async action notifies subscribers after completion', async () => {
|
|
212
|
-
const store = createStore({
|
|
213
|
-
data: '',
|
|
214
|
-
actions: {
|
|
215
|
-
async load() {
|
|
216
|
-
await Promise.resolve()
|
|
217
|
-
store.setState({ data: 'loaded' })
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
})
|
|
221
|
-
const listener = vi.fn()
|
|
222
|
-
store.subscribe(listener)
|
|
223
|
-
await store.load()
|
|
224
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('multiple concurrent async actions complete independently', async () => {
|
|
228
|
-
const store = createStore({
|
|
229
|
-
a: 0, b: 0,
|
|
230
|
-
actions: {
|
|
231
|
-
async setA() { await Promise.resolve(); store.setState({ a: 1 }) },
|
|
232
|
-
async setB() { await Promise.resolve(); store.setState({ b: 2 }) },
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
await Promise.all([store.setA(), store.setB()])
|
|
236
|
-
expect(store.getState()).toMatchObject({ a: 1, b: 2 })
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('async action called 3 times sequentially accumulates state', async () => {
|
|
240
|
-
const store = createStore({
|
|
241
|
-
count: 0,
|
|
242
|
-
actions: {
|
|
243
|
-
async inc() {
|
|
244
|
-
await Promise.resolve()
|
|
245
|
-
store.setState(s => ({ count: s.count + 1 }))
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
})
|
|
249
|
-
await store.inc()
|
|
250
|
-
await store.inc()
|
|
251
|
-
await store.inc()
|
|
252
|
-
expect(store.getState().count).toBe(3)
|
|
253
|
-
})
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
describe('Actions + subscribers', () => {
|
|
257
|
-
it('calling action notifies subscribers once', () => {
|
|
258
|
-
const store = createStore({
|
|
259
|
-
count: 0,
|
|
260
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
261
|
-
})
|
|
262
|
-
const listener = vi.fn()
|
|
263
|
-
store.subscribe(listener)
|
|
264
|
-
store.increment()
|
|
265
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
it('calling action N times notifies subscribers N times', () => {
|
|
269
|
-
const store = createStore({
|
|
270
|
-
count: 0,
|
|
271
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
272
|
-
})
|
|
273
|
-
const listener = vi.fn()
|
|
274
|
-
store.subscribe(listener)
|
|
275
|
-
for (let i = 0; i < 5; i++) store.increment()
|
|
276
|
-
expect(listener).toHaveBeenCalledTimes(5)
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
it('unsubscribed listener does not receive action notification', () => {
|
|
280
|
-
const store = createStore({
|
|
281
|
-
count: 0,
|
|
282
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
283
|
-
})
|
|
284
|
-
const listener = vi.fn()
|
|
285
|
-
const unsub = store.subscribe(listener)
|
|
286
|
-
unsub()
|
|
287
|
-
store.increment()
|
|
288
|
-
expect(listener).not.toHaveBeenCalled()
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
it('multiple subscribers all notified on action call', () => {
|
|
292
|
-
const store = createStore({
|
|
293
|
-
count: 0,
|
|
294
|
-
actions: { increment() { store.setState(s => ({ count: s.count + 1 })) } }
|
|
295
|
-
})
|
|
296
|
-
const l1 = vi.fn(), l2 = vi.fn(), l3 = vi.fn()
|
|
297
|
-
store.subscribe(l1)
|
|
298
|
-
store.subscribe(l2)
|
|
299
|
-
store.subscribe(l3)
|
|
300
|
-
store.increment()
|
|
301
|
-
expect(l1).toHaveBeenCalledTimes(1)
|
|
302
|
-
expect(l2).toHaveBeenCalledTimes(1)
|
|
303
|
-
expect(l3).toHaveBeenCalledTimes(1)
|
|
304
|
-
})
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
describe('Edge cases', () => {
|
|
308
|
-
it('store with no actions still works normally', () => {
|
|
309
|
-
const store = createStore({ count: 0 })
|
|
310
|
-
store.setState({ count: 5 })
|
|
311
|
-
expect(store.getState().count).toBe(5)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('store with empty actions object works normally', () => {
|
|
315
|
-
const store = createStore({ count: 0, actions: {} })
|
|
316
|
-
store.setState({ count: 5 })
|
|
317
|
-
expect(store.getState().count).toBe(5)
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
it('action that does not call setState does not notify subscribers', () => {
|
|
321
|
-
const store = createStore({
|
|
322
|
-
count: 0,
|
|
323
|
-
actions: { noop() { /* intentionally empty */ } }
|
|
324
|
-
})
|
|
325
|
-
const listener = vi.fn()
|
|
326
|
-
store.subscribe(listener)
|
|
327
|
-
store.noop()
|
|
328
|
-
expect(listener).not.toHaveBeenCalled()
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
it('action calling setState multiple times notifies subscribers multiple times', () => {
|
|
332
|
-
const store = createStore({
|
|
333
|
-
a: 0, b: 0,
|
|
334
|
-
actions: {
|
|
335
|
-
setboth() {
|
|
336
|
-
store.setState({ a: 1 })
|
|
337
|
-
store.setState({ b: 2 })
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
})
|
|
341
|
-
const listener = vi.fn()
|
|
342
|
-
store.subscribe(listener)
|
|
343
|
-
store.setboth()
|
|
344
|
-
expect(listener).toHaveBeenCalledTimes(2)
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it('100 stores created independently do not share state', () => {
|
|
348
|
-
const stores = Array.from({ length: 100 }, () =>
|
|
349
|
-
createStore({
|
|
350
|
-
count: 0,
|
|
351
|
-
actions: { inc() { stores[0].setState(s => ({ count: s.count + 1 })) } }
|
|
352
|
-
})
|
|
353
|
-
)
|
|
354
|
-
stores[0].setState({ count: 99 })
|
|
355
|
-
expect(stores[1].getState().count).toBe(0)
|
|
356
|
-
expect(stores[99].getState().count).toBe(0)
|
|
357
|
-
})
|
|
358
|
-
})
|
|
359
|
-
})
|
|
360
|
-
|
|
361
|
-
// ─────────────────────────────────────────────
|
|
362
|
-
// IMMER
|
|
363
|
-
// ─────────────────────────────────────────────
|
|
364
|
-
describe('Immer Integration', () => {
|
|
365
|
-
|
|
366
|
-
describe('Basic mutations', () => {
|
|
367
|
-
it('primitive mutation is applied', () => {
|
|
368
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
369
|
-
store.setState(draft => { draft.count = 5 })
|
|
370
|
-
expect(store.getState().count).toBe(5)
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
it('string mutation is applied', () => {
|
|
374
|
-
const store = createStore({ name: 'alice' }, { immer: true })
|
|
375
|
-
store.setState(draft => { draft.name = 'bob' })
|
|
376
|
-
expect(store.getState().name).toBe('bob')
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it('boolean mutation is applied', () => {
|
|
380
|
-
const store = createStore({ active: false }, { immer: true })
|
|
381
|
-
store.setState(draft => { draft.active = true })
|
|
382
|
-
expect(store.getState().active).toBe(true)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('multiple fields mutated in single setState', () => {
|
|
386
|
-
const store = createStore({ a: 0, b: 0, c: 0 }, { immer: true })
|
|
387
|
-
store.setState(draft => { draft.a = 1; draft.b = 2; draft.c = 3 })
|
|
388
|
-
expect(store.getState()).toMatchObject({ a: 1, b: 2, c: 3 })
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
it('sequential mutations accumulate correctly', () => {
|
|
392
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
393
|
-
store.setState(draft => { draft.count++ })
|
|
394
|
-
store.setState(draft => { draft.count++ })
|
|
395
|
-
store.setState(draft => { draft.count++ })
|
|
396
|
-
expect(store.getState().count).toBe(3)
|
|
397
|
-
})
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
describe('Immutability', () => {
|
|
401
|
-
it('original state object is never mutated', () => {
|
|
402
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
403
|
-
const before = store.getState()
|
|
404
|
-
store.setState(draft => { draft.count = 99 })
|
|
405
|
-
expect(before.count).toBe(0)
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
it('new state is a new object reference after mutation', () => {
|
|
409
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
410
|
-
const before = store.getState()
|
|
411
|
-
store.setState(draft => { draft.count = 1 })
|
|
412
|
-
expect(store.getState()).not.toBe(before)
|
|
413
|
-
})
|
|
414
|
-
|
|
415
|
-
it('unchanged fields are preserved across mutations', () => {
|
|
416
|
-
const store = createStore({ a: 1, b: 2, c: 3 }, { immer: true })
|
|
417
|
-
store.setState(draft => { draft.a = 99 })
|
|
418
|
-
expect(store.getState().b).toBe(2)
|
|
419
|
-
expect(store.getState().c).toBe(3)
|
|
420
|
-
})
|
|
421
|
-
})
|
|
422
|
-
|
|
423
|
-
describe('Nested state', () => {
|
|
424
|
-
it('nested object field mutation works', () => {
|
|
425
|
-
const store = createStore({ user: { name: 'Alice', age: 30 } }, { immer: true })
|
|
426
|
-
store.setState(draft => { draft.user.age = 31 })
|
|
427
|
-
expect(store.getState().user.age).toBe(31)
|
|
428
|
-
expect(store.getState().user.name).toBe('Alice')
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
it('deeply nested mutation works', () => {
|
|
432
|
-
const store = createStore({ a: { b: { c: { value: 0 } } } }, { immer: true })
|
|
433
|
-
store.setState(draft => { draft.a.b.c.value = 42 })
|
|
434
|
-
expect(store.getState().a.b.c.value).toBe(42)
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('multiple nested fields mutated independently', () => {
|
|
438
|
-
const store = createStore({
|
|
439
|
-
config: { theme: 'light', lang: 'en', debug: false }
|
|
440
|
-
}, { immer: true })
|
|
441
|
-
store.setState(draft => {
|
|
442
|
-
draft.config.theme = 'dark'
|
|
443
|
-
draft.config.debug = true
|
|
444
|
-
})
|
|
445
|
-
expect(store.getState().config.theme).toBe('dark')
|
|
446
|
-
expect(store.getState().config.debug).toBe(true)
|
|
447
|
-
expect(store.getState().config.lang).toBe('en')
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
it('nested object is replaced entirely', () => {
|
|
451
|
-
const store = createStore({ user: { name: 'Alice', age: 30 } }, { immer: true })
|
|
452
|
-
store.setState(draft => {
|
|
453
|
-
draft.user = { name: 'Bob', age: 25 }
|
|
454
|
-
})
|
|
455
|
-
expect(store.getState().user).toEqual({ name: 'Bob', age: 25 })
|
|
456
|
-
})
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
describe('Array operations', () => {
|
|
460
|
-
it('array push works', () => {
|
|
461
|
-
const store = createStore({ items: [1, 2, 3] }, { immer: true })
|
|
462
|
-
store.setState(draft => { draft.items.push(4) })
|
|
463
|
-
expect(store.getState().items).toEqual([1, 2, 3, 4])
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
it('array pop works', () => {
|
|
467
|
-
const store = createStore({ items: [1, 2, 3] }, { immer: true })
|
|
468
|
-
store.setState(draft => { draft.items.pop() })
|
|
469
|
-
expect(store.getState().items).toEqual([1, 2])
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
it('array filter works', () => {
|
|
473
|
-
const store = createStore({ items: [1, 2, 3, 4] }, { immer: true })
|
|
474
|
-
store.setState(draft => {
|
|
475
|
-
draft.items = draft.items.filter(i => i % 2 === 0)
|
|
476
|
-
})
|
|
477
|
-
expect(store.getState().items).toEqual([2, 4])
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
it('array splice works', () => {
|
|
481
|
-
const store = createStore({ items: ['a', 'b', 'c'] }, { immer: true })
|
|
482
|
-
store.setState(draft => { draft.items.splice(1, 1) })
|
|
483
|
-
expect(store.getState().items).toEqual(['a', 'c'])
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
it('array item property mutation works', () => {
|
|
487
|
-
type Todo = { id: number; done: boolean }
|
|
488
|
-
const store = createStore({
|
|
489
|
-
todos: [{ id: 1, done: false }, { id: 2, done: false }] as Todo[]
|
|
490
|
-
}, { immer: true })
|
|
491
|
-
store.setState(draft => {
|
|
492
|
-
const t = draft.todos.find(t => t.id === 1)
|
|
493
|
-
if (t) t.done = true
|
|
494
|
-
})
|
|
495
|
-
expect(store.getState().todos[0].done).toBe(true)
|
|
496
|
-
expect(store.getState().todos[1].done).toBe(false)
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
it('array unshift works', () => {
|
|
500
|
-
const store = createStore({ items: [2, 3] }, { immer: true })
|
|
501
|
-
store.setState(draft => { draft.items.unshift(1) })
|
|
502
|
-
expect(store.getState().items).toEqual([1, 2, 3])
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
it('array sort works', () => {
|
|
506
|
-
const store = createStore({ items: [3, 1, 2] }, { immer: true })
|
|
507
|
-
store.setState(draft => { draft.items.sort((a, b) => a - b) })
|
|
508
|
-
expect(store.getState().items).toEqual([1, 2, 3])
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
it('array cleared by reassignment works', () => {
|
|
512
|
-
const store = createStore({ items: [1, 2, 3] }, { immer: true })
|
|
513
|
-
store.setState(draft => { draft.items = [] })
|
|
514
|
-
expect(store.getState().items).toEqual([])
|
|
515
|
-
})
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
describe('setState form compatibility', () => {
|
|
519
|
-
it('plain object setState still works when immer: true', () => {
|
|
520
|
-
const store = createStore({ count: 0, name: 'a' }, { immer: true })
|
|
521
|
-
store.setState({ count: 5 })
|
|
522
|
-
expect(store.getState().count).toBe(5)
|
|
523
|
-
expect(store.getState().name).toBe('a')
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
it('updater function returning new state works when immer: true', () => {
|
|
527
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
528
|
-
store.setState(s => ({ count: s.count + 10 }))
|
|
529
|
-
expect(store.getState().count).toBe(10)
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
it('immer mutator notifies subscribers', () => {
|
|
533
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
534
|
-
const listener = vi.fn()
|
|
535
|
-
store.subscribe(listener)
|
|
536
|
-
store.setState(draft => { draft.count++ })
|
|
537
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
538
|
-
})
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
describe('Immer disabled (default)', () => {
|
|
542
|
-
it('plain updater works without immer option', () => {
|
|
543
|
-
const store = createStore({ count: 0 })
|
|
544
|
-
store.setState(s => ({ count: s.count + 1 }))
|
|
545
|
-
expect(store.getState().count).toBe(1)
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
it('plain object setState works without immer option', () => {
|
|
549
|
-
const store = createStore({ count: 0 })
|
|
550
|
-
store.setState({ count: 42 })
|
|
551
|
-
expect(store.getState().count).toBe(42)
|
|
552
|
-
})
|
|
553
|
-
})
|
|
554
|
-
|
|
555
|
-
describe('Immer + Actions', () => {
|
|
556
|
-
it('action uses immer mutation style', () => {
|
|
557
|
-
type Todo = { id: number; text: string; done: boolean }
|
|
558
|
-
const store = createStore({
|
|
559
|
-
todos: [] as Todo[],
|
|
560
|
-
actions: {
|
|
561
|
-
add(text: string) {
|
|
562
|
-
store.setState(draft => {
|
|
563
|
-
draft.todos.push({ id: 1, text, done: false })
|
|
564
|
-
})
|
|
565
|
-
},
|
|
566
|
-
toggle(id: number) {
|
|
567
|
-
store.setState(draft => {
|
|
568
|
-
const t = draft.todos.find(t => t.id === id)
|
|
569
|
-
if (t) t.done = !t.done
|
|
570
|
-
})
|
|
571
|
-
},
|
|
572
|
-
remove(id: number) {
|
|
573
|
-
store.setState(draft => {
|
|
574
|
-
draft.todos = draft.todos.filter(t => t.id !== id)
|
|
575
|
-
})
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
}, { immer: true })
|
|
579
|
-
|
|
580
|
-
store.add('Buy milk')
|
|
581
|
-
expect(store.getState().todos).toHaveLength(1)
|
|
582
|
-
store.toggle(1)
|
|
583
|
-
expect(store.getState().todos[0].done).toBe(true)
|
|
584
|
-
store.remove(1)
|
|
585
|
-
expect(store.getState().todos).toHaveLength(0)
|
|
586
|
-
})
|
|
587
|
-
|
|
588
|
-
it('action using immer does not mutate previous state snapshots', () => {
|
|
589
|
-
const store = createStore({ count: 0 }, { immer: true })
|
|
590
|
-
const snapshots: number[] = []
|
|
591
|
-
store.subscribe(s => snapshots.push((s as { count: number }).count))
|
|
592
|
-
store.setState(draft => { draft.count = 1 })
|
|
593
|
-
store.setState(draft => { draft.count = 2 })
|
|
594
|
-
store.setState(draft => { draft.count = 3 })
|
|
595
|
-
expect(snapshots).toEqual([1, 2, 3])
|
|
596
|
-
})
|
|
597
|
-
})
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
// ─────────────────────────────────────────────
|
|
601
|
-
// BATCH UPDATES
|
|
602
|
-
// ─────────────────────────────────────────────
|
|
603
|
-
describe('Batch Updates', () => {
|
|
604
|
-
|
|
605
|
-
describe('Notification count', () => {
|
|
606
|
-
it('3 setState calls inside batch fire exactly 1 notification', () => {
|
|
607
|
-
const store = createStore({ a: 0, b: 0, c: 0 })
|
|
608
|
-
const listener = vi.fn()
|
|
609
|
-
store.subscribe(listener)
|
|
610
|
-
store.batch(() => {
|
|
611
|
-
store.setState({ a: 1 })
|
|
612
|
-
store.setState({ b: 2 })
|
|
613
|
-
store.setState({ c: 3 })
|
|
614
|
-
})
|
|
615
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
it('10 setState calls inside batch fire exactly 1 notification', () => {
|
|
619
|
-
const store = createStore({
|
|
620
|
-
v0:0,v1:0,v2:0,v3:0,v4:0,v5:0,v6:0,v7:0,v8:0,v9:0
|
|
621
|
-
})
|
|
622
|
-
const listener = vi.fn()
|
|
623
|
-
store.subscribe(listener)
|
|
624
|
-
store.batch(() => {
|
|
625
|
-
for (let i = 0; i < 10; i++) store.setState({ v0: i })
|
|
626
|
-
})
|
|
627
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
628
|
-
})
|
|
629
|
-
|
|
630
|
-
it('single setState inside batch fires exactly 1 notification', () => {
|
|
631
|
-
const store = createStore({ count: 0 })
|
|
632
|
-
const listener = vi.fn()
|
|
633
|
-
store.subscribe(listener)
|
|
634
|
-
store.batch(() => { store.setState({ count: 1 }) })
|
|
635
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
636
|
-
})
|
|
637
|
-
|
|
638
|
-
it('empty batch fires 0 notifications', () => {
|
|
639
|
-
const store = createStore({ count: 0 })
|
|
640
|
-
const listener = vi.fn()
|
|
641
|
-
store.subscribe(listener)
|
|
642
|
-
store.batch(() => {})
|
|
643
|
-
expect(listener).toHaveBeenCalledTimes(0)
|
|
644
|
-
})
|
|
645
|
-
|
|
646
|
-
it('outside batch, 3 setState calls fire 3 notifications', () => {
|
|
647
|
-
const store = createStore({ a: 0, b: 0, c: 0 })
|
|
648
|
-
const listener = vi.fn()
|
|
649
|
-
store.subscribe(listener)
|
|
650
|
-
store.setState({ a: 1 })
|
|
651
|
-
store.setState({ b: 2 })
|
|
652
|
-
store.setState({ c: 3 })
|
|
653
|
-
expect(listener).toHaveBeenCalledTimes(3)
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
it('multiple subscribers each receive exactly 1 notification from batch', () => {
|
|
657
|
-
const store = createStore({ a: 0, b: 0 })
|
|
658
|
-
const l1 = vi.fn(), l2 = vi.fn(), l3 = vi.fn()
|
|
659
|
-
store.subscribe(l1)
|
|
660
|
-
store.subscribe(l2)
|
|
661
|
-
store.subscribe(l3)
|
|
662
|
-
store.batch(() => {
|
|
663
|
-
store.setState({ a: 1 })
|
|
664
|
-
store.setState({ b: 2 })
|
|
665
|
-
})
|
|
666
|
-
expect(l1).toHaveBeenCalledTimes(1)
|
|
667
|
-
expect(l2).toHaveBeenCalledTimes(1)
|
|
668
|
-
expect(l3).toHaveBeenCalledTimes(1)
|
|
669
|
-
})
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
describe('State correctness', () => {
|
|
673
|
-
it('all changes from batch are visible after batch completes', () => {
|
|
674
|
-
const store = createStore({ a: 0, b: 0, c: 0 })
|
|
675
|
-
store.batch(() => {
|
|
676
|
-
store.setState({ a: 1 })
|
|
677
|
-
store.setState({ b: 2 })
|
|
678
|
-
store.setState({ c: 3 })
|
|
679
|
-
})
|
|
680
|
-
expect(store.getState()).toMatchObject({ a: 1, b: 2, c: 3 })
|
|
681
|
-
})
|
|
682
|
-
|
|
683
|
-
it('subscriber receives final merged state from batch', () => {
|
|
684
|
-
const store = createStore({ a: 0, b: 0, c: 0 })
|
|
685
|
-
let received: Record<string, number> = {}
|
|
686
|
-
store.subscribe(s => { received = s as Record<string, number> })
|
|
687
|
-
store.batch(() => {
|
|
688
|
-
store.setState({ a: 1 })
|
|
689
|
-
store.setState({ b: 2 })
|
|
690
|
-
store.setState({ c: 3 })
|
|
691
|
-
})
|
|
692
|
-
expect(received).toMatchObject({ a: 1, b: 2, c: 3 })
|
|
693
|
-
})
|
|
694
|
-
|
|
695
|
-
it('later setState in batch overwrites earlier one for same key', () => {
|
|
696
|
-
const store = createStore({ count: 0 })
|
|
697
|
-
store.batch(() => {
|
|
698
|
-
store.setState({ count: 1 })
|
|
699
|
-
store.setState({ count: 2 })
|
|
700
|
-
store.setState({ count: 3 })
|
|
701
|
-
})
|
|
702
|
-
expect(store.getState().count).toBe(3)
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
it('updater functions in batch receive correct intermediate state', () => {
|
|
706
|
-
const store = createStore({ count: 10 })
|
|
707
|
-
store.batch(() => {
|
|
708
|
-
store.setState(s => ({ count: s.count + 1 }))
|
|
709
|
-
store.setState(s => ({ count: s.count + 1 }))
|
|
710
|
-
store.setState(s => ({ count: s.count + 1 }))
|
|
711
|
-
})
|
|
712
|
-
expect(store.getState().count).toBe(13)
|
|
713
|
-
})
|
|
714
|
-
|
|
715
|
-
it('state during batch is not visible to outside code mid-batch', () => {
|
|
716
|
-
const store = createStore({ count: 0 })
|
|
717
|
-
const snapshots: number[] = []
|
|
718
|
-
store.subscribe(s => snapshots.push((s as { count: number }).count))
|
|
719
|
-
store.batch(() => {
|
|
720
|
-
store.setState({ count: 1 })
|
|
721
|
-
store.setState({ count: 2 })
|
|
722
|
-
store.setState({ count: 3 })
|
|
723
|
-
})
|
|
724
|
-
// Only the final value should have been published
|
|
725
|
-
expect(snapshots).toEqual([3])
|
|
726
|
-
})
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
describe('Nested batch', () => {
|
|
730
|
-
it('nested batch results in 1 total notification', () => {
|
|
731
|
-
const store = createStore({ a: 0, b: 0 })
|
|
732
|
-
const listener = vi.fn()
|
|
733
|
-
store.subscribe(listener)
|
|
734
|
-
store.batch(() => {
|
|
735
|
-
store.setState({ a: 1 })
|
|
736
|
-
store.batch(() => {
|
|
737
|
-
store.setState({ b: 2 })
|
|
738
|
-
})
|
|
739
|
-
})
|
|
740
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
it('3-level nested batch results in 1 total notification', () => {
|
|
744
|
-
const store = createStore({ a: 0, b: 0, c: 0 })
|
|
745
|
-
const listener = vi.fn()
|
|
746
|
-
store.subscribe(listener)
|
|
747
|
-
store.batch(() => {
|
|
748
|
-
store.setState({ a: 1 })
|
|
749
|
-
store.batch(() => {
|
|
750
|
-
store.setState({ b: 2 })
|
|
751
|
-
store.batch(() => {
|
|
752
|
-
store.setState({ c: 3 })
|
|
753
|
-
})
|
|
754
|
-
})
|
|
755
|
-
})
|
|
756
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
757
|
-
expect(store.getState()).toMatchObject({ a: 1, b: 2, c: 3 })
|
|
758
|
-
})
|
|
759
|
-
})
|
|
760
|
-
|
|
761
|
-
describe('Batch + Actions', () => {
|
|
762
|
-
it('multiple actions in batch fire 1 notification', () => {
|
|
763
|
-
const store = createStore({
|
|
764
|
-
count: 0,
|
|
765
|
-
name: 'a',
|
|
766
|
-
actions: {
|
|
767
|
-
setCount(n: number) { store.setState({ count: n }) },
|
|
768
|
-
setName(n: string) { store.setState({ name: n }) },
|
|
769
|
-
}
|
|
770
|
-
})
|
|
771
|
-
const listener = vi.fn()
|
|
772
|
-
store.subscribe(listener)
|
|
773
|
-
store.batch(() => {
|
|
774
|
-
store.setCount(5)
|
|
775
|
-
store.setName('z')
|
|
776
|
-
})
|
|
777
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
778
|
-
expect(store.getState()).toMatchObject({ count: 5, name: 'z' })
|
|
779
|
-
})
|
|
780
|
-
})
|
|
781
|
-
|
|
782
|
-
describe('Batch + Immer', () => {
|
|
783
|
-
it('immer mutations inside batch fire 1 notification', () => {
|
|
784
|
-
const store = createStore({ a: 0, b: 0 }, { immer: true })
|
|
785
|
-
const listener = vi.fn()
|
|
786
|
-
store.subscribe(listener)
|
|
787
|
-
store.batch(() => {
|
|
788
|
-
store.setState(draft => { draft.a = 1 })
|
|
789
|
-
store.setState(draft => { draft.b = 2 })
|
|
790
|
-
})
|
|
791
|
-
expect(listener).toHaveBeenCalledTimes(1)
|
|
792
|
-
expect(store.getState()).toMatchObject({ a: 1, b: 2 })
|
|
793
|
-
})
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
describe('Batch error handling', () => {
|
|
797
|
-
it('batchCount resets to 0 if batch fn throws', () => {
|
|
798
|
-
const store = createStore({ count: 0 })
|
|
799
|
-
const listener = vi.fn()
|
|
800
|
-
store.subscribe(listener)
|
|
801
|
-
expect(() => {
|
|
802
|
-
store.batch(() => {
|
|
803
|
-
store.setState({ count: 1 })
|
|
804
|
-
throw new Error('intentional error')
|
|
805
|
-
})
|
|
806
|
-
}).toThrow('intentional error')
|
|
807
|
-
// After throw, store should still be functional
|
|
808
|
-
store.setState({ count: 99 })
|
|
809
|
-
expect(listener).toHaveBeenCalled()
|
|
810
|
-
expect(store.getState().count).toBe(99)
|
|
811
|
-
})
|
|
812
|
-
})
|
|
813
|
-
})
|