hermes-test 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hermes-test.js +14 -14
- package/dist/harness.bundle.js +212 -98
- package/globals.d.ts +12 -9
- package/index.d.ts +33 -26
- package/package.json +4 -4
- package/src/expect.ts +202 -114
- package/src/fetch.ts +19 -5
- package/src/harness.ts +129 -33
- package/src/hooks.ts +131 -43
- package/src/index.ts +22 -14
- package/src/mock.ts +15 -11
- package/src/polyfills.js +151 -73
- package/src/render.ts +54 -32
- package/src/shims/async-storage.js +9 -9
- package/src/shims/react-i18next.js +30 -10
- package/src/shims/react-native-launch-arguments.js +3 -1
- package/src/shims/react-native.js +133 -41
- package/src/shims/react.js +3 -3
- package/src/shims/rtk-query-core.js +18 -8
- package/src/shims/rtk-query.js +18 -8
- package/src/shims/tanstack-query.js +2 -2
- package/src/spy.ts +32 -36
- package/src/store.ts +27 -17
- package/src/timers.ts +5 -3
package/src/fetch.ts
CHANGED
|
@@ -75,13 +75,17 @@ function fakeFetch(input: any, init?: any): any {
|
|
|
75
75
|
const method = (init?.method || 'GET').toUpperCase();
|
|
76
76
|
let body = init?.body;
|
|
77
77
|
if (typeof body === 'string') {
|
|
78
|
-
try {
|
|
78
|
+
try {
|
|
79
|
+
body = JSON.parse(body);
|
|
80
|
+
} catch {}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
const reqHeaders: Record<string, string> = {};
|
|
82
84
|
if (init?.headers) {
|
|
83
85
|
if (typeof init.headers.forEach === 'function') {
|
|
84
|
-
init.headers.forEach((v: string, k: string) => {
|
|
86
|
+
init.headers.forEach((v: string, k: string) => {
|
|
87
|
+
reqHeaders[k] = v;
|
|
88
|
+
});
|
|
85
89
|
} else {
|
|
86
90
|
Object.assign(reqHeaders, init.headers);
|
|
87
91
|
}
|
|
@@ -122,14 +126,24 @@ function fakeFetch(input: any, init?: any): any {
|
|
|
122
126
|
has: (k: string) => k.toLowerCase() in responseHeaders,
|
|
123
127
|
},
|
|
124
128
|
json: () => Promise.resolve(responseBody),
|
|
125
|
-
text: () =>
|
|
126
|
-
|
|
129
|
+
text: () =>
|
|
130
|
+
Promise.resolve(
|
|
131
|
+
typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody),
|
|
132
|
+
),
|
|
133
|
+
clone: function () {
|
|
134
|
+
return this;
|
|
135
|
+
},
|
|
127
136
|
});
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
// --- Public API ---
|
|
131
140
|
|
|
132
|
-
function createHandler(
|
|
141
|
+
function createHandler(
|
|
142
|
+
method: Method,
|
|
143
|
+
url: string | RegExp,
|
|
144
|
+
response: MockResponseInit | ((req: MockRequest) => MockResponseInit),
|
|
145
|
+
once = false,
|
|
146
|
+
): MockHandler {
|
|
133
147
|
const handler = typeof response === 'function' ? response : () => response;
|
|
134
148
|
return { method, url, handler, once };
|
|
135
149
|
}
|
package/src/harness.ts
CHANGED
|
@@ -4,13 +4,18 @@
|
|
|
4
4
|
// Console interceptor — replace console with print()-based output.
|
|
5
5
|
// Hermes's native print() writes to stdout and is always available.
|
|
6
6
|
// This works in all bundle modes (single, split, watch).
|
|
7
|
-
(function() {
|
|
7
|
+
(function () {
|
|
8
8
|
const p = (globalThis as any).print || (() => {});
|
|
9
9
|
function fmt(...args: any[]) {
|
|
10
|
-
return args
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
return args
|
|
11
|
+
.map((a: any) => {
|
|
12
|
+
try {
|
|
13
|
+
return typeof a === 'string' ? a : JSON.stringify(a, null, 2);
|
|
14
|
+
} catch {
|
|
15
|
+
return String(a);
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.join(' ');
|
|
14
19
|
}
|
|
15
20
|
(globalThis as any).console = {
|
|
16
21
|
log: (...args: any[]) => p(fmt(...args)),
|
|
@@ -32,8 +37,22 @@ import { spy, spyOn, clearAllMocks } from './spy';
|
|
|
32
37
|
import { renderHook, act, waitFor } from './hooks';
|
|
33
38
|
import { render, fireEvent } from './render';
|
|
34
39
|
import { useMock, mockModule, resetMocks, resetMockModulePatches } from './mock';
|
|
35
|
-
import {
|
|
36
|
-
|
|
40
|
+
import {
|
|
41
|
+
mockFetch,
|
|
42
|
+
mockFetchUse,
|
|
43
|
+
mockFetchReset,
|
|
44
|
+
mockFetchClear,
|
|
45
|
+
http,
|
|
46
|
+
HttpResponse,
|
|
47
|
+
} from './fetch';
|
|
48
|
+
import {
|
|
49
|
+
useFakeTimers,
|
|
50
|
+
useRealTimers,
|
|
51
|
+
advanceTimersByTime,
|
|
52
|
+
runAllTimers,
|
|
53
|
+
getTimerCount,
|
|
54
|
+
advanceTimersToNextTimer,
|
|
55
|
+
} from './timers';
|
|
37
56
|
|
|
38
57
|
type TestFn = ((ctx: TestContext) => void | Promise<void>) | (() => void | Promise<void>);
|
|
39
58
|
type TestContext = {
|
|
@@ -83,11 +102,11 @@ function test(name: string, fn: TestFn, options?: TestOptions): void {
|
|
|
83
102
|
});
|
|
84
103
|
}
|
|
85
104
|
|
|
86
|
-
test.only = function(name: string, fn: TestFn): void {
|
|
105
|
+
test.only = function (name: string, fn: TestFn): void {
|
|
87
106
|
test(name, fn, { only: true });
|
|
88
107
|
};
|
|
89
108
|
|
|
90
|
-
test.skip = function(name: string, fn: TestFn): void {
|
|
109
|
+
test.skip = function (name: string, fn: TestFn): void {
|
|
91
110
|
test(name, fn, { skip: true });
|
|
92
111
|
};
|
|
93
112
|
|
|
@@ -156,8 +175,14 @@ function flushAsync<T = any>(promise: Promise<T> | T): T {
|
|
|
156
175
|
let error: any;
|
|
157
176
|
let settled = false;
|
|
158
177
|
(promise as Promise<T>).then(
|
|
159
|
-
|
|
160
|
-
|
|
178
|
+
v => {
|
|
179
|
+
result = v;
|
|
180
|
+
settled = true;
|
|
181
|
+
},
|
|
182
|
+
e => {
|
|
183
|
+
error = e;
|
|
184
|
+
settled = true;
|
|
185
|
+
},
|
|
161
186
|
);
|
|
162
187
|
// drainMicrotasks() flushes all pending microtasks. One call should settle
|
|
163
188
|
// most promises. Loop only as safety net for edge cases (macrotask scheduling).
|
|
@@ -199,10 +224,14 @@ function _printFileResult(file: string, passed: number, failed: number, duration
|
|
|
199
224
|
// Clear progress line before printing failure
|
|
200
225
|
_print(`\r\x1b[K`);
|
|
201
226
|
}
|
|
202
|
-
_print(
|
|
227
|
+
_print(
|
|
228
|
+
` \x1b[31mFAIL\x1b[0m ${file} \x1b[2m(${passed} passed, ${failed} failed)\x1b[0m${time}\n`,
|
|
229
|
+
);
|
|
203
230
|
} else if ((globalThis as any).__HT_coverage) {
|
|
204
231
|
// In-place progress counter
|
|
205
|
-
_print(
|
|
232
|
+
_print(
|
|
233
|
+
`\r\x1b[K \x1b[2mRunning...\x1b[0m ${_filesCompleted}/${_totalFiles} files (${_testsCompleted} tests)`,
|
|
234
|
+
);
|
|
206
235
|
} else {
|
|
207
236
|
_print(` \x1b[32mPASS\x1b[0m ${file} \x1b[2m(${total} tests)\x1b[0m${time}\n`);
|
|
208
237
|
}
|
|
@@ -225,18 +254,35 @@ function formatTestError(e: any): string {
|
|
|
225
254
|
|
|
226
255
|
// Filter to application frames only (skip react internals, harness, native)
|
|
227
256
|
const skipFn = new Set([
|
|
228
|
-
'anonymous',
|
|
229
|
-
'
|
|
257
|
+
'anonymous',
|
|
258
|
+
'global',
|
|
259
|
+
'__init',
|
|
260
|
+
'apply',
|
|
261
|
+
'map',
|
|
262
|
+
'react-stack-bottom-frame',
|
|
263
|
+
'proxy trap',
|
|
230
264
|
]);
|
|
231
265
|
const skipPrefix = [
|
|
232
|
-
'render',
|
|
233
|
-
'
|
|
266
|
+
'render',
|
|
267
|
+
'run',
|
|
268
|
+
'perform',
|
|
269
|
+
'work',
|
|
270
|
+
'flush',
|
|
271
|
+
'begin',
|
|
272
|
+
'update',
|
|
273
|
+
'reconcile',
|
|
274
|
+
'create',
|
|
275
|
+
'complete',
|
|
276
|
+
'commit',
|
|
277
|
+
'process',
|
|
234
278
|
];
|
|
235
279
|
const appFrames = frames.filter(f => {
|
|
236
280
|
if (skipFn.has(f.fn)) return false;
|
|
237
281
|
if (f.file.includes('harness') || f.file.includes('runner')) return false;
|
|
238
282
|
if (f.fn === '' && !f.file.includes('/src/') && !f.file.includes('packages/')) return false;
|
|
239
|
-
for (const p of skipPrefix) {
|
|
283
|
+
for (const p of skipPrefix) {
|
|
284
|
+
if (f.fn.startsWith(p)) return false;
|
|
285
|
+
}
|
|
240
286
|
return true;
|
|
241
287
|
});
|
|
242
288
|
|
|
@@ -246,7 +292,11 @@ function formatTestError(e: any): string {
|
|
|
246
292
|
cleanStack += '\n';
|
|
247
293
|
for (const f of appFrames.slice(0, 8)) {
|
|
248
294
|
// esbuild names __esm blocks with source paths like "src/utils/string.ts"
|
|
249
|
-
const loc = f.fn.includes('/')
|
|
295
|
+
const loc = f.fn.includes('/')
|
|
296
|
+
? f.fn
|
|
297
|
+
: f.fn
|
|
298
|
+
? f.fn + ' (' + f.file + ':' + f.line + ')'
|
|
299
|
+
: f.file + ':' + f.line;
|
|
250
300
|
cleanStack += '\n at ' + loc;
|
|
251
301
|
}
|
|
252
302
|
}
|
|
@@ -265,15 +315,23 @@ function formatTestError(e: any): string {
|
|
|
265
315
|
const nmMatch = srcPath.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/);
|
|
266
316
|
if (nmMatch) {
|
|
267
317
|
const pkg = nmMatch[1];
|
|
268
|
-
hint =
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
318
|
+
hint =
|
|
319
|
+
'\n\n "' +
|
|
320
|
+
pkg +
|
|
321
|
+
'" crashed during initialization (native dependency).' +
|
|
322
|
+
'\n Add to externals in hermes-test.config.json:\n\n' +
|
|
323
|
+
' { "externals": ["' +
|
|
324
|
+
pkg +
|
|
325
|
+
'"] }\n' +
|
|
326
|
+
'\n Or mock the module that imports it with ht.mock().\n';
|
|
272
327
|
} else {
|
|
273
328
|
const cleanPath = srcPath.replace(/\/index\.(tsx?|jsx?)$/, '');
|
|
274
|
-
hint =
|
|
275
|
-
|
|
276
|
-
+
|
|
329
|
+
hint =
|
|
330
|
+
'\n\n Module "' +
|
|
331
|
+
cleanPath +
|
|
332
|
+
'" crashed during initialization.' +
|
|
333
|
+
'\n A dependency uses an API not available in Hermes.' +
|
|
334
|
+
'\n Mock it with ht.mock() or add the native dep to externals.\n';
|
|
277
335
|
}
|
|
278
336
|
break;
|
|
279
337
|
}
|
|
@@ -288,9 +346,18 @@ function formatTestError(e: any): string {
|
|
|
288
346
|
if (importMap[k] === modPath && siblings.indexOf(k) === -1) siblings.push(k);
|
|
289
347
|
}
|
|
290
348
|
const mockBody = siblings.map(s => ' ' + s + ': () => {}').join(',\n');
|
|
291
|
-
hint =
|
|
292
|
-
|
|
293
|
-
|
|
349
|
+
hint =
|
|
350
|
+
'\n\n "' +
|
|
351
|
+
fnName +
|
|
352
|
+
'" from "' +
|
|
353
|
+
modPath +
|
|
354
|
+
'" failed.' +
|
|
355
|
+
'\n Add this mock to your test file:\n\n' +
|
|
356
|
+
" ht.mock('" +
|
|
357
|
+
modPath +
|
|
358
|
+
"', () => ({\n" +
|
|
359
|
+
mockBody +
|
|
360
|
+
'\n }));\n';
|
|
294
361
|
break;
|
|
295
362
|
}
|
|
296
363
|
}
|
|
@@ -301,7 +368,7 @@ function formatTestError(e: any): string {
|
|
|
301
368
|
|
|
302
369
|
function runTests(): TestResult[] {
|
|
303
370
|
const results: TestResult[] = [];
|
|
304
|
-
const hasOnly = tests.some(
|
|
371
|
+
const hasOnly = tests.some(t => t.options.only);
|
|
305
372
|
|
|
306
373
|
// Count unique files for progress counter
|
|
307
374
|
const uniqueFiles = new Set(tests.map(t => t.file));
|
|
@@ -317,7 +384,7 @@ function runTests(): TestResult[] {
|
|
|
317
384
|
let _fileFailures: { name: string; error: string }[] = [];
|
|
318
385
|
|
|
319
386
|
function _flushFileResult() {
|
|
320
|
-
if (_currentFile &&
|
|
387
|
+
if (_currentFile && _filePassed + _fileFailed > 0) {
|
|
321
388
|
_printFileResult(_currentFile, _filePassed, _fileFailed, Date.now() - _fileStart);
|
|
322
389
|
// Print failure details
|
|
323
390
|
for (const f of _fileFailures) {
|
|
@@ -476,7 +543,9 @@ function registerCrash(file: string, error: string): void {
|
|
|
476
543
|
const formatted = formatTestError({ message: error.split('\n')[0], stack: error });
|
|
477
544
|
tests.push({
|
|
478
545
|
name: `[CRASH] ${file}`,
|
|
479
|
-
fn: () => {
|
|
546
|
+
fn: () => {
|
|
547
|
+
throw new Error(formatted);
|
|
548
|
+
},
|
|
480
549
|
options: {},
|
|
481
550
|
file,
|
|
482
551
|
});
|
|
@@ -553,4 +622,31 @@ const unmock = (_modulePath: string) => {}; // Bundler directive — no runtime
|
|
|
553
622
|
advanceTimersToNextTimer,
|
|
554
623
|
};
|
|
555
624
|
|
|
556
|
-
export {
|
|
625
|
+
export {
|
|
626
|
+
test,
|
|
627
|
+
expect,
|
|
628
|
+
spy,
|
|
629
|
+
spyOn,
|
|
630
|
+
clearAllMocks,
|
|
631
|
+
group,
|
|
632
|
+
describe,
|
|
633
|
+
beforeEach,
|
|
634
|
+
afterEach,
|
|
635
|
+
beforeAll,
|
|
636
|
+
afterAll,
|
|
637
|
+
renderHook,
|
|
638
|
+
act,
|
|
639
|
+
waitFor,
|
|
640
|
+
render,
|
|
641
|
+
fireEvent,
|
|
642
|
+
useMock,
|
|
643
|
+
http,
|
|
644
|
+
HttpResponse,
|
|
645
|
+
flushAsync,
|
|
646
|
+
useFakeTimers,
|
|
647
|
+
useRealTimers,
|
|
648
|
+
advanceTimersByTime,
|
|
649
|
+
runAllTimers,
|
|
650
|
+
getTimerCount,
|
|
651
|
+
advanceTimersToNextTimer,
|
|
652
|
+
};
|
package/src/hooks.ts
CHANGED
|
@@ -14,7 +14,10 @@ function getReact(): typeof import('react') {
|
|
|
14
14
|
|
|
15
15
|
function getReconcilerModule(): any {
|
|
16
16
|
const R = (globalThis as any).__HT_Reconciler;
|
|
17
|
-
if (!R)
|
|
17
|
+
if (!R)
|
|
18
|
+
throw new Error(
|
|
19
|
+
'react-reconciler not available. Make sure it is installed (it ships with hermes-test).',
|
|
20
|
+
);
|
|
18
21
|
return R;
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -33,55 +36,129 @@ const hostConfig = {
|
|
|
33
36
|
supportsMicrotasks: true,
|
|
34
37
|
isPrimaryRenderer: true,
|
|
35
38
|
warnsIfNotActing: true,
|
|
36
|
-
createInstance(type: string, props: any) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
createInstance(type: string, props: any) {
|
|
40
|
+
const { children: _c, ...rest } = props;
|
|
41
|
+
return { type, props: rest, children: [] };
|
|
42
|
+
},
|
|
43
|
+
createTextInstance(text: string) {
|
|
44
|
+
return { type: '__TEXT__', props: {}, text, children: [] };
|
|
45
|
+
},
|
|
46
|
+
appendInitialChild(p: any, c: any) {
|
|
47
|
+
p.children.push(c);
|
|
48
|
+
c._parent = p;
|
|
49
|
+
},
|
|
50
|
+
appendChild(p: any, c: any) {
|
|
51
|
+
p.children.push(c);
|
|
52
|
+
c._parent = p;
|
|
53
|
+
},
|
|
54
|
+
appendChildToContainer(p: any, c: any) {
|
|
55
|
+
p.children.push(c);
|
|
56
|
+
c._parent = p;
|
|
57
|
+
},
|
|
58
|
+
removeChild(p: any, c: any) {
|
|
59
|
+
const i = p.children.indexOf(c);
|
|
60
|
+
if (i !== -1) p.children.splice(i, 1);
|
|
61
|
+
},
|
|
62
|
+
removeChildFromContainer(p: any, c: any) {
|
|
63
|
+
const i = p.children.indexOf(c);
|
|
64
|
+
if (i !== -1) p.children.splice(i, 1);
|
|
65
|
+
},
|
|
66
|
+
insertBefore(p: any, c: any, b: any) {
|
|
67
|
+
const i = p.children.indexOf(b);
|
|
68
|
+
p.children.splice(i, 0, c);
|
|
69
|
+
c._parent = p;
|
|
70
|
+
},
|
|
71
|
+
insertInContainerBefore(p: any, c: any, b: any) {
|
|
72
|
+
const i = p.children.indexOf(b);
|
|
73
|
+
p.children.splice(i, 0, c);
|
|
74
|
+
c._parent = p;
|
|
75
|
+
},
|
|
76
|
+
commitUpdate(inst: any, _type: any, _oldProps: any, newProps: any) {
|
|
77
|
+
const { children: _c, ...rest } = newProps;
|
|
78
|
+
inst.props = rest;
|
|
79
|
+
},
|
|
80
|
+
commitTextUpdate(inst: any, _oldText: string, newText: string) {
|
|
81
|
+
inst.text = newText;
|
|
82
|
+
},
|
|
47
83
|
commitMount() {},
|
|
48
|
-
prepareForCommit() {
|
|
84
|
+
prepareForCommit() {
|
|
85
|
+
return null;
|
|
86
|
+
},
|
|
49
87
|
resetAfterCommit() {},
|
|
50
88
|
resetTextContent() {},
|
|
51
|
-
finalizeInitialChildren() {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
89
|
+
finalizeInitialChildren() {
|
|
90
|
+
return false;
|
|
91
|
+
},
|
|
92
|
+
shouldSetTextContent() {
|
|
93
|
+
return false;
|
|
94
|
+
},
|
|
95
|
+
getRootHostContext() {
|
|
96
|
+
return null;
|
|
97
|
+
},
|
|
98
|
+
getChildHostContext(ctx: any) {
|
|
99
|
+
return ctx;
|
|
100
|
+
},
|
|
101
|
+
getPublicInstance(inst: any) {
|
|
102
|
+
return inst;
|
|
103
|
+
},
|
|
104
|
+
prepareUpdate() {
|
|
105
|
+
return {};
|
|
106
|
+
},
|
|
107
|
+
clearContainer(c: any) {
|
|
108
|
+
c.children = [];
|
|
109
|
+
},
|
|
58
110
|
scheduleTimeout: (globalThis as any).setTimeout || ((fn: any) => fn()),
|
|
59
111
|
cancelTimeout: (globalThis as any).clearTimeout || (() => {}),
|
|
60
112
|
noTimeout: -1,
|
|
61
|
-
scheduleMicrotask:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
113
|
+
scheduleMicrotask:
|
|
114
|
+
typeof queueMicrotask === 'function' ? queueMicrotask : (fn: any) => Promise.resolve().then(fn),
|
|
115
|
+
getCurrentEventPriority() {
|
|
116
|
+
return getReconcilerConstants().DefaultEventPriority ?? 0;
|
|
117
|
+
},
|
|
118
|
+
setCurrentUpdatePriority(priority: number) {
|
|
119
|
+
currentUpdatePriority = priority;
|
|
120
|
+
},
|
|
121
|
+
getCurrentUpdatePriority() {
|
|
122
|
+
return currentUpdatePriority;
|
|
123
|
+
},
|
|
124
|
+
resolveUpdatePriority() {
|
|
125
|
+
return currentUpdatePriority || (getReconcilerConstants().DefaultEventPriority ?? 0);
|
|
126
|
+
},
|
|
127
|
+
shouldAttemptEagerTransition() {
|
|
128
|
+
return false;
|
|
129
|
+
},
|
|
67
130
|
trackSchedulerEvent() {},
|
|
68
|
-
resolveEventType() {
|
|
69
|
-
|
|
131
|
+
resolveEventType() {
|
|
132
|
+
return '';
|
|
133
|
+
},
|
|
134
|
+
resolveEventTimeStamp() {
|
|
135
|
+
return -1.1;
|
|
136
|
+
},
|
|
70
137
|
requestPostPaintCallback() {},
|
|
71
|
-
maySuspendCommit() {
|
|
72
|
-
|
|
138
|
+
maySuspendCommit() {
|
|
139
|
+
return false;
|
|
140
|
+
},
|
|
141
|
+
preloadInstance() {
|
|
142
|
+
return true;
|
|
143
|
+
},
|
|
73
144
|
startSuspendingCommit() {},
|
|
74
145
|
suspendInstance() {},
|
|
75
|
-
waitForCommitToBeReady() {
|
|
146
|
+
waitForCommitToBeReady() {
|
|
147
|
+
return null;
|
|
148
|
+
},
|
|
76
149
|
NotPendingTransition: null,
|
|
77
150
|
resetFormInstance() {},
|
|
78
151
|
hideInstance() {},
|
|
79
152
|
unhideInstance() {},
|
|
80
153
|
hideTextInstance() {},
|
|
81
154
|
unhideTextInstance() {},
|
|
82
|
-
getInstanceFromNode() {
|
|
155
|
+
getInstanceFromNode() {
|
|
156
|
+
return null;
|
|
157
|
+
},
|
|
83
158
|
prepareScopeUpdate() {},
|
|
84
|
-
getInstanceFromScope() {
|
|
159
|
+
getInstanceFromScope() {
|
|
160
|
+
return null;
|
|
161
|
+
},
|
|
85
162
|
detachDeletedInstance() {},
|
|
86
163
|
beforeActiveInstanceBlur() {},
|
|
87
164
|
afterActiveInstanceBlur() {},
|
|
@@ -133,8 +210,13 @@ export function act(fn: () => void | Promise<void>): void {
|
|
|
133
210
|
let settled = false;
|
|
134
211
|
let error: any;
|
|
135
212
|
(result as Promise<void>).then(
|
|
136
|
-
() => {
|
|
137
|
-
|
|
213
|
+
() => {
|
|
214
|
+
settled = true;
|
|
215
|
+
},
|
|
216
|
+
(e: any) => {
|
|
217
|
+
settled = true;
|
|
218
|
+
error = e;
|
|
219
|
+
},
|
|
138
220
|
);
|
|
139
221
|
drain();
|
|
140
222
|
if (error) throw error;
|
|
@@ -152,7 +234,7 @@ export function act(fn: () => void | Promise<void>): void {
|
|
|
152
234
|
|
|
153
235
|
export function renderHook<T>(
|
|
154
236
|
hookFn: (props?: any) => T,
|
|
155
|
-
options?: { initialProps?: any; wrapper?: any }
|
|
237
|
+
options?: { initialProps?: any; wrapper?: any },
|
|
156
238
|
): HookResult<T> {
|
|
157
239
|
const history: T[] = [];
|
|
158
240
|
let currentValue: T;
|
|
@@ -164,13 +246,17 @@ export function renderHook<T>(
|
|
|
164
246
|
const root = reconciler.createContainer(
|
|
165
247
|
container,
|
|
166
248
|
0, // LegacyRoot — effects fire synchronously in act()
|
|
167
|
-
null,
|
|
249
|
+
null, // hydrationCallbacks
|
|
168
250
|
false, // isStrictMode
|
|
169
251
|
false, // concurrentUpdatesByDefaultOverride
|
|
170
|
-
'',
|
|
171
|
-
(err: any) => {
|
|
172
|
-
|
|
173
|
-
|
|
252
|
+
'', // identifierPrefix
|
|
253
|
+
(err: any) => {
|
|
254
|
+
throw err;
|
|
255
|
+
}, // onUncaughtError
|
|
256
|
+
(err: any) => {
|
|
257
|
+
throw err;
|
|
258
|
+
}, // onCaughtError
|
|
259
|
+
null, // onRecoverableError
|
|
174
260
|
() => {}, // onDefaultTransitionIndicator
|
|
175
261
|
);
|
|
176
262
|
|
|
@@ -223,13 +309,15 @@ export function renderHook<T>(
|
|
|
223
309
|
|
|
224
310
|
export function waitFor<T>(
|
|
225
311
|
predicate: () => T | false | null | undefined,
|
|
226
|
-
options?: { timeout?: number; interval?: number }
|
|
312
|
+
options?: { timeout?: number; interval?: number },
|
|
227
313
|
): T {
|
|
228
314
|
const timeout = options?.timeout ?? 1000;
|
|
229
315
|
const start = Date.now();
|
|
230
316
|
|
|
231
317
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
232
|
-
act(() => {
|
|
318
|
+
act(() => {
|
|
319
|
+
drain();
|
|
320
|
+
});
|
|
233
321
|
drain();
|
|
234
322
|
|
|
235
323
|
const result = predicate();
|
package/src/index.ts
CHANGED
|
@@ -28,8 +28,12 @@ function _makeCtx(store: any) {
|
|
|
28
28
|
wrapper,
|
|
29
29
|
dispatch: store.dispatch.bind(store),
|
|
30
30
|
getState: store.getState.bind(store),
|
|
31
|
-
setState(state: Record<string, any>) {
|
|
32
|
-
|
|
31
|
+
setState(state: Record<string, any>) {
|
|
32
|
+
store.dispatch({ type: '__SET_STATE__', payload: state });
|
|
33
|
+
},
|
|
34
|
+
patchState(partial: Record<string, any>) {
|
|
35
|
+
store.dispatch({ type: '__PATCH__', payload: partial });
|
|
36
|
+
},
|
|
33
37
|
renderHookWithReduxStore<T>(hookFn: (props?: any) => T, options?: { initialProps?: any }) {
|
|
34
38
|
return ht.renderHook(hookFn, { ...options, wrapper });
|
|
35
39
|
},
|
|
@@ -38,11 +42,13 @@ function _makeCtx(store: any) {
|
|
|
38
42
|
|
|
39
43
|
export function withStore(initialState: Record<string, any> = {}) {
|
|
40
44
|
const { configureStore } = require('@reduxjs/toolkit');
|
|
41
|
-
return _makeCtx(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
return _makeCtx(
|
|
46
|
+
configureStore({
|
|
47
|
+
reducer: _withTestActions((s: any = initialState) => s),
|
|
48
|
+
preloadedState: initialState,
|
|
49
|
+
middleware: (gdm: any) => gdm({ serializableCheck: false, immutableCheck: false }),
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export function withAppReducer(
|
|
@@ -50,11 +56,13 @@ export function withAppReducer(
|
|
|
50
56
|
preloadedState?: Record<string, any>,
|
|
51
57
|
) {
|
|
52
58
|
const { configureStore } = require('@reduxjs/toolkit');
|
|
53
|
-
return _makeCtx(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
return _makeCtx(
|
|
60
|
+
configureStore({
|
|
61
|
+
reducer: _withTestActions(reducer),
|
|
62
|
+
preloadedState,
|
|
63
|
+
middleware: (gdm: any) => gdm({ serializableCheck: false, immutableCheck: false }),
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
interface RtkQueryApi {
|
|
@@ -90,8 +98,8 @@ export function setupApiStore(
|
|
|
90
98
|
middleware: (gdm: any) => {
|
|
91
99
|
let chain = gdm({ serializableCheck: false, immutableCheck: false });
|
|
92
100
|
for (const a of apis) chain = chain.concat(a.middleware);
|
|
93
|
-
for (const mw of
|
|
94
|
-
for (const mw of
|
|
101
|
+
for (const mw of options?.middleware?.concat ?? []) chain = chain.concat(mw);
|
|
102
|
+
for (const mw of options?.middleware?.prepend ?? []) chain = chain.prepend(mw);
|
|
95
103
|
return chain;
|
|
96
104
|
},
|
|
97
105
|
});
|
package/src/mock.ts
CHANGED
|
@@ -18,16 +18,13 @@ const mockRegistry: Record<string, Record<string, any>> = (globalThis as any).__
|
|
|
18
18
|
(globalThis as any).__HT_mocks = mockRegistry;
|
|
19
19
|
|
|
20
20
|
// Per-file mock scoping: __HT_file_mocks[filename][modulePath] = mock
|
|
21
|
-
const fileMocks: Record<string, Record<string, any>> =
|
|
22
|
-
|
|
21
|
+
const fileMocks: Record<string, Record<string, any>> = (globalThis as any).__HT_file_mocks ||
|
|
22
|
+
((globalThis as any).__HT_file_mocks = {});
|
|
23
23
|
|
|
24
24
|
// Track patches applied by mock() so they can be undone between files
|
|
25
25
|
let mockModulePatches: { target: any; key: string; original: any }[] = [];
|
|
26
26
|
|
|
27
|
-
export function mockModule(
|
|
28
|
-
modulePath: string,
|
|
29
|
-
factory: () => Record<string, any>
|
|
30
|
-
): void {
|
|
27
|
+
export function mockModule(modulePath: string, factory: () => Record<string, any>): void {
|
|
31
28
|
const impl = factory();
|
|
32
29
|
const value = typeof impl === 'function' ? impl : wrapWithSpies(impl);
|
|
33
30
|
|
|
@@ -55,7 +52,9 @@ export function mockModule(
|
|
|
55
52
|
mockModulePatches.push({ target: globalMock, key, original: globalMock[key] });
|
|
56
53
|
globalMock[key] = value[key];
|
|
57
54
|
}
|
|
58
|
-
} catch {
|
|
55
|
+
} catch {
|
|
56
|
+
/* frozen or non-configurable */
|
|
57
|
+
}
|
|
59
58
|
}
|
|
60
59
|
// Also patch 'default' export if mock provides it
|
|
61
60
|
if ('default' in value && 'default' in globalMock) {
|
|
@@ -68,7 +67,9 @@ export function mockModule(
|
|
|
68
67
|
mockModulePatches.push({ target: realDefault, key, original: realDefault[key] });
|
|
69
68
|
realDefault[key] = mockDefault[key];
|
|
70
69
|
}
|
|
71
|
-
} catch {
|
|
70
|
+
} catch {
|
|
71
|
+
/* frozen */
|
|
72
|
+
}
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -80,10 +81,13 @@ export function mockModule(
|
|
|
80
81
|
// with the new __currentTestFile).
|
|
81
82
|
export function resetMockModulePatches(): void {
|
|
82
83
|
for (const { target, key, original } of mockModulePatches) {
|
|
83
|
-
try {
|
|
84
|
+
try {
|
|
85
|
+
target[key] = original;
|
|
86
|
+
} catch {
|
|
87
|
+
/* best effort */
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
mockModulePatches = [];
|
|
86
|
-
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
function wrapWithSpies<T extends Record<string, any>>(impl: T): T {
|
|
@@ -101,7 +105,7 @@ function wrapWithSpies<T extends Record<string, any>>(impl: T): T {
|
|
|
101
105
|
|
|
102
106
|
export function useMock<T extends Record<string, any>>(
|
|
103
107
|
moduleExports: T,
|
|
104
|
-
implementation: Partial<T
|
|
108
|
+
implementation: Partial<T>,
|
|
105
109
|
): { [K in keyof T]: T[K] extends (...args: any[]) => any ? Spy<T[K]> : T[K] } {
|
|
106
110
|
const wrapped = wrapWithSpies(implementation as Record<string, any>);
|
|
107
111
|
|