diff --git a/CHANGELOG.md b/CHANGELOG.md index bf234ab83e92..951c09c72854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes - `[babel-plugin-jest-hoist]` Ignore `TSTypeQuery` when checking for hoisted references ([#13367](https://github.com/facebook/jest/pull/13367)) +- `[jest-mock]` Fix mocking of getters and setters on classes ([#13398](https://github.com/facebook/jest/pull/13398)) - `[jest-reporters]` Revert: Transform file paths into hyperlinks ([#13399](https://github.com/facebook/jest/pull/13399)) - `[@jest/types]` Infer type of `each` table correctly when the table is a tuple or array ([#13381](https://github.com/facebook/jest/pull/13381)) - `[@jest/types]` Rework typings to allow the `*ReturnedWith` matchers to be called with no argument ([#13385](https://github.com/facebook/jest/pull/13385)) diff --git a/packages/jest-mock/src/__tests__/__fixtures__/SuperTestClass.ts b/packages/jest-mock/src/__tests__/__fixtures__/SuperTestClass.ts new file mode 100644 index 000000000000..ae1c262e34f6 --- /dev/null +++ b/packages/jest-mock/src/__tests__/__fixtures__/SuperTestClass.ts @@ -0,0 +1,36 @@ +/** + * 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__/__fixtures__/TestClass.ts b/packages/jest-mock/src/__tests__/__fixtures__/TestClass.ts new file mode 100644 index 000000000000..e4197db98abe --- /dev/null +++ b/packages/jest-mock/src/__tests__/__fixtures__/TestClass.ts @@ -0,0 +1,11 @@ +/** + * 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__/__fixtures__/class-mocks-types.ts b/packages/jest-mock/src/__tests__/__fixtures__/class-mocks-types.ts new file mode 100644 index 000000000000..ccb2eae1fc4f --- /dev/null +++ b/packages/jest-mock/src/__tests__/__fixtures__/class-mocks-types.ts @@ -0,0 +1,54 @@ +/** + * 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'; + } + + static get(): string { + return 'get'; + } + + static set(): string { + return 'set'; + } + + testProperty = 'testProperty'; + + get testAccessor(): string { + return 'testAccessor'; + } + set testAccessor(_x: string) { + return; + } + + testMethod(): string { + return 'testMethod'; + } + + get(): string { + return 'get'; + } + + set(): string { + return 'set'; + } +} + +export 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 new file mode 100644 index 000000000000..610469e9deba --- /dev/null +++ b/packages/jest-mock/src/__tests__/class-mocks-dual-import.test.ts @@ -0,0 +1,130 @@ +/** + * 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 './__fixtures__/SuperTestClass'; +import TestClass from './__fixtures__/TestClass'; +jest.mock('./__fixtures__/SuperTestClass'); +jest.mock('./__fixtures__/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()).toBe('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()).toBe('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).toBe('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).toBe('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).toBe('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).toBe('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 new file mode 100644 index 000000000000..9b354db44d95 --- /dev/null +++ b/packages/jest-mock/src/__tests__/class-mocks-single-import.test.ts @@ -0,0 +1,250 @@ +/** + * 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 './__fixtures__/class-mocks-types'; +jest.mock('./__fixtures__/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()).toBe('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()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + }); + + it('can call an instance method named "get" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(SuperTestClass.prototype, 'get') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new SuperTestClass(); + expect(testClassInstance.get()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call a superclass instance method named "get" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(TestClass.prototype, 'get') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new TestClass(); + expect(testClassInstance.get()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call an instance method named "set" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(SuperTestClass.prototype, 'set') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new SuperTestClass(); + expect(testClassInstance.set()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call a superclass instance method named "set" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(TestClass.prototype, 'set') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new TestClass(); + expect(testClassInstance.set()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + 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).toBe('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).toBe('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 call a static method - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(SuperTestClass, 'staticTestMethod') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(SuperTestClass.staticTestMethod()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call a superclass static method - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(TestClass, 'staticTestMethod') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.staticTestMethod()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + }); + + it('can call a static method named "get" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(SuperTestClass, 'get') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(SuperTestClass.get()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call a superclass static method named "get" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(TestClass, 'get') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.get()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call a static method named "set" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(SuperTestClass, 'set') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(SuperTestClass.set()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + it('can call a superclass static method named "set" - Auto-mocked class', () => { + const mockTestMethod = jest + .spyOn(TestClass, 'set') + .mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.set()).toBe('mockTestMethod'); + expect(mockTestMethod).toHaveBeenCalledTimes(1); + + mockTestMethod.mockClear(); + }); + + 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).toBe('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).toBe('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.test.ts b/packages/jest-mock/src/__tests__/class-mocks.test.ts new file mode 100644 index 000000000000..4b33c6c532dd --- /dev/null +++ b/packages/jest-mock/src/__tests__/class-mocks.test.ts @@ -0,0 +1,319 @@ +/** + * 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()).toBe('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()).toBe('mockTestMethod'); + }); + + it('can call an instance method named "get"', () => { + class TestClass { + get(): string { + return 'get'; + } + } + + jest.spyOn(TestClass.prototype, 'get').mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new TestClass(); + expect(testClassInstance.get()).toBe('mockTestMethod'); + }); + + it('can call a superclass instance method named "get"', () => { + class SuperTestClass { + get(): string { + return 'get'; + } + } + + class TestClass extends SuperTestClass {} + + jest.spyOn(TestClass.prototype, 'get').mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new TestClass(); + expect(testClassInstance.get()).toBe('mockTestMethod'); + }); + + it('can call an instance method named "set"', () => { + class TestClass { + set(): string { + return 'set'; + } + } + + jest.spyOn(TestClass.prototype, 'set').mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new TestClass(); + expect(testClassInstance.set()).toBe('mockTestMethod'); + }); + + it('can call a superclass instance method named "set"', () => { + class SuperTestClass { + set(): string { + return 'set'; + } + } + + class TestClass extends SuperTestClass {} + + jest.spyOn(TestClass.prototype, 'set').mockImplementation(() => { + return 'mockTestMethod'; + }); + const testClassInstance = new TestClass(); + expect(testClassInstance.set()).toBe('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).toBe('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).toBe('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 call a static method', () => { + class TestClass { + static testMethod(): string { + return 'testMethod'; + } + } + + jest.spyOn(TestClass, 'testMethod').mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.testMethod()).toBe('mockTestMethod'); + }); + + it('can call a superclass static method', () => { + class SuperTestClass { + static testMethod(): string { + return 'testMethod'; + } + } + + class TestClass extends SuperTestClass {} + + jest.spyOn(TestClass, 'testMethod').mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.testMethod()).toBe('mockTestMethod'); + }); + + it('can call a static method named "get"', () => { + class TestClass { + static get(): string { + return 'get'; + } + } + + jest.spyOn(TestClass, 'get').mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.get()).toBe('mockTestMethod'); + }); + + it('can call a superclass static method named "get"', () => { + class SuperTestClass { + static get(): string { + return 'get'; + } + } + + class TestClass extends SuperTestClass {} + + jest.spyOn(TestClass, 'get').mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.get()).toBe('mockTestMethod'); + }); + + it('can call a static method named "set"', () => { + class TestClass { + static set(): string { + return 'set'; + } + } + + jest.spyOn(TestClass, 'set').mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.set()).toBe('mockTestMethod'); + }); + + it('can call a superclass static method named "set"', () => { + class SuperTestClass { + static set(): string { + return 'set'; + } + } + + class TestClass extends SuperTestClass {} + + jest.spyOn(TestClass, 'set').mockImplementation(() => { + return 'mockTestMethod'; + }); + expect(TestClass.set()).toBe('mockTestMethod'); + }); + + 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).toBe('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).toBe('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 fc965fa06e48..8f1436f4b042 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1261,12 +1261,10 @@ describe('moduleMocker', () => { it('should throw on invalid input', () => { expect(() => { moduleMocker.spyOn(null, 'method'); - }).toThrow('Cannot read prop'); + }).toThrow('spyOn could not find an object to spy upon for method'); expect(() => { moduleMocker.spyOn({}, 'method'); - }).toThrow( - 'Cannot spy the method property because it is not a function; undefined given instead', - ); + }).toThrow('method property does not exist'); expect(() => { moduleMocker.spyOn({method: 10}, 'method'); }).toThrow( @@ -1325,7 +1323,7 @@ describe('moduleMocker', () => { }, }; - const spy = moduleMocker.spyOn(obj, 'method'); + const spy = moduleMocker.spyOn(obj, 'method', 'get'); const thisArg = {this: true}; const firstArg = {first: true}; @@ -1431,12 +1429,10 @@ describe('moduleMocker', () => { it('should throw on invalid input', () => { expect(() => { moduleMocker.spyOn(null, 'method'); - }).toThrow('Cannot read prop'); + }).toThrow('spyOn could not find an object to spy upon for method'); expect(() => { moduleMocker.spyOn({}, 'method'); - }).toThrow( - 'Cannot spy the method property because it is not a function; undefined given instead', - ); + }).toThrow('method property does not exist'); expect(() => { moduleMocker.spyOn({method: 10}, 'method'); }).toThrow( diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 157b47c76c67..408690efaaab 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -516,7 +516,10 @@ export class ModuleMocker { if (!isReadonlyProp(object, prop)) { const propDesc = Object.getOwnPropertyDescriptor(object, prop); - if ((propDesc !== undefined && !propDesc.get) || object.__esModule) { + if ( + propDesc !== undefined && + !(propDesc.get && prop == '__proto__') + ) { slots.add(prop); } } @@ -906,7 +909,9 @@ 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) { @@ -914,7 +919,40 @@ export class ModuleMocker { })(slotMetadata.ref), ); } else { - mock[slot] = this._generateMock(slotMetadata, callbacks, refs); + 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. + + // 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. + if ( + (slotMetadata.members?.get?.ref !== undefined || + slotMetadata.members?.set?.ref !== undefined) && + slotMetadata.members?.configurable && + slotMetadata.members?.enumerable + ) { + 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 + ) { + Object.defineProperty(mock, slot, slotMock as PropertyDescriptor); + } else { + mock[slot] = slotMock; + } } }); @@ -998,8 +1036,33 @@ export class ModuleMocker { ) { return; } - // @ts-expect-error no index signature - const slotMetadata = this.getMetadata(component[slot], refs); + + 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); + } + if (slotMetadata) { if (!members) { members = {}; @@ -1080,146 +1143,130 @@ export class ModuleMocker { methodKey: K, accessType?: 'get' | 'set', ) { - if (accessType) { - return this._spyOnProperty(object, methodKey, accessType); - } - - if (typeof object !== 'object' && typeof object !== 'function') { + if (!object) { throw new Error( - `Cannot spyOn on a primitive value; ${this._typeOf(object)} given`, + `spyOn could not find an object to spy upon for ${String(methodKey)}`, ); } - 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 (!methodKey) { + throw new Error('No property name 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 (accessType && accessType != 'get' && accessType != 'set') { + throw new Error('Invalid accessType supplied'); } - if (!obj) { + if (typeof object !== 'object' && typeof object !== 'function') { throw new Error( - `spyOn could not find an object to spy upon for ${String(propertyKey)}`, + `Cannot spyOn on a primitive value; ${this._typeOf(object)} given`, ); } - if (!propertyKey) { - throw new Error('No property name supplied'); - } - - let descriptor = Object.getOwnPropertyDescriptor(obj, propertyKey); - let proto = Object.getPrototypeOf(obj); - + let descriptor = Object.getOwnPropertyDescriptor(object, methodKey); + let proto = Object.getPrototypeOf(object); while (!descriptor && proto !== null) { - descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); + descriptor = Object.getOwnPropertyDescriptor(proto, methodKey); proto = Object.getPrototypeOf(proto); } - if (!descriptor) { - throw new Error(`${String(propertyKey)} property does not exist`); + throw new Error(`${String(methodKey)} property does not exist`); } - if (!descriptor.configurable) { - throw new Error(`${String(propertyKey)} is not declared configurable`); + throw new Error(`${String(methodKey)} is not declared configurable`); } - if (!descriptor[accessType]) { - throw new Error( - `Property ${String( - propertyKey, - )} does not have access type ${accessType}`, - ); + 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; } - const original = descriptor[accessType]; - - if (!this.isMockFunction(original)) { - if (typeof original !== 'function') { + if (accessType) { + if (typeof descriptor[accessType] !== 'function') { throw new Error( - `Cannot spy the ${String( - propertyKey, - )} property because it is not a function; ${this._typeOf( - original, - )} given instead`, + `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') { + throw new Error( + `Cannot spy the ${String( + methodKey, + )} property because it is not a function; ${this._typeOf( + descriptor.value, + )} given instead`, + ); + } + + 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!); + }, + ); - descriptor[accessType] = this._makeComponent({type: 'function'}, () => { - // @ts-expect-error: mock is assignable - descriptor![accessType] = original; - Object.defineProperty(obj, propertyKey, descriptor!); + 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!); + }, + ); - (descriptor[accessType] as Mock<() => T>).mockImplementation(function ( - this: unknown, - ) { - // @ts-expect-error - wrong context - return original.apply(this, arguments); + descriptor[accessType] = mock; + mock.mockImplementation(function (this: unknown) { + return originalAccessor.call(this, arguments[0]); + }); + 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); }); } - Object.defineProperty(obj, propertyKey, descriptor); - return descriptor[accessType] as Mock<() => T>; + return mock; } clearAllMocks(): void {