@pyreon/store 0.0.1
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 +79 -0
- package/lib/analysis/devtools.js.html +5406 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/devtools.js +37 -0
- package/lib/devtools.js.map +1 -0
- package/lib/index.js +225 -0
- package/lib/index.js.map +1 -0
- package/lib/types/devtools.d.ts +34 -0
- package/lib/types/devtools.d.ts.map +1 -0
- package/lib/types/devtools2.d.ts +58 -0
- package/lib/types/devtools2.d.ts.map +1 -0
- package/lib/types/index.d.ts +222 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +73 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/devtools.ts +35 -0
- package/src/index.ts +379 -0
- package/src/registry.ts +26 -0
- package/src/tests/devtools.test.ts +70 -0
- package/src/tests/store.test.ts +592 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type MutationInfo,
|
|
3
|
+
addStorePlugin,
|
|
4
|
+
computed,
|
|
5
|
+
defineStore,
|
|
6
|
+
resetAllStores,
|
|
7
|
+
resetStore,
|
|
8
|
+
setStoreRegistryProvider,
|
|
9
|
+
signal,
|
|
10
|
+
} from '../index'
|
|
11
|
+
|
|
12
|
+
afterEach(() => resetAllStores())
|
|
13
|
+
|
|
14
|
+
describe('defineStore', () => {
|
|
15
|
+
test('returns singleton — setup runs once', () => {
|
|
16
|
+
let runs = 0
|
|
17
|
+
const useStore = defineStore('counter', () => {
|
|
18
|
+
runs++
|
|
19
|
+
const count = signal(0)
|
|
20
|
+
return { count }
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
useStore()
|
|
24
|
+
useStore()
|
|
25
|
+
expect(runs).toBe(1)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('state is shared across calls', () => {
|
|
29
|
+
const useStore = defineStore('shared', () => {
|
|
30
|
+
const count = signal(0)
|
|
31
|
+
return { count }
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const a = useStore()
|
|
35
|
+
const b = useStore()
|
|
36
|
+
a.store.count.set(42)
|
|
37
|
+
expect(b.store.count()).toBe(42)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('supports computed values', () => {
|
|
41
|
+
const useStore = defineStore('computed-store', () => {
|
|
42
|
+
const count = signal(3)
|
|
43
|
+
const double = computed(() => count() * 2)
|
|
44
|
+
return { count, double }
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const { store } = useStore()
|
|
48
|
+
expect(store.double()).toBe(6)
|
|
49
|
+
store.count.set(5)
|
|
50
|
+
expect(store.double()).toBe(10)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('supports actions (plain functions)', () => {
|
|
54
|
+
const useStore = defineStore('actions-store', () => {
|
|
55
|
+
const count = signal(0)
|
|
56
|
+
const increment = () => count.update((n) => n + 1)
|
|
57
|
+
return { count, increment }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const { store } = useStore()
|
|
61
|
+
store.increment()
|
|
62
|
+
store.increment()
|
|
63
|
+
expect(store.count()).toBe(2)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('different ids create independent stores', () => {
|
|
67
|
+
const useA = defineStore('a', () => ({ val: signal(1) }))
|
|
68
|
+
const useB = defineStore('b', () => ({ val: signal(2) }))
|
|
69
|
+
|
|
70
|
+
expect(useA().store.val()).toBe(1)
|
|
71
|
+
expect(useB().store.val()).toBe(2)
|
|
72
|
+
useA().store.val.set(99)
|
|
73
|
+
expect(useB().store.val()).toBe(2)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('same id from different defineStore calls shares state', () => {
|
|
77
|
+
const useA = defineStore('dup', () => ({ val: signal('first') }))
|
|
78
|
+
const useB = defineStore('dup', () => ({ val: signal('second') }))
|
|
79
|
+
|
|
80
|
+
const a = useA()
|
|
81
|
+
const b = useB()
|
|
82
|
+
expect(a).toBe(b)
|
|
83
|
+
expect(a.store.val()).toBe('first')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('resetStore', () => {
|
|
88
|
+
test('re-runs setup after reset', () => {
|
|
89
|
+
let runs = 0
|
|
90
|
+
const useStore = defineStore('resettable', () => {
|
|
91
|
+
runs++
|
|
92
|
+
return { val: signal(runs) }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
useStore()
|
|
96
|
+
resetStore('resettable')
|
|
97
|
+
useStore()
|
|
98
|
+
expect(runs).toBe(2)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('fresh state after reset', () => {
|
|
102
|
+
const useStore = defineStore('fresh', () => ({ count: signal(0) }))
|
|
103
|
+
|
|
104
|
+
useStore().store.count.set(99)
|
|
105
|
+
resetStore('fresh')
|
|
106
|
+
expect(useStore().store.count()).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('resetting non-existent id is a no-op', () => {
|
|
110
|
+
expect(() => resetStore('does-not-exist')).not.toThrow()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('resetAllStores', () => {
|
|
115
|
+
test('clears all registrations', () => {
|
|
116
|
+
let runsA = 0
|
|
117
|
+
let runsB = 0
|
|
118
|
+
const useA = defineStore('all-a', () => {
|
|
119
|
+
runsA++
|
|
120
|
+
return {}
|
|
121
|
+
})
|
|
122
|
+
const useB = defineStore('all-b', () => {
|
|
123
|
+
runsB++
|
|
124
|
+
return {}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
useA()
|
|
128
|
+
useB()
|
|
129
|
+
resetAllStores()
|
|
130
|
+
useA()
|
|
131
|
+
useB()
|
|
132
|
+
expect(runsA).toBe(2)
|
|
133
|
+
expect(runsB).toBe(2)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('setStoreRegistryProvider', () => {
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
// Restore default registry
|
|
140
|
+
setStoreRegistryProvider(() => new Map())
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('custom provider isolates registries', () => {
|
|
144
|
+
const registryA = new Map<string, unknown>()
|
|
145
|
+
const registryB = new Map<string, unknown>()
|
|
146
|
+
|
|
147
|
+
const useStore = defineStore('isolated', () => ({ val: signal(0) }))
|
|
148
|
+
|
|
149
|
+
setStoreRegistryProvider(() => registryA)
|
|
150
|
+
useStore().store.val.set(10)
|
|
151
|
+
|
|
152
|
+
setStoreRegistryProvider(() => registryB)
|
|
153
|
+
expect(useStore().store.val()).toBe(0)
|
|
154
|
+
|
|
155
|
+
setStoreRegistryProvider(() => registryA)
|
|
156
|
+
expect(useStore().store.val()).toBe(10)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('resetAllStores clears current provider registry', () => {
|
|
160
|
+
const custom = new Map<string, unknown>()
|
|
161
|
+
setStoreRegistryProvider(() => custom)
|
|
162
|
+
|
|
163
|
+
const useStore = defineStore('custom-reset', () => ({ val: signal(1) }))
|
|
164
|
+
useStore()
|
|
165
|
+
expect(custom.size).toBe(1)
|
|
166
|
+
|
|
167
|
+
resetAllStores()
|
|
168
|
+
expect(custom.size).toBe(0)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// ─── StoreApi Tests ─────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe('id', () => {
|
|
175
|
+
test('exposes the store id', () => {
|
|
176
|
+
const useStore = defineStore('my-store', () => ({
|
|
177
|
+
count: signal(0),
|
|
178
|
+
}))
|
|
179
|
+
const api = useStore()
|
|
180
|
+
expect(api.id).toBe('my-store')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('state', () => {
|
|
185
|
+
test('returns a plain snapshot of all signal values', () => {
|
|
186
|
+
const useStore = defineStore('state-test', () => ({
|
|
187
|
+
count: signal(10),
|
|
188
|
+
name: signal('Alice'),
|
|
189
|
+
double: computed(() => 20),
|
|
190
|
+
greet: () => 'hello',
|
|
191
|
+
}))
|
|
192
|
+
const api = useStore()
|
|
193
|
+
expect(api.state).toEqual({ count: 10, name: 'Alice' })
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('reflects current values after mutation', () => {
|
|
197
|
+
const useStore = defineStore('state-mut', () => ({
|
|
198
|
+
count: signal(0),
|
|
199
|
+
}))
|
|
200
|
+
const api = useStore()
|
|
201
|
+
api.store.count.set(42)
|
|
202
|
+
expect(api.state).toEqual({ count: 42 })
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('patch', () => {
|
|
207
|
+
test('object form: batch-updates multiple signals', () => {
|
|
208
|
+
const useStore = defineStore('patch-obj', () => ({
|
|
209
|
+
count: signal(0),
|
|
210
|
+
name: signal('Bob'),
|
|
211
|
+
}))
|
|
212
|
+
const api = useStore()
|
|
213
|
+
api.patch({ count: 5, name: 'Alice' })
|
|
214
|
+
expect(api.store.count()).toBe(5)
|
|
215
|
+
expect(api.store.name()).toBe('Alice')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('function form: receives signals for manual updates', () => {
|
|
219
|
+
const useStore = defineStore('patch-fn', () => ({
|
|
220
|
+
count: signal(0),
|
|
221
|
+
name: signal('Bob'),
|
|
222
|
+
}))
|
|
223
|
+
const api = useStore()
|
|
224
|
+
api.patch((state) => {
|
|
225
|
+
state.count.set(10)
|
|
226
|
+
state.name.set('Charlie')
|
|
227
|
+
})
|
|
228
|
+
expect(api.store.count()).toBe(10)
|
|
229
|
+
expect(api.store.name()).toBe('Charlie')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test("emits single subscribe notification with type 'patch'", () => {
|
|
233
|
+
const useStore = defineStore('patch-notify', () => ({
|
|
234
|
+
count: signal(0),
|
|
235
|
+
name: signal('Bob'),
|
|
236
|
+
}))
|
|
237
|
+
const api = useStore()
|
|
238
|
+
const mutations: MutationInfo[] = []
|
|
239
|
+
api.subscribe((mutation) => {
|
|
240
|
+
mutations.push(mutation)
|
|
241
|
+
})
|
|
242
|
+
api.patch({ count: 5, name: 'Alice' })
|
|
243
|
+
expect(mutations).toHaveLength(1)
|
|
244
|
+
expect(mutations[0]!.type).toBe('patch')
|
|
245
|
+
expect(mutations[0]!.events).toHaveLength(2)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('ignores keys that are not signals', () => {
|
|
249
|
+
const useStore = defineStore('patch-ignore', () => ({
|
|
250
|
+
count: signal(0),
|
|
251
|
+
greet: () => 'hello',
|
|
252
|
+
}))
|
|
253
|
+
const api = useStore()
|
|
254
|
+
// Should not throw
|
|
255
|
+
api.patch({ count: 5, greet: 'nope' as any, nonExistent: 99 })
|
|
256
|
+
expect(api.store.count()).toBe(5)
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('subscribe', () => {
|
|
261
|
+
test('fires on direct signal changes', () => {
|
|
262
|
+
const useStore = defineStore('sub-direct', () => ({
|
|
263
|
+
count: signal(0),
|
|
264
|
+
}))
|
|
265
|
+
const api = useStore()
|
|
266
|
+
const mutations: MutationInfo[] = []
|
|
267
|
+
api.subscribe((mutation) => {
|
|
268
|
+
mutations.push(mutation)
|
|
269
|
+
})
|
|
270
|
+
api.store.count.set(5)
|
|
271
|
+
expect(mutations).toHaveLength(1)
|
|
272
|
+
expect(mutations[0]!.type).toBe('direct')
|
|
273
|
+
expect(mutations[0]!.storeId).toBe('sub-direct')
|
|
274
|
+
expect(mutations[0]!.events).toEqual([
|
|
275
|
+
{ key: 'count', oldValue: 0, newValue: 5 },
|
|
276
|
+
])
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
test('provides current state snapshot', () => {
|
|
280
|
+
const useStore = defineStore('sub-state', () => ({
|
|
281
|
+
count: signal(0),
|
|
282
|
+
name: signal('X'),
|
|
283
|
+
}))
|
|
284
|
+
const api = useStore()
|
|
285
|
+
let capturedState: Record<string, unknown> | null = null
|
|
286
|
+
api.subscribe((_mutation, state) => {
|
|
287
|
+
capturedState = state
|
|
288
|
+
})
|
|
289
|
+
api.store.count.set(42)
|
|
290
|
+
expect(capturedState).toEqual({ count: 42, name: 'X' })
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
test('immediate option calls callback right away', () => {
|
|
294
|
+
const useStore = defineStore('sub-immediate', () => ({
|
|
295
|
+
count: signal(7),
|
|
296
|
+
}))
|
|
297
|
+
const api = useStore()
|
|
298
|
+
let called = false
|
|
299
|
+
let capturedState: Record<string, unknown> | null = null
|
|
300
|
+
api.subscribe(
|
|
301
|
+
(_mutation, state) => {
|
|
302
|
+
called = true
|
|
303
|
+
capturedState = state
|
|
304
|
+
},
|
|
305
|
+
{ immediate: true },
|
|
306
|
+
)
|
|
307
|
+
expect(called).toBe(true)
|
|
308
|
+
expect(capturedState).toEqual({ count: 7 })
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('unsubscribe stops notifications', () => {
|
|
312
|
+
const useStore = defineStore('sub-unsub', () => ({
|
|
313
|
+
count: signal(0),
|
|
314
|
+
}))
|
|
315
|
+
const api = useStore()
|
|
316
|
+
let callCount = 0
|
|
317
|
+
const unsub = api.subscribe(() => {
|
|
318
|
+
callCount++
|
|
319
|
+
})
|
|
320
|
+
api.store.count.set(1)
|
|
321
|
+
expect(callCount).toBe(1)
|
|
322
|
+
unsub()
|
|
323
|
+
api.store.count.set(2)
|
|
324
|
+
expect(callCount).toBe(1)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('does not fire if signal is set to same value', () => {
|
|
328
|
+
const useStore = defineStore('sub-same', () => ({
|
|
329
|
+
count: signal(5),
|
|
330
|
+
}))
|
|
331
|
+
const api = useStore()
|
|
332
|
+
let callCount = 0
|
|
333
|
+
api.subscribe(() => {
|
|
334
|
+
callCount++
|
|
335
|
+
})
|
|
336
|
+
api.store.count.set(5) // same value
|
|
337
|
+
expect(callCount).toBe(0)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
describe('onAction', () => {
|
|
342
|
+
test('intercepts action calls with name and args', () => {
|
|
343
|
+
const useStore = defineStore('action-intercept', () => {
|
|
344
|
+
const count = signal(0)
|
|
345
|
+
const add = (n: number) => count.update((c) => c + n)
|
|
346
|
+
return { count, add }
|
|
347
|
+
})
|
|
348
|
+
const api = useStore()
|
|
349
|
+
const calls: { name: string; args: unknown[] }[] = []
|
|
350
|
+
api.onAction(({ name, args }) => {
|
|
351
|
+
calls.push({ name, args })
|
|
352
|
+
})
|
|
353
|
+
api.store.add(5)
|
|
354
|
+
expect(calls).toEqual([{ name: 'add', args: [5] }])
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
test('after callback runs on success', () => {
|
|
358
|
+
const useStore = defineStore('action-after', () => {
|
|
359
|
+
const count = signal(0)
|
|
360
|
+
const getCount = () => count()
|
|
361
|
+
return { count, getCount }
|
|
362
|
+
})
|
|
363
|
+
const api = useStore()
|
|
364
|
+
let result: unknown = null
|
|
365
|
+
api.onAction(({ after }) => {
|
|
366
|
+
after((r) => {
|
|
367
|
+
result = r
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
api.store.count.set(42)
|
|
371
|
+
api.store.getCount()
|
|
372
|
+
expect(result).toBe(42)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('onError callback runs when action throws', () => {
|
|
376
|
+
const useStore = defineStore('action-error', () => {
|
|
377
|
+
const fail = () => {
|
|
378
|
+
throw new Error('boom')
|
|
379
|
+
}
|
|
380
|
+
return { fail }
|
|
381
|
+
})
|
|
382
|
+
const api = useStore()
|
|
383
|
+
let caughtError: unknown = null
|
|
384
|
+
api.onAction(({ onError }) => {
|
|
385
|
+
onError((err) => {
|
|
386
|
+
caughtError = err
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
expect(() => api.store.fail()).toThrow('boom')
|
|
390
|
+
expect(caughtError).toBeInstanceOf(Error)
|
|
391
|
+
expect((caughtError as Error).message).toBe('boom')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
test('storeId is provided in context', () => {
|
|
395
|
+
const useStore = defineStore('action-store-id', () => ({
|
|
396
|
+
noop: () => {
|
|
397
|
+
/* noop */
|
|
398
|
+
},
|
|
399
|
+
}))
|
|
400
|
+
const api = useStore()
|
|
401
|
+
let capturedId: string | null = null
|
|
402
|
+
api.onAction(({ storeId }) => {
|
|
403
|
+
capturedId = storeId
|
|
404
|
+
})
|
|
405
|
+
api.store.noop()
|
|
406
|
+
expect(capturedId).toBe('action-store-id')
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('unsubscribe stops interception', () => {
|
|
410
|
+
const useStore = defineStore('action-unsub', () => ({
|
|
411
|
+
noop: () => {
|
|
412
|
+
/* noop */
|
|
413
|
+
},
|
|
414
|
+
}))
|
|
415
|
+
const api = useStore()
|
|
416
|
+
let callCount = 0
|
|
417
|
+
const unsub = api.onAction(() => {
|
|
418
|
+
callCount++
|
|
419
|
+
})
|
|
420
|
+
api.store.noop()
|
|
421
|
+
expect(callCount).toBe(1)
|
|
422
|
+
unsub()
|
|
423
|
+
api.store.noop()
|
|
424
|
+
expect(callCount).toBe(1)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('after callback receives resolved value for async actions', async () => {
|
|
428
|
+
const useStore = defineStore('action-async', () => {
|
|
429
|
+
const fetchData = async () => {
|
|
430
|
+
await new Promise((r) => setTimeout(r, 5))
|
|
431
|
+
return 'resolved-data'
|
|
432
|
+
}
|
|
433
|
+
return { fetchData }
|
|
434
|
+
})
|
|
435
|
+
const api = useStore()
|
|
436
|
+
let result: unknown = null
|
|
437
|
+
api.onAction(({ after }) => {
|
|
438
|
+
after((r) => {
|
|
439
|
+
result = r
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
await api.store.fetchData()
|
|
443
|
+
expect(result).toBe('resolved-data')
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('onError callback fires for async action rejection', async () => {
|
|
447
|
+
const useStore = defineStore('action-async-error', () => {
|
|
448
|
+
const failAsync = async () => {
|
|
449
|
+
await new Promise((r) => setTimeout(r, 5))
|
|
450
|
+
throw new Error('async boom')
|
|
451
|
+
}
|
|
452
|
+
return { failAsync }
|
|
453
|
+
})
|
|
454
|
+
const api = useStore()
|
|
455
|
+
let caughtError: unknown = null
|
|
456
|
+
api.onAction(({ onError }) => {
|
|
457
|
+
onError((err) => {
|
|
458
|
+
caughtError = err
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
await api.store.failAsync().catch(() => {
|
|
462
|
+
/* expected */
|
|
463
|
+
})
|
|
464
|
+
expect(caughtError).toBeInstanceOf(Error)
|
|
465
|
+
expect((caughtError as Error).message).toBe('async boom')
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
describe('reset', () => {
|
|
470
|
+
test('resets all signals to initial values', () => {
|
|
471
|
+
const useStore = defineStore('reset-test', () => ({
|
|
472
|
+
count: signal(0),
|
|
473
|
+
name: signal('initial'),
|
|
474
|
+
}))
|
|
475
|
+
const api = useStore()
|
|
476
|
+
api.store.count.set(99)
|
|
477
|
+
api.store.name.set('changed')
|
|
478
|
+
api.reset()
|
|
479
|
+
expect(api.store.count()).toBe(0)
|
|
480
|
+
expect(api.store.name()).toBe('initial')
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
test('does not affect computed values (they recompute)', () => {
|
|
484
|
+
const useStore = defineStore('reset-computed', () => {
|
|
485
|
+
const count = signal(5)
|
|
486
|
+
const double = computed(() => count() * 2)
|
|
487
|
+
return { count, double }
|
|
488
|
+
})
|
|
489
|
+
const api = useStore()
|
|
490
|
+
api.store.count.set(20)
|
|
491
|
+
expect(api.store.double()).toBe(40)
|
|
492
|
+
api.reset()
|
|
493
|
+
expect(api.store.count()).toBe(5)
|
|
494
|
+
expect(api.store.double()).toBe(10)
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
describe('dispose', () => {
|
|
499
|
+
test('removes store from registry', () => {
|
|
500
|
+
const useStore = defineStore('dispose-test', () => ({
|
|
501
|
+
count: signal(0),
|
|
502
|
+
}))
|
|
503
|
+
const api = useStore()
|
|
504
|
+
api.dispose()
|
|
505
|
+
// Next call should re-run setup
|
|
506
|
+
const api2 = useStore()
|
|
507
|
+
expect(api2).not.toBe(api)
|
|
508
|
+
expect(api2.store.count()).toBe(0)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
test('clears subscribers after dispose', () => {
|
|
512
|
+
const useStore = defineStore('dispose-sub', () => ({
|
|
513
|
+
count: signal(0),
|
|
514
|
+
}))
|
|
515
|
+
const api = useStore()
|
|
516
|
+
let callCount = 0
|
|
517
|
+
api.subscribe(() => {
|
|
518
|
+
callCount++
|
|
519
|
+
})
|
|
520
|
+
api.store.count.set(1)
|
|
521
|
+
expect(callCount).toBe(1)
|
|
522
|
+
api.dispose()
|
|
523
|
+
// Mutating the old signal should not trigger subscriber
|
|
524
|
+
api.store.count.set(2)
|
|
525
|
+
expect(callCount).toBe(1)
|
|
526
|
+
})
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
describe('addStorePlugin', () => {
|
|
530
|
+
test('plugin receives StoreApi on creation', () => {
|
|
531
|
+
let receivedId: string | null = null
|
|
532
|
+
let receivedApi: any = null
|
|
533
|
+
|
|
534
|
+
addStorePlugin((pluginApi) => {
|
|
535
|
+
receivedId = pluginApi.id
|
|
536
|
+
receivedApi = pluginApi
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
const useStore = defineStore('plugin-test', () => ({
|
|
540
|
+
count: signal(0),
|
|
541
|
+
}))
|
|
542
|
+
const api = useStore()
|
|
543
|
+
|
|
544
|
+
expect(receivedId).toBe('plugin-test')
|
|
545
|
+
expect(receivedApi).toBe(api)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
test('plugin can use subscribe', () => {
|
|
549
|
+
const changes: MutationInfo[] = []
|
|
550
|
+
|
|
551
|
+
addStorePlugin((pluginApi) => {
|
|
552
|
+
pluginApi.subscribe((mutation: MutationInfo) => {
|
|
553
|
+
changes.push(mutation)
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
const useStore = defineStore('plugin-subscribe', () => ({
|
|
558
|
+
count: signal(0),
|
|
559
|
+
}))
|
|
560
|
+
const api = useStore()
|
|
561
|
+
api.store.count.set(5)
|
|
562
|
+
|
|
563
|
+
expect(changes.length).toBeGreaterThanOrEqual(1)
|
|
564
|
+
const relevant = changes.filter((m) => m.storeId === 'plugin-subscribe')
|
|
565
|
+
expect(relevant).toHaveLength(1)
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
test('plugin can use onAction', () => {
|
|
569
|
+
const actionNames: string[] = []
|
|
570
|
+
|
|
571
|
+
addStorePlugin((pluginApi) => {
|
|
572
|
+
pluginApi.onAction(
|
|
573
|
+
({ name, storeId }: { name: string; storeId: string }) => {
|
|
574
|
+
if (storeId === 'plugin-action') {
|
|
575
|
+
actionNames.push(name)
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
const useStore = defineStore('plugin-action', () => ({
|
|
582
|
+
count: signal(0),
|
|
583
|
+
increment: () => {
|
|
584
|
+
/* noop */
|
|
585
|
+
},
|
|
586
|
+
}))
|
|
587
|
+
const api = useStore()
|
|
588
|
+
api.store.increment()
|
|
589
|
+
|
|
590
|
+
expect(actionNames).toContain('increment')
|
|
591
|
+
})
|
|
592
|
+
})
|