--- /dev/null
+import { createTestingPinia, defineStore, TestingOptions } from '../src'
+import { mount } from '@vue/test-utils'
+import { defineComponent } from 'vue'
+
+describe('Testing', () => {
+ const useCounter = defineStore({
+ id: 'counter',
+ state: () => ({ n: 0 }),
+ actions: {
+ increment(amount = 1) {
+ this.n += amount
+ },
+ },
+ })
+
+ const Counter = defineComponent({
+ setup() {
+ const counter = useCounter()
+ return { counter }
+ },
+ template: `
+ <button @click="counter.increment()">+1</button>
+ <span>{{ counter.n }}</span>
+ <button @click="counter.increment(10)">+10</button>
+ `,
+ })
+
+ function factory(options: TestingOptions = {}) {
+ const wrapper = mount(Counter, {
+ global: {
+ plugins: [createTestingPinia(options)],
+ },
+ })
+
+ const counter = useCounter()
+
+ return { wrapper, counter }
+ }
+
+ it('spies with no config', () => {
+ const { counter, wrapper } = factory()
+
+ counter.increment()
+ expect(counter.n).toBe(0)
+ expect(counter.increment).toHaveBeenCalledTimes(1)
+ expect(counter.increment).toHaveBeenLastCalledWith()
+
+ counter.increment(5)
+ expect(counter.n).toBe(0)
+ expect(counter.increment).toHaveBeenCalledTimes(2)
+ expect(counter.increment).toHaveBeenLastCalledWith(5)
+
+ wrapper.findAll('button')[0].trigger('click')
+ expect(counter.n).toBe(0)
+ expect(counter.increment).toHaveBeenCalledTimes(3)
+ expect(counter.increment).toHaveBeenLastCalledWith()
+
+ wrapper.findAll('button')[1].trigger('click')
+ expect(counter.n).toBe(0)
+ expect(counter.increment).toHaveBeenCalledTimes(4)
+ expect(counter.increment).toHaveBeenLastCalledWith(10)
+ })
+
+ it('can execute actions', () => {
+ const { counter, wrapper } = factory({ bypassActions: false })
+
+ counter.increment()
+ expect(counter.n).toBe(1)
+ expect(counter.increment).toHaveBeenCalledTimes(1)
+ expect(counter.increment).toHaveBeenLastCalledWith()
+
+ counter.increment(5)
+ expect(counter.n).toBe(6)
+ expect(counter.increment).toHaveBeenCalledTimes(2)
+ expect(counter.increment).toHaveBeenLastCalledWith(5)
+
+ wrapper.findAll('button')[0].trigger('click')
+ expect(counter.n).toBe(7)
+ expect(counter.increment).toHaveBeenCalledTimes(3)
+ expect(counter.increment).toHaveBeenLastCalledWith()
+
+ wrapper.findAll('button')[1].trigger('click')
+ expect(counter.n).toBe(17)
+ expect(counter.increment).toHaveBeenCalledTimes(4)
+ expect(counter.increment).toHaveBeenLastCalledWith(10)
+ })
+})
_Spread,
_StoreObject,
} from './mapHelpers'
+
+export { createTestingPinia, getMockedStore } from './testing'
+export type { TestingOptions } from './testing'
--- /dev/null
+import { createPinia } from './createPinia'
+import { PiniaStorePlugin, setActivePinia } from './rootStore'
+import { GettersTree, StateTree, Store } from './types'
+
+export interface TestingOptions {
+ /**
+ * Plugins to be installed before the testing plugin.
+ */
+ plugins?: PiniaStorePlugin[]
+
+ /**
+ * When set to false, actions are only spied, they still get executed. When
+ * set to true, actions will be replaced with spies, resulting in their code
+ * not being executed. Defaults to true.
+ */
+ bypassActions?: boolean
+
+ createSpy?: (fn?: (...args: any[]) => any) => (...args: any[]) => any
+}
+
+/**
+ * Creates a pinia instance designed for unit tests that **requires mocking**
+ * the stores. By default, **all actions are mocked** and therefore not
+ * executed. This allows you to unit test your store and components separately.
+ * You can change this with the `bypassActions` option. If you are using jest,
+ * they are replaced with `jest.fn()`, otherwise, you must provide your own
+ * `createSpy` option.
+ *
+ * @param options - options to configure the testing pinia
+ * @returns a augmented pinia instance
+ */
+export function createTestingPinia({
+ plugins = [],
+ bypassActions = true,
+ createSpy,
+}: TestingOptions = {}) {
+ const pinia = createPinia()
+
+ plugins.forEach((plugin) => pinia.use(plugin))
+
+ // @ts-ignore
+ createSpy = createSpy || (typeof jest !== undefined && jest.fn)
+ if (!createSpy) {
+ throw new Error('You must configure the `createSpy` option.')
+ }
+
+ // Cache of all actions to share them across all stores
+ const spiedActions = new Map<string, Record<string, any>>()
+
+ pinia.use(({ store, options }) => {
+ if (!spiedActions.has(options.id)) {
+ spiedActions.set(options.id, {})
+ }
+ const actionsCache = spiedActions.get(options.id)!
+
+ Object.keys(options.actions || {}).forEach((action) => {
+ actionsCache[action] =
+ actionsCache[action] ||
+ (bypassActions
+ ? createSpy!()
+ : // @ts-expect-error:
+ createSpy!(store[action]))
+ // @ts-expect-error:
+ store[action] = actionsCache[action]
+ })
+ })
+
+ setActivePinia(pinia)
+
+ return pinia
+}
+
+type StoreWithMockedActions<Spy, S extends Store> = S extends Store<
+ string,
+ StateTree,
+ GettersTree<StateTree>,
+ infer A
+>
+ ? {
+ [K in keyof A]: Spy
+ }
+ : {}
+
+/**
+ * Returns a type safe store that has mocks instead of actions. Requires a Mock type as a generic
+ *
+ * @example
+ * ```ts
+ * const pinia = createTestingPinia({ createSpy: jest.fn })
+ * ```
+ *
+ * @param store - store created with a testing pinia
+ * @returns a type safe store
+ */
+export function getMockedStore<Spy, S extends Store>(
+ store: S
+): S & StoreWithMockedActions<Spy, S> {
+ return store as S & StoreWithMockedActions<Spy, S>
+}