diff --git a/docs/api/mock.md b/docs/api/mock.md index cfea4e42ca1f..53e4575f135f 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -20,6 +20,24 @@ getApplesSpy.mock.calls.length === 1 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. +::: warning IMPORTANT +Vitest spies inherit implementation's `length` property. This means that `length` can be different from the original implementation: + +```ts +const example = { + fn(arg1, arg2) { + // ... + } +} + +const fn = vi.spyOn(example, 'fn') +fn.length // == 2 + +fn.mockImplementation(() => {}) +fn.length // == 0 +``` +::: + ::: tip The custom function implementation in the types below is marked with a generic ``. ::: diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index bf54c1111cb9..3de0c9cc2a39 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -494,6 +494,25 @@ function createMock( if (original) { copyOriginalStaticProperties(namedObject[name], original) } + let overrideLength: number | undefined + Object.defineProperty(namedObject[name], 'length', { + configurable: true, + get: () => { + if (overrideLength != null) { + return overrideLength + } + + const implementation = config.onceMockImplementations[0] + || config.mockImplementation + || prototypeConfig?.onceMockImplementations[0] + || prototypeConfig?.mockImplementation + || original + return implementation?.length ?? 0 + }, + set: (length: number) => { + overrideLength = length + }, + }) return namedObject[name] } diff --git a/test/browser/fixtures/mocking/import-mock.test.ts b/test/browser/fixtures/mocking/import-mock.test.ts index 71500a242919..5d436544e072 100644 --- a/test/browser/fixtures/mocking/import-mock.test.ts +++ b/test/browser/fixtures/mocking/import-mock.test.ts @@ -12,11 +12,11 @@ test('all mocked are valid', async () => { // creates a new mocked function with no formal arguments. expect(example.square.name).toEqual('square') - expect(example.square.length).toEqual(0) + expect(example.square.length).toEqual(2) // async functions get the same treatment as standard synchronous functions. expect(example.asyncSquare.name).toEqual('asyncSquare') - expect(example.asyncSquare.length).toEqual(0) + expect(example.asyncSquare.length).toEqual(2) // creates a new class with the same interface, member functions and properties are mocked. expect(example.someClasses.constructor.name).toEqual('Bar') diff --git a/test/core/test/mocking/automocking.spec.ts b/test/core/test/mocking/automocking.spec.ts index 021fe52d1b2a..f815b4bff0e8 100644 --- a/test/core/test/mocking/automocking.spec.ts +++ b/test/core/test/mocking/automocking.spec.ts @@ -13,11 +13,11 @@ test('all mocked are valid', async () => { // creates a new mocked function with no formal arguments. expect(example.square.name).toEqual('square') - expect(example.square.length).toEqual(0) + expect(example.square.length).toEqual(2) // async functions get the same treatment as standard synchronous functions. expect(example.asyncSquare.name).toEqual('asyncSquare') - expect(example.asyncSquare.length).toEqual(0) + expect(example.asyncSquare.length).toEqual(2) // creates a new class with the same interface, member functions and properties are mocked. expect(example.someClasses.constructor.name).toEqual('Bar') diff --git a/test/core/test/mocking/vi-fn.test.ts b/test/core/test/mocking/vi-fn.test.ts index 43e33c65e938..bdce6c89732c 100644 --- a/test/core/test/mocking/vi-fn.test.ts +++ b/test/core/test/mocking/vi-fn.test.ts @@ -20,6 +20,30 @@ test('vi.fn().mock cannot be overriden', () => { }).toThrowError() }) +test('vi.fn() has correct length', () => { + const fn0 = vi.fn(() => {}) + expect(fn0.length).toBe(0) + + const fnArgs = vi.fn((..._args) => {}) + expect(fnArgs.length).toBe(0) + + const fn1 = vi.fn((_arg1) => {}) + expect(fn1.length).toBe(1) + + const fn2 = vi.fn((_arg1, _arg2) => {}) + expect(fn2.length).toBe(2) + + const fn3 = vi.fn((_arg1, _arg2, _arg3) => {}) + expect(fn3.length).toBe(3) +}) + +test('vi.fn() has overridable length', () => { + const fn0 = vi.fn(() => {}) + // @ts-expect-error TS doesn't allow override + fn0.length = 5 + expect(fn0.length).toBe(5) +}) + describe('vi.fn() state', () => { // TODO: test when calls is not empty test('vi.fn() clears calls without a custom implementation', () => { diff --git a/test/core/test/mocking/vi-spyOn.test.ts b/test/core/test/mocking/vi-spyOn.test.ts index 17168845d805..8311a9a5b8c5 100644 --- a/test/core/test/mocking/vi-spyOn.test.ts +++ b/test/core/test/mocking/vi-spyOn.test.ts @@ -1,6 +1,23 @@ import type { MockContext } from 'vitest' import { describe, expect, test, vi } from 'vitest' +test('vi.fn() has correct length', () => { + const fn0 = vi.spyOn({ fn: () => {} }, 'fn') + expect(fn0.length).toBe(0) + + const fnArgs = vi.spyOn({ fn: (..._args: any[]) => {} }, 'fn') + expect(fnArgs.length).toBe(0) + + const fn1 = vi.spyOn({ fn: (_arg1: any) => {} }, 'fn') + expect(fn1.length).toBe(1) + + const fn2 = vi.spyOn({ fn: (_arg1: any, _arg2: any) => {} }, 'fn') + expect(fn2.length).toBe(2) + + const fn3 = vi.spyOn({ fn: (_arg1: any, _arg2: any, _arg3: any) => {} }, 'fn') + expect(fn3.length).toBe(3) +}) + describe('vi.spyOn() state', () => { test('vi.spyOn() spies on an object and tracks the calls', () => { const object = createObject()