-
-
Notifications
You must be signed in to change notification settings - Fork 975
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(testing): add createTestingPinia
- Loading branch information
Showing
3 changed files
with
189 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
} |