From c971ab186f1678e8949716b700ac56b5e3e7a6ff Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Tue, 7 Feb 2023 11:08:31 +0200 Subject: [PATCH 01/14] fix(jest-mock): do not restore mocks when `jest.resetAllMocks()` is called --- .../jest-mock/src/__tests__/index.test.ts | 59 +++++++-- packages/jest-mock/src/index.ts | 121 +++++++++++------- 2 files changed, 122 insertions(+), 58 deletions(-) diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index 56c7c4adee54..6dfa2f35669d 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1227,18 +1227,6 @@ describe('moduleMocker', () => { 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()'); @@ -1312,6 +1300,45 @@ describe('moduleMocker', () => { ); }); + it('supports resetting all spies', () => { + const methodOneReturn = 0; + const methodTwoReturn = 0; + const obj = { + methodOne() { + return methodOneReturn; + }, + methodTwo() { + return methodTwoReturn; + }, + }; + + moduleMocker.spyOn(obj, 'methodOne').mockReturnValue(10); + moduleMocker.spyOn(obj, 'methodTwo').mockReturnValue(20); + + expect(methodOneReturn).toBe(0); + expect(methodTwoReturn).toBe(0); + + // Return values are mocked. + expect(obj.methodOne()).toBe(10); + expect(obj.methodTwo()).toBe(20); + + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); + + moduleMocker.resetAllMocks(); + + // The methods are still mock functions. + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(true); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); + + expect(methodOneReturn).toBe(0); + expect(methodTwoReturn).toBe(0); + + // The methods return the original return value. + expect(obj.methodOne()).toBe(0); + expect(obj.methodTwo()).toBe(0); + }); + it('supports restoring all spies', () => { let methodOneCalls = 0; let methodTwoCalls = 0; @@ -1336,9 +1363,15 @@ describe('moduleMocker', () => { 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.restoreAllMocks(); - // Then, after resetting all mocks, we call methods again. Only the real + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(false); + expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(false); + + // Then, after restoring all mocks, we call methods again. Only the real // methods should bump their count, not the spies. obj.methodOne(); obj.methodTwo(); diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index e5b84cc59f23..2f2b610ef79e 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -259,6 +259,8 @@ type MockFunctionConfig = { specificMockImpls: Array; }; +type SpyState = {reset?: () => void; restore: () => void}; + const MOCK_CONSTRUCTOR_NAME = 'mockConstructor'; const FUNCTION_NAME_RESERVED_PATTERN = /[\s!-/:-@[-`{-~]/; @@ -507,9 +509,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 @@ -522,7 +523,6 @@ export class ModuleMocker { this._mockConfigRegistry = new WeakMap(); this._spyState = new Set(); this._invocationCallCounter = 1; - this._originalFn = new WeakMap(); } private _getSlots(object?: Record): Array { @@ -615,27 +615,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(); @@ -756,8 +756,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()); @@ -777,18 +777,21 @@ 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; + + if (spyState != null) { + spyState.restore(); + } }; f.mockReturnValueOnce = (value: ReturnType) => @@ -992,14 +995,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; } } @@ -1200,20 +1203,40 @@ 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: () => { + mock.mockImplementation(function (this: unknown) { + return original.apply(this, arguments); + }); + }, + 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]; - } - }); + mock = this._makeComponent( + {type: 'function'}, + { + reset: () => { + mock.mockImplementation(function (this: unknown) { + return original.apply(this, arguments); + }); + }, + restore: () => { + if (isMethodOwner) { + object[methodKey] = original; + } else { + delete object[methodKey]; + } + }, + }, + ); // @ts-expect-error overriding original method with a Mock object[methodKey] = mock; } @@ -1222,7 +1245,7 @@ export class ModuleMocker { return original.apply(this, arguments); }); } - this._originalFn.set(object[methodKey] as Mock, original); + return object[methodKey] as Mock; } @@ -1274,11 +1297,19 @@ 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: () => { + // TODO + }, + restore: () => { + // @ts-expect-error: mock is assignable + descriptor![accessType] = original; + Object.defineProperty(object, propertyKey, descriptor!); + }, + }, + ); (descriptor[accessType] as Mock).mockImplementation(function ( this: unknown, @@ -1390,7 +1421,7 @@ export class ModuleMocker { restore: () => { restore(); - this._spyState.delete(restore); + this._spyState.delete({restore}); }, }; @@ -1398,7 +1429,7 @@ export class ModuleMocker { restore.property = propertyKey; restore.replaced = replaced; - this._spyState.add(restore); + this._spyState.add({restore}); return replaced.replaceValue(value); } @@ -1408,13 +1439,13 @@ export class ModuleMocker { } resetAllMocks(): void { - this._spyState.forEach(reset => reset()); this._mockConfigRegistry = new WeakMap(); this._mockState = new WeakMap(); + this._spyState.forEach(spyState => spyState.reset?.()); } restoreAllMocks(): void { - this._spyState.forEach(restore => restore()); + this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); } From 8e5b4c6ca4bb4c4a74c635dad1524c1b54d8d905 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Tue, 7 Feb 2023 13:07:27 +0200 Subject: [PATCH 02/14] always clear mocks before restoring --- packages/jest-mock/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 2f2b610ef79e..ab696f5396c2 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -787,7 +787,7 @@ export class ModuleMocker { }; f.mockRestore = () => { - f.mockReset(); + f.mockClear(); if (spyState != null) { spyState.restore(); @@ -1439,12 +1439,13 @@ export class ModuleMocker { } resetAllMocks(): void { - this._mockConfigRegistry = new WeakMap(); this._mockState = new WeakMap(); + this._mockConfigRegistry = new WeakMap(); this._spyState.forEach(spyState => spyState.reset?.()); } restoreAllMocks(): void { + this._mockState = new WeakMap(); this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); } From bcc33cfe6027da7efd33553d9866168ad9b19876 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Tue, 7 Feb 2023 13:07:34 +0200 Subject: [PATCH 03/14] more tests --- .../jest-mock/src/__tests__/index.test.ts | 162 +++++++++++++++--- 1 file changed, 135 insertions(+), 27 deletions(-) diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index 6dfa2f35669d..ffb653f7bb9f 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1215,18 +1215,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('mockName gets reset by mockRestore', () => { const fn = jest.fn(); expect(fn.getMockName()).toBe('jest.fn()'); @@ -1300,6 +1288,97 @@ 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 = 0; const methodTwoReturn = 0; @@ -1315,10 +1394,9 @@ describe('moduleMocker', () => { moduleMocker.spyOn(obj, 'methodOne').mockReturnValue(10); moduleMocker.spyOn(obj, 'methodTwo').mockReturnValue(20); + // Return values are mocked. expect(methodOneReturn).toBe(0); expect(methodTwoReturn).toBe(0); - - // Return values are mocked. expect(obj.methodOne()).toBe(10); expect(obj.methodTwo()).toBe(20); @@ -1327,18 +1405,47 @@ describe('moduleMocker', () => { moduleMocker.resetAllMocks(); - // The methods are still mock functions. + // 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(0); expect(methodTwoReturn).toBe(0); - - // The methods return the original return value. expect(obj.methodOne()).toBe(0); expect(obj.methodTwo()).toBe(0); }); + it('supports restoring 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.mockRestore(); + + // After restoring the spy, the method is not mock function. + expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(false); + + obj.methodOne(); + + // After restoring the spy only the real method bumps its call count, not the spy. + expect(methodOneCalls).toBe(2); + expect(spy1.mock.calls).toHaveLength(0); + }); + it('supports restoring all spies', () => { let methodOneCalls = 0; let methodTwoCalls = 0; @@ -1354,10 +1461,10 @@ describe('moduleMocker', () => { const spy1 = moduleMocker.spyOn(obj, 'methodOne'); const spy2 = moduleMocker.spyOn(obj, 'methodTwo'); - // First, we call with the spies: both spies and both original functions - // should be called. 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); @@ -1368,17 +1475,18 @@ describe('moduleMocker', () => { moduleMocker.restoreAllMocks(); + // After restoring all mocks, the methods are not mock functions. expect(moduleMocker.isMockFunction(obj.methodOne)).toBe(false); expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(false); - // Then, after restoring all mocks, we call methods again. Only the real - // methods should bump their count, not the spies. obj.methodOne(); obj.methodTwo(); + + // After restoring all mocks only the real methods bump their count, not the spies. expect(methodOneCalls).toBe(2); expect(methodTwoCalls).toBe(2); - expect(spy1.mock.calls).toHaveLength(1); - expect(spy2.mock.calls).toHaveLength(1); + expect(spy1.mock.calls).toHaveLength(0); + expect(spy2.mock.calls).toHaveLength(0); }); it('should work with getters', () => { @@ -1551,8 +1659,8 @@ describe('moduleMocker', () => { obj.methodTwo(); expect(methodOneCalls).toBe(2); expect(methodTwoCalls).toBe(2); - expect(spy1.mock.calls).toHaveLength(1); - expect(spy2.mock.calls).toHaveLength(1); + expect(spy1.mock.calls).toHaveLength(0); + expect(spy2.mock.calls).toHaveLength(0); }); it('should work with getters on the prototype chain', () => { @@ -1657,8 +1765,8 @@ describe('moduleMocker', () => { obj.methodTwo(); expect(methodOneCalls).toBe(2); expect(methodTwoCalls).toBe(2); - expect(spy1.mock.calls).toHaveLength(1); - expect(spy2.mock.calls).toHaveLength(1); + expect(spy1.mock.calls).toHaveLength(0); + expect(spy2.mock.calls).toHaveLength(0); }); }); From aaedbfc1b0a08a22478eea4d8c668c34e994192a Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 11:15:16 +0200 Subject: [PATCH 04/14] fix? --- packages/jest-mock/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index ab696f5396c2..33339fd1808f 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -788,6 +788,7 @@ export class ModuleMocker { f.mockRestore = () => { f.mockClear(); + this._mockConfigRegistry.delete(f); if (spyState != null) { spyState.restore(); From d2f0dd8050e5a81317474563a43ecddeb886f23f Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 11:18:42 +0200 Subject: [PATCH 05/14] better call `mockReset()` --- packages/jest-mock/src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 33339fd1808f..48044696de8f 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -787,8 +787,7 @@ export class ModuleMocker { }; f.mockRestore = () => { - f.mockClear(); - this._mockConfigRegistry.delete(f); + f.mockClear().mockReset(); if (spyState != null) { spyState.restore(); From 18877e4b5f1f84bfb6bc966a021df0baccd520ca Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 11:53:27 +0200 Subject: [PATCH 06/14] handle property accessors --- .../jest-mock/src/__tests__/index.test.ts | 132 ++++++++++++++++-- packages/jest-mock/src/index.ts | 7 +- 2 files changed, 126 insertions(+), 13 deletions(-) diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index cf1c14ce40c7..67c7f6b4c7c7 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1380,8 +1380,8 @@ describe('moduleMocker', () => { }); it('supports resetting all spies', () => { - const methodOneReturn = 0; - const methodTwoReturn = 0; + const methodOneReturn = 10; + const methodTwoReturn = 20; const obj = { methodOne() { return methodOneReturn; @@ -1391,14 +1391,14 @@ describe('moduleMocker', () => { }, }; - moduleMocker.spyOn(obj, 'methodOne').mockReturnValue(10); - moduleMocker.spyOn(obj, 'methodTwo').mockReturnValue(20); + moduleMocker.spyOn(obj, 'methodOne').mockReturnValue(100); + moduleMocker.spyOn(obj, 'methodTwo').mockReturnValue(200); // Return values are mocked. - expect(methodOneReturn).toBe(0); - expect(methodTwoReturn).toBe(0); - expect(obj.methodOne()).toBe(10); - expect(obj.methodTwo()).toBe(20); + 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); @@ -1410,10 +1410,10 @@ describe('moduleMocker', () => { expect(moduleMocker.isMockFunction(obj.methodTwo)).toBe(true); // After resetting all mocks, the methods return the original return value. - expect(methodOneReturn).toBe(0); - expect(methodTwoReturn).toBe(0); - expect(obj.methodOne()).toBe(0); - expect(obj.methodTwo()).toBe(0); + expect(methodOneReturn).toBe(10); + expect(methodTwoReturn).toBe(20); + expect(obj.methodOne()).toBe(10); + expect(obj.methodTwo()).toBe(20); }); it('supports restoring a spy', () => { @@ -1623,6 +1623,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 = { @@ -1755,6 +1808,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 48044696de8f..4cd9dae8026c 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1301,7 +1301,12 @@ export class ModuleMocker { {type: 'function'}, { reset: () => { - // TODO + (descriptor![accessType] as Mock).mockImplementation(function ( + this: unknown, + ) { + // @ts-expect-error - wrong context + return original.apply(this, arguments); + }); }, restore: () => { // @ts-expect-error: mock is assignable From 7c37365d6c6f0431807af8d962dda533d7b5b646 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 12:08:12 +0200 Subject: [PATCH 07/14] factor out `attachMockImplementation` --- packages/jest-mock/src/index.ts | 42 +++++++++++++++------------------ 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 4cd9dae8026c..e71a68f73335 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1118,6 +1118,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, @@ -1207,9 +1216,7 @@ export class ModuleMocker { {type: 'function'}, { reset: () => { - mock.mockImplementation(function (this: unknown) { - return original.apply(this, arguments); - }); + this._attachMockImplementation(mock, original); }, restore: () => { descriptor!.get = originalGet; @@ -1224,9 +1231,7 @@ export class ModuleMocker { {type: 'function'}, { reset: () => { - mock.mockImplementation(function (this: unknown) { - return original.apply(this, arguments); - }); + this._attachMockImplementation(mock, original); }, restore: () => { if (isMethodOwner) { @@ -1237,13 +1242,11 @@ export class ModuleMocker { }, }, ); - // @ts-expect-error overriding original method with a Mock + // @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); } return object[methodKey] as Mock; @@ -1301,27 +1304,20 @@ export class ModuleMocker { {type: 'function'}, { reset: () => { - (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, + ); }, restore: () => { - // @ts-expect-error: mock is assignable + // @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); From 2da3e7ca7ccdec77803bae345ce151a35c7e6b38 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 12:30:40 +0200 Subject: [PATCH 08/14] refactor methods --- packages/jest-mock/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index e71a68f73335..fe592843e24c 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1440,13 +1440,13 @@ export class ModuleMocker { } resetAllMocks(): void { - this._mockState = new WeakMap(); + this.clearAllMocks(); this._mockConfigRegistry = new WeakMap(); this._spyState.forEach(spyState => spyState.reset?.()); } restoreAllMocks(): void { - this._mockState = new WeakMap(); + this.resetAllMocks(); this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); } From 76d6f007d7bddb3a5415854a2fbc25828d867636 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 12:33:24 +0200 Subject: [PATCH 09/14] cleaner --- packages/jest-mock/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index fe592843e24c..4d0ee299dd86 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -787,7 +787,7 @@ export class ModuleMocker { }; f.mockRestore = () => { - f.mockClear().mockReset(); + f.mockReset(); if (spyState != null) { spyState.restore(); From 25df46238eda7ac677722071c85d5ee2fd47a41e Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 12:36:15 +0200 Subject: [PATCH 10/14] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c409ba51bc7..055c39be0002 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - `[jest-mock]` Clear mock state when `jest.restoreAllMocks()` is called ([#13867](https://github.com/facebook/jest/pull/13867)) +- `[jest-mock]` Do not restore mocks when `jest.resetAllMocks()` is called ([#13866](https://github.com/facebook/jest/pull/13866)) ### Chore & Maintenance From 4ca3ae2754609a38a3b317f42375dac3a0145c32 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 13:42:27 +0200 Subject: [PATCH 11/14] revert "refactor methods" --- packages/jest-mock/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 4d0ee299dd86..fd79d11a1ab8 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1440,13 +1440,14 @@ export class ModuleMocker { } resetAllMocks(): void { - this.clearAllMocks(); + this._mockState = new WeakMap(); this._mockConfigRegistry = new WeakMap(); this._spyState.forEach(spyState => spyState.reset?.()); } restoreAllMocks(): void { - this.resetAllMocks(); + this._mockState = new WeakMap(); + this._mockConfigRegistry = new WeakMap(); this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); } From 46580819375ddabfa6718d00d48eec5308af8a0f Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 14:34:17 +0200 Subject: [PATCH 12/14] use optional chaining --- packages/jest-mock/src/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index fd79d11a1ab8..ecb0c930d965 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1122,7 +1122,7 @@ export class ModuleMocker { mock: Mock, original: T, ) { - mock.mockImplementation(function (this: unknown) { + mock.mockImplementation?.(function (this: unknown) { return original.apply(this, arguments); }); } @@ -1440,14 +1440,13 @@ export class ModuleMocker { } resetAllMocks(): void { - this._mockState = new WeakMap(); + this.clearAllMocks(); this._mockConfigRegistry = new WeakMap(); this._spyState.forEach(spyState => spyState.reset?.()); } restoreAllMocks(): void { - this._mockState = new WeakMap(); - this._mockConfigRegistry = new WeakMap(); + this.resetAllMocks(); this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); } From 88fef348079fd6a3e0ea6fce1dfce2919a902656 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 19:44:13 +0200 Subject: [PATCH 13/14] dispose state --- packages/jest-mock/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index ecb0c930d965..526c4c3b6855 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -791,6 +791,7 @@ export class ModuleMocker { if (spyState != null) { spyState.restore(); + this._spyState.delete(spyState); } }; @@ -1122,7 +1123,7 @@ export class ModuleMocker { mock: Mock, original: T, ) { - mock.mockImplementation?.(function (this: unknown) { + mock.mockImplementation(function (this: unknown) { return original.apply(this, arguments); }); } From 4991beace3e5c562fd21e3c59d01136df5873980 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Wed, 8 Feb 2023 21:55:07 +0200 Subject: [PATCH 14/14] perf: no need to reset before restoring --- packages/jest-mock/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 526c4c3b6855..0c7b1c833045 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -787,7 +787,8 @@ export class ModuleMocker { }; f.mockRestore = () => { - f.mockReset(); + f.mockClear(); + this._mockConfigRegistry.delete(f); if (spyState != null) { spyState.restore(); @@ -1447,7 +1448,8 @@ export class ModuleMocker { } restoreAllMocks(): void { - this.resetAllMocks(); + this.clearAllMocks(); + this._mockConfigRegistry = new WeakMap(); this._spyState.forEach(spyState => spyState.restore()); this._spyState = new Set(); }