Skip to content

Commit e40f969

Browse files
committed
style: fix formatting with prettier
feat(testing): add selective action stubbing support - Add support for include/exclude options in stubActions parameter - Allow stubbing only specific actions or excluding specific actions from stubbing - Maintain backward compatibility with boolean stubActions values - Add comprehensive tests for all selective stubbing scenarios - Update documentation with examples and usage patterns - Fix workspace naming conflict in online-playground package Closes #2970
1 parent 57bec95 commit e40f969

File tree

5 files changed

+353
-5
lines changed

5 files changed

+353
-5
lines changed

packages/docs/cookbook/testing.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,70 @@ store.someAction()
166166
expect(store.someAction).toHaveBeenCalledTimes(1)
167167
```
168168

169+
### Selective action stubbing
170+
171+
Sometimes you may want to stub only specific actions while allowing others to execute normally. You can achieve this by passing an object with `include` or `exclude` arrays to the `stubActions` option:
172+
173+
```js
174+
// Only stub the 'increment' and 'reset' actions
175+
const wrapper = mount(Counter, {
176+
global: {
177+
plugins: [
178+
createTestingPinia({
179+
stubActions: { include: ['increment', 'reset'] }
180+
})
181+
],
182+
},
183+
})
184+
185+
const store = useSomeStore()
186+
187+
// These actions will be stubbed (not executed)
188+
store.increment() // stubbed
189+
store.reset() // stubbed
190+
191+
// Other actions will execute normally but still be spied
192+
store.fetchData() // executed normally
193+
expect(store.fetchData).toHaveBeenCalledTimes(1)
194+
```
195+
196+
Alternatively, you can exclude specific actions from stubbing:
197+
198+
```js
199+
// Stub all actions except 'fetchData'
200+
const wrapper = mount(Counter, {
201+
global: {
202+
plugins: [
203+
createTestingPinia({
204+
stubActions: { exclude: ['fetchData'] }
205+
})
206+
],
207+
},
208+
})
209+
210+
const store = useSomeStore()
211+
212+
// This action will execute normally
213+
store.fetchData() // executed normally
214+
215+
// Other actions will be stubbed
216+
store.increment() // stubbed
217+
store.reset() // stubbed
218+
```
219+
220+
::: tip
221+
If both `include` and `exclude` are provided, `include` takes precedence. If neither is provided or both arrays are empty, all actions will be stubbed (equivalent to `stubActions: true`).
222+
:::
223+
224+
You can also manually mock specific actions after creating the store:
225+
226+
```ts
227+
const store = useSomeStore()
228+
vi.spyOn(store, 'increment').mockImplementation(() => {})
229+
// or if using testing pinia with stubbed actions
230+
store.increment.mockImplementation(() => {})
231+
```
232+
169233
### Mocking the returned value of an action
170234

171235
Actions are automatically spied but type-wise, they are still the regular actions. In order to get the correct type, we must implement a custom type-wrapper that applies the `Mock` type to each action. **This type depends on the testing framework you are using**. Here is an example with Vitest:

packages/docs/zh/cookbook/testing.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,69 @@ store.someAction()
173173
expect(store.someAction).toHaveBeenCalledTimes(1)
174174
```
175175

