From 30e802036291f4c9c9fd4feef6faba485df54dd2 Mon Sep 17 00:00:00 2001 From: Maxime LUCE Date: Tue, 20 Oct 2020 01:07:28 +0200 Subject: [PATCH] fix(mock): allow to mock methods in getters (#10156) --- CHANGELOG.md | 2 + .../jest-mock/src/__tests__/index.test.ts | 40 +++++++++++++++++++ packages/jest-mock/src/index.ts | 40 ++++++++++++++----- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94312007da65..f563a15a2e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Fixes +- `[jest-mock]` Allow to mock methods in getters (TypeScript 3.9 export) + ### Chore & Maintenance ### Performance diff --git a/packages/jest-mock/src/__tests__/index.test.ts b/packages/jest-mock/src/__tests__/index.test.ts index 90da4d5a3d97..d68857294cb4 100644 --- a/packages/jest-mock/src/__tests__/index.test.ts +++ b/packages/jest-mock/src/__tests__/index.test.ts @@ -1176,6 +1176,46 @@ describe('moduleMocker', () => { expect(spy1.mock.calls.length).toBe(1); expect(spy2.mock.calls.length).toBe(1); }); + + it('should work with getters', () => { + let isOriginalCalled = false; + let originalCallThis; + let originalCallArguments; + const obj = { + get method() { + return function () { + isOriginalCalled = true; + originalCallThis = this; + originalCallArguments = arguments; + }; + }, + }; + + const spy = moduleMocker.spyOn(obj, 'method'); + + const thisArg = {this: true}; + const firstArg = {first: true}; + const secondArg = {second: true}; + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).toHaveBeenCalled(); + + isOriginalCalled = false; + originalCallThis = null; + originalCallArguments = null; + spy.mockRestore(); + obj.method.call(thisArg, firstArg, secondArg); + expect(isOriginalCalled).toBe(true); + expect(originalCallThis).toBe(thisArg); + expect(originalCallArguments.length).toBe(2); + expect(originalCallArguments[0]).toBe(firstArg); + expect(originalCallArguments[1]).toBe(secondArg); + expect(spy).not.toHaveBeenCalled(); + }); }); describe('spyOnProperty', () => { diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 073c9d6b8a6d..948c31fcf59a 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -985,17 +985,37 @@ class ModuleMockerClass { const isMethodOwner = object.hasOwnProperty(methodName); - // @ts-expect-error overriding original method with a Mock - object[methodName] = this._makeComponent({type: 'function'}, () => { - if (isMethodOwner) { - object[methodName] = original; - } else { - delete object[methodName]; - } - }); + let descriptor = Object.getOwnPropertyDescriptor(object, methodName); + let proto = Object.getPrototypeOf(object); + + while (!descriptor && proto !== null) { + descriptor = Object.getOwnPropertyDescriptor(proto, methodName); + proto = Object.getPrototypeOf(proto); + } + + let mock: JestMock.Mock>; + + if (descriptor && descriptor.get) { + const originalGet = descriptor.get; + mock = this._makeComponent({type: 'function'}, () => { + descriptor!.get = originalGet; + Object.defineProperty(object, methodName, descriptor!); + }); + descriptor.get = () => mock; + Object.defineProperty(object, methodName, descriptor); + } else { + mock = this._makeComponent({type: 'function'}, () => { + if (isMethodOwner) { + object[methodName] = original; + } else { + delete object[methodName]; + } + }); + // @ts-expect-error overriding original method with a Mock + object[methodName] = mock; + } - // @ts-expect-error original method is now a Mock - object[methodName].mockImplementation(function (this: unknown) { + mock.mockImplementation(function (this: unknown) { return original.apply(this, arguments); }); }