Skip to content

Commit

Permalink
fix: typescript allows new Spy, add lastCall support for spy (#1085)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Apr 3, 2022
1 parent db824cf commit 6680152
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 17 deletions.
31 changes: 30 additions & 1 deletion docs/api/index.md
Expand Up @@ -1787,6 +1787,10 @@ If a function was invoked twice with the following arguments `fn(arg1, arg2)`, `
]
```

### mock.lastCall

This contains the arguments of the last call. If spy wasn't called, will return `undefined`.

### mock.results

This is an array containing all values, that were `returned` from function. One item of the array is an object with properties `type` and `value`. Available types are:
Expand All @@ -1813,4 +1817,29 @@ If function returned `result`, then threw an error, then `mock.results` will be:

### mock.instances

Currently, this property is not implemented.
This is an array containing all instances that were instantiated when mock was called with a `new` keyword. Note, this is an actual context (`this`) of the function, not a return value.

For example, if mock was instantiated with `new MyClass()`, then `mock.instances` will be an array of one value:

```js
import { expect, vi } from 'vitest'

const MyClass = vi.fn()

const a = new MyClass()

expect(MyClass.mock.instances[0]).toBe(a)
```

If you return a value from constructor, it will not be in `instances` array, but instead on `results`:

```js
import { expect, vi } from 'vitest'

const Spy = vi.fn(() => ({ method: vi.fn() }))

const a = new Spy()

expect(Spy.mock.instances[0]).not.toBe(a)
expect(Spy.mock.results[0]).toBe(a)
```
2 changes: 1 addition & 1 deletion packages/vitest/rollup.config.js
Expand Up @@ -19,7 +19,7 @@ const entries = [
'src/node.ts',
'src/runtime/worker.ts',
'src/runtime/entry.ts',
'src/integrations/jest-mock.ts',
'src/integrations/spy.ts',
]

const dtsEntries = [
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/index.ts
Expand Up @@ -7,7 +7,7 @@ export * from './runtime/hooks'

export { runOnce, isFirstRun } from './integrations/run-once'
export * from './integrations/chai'
export * from './integrations/jest-mock'
export * from './integrations/spy'
export * from './integrations/vi'
export * from './integrations/utils'

Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/integrations/chai/jest-expect.ts
@@ -1,5 +1,5 @@
import type { EnhancedSpy } from '../jest-mock'
import { isMockFunction } from '../jest-mock'
import type { EnhancedSpy } from '../spy'
import { isMockFunction } from '../spy'
import { addSerializer } from '../snapshot/port/plugins'
import type { Constructable } from '../../types'
import { assertTypes } from '../../utils'
Expand Down
Expand Up @@ -16,11 +16,12 @@ interface MockResultThrow {

type MockResult<T> = MockResultReturn<T> | MockResultThrow | MockResultIncomplete

export interface JestMockCompatContext<TArgs, TReturns> {
export interface SpyContext<TArgs, TReturns> {
calls: TArgs[]
instances: TReturns[]
invocationCallOrder: number[]
results: MockResult<TReturns>[]
lastCall: TArgs | undefined
}

type Procedure = (...args: any[]) => any
Expand All @@ -38,7 +39,7 @@ type Classes<T> = {
export interface SpyInstance<TArgs extends any[] = any[], TReturns = any> {
getMockName(): string
mockName(n: string): this
mock: JestMockCompatContext<TArgs, TReturns>
mock: SpyContext<TArgs, TReturns>
mockClear(): this
mockReset(): this
mockRestore(): void
Expand All @@ -56,6 +57,7 @@ export interface SpyInstance<TArgs extends any[] = any[], TReturns = any> {

export interface SpyInstanceFn<TArgs extends any[] = any, TReturns = any> extends SpyInstance<TArgs, TReturns> {
(...args: TArgs): TReturns
new (...args: TArgs): TReturns
}

export type MaybeMockedConstructor<T> = T extends new (
Expand Down Expand Up @@ -164,6 +166,9 @@ function enhanceSpy<TArgs extends any[], TReturns>(
return { type, value }
})
},
get lastCall() {
return stub.calls.at(-1)
},
}

let onceImplementations: ((...args: TArgs) => TReturns)[] = []
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/integrations/vi.ts
Expand Up @@ -3,8 +3,8 @@
import { parseStacktrace } from '../utils/source-map'
import type { VitestMocker } from '../runtime/mocker'
import { FakeTimers } from './timers'
import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep } from './jest-mock'
import { fn, isMockFunction, spies, spyOn } from './jest-mock'
import type { EnhancedSpy, MaybeMocked, MaybeMockedDeep } from './spy'
import { fn, isMockFunction, spies, spyOn } from './spy'

class VitestUtils {
private _timers: FakeTimers
Expand Down
8 changes: 4 additions & 4 deletions packages/vitest/src/runtime/mocker.ts
Expand Up @@ -18,8 +18,8 @@ function getAllProperties(obj: any) {
const allProps = new Set<string>()
let curr = obj
do {
// we don't need propterties from 'Object'
if (curr === Object.prototype)
// we don't need propterties from these
if (curr === Object.prototype || curr === Function.prototype || curr === RegExp.prototype)
break
const props = Object.getOwnPropertyNames(curr)
props.forEach(prop => allProps.add(prop))
Expand All @@ -30,7 +30,7 @@ function getAllProperties(obj: any) {

export class VitestMocker {
private static pendingIds: PendingSuiteMock[] = []
private static spyModule?: typeof import('../integrations/jest-mock')
private static spyModule?: typeof import('../integrations/spy')

private request!: (dep: string) => unknown

Expand Down Expand Up @@ -239,7 +239,7 @@ export class VitestMocker {
private async ensureSpy() {
if (VitestMocker.spyModule)
return
VitestMocker.spyModule = await this.request(resolve(distDir, 'jest-mock.js')) as typeof import('../integrations/jest-mock')
VitestMocker.spyModule = await this.request(resolve(distDir, 'spy.js')) as typeof import('../integrations/spy')
}

public async requestWithMock(dep: string) {
Expand Down
87 changes: 83 additions & 4 deletions test/core/test/jest-mock.test.ts
Expand Up @@ -29,10 +29,9 @@ describe('jest mock compat layer', () => {
})

it('clearing instances', () => {
const Spy = vi.fn(() => {})
const Spy = vi.fn(() => ({}))

expect(Spy.mock.instances).toHaveLength(0)
// @ts-expect-error ignore
// eslint-disable-next-line no-new
new Spy()
expect(Spy.mock.instances).toHaveLength(1)
Expand Down Expand Up @@ -168,6 +167,24 @@ describe('jest mock compat layer', () => {
expect(obj.getter).toBe('original')
})

it('getter function spyOn', () => {
const obj = {
get getter() {
return function() { return 'original' }
},
}

const spy = vi.spyOn(obj, 'getter')

expect(obj.getter()).toBe('original')

spy.mockImplementation(() => 'mocked').mockImplementationOnce(() => 'once')

expect(obj.getter()).toBe('once')
expect(obj.getter()).toBe('mocked')
expect(obj.getter()).toBe('mocked')
})

it('setter spyOn', () => {
let settedValue = 'original'
let mockedValue = 'none'
Expand Down Expand Up @@ -211,6 +228,30 @@ describe('jest mock compat layer', () => {
expect(settedValue).toBe('last')
})

it('should work - setter', () => {
const obj = {
_property: false,
set property(value) {
this._property = value
},
get property() {
return this._property
},
}

const spy = vi.spyOn(obj, 'property', 'set')
obj.property = true
expect(spy).toHaveBeenCalled()
expect(obj.property).toBe(true)
obj.property = false
spy.mockRestore()
obj.property = true
// unlike jest, mockRestore only restores implementation to the original one,
// we are still spying on the setter
expect(spy).toHaveBeenCalled()
expect(obj.property).toBe(true)
})

it('throwing', async() => {
const fn = vi.fn(() => {
// eslint-disable-next-line no-throw-literal
Expand All @@ -227,6 +268,44 @@ describe('jest mock compat layer', () => {
])
})

it.todo('mockRejectedValue')
it.todo('mockResolvedValue')
it('mockRejectedValue', async() => {
const safeCall = async(fn: () => void) => {
try {
await fn()
}
catch {}
}

const spy = vi.fn()
.mockRejectedValue(new Error('error'))
.mockRejectedValueOnce(new Error('once'))

await safeCall(spy)
await safeCall(spy)

expect(spy.mock.results[0]).toEqual(e(new Error('once')))
expect(spy.mock.results[1]).toEqual(e(new Error('error')))
})
it('mockResolvedValue', async() => {
const spy = vi.fn()
.mockResolvedValue('resolved')
.mockResolvedValueOnce('once')

await spy()
await spy()

expect(spy.mock.results[0]).toEqual(r('once'))
expect(spy.mock.results[1]).toEqual(r('resolved'))
})

it('tracks instances made by mocks', () => {
const Fn = vi.fn()
expect(Fn.mock.instances).toEqual([])

const instance1 = new Fn()
expect(Fn.mock.instances[0]).toBe(instance1)

const instance2 = new Fn()
expect(Fn.mock.instances[1]).toBe(instance2)
})
})

0 comments on commit 6680152

Please sign in to comment.