From 573417fc8e954de711f9c3bbdf28f89dbffd7372 Mon Sep 17 00:00:00 2001 From: Emmanuel Ogbizi Date: Sat, 19 Dec 2020 13:24:44 -0500 Subject: [PATCH] feat: overload jest.spyOn to spy on prop (#523) * feat: overload jest.spyOn to use jest.spyOnProp as fallback * chore: remove unused types --- README.md | 4 +++- src/index.ts | 14 ++++++++++++++ tests/index.test.ts | 30 +++++++++++++++++++----------- typings/globals.d.ts | 4 ++++ 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 12b80fba..247d89b5 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Determines if the given object property has been mocked. #### `jest.spyOnProp(object, propertyName)` +**Note**: This is aliased as `jest.spyOn` as of `v1.9.0`, overriding the existing `jest.spyOn` to use `spyOnProp` when spying on a regular object property. + Creates a mock property attached to `object[propertyName]` and returns a mock property spy object, which controls all access to the object property. Repeating spying on the same object property will return the same mocked property spy. **Note**: By default, `spyOnProp` preserves the object property value. If you want to overwrite the original value, you can use `jest.spyOnProp(object, propertyName).mockValue(customValue)` or [`jest.spyOn(object, methodName, accessType?)`](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname-accesstype) to spy on a getter or a setter. @@ -89,7 +91,7 @@ mockProps.extend(jest); const video = require("./video"); it("mocks video length", () => { - const spy = jest.spyOnProp(video, "length"); + const spy = jest.spyOn(video, "length"); spy.mockValueOnce(200) .mockValueOnce(400) .mockValueOnce(600); diff --git a/src/index.ts b/src/index.ts index 60f90345..5cf981c3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -171,11 +171,25 @@ export const extend: ExtendJest = (jestInstance: typeof jest): void => { const jestClearAll = jestInstance.clearAllMocks; const jestResetAll = jestInstance.resetAllMocks; const jestRestoreAll = jestInstance.restoreAllMocks; + const jestSpyOn = jestInstance.spyOn; Object.assign(jestInstance, { isMockProp, clearAllMocks: () => jestClearAll() && clearAllMocks(), resetAllMocks: () => jestResetAll() && resetAllMocks(), restoreAllMocks: () => jestRestoreAll() && restoreAllMocks(), + spyOn: ( + object: T, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + propName: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + accessType: any, + ) => { + try { + return jestSpyOn(object, propName, accessType); + } catch (e) { + return spyOnProp(object, propName); + } + }, spyOnProp, }); }; diff --git a/tests/index.test.ts b/tests/index.test.ts index 2afcb49b..4e6a23c6 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -3,7 +3,7 @@ import * as mockProps from "src/index"; import { Spyable } from "typings/globals"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const mockObject: Spyable = { +const mockObject = { fn1: (): string => "fnReturnValue", prop1: "1", prop2: 2, @@ -37,7 +37,7 @@ it("mock object undefined property", () => { it("mocks object property value undefined", () => { const testObject: Record = { propUndefined: undefined }; - const spy = jest.spyOnProp(testObject, "propUndefined").mockValue(1); + const spy = jest.spyOn(testObject, "propUndefined").mockValue(1); expect(testObject.propUndefined).toEqual(1); testObject.propUndefined = 5; expect(testObject.propUndefined).toEqual(5); @@ -62,8 +62,9 @@ it("mocks object property value null", () => { it("mocks object property value", () => { const testObject = { ...mockObject }; const mockValue = 99; - const spy = jest.spyOnProp(testObject, "prop1"); + const spy = jest.spyOn(testObject, "prop1"); expect(testObject.prop1).toEqual("1"); + // @ts-expect-error allow string assignment testObject.prop1 = mockValue; expect(testObject.prop1).toEqual(mockValue); expect(testObject.prop1).toEqual(mockValue); @@ -89,7 +90,7 @@ it("mocks object property replaces once", () => { const testObject = { ...mockObject }; const mockValue1 = 99; const mockValue2 = 100; - const spy = jest.spyOnProp(testObject, "prop2").mockValueOnce(mockValue1); + const spy = jest.spyOn(testObject, "prop2").mockValueOnce(mockValue1); spy.mockValueOnce(mockValue2).mockValueOnce(101); expect(testObject.prop2).toEqual(mockValue1); expect(testObject.prop2).toEqual(mockValue2); @@ -102,7 +103,7 @@ it("mocks object property replaces once", () => { it("mocks object multiple properties", () => { const testObject = { ...mockObject }; const mockValue = 99; - const spy = jest.spyOnProp(testObject, "prop1").mockValue(mockValue); + const spy = jest.spyOn(testObject, "prop1").mockValue(mockValue); jest.spyOnProp(testObject, "prop2").mockValue(mockValue); spy.mockRestore(); expect(testObject.prop1).toEqual("1"); @@ -114,7 +115,7 @@ it("mocks object multiple properties", () => { it("resets mocked object property", () => { const testObject = { ...mockObject }; const mockValue = 99; - const spy = jest.spyOnProp(testObject, "prop1").mockValue(mockValue); + const spy = jest.spyOn(testObject, "prop1").mockValue(mockValue); expect(testObject.prop1).toEqual(mockValue); expect(jest.isMockProp(testObject, "prop1")).toBe(true); spy.mockReset(); @@ -143,7 +144,7 @@ it.each` const testObject = { ...mockObject }; const mockValue1 = 99; const mockValue2 = 100; - jest.spyOnProp(testObject, "prop1").mockValue(mockValue1); + jest.spyOn(testObject, "prop1").mockValue(mockValue1); jest.spyOnProp(testObject, "prop2").mockValue(mockValue2); expect(testObject.prop1).toEqual(mockValue1); expect(testObject.prop2).toEqual(mockValue2); @@ -162,7 +163,7 @@ it("restores mocked object property in jest.restoreAllMocks", () => { const testObject = { ...mockObject }; const mockValue1 = 99; const mockValue2 = 100; - jest.spyOnProp(testObject, "prop1").mockValue(mockValue1); + jest.spyOn(testObject, "prop1").mockValue(mockValue1); jest.spyOnProp(testObject, "prop2").mockValue(mockValue2); expect(testObject.prop1).toEqual(mockValue1); expect(testObject.prop2).toEqual(mockValue2); @@ -178,7 +179,7 @@ it("restores mocked object property in jest.restoreAllMocks", () => { it("does not remock object property", () => { const testObject1 = { ...mockObject }; const mockValue = 99; - const spy1 = jest.spyOnProp(testObject1, "prop1").mockValue(mockValue); + const spy1 = jest.spyOn(testObject1, "prop1").mockValue(mockValue); expect(testObject1.prop1).toEqual(mockValue); const testObject2 = testObject1; const spy2 = jest.spyOnProp(testObject2, "prop1").mockValue(mockValue); @@ -194,7 +195,7 @@ it.each([undefined, null, 99, "value", true].map((v) => [v && typeof v, v]))( (_, v) => { expect(() => // @ts-expect-error primitives not indexable by string - jest.spyOnProp(v, "propName"), + jest.spyOn(v, "propName"), ).toThrowErrorMatchingSnapshot(); }, ); @@ -213,6 +214,8 @@ it("does not mock object method property", () => { ).toThrowErrorMatchingSnapshot(); expect(jest.isMockProp(mockObject, "fn1")).toBe(false); expect(mockObject.fn1()).toEqual("fnReturnValue"); + jest.spyOn(mockObject, "fn1").mockReturnValue("fnMockReturnValue"); + expect(mockObject.fn1()).toEqual("fnMockReturnValue"); }); it("does not mock object getter property", () => { @@ -221,6 +224,8 @@ it("does not mock object getter property", () => { ).toThrowErrorMatchingSnapshot(); expect(jest.isMockProp(mockObject, "propZ")).toBe(false); expect(mockObject.propZ).toEqual("z"); + jest.spyOn(mockObject, "propZ", "get").mockReturnValue("Z"); + expect(mockObject.propZ).toEqual("Z"); }); it("does not mock object setter property", () => { @@ -231,9 +236,12 @@ it("does not mock object setter property", () => { }, }; expect(() => - jest.spyOnProp(testObject, "propY"), + jest.spyOn(testObject, "propY"), ).toThrowErrorMatchingSnapshot(); expect(jest.isMockProp(testObject, "propY")).toBe(false); + const setterSpy = jest.spyOn(testObject, "propY", "set"); testObject.propY = 4; expect(testObject._value).toEqual(4); + expect(setterSpy).toHaveBeenCalledTimes(1); + expect(setterSpy).toHaveBeenLastCalledWith(4); }); diff --git a/typings/globals.d.ts b/typings/globals.d.ts index 95918d2c..bee84fb7 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -21,6 +21,10 @@ export type IsMockProp = ( declare global { namespace jest { const isMockProp: IsMockProp; + function spyOn>>( + object: T, + propName: P, + ): MockProp; const spyOnProp: SpyOnProp; } }