diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf57eb1ecf2..dfff58ba0d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ ### Performance +## 29.1.1 + +### Fixes + +- `[jest-mock]` Revert ([#13145](https://github.com/facebook/jest/pull/13145)) + ## 29.1.0 ### Features diff --git a/jest.config.mjs b/jest.config.mjs index 5459b96db2b2..192ccd07dcfa 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -58,9 +58,6 @@ export default { '/packages/jest-haste-map/src/__tests__/haste_impl.js', '/packages/jest-haste-map/src/__tests__/dependencyExtractor.js', '/packages/jest-haste-map/src/__tests__/test_dotfiles_root/', - '/packages/jest-mock/src/__tests__/class-mocks-types.ts', - '/packages/jest-mock/src/__tests__/TestClass.ts', - '/packages/jest-mock/src/__tests__/SuperTestClass.ts', '/packages/jest-repl/src/__tests__/test_root', '/packages/jest-resolve-dependencies/src/__tests__/__fixtures__/', '/packages/jest-runtime/src/__tests__/defaultResolver.js', diff --git a/packages/jest-mock/src/__tests__/SuperTestClass.ts b/packages/jest-mock/src/__tests__/SuperTestClass.ts deleted file mode 100644 index ae1c262e34f6..000000000000 --- a/packages/jest-mock/src/__tests__/SuperTestClass.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export class SuperTestClass { - static staticTestProperty = 'staticTestProperty'; - - static get staticTestAccessor(): string { - return 'staticTestAccessor'; - } - - static set staticTestAccessor(_x: string) { - return; - } - - static staticTestMethod(): string { - return 'staticTestMethod'; - } - - testProperty = 'testProperty'; - - get testAccessor(): string { - return 'testAccessor'; - } - set testAccessor(_x: string) { - return; - } - - testMethod(): string { - return 'testMethod'; - } -} diff --git a/packages/jest-mock/src/__tests__/TestClass.ts b/packages/jest-mock/src/__tests__/TestClass.ts deleted file mode 100644 index e4197db98abe..000000000000 --- a/packages/jest-mock/src/__tests__/TestClass.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import {SuperTestClass} from './SuperTestClass'; - -export default class TestClass extends SuperTestClass {} diff --git a/packages/jest-mock/src/__tests__/class-mocks-dual-import.test.ts b/packages/jest-mock/src/__tests__/class-mocks-dual-import.test.ts deleted file mode 100644 index 510484d06700..000000000000 --- a/packages/jest-mock/src/__tests__/class-mocks-dual-import.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import {SuperTestClass} from './SuperTestClass'; -import TestClass from './TestClass'; -jest.mock('./SuperTestClass'); -jest.mock('./TestClass'); - -describe('Testing the mocking of a class hierarchy defined in multiple imports', () => { - it('can call an instance method - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass.prototype, 'testMethod') - .mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new SuperTestClass(); - expect(testClassInstance.testMethod()).toEqual('mockTestMethod'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can call a superclass instance method - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass.prototype, 'testMethod') - .mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testMethod()).toEqual('mockTestMethod'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can read a value from an instance getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass.prototype, 'testAccessor', 'get') - .mockImplementation(() => { - return 'mockTestAccessor'; - }); - const testClassInstance = new SuperTestClass(); - expect(testClassInstance.testAccessor).toEqual('mockTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can read a value from a superclass instance getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass.prototype, 'testAccessor', 'get') - .mockImplementation(() => { - return 'mockTestAccessor'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testAccessor).toEqual('mockTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can write a value to an instance setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass.prototype, 'testAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - const testClassInstance = new SuperTestClass(); - testClassInstance.testAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can write a value to a superclass instance setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass.prototype, 'testAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - const testClassInstance = new TestClass(); - testClassInstance.testAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can read a value from a static getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass, 'staticTestAccessor', 'get') - .mockImplementation(() => { - return 'mockStaticTestAccessor'; - }); - expect(SuperTestClass.staticTestAccessor).toEqual('mockStaticTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can read a value from a superclass static getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass, 'staticTestAccessor', 'get') - .mockImplementation(() => { - return 'mockStaticTestAccessor'; - }); - expect(TestClass.staticTestAccessor).toEqual('mockStaticTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can write a value to a static setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass, 'staticTestAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - SuperTestClass.staticTestAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can write a value to a superclass static setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass, 'staticTestAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - TestClass.staticTestAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/jest-mock/src/__tests__/class-mocks-single-import.test.ts b/packages/jest-mock/src/__tests__/class-mocks-single-import.test.ts deleted file mode 100644 index 2b326b971ea5..000000000000 --- a/packages/jest-mock/src/__tests__/class-mocks-single-import.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -import SuperTestClass, {TestClass} from './class-mocks-types'; -jest.mock('./class-mocks-types'); - -describe('Testing the mocking of a class hierarchy defined in a single import', () => { - it('can call an instance method - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass.prototype, 'testMethod') - .mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new SuperTestClass(); - expect(testClassInstance.testMethod()).toEqual('mockTestMethod'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can call a superclass instance method - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass.prototype, 'testMethod') - .mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testMethod()).toEqual('mockTestMethod'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can read a value from an instance getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass.prototype, 'testAccessor', 'get') - .mockImplementation(() => { - return 'mockTestAccessor'; - }); - const testClassInstance = new SuperTestClass(); - expect(testClassInstance.testAccessor).toEqual('mockTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can read a value from a superclass instance getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass.prototype, 'testAccessor', 'get') - .mockImplementation(() => { - return 'mockTestAccessor'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testAccessor).toEqual('mockTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can write a value to an instance setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass.prototype, 'testAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - const testClassInstance = new SuperTestClass(); - testClassInstance.testAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can write a value to a superclass instance setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass.prototype, 'testAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - const testClassInstance = new TestClass(); - testClassInstance.testAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can read a value from a static getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass, 'staticTestAccessor', 'get') - .mockImplementation(() => { - return 'mockStaticTestAccessor'; - }); - expect(SuperTestClass.staticTestAccessor).toEqual('mockStaticTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can read a value from a superclass static getter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass, 'staticTestAccessor', 'get') - .mockImplementation(() => { - return 'mockStaticTestAccessor'; - }); - expect(TestClass.staticTestAccessor).toEqual('mockStaticTestAccessor'); - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); - - it('can write a value to a static setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(SuperTestClass, 'staticTestAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - SuperTestClass.staticTestAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - - mockTestMethod.mockClear(); - }); - - it('can write a value to a superclass static setter - Auto-mocked class', () => { - const mockTestMethod = jest - .spyOn(TestClass, 'staticTestAccessor', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - TestClass.staticTestAccessor = ''; - expect(mockTestMethod).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/jest-mock/src/__tests__/class-mocks-types.ts b/packages/jest-mock/src/__tests__/class-mocks-types.ts deleted file mode 100644 index 4e277903940f..000000000000 --- a/packages/jest-mock/src/__tests__/class-mocks-types.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -export default class SuperTestClass { - static staticTestProperty = 'staticTestProperty'; - - static get staticTestAccessor(): string { - return 'staticTestAccessor'; - } - - static set staticTestAccessor(_x: string) { - return; - } - - static staticTestMethod(): string { - return 'staticTestMethod'; - } - - testProperty = 'testProperty'; - - get testAccessor(): string { - return 'testAccessor'; - } - set testAccessor(_x: string) { - return; - } - - testMethod(): string { - return 'testMethod'; - } -} - -export class TestClass extends SuperTestClass {} diff --git a/packages/jest-mock/src/__tests__/class-mocks.test.ts b/packages/jest-mock/src/__tests__/class-mocks.test.ts deleted file mode 100644 index a0120958b54b..000000000000 --- a/packages/jest-mock/src/__tests__/class-mocks.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -describe('Testing the mocking of a class', () => { - it('can call an instance method', () => { - class TestClass { - testMethod(): string { - return 'testMethod'; - } - } - - jest.spyOn(TestClass.prototype, 'testMethod').mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testMethod()).toEqual('mockTestMethod'); - }); - - it('can call a superclass instance method', () => { - class SuperTestClass { - testMethod(): string { - return 'testMethod'; - } - } - - class TestClass extends SuperTestClass {} - - jest.spyOn(TestClass.prototype, 'testMethod').mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testMethod()).toEqual('mockTestMethod'); - }); - - it('can read a value from an instance getter', () => { - class TestClass { - get testMethod(): string { - return 'testMethod'; - } - } - - jest - .spyOn(TestClass.prototype, 'testMethod', 'get') - .mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testMethod).toEqual('mockTestMethod'); - }); - - it('can read a value from an superclass instance getter', () => { - class SuperTestClass { - get testMethod(): string { - return 'testMethod'; - } - } - - class TestClass extends SuperTestClass {} - - jest - .spyOn(TestClass.prototype, 'testMethod', 'get') - .mockImplementation(() => { - return 'mockTestMethod'; - }); - const testClassInstance = new TestClass(); - expect(testClassInstance.testMethod).toEqual('mockTestMethod'); - }); - - it('can write a value to an instance setter', () => { - class TestClass { - // eslint-disable-next-line accessor-pairs - set testMethod(_x: string) { - return; - } - } - - const mocktestMethod = jest - .spyOn(TestClass.prototype, 'testMethod', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - const testClassInstance = new TestClass(); - testClassInstance.testMethod = ''; - expect(mocktestMethod).toHaveBeenCalledTimes(1); - }); - - it('can write a value to a superclass instance setter', () => { - class SuperTestClass { - // eslint-disable-next-line accessor-pairs - set testMethod(_x: string) { - return; - } - } - - class TestClass extends SuperTestClass {} - - const mocktestMethod = jest - .spyOn(TestClass.prototype, 'testMethod', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - const testClassInstance = new TestClass(); - testClassInstance.testMethod = ''; - expect(mocktestMethod).toHaveBeenCalledTimes(1); - }); - - it('can read a value from a static getter', () => { - class TestClass { - static get testMethod(): string { - return 'testMethod'; - } - } - - jest.spyOn(TestClass, 'testMethod', 'get').mockImplementation(() => { - return 'mockTestMethod'; - }); - expect(TestClass.testMethod).toEqual('mockTestMethod'); - }); - - it('can read a value from a superclass static getter', () => { - class SuperTestClass { - static get testMethod(): string { - return 'testMethod'; - } - } - - class TestClass extends SuperTestClass {} - - jest.spyOn(TestClass, 'testMethod', 'get').mockImplementation(() => { - return 'mockTestMethod'; - }); - expect(TestClass.testMethod).toEqual('mockTestMethod'); - }); - - it('can write a value to a static setter', () => { - class TestClass { - // eslint-disable-next-line accessor-pairs - static set testMethod(_x: string) { - return; - } - } - - const mocktestMethod = jest - .spyOn(TestClass, 'testMethod', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - TestClass.testMethod = ''; - expect(mocktestMethod).toHaveBeenCalledTimes(1); - }); - - it('can write a value to a superclass static setter', () => { - class SuperTestClass { - // eslint-disable-next-line accessor-pairs - static set testMethod(_x: string) { - return; - } - } - - class TestClass extends SuperTestClass {} - - const mocktestMethod = jest - .spyOn(TestClass, 'testMethod', 'set') - .mockImplementation((_x: string) => { - return () => {}; - }); - TestClass.testMethod = ''; - expect(mocktestMethod).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index 06aba93300ee..27f7ba18369c 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1320,7 +1320,7 @@ describe('moduleMocker', () => { }, }; - const spy = moduleMocker.spyOn(obj, 'method', 'get'); + const spy = moduleMocker.spyOn(obj, 'method'); const thisArg = {this: true}; const firstArg = {first: true}; diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 117be1f3cbe5..b727d18e749f 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -516,10 +516,7 @@ export class ModuleMocker { if (!isReadonlyProp(object, prop)) { const propDesc = Object.getOwnPropertyDescriptor(object, prop); - if ( - propDesc !== undefined && - !(propDesc.get && prop == '__proto__') - ) { + if ((propDesc !== undefined && !propDesc.get) || object.__esModule) { slots.add(prop); } } @@ -909,9 +906,7 @@ export class ModuleMocker { } this._getSlots(metadata.members).forEach(slot => { - let slotMock: Mocked; const slotMetadata = (metadata.members && metadata.members[slot]) || {}; - if (slotMetadata.ref != null) { callbacks.push( (function (ref) { @@ -919,37 +914,7 @@ export class ModuleMocker { })(slotMetadata.ref), ); } else { - slotMock = this._generateMock(slotMetadata, callbacks, refs); - - // For superclass accessor properties the subclass metadata contains the definitions - // for the getter and setter methods, and the superclass refs to them. - // The mock implementations are not available until the callbacks have been executed. - // Missing getter and setter refs will be resolved as their callbacks have been - // stacked before the setting of the accessor definition is stacked. - if ( - slotMetadata.members?.get?.ref !== undefined || - slotMetadata.members?.set?.ref !== undefined - ) { - callbacks.push( - (function (ref) { - return () => Object.defineProperty(mock, slot, ref); - })(slotMock as PropertyDescriptor), - ); - } else if ( - (slotMetadata.members?.get || slotMetadata.members?.set) && - slotMetadata.members?.configurable && - slotMetadata.members?.enumerable - ) { - // In some cases, e.g. third-party APIs, a 'prototype' ancestor to be - // mocked has a function property called 'get'. In this circumstance - // the 'prototype' property cannot be redefined and doing so causes an - // exception. Checks have been added for the 'configurable' and - // 'enumberable' properties present on true accessor property - // descriptors to prevent the attempt to replace the API. - Object.defineProperty(mock, slot, slotMock as PropertyDescriptor); - } else { - mock[slot] = slotMock; - } + mock[slot] = this._generateMock(slotMetadata, callbacks, refs); } }); @@ -1033,33 +998,8 @@ export class ModuleMocker { ) { return; } - - let descriptor = Object.getOwnPropertyDescriptor(component, slot); - let proto = Object.getPrototypeOf(component); - while (!descriptor && proto !== null) { - descriptor = Object.getOwnPropertyDescriptor(proto, slot); - proto = Object.getPrototypeOf(proto); - } - - let slotMetadata: MockMetadata | null = null; - if (descriptor?.get || descriptor?.set) { - // Specific case required for mocking class definitions imported via modules. - // In this case the class definitions are stored in accessor properties. - // All getters were previously ignored except where the containing object had __esModule == true - // Now getters are mocked the class definitions must still be read. - // @ts-expect-error ignore type mismatch - if (component.__esModule) { - // @ts-expect-error no index signature - slotMetadata = this.getMetadata(component[slot], refs); - } else { - // @ts-expect-error ignore type mismatch - slotMetadata = this.getMetadata(descriptor, refs); - } - } else { - // @ts-expect-error no index signature - slotMetadata = this.getMetadata(component[slot], refs); - } - + // @ts-expect-error no index signature + const slotMetadata = this.getMetadata(component[slot], refs); if (slotMetadata) { if (!members) { members = {}; @@ -1140,130 +1080,146 @@ export class ModuleMocker { methodKey: K, accessType?: 'get' | 'set', ) { - if (!object) { + if (accessType) { + return this._spyOnProperty(object, methodKey, accessType); + } + + if (typeof object !== 'object' && typeof object !== 'function') { throw new Error( - `spyOn could not find an object to spy upon for ${String(methodKey)}`, + `Cannot spyOn on a primitive value; ${this._typeOf(object)} given`, ); } - if (!methodKey) { - throw new Error('No property name supplied'); + const original = object[methodKey]; + + if (!this.isMockFunction(original)) { + if (typeof original !== 'function') { + throw new Error( + `Cannot spy the ${String( + methodKey, + )} property because it is not a function; ${this._typeOf( + original, + )} given instead`, + ); + } + + const isMethodOwner = Object.prototype.hasOwnProperty.call( + object, + methodKey, + ); + + let descriptor = Object.getOwnPropertyDescriptor(object, methodKey); + let proto = Object.getPrototypeOf(object); + + while (!descriptor && proto !== null) { + descriptor = Object.getOwnPropertyDescriptor(proto, methodKey); + proto = Object.getPrototypeOf(proto); + } + + let mock: Mock; + + if (descriptor && descriptor.get) { + const originalGet = descriptor.get; + mock = this._makeComponent({type: 'function'}, () => { + 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 + object[methodKey] = mock; + } + + mock.mockImplementation(function (this: unknown) { + return original.apply(this, arguments); + }); } - if (accessType && accessType != 'get' && accessType != 'set') { - throw new Error('Invalid accessType supplied'); + return object[methodKey]; + } + + private _spyOnProperty>( + obj: T, + propertyKey: K, + accessType: 'get' | 'set', + ): Mock<() => T> { + if (typeof obj !== 'object' && typeof obj !== 'function') { + throw new Error( + `Cannot spyOn on a primitive value; ${this._typeOf(obj)} given`, + ); } - if (typeof object !== 'object' && typeof object !== 'function') { + if (!obj) { throw new Error( - `Cannot spyOn on a primitive value; ${this._typeOf(object)} given`, + `spyOn could not find an object to spy upon for ${String(propertyKey)}`, ); } - let descriptor = Object.getOwnPropertyDescriptor(object, methodKey); - let proto = Object.getPrototypeOf(object); + if (!propertyKey) { + throw new Error('No property name supplied'); + } + + let descriptor = Object.getOwnPropertyDescriptor(obj, propertyKey); + let proto = Object.getPrototypeOf(obj); + while (!descriptor && proto !== null) { - descriptor = Object.getOwnPropertyDescriptor(proto, methodKey); + descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); proto = Object.getPrototypeOf(proto); } + if (!descriptor) { - throw new Error(`${String(methodKey)} property does not exist`); - } - if (!descriptor.configurable) { - throw new Error(`${String(methodKey)} is not declared configurable`); + throw new Error(`${String(propertyKey)} property does not exist`); } - if (this.isMockFunction(descriptor.value)) { - return object[methodKey]; - } else if (accessType == 'get' && this.isMockFunction(descriptor.get)) { - return descriptor.get; - } else if (accessType == 'set' && this.isMockFunction(descriptor.set)) { - return descriptor.set; + if (!descriptor.configurable) { + throw new Error(`${String(propertyKey)} is not declared configurable`); } - if (accessType) { - if (typeof descriptor[accessType] !== 'function') { - throw new Error( - `Cannot spy the ${String(accessType)} ${String( - methodKey, - )} property because it is not a function; - ${this._typeOf(descriptor?.[accessType])} given instead`, - ); - } - } else if (typeof descriptor.value !== 'function') { + if (!descriptor[accessType]) { throw new Error( - `Cannot spy the ${String( - methodKey, - )} property because it is not a function; ${this._typeOf( - descriptor.value, - )} given instead`, + `Property ${String( + propertyKey, + )} does not have access type ${accessType}`, ); } - let mock: Mock; - - if (accessType == 'get' && descriptor['get']) { - const originalAccessor = descriptor['get']; - mock = this._makeComponent( - { - type: 'function', - }, - () => { - descriptor![accessType] = originalAccessor; - Object.defineProperty(object, methodKey, descriptor!); - }, - ); + const original = descriptor[accessType]; - descriptor[accessType] = mock; - mock.mockImplementation(function (this: unknown) { - return originalAccessor.call(this); - }); - Object.defineProperty(object, methodKey, descriptor); - } else if (accessType == 'set' && descriptor['set']) { - const originalAccessor = descriptor['set']; - mock = this._makeComponent( - { - type: 'function', - }, - () => { - descriptor![accessType] = originalAccessor; - Object.defineProperty(object, methodKey, descriptor!); - }, - ); + if (!this.isMockFunction(original)) { + if (typeof original !== 'function') { + throw new Error( + `Cannot spy the ${String( + propertyKey, + )} property because it is not a function; ${this._typeOf( + original, + )} given instead`, + ); + } - descriptor[accessType] = mock; - mock.mockImplementation(function (this: unknown) { - return originalAccessor.call(this, arguments[0]); + descriptor[accessType] = this._makeComponent({type: 'function'}, () => { + // @ts-expect-error: mock is assignable + descriptor![accessType] = original; + Object.defineProperty(obj, propertyKey, descriptor!); }); - Object.defineProperty(object, methodKey, descriptor); - } else { - const isMethodOwner = Object.prototype.hasOwnProperty.call( - object, - methodKey, - ); - const original = descriptor; - mock = this._makeComponent( - { - type: 'function', - }, - () => { - if (isMethodOwner) { - object[methodKey] = original.value; - } else { - delete object[methodKey]; - } - }, - ); - - // @ts-expect-error overriding original method with a Mock - object[methodKey] = mock; - mock.mockImplementation(function (this: unknown) { - return original.value.apply(this, arguments); + (descriptor[accessType] as Mock<() => T>).mockImplementation(function ( + this: unknown, + ) { + // @ts-expect-error - wrong context + return original.apply(this, arguments); }); } - return mock; + Object.defineProperty(obj, propertyKey, descriptor); + return descriptor[accessType] as Mock<() => T>; } clearAllMocks(): void {