176-
<!-- TODO: translation -->
176+
### 选择性 action 存根 %{#selective-action-stubbing}%
177+
178+
有时你可能只想存根特定的 action,而让其他 action 正常执行。你可以通过向 `stubActions` 选项传递一个包含 `include``exclude` 数组的对象来实现:
179+
180+
```js
181+
// 只存根 'increment' 和 'reset' action
182+
const wrapper = mount(Counter, {
183+
global: {
184+
plugins: [
185+
createTestingPinia({
186+
stubActions: { include: ['increment', 'reset'] }
187+
})
188+
],
189+
},
190+
})
191+
192+
const store = useSomeStore()
193+
194+
// 这些 action 将被存根(不执行)
195+
store.increment() // 存根
196+
store.reset() // 存根
197+
198+
// 其他 action 将正常执行但仍被监听
199+
store.fetchData() // 正常执行
200+
expect(store.fetchData).toHaveBeenCalledTimes(1)
201+
```
202+
203+
或者,你可以排除特定的 action 不被存根:
204+
205+
```js
206+
// 存根所有 action 除了 'fetchData'
207+
const wrapper = mount(Counter, {
208+
global: {
209+
plugins: [
210+
createTestingPinia({
211+
stubActions: { exclude: ['fetchData'] }
212+
})
213+
],
214+
},
215+
})
216+
217+
const store = useSomeStore()
218+
219+
// 这个 action 将正常执行
220+
store.fetchData() // 正常执行
221+
222+
// 其他 action 将被存根
223+
store.increment() // 存根
224+
store.reset() // 存根
225+
```
226+
227+
::: tip
228+
如果同时提供了 `include``exclude``include` 优先。如果两者都没有提供或两个数组都为空,所有 action 都将被存根(等同于 `stubActions: true`)。
229+
:::
230+
231+
你也可以在创建 store 后手动模拟特定的 action:
232+
233+
```ts
234+
const store = useSomeStore()
235+
vi.spyOn(store, 'increment').mockImplementation(() => {})
236+
// 或者如果使用带有存根 action 的测试 pinia
237+
store.increment.mockImplementation(() => {})
238+
```
177239

178240
### Mocking the returned value of an action
179241

packages/online-playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@pinia/playground",
2+
"name": "@pinia/online-playground",
33
"version": "0.0.0",
44
"type": "module",
55
"private": true,

packages/testing/src/testing.spec.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,25 @@ describe('Testing', () => {
2424
},
2525
})
2626

27+
const useMultiActionStore = defineStore('multi-action', {
28+
state: () => ({ count: 0, value: 0 }),
29+
actions: {
30+
increment() {
31+
this.count++
32+
},
33+
decrement() {
34+
this.count--
35+
},
36+
setValue(newValue: number) {
37+
this.value = newValue
38+
},
39+
reset() {
40+
this.count = 0
41+
this.value = 0
42+
},
43+
},
44+
})
45+
2746
const useCounterSetup = defineStore('counter-setup', () => {
2847
const n = ref(0)
2948
const doubleComputedCallCount = ref(0)
@@ -389,4 +408,188 @@ describe('Testing', () => {
389408
b: { n: 0 },
390409
})
391410
})
411+
412+
describe('selective action stubbing', () => {
413+
it('stubs only included actions', () => {
414+
setActivePinia(
415+
createTestingPinia({
416+
stubActions: { include: ['increment', 'setValue'] },
417+
createSpy: vi.fn,
418+
})
419+
)
420+
421+
const store = useMultiActionStore()
422+
423+
// Included actions should be stubbed (not execute)
424+
store.increment()
425+
expect(store.count).toBe(0) // Should not change
426+
expect(store.increment).toHaveBeenCalledTimes(1)
427+
428+
store.setValue(42)
429+
expect(store.value).toBe(0) // Should not change
430+
expect(store.setValue).toHaveBeenCalledTimes(1)
431+
expect(store.setValue).toHaveBeenLastCalledWith(42)
432+
433+
// Excluded actions should execute normally but still be spied
434+
store.decrement()
435+
expect(store.count).toBe(-1) // Should change
436+
expect(store.decrement).toHaveBeenCalledTimes(1)
437+
438+
store.reset()
439+
expect(store.count).toBe(0) // Should change
440+
expect(store.value).toBe(0) // Should change
441+
expect(store.reset).toHaveBeenCalledTimes(1)
442+
})
443+
444+
it('stubs all actions except excluded ones', () => {
445+
setActivePinia(
446+
createTestingPinia({
447+
stubActions: { exclude: ['increment', 'setValue'] },
448+
createSpy: vi.fn,
449+
})
450+
)
451+
452+
const store = useMultiActionStore()
453+
454+
// Excluded actions should execute normally but still be spied
455+
store.increment()
456+
expect(store.count).toBe(1) // Should change
457+
expect(store.increment).toHaveBeenCalledTimes(1)
458+
459+
store.setValue(42)
460+
expect(store.value).toBe(42) // Should change
461+
expect(store.setValue).toHaveBeenCalledTimes(1)
462+
expect(store.setValue).toHaveBeenLastCalledWith(42)
463+
464+
// Non-excluded actions should be stubbed (not execute)
465+
store.decrement()
466+
expect(store.count).toBe(1) // Should not change
467+
expect(store.decrement).toHaveBeenCalledTimes(1)
468+
469+
store.reset()
470+
expect(store.count).toBe(1) // Should not change
471+
expect(store.value).toBe(42) // Should not change
472+
expect(store.reset).toHaveBeenCalledTimes(1)
473+
})
474+
475+
it('handles empty include array (stubs all actions)', () => {
476+
setActivePinia(
477+
createTestingPinia({
478+
stubActions: { include: [] },
479+
createSpy: vi.fn,
480+
})
481+
)
482+
483+
const store = useMultiActionStore()
484+
485+
store.increment()
486+
expect(store.count).toBe(0) // Should not change
487+
expect(store.increment).toHaveBeenCalledTimes(1)
488+
489+
store.setValue(42)
490+
expect(store.value).toBe(0) // Should not change
491+
expect(store.setValue).toHaveBeenCalledTimes(1)
492+
})
493+
494+
it('handles empty exclude array (stubs all actions)', () => {
495+
setActivePinia(
496+
createTestingPinia({
497+
stubActions: { exclude: [] },
498+
createSpy: vi.fn,
499+
})
500+
)
501+
502+
const store = useMultiActionStore()
503+
504+
store.increment()
505+
expect(store.count).toBe(0) // Should not change
506+
expect(store.increment).toHaveBeenCalledTimes(1)
507+
508+
store.setValue(42)
509+
expect(store.value).toBe(0) // Should not change
510+
expect(store.setValue).toHaveBeenCalledTimes(1)
511+
})
512+
513+
it('handles both include and exclude (include takes precedence)', () => {
514+
setActivePinia(
515+
createTestingPinia({
516+
stubActions: {
517+
include: ['increment'],
518+
exclude: ['increment', 'setValue'],
519+
},
520+
createSpy: vi.fn,
521+
})
522+
)
523+
524+
const store = useMultiActionStore()
525+
526+
// Include takes precedence - increment should be stubbed
527+
store.increment()
528+
expect(store.count).toBe(0) // Should not change
529+
expect(store.increment).toHaveBeenCalledTimes(1)
530+
531+
// Not in include list - should execute normally
532+
store.setValue(42)
533+
expect(store.value).toBe(42) // Should change
534+
expect(store.setValue).toHaveBeenCalledTimes(1)
535+
})
536+
537+
it('maintains backward compatibility with boolean true', () => {
538+
setActivePinia(
539+
createTestingPinia({
540+
stubActions: true,
541+
createSpy: vi.fn,
542+
})
543+
)
544+
545+
const store = useMultiActionStore()
546+
547+
store.increment()
548+
expect(store.count).toBe(0) // Should not change
549+
expect(store.increment).toHaveBeenCalledTimes(1)
550+
551+
store.setValue(42)
552+
expect(store.value).toBe(0) // Should not change
553+
expect(store.setValue).toHaveBeenCalledTimes(1)
554+
})
555+
556+
it('maintains backward compatibility with boolean false', () => {
557+
setActivePinia(
558+
createTestingPinia({
559+
stubActions: false,
560+
createSpy: vi.fn,
561+
})
562+
)
563+
564+
const store = useMultiActionStore()
565+
566+
store.increment()
567+
expect(store.count).toBe(1) // Should change
568+
expect(store.increment).toHaveBeenCalledTimes(1)
569+
570+
store.setValue(42)
571+
expect(store.value).toBe(42) // Should change
572+
expect(store.setValue).toHaveBeenCalledTimes(1)
573+
})
574+
575+
it('handles non-existent action names gracefully', () => {
576+
setActivePinia(
577+
createTestingPinia({
578+
stubActions: { include: ['increment', 'nonExistentAction'] },
579+
createSpy: vi.fn,
580+
})
581+
)
582+
583+
const store = useMultiActionStore()
584+
585+
// Should work normally despite non-existent action in include list
586+
store.increment()
587+
expect(store.count).toBe(0) // Should not change
588+
expect(store.increment).toHaveBeenCalledTimes(1)
589+
590+
store.setValue(42)
591+
expect(store.value).toBe(42) // Should change (not in include list)
592+
expect(store.setValue).toHaveBeenCalledTimes(1)
593+
})
594+
})
392595
})

0 commit comments

Comments
 (0)