Skip to content

Commit

Permalink
feat: create and restore spies with other test frameworks (#144)
Browse files Browse the repository at this point in the history
Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
  • Loading branch information
cexbrayat and posva authored Jan 21, 2022
1 parent f87b32c commit b7fd685
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 45 deletions.
7 changes: 7 additions & 0 deletions __tests__/push.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { _InferSpyType } from '../src/autoSpy'
import { getRouter, createRouterMock } from '../src'

declare module '../src' {
interface RouterMockSpy<Fn> {
// spy: jest.Mock<ReturnType<Fn>, Parameters<Fn>>
}
}

describe('router.push mock', () => {
it('still calls push for non valid routes', async () => {
const router = getRouter()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
87 changes: 87 additions & 0 deletions src/autoSpy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { getJestGlobal } from './testers/jest'
import { getSinonGlobal } from './testers/sinon'
import { getVitestGlobal } from './testers/vitest'

/**
* Creates a spy on a function
*
* @param fn function to spy on
* @returns [spy, mockClear]
*/
export function createSpy<Fn extends (...args: any[]) => any>(
fn: Fn,
spyFactory?: RouterMockSpyOptions
): [_InferSpyType<Fn>, () => 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<Fn>, () => spy.resetHistory()]
}

const jest = getVitestGlobal() || getJestGlobal()
if (jest) {
const spy = jest.fn(fn)
return [spy as unknown as _InferSpyType<Fn>, () => 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<Fn> {
* spy: Sinon.Spy<Parameters<Fn>, ReturnType<Fn>>
* }
* }
* ```
*/
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<Fn> extends 'spy' ? RouterMockSpy<Fn>['spy'] : Fn
// > = RouterMockSpy<Fn> extends Record<'spy', any> ? RouterMockSpy<Fn>['spy'] : Fn
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
74 changes: 30 additions & 44 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,13 @@ import {
RouterOptions,
START_LOCATION,
} from 'vue-router'
import type { 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 and allows clearing the mock.
*
* @param fn function to spy on
* @returns [spy, mockClear()]
*/
function createSpy<Fn extends (...args: any[]) => any>(
fn: Fn
): [Fn, () => void] {
const sinon = getSinonGlobal()
if (sinon) {
const spy = sinon.spy(fn)
return [spy as unknown as Fn, () => spy.resetHistory()]
}

const jest = getJestGlobal()
if (jest) {
const spy = jest.fn(fn)
return [spy as unknown as Fn, () => spy.mockClear()]
}

console.error(
`Couldn't detect a global spy (tried jest and sinon). Make sure to provide a "createSpy" option when creating the router mock.`
)
throw new Error('No Spy Available')
}

/**
* Router Mock instance
*/
Expand Down Expand Up @@ -110,11 +74,12 @@ export interface RouterMock extends Router {
* to reset the router state before each test.
*/
reset(): void
}

/**
* TODO: Allow passing a custom spy and detect common global ones like jest and cypress.
*/
push: _InferSpyType<Router['push']>
replace: _InferSpyType<Router['replace']>
// FIXME: it doesn't seem to work for overloads
// addRoute: _InferSpyType<Router['addRoute']>
}

/**
* Options passed to `createRouterMock()`.
Expand Down Expand Up @@ -155,6 +120,22 @@ export interface RouterMockOptions extends Partial<RouterOptions> {
* 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
}

/**
Expand Down Expand Up @@ -183,6 +164,7 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock {
runInComponentGuards,
useRealNavigation,
noUndeclaredRoutes,
spy,
} = options
const initialLocation = options.initialLocation || START_LOCATION

Expand All @@ -203,16 +185,17 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock {

// @ts-ignore: this should be valid
return addRoute(parentRecordName, record)
}
},
spy
)

const [pushMock, pushMockClear] = createSpy((to: RouteLocationRaw) => {
return consumeNextReturn(to)
})
}, spy)

const [replaceMock, replaceMockClear] = createSpy((to: RouteLocationRaw) => {
return consumeNextReturn(to, { replace: true })
})
}, spy)

router.push = pushMock
router.replace = replaceMock
Expand Down Expand Up @@ -332,6 +315,9 @@ export function createRouterMock(options: RouterMockOptions = {}): RouterMock {

return {
...router,
push: pushMock,
replace: replaceMock,
addRoute: addRouteMock,
depth,
setNextGuardReturn,
getPendingNavigation,
Expand Down
3 changes: 3 additions & 0 deletions src/testers/jest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function getJestGlobal() {
return typeof jest !== 'undefined' && jest
}
7 changes: 7 additions & 0 deletions src/testers/sinon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { SinonStatic } from 'sinon'

declare const sinon: SinonStatic | undefined

export function getSinonGlobal() {
return typeof sinon !== 'undefined' && sinon
}
6 changes: 6 additions & 0 deletions src/testers/vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// cannot import the actual typ
declare const vi: typeof jest

export function getVitestGlobal() {
return typeof vi !== 'undefined' && vi
}

0 comments on commit b7fd685

Please sign in to comment.