Skip to content

Commit 1f6e444

Browse files
author
s.v.zaytsev
committed
feat: add onGenerateMock transformer callback (jestjs#15429)
1 parent 611d1a4 commit 1f6e444

File tree

4 files changed

+189
-1
lines changed

4 files changed

+189
-1
lines changed

packages/jest-environment/src/index.ts

+12
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ export interface Jest {
207207
moduleFactory?: () => T,
208208
options?: {virtual?: boolean},
209209
): Jest;
210+
/**
211+
* Registers a callback function that is invoked whenever a mock is generated for a module.
212+
* This callback is passed the module name and the newly created mock object, and must return
213+
* the (potentially modified) mock object.
214+
*
215+
* If multiple callbacks are registered, they will be called in the order they were added.
216+
* Each callback receives the result of the previous callback as the `moduleMock` parameter,
217+
* making it possible to apply sequential transformations.
218+
*
219+
* @param cb
220+
*/
221+
onGenerateMock<T>(cb: (moduleName: string, moduleMock: T) => T): Jest;
210222
/**
211223
* Mocks a module with the provided module factory when it is being imported.
212224
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Runtime jest.onGenerateMock calls single callback and returns transformed value 1`] = `
4+
Object {
5+
"filename": "/Users/s.v.zaytsev/IdeaProjects/jest/packages/jest-runtime/src/__tests__/test_root/RegularModule.js",
6+
"getModuleStateValue": [MockFunction],
7+
"isLoaded": [MockFunction],
8+
"isRealModule": true,
9+
"jest": Object {
10+
"advanceTimersByTime": [MockFunction],
11+
"advanceTimersByTimeAsync": [MockFunction],
12+
"advanceTimersToNextFrame": [MockFunction],
13+
"advanceTimersToNextTimer": [MockFunction],
14+
"advanceTimersToNextTimerAsync": [MockFunction],
15+
"autoMockOff": [MockFunction],
16+
"autoMockOn": [MockFunction],
17+
"clearAllMocks": [MockFunction],
18+
"clearAllTimers": [MockFunction],
19+
"createMockFromModule": [MockFunction],
20+
"deepUnmock": [MockFunction],
21+
"disableAutomock": [MockFunction],
22+
"doMock": [MockFunction],
23+
"dontMock": [MockFunction],
24+
"enableAutomock": [MockFunction],
25+
"fn": [MockFunction],
26+
"getRealSystemTime": [MockFunction],
27+
"getSeed": [MockFunction],
28+
"getTimerCount": [MockFunction],
29+
"isEnvironmentTornDown": [MockFunction],
30+
"isMockFunction": [MockFunction],
31+
"isolateModules": [MockFunction],
32+
"isolateModulesAsync": [MockFunction],
33+
"mock": [MockFunction],
34+
"mocked": [MockFunction],
35+
"now": [MockFunction],
36+
"onGenerateMock": [MockFunction],
37+
"replaceProperty": [MockFunction],
38+
"requireActual": [MockFunction],
39+
"requireMock": [MockFunction],
40+
"resetAllMocks": [MockFunction],
41+
"resetModules": [MockFunction],
42+
"restoreAllMocks": [MockFunction],
43+
"retryTimes": [MockFunction],
44+
"runAllImmediates": [MockFunction],
45+
"runAllTicks": [MockFunction],
46+
"runAllTimers": [MockFunction],
47+
"runAllTimersAsync": [MockFunction],
48+
"runOnlyPendingTimers": [MockFunction],
49+
"runOnlyPendingTimersAsync": [MockFunction],
50+
"setMock": [MockFunction],
51+
"setSystemTime": [MockFunction],
52+
"setTimeout": [MockFunction],
53+
"spyOn": [MockFunction],
54+
"unmock": [MockFunction],
55+
"unstable_mockModule": [MockFunction],
56+
"unstable_unmockModule": [MockFunction],
57+
"useFakeTimers": [MockFunction],
58+
"useRealTimers": [MockFunction],
59+
},
60+
"lazyRequire": [MockFunction],
61+
"loaded": false,
62+
"module": Object {
63+
"children": Array [],
64+
"exports": [Circular],
65+
"filename": "/Users/s.v.zaytsev/IdeaProjects/jest/packages/jest-runtime/src/__tests__/test_root/RegularModule.js",
66+
"id": "/Users/s.v.zaytsev/IdeaProjects/jest/packages/jest-runtime/src/__tests__/test_root/RegularModule.js",
67+
"isPreloading": false,
68+
"loaded": true,
69+
"main": null,
70+
"path": "/Users/s.v.zaytsev/IdeaProjects/jest/packages/jest-runtime/src/__tests__/test_root",
71+
"paths": Array [],
72+
"require": [MockFunction],
73+
},
74+
"object": Object {},
75+
"parent": null,
76+
"path": "/Users/s.v.zaytsev/IdeaProjects/jest/packages/jest-runtime/src/__tests__/test_root",
77+
"paths": Array [],
78+
"setModuleStateValue": [MockFunction],
79+
}
80+
`;

packages/jest-runtime/src/__tests__/runtime_mock.test.js

+79
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,83 @@ describe('Runtime', () => {
136136
).toBe(mockReference);
137137
});
138138
});
139+
140+
describe('jest.onGenerateMock', () => {
141+
it('calls single callback and returns transformed value', async () => {
142+
const runtime = await createRuntime(__filename);
143+
const mockReference = {isMock: true};
144+
const root = runtime.requireModule(runtime.__mockRootPath, rootJsPath);
145+
// Erase module registry because root.js requires most other modules.
146+
root.jest.resetModules();
147+
148+
const onGenerateMock = jest.fn((moduleName, moduleMock) => mockReference);
149+
150+
root.jest.onGenerateMock(onGenerateMock);
151+
root.jest.mock('RegularModule');
152+
root.jest.mock('ManuallyMocked');
153+
154+
expect(
155+
runtime.requireModuleOrMock(runtime.__mockRootPath, 'RegularModule'),
156+
).toEqual(mockReference);
157+
expect(onGenerateMock).toHaveBeenCalledWith(
158+
'RegularModule',
159+
expect.anything(),
160+
);
161+
expect(onGenerateMock.mock.calls[0][1]).toMatchSnapshot();
162+
163+
onGenerateMock.mockReset();
164+
165+
expect(
166+
runtime.requireModuleOrMock(runtime.__mockRootPath, 'ManuallyMocked'),
167+
).not.toEqual(mockReference);
168+
expect(onGenerateMock).not.toHaveBeenCalled();
169+
});
170+
171+
it('calls multiple callback and returns transformed value', async () => {
172+
const runtime = await createRuntime(__filename);
173+
const root = runtime.requireModule(runtime.__mockRootPath, rootJsPath);
174+
// Erase module registry because root.js requires most other modules.
175+
root.jest.resetModules();
176+
177+
const onGenerateMock1 = jest.fn((moduleName, moduleMock) => ({
178+
isMock: true,
179+
value: 1,
180+
}));
181+
182+
const onGenerateMock2 = jest.fn((moduleName, moduleMock) => ({
183+
...moduleMock,
184+
value: moduleMock.value + 1,
185+
}));
186+
187+
const onGenerateMock3 = jest.fn((moduleName, moduleMock) => ({
188+
...moduleMock,
189+
value: moduleMock.value ** 2,
190+
}));
191+
192+
root.jest.onGenerateMock(onGenerateMock1);
193+
root.jest.onGenerateMock(onGenerateMock2);
194+
root.jest.onGenerateMock(onGenerateMock3);
195+
root.jest.mock('RegularModule');
196+
root.jest.mock('ManuallyMocked');
197+
198+
expect(
199+
runtime.requireModuleOrMock(runtime.__mockRootPath, 'RegularModule'),
200+
).toEqual({
201+
isMock: true,
202+
value: 4,
203+
});
204+
expect(onGenerateMock1).toHaveBeenCalledWith(
205+
'RegularModule',
206+
expect.anything(),
207+
);
208+
expect(onGenerateMock2).toHaveBeenCalledWith('RegularModule', {
209+
isMock: true,
210+
value: 1,
211+
});
212+
expect(onGenerateMock3).toHaveBeenCalledWith('RegularModule', {
213+
isMock: true,
214+
value: 2,
215+
});
216+
});
217+
});
139218
});

packages/jest-runtime/src/index.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ export default class Runtime {
171171
private readonly _environment: JestEnvironment;
172172
private readonly _explicitShouldMock: Map<string, boolean>;
173173
private readonly _explicitShouldMockModule: Map<string, boolean>;
174+
private readonly _onGenerateMock: Set<
175+
(moduleName: string, moduleMock: any) => any
176+
>;
174177
private _fakeTimersImplementation:
175178
| LegacyFakeTimers<unknown>
176179
| ModernFakeTimers
@@ -235,6 +238,7 @@ export default class Runtime {
235238
this._globalConfig = globalConfig;
236239
this._explicitShouldMock = new Map();
237240
this._explicitShouldMockModule = new Map();
241+
this._onGenerateMock = new Set();
238242
this._internalModuleRegistry = new Map();
239243
this._isCurrentlyExecutingManualMock = null;
240244
this._mainModule = null;
@@ -1930,10 +1934,16 @@ export default class Runtime {
19301934
}
19311935
this._mockMetaDataCache.set(modulePath, mockMetadata);
19321936
}
1933-
return this._moduleMocker.generateFromMetadata<T>(
1937+
let moduleMock = this._moduleMocker.generateFromMetadata<T>(
19341938
// added above if missing
19351939
this._mockMetaDataCache.get(modulePath)!,
19361940
);
1941+
1942+
for (const onGenerateMock of this._onGenerateMock) {
1943+
moduleMock = onGenerateMock(moduleName, moduleMock);
1944+
}
1945+
1946+
return moduleMock;
19371947
}
19381948

19391949
private _shouldMockCjs(
@@ -2193,6 +2203,12 @@ export default class Runtime {
21932203
this._explicitShouldMock.set(moduleID, true);
21942204
return jestObject;
21952205
};
2206+
const onGenerateMock: Jest['onGenerateMock'] = <T>(
2207+
cb: (moduleName: string, moduleMock: T) => T,
2208+
) => {
2209+
this._onGenerateMock.add(cb);
2210+
return jestObject;
2211+
};
21962212
const setMockFactory = (
21972213
moduleName: string,
21982214
mockFactory: () => unknown,
@@ -2364,6 +2380,7 @@ export default class Runtime {
23642380
mock,
23652381
mocked,
23662382
now: () => _getFakeTimers().now(),
2383+
onGenerateMock,
23672384
replaceProperty,
23682385
requireActual: moduleName => this.requireActual(from, moduleName),
23692386
requireMock: moduleName => this.requireMock(from, moduleName),

0 commit comments

Comments
 (0)