diff --git a/CHANGELOG.md b/CHANGELOG.md index ce86f3d161fa..46b486654d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `[jest-mock]` Clear mock state when `jest.restoreAllMocks()` is called ([#13867](https://github.com/facebook/jest/pull/13867)) - `[jest-mock]` Prevent `mockImplementationOnce` and `mockReturnValueOnce` bleeding into `withImplementation` ([#13888](https://github.com/facebook/jest/pull/13888)) +- `[jest-mock]` Do not restore mocks when `jest.resetAllMocks()` is called ([#13866](https://github.com/facebook/jest/pull/13866)) ### Chore & Maintenance diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index 1b13b93acd72..8f178c896b8d 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1247,30 +1247,6 @@ describe('moduleMocker', () => { expect(fn.getMockName()).toBe('jest.fn()'); }); - test('after mock reset, the object should return to its original value', () => { - const myObject = {bar: () => 'bar'}; - - const barStub = moduleMocker.spyOn(myObject, 'bar'); - - barStub.mockReturnValue('POTATO!'); - expect(myObject.bar()).toBe('POTATO!'); - barStub.mockReset(); - - expect(myObject.bar()).toBe('bar'); - }); - - test('after resetAllMocks, the object should return to its original value', () => { - const myObject = {bar: () => 'bar'}; - - const barStub = moduleMocker.spyOn(myObject, 'bar'); - - barStub.mockReturnValue('POTATO!'); - expect(myObject.bar()).toBe('POTATO!'); - moduleMocker.resetAllMocks(); - - expect(myObject.bar()).toBe('bar'); - }); - test('mockName gets reset by mockRestore', () => { const fn = jest.fn(); expect(fn.getMockName()).toBe('jest.fn()'); @@ -1344,6 +1320,134 @@ describe('moduleMocker', () => { ); }); + it('supports clearing a spy', () => { + let methodOneCalls = 0; + const obj = { + methodOne() { + methodOneCalls++; + }, + }; + + const spy1 = moduleMocker.spyOn(obj, 'methodOne'); + + obj.methodOne(); + + // The spy and the original function are called. + expect(methodOneCalls).toBe(1); + expect(spy1.mock.calls).toHaveLength(1); + + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + + spy1.mockClear(); + + // After clearing the spy, the method is still mock function. + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + + // After clearing the spy, call count is reset. + expect(spy1.mock.calls).toHaveLength(0); + }); + + it('supports clearing all spies', () => { + let methodOneCalls = 0; + let methodTwoCalls = 0; + const obj = { + methodOne() { + methodOneCalls++; + }, + methodTwo() { + methodTwoCalls++; + }, + }; + + const spy1 = moduleMocker.spyOn(obj, 'methodOne'); + const spy2 = moduleMocker.spyOn(obj, 'methodTwo'); + + obj.methodOne(); + obj.methodTwo(); + + // Both spies and both original functions are called. + expect(methodOneCalls).toBe(1); + expect(methodTwoCalls).toBe(1); + expect(spy1.mock.calls).toHaveLength(1); + expect(spy2.mock.calls).toHaveLength(1); + + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); + + moduleMocker.clearAllMocks(); + + // After clearing all mocks, the methods are still mock functions. + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); + + // After clearing all mocks, call counts are reset. + expect(spy1.mock.calls).toHaveLength(0); + expect(spy2.mock.calls).toHaveLength(0); + }); + + it('supports resetting a spy', () => { + const methodOneReturn = 0; + const obj = { + methodOne() { + return methodOneReturn; + }, + }; + + const spy1 = moduleMocker.spyOn(obj, 'methodOne').mockReturnValue(10); + + // Return value is mocked. + expect(methodOneReturn).toBe(0); + expect(obj.methodOne()).toBe(10); + + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + + spy1.mockReset(); + + // After resetting the spy, the method is still mock functions. + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + + // After resetting the spy, the method returns the original return value. + expect(methodOneReturn).toBe(0); + expect(obj.methodOne()).toBe(0); + }); + + it('supports resetting all spies', () => { + const methodOneReturn = 10; + const methodTwoReturn = 20; + const obj = { + methodOne() { + return methodOneReturn; + }, + methodTwo() { + return methodTwoReturn; + }, + }; + + moduleMocker.spyOn(obj, 'methodOne').mockReturnValue(100); + moduleMocker.spyOn(obj, 'methodTwo').mockReturnValue(200); + + // Return values are mocked. + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne()).toBe(100); + expect(obj.methodTwo()).toBe(200); + + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); + + moduleMocker.resetAllMocks(); + + // After resetting all mocks, the methods are still mock functions. + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); + + // After resetting all mocks, the methods return the original return value. + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne()).toBe(10); + expect(obj.methodTwo()).toBe(20); + }); + it('supports restoring a spy', () => { let methodOneCalls = 0; const obj = { @@ -1551,6 +1655,59 @@ describe('moduleMocker', () => { ); }); + it('supports resetting a spy', () => { + const methodOneReturn = 0; + const obj = { + get methodOne() { + return methodOneReturn; + }, + }; + + const spy1 = moduleMocker + .spyOn(obj, 'methodOne', 'get') + .mockReturnValue(10); + + // Return value is mocked. + expect(methodOneReturn).toBe(0); + expect(obj.methodOne).toBe(10); + + spy1.mockReset(); + + // After resetting the spy, the method returns the original return value. + expect(methodOneReturn).toBe(0); + expect(obj.methodOne).toBe(0); + }); + + it('supports resetting all spies', () => { + const methodOneReturn = 10; + const methodTwoReturn = 20; + const obj = { + get methodOne() { + return methodOneReturn; + }, + get methodTwo() { + return methodTwoReturn; + }, + }; + + moduleMocker.spyOn(obj, 'methodOne', 'get').mockReturnValue(100); + moduleMocker.spyOn(obj, 'methodTwo', 'get').mockReturnValue(200); + + // Return values are mocked. + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne).toBe(100); + expect(obj.methodTwo).toBe(200); + + moduleMocker.resetAllMocks(); + + // After resetting all mocks, the methods return the original return value. + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne).toBe(10); + expect(obj.methodTwo).toBe(20); + }); + it('supports restoring a spy', () => { let methodOneCalls = 0; const obj = { @@ -1683,6 +1840,61 @@ describe('moduleMocker', () => { expect(obj.property).toBe(true); }); + it('supports resetting a spy on the prototype chain', () => { + const methodOneReturn = 0; + const prototype = { + get methodOne() { + return methodOneReturn; + }, + }; + const obj = Object.create(prototype, {}); + + const spy1 = moduleMocker + .spyOn(obj, 'methodOne', 'get') + .mockReturnValue(10); + + // Return value is mocked. + expect(methodOneReturn).toBe(0); + expect(obj.methodOne).toBe(10); + + spy1.mockReset(); + + // After resetting the spy, the method returns the original return value. + expect(methodOneReturn).toBe(0); + expect(obj.methodOne).toBe(0); + }); + + it('supports resetting all spies on the prototype chain', () => { + const methodOneReturn = 10; + const methodTwoReturn = 20; + const prototype = { + get methodOne() { + return methodOneReturn; + }, + get methodTwo() { + return methodTwoReturn; + }, + }; + const obj = Object.create(prototype, {}); + + moduleMocker.spyOn(obj, 'methodOne', 'get').mockReturnValue(100); + moduleMocker.spyOn(obj, 'methodTwo', 'get').mockReturnValue(200); + + // Return values are mocked. + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne).toBe(100); + expect(obj.methodTwo).toBe(200); + + moduleMocker.resetAllMocks(); + + // After resetting all mocks, the methods return the original return value. + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne).toBe(10); + expect(obj.methodTwo).toBe(20); + }); + it('supports restoring a spy on the prototype chain', () => { let methodOneCalls = 0; const prototype = { diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index bec770b88ef9..8456c1312ac7 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -258,6 +258,8 @@ type MockFunctionConfig = { specificMockImpls: Array; }; +type SpyState = {reset?: () => void; restore: () => void}; + const MOCK_CONSTRUCTOR_NAME = 'mockConstructor'; const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-/:-@[-`{-~]/; @@ -506,9 +508,8 @@ export class ModuleMocker { private readonly _environmentGlobal: typeof globalThis; private _mockState: WeakMap; private _mockConfigRegistry: WeakMap; - private _spyState: Set<() => void>; + private _spyState: Set; private _invocationCallCounter: number; - private _originalFn: WeakMap; /** * @see README.md @@ -521,7 +522,6 @@ export class ModuleMocker { this._mockConfigRegistry = new WeakMap(); this._spyState = new Set(); this._invocationCallCounter = 1; - this._originalFn = new WeakMap(); } private _getSlots(object?: Record): Array { @@ -613,27 +613,27 @@ export class ModuleMocker { private _makeComponent>( metadata: MockMetadata, - restore?: () => void, + spyState?: SpyState, ): T; private _makeComponent>( metadata: MockMetadata, - restore?: () => void, + spyState?: SpyState, ): T; private _makeComponent( metadata: MockMetadata, - restore?: () => void, + spyState?: SpyState, ): T; private _makeComponent( metadata: MockMetadata, - restore?: () => void, + spyState?: SpyState, ): T; private _makeComponent( metadata: MockMetadata, - restore?: () => void, + spyState?: SpyState, ): Mock; private _makeComponent( metadata: MockMetadata, - restore?: () => void, + spyState?: SpyState, ): Record | Array | RegExp | T | Mock | undefined { if (metadata.type === 'object') { return new this._environmentGlobal.Object(); @@ -754,8 +754,8 @@ export class ModuleMocker { f._isMockFunction = true; f.getMockImplementation = () => this._ensureMockConfig(f).mockImpl as T; - if (typeof restore === 'function') { - this._spyState.add(restore); + if (spyState != null) { + this._spyState.add(spyState); } this._mockState.set(f, this._defaultMockState()); @@ -775,18 +775,23 @@ export class ModuleMocker { f.mockReset = () => { f.mockClear(); - const originalFn = this._originalFn.get(f); - const originalMockImpl = { - ...this._defaultMockConfig(), - mockImpl: originalFn, - }; - this._mockConfigRegistry.set(f, originalMockImpl); + this._mockConfigRegistry.delete(f); + + if (spyState != null) { + spyState.reset?.(); + } + return f; }; f.mockRestore = () => { - f.mockReset(); - return restore ? restore() : undefined; + f.mockClear(); + this._mockConfigRegistry.delete(f); + + if (spyState != null) { + spyState.restore(); + this._spyState.delete(spyState); + } }; f.mockReturnValueOnce = (value: ReturnType) => @@ -994,14 +999,14 @@ export class ModuleMocker { T extends object, K extends PropertyLikeKeys, >(object: T, propertyKey: K): ReplacedPropertyRestorer | undefined { - for (const spyState of this._spyState) { + for (const {restore} of this._spyState) { if ( - 'object' in spyState && - 'property' in spyState && - spyState.object === object && - spyState.property === propertyKey + 'object' in restore && + 'property' in restore && + restore.object === object && + restore.property === propertyKey ) { - return spyState as ReplacedPropertyRestorer; + return restore as ReplacedPropertyRestorer; } } @@ -1117,6 +1122,15 @@ export class ModuleMocker { return fn; } + private _attachMockImplementation( + mock: Mock, + original: T, + ) { + mock.mockImplementation(function (this: unknown) { + return original.apply(this, arguments); + }); + } + spyOn< T extends object, K extends PropertyLikeKeys, @@ -1202,29 +1216,43 @@ export class ModuleMocker { if (descriptor && descriptor.get) { const originalGet = descriptor.get; - mock = this._makeComponent({type: 'function'}, () => { - descriptor!.get = originalGet; - Object.defineProperty(object, methodKey, descriptor!); - }); + mock = this._makeComponent( + {type: 'function'}, + { + reset: () => { + this._attachMockImplementation(mock, original); + }, + restore: () => { + descriptor!.get = originalGet; + Object.defineProperty(object, methodKey, descriptor!); + }, + }, + ); descriptor.get = () => mock; Object.defineProperty(object, methodKey, descriptor); } else { - mock = this._makeComponent({type: 'function'}, () => { - if (isMethodOwner) { - object[methodKey] = original; - } else { - delete object[methodKey]; - } - }); - // @ts-expect-error overriding original method with a Mock + mock = this._makeComponent( + {type: 'function'}, + { + reset: () => { + this._attachMockImplementation(mock, original); + }, + restore: () => { + if (isMethodOwner) { + object[methodKey] = original; + } else { + delete object[methodKey]; + } + }, + }, + ); + // @ts-expect-error: overriding original method with a mock object[methodKey] = mock; } - mock.mockImplementation(function (this: unknown) { - return original.apply(this, arguments); - }); + this._attachMockImplementation(mock, original); } - this._originalFn.set(object[methodKey] as Mock, original); + return object[methodKey] as Mock; } @@ -1276,18 +1304,24 @@ export class ModuleMocker { ); } - descriptor[accessType] = this._makeComponent({type: 'function'}, () => { - // @ts-expect-error: mock is assignable - descriptor![accessType] = original; - Object.defineProperty(object, propertyKey, descriptor!); - }); + descriptor[accessType] = this._makeComponent( + {type: 'function'}, + { + reset: () => { + this._attachMockImplementation( + descriptor![accessType] as Mock, + original, + ); + }, + restore: () => { + // @ts-expect-error: overriding original method with a mock + descriptor![accessType] = original; + Object.defineProperty(object, propertyKey, descriptor!); + }, + }, + ); - (descriptor[accessType] as Mock).mockImplementation(function ( - this: unknown, - ) { - // @ts-expect-error - wrong context - return original.apply(this, arguments); - }); + this._attachMockImplementation(descriptor[accessType] as Mock, original); } Object.defineProperty(object, propertyKey, descriptor); @@ -1392,7 +1426,7 @@ export class ModuleMocker { restore: () => { restore(); - this._spyState.delete(restore); + this._spyState.delete({restore}); }, }; @@ -1400,7 +1434,7 @@ export class ModuleMocker { restore.property = propertyKey; restore.replaced = replaced; - this._spyState.add(restore); + this._spyState.add({restore}); return replaced.replaceValue(value); } @@ -1410,14 +1444,15 @@ export class ModuleMocker { } resetAllMocks(): void { - this._spyState.forEach(reset => reset()); + this.clearAllMocks(); this._mockConfigRegistry = new WeakMap(); - this._mockState = new WeakMap(); + this._spyState.forEach(spyState => spyState.reset?.()); } restoreAllMocks(): void { - this._mockState = new WeakMap(); - this._spyState.forEach(restore => restore()); + this.clearAllMocks(); + this._mockConfigRegistry = new WeakMap(); + this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); }