Skip to content

Commit d9caa88

Browse files
committed
refactor: rewrite module mocking
1 parent 91ea1ea commit d9caa88

6 files changed

Lines changed: 55 additions & 122 deletions

File tree

apps/playground/src/__tests__/mocking/modules.harness.ts

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ import {
77
unmock,
88
requireActual,
99
fn,
10-
clearMocks,
1110
resetModules,
1211
} from 'react-native-harness';
1312

1413
describe('Module mocking', () => {
1514
afterEach(() => {
16-
// Clean up mocks after each test
17-
clearMocks();
15+
resetModules();
16+
});
17+
18+
it('should not interfere with modules that are not mocked', () => {
19+
const moduleA = require('react-native');
20+
const moduleB = require('react-native');
21+
expect(moduleA === moduleB).toBe(true);
1822
});
1923

2024
it('should completely mock a module and return mock implementation', () => {
@@ -133,21 +137,4 @@ describe('Module mocking', () => {
133137
const newNow = require('react-native').now;
134138
expect(newNow).not.toBe(oldNow);
135139
});
136-
137-
it('should unmock all modules when clearMocks is called', () => {
138-
// Mock a module
139-
const mockFactory = () => ({ mockProperty: 'mocked' });
140-
mock('react-native', mockFactory);
141-
142-
// Verify it's mocked
143-
const module = require('react-native');
144-
expect(module.mockProperty).toBe('mocked');
145-
146-
// Unmock all modules
147-
clearMocks();
148-
149-
// Verify it's back to actual
150-
const actualModule = require('react-native');
151-
expect(actualModule).not.toHaveProperty('mockProperty');
152-
});
153140
});

packages/runtime/assets/harness-module-system.js

Lines changed: 25 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,22 @@
55
// to allow capturing nested require calls.
66

77
(function (globalObject) {
8-
// @ts-ignore
9-
const originalDefine = globalObject.__d;
8+
const myRequire = function (id) {
9+
return globalObject.__r(id);
10+
};
1011

11-
if (!originalDefine) {
12-
// If __d is not defined, we are probably not in a Metro environment or
13-
// the module system hasn't loaded yet.
14-
return;
15-
}
12+
const myImportDefault = function (id) {
13+
return globalObject.__r.importDefault(id);
14+
};
15+
16+
const myImportAll = function (id) {
17+
return globalObject.__r.importAll(id);
18+
};
1619

1720
// Monkey-patch define
18-
// @ts-ignore
21+
const originalDefine = globalObject.__d;
1922
globalObject.__d = function (factory, moduleId, dependencyMap) {
20-
// Create a wrapped factory
2123
const wrappedFactory = function (...args) {
22-
// 1. Your Custom Require
23-
const myRequire = function (id) {
24-
// Logic to capture/redirect the require
25-
// globalObject.__r is the global require function from Metro
26-
// @ts-ignore
27-
return globalObject.__r(id);
28-
};
29-
30-
// 2. Custom importDefault (MUST use myRequire)
31-
const myImportDefault = function (id) {
32-
const mod = myRequire(id);
33-
return mod && mod.__esModule ? mod.default : mod;
34-
};
35-
36-
// 3. Custom importAll (MUST use myRequire)
37-
const myImportAll = function (id) {
38-
const mod = myRequire(id);
39-
if (mod && mod.__esModule) {
40-
return mod;
41-
}
42-
43-
const result = {};
44-
if (mod) {
45-
for (const key in mod) {
46-
if (Object.prototype.hasOwnProperty.call(mod, key)) {
47-
result[key] = mod[key];
48-
}
49-
}
50-
}
51-
result.default = mod;
52-
return result;
53-
};
54-
5524
// Standard Metro with import support (7 arguments)
5625
// args: global, require, importDefault, importAll, module, exports, dependencyMap
5726
const global = args[0];
@@ -74,49 +43,24 @@
7443
return originalDefine.call(this, wrappedFactory, moduleId, dependencyMap);
7544
};
7645

77-
// Implement __clearModule
78-
// This allows the test runner to re-evaluate modules by clearing them from the cache
79-
globalObject.__clearModule = function (moduleId) {
80-
if (globalObject.__r && globalObject.__r.getModules) {
81-
const modules = globalObject.__r.getModules();
82-
if (modules && modules.has(moduleId)) {
83-
modules.delete(moduleId);
84-
}
46+
globalObject.__resetModule = function (moduleId) {
47+
const module = globalObject.__r.getModules().get(moduleId);
48+
49+
if (!module) {
50+
return;
8551
}
52+
53+
module.hasError = false;
54+
module.error = undefined;
55+
module.isInitialized = false;
8656
};
8757

88-
// Implement __resetAllModules
89-
// This allows the test runner to reset the state of all modules
90-
globalObject.__resetAllModules = function () {
91-
if (globalObject.__r && globalObject.__r.getModules) {
92-
const modules = globalObject.__r.getModules();
93-
if (modules) {
94-
modules.forEach(function (mod, moduleId) {
95-
if (mod) {
96-
// We need to create a new object to ensure that the module is re-evaluated
97-
// Mutating existing module directly might not work as expected in some cases
98-
const newMod = {};
99-
for (const key in mod) {
100-
if (Object.prototype.hasOwnProperty.call(mod, key)) {
101-
newMod[key] = mod[key];
102-
}
103-
}
104-
newMod.isInitialized = false;
105-
// Reset publicModule.exports to ensure a clean start
106-
// This is crucial because if we reuse the old exports object,
107-
// the module factory might append to it instead of overwriting it,
108-
// or we might be left with stale state (e.g., class definitions)
109-
newMod.publicModule = { exports: {} };
110-
111-
// Also clear error state
112-
newMod.hasError = false;
113-
newMod.error = undefined;
58+
globalObject.__resetModules = function () {
59+
const modules = globalObject.__r.getModules();
11460

115-
modules.set(moduleId, newMod);
116-
}
117-
});
118-
}
119-
}
61+
modules.forEach(function (mod, moduleId) {
62+
globalObject.__resetModule(moduleId);
63+
});
12064
};
12165
})(
12266
typeof globalThis !== 'undefined'

packages/runtime/src/bundler/evaluate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const evaluateModule = (moduleJs: string, modulePath: string): void => {
1818
const moduleId = Number(__rParam);
1919

2020
// This is important as if module was already initialized, it would not be re-initialized
21-
global.__clearModule(moduleId);
21+
global.__resetModule(moduleId);
2222

2323
// eslint-disable-next-line no-eval
2424
eval(moduleJs);
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export {
22
mock,
33
requireActual,
4-
clearMocks,
54
unmock,
65
resetModules,
76
} from './registry.js';

packages/runtime/src/mocker/metro-require.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ import type { Require } from './types.js';
33
declare global {
44
var __r: Require;
55
var __resetAllModules: () => void;
6-
var __clearModule: (moduleId: number) => void;
6+
var __resetModule: (moduleId: number) => void;
77
}

packages/runtime/src/mocker/registry.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,28 @@
11
import { ModuleFactory, ModuleId, Require } from './types.js';
22

3+
const modulesCache = new Map<number, unknown>();
34
const mockRegistry = new Map<number, ModuleFactory>();
4-
const mockCache = new Map<number, unknown>();
55

66
const originalRequire = global.__r;
77

88
export const mock = (moduleId: string, factory: ModuleFactory): void => {
9-
mockCache.delete(moduleId as unknown as ModuleId);
9+
modulesCache.delete(moduleId as unknown as ModuleId);
1010
mockRegistry.set(moduleId as unknown as ModuleId, factory);
1111
};
1212

13-
export const clearMocks = (): void => {
14-
mockRegistry.clear();
15-
mockCache.clear();
13+
const isModuleMocked = (moduleId: number): boolean => {
14+
return mockRegistry.has(moduleId);
1615
};
1716

1817
const getMockImplementation = (moduleId: number): unknown | null => {
19-
if (mockCache.has(moduleId)) {
20-
return mockCache.get(moduleId);
21-
}
22-
2318
const factory = mockRegistry.get(moduleId);
19+
2420
if (!factory) {
2521
return null;
2622
}
2723

2824
const implementation = factory();
29-
mockCache.set(moduleId, implementation);
25+
modulesCache.set(moduleId, implementation);
3026
return implementation;
3127
};
3228

@@ -37,25 +33,32 @@ export const requireActual = <T = any>(moduleId: string): T =>
3733

3834
export const unmock = (moduleId: string) => {
3935
mockRegistry.delete(moduleId as unknown as ModuleId);
40-
mockCache.delete(moduleId as unknown as ModuleId);
36+
modulesCache.delete(moduleId as unknown as ModuleId);
4137
};
4238

4339
export const resetModules = (): void => {
44-
mockCache.clear();
45-
46-
// Reset Metro's module cache
47-
global.__resetAllModules();
40+
modulesCache.clear();
41+
mockRegistry.clear();
4842
};
4943

5044
const mockRequire = (moduleId: string) => {
5145
// babel plugin will transform 'moduleId' to a number
52-
const mockedModule = getMockImplementation(moduleId as unknown as ModuleId);
46+
const moduleIdNumber = moduleId as unknown as ModuleId;
47+
const cachedModule = modulesCache.get(moduleIdNumber);
48+
49+
if (cachedModule) {
50+
return cachedModule;
51+
}
5352

54-
if (mockedModule) {
53+
if (isModuleMocked(moduleIdNumber)) {
54+
const mockedModule = getMockImplementation(moduleIdNumber);
55+
modulesCache.set(moduleIdNumber, mockedModule);
5556
return mockedModule;
5657
}
5758

58-
return originalRequire(moduleId as unknown as ModuleId);
59+
const originalModule = originalRequire(moduleIdNumber);
60+
modulesCache.set(moduleIdNumber, originalModule);
61+
return originalModule;
5962
};
6063

6164
Object.setPrototypeOf(mockRequire, Object.getPrototypeOf(originalRequire));

0 commit comments

Comments
 (0)