]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat(testing): add createTestingPinia
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 25 Jun 2021 10:02:30 +0000 (12:02 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 25 Jun 2021 10:02:30 +0000 (12:02 +0200)
__tests__/testing.spec.ts [new file with mode: 0644]
src/index.ts
src/testing.ts [new file with mode: 0644]

diff --git a/__tests__/testing.spec.ts b/__tests__/testing.spec.ts
new file mode 100644 (file)
index 0000000..e0ddab4
--- /dev/null
@@ -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)
+  })
+})
index a3d669ab2b89c3389ea8b78ccad91cfca1e414c8..e9d31f238fd6e35fcf0e36f59970aa79c1cc9815 100644 (file)
@@ -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 (file)
index 0000000..68867b5
--- /dev/null
@@ -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>
+}