Skip to content

Commit baea33f

Browse files
authored
fix(core): restore async doMock factory and doUnmock recovery (#998)
1 parent 1369064 commit baea33f

5 files changed

Lines changed: 85 additions & 19 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { expect, rs, test } from '@rstest/core';
2+
3+
test('doMock should support async factory with importActual', async () => {
4+
rs.doMock('../src/increment', async () => {
5+
const actual =
6+
await rs.importActual<typeof import('../src/increment')>(
7+
'../src/increment',
8+
);
9+
10+
return {
11+
...actual,
12+
increment: (num: number) => num + 100,
13+
};
14+
});
15+
16+
const { increment } = await import('../src/increment');
17+
expect(increment(1)).toBe(101);
18+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { afterEach, expect, rs, test } from '@rstest/core';
2+
3+
afterEach(() => {
4+
rs.doUnmock('../src/increment');
5+
rs.resetModules();
6+
});
7+
8+
test('doUnmock should restore original after multiple doMock calls', async () => {
9+
rs.doMock('../src/increment', () => ({
10+
increment: (num: number) => num + 100,
11+
}));
12+
13+
const { increment: firstIncrement } = await import('../src/increment');
14+
expect(firstIncrement(1)).toBe(101);
15+
16+
rs.resetModules();
17+
18+
rs.doMock('../src/increment', () => ({
19+
increment: (num: number) => num + 200,
20+
}));
21+
22+
const { increment: secondIncrement } = await import('../src/increment');
23+
expect(secondIncrement(1)).toBe(201);
24+
25+
rs.doUnmock('../src/increment');
26+
rs.resetModules();
27+
28+
const { increment } = await import('../src/increment');
29+
expect(increment(1)).toBe(2);
30+
});

packages/core/src/core/plugins/mockRuntimeCode.js

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ __webpack_require__ = new Proxy(
3535
__webpack_require__.rstest_original_modules = {};
3636
__webpack_require__.rstest_original_module_factories = {};
3737

38+
const hasOwn = (target, property) => Object.hasOwn(target, property);
39+
40+
const isPromise = (value) => value instanceof Promise;
41+
3842
//#region rs.unmock
3943
__webpack_require__.rstest_unmock = (id) => {
4044
const originalModuleFactory =
@@ -55,12 +59,11 @@ __webpack_require__.rstest_do_unmock = __webpack_require__.rstest_unmock;
5559
//#region rs.requireActual
5660
__webpack_require__.rstest_require_actual =
5761
__webpack_require__.rstest_import_actual = (id) => {
58-
const originalModule = __webpack_require__.rstest_original_modules[id];
59-
60-
if (originalModule) {
61-
return originalModule;
62+
if (hasOwn(__webpack_require__.rstest_original_modules, id)) {
63+
return __webpack_require__.rstest_original_modules[id];
6264
}
63-
if (id in __webpack_require__.rstest_original_module_factories) {
65+
66+
if (hasOwn(__webpack_require__.rstest_original_module_factories, id)) {
6467
const mod = __webpack_require__.rstest_original_module_factories[id];
6568
const moduleInstance = { exports: {} };
6669
mod(moduleInstance, moduleInstance.exports, __webpack_require__);
@@ -75,20 +78,30 @@ __webpack_require__.rstest_require_actual =
7578
const getMockImplementation = (mockType = 'mock') => {
7679
const isMockRequire =
7780
mockType === 'mockRequire' || mockType === 'doMockRequire';
81+
7882
// The mock and mockRequire will resolve to different module ids when the module is a dual package
7983
return (id, modFactory) => {
8084
// Only load the module if it's already in cache (to avoid side effects)
81-
let requiredModule = __webpack_module_cache__[id]?.exports;
82-
const wasAlreadyLoaded = !!requiredModule;
83-
84-
if (!requiredModule) {
85-
// Module hasn't been loaded yet, so we can't get the original
86-
// But we still need to save the original factory if it exists
87-
__webpack_require__.rstest_original_module_factories[id] =
88-
__webpack_modules__[id];
89-
} else {
90-
// Module was already loaded, save it
85+
const hasCachedModule = hasOwn(__webpack_module_cache__, id);
86+
let requiredModule = hasCachedModule
87+
? __webpack_module_cache__[id].exports
88+
: undefined;
89+
const wasAlreadyLoaded = hasCachedModule;
90+
91+
const hasSavedOriginalModule = hasOwn(
92+
__webpack_require__.rstest_original_modules,
93+
id,
94+
);
95+
const hasSavedOriginalFactory = hasOwn(
96+
__webpack_require__.rstest_original_module_factories,
97+
id,
98+
);
99+
100+
if (!hasSavedOriginalModule && hasCachedModule) {
91101
__webpack_require__.rstest_original_modules[id] = requiredModule;
102+
}
103+
104+
if (!hasSavedOriginalFactory) {
92105
__webpack_require__.rstest_original_module_factories[id] =
93106
__webpack_modules__[id];
94107
}
@@ -170,6 +183,11 @@ const getMockImplementation = (mockType = 'mock') => {
170183
) {
171184
const res = modFactory();
172185

186+
if (isPromise(res)) {
187+
__webpack_module__.exports = res;
188+
return;
189+
}
190+
173191
if (isMockRequire) {
174192
__webpack_module__.exports = res;
175193
return;

website/docs/en/api/runtime-api/rstest/mock-modules.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Rstest supports mocking modules, which allows you to replace the implementation
88

99
## rs.mock
1010

11-
- **Type**: `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Partial<T>) | { spy: true } | { mock: true }) => void`
11+
- **Type**: `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Promise<Partial<T>> | Partial<T>) | { spy: true } | { mock: true }) => void`
1212

1313
Mocks and replaces the module specified in the first parameter.
1414

@@ -226,7 +226,7 @@ rs.mock(import('../src/b'), () => {
226226

227227
## rs.doMock
228228

229-
- **Type**: `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Partial<T>) | { spy: true } | { mock: true }) => void`
229+
- **Type**: `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Promise<Partial<T>> | Partial<T>) | { spy: true } | { mock: true }) => void`
230230

231231
Similar to `rs.mock`, but it is **not hoisted** to the top of the module. It is called when it's executed, which means that if a module has already been imported before calling `rs.doMock`, that module will not be mocked, while modules imported after calling `rs.doMock` will be mocked.
232232

website/docs/zh/api/runtime-api/rstest/mock-modules.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Rstest 支持对模块进行 mock,这使得你可以在测试中替换模块
88

99
## rs.mock
1010

11-
- **类型:** `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Partial<T>) | { spy: true } | { mock: true }) => void`
11+
- **类型:** `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Promise<Partial<T>> | Partial<T>) | { spy: true } | { mock: true }) => void`
1212

1313
对第一个参数对应的模块进行 mock 替换。
1414

@@ -226,7 +226,7 @@ rs.mock(import('../src/b'), () => {
226226

227227
## rs.doMock
228228

229-
- **类型:** `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Partial<T>) | { spy: true } | { mock: true }) => void`
229+
- **类型:** `<T = unknown>(moduleName: string | Promise<T>, factoryOrOptions?: (() => Promise<Partial<T>> | Partial<T>) | { spy: true } | { mock: true }) => void`
230230

231231
`rs.mock` 类似,但它**不会被提升**到模块顶部。它会在被执行到时调用,这意味着如果在调用 `rs.doMock` 之前已经导入了模块,则该模块不会被 mock,而在调用 `rs.doMock` 之后导入的模块会被 mock。
232232

0 commit comments

Comments
 (0)