diff --git a/docs/api/mock.md b/docs/api/mock.md index 17843f0d6db2..b173bc617bb0 100644 --- a/docs/api/mock.md +++ b/docs/api/mock.md @@ -177,7 +177,10 @@ If you want this method to be called before each test automatically, you can ena Does what `mockReset` does and restores inner implementation to the original function. -Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. +Restoring a mock from `vi.spyOn(object, property)` will also restore the original descriptor of the spied-on object. + +Note that restoring a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. +Restoring a mock from `vi.fn(impl)` will set implementation to `impl`. If you want this method to be called before each test automatically, you can enable [`restoreMocks`](/config/#restoremocks) setting in config. @@ -257,6 +260,19 @@ const myMockFn = vi console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()) ``` +## mockRevert + +- **Type:** `() => MockInstance` + +Does what `mockReset` does and reverts inner implementation to the original function. + +Reverting a mock from `vi.spyOn(object, property)` will **not** restore the original descriptor of the spied-on object. + +Note that reverting a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. +Reverting a mock from `vi.fn(impl)` will set implementation to `impl`. + +If you want this method to be called before each test automatically, you can enable [`revertMocks`](/config/#revertmocks) setting in config. + ## mock.calls This is an array containing all arguments for each call. One item of the array is the arguments of that call. diff --git a/docs/api/vi.md b/docs/api/vi.md index 8288d7683b74..5ab9281bd511 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -385,15 +385,23 @@ Checks that a given parameter is a mock function. If you are using TypeScript, i ### vi.clearAllMocks -Will call [`.mockClear()`](/api/mock#mockclear) on all spies. This will clear mock history, but not reset its implementation to the default one. +Calls [`.mockClear()`](/api/mock#mockclear) on all spies. +This will clear mock history without affecting mock implementations. ### vi.resetAllMocks -Will call [`.mockReset()`](/api/mock#mockreset) on all spies. This will clear mock history and reset its implementation to an empty function (will return `undefined`). +Calls [`.mockReset()`](/api/mock#mockreset) on all spies. +This will clear mock history and reset each implementation to an empty function (will return `undefined`). ### vi.restoreAllMocks -Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies. This will clear mock history and reset its implementation to the original one. +Calls [`.mockRestore()`](/api/mock#mockrestore) on all spies. +This will clear mock history, restore each implementation to its original, and restore original descriptors of spied-on objects. + +### vi.revertAllMocks + +Calls [`.mockRevert()`](/api/mock#mockrevert) on all spies. +This will clear mock history and revert each implementation to its original without restoring original descriptors of spied-on objects. ### vi.spyOn @@ -417,7 +425,7 @@ expect(spy).toHaveReturnedWith(1) ``` ::: tip -You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to their original implementations. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation: +You can call [`vi.restoreAllMocks`](#vi-restoreallmocks) inside [`afterEach`](/api/#aftereach) (or enable [`test.restoreMocks`](/config/#restoreMocks)) to restore all methods to its original implementations. This will restore the original [object descriptor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty), so you won't be able to change method's implementation: ```ts const cart = { diff --git a/docs/config/index.md b/docs/config/index.md index a62e485720ca..f858fe82f053 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1811,21 +1811,32 @@ Custom [commands](/guide/browser/commands) that can be imported during browser t - **Type:** `boolean` - **Default:** `false` -Will call [`.mockClear()`](/api/mock#mockclear) on all spies before each test. This will clear mock history, but not reset its implementation to the default one. +Will call [`.mockClear()`](/api/mock#mockclear) on all spies before each test. +This will clear mock history without affecting mock implementations. ### mockReset - **Type:** `boolean` - **Default:** `false` -Will call [`.mockReset()`](/api/mock#mockreset) on all spies before each test. This will clear mock history and reset its implementation to an empty function (will return `undefined`). +Will call [`.mockReset()`](/api/mock#mockreset) on all spies before each test. +This will clear mock history and reset each implementation to an empty function (will return `undefined`). ### restoreMocks - **Type:** `boolean` - **Default:** `false` -Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies before each test. This will clear mock history and reset its implementation to the original one. +Will call [`.mockRestore()`](/api/mock#mockrestore) on all spies before each test. +This will clear mock history, restore each implementation to its original, and restore original descriptors of spied-on objects. + +### revertMocks + +- **Type:** `boolean` +- **Default:** `false` + +Will call [`.mockRevert()`](/api/mock#mockrevert) on all spies before each test. +This will clear mock history and revert each implementation to its original without restoring original descriptors of spied-on objects. ### unstubEnvs {#unstubenvs} diff --git a/packages/mocker/src/automocker.ts b/packages/mocker/src/automocker.ts index e6b193d96899..97b789e14ad8 100644 --- a/packages/mocker/src/automocker.ts +++ b/packages/mocker/src/automocker.ts @@ -120,7 +120,7 @@ export function mockObject( const original = this[key] const mock = spyOn(this, key as string) .mockImplementation(original) - mock.mockRestore = () => { + mock.mockRevert = mock.mockRestore = () => { mock.mockReset() mock.mockImplementation(original) return mock @@ -132,7 +132,7 @@ export function mockObject( const mock = spyOn(newContainer, property) if (options.type === 'automock') { mock.mockImplementation(mockFunction) - mock.mockRestore = () => { + mock.mockRevert = mock.mockRestore = () => { mock.mockReset() mock.mockImplementation(mockFunction) return mock diff --git a/packages/spy/src/index.ts b/packages/spy/src/index.ts index f8e52892c66b..65478245419b 100644 --- a/packages/spy/src/index.ts +++ b/packages/spy/src/index.ts @@ -185,20 +185,51 @@ export interface MockInstance { * Clears all information about every call. After calling it, all properties on `.mock` will return an empty state. This method does not reset implementations. * * It is useful if you need to clean up mock between different assertions. + * + * @see mockReset + * @see mockRestore + * @see mockRevert */ mockClear(): this /** - * Does what `mockClear` does and makes inner implementation an empty function (returning `undefined` when invoked). This also resets all "once" implementations. + * Does what `mockClear` does and makes inner implementation an empty function (returning `undefined` when invoked). + * This also resets all "once" implementations. * * This is useful when you want to completely reset a mock to the default state. + * + * @see mockClear + * @see mockRestore + * @see mockRevert */ mockReset(): this /** * Does what `mockReset` does and restores inner implementation to the original function. * - * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. Restoring a `vi.fn(impl)` will restore implementation to `impl`. + * Restoring a mock from `vi.spyOn(object, property)` will also restore the original + * descriptor of the spied-on object. + * + * Note that restoring mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. + * Restoring a mock from `vi.fn(impl)` will set implementation to `impl`. + * + * @see mockClear + * @see mockReset + * @see mockRevert */ mockRestore(): void + /** + * Does what `mockReset` does and reverts inner implementation to the original function. + * + * Reverting a mock from `vi.spyOn(object, property)` will **not** restore the original + * descriptor of the spied-on object. + * + * Note that reverting a mock from `vi.fn()` will set implementation to an empty function that returns `undefined`. + * Reverting a mock from `vi.fn(impl)` will set implementation to `impl`. + * + * @see mockClear + * @see mockReset + * @see mockRestore + */ + mockRevert(): void /** * Returns current mock implementation if there is one. * @@ -520,6 +551,12 @@ function enhanceSpy( return stub } + stub.mockRevert = () => { + stub.mockReset() + implementation = undefined + return stub + } + stub.getMockImplementation = () => implementation stub.mockImplementation = (fn: T) => { implementation = fn diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 07bc35a37910..2b676825fd8f 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -106,6 +106,7 @@ const config = { pool: 'forks' as const, clearMocks: false, restoreMocks: false, + revertMocks: false, mockReset: false, unstubGlobals: false, unstubEnvs: false, diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index c378884f008e..f04ba64d7524 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -315,24 +315,41 @@ export interface VitestUtils { isMockFunction: (fn: any) => fn is MockInstance /** - * Calls [`.mockClear()`](https://vitest.dev/api/mock#mockclear) on every mocked function. This will only empty `.mock` state, it will not reset implementation. + * Calls [`.mockClear()`](https://vitest.dev/api/mock#mockclear) on every mocked function. * - * It is useful if you need to clean up mock between different assertions. + * This will only empty `.mock` state, it will not affect mock implementations. + * + * This is useful if you need to clean up mocks between different assertions within a test. */ clearAllMocks: () => VitestUtils /** - * Calls [`.mockReset()`](https://vitest.dev/api/mock#mockreset) on every mocked function. This will empty `.mock` state, reset "once" implementations and force the base implementation to return `undefined` when invoked. + * Calls [`.mockReset()`](https://vitest.dev/api/mock#mockreset) on every mocked function. + * + * This will empty `.mock` state, reset "once" implementations and force the base implementation to return `undefined` when invoked. * - * This is useful when you want to completely reset a mock to the default state. + * This is useful when you want to completely reset mocks to the default state. */ resetAllMocks: () => VitestUtils /** - * Calls [`.mockRestore()`](https://vitest.dev/api/mock#mockrestore) on every mocked function. This will restore all original implementations. + * Calls [`.mockRestore()`](https://vitest.dev/api/mock#mockrestore) on every mocked function. + * + * This will empty `.mock` state, restore all original mock implementations, and restore original descriptors of spied-on objects. + * + * This is useful for inter-test cleanup and/or removing mocks created by [`vi.spyOn(...)`](https://vitest.dev/api/vi#vi-spyon). */ restoreAllMocks: () => VitestUtils + /** + * Calls [`.mockRevert()`](https://vitest.dev/api/mock#mockrevert) on every mocked function. + * + * This will empty `.mock` state and revert all mocks to their original implementations without restoring original descriptors of spied-on objects. + * + * This is useful for inter-test cleanup without removing mocks created by [`vi.spyOn(...)`](https://vitest.dev/api/vi#vi-spyon). + */ + revertAllMocks: () => VitestUtils + /** * Makes value available on global namespace. * Useful, if you want to have global variables available, like `IntersectionObserver`. @@ -655,6 +672,11 @@ function createVitest(): VitestUtils { return utils }, + revertAllMocks() { + mocks.forEach(spy => spy.mockRevert()) + return utils + }, + stubGlobal(name: string | symbol | number, value: any) { if (!_stubsGlobal.has(name)) { _stubsGlobal.set( diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index b9bf3295262e..9699ceefad14 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -774,6 +774,7 @@ export const cliOptionsConfig: VitestCLIOptions = { unstubEnvs: null, related: null, restoreMocks: null, + revertMocks: null, runner: null, mockReset: null, forceRerunTriggers: null, diff --git a/packages/vitest/src/node/config/serializeConfig.ts b/packages/vitest/src/node/config/serializeConfig.ts index 03e78489c54a..3c3df98cb2d2 100644 --- a/packages/vitest/src/node/config/serializeConfig.ts +++ b/packages/vitest/src/node/config/serializeConfig.ts @@ -31,6 +31,7 @@ export function serializeConfig( clearMocks: config.clearMocks, mockReset: config.mockReset, restoreMocks: config.restoreMocks, + revertMocks: config.revertMocks, unstubEnvs: config.unstubEnvs, unstubGlobals: config.unstubGlobals, maxConcurrency: config.maxConcurrency, diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 4b0939dbc2f0..68dd5fe76daa 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -500,6 +500,12 @@ export interface InlineConfig { */ restoreMocks?: boolean + /** + * Will call `.mockRevert()` on all spies before each test + * @default false + */ + revertMocks?: boolean + /** * Will restore all global stubs to their original values before each test * @default false diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 0ecbf4cd1628..937d56daea19 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -28,6 +28,7 @@ export interface SerializedConfig { clearMocks: boolean mockReset: boolean restoreMocks: boolean + revertMocks: boolean unstubGlobals: boolean unstubEnvs: boolean // TODO: make optional @@ -149,6 +150,7 @@ export type RuntimeConfig = Pick< | 'clearMocks' | 'mockReset' | 'restoreMocks' + | 'revertMocks' | 'fakeTimers' | 'maxConcurrency' | 'expect' diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 40d739dea011..b5778bd8af8f 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -192,13 +192,16 @@ export class VitestTestRunner implements VitestRunner { } function clearModuleMocks(config: SerializedConfig) { - const { clearMocks, mockReset, restoreMocks, unstubEnvs, unstubGlobals } + const { clearMocks, mockReset, restoreMocks, revertMocks, unstubEnvs, unstubGlobals } = config // since each function calls another, we can just call one if (restoreMocks) { vi.restoreAllMocks() } + else if (revertMocks) { + vi.revertAllMocks() + } else if (mockReset) { vi.resetAllMocks() } diff --git a/test/core/test/jest-mock.test.ts b/test/core/test/jest-mock.test.ts index 10bd7c27dba7..b7ef0398dbb0 100644 --- a/test/core/test/jest-mock.test.ts +++ b/test/core/test/jest-mock.test.ts @@ -218,7 +218,7 @@ describe('jest mock compat layer', () => { expect(a.mock.invocationCallOrder[0]).toBeLessThan(b.mock.invocationCallOrder[0]) }) - it('getter spyOn', () => { + it('should spy on property getter, and mockRestore should restore original descriptor', () => { const obj = { get getter() { return 'original' @@ -244,9 +244,39 @@ describe('jest mock compat layer', () => { spy.mockRestore() expect(obj.getter).toBe('original') + expect(spy).not.toHaveBeenCalled() }) - it('getter function spyOn', () => { + it('should spy on property getter, and mockRevert should not restore original descriptor', () => { + const obj = { + get getter() { + return 'original' + }, + } + + const spy = vi.spyOn(obj, 'getter', 'get') + + 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') + + spy.mockReturnValue('returned').mockReturnValueOnce('returned-once') + + expect(obj.getter).toBe('returned-once') + expect(obj.getter).toBe('returned') + expect(obj.getter).toBe('returned') + + spy.mockRevert() + + expect(obj.getter).toBe('original') + expect(spy).toHaveBeenCalled() + }) + + it('should spy on function returned from property getter', () => { const obj = { get getter() { return function () { @@ -266,7 +296,7 @@ describe('jest mock compat layer', () => { expect(obj.getter()).toBe('mocked') }) - it('setter spyOn', () => { + it('should spy on property setter (1)', () => { let setValue = 'original' let mockedValue = 'none' @@ -309,7 +339,7 @@ describe('jest mock compat layer', () => { expect(setValue).toBe('last') }) - it('should work - setter', () => { + it('should spy on property setter (2), and mockRestore should restore original descriptor', () => { const obj = { _property: false, set property(value) { @@ -327,12 +357,36 @@ describe('jest mock compat layer', () => { 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 + // like jest, mockRestore restores the original descriptor, + // we are not spying on the setter any more expect(spy).not.toHaveBeenCalled() expect(obj.property).toBe(true) }) + it('should spy on property setter (2), and mockRevert should not restore original descriptor', () => { + 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.mockRevert() + obj.property = true + // unlike jest, vitest includes mockRevert, which does not restore the original descriptor. + // 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 @@ -408,4 +462,15 @@ describe('jest mock compat layer', () => { testFn.mockRestore() expect(testFn()).toBe(true) }) + + it('.mockRevert() should restore initial implementation', () => { + const testFn = vi.fn(() => true) + expect(testFn()).toBe(true) + + testFn.mockReturnValue(false) + expect(testFn()).toBe(false) + + testFn.mockRevert() + expect(testFn()).toBe(true) + }) })