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 array of action names to the `stubActions` option:
+
+```js
+// Only stub the 'increment' and 'reset' actions
+const wrapper = mount(Counter, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ stubActions: ['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)
+```
+
+For more complex scenarios, you can pass a function that receives the action name and store instance, and returns whether the action should be stubbed:
+
+```js
+// Stub actions based on custom logic
+const wrapper = mount(Counter, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ stubActions: (actionName, store) => {
+ // Stub all actions that start with 'set'
+ if (actionName.startsWith('set')) return true
+
+ // Stub actions based on initial store state
+ if (store.isPremium) return false
+
+ return true
+ },
+ }),
+ ],
+ },
+})
+
+const store = useSomeStore()
+
+// Actions starting with 'set' are stubbed
+store.setValue(42) // stubbed
+
+// Other actions may execute based on the initial store state
+store.fetchData() // executed or stubbed based on initial store.isPremium
+```
+
+::: tip
+
+- An empty array `[]` means no actions will be stubbed (same as `false`)
+- The function is evaluated once at store setup time, receiving the store instance in its initial state
+
+:::
+
+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:
expect(store.someAction).toHaveBeenCalledTimes(1)
```
-<!-- TODO: translation -->
+### 选择性 action 存根 %{#selective-action-stubbing}%
+
+有时你可能只想存根特定的 action,而让其他 action 正常执行。你可以通过向 `stubActions` 选项传递一个 action 名称数组来实现:
+
+```js
+// 只存根 'increment' 和 'reset' action
+const wrapper = mount(Counter, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ stubActions: ['increment', 'reset'],
+ }),
+ ],
+ },
+})
+
+const store = useSomeStore()
+
+// 这些 action 将被存根(不执行)
+store.increment() // 存根
+store.reset() // 存根
+
+// 其他 action 将正常执行但仍被监听
+store.fetchData() // 正常执行
+expect(store.fetchData).toHaveBeenCalledTimes(1)
+```
+
+对于更复杂的场景,你可以传递一个函数,该函数接收 action 名称和 store 实例,并返回是否应该存根该 action:
+
+```js
+// 基于自定义逻辑存根 action
+const wrapper = mount(Counter, {
+ global: {
+ plugins: [
+ createTestingPinia({
+ stubActions: (actionName, store) => {
+ // 存根所有以 'set' 开头的 action
+ if (actionName.startsWith('set')) return true
+
+ // 根据初始 store 状态存根 action
+ if (store.isPremium) return false
+
+ return true
+ },
+ }),
+ ],
+ },
+})
+
+const store = useSomeStore()
+
+// 以 'set' 开头的 action 被存根
+store.setValue(42) // 存根
+
+// 其他 action 可能根据初始 store 状态执行
+store.fetchData() // 根据初始 store.isPremium 执行或存根
+```
+
+::: tip
+
+- 空数组 `[]` 表示不存根任何 action(与 `false` 相同)
+- 函数在 store 设置时被评估一次,接收处于初始状态的 store 实例
+
+:::
+
+你也可以在创建 store 后手动模拟特定的 action:
+
+```ts
+const store = useSomeStore()
+vi.spyOn(store, 'increment').mockImplementation(() => {})
+// 或者如果使用带有存根 action 的测试 pinia
+store.increment.mockImplementation(() => {})
+```
### Mocking the returned value of an action
increment(amount = 1) {
this.n += amount
},
+ decrement() {
+ this.n--
+ },
+ setValue(newValue: number) {
+ this.n = newValue
+ },
},
})
function increment(amount = 1) {
n.value += amount
}
+ function decrement() {
+ n.value--
+ }
+ function setValue(newValue: number) {
+ n.value = newValue
+ }
function $reset() {
n.value = 0
}
double,
doublePlusOne,
increment,
+ decrement,
+ setValue,
$reset,
}
})
storeToRefs(store)
expect(store.doubleComputedCallCount).toBe(0)
})
+
+ describe('selective action stubbing', () => {
+ it('stubs only actions in array', () => {
+ setActivePinia(
+ createTestingPinia({
+ stubActions: ['increment', 'setValue'],
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ // Actions in array should be stubbed (not execute)
+ store.increment()
+ expect(store.n).toBe(0) // Should not change
+ expect(store.increment).toHaveBeenCalledTimes(1)
+
+ store.setValue(42)
+ expect(store.n).toBe(0) // Should not change
+ expect(store.setValue).toHaveBeenCalledTimes(1)
+ expect(store.setValue).toHaveBeenLastCalledWith(42)
+
+ // Actions not in array should execute normally but still be spied
+ store.decrement()
+ expect(store.n).toBe(-1) // Should change
+ expect(store.decrement).toHaveBeenCalledTimes(1)
+ })
+
+ it('handles empty array (same as false)', () => {
+ setActivePinia(
+ createTestingPinia({
+ stubActions: [],
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ // All actions should execute normally
+ store.increment()
+ expect(store.n).toBe(1) // Should change
+ expect(store.increment).toHaveBeenCalledTimes(1)
+
+ store.setValue(42)
+ expect(store.n).toBe(42) // Should change
+ expect(store.setValue).toHaveBeenCalledTimes(1)
+ })
+
+ it('handles non-existent action names gracefully', () => {
+ setActivePinia(
+ createTestingPinia({
+ stubActions: ['increment', 'nonExistentAction'],
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ // Should work normally despite non-existent action in array
+ store.increment()
+ expect(store.n).toBe(0) // Should not change
+ expect(store.increment).toHaveBeenCalledTimes(1)
+
+ store.setValue(42)
+ expect(store.n).toBe(42) // Should change (not in array)
+ expect(store.setValue).toHaveBeenCalledTimes(1)
+ })
+
+ it('stubs actions based on function predicate', () => {
+ setActivePinia(
+ createTestingPinia({
+ stubActions: (actionName) =>
+ actionName.startsWith('set') || actionName === 'decrement',
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ // setValue should be stubbed (starts with 'set')
+ store.setValue(42)
+ expect(store.n).toBe(0) // Should not change
+ expect(store.setValue).toHaveBeenCalledTimes(1)
+
+ // increment should execute (doesn't match predicate)
+ store.increment()
+ expect(store.n).toBe(1) // Should change
+ expect(store.increment).toHaveBeenCalledTimes(1)
+
+ // decrement should be stubbed (matches predicate)
+ store.decrement()
+ expect(store.n).toBe(1) // Should not change (stubbed)
+ expect(store.decrement).toHaveBeenCalledTimes(1)
+ })
+
+ it('function predicate receives correct store instance', () => {
+ const predicateSpy = vi.fn(() => false)
+
+ setActivePinia(
+ createTestingPinia({
+ stubActions: predicateSpy,
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ expect(predicateSpy).toHaveBeenCalledWith('increment', store)
+ })
+
+ it('can stub all actions (default)', () => {
+ setActivePinia(
+ createTestingPinia({
+ stubActions: true,
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ store.increment()
+ expect(store.n).toBe(0) // Should not change
+ expect(store.increment).toHaveBeenCalledTimes(1)
+
+ store.setValue(42)
+ expect(store.n).toBe(0) // Should not change
+ expect(store.setValue).toHaveBeenCalledTimes(1)
+ })
+
+ it('can not stub any action', () => {
+ setActivePinia(
+ createTestingPinia({
+ stubActions: false,
+ createSpy: vi.fn,
+ })
+ )
+
+ const store = useStore()
+
+ store.increment()
+ expect(store.n).toBe(1) // Should change
+ expect(store.increment).toHaveBeenCalledTimes(1)
+
+ store.setValue(42)
+ expect(store.n).toBe(42) // Should change
+ expect(store.setValue).toHaveBeenCalledTimes(1)
+ })
+ })
}
it('works with no actions', () => {
import { computed, createApp, isReactive, isRef, toRaw, triggerRef } from 'vue'
import type { App, ComputedRef, WritableComputedRef } from 'vue'
import {
- Pinia,
- PiniaPlugin,
+ type Pinia,
+ type PiniaPlugin,
setActivePinia,
createPinia,
- StateTree,
- _DeepPartial,
- PiniaPluginContext,
+ type StateTree,
+ type _DeepPartial,
+ type PiniaPluginContext,
+ type StoreGeneric,
} from 'pinia'
// NOTE: the implementation type is correct and contains up to date types
// while the other types hide internal properties
/**
* When set to false, actions are only spied, but they will still get executed. When
- * set to true, actions will be replaced with spies, resulting in their code
- * not being executed. Defaults to true. NOTE: when providing `createSpy()`,
+ * set to true, **all** actions will be replaced with spies, resulting in their code
+ * not being executed. When set to an array of action names, only those actions
+ * will be stubbed. When set to a function, it will be called for each action with
+ * the action name and store instance, and should return true to stub the action.
+ *
+ * NOTE: when providing `createSpy()`,
* it will **only** make the `fn` argument `undefined`. You still have to
* handle this in `createSpy()`.
+ *
+ * @default `true`
*/
- stubActions?: boolean
+ stubActions?:
+ | boolean
+ | string[]
+ | ((actionName: string, store: any) => boolean)
/**
* When set to true, calls to `$patch()` won't change the state. Defaults to
pinia._p.push(({ store, options }) => {
Object.keys(options.actions).forEach((action) => {
if (action === '$reset') return
- store[action] = stubActions ? createSpy() : createSpy(store[action])
+
+ store[action] = shouldStubAction(stubActions, action, store)
+ ? createSpy()
+ : createSpy(store[action])
})
store.$patch = stubPatch ? createSpy() : createSpy(store.$patch)
}
}
}
+
+/**
+ * Should the given action be stubbed?
+ *
+ * @param stubActions - config option
+ * @param action - action name
+ * @param store - Store instance
+ */
+function shouldStubAction(
+ stubActions: TestingOptions['stubActions'],
+ action: string,
+ store: StoreGeneric
+): boolean {
+ if (typeof stubActions === 'boolean') {
+ return stubActions
+ } else if (Array.isArray(stubActions)) {
+ return stubActions.includes(action)
+ } else if (typeof stubActions === 'function') {
+ return stubActions(action, store)
+ }
+ return false
+}