Skip to content

Commit

Permalink
feat(testing): add createTestingPinia
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jun 25, 2021
1 parent 399a930 commit 120ac9d
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 0 deletions.
87 changes: 87 additions & 0 deletions __tests__/testing.spec.ts
@@ -0,0 +1,87 @@
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)
})
})
3 changes: 3 additions & 0 deletions src/index.ts
Expand Up @@ -49,3 +49,6 @@ export type {
_Spread,
_StoreObject,
} from './mapHelpers'

export { createTestingPinia, getMockedStore } from './testing'
export type { TestingOptions } from './testing'
99 changes: 99 additions & 0 deletions src/testing.ts
@@ -0,0 +1,99 @@
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>
}

0 comments on commit 120ac9d

Please sign in to comment.