Skip to content

Commit 573417f

Browse files
authored
feat: overload jest.spyOn to spy on prop (#523)
* feat: overload jest.spyOn to use jest.spyOnProp as fallback * chore: remove unused types
1 parent 82cbf70 commit 573417f

File tree

4 files changed

+40
-12
lines changed

4 files changed

+40
-12
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ Determines if the given object property has been mocked.
6464

6565
#### `jest.spyOnProp(object, propertyName)`
6666

67+
**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.
68+
6769
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.
6870

6971
**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);
8991
const video = require("./video");
9092

9193
it("mocks video length", () => {
92-
const spy = jest.spyOnProp(video, "length");
94+
const spy = jest.spyOn(video, "length");
9395
spy.mockValueOnce(200)
9496
.mockValueOnce(400)
9597
.mockValueOnce(600);

src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,25 @@ export const extend: ExtendJest = (jestInstance: typeof jest): void => {
171171
const jestClearAll = jestInstance.clearAllMocks;
172172
const jestResetAll = jestInstance.resetAllMocks;
173173
const jestRestoreAll = jestInstance.restoreAllMocks;
174+
const jestSpyOn = jestInstance.spyOn;
174175
Object.assign(jestInstance, {
175176
isMockProp,
176177
clearAllMocks: () => jestClearAll() && clearAllMocks(),
177178
resetAllMocks: () => jestResetAll() && resetAllMocks(),
178179
restoreAllMocks: () => jestRestoreAll() && restoreAllMocks(),
180+
spyOn: <T>(
181+
object: T,
182+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
183+
propName: any,
184+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
185+
accessType: any,
186+
) => {
187+
try {
188+
return jestSpyOn(object, propName, accessType);
189+
} catch (e) {
190+
return spyOnProp(object, propName);
191+
}
192+
},
179193
spyOnProp,
180194
});
181195
};

tests/index.test.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as mockProps from "src/index";
33
import { Spyable } from "typings/globals";
44

55
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6-
const mockObject: Spyable = {
6+
const mockObject = {
77
fn1: (): string => "fnReturnValue",
88
prop1: "1",
99
prop2: 2,
@@ -37,7 +37,7 @@ it("mock object undefined property", () => {
3737

3838
it("mocks object property value undefined", () => {
3939
const testObject: Record<string, number> = { propUndefined: undefined };
40-
const spy = jest.spyOnProp(testObject, "propUndefined").mockValue(1);
40+
const spy = jest.spyOn(testObject, "propUndefined").mockValue(1);
4141
expect(testObject.propUndefined).toEqual(1);
4242
testObject.propUndefined = 5;
4343
expect(testObject.propUndefined).toEqual(5);
@@ -62,8 +62,9 @@ it("mocks object property value null", () => {
6262
it("mocks object property value", () => {
6363
const testObject = { ...mockObject };
6464
const mockValue = 99;
65-
const spy = jest.spyOnProp(testObject, "prop1");
65+
const spy = jest.spyOn(testObject, "prop1");
6666
expect(testObject.prop1).toEqual("1");
67+
// @ts-expect-error allow string assignment
6768
testObject.prop1 = mockValue;
6869
expect(testObject.prop1).toEqual(mockValue);
6970
expect(testObject.prop1).toEqual(mockValue);
@@ -89,7 +90,7 @@ it("mocks object property replaces once", () => {
8990
const testObject = { ...mockObject };
9091
const mockValue1 = 99;
9192
const mockValue2 = 100;
92-
const spy = jest.spyOnProp(testObject, "prop2").mockValueOnce(mockValue1);
93+
const spy = jest.spyOn(testObject, "prop2").mockValueOnce(mockValue1);
9394
spy.mockValueOnce(mockValue2).mockValueOnce(101);
9495
expect(testObject.prop2).toEqual(mockValue1);
9596
expect(testObject.prop2).toEqual(mockValue2);
@@ -102,7 +103,7 @@ it("mocks object property replaces once", () => {
102103
it("mocks object multiple properties", () => {
103104
const testObject = { ...mockObject };
104105
const mockValue = 99;
105-
const spy = jest.spyOnProp(testObject, "prop1").mockValue(mockValue);
106+
const spy = jest.spyOn(testObject, "prop1").mockValue(mockValue);
106107
jest.spyOnProp(testObject, "prop2").mockValue(mockValue);
107108
spy.mockRestore();
108109
expect(testObject.prop1).toEqual("1");
@@ -114,7 +115,7 @@ it("mocks object multiple properties", () => {
114115
it("resets mocked object property", () => {
115116
const testObject = { ...mockObject };
116117
const mockValue = 99;
117-
const spy = jest.spyOnProp(testObject, "prop1").mockValue(mockValue);
118+
const spy = jest.spyOn(testObject, "prop1").mockValue(mockValue);
118119
expect(testObject.prop1).toEqual(mockValue);
119120
expect(jest.isMockProp(testObject, "prop1")).toBe(true);
120121
spy.mockReset();
@@ -143,7 +144,7 @@ it.each`
143144
const testObject = { ...mockObject };
144145
const mockValue1 = 99;
145146
const mockValue2 = 100;
146-
jest.spyOnProp(testObject, "prop1").mockValue(mockValue1);
147+
jest.spyOn(testObject, "prop1").mockValue(mockValue1);
147148
jest.spyOnProp(testObject, "prop2").mockValue(mockValue2);
148149
expect(testObject.prop1).toEqual(mockValue1);
149150
expect(testObject.prop2).toEqual(mockValue2);
@@ -162,7 +163,7 @@ it("restores mocked object property in jest.restoreAllMocks", () => {
162163
const testObject = { ...mockObject };
163164
const mockValue1 = 99;
164165
const mockValue2 = 100;
165-
jest.spyOnProp(testObject, "prop1").mockValue(mockValue1);
166+
jest.spyOn(testObject, "prop1").mockValue(mockValue1);
166167
jest.spyOnProp(testObject, "prop2").mockValue(mockValue2);
167168
expect(testObject.prop1).toEqual(mockValue1);
168169
expect(testObject.prop2).toEqual(mockValue2);
@@ -178,7 +179,7 @@ it("restores mocked object property in jest.restoreAllMocks", () => {
178179
it("does not remock object property", () => {
179180
const testObject1 = { ...mockObject };
180181
const mockValue = 99;
181-
const spy1 = jest.spyOnProp(testObject1, "prop1").mockValue(mockValue);
182+
const spy1 = jest.spyOn(testObject1, "prop1").mockValue(mockValue);
182183
expect(testObject1.prop1).toEqual(mockValue);
183184
const testObject2 = testObject1;
184185
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]))(
194195
(_, v) => {
195196
expect(() =>
196197
// @ts-expect-error primitives not indexable by string
197-
jest.spyOnProp(v, "propName"),
198+
jest.spyOn(v, "propName"),
198199
).toThrowErrorMatchingSnapshot();
199200
},
200201
);
@@ -213,6 +214,8 @@ it("does not mock object method property", () => {
213214
).toThrowErrorMatchingSnapshot();
214215
expect(jest.isMockProp(mockObject, "fn1")).toBe(false);
215216
expect(mockObject.fn1()).toEqual("fnReturnValue");
217+
jest.spyOn(mockObject, "fn1").mockReturnValue("fnMockReturnValue");
218+
expect(mockObject.fn1()).toEqual("fnMockReturnValue");
216219
});
217220

218221
it("does not mock object getter property", () => {
@@ -221,6 +224,8 @@ it("does not mock object getter property", () => {
221224
).toThrowErrorMatchingSnapshot();
222225
expect(jest.isMockProp(mockObject, "propZ")).toBe(false);
223226
expect(mockObject.propZ).toEqual("z");
227+
jest.spyOn(mockObject, "propZ", "get").mockReturnValue("Z");
228+
expect(mockObject.propZ).toEqual("Z");
224229
});
225230

226231
it("does not mock object setter property", () => {
@@ -231,9 +236,12 @@ it("does not mock object setter property", () => {
231236
},
232237
};
233238
expect(() =>
234-
jest.spyOnProp(testObject, "propY"),
239+
jest.spyOn(testObject, "propY"),
235240
).toThrowErrorMatchingSnapshot();
236241
expect(jest.isMockProp(testObject, "propY")).toBe(false);
242+
const setterSpy = jest.spyOn(testObject, "propY", "set");
237243
testObject.propY = 4;
238244
expect(testObject._value).toEqual(4);
245+
expect(setterSpy).toHaveBeenCalledTimes(1);
246+
expect(setterSpy).toHaveBeenLastCalledWith(4);
239247
});

typings/globals.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export type IsMockProp = <T, K extends keyof T>(
2121
declare global {
2222
namespace jest {
2323
const isMockProp: IsMockProp;
24+
function spyOn<T, P extends NonFunctionPropertyNames<Required<T>>>(
25+
object: T,
26+
propName: P,
27+
): MockProp<T>;
2428
const spyOnProp: SpyOnProp;
2529
}
2630
}

0 commit comments

Comments
 (0)