From 120ac9d98eca0e11f24c5334022ef9bc805371af Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 25 Jun 2021 12:02:30 +0200 Subject: [PATCH] feat(testing): add createTestingPinia --- __tests__/testing.spec.ts | 87 ++++++++++++++++++++++++++++++++++ src/index.ts | 3 ++ src/testing.ts | 99 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 __tests__/testing.spec.ts create mode 100644 src/testing.ts diff --git a/__tests__/testing.spec.ts b/__tests__/testing.spec.ts new file mode 100644 index 00000000..e0ddab4a --- /dev/null +++ b/__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: ` + + {{ counter.n }} + + `, + }) + + 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) + }) +}) diff --git a/src/index.ts b/src/index.ts index a3d669ab..e9d31f23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,3 +49,6 @@ export type { _Spread, _StoreObject, } from './mapHelpers' + +export { createTestingPinia, getMockedStore } from './testing' +export type { TestingOptions } from './testing' diff --git a/src/testing.ts b/src/testing.ts new file mode 100644 index 00000000..68867b5f --- /dev/null +++ b/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>() + + 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 = S extends Store< + string, + StateTree, + GettersTree, + 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( + store: S +): S & StoreWithMockedActions { + return store as S & StoreWithMockedActions +} -- 2.47.2