Skip to content

Commit d4c2b28

Browse files
authored
fix(spy): properly inherit implementation's length (#8778)
1 parent 588f768 commit d4c2b28

File tree

6 files changed

+82
-4
lines changed

6 files changed

+82
-4
lines changed

docs/api/mock.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ getApplesSpy.mock.calls.length === 1
2020

2121
You should use mock assertions (e.g., [`toHaveBeenCalled`](/api/expect#tohavebeencalled)) on [`expect`](/api/expect) to assert mock results. This API reference describes available properties and methods to manipulate mock behavior.
2222

23+
::: warning IMPORTANT
24+
Vitest spies inherit implementation's `length` property. This means that `length` can be different from the original implementation:
25+
26+
```ts
27+
const example = {
28+
fn(arg1, arg2) {
29+
// ...
30+
}
31+
}
32+
33+
const fn = vi.spyOn(example, 'fn')
34+
fn.length // == 2
35+
36+
fn.mockImplementation(() => {})
37+
fn.length // == 0
38+
```
39+
:::
40+
2341
::: tip
2442
The custom function implementation in the types below is marked with a generic `<T>`.
2543
:::

packages/spy/src/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,25 @@ function createMock(
494494
if (original) {
495495
copyOriginalStaticProperties(namedObject[name], original)
496496
}
497+
let overrideLength: number | undefined
498+
Object.defineProperty(namedObject[name], 'length', {
499+
configurable: true,
500+
get: () => {
501+
if (overrideLength != null) {
502+
return overrideLength
503+
}
504+
505+
const implementation = config.onceMockImplementations[0]
506+
|| config.mockImplementation
507+
|| prototypeConfig?.onceMockImplementations[0]
508+
|| prototypeConfig?.mockImplementation
509+
|| original
510+
return implementation?.length ?? 0
511+
},
512+
set: (length: number) => {
513+
overrideLength = length
514+
},
515+
})
497516
return namedObject[name]
498517
}
499518

test/browser/fixtures/mocking/import-mock.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ test('all mocked are valid', async () => {
1212

1313
// creates a new mocked function with no formal arguments.
1414
expect(example.square.name).toEqual('square')
15-
expect(example.square.length).toEqual(0)
15+
expect(example.square.length).toEqual(2)
1616

1717
// async functions get the same treatment as standard synchronous functions.
1818
expect(example.asyncSquare.name).toEqual('asyncSquare')
19-
expect(example.asyncSquare.length).toEqual(0)
19+
expect(example.asyncSquare.length).toEqual(2)
2020

2121
// creates a new class with the same interface, member functions and properties are mocked.
2222
expect(example.someClasses.constructor.name).toEqual('Bar')

test/core/test/mocking/automocking.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ test('all mocked are valid', async () => {
1313

1414
// creates a new mocked function with no formal arguments.
1515
expect(example.square.name).toEqual('square')
16-
expect(example.square.length).toEqual(0)
16+
expect(example.square.length).toEqual(2)
1717

1818
// async functions get the same treatment as standard synchronous functions.
1919
expect(example.asyncSquare.name).toEqual('asyncSquare')
20-
expect(example.asyncSquare.length).toEqual(0)
20+
expect(example.asyncSquare.length).toEqual(2)
2121

2222
// creates a new class with the same interface, member functions and properties are mocked.
2323
expect(example.someClasses.constructor.name).toEqual('Bar')

test/core/test/mocking/vi-fn.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,30 @@ test('vi.fn().mock cannot be overriden', () => {
2020
}).toThrowError()
2121
})
2222

23+
test('vi.fn() has correct length', () => {
24+
const fn0 = vi.fn(() => {})
25+
expect(fn0.length).toBe(0)
26+
27+
const fnArgs = vi.fn((..._args) => {})
28+
expect(fnArgs.length).toBe(0)
29+
30+
const fn1 = vi.fn((_arg1) => {})
31+
expect(fn1.length).toBe(1)
32+
33+
const fn2 = vi.fn((_arg1, _arg2) => {})
34+
expect(fn2.length).toBe(2)
35+
36+
const fn3 = vi.fn((_arg1, _arg2, _arg3) => {})
37+
expect(fn3.length).toBe(3)
38+
})
39+
40+
test('vi.fn() has overridable length', () => {
41+
const fn0 = vi.fn(() => {})
42+
// @ts-expect-error TS doesn't allow override
43+
fn0.length = 5
44+
expect(fn0.length).toBe(5)
45+
})
46+
2347
describe('vi.fn() state', () => {
2448
// TODO: test when calls is not empty
2549
test('vi.fn() clears calls without a custom implementation', () => {

test/core/test/mocking/vi-spyOn.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import type { MockContext } from 'vitest'
22
import { describe, expect, test, vi } from 'vitest'
33

4+
test('vi.fn() has correct length', () => {
5+
const fn0 = vi.spyOn({ fn: () => {} }, 'fn')
6+
expect(fn0.length).toBe(0)
7+
8+
const fnArgs = vi.spyOn({ fn: (..._args: any[]) => {} }, 'fn')
9+
expect(fnArgs.length).toBe(0)
10+
11+
const fn1 = vi.spyOn({ fn: (_arg1: any) => {} }, 'fn')
12+
expect(fn1.length).toBe(1)
13+
14+
const fn2 = vi.spyOn({ fn: (_arg1: any, _arg2: any) => {} }, 'fn')
15+
expect(fn2.length).toBe(2)
16+
17+
const fn3 = vi.spyOn({ fn: (_arg1: any, _arg2: any, _arg3: any) => {} }, 'fn')
18+
expect(fn3.length).toBe(3)
19+
})
20+
421
describe('vi.spyOn() state', () => {
522
test('vi.spyOn() spies on an object and tracks the calls', () => {
623
const object = createObject()

0 commit comments

Comments
 (0)