Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/docs/cookbook/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,70 @@ store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)
```

### Selective action stubbing

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:

```js
// Only stub the 'increment' and 'reset' actions
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
stubActions: { include: ['increment', 'reset'] }
})
],
},
})

const store = useSomeStore()

// These actions will be stubbed (not executed)
store.increment() // stubbed
store.reset() // stubbed

// Other actions will execute normally but still be spied
store.fetchData() // executed normally
expect(store.fetchData).toHaveBeenCalledTimes(1)
```

Alternatively, you can exclude specific actions from stubbing:

```js
// Stub all actions except 'fetchData'
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
stubActions: { exclude: ['fetchData'] }
})
],
},
})

const store = useSomeStore()

// This action will execute normally
store.fetchData() // executed normally

// Other actions will be stubbed
store.increment() // stubbed
store.reset() // stubbed
```

::: tip
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`).
:::

You can also manually mock specific actions after creating the store:

```ts
const store = useSomeStore()
vi.spyOn(store, 'increment').mockImplementation(() => {})
// or if using testing pinia with stubbed actions
store.increment.mockImplementation(() => {})
```

### Mocking the returned value of an action

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:
Expand Down
64 changes: 63 additions & 1 deletion packages/docs/zh/cookbook/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,69 @@ store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)
```

<!-- TODO: translation -->
### 选择性 action 存根 %{#selective-action-stubbing}%

有时你可能只想存根特定的 action,而让其他 action 正常执行。你可以通过向 `stubActions` 选项传递一个包含 `include` 或 `exclude` 数组的对象来实现:

```js
// 只存根 'increment' 和 'reset' action
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
stubActions: { include: ['increment', 'reset'] }
})
],
},
})

const store = useSomeStore()

// 这些 action 将被存根(不执行)
store.increment() // 存根
store.reset() // 存根

// 其他 action 将正常执行但仍被监听
store.fetchData() // 正常执行
expect(store.fetchData).toHaveBeenCalledTimes(1)
```

或者,你可以排除特定的 action 不被存根:

```js
// 存根所有 action 除了 'fetchData'
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
stubActions: { exclude: ['fetchData'] }
})
],
},
})

const store = useSomeStore()

// 这个 action 将正常执行
store.fetchData() // 正常执行

// 其他 action 将被存根
store.increment() // 存根
store.reset() // 存根
```

::: tip
如果同时提供了 `include` 和 `exclude`,`include` 优先。如果两者都没有提供或两个数组都为空,所有 action 都将被存根(等同于 `stubActions: true`)。
:::

你也可以在创建 store 后手动模拟特定的 action:

```ts
const store = useSomeStore()
vi.spyOn(store, 'increment').mockImplementation(() => {})
// 或者如果使用带有存根 action 的测试 pinia
store.increment.mockImplementation(() => {})
```

### Mocking the returned value of an action

Expand Down
2 changes: 1 addition & 1 deletion packages/online-playground/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@pinia/playground",
"name": "@pinia/online-playground",
"version": "0.0.0",
"type": "module",
"private": true,
Expand Down
203 changes: 203 additions & 0 deletions packages/testing/src/testing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ describe('Testing', () => {
},
})

const useMultiActionStore = defineStore('multi-action', {
state: () => ({ count: 0, value: 0 }),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
setValue(newValue: number) {
this.value = newValue
},
reset() {
this.count = 0
this.value = 0
},
},
})

const useCounterSetup = defineStore('counter-setup', () => {
const n = ref(0)
const doubleComputedCallCount = ref(0)
Expand Down Expand Up @@ -389,4 +408,188 @@ describe('Testing', () => {
b: { n: 0 },
})
})

describe('selective action stubbing', () => {
it('stubs only included actions', () => {
setActivePinia(
createTestingPinia({
stubActions: { include: ['increment', 'setValue'] },
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

// Included actions should be stubbed (not execute)
store.increment()
expect(store.count).toBe(0) // Should not change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(0) // Should not change
expect(store.setValue).toHaveBeenCalledTimes(1)
expect(store.setValue).toHaveBeenLastCalledWith(42)

// Excluded actions should execute normally but still be spied
store.decrement()
expect(store.count).toBe(-1) // Should change
expect(store.decrement).toHaveBeenCalledTimes(1)

store.reset()
expect(store.count).toBe(0) // Should change
expect(store.value).toBe(0) // Should change
expect(store.reset).toHaveBeenCalledTimes(1)
})

it('stubs all actions except excluded ones', () => {
setActivePinia(
createTestingPinia({
stubActions: { exclude: ['increment', 'setValue'] },
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

// Excluded actions should execute normally but still be spied
store.increment()
expect(store.count).toBe(1) // Should change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(42) // Should change
expect(store.setValue).toHaveBeenCalledTimes(1)
expect(store.setValue).toHaveBeenLastCalledWith(42)

// Non-excluded actions should be stubbed (not execute)
store.decrement()
expect(store.count).toBe(1) // Should not change
expect(store.decrement).toHaveBeenCalledTimes(1)

store.reset()
expect(store.count).toBe(1) // Should not change
expect(store.value).toBe(42) // Should not change
expect(store.reset).toHaveBeenCalledTimes(1)
})

it('handles empty include array (stubs all actions)', () => {
setActivePinia(
createTestingPinia({
stubActions: { include: [] },
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

store.increment()
expect(store.count).toBe(0) // Should not change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(0) // Should not change
expect(store.setValue).toHaveBeenCalledTimes(1)
})

it('handles empty exclude array (stubs all actions)', () => {
setActivePinia(
createTestingPinia({
stubActions: { exclude: [] },
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

store.increment()
expect(store.count).toBe(0) // Should not change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(0) // Should not change
expect(store.setValue).toHaveBeenCalledTimes(1)
})

it('handles both include and exclude (include takes precedence)', () => {
setActivePinia(
createTestingPinia({
stubActions: {
include: ['increment'],
exclude: ['increment', 'setValue'],
},
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

// Include takes precedence - increment should be stubbed
store.increment()
expect(store.count).toBe(0) // Should not change
expect(store.increment).toHaveBeenCalledTimes(1)

// Not in include list - should execute normally
store.setValue(42)
expect(store.value).toBe(42) // Should change
expect(store.setValue).toHaveBeenCalledTimes(1)
})

it('maintains backward compatibility with boolean true', () => {
setActivePinia(
createTestingPinia({
stubActions: true,
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

store.increment()
expect(store.count).toBe(0) // Should not change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(0) // Should not change
expect(store.setValue).toHaveBeenCalledTimes(1)
})

it('maintains backward compatibility with boolean false', () => {
setActivePinia(
createTestingPinia({
stubActions: false,
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

store.increment()
expect(store.count).toBe(1) // Should change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(42) // Should change
expect(store.setValue).toHaveBeenCalledTimes(1)
})

it('handles non-existent action names gracefully', () => {
setActivePinia(
createTestingPinia({
stubActions: { include: ['increment', 'nonExistentAction'] },
createSpy: vi.fn,
})
)

const store = useMultiActionStore()

// Should work normally despite non-existent action in include list
store.increment()
expect(store.count).toBe(0) // Should not change
expect(store.increment).toHaveBeenCalledTimes(1)

store.setValue(42)
expect(store.value).toBe(42) // Should change (not in include list)
expect(store.setValue).toHaveBeenCalledTimes(1)
})
})
})
Loading