From 0a2bd3ba8e5e13e9955de8cb044b8213b5073778 Mon Sep 17 00:00:00 2001 From: cexbrayat Date: Tue, 11 Jan 2022 18:24:13 +0100 Subject: [PATCH 1/5] feat: create and restore spies with other test frameworks Refs #143 By default the mock will use sinon or jest support to create and restore spies. This commit adds a `spy` option that allows to use a different testing framework, by providing a method to create spies, and one to restore them. For example, with vitest: ```ts const router = createRouterMock({ spy: { create: fn => vi.fn(fn), restore: spy => () => spy.restore() } }); ``` --- src/router.ts | 88 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 14 deletions(-) diff --git a/src/router.ts b/src/router.ts index 55ea1f4..3864ef0 100644 --- a/src/router.ts +++ b/src/router.ts @@ -11,7 +11,7 @@ import { RouterOptions, START_LOCATION, } from 'vue-router' -import type { SinonStatic } from 'sinon' +import type { SinonSpy, SinonStatic } from 'sinon' export const EmptyView = defineComponent({ name: 'RouterMockEmptyView', @@ -28,28 +28,48 @@ function getJestGlobal() { } /** - * Creates a spy on a function and allows clearing the mock. + * Creates a spy on a function * * @param fn function to spy on - * @returns [spy, mockClear()] + * @returns spy */ -function createSpy any>( - fn: Fn -): [Fn, () => void] { +function createSpyAuto any>(fn: Fn): Fn { const sinon = getSinonGlobal() if (sinon) { - const spy = sinon.spy(fn) - return [spy as unknown as Fn, () => spy.resetHistory()] + return sinon.spy(fn) as unknown as Fn } const jest = getJestGlobal() if (jest) { - const spy = jest.fn(fn) - return [spy as unknown as Fn, () => spy.mockClear()] + return jest.fn(fn) as unknown as Fn } console.error( - `Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "createSpy" option when creating the router mock.` + `Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "spy.create" option when creating the router mock.` + ) + throw new Error('No Spy Available') +} + +/** + * Restores a mock + * + * @param spy the spy to restore + */ +function restoreSpyAuto any>( + spy: Fn +): () => void { + const sinon = getSinonGlobal() + if (sinon) { + return () => (spy as unknown as SinonSpy).resetHistory() + } + + const jest = getJestGlobal() + if (jest) { + return () => (spy as unknown as jest.Mock).mockClear() + } + + console.error( + `Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "spy.restore" option when creating the router mock.` ) throw new Error('No Spy Available') } @@ -112,6 +132,21 @@ export interface RouterMock extends Router { reset(): void } +/** + * Options passed to the `spy` option of the `createRouterMock` function + */ +export interface RouterMockSpyOptions { + /** + * Creates a spy (for example, `create: fn => vi.fn(fn)` with vitest) + */ + create: (...args: any[]) => any + + /** + * function to restore a spy (for example, `restore: spy => () => spy.restore()` with vitest) + */ + restore: (spy: any) => () => void +} + /** * TODO: Allow passing a custom spy and detect common global ones like jest and cypress. */ @@ -155,6 +190,22 @@ export interface RouterMockOptions extends Partial { * disable that behavior and throw when `router.push()` fails. */ noUndeclaredRoutes?: boolean + + /** + * By default the mock will use sinon or jest support to create and restore spies. + * This option allows to use a different testing framework, + * by providing a method to create spies, and one to restore them. + * For example, with vitest: + * ``` + * const router = createRouterMock({ + * spy: { + * create: fn => vi.fn(fn), + * restore: spy => () => spy.restore() + * } + * }); + * ``` + */ + spy?: RouterMockSpyOptions } /** @@ -188,7 +239,10 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock { const { push, addRoute, replace, beforeEach, beforeResolve } = router - const [addRouteMock, addRouteMockClear] = createSpy( + const createSpy = options.spy?.create ?? createSpyAuto + const restoreSpy = options.spy?.restore ?? restoreSpyAuto + + const addRouteMock = createSpy( ( parentRecordName: Required['name'] | RouteRecordRaw, record?: RouteRecordRaw @@ -206,14 +260,20 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock { } ) - const [pushMock, pushMockClear] = createSpy((to: RouteLocationRaw) => { + const addRouteMockClear = restoreSpy(addRouteMock) + + const pushMock = createSpy((to: RouteLocationRaw) => { return consumeNextReturn(to) }) - const [replaceMock, replaceMockClear] = createSpy((to: RouteLocationRaw) => { + const pushMockClear = restoreSpy(pushMock) + + const replaceMock = createSpy((to: RouteLocationRaw) => { return consumeNextReturn(to, { replace: true }) }) + const replaceMockClear = restoreSpy(replaceMock) + router.push = pushMock router.replace = replaceMock router.addRoute = addRouteMock From 3d0670c308685d509a30bf91f4092c9a0706c20b Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 21 Jan 2022 18:23:57 +0100 Subject: [PATCH 2/5] refactor: handle types --- __tests__/push.spec.ts | 7 +++ src/autoSpy.ts | 86 +++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/router.ts | 106 +++++++---------------------------------- src/testers/jest.ts | 3 ++ src/testers/sinon.ts | 7 +++ 6 files changed, 120 insertions(+), 90 deletions(-) create mode 100644 src/autoSpy.ts create mode 100644 src/testers/jest.ts create mode 100644 src/testers/sinon.ts diff --git a/__tests__/push.spec.ts b/__tests__/push.spec.ts index ca8fa13..d74d3c5 100644 --- a/__tests__/push.spec.ts +++ b/__tests__/push.spec.ts @@ -1,5 +1,12 @@ +import { _InferSpyType } from '../src/autoSpy' import { getRouter, createRouterMock } from '../src' +declare module '../src' { + interface RouterMockSpy { + // spy: jest.Mock, Parameters> + } +} + describe('router.push mock', () => { it('still calls push for non valid routes', async () => { const router = getRouter() diff --git a/src/autoSpy.ts b/src/autoSpy.ts new file mode 100644 index 0000000..fa8f689 --- /dev/null +++ b/src/autoSpy.ts @@ -0,0 +1,86 @@ +import { getJestGlobal } from './testers/jest' +import { getSinonGlobal } from './testers/sinon' + +/** + * Creates a spy on a function + * + * @param fn function to spy on + * @returns [spy, mockClear] + */ +export function createSpy any>( + fn: Fn, + spyFactory?: RouterMockSpyOptions +): [_InferSpyType, () => void] { + if (spyFactory) { + const spy = spyFactory.create(fn) + return [spy, () => spyFactory.reset(spy)] + } + + const sinon = getSinonGlobal() + if (sinon) { + const spy = sinon.spy(fn) + return [spy as unknown as _InferSpyType, () => spy.resetHistory()] + } + + const jest = getJestGlobal() + if (jest) { + const spy = jest.fn(fn) + return [spy as unknown as _InferSpyType, () => spy.mockClear()] + } + + console.error( + `Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "spy.create" option when creating the router mock.` + ) + throw new Error('No Spy Available') +} + +/** + * Options passed to the `spy` option of the `createRouterMock` function + */ +export interface RouterMockSpyOptions { + /** + * Creates a spy (for example, `create: fn => vi.fn(fn)` with vitest) + */ + create: (...args: any[]) => any + + /** + * Resets a spy but keeps it active. + */ + reset: (spy: _InferSpyType) => void + + /** + * Restores the original function given to the spy. + */ + restore: (spy: _InferSpyType) => void +} + +/** + * Define your own Spy to adapt to your testing framework (jest, peeky, sinon, vitest, etc) + * @beta: still trying out, could change in the future + * + * @example + * ```ts + * import 'vue-router-mock' // Only needed on external d.ts files + * + * declare module 'vue-router-mock' { + * export interface RouterMockSpy { + * spy: Sinon.Spy, ReturnType> + * } + * } + * ``` + */ +export interface RouterMockSpy< + Fn extends (...args: any[]) => any = (...args: any[]) => any +> { + // cannot be added or it wouldn't be extensible + // spy: any +} + +/** + * @internal + */ +export type _InferSpyType< + Fn extends (...args: any[]) => any = (...args: any[]) => any + // @ts-ignore: the version with Record<'spy', any> doesn't work... +> = keyof RouterMockSpy extends 'spy' ? RouterMockSpy['spy'] : Fn +// > = RouterMockSpy extends Record<'spy', any> ? RouterMockSpy['spy'] : Fn diff --git a/src/index.ts b/src/index.ts index 8387b86..024599f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export { injectRouterMock, createProvide } from './injections' export { createRouterMock, EmptyView } from './router' export type { RouterMock, RouterMockOptions } from './router' export { plugin as VueRouterMock, getRouter } from './plugin' +export type { RouterMockSpy, RouterMockSpyOptions } from './autoSpy' diff --git a/src/router.ts b/src/router.ts index 3864ef0..48d9a3d 100644 --- a/src/router.ts +++ b/src/router.ts @@ -11,69 +11,13 @@ import { RouterOptions, START_LOCATION, } from 'vue-router' -import type { SinonSpy, SinonStatic } from 'sinon' +import { createSpy, RouterMockSpyOptions, _InferSpyType } from './autoSpy' export const EmptyView = defineComponent({ name: 'RouterMockEmptyView', render: () => null, }) -declare const sinon: SinonStatic | undefined -function getSinonGlobal() { - return typeof sinon !== 'undefined' && sinon -} - -function getJestGlobal() { - return typeof jest !== 'undefined' && jest -} - -/** - * Creates a spy on a function - * - * @param fn function to spy on - * @returns spy - */ -function createSpyAuto any>(fn: Fn): Fn { - const sinon = getSinonGlobal() - if (sinon) { - return sinon.spy(fn) as unknown as Fn - } - - const jest = getJestGlobal() - if (jest) { - return jest.fn(fn) as unknown as Fn - } - - console.error( - `Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "spy.create" option when creating the router mock.` - ) - throw new Error('No Spy Available') -} - -/** - * Restores a mock - * - * @param spy the spy to restore - */ -function restoreSpyAuto any>( - spy: Fn -): () => void { - const sinon = getSinonGlobal() - if (sinon) { - return () => (spy as unknown as SinonSpy).resetHistory() - } - - const jest = getJestGlobal() - if (jest) { - return () => (spy as unknown as jest.Mock).mockClear() - } - - console.error( - `Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "spy.restore" option when creating the router mock.` - ) - throw new Error('No Spy Available') -} - /** * Router Mock instance */ @@ -130,27 +74,13 @@ export interface RouterMock extends Router { * to reset the router state before each test. */ reset(): void -} -/** - * Options passed to the `spy` option of the `createRouterMock` function - */ -export interface RouterMockSpyOptions { - /** - * Creates a spy (for example, `create: fn => vi.fn(fn)` with vitest) - */ - create: (...args: any[]) => any - - /** - * function to restore a spy (for example, `restore: spy => () => spy.restore()` with vitest) - */ - restore: (spy: any) => () => void + push: _InferSpyType + replace: _InferSpyType + // FIXME: it doesn't seem to work for overloads + // addRoute: _InferSpyType } -/** - * TODO: Allow passing a custom spy and detect common global ones like jest and cypress. - */ - /** * Options passed to `createRouterMock()`. */ @@ -234,15 +164,13 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock { runInComponentGuards, useRealNavigation, noUndeclaredRoutes, + spy, } = options const initialLocation = options.initialLocation || START_LOCATION const { push, addRoute, replace, beforeEach, beforeResolve } = router - const createSpy = options.spy?.create ?? createSpyAuto - const restoreSpy = options.spy?.restore ?? restoreSpyAuto - - const addRouteMock = createSpy( + const [addRouteMock, addRouteMockClear] = createSpy( ( parentRecordName: Required['name'] | RouteRecordRaw, record?: RouteRecordRaw @@ -257,22 +185,17 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock { // @ts-ignore: this should be valid return addRoute(parentRecordName, record) - } + }, + spy ) - const addRouteMockClear = restoreSpy(addRouteMock) - - const pushMock = createSpy((to: RouteLocationRaw) => { + const [pushMock, pushMockClear] = createSpy((to: RouteLocationRaw) => { return consumeNextReturn(to) - }) - - const pushMockClear = restoreSpy(pushMock) + }, spy) - const replaceMock = createSpy((to: RouteLocationRaw) => { + const [replaceMock, replaceMockClear] = createSpy((to: RouteLocationRaw) => { return consumeNextReturn(to, { replace: true }) - }) - - const replaceMockClear = restoreSpy(replaceMock) + }, spy) router.push = pushMock router.replace = replaceMock @@ -392,6 +315,9 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock { return { ...router, + push: pushMock, + replace: replaceMock, + addRoute: addRouteMock, depth, setNextGuardReturn, getPendingNavigation, diff --git a/src/testers/jest.ts b/src/testers/jest.ts new file mode 100644 index 0000000..a07f9e8 --- /dev/null +++ b/src/testers/jest.ts @@ -0,0 +1,3 @@ +export function getJestGlobal() { + return typeof jest !== 'undefined' && jest +} diff --git a/src/testers/sinon.ts b/src/testers/sinon.ts new file mode 100644 index 0000000..d854181 --- /dev/null +++ b/src/testers/sinon.ts @@ -0,0 +1,7 @@ +import type { SinonStatic } from 'sinon' + +declare const sinon: SinonStatic | undefined + +export function getSinonGlobal() { + return typeof sinon !== 'undefined' && sinon +} From a12469897f2e86fb3c0347f7bff82bd392b19779 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 21 Jan 2022 18:24:10 +0100 Subject: [PATCH 3/5] build: remove non existant file in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e928480..8db4010 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vue-router-mock", "version": "0.1.3", "description": "Easily test your components by mocking the router", - "main": "dist/index.cjs", + "main": "dist/index.mjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", "sideEffects": false, From d501bbae55a2dd7c0493b580a880cdd0e9a29a4f Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 21 Jan 2022 18:29:53 +0100 Subject: [PATCH 4/5] feat: add vitest support --- src/autoSpy.ts | 3 ++- src/testers/vitest.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/testers/vitest.ts diff --git a/src/autoSpy.ts b/src/autoSpy.ts index fa8f689..fac1a85 100644 --- a/src/autoSpy.ts +++ b/src/autoSpy.ts @@ -1,5 +1,6 @@ import { getJestGlobal } from './testers/jest' import { getSinonGlobal } from './testers/sinon' +import { getVitestGlobal } from './testers/vitest' /** * Creates a spy on a function @@ -22,7 +23,7 @@ export function createSpy any>( return [spy as unknown as _InferSpyType, () => spy.resetHistory()] } - const jest = getJestGlobal() + const jest = getVitestGlobal() || getJestGlobal() if (jest) { const spy = jest.fn(fn) return [spy as unknown as _InferSpyType, () => spy.mockClear()] diff --git a/src/testers/vitest.ts b/src/testers/vitest.ts new file mode 100644 index 0000000..b59a080 --- /dev/null +++ b/src/testers/vitest.ts @@ -0,0 +1,7 @@ +import type { vi as Vi } from 'vitest' + +declare const vi: typeof Vi | undefined + +export function getVitestGlobal() { + return typeof vi !== 'undefined' && vi +} From d3eaa57bcb2860d2f6927b1cbd652612eed016ed Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Fri, 21 Jan 2022 18:31:09 +0100 Subject: [PATCH 5/5] refactor: simpler types --- src/testers/vitest.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/testers/vitest.ts b/src/testers/vitest.ts index b59a080..49fb60b 100644 --- a/src/testers/vitest.ts +++ b/src/testers/vitest.ts @@ -1,6 +1,5 @@ -import type { vi as Vi } from 'vitest' - -declare const vi: typeof Vi | undefined +// cannot import the actual typ +declare const vi: typeof jest export function getVitestGlobal() { return typeof vi !== 'undefined' && vi