hermes-test 0.2.4 → 1.0.1
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/README.md +81 -28
- package/bin/hermes-test.js +12 -0
- package/dist/harness.bundle.js +2000 -272
- package/globals.d.ts +19 -0
- package/index.d.ts +77 -7
- package/package.json +13 -8
- package/src/expect.ts +286 -19
- package/src/fetch.ts +22 -10
- package/src/harness.ts +187 -17
- package/src/hooks.ts +54 -34
- package/src/index.ts +3 -5
- package/src/mock.ts +4 -4
- package/src/render.ts +296 -0
- package/src/shims/rtk-query-core.js +39 -0
- package/src/spy.ts +6 -0
- package/src/store.ts +1 -0
package/src/harness.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
// Polyfills (process, setImmediate) are injected via esbuild banner in bundle.mjs
|
|
2
2
|
// to ensure they run before any bundled dependency (React checks process.env.NODE_ENV at load time)
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
// Console interceptor — replace console with print()-based output.
|
|
5
|
+
// Hermes's native print() writes to stdout and is always available.
|
|
6
|
+
// This works in all bundle modes (single, split, watch).
|
|
7
|
+
(function() {
|
|
8
|
+
const p = (globalThis as any).print || (() => {});
|
|
9
|
+
function fmt(...args: any[]) {
|
|
10
|
+
return args.map((a: any) => {
|
|
11
|
+
try { return typeof a === 'string' ? a : JSON.stringify(a, null, 2); }
|
|
12
|
+
catch { return String(a); }
|
|
13
|
+
}).join(' ');
|
|
14
|
+
}
|
|
15
|
+
(globalThis as any).console = {
|
|
16
|
+
log: (...args: any[]) => p(fmt(...args)),
|
|
17
|
+
info: (...args: any[]) => p(fmt(...args)),
|
|
18
|
+
debug: (...args: any[]) => p(fmt(...args)),
|
|
19
|
+
warn: (...args: any[]) => p('\x1b[33m⚠ ' + fmt(...args) + '\x1b[0m'),
|
|
20
|
+
error: (...args: any[]) => {
|
|
21
|
+
const msg = fmt(...args);
|
|
22
|
+
// Filter internal framework noise (not actionable by test authors)
|
|
23
|
+
if (msg.includes('Expected host context to exist')) return;
|
|
24
|
+
if (msg.includes('An unhandled error occurred processing a request for the endpoint')) return;
|
|
25
|
+
p('\x1b[31m✗ ' + msg + '\x1b[0m');
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
import { expect, _setSnapshotContext, getSnapshotCount } from './expect';
|
|
5
31
|
import { spy, spyOn, clearAllMocks } from './spy';
|
|
6
32
|
import { renderHook, act, waitFor } from './hooks';
|
|
33
|
+
import { render, fireEvent } from './render';
|
|
7
34
|
import { useMock, mockModule, resetMocks, resetMockModulePatches } from './mock';
|
|
8
35
|
import { mockFetch, mockFetchUse, mockFetchReset, mockFetchClear, http, HttpResponse } from './fetch';
|
|
9
36
|
import { useFakeTimers, useRealTimers, advanceTimersByTime, runAllTimers, getTimerCount, advanceTimersToNextTimer } from './timers';
|
|
@@ -70,6 +97,7 @@ function group(name: string, fn: () => void): void {
|
|
|
70
97
|
fn();
|
|
71
98
|
currentGroup = prev;
|
|
72
99
|
}
|
|
100
|
+
const describe = group;
|
|
73
101
|
|
|
74
102
|
function beforeEach(fn: LifecycleHook): void {
|
|
75
103
|
beforeEachHooks.push({ fn, group: currentGroup });
|
|
@@ -131,13 +159,14 @@ function flushAsync<T = any>(promise: Promise<T> | T): T {
|
|
|
131
159
|
(v) => { result = v; settled = true; },
|
|
132
160
|
(e) => { error = e; settled = true; }
|
|
133
161
|
);
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
162
|
+
// drainMicrotasks() flushes all pending microtasks. One call should settle
|
|
163
|
+
// most promises. Loop only as safety net for edge cases (macrotask scheduling).
|
|
164
|
+
drain();
|
|
165
|
+
if (!settled) {
|
|
166
|
+
for (let i = 0; i < 100 && !settled; i++) {
|
|
167
|
+
drain();
|
|
168
|
+
checkDeadline();
|
|
169
|
+
}
|
|
141
170
|
}
|
|
142
171
|
if (!settled) {
|
|
143
172
|
throw new Error('flushAsync: promise did not resolve after 100 drain cycles');
|
|
@@ -179,6 +208,97 @@ function _printFileResult(file: string, passed: number, failed: number, duration
|
|
|
179
208
|
}
|
|
180
209
|
}
|
|
181
210
|
|
|
211
|
+
/// Format a test error with a clean stack trace and actionable hints.
|
|
212
|
+
/// Works consistently across single-bundle and split-bundle modes.
|
|
213
|
+
function formatTestError(e: any): string {
|
|
214
|
+
const message = e?.message ?? String(e);
|
|
215
|
+
const stack = e?.stack as string | undefined;
|
|
216
|
+
if (!stack) return message;
|
|
217
|
+
|
|
218
|
+
// Parse stack into structured frames
|
|
219
|
+
const frames: { fn: string; file: string; line: string }[] = [];
|
|
220
|
+
for (const raw of stack.split('\n').slice(1)) {
|
|
221
|
+
// Hermes format: "at fnName (file:line:col)" or "at file:line:col"
|
|
222
|
+
const m = raw.match(/at\s+(?:([^\s(]+)\s+\()?([^:)]+):(\d+)/);
|
|
223
|
+
if (m) frames.push({ fn: m[1] || '', file: m[2], line: m[3] });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Filter to application frames only (skip react internals, harness, native)
|
|
227
|
+
const skipFn = new Set([
|
|
228
|
+
'anonymous', 'global', '__init', 'apply', 'map',
|
|
229
|
+
'react-stack-bottom-frame', 'proxy trap',
|
|
230
|
+
]);
|
|
231
|
+
const skipPrefix = [
|
|
232
|
+
'render', 'run', 'perform', 'work', 'flush', 'begin', 'update',
|
|
233
|
+
'reconcile', 'create', 'complete', 'commit', 'process',
|
|
234
|
+
];
|
|
235
|
+
const appFrames = frames.filter(f => {
|
|
236
|
+
if (skipFn.has(f.fn)) return false;
|
|
237
|
+
if (f.file.includes('harness') || f.file.includes('runner')) return false;
|
|
238
|
+
if (f.fn === '' && !f.file.includes('/src/') && !f.file.includes('packages/')) return false;
|
|
239
|
+
for (const p of skipPrefix) { if (f.fn.startsWith(p)) return false; }
|
|
240
|
+
return true;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Build clean stack: show source file paths where possible
|
|
244
|
+
let cleanStack = message;
|
|
245
|
+
if (appFrames.length > 0) {
|
|
246
|
+
cleanStack += '\n';
|
|
247
|
+
for (const f of appFrames.slice(0, 8)) {
|
|
248
|
+
// esbuild names __esm blocks with source paths like "src/utils/string.ts"
|
|
249
|
+
const loc = f.fn.includes('/') ? f.fn : (f.fn ? f.fn + ' (' + f.file + ':' + f.line + ')' : f.file + ':' + f.line);
|
|
250
|
+
cleanStack += '\n at ' + loc;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Build hint: resolve the crashing function/module to an ht.mock() suggestion
|
|
255
|
+
const importMap = (globalThis as any).__HT_shallow_imports;
|
|
256
|
+
let hint = '';
|
|
257
|
+
|
|
258
|
+
for (const f of appFrames) {
|
|
259
|
+
const fnName = f.fn;
|
|
260
|
+
|
|
261
|
+
// Source file path (module init crash): "packages/ui/src/..." or "../../node_modules/@pkg/..."
|
|
262
|
+
if (fnName.includes('/') && (fnName.includes('.ts') || fnName.includes('.js'))) {
|
|
263
|
+
const srcPath = fnName.replace(/^(\.\.\/)*/, '');
|
|
264
|
+
// Extract npm package name from node_modules path
|
|
265
|
+
const nmMatch = srcPath.match(/node_modules\/((?:@[^/]+\/)?[^/]+)/);
|
|
266
|
+
if (nmMatch) {
|
|
267
|
+
const pkg = nmMatch[1];
|
|
268
|
+
hint = '\n\n "' + pkg + '" crashed during initialization (native dependency).'
|
|
269
|
+
+ '\n Add to externals in hermes-test.config.json:\n\n'
|
|
270
|
+
+ ' { "externals": ["' + pkg + '"] }\n'
|
|
271
|
+
+ '\n Or mock the module that imports it with ht.mock().\n';
|
|
272
|
+
} else {
|
|
273
|
+
const cleanPath = srcPath.replace(/\/index\.(tsx?|jsx?)$/, '');
|
|
274
|
+
hint = '\n\n Module "' + cleanPath + '" crashed during initialization.'
|
|
275
|
+
+ '\n A dependency uses an API not available in Hermes.'
|
|
276
|
+
+ '\n Mock it with ht.mock() or add the native dep to externals.\n';
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Function name: resolve via import map
|
|
282
|
+
if (fnName && fnName.length > 2 && !fnName.includes('(') && importMap) {
|
|
283
|
+
const modPath = importMap[fnName];
|
|
284
|
+
if (modPath) {
|
|
285
|
+
// Collect all exports from this module for a complete mock
|
|
286
|
+
const siblings: string[] = [];
|
|
287
|
+
for (const k in importMap) {
|
|
288
|
+
if (importMap[k] === modPath && siblings.indexOf(k) === -1) siblings.push(k);
|
|
289
|
+
}
|
|
290
|
+
const mockBody = siblings.map(s => ' ' + s + ': () => {}').join(',\n');
|
|
291
|
+
hint = '\n\n "' + fnName + '" from "' + modPath + '" failed.'
|
|
292
|
+
+ '\n Add this mock to your test file:\n\n'
|
|
293
|
+
+ " ht.mock('" + modPath + "', () => ({\n" + mockBody + '\n }));\n';
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return cleanStack + hint;
|
|
300
|
+
}
|
|
301
|
+
|
|
182
302
|
function runTests(): TestResult[] {
|
|
183
303
|
const results: TestResult[] = [];
|
|
184
304
|
const hasOnly = tests.some((t) => t.options.only);
|
|
@@ -217,6 +337,11 @@ function runTests(): TestResult[] {
|
|
|
217
337
|
for (const entry of tests) {
|
|
218
338
|
// Flush live output when switching to a new file
|
|
219
339
|
if (entry.file !== _currentFile) {
|
|
340
|
+
// Reset RTK Query API state and drain pending microtasks from
|
|
341
|
+
// previous file to prevent cross-test contamination.
|
|
342
|
+
if (_currentFile) {
|
|
343
|
+
drain();
|
|
344
|
+
}
|
|
220
345
|
_flushFileResult();
|
|
221
346
|
_currentFile = entry.file;
|
|
222
347
|
}
|
|
@@ -237,6 +362,19 @@ function runTests(): TestResult[] {
|
|
|
237
362
|
}
|
|
238
363
|
}
|
|
239
364
|
|
|
365
|
+
// Set up snapshot context for this test
|
|
366
|
+
{
|
|
367
|
+
// Use full file path (set by entry code) for correct snapshot directory
|
|
368
|
+
const filePath = (globalThis as any).__currentTestFilePath || entry.file || 'unknown';
|
|
369
|
+
// Strip leading ./ if present
|
|
370
|
+
const clean = filePath.startsWith('./') ? filePath.substring(2) : filePath;
|
|
371
|
+
const lastSlash = clean.lastIndexOf('/');
|
|
372
|
+
const dir = lastSlash >= 0 ? clean.substring(0, lastSlash) : '.';
|
|
373
|
+
const basename = lastSlash >= 0 ? clean.substring(lastSlash + 1) : clean;
|
|
374
|
+
const snapFile = dir + '/__snapshots__/' + basename + '.snap';
|
|
375
|
+
_setSnapshotContext(snapFile, entry.name, !!(globalThis as any).__HT_updateSnapshots);
|
|
376
|
+
}
|
|
377
|
+
|
|
240
378
|
// Set up timeout for this test
|
|
241
379
|
const timeoutMs = entry.options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
242
380
|
__testTimeoutMs = timeoutMs;
|
|
@@ -267,6 +405,10 @@ function runTests(): TestResult[] {
|
|
|
267
405
|
// Reset mocks between tests
|
|
268
406
|
resetMocks();
|
|
269
407
|
|
|
408
|
+
// Drain all pending microtasks so async effects from this test
|
|
409
|
+
// don't leak into the next test (RTK Query dispatches, promises, etc.)
|
|
410
|
+
drain();
|
|
411
|
+
|
|
270
412
|
// Clear deadline
|
|
271
413
|
__testMaxDrains = 0;
|
|
272
414
|
|
|
@@ -293,12 +435,16 @@ function runTests(): TestResult[] {
|
|
|
293
435
|
// Reset mocks between tests
|
|
294
436
|
resetMocks();
|
|
295
437
|
|
|
438
|
+
// Drain pending microtasks to prevent cross-test contamination
|
|
439
|
+
drain();
|
|
440
|
+
|
|
296
441
|
_fileFailed++;
|
|
297
|
-
|
|
442
|
+
const errMsg = e?.stack ?? e?.message ?? String(e);
|
|
443
|
+
_fileFailures.push({ name: entry.name, error: errMsg });
|
|
298
444
|
results.push({
|
|
299
445
|
name: entry.name,
|
|
300
446
|
status: 'fail',
|
|
301
|
-
error:
|
|
447
|
+
error: errMsg,
|
|
302
448
|
duration: Date.now() - start,
|
|
303
449
|
file: entry.file,
|
|
304
450
|
});
|
|
@@ -325,9 +471,12 @@ function runTests(): TestResult[] {
|
|
|
325
471
|
|
|
326
472
|
// Reset between watch cycles (persistent runtime)
|
|
327
473
|
function registerCrash(file: string, error: string): void {
|
|
474
|
+
// Error string already has stack from entry.rs (e.stack || e.message).
|
|
475
|
+
// Parse it through formatTestError for consistent hints.
|
|
476
|
+
const formatted = formatTestError({ message: error.split('\n')[0], stack: error });
|
|
328
477
|
tests.push({
|
|
329
478
|
name: `[CRASH] ${file}`,
|
|
330
|
-
fn: () => { throw new Error(
|
|
479
|
+
fn: () => { throw new Error(formatted); },
|
|
331
480
|
options: {},
|
|
332
481
|
file,
|
|
333
482
|
});
|
|
@@ -346,6 +495,28 @@ function resetRegistry(): void {
|
|
|
346
495
|
resetMockModulePatches();
|
|
347
496
|
}
|
|
348
497
|
|
|
498
|
+
// --- ht global: ht.mock(path, factory) + ht.mock.fetch / ht.mock.fetch.overwrite / etc. ---
|
|
499
|
+
// Available globally without import, like jest.mock().
|
|
500
|
+
const mock = mockModule as typeof mockModule & {
|
|
501
|
+
fetch: typeof mockFetch & {
|
|
502
|
+
overwrite: typeof mockFetchUse;
|
|
503
|
+
reset: typeof mockFetchReset;
|
|
504
|
+
clear: typeof mockFetchClear;
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
mock.fetch = mockFetch as typeof mockFetch & {
|
|
508
|
+
overwrite: typeof mockFetchUse;
|
|
509
|
+
reset: typeof mockFetchReset;
|
|
510
|
+
clear: typeof mockFetchClear;
|
|
511
|
+
};
|
|
512
|
+
mock.fetch.overwrite = mockFetchUse;
|
|
513
|
+
mock.fetch.reset = mockFetchReset;
|
|
514
|
+
mock.fetch.clear = mockFetchClear;
|
|
515
|
+
|
|
516
|
+
const shallow = (_componentPath: string) => {};
|
|
517
|
+
const unmock = (_modulePath: string) => {}; // Bundler directive — no runtime effect
|
|
518
|
+
(globalThis as any).ht = { mock, shallow, unmock };
|
|
519
|
+
|
|
349
520
|
// Expose to the global scope for the harness entry
|
|
350
521
|
(globalThis as any).__HT = {
|
|
351
522
|
test,
|
|
@@ -354,6 +525,7 @@ function resetRegistry(): void {
|
|
|
354
525
|
spyOn,
|
|
355
526
|
clearAllMocks,
|
|
356
527
|
group,
|
|
528
|
+
describe,
|
|
357
529
|
beforeEach,
|
|
358
530
|
afterEach,
|
|
359
531
|
beforeAll,
|
|
@@ -363,17 +535,15 @@ function resetRegistry(): void {
|
|
|
363
535
|
act,
|
|
364
536
|
waitFor,
|
|
365
537
|
useMock,
|
|
366
|
-
mockModule,
|
|
367
|
-
mockFetch,
|
|
368
|
-
mockFetchUse,
|
|
369
|
-
mockFetchReset,
|
|
370
|
-
mockFetchClear,
|
|
371
538
|
http,
|
|
372
539
|
HttpResponse,
|
|
540
|
+
render,
|
|
541
|
+
fireEvent,
|
|
373
542
|
flushAsync,
|
|
374
543
|
registerCrash,
|
|
375
544
|
resetRegistry,
|
|
376
545
|
resetMockModulePatches,
|
|
546
|
+
getSnapshotCount,
|
|
377
547
|
// Timer control
|
|
378
548
|
useFakeTimers,
|
|
379
549
|
useRealTimers,
|
|
@@ -383,4 +553,4 @@ function resetRegistry(): void {
|
|
|
383
553
|
advanceTimersToNextTimer,
|
|
384
554
|
};
|
|
385
555
|
|
|
386
|
-
export { test, expect, spy, spyOn, clearAllMocks, group, beforeEach, afterEach, beforeAll, afterAll, renderHook, act, waitFor,
|
|
556
|
+
export { test, expect, spy, spyOn, clearAllMocks, group, describe, beforeEach, afterEach, beforeAll, afterAll, renderHook, act, waitFor, render, fireEvent, useMock, http, HttpResponse, flushAsync, useFakeTimers, useRealTimers, advanceTimersByTime, runAllTimers, getTimerCount, advanceTimersToNextTimer };
|
package/src/hooks.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Uses react-reconciler to run hooks in a minimal React tree.
|
|
3
3
|
// No dependency on react-test-renderer (deprecated in React 19).
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
// the user's
|
|
5
|
+
// react-reconciler is NOT bundled with the harness — it's loaded at runtime
|
|
6
|
+
// from the user's node_modules via globalThis.__HT_Reconciler. This ensures
|
|
7
|
+
// the reconciler always matches the user's React version.
|
|
7
8
|
|
|
8
9
|
function getReact(): typeof import('react') {
|
|
9
10
|
const R = (globalThis as any).__HT_React;
|
|
@@ -11,12 +12,19 @@ function getReact(): typeof import('react') {
|
|
|
11
12
|
return R;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
function getReconcilerModule(): any {
|
|
16
|
+
const R = (globalThis as any).__HT_Reconciler;
|
|
17
|
+
if (!R) throw new Error('react-reconciler not available. Make sure it is installed (it ships with hermes-test).');
|
|
18
|
+
return R;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getReconcilerConstants(): any {
|
|
22
|
+
return (globalThis as any).__HT_ReconcilerConstants || {};
|
|
23
|
+
}
|
|
16
24
|
|
|
17
25
|
// Based on mdjastrzebski/test-renderer — the universal-test-renderer for React 19
|
|
18
26
|
// https://github.com/mdjastrzebski/test-renderer
|
|
19
|
-
let currentUpdatePriority: number =
|
|
27
|
+
let currentUpdatePriority: number = 0;
|
|
20
28
|
|
|
21
29
|
const hostConfig = {
|
|
22
30
|
supportsMutation: true,
|
|
@@ -25,17 +33,17 @@ const hostConfig = {
|
|
|
25
33
|
supportsMicrotasks: true,
|
|
26
34
|
isPrimaryRenderer: true,
|
|
27
35
|
warnsIfNotActing: true,
|
|
28
|
-
createInstance() { return { children: [] }; },
|
|
29
|
-
createTextInstance() { return {}; },
|
|
30
|
-
appendInitialChild(p: any, c: any) { p.children.push(c); },
|
|
31
|
-
appendChild(p: any, c: any) { p.children.push(c); },
|
|
32
|
-
appendChildToContainer(p: any, c: any) { p.children.push(c); },
|
|
36
|
+
createInstance(type: string, props: any) { const { children: _c, ...rest } = props; return { type, props: rest, children: [] }; },
|
|
37
|
+
createTextInstance(text: string) { return { type: '__TEXT__', props: {}, text, children: [] }; },
|
|
38
|
+
appendInitialChild(p: any, c: any) { p.children.push(c); c._parent = p; },
|
|
39
|
+
appendChild(p: any, c: any) { p.children.push(c); c._parent = p; },
|
|
40
|
+
appendChildToContainer(p: any, c: any) { p.children.push(c); c._parent = p; },
|
|
33
41
|
removeChild(p: any, c: any) { const i = p.children.indexOf(c); if (i !== -1) p.children.splice(i, 1); },
|
|
34
42
|
removeChildFromContainer(p: any, c: any) { const i = p.children.indexOf(c); if (i !== -1) p.children.splice(i, 1); },
|
|
35
|
-
insertBefore(p: any, c: any, b: any) { const i = p.children.indexOf(b); p.children.splice(i, 0, c); },
|
|
36
|
-
insertInContainerBefore(p: any, c: any, b: any) { const i = p.children.indexOf(b); p.children.splice(i, 0, c); },
|
|
37
|
-
commitUpdate() {},
|
|
38
|
-
commitTextUpdate() {},
|
|
43
|
+
insertBefore(p: any, c: any, b: any) { const i = p.children.indexOf(b); p.children.splice(i, 0, c); c._parent = p; },
|
|
44
|
+
insertInContainerBefore(p: any, c: any, b: any) { const i = p.children.indexOf(b); p.children.splice(i, 0, c); c._parent = p; },
|
|
45
|
+
commitUpdate(inst: any, _type: any, _oldProps: any, newProps: any) { const { children: _c, ...rest } = newProps; inst.props = rest; },
|
|
46
|
+
commitTextUpdate(inst: any, _oldText: string, newText: string) { inst.text = newText; },
|
|
39
47
|
commitMount() {},
|
|
40
48
|
prepareForCommit() { return null; },
|
|
41
49
|
resetAfterCommit() {},
|
|
@@ -51,10 +59,10 @@ const hostConfig = {
|
|
|
51
59
|
cancelTimeout: (globalThis as any).clearTimeout || (() => {}),
|
|
52
60
|
noTimeout: -1,
|
|
53
61
|
scheduleMicrotask: typeof queueMicrotask === 'function' ? queueMicrotask : (fn: any) => Promise.resolve().then(fn),
|
|
54
|
-
getCurrentEventPriority() { return DefaultEventPriority; },
|
|
62
|
+
getCurrentEventPriority() { return getReconcilerConstants().DefaultEventPriority ?? 0; },
|
|
55
63
|
setCurrentUpdatePriority(priority: number) { currentUpdatePriority = priority; },
|
|
56
64
|
getCurrentUpdatePriority() { return currentUpdatePriority; },
|
|
57
|
-
resolveUpdatePriority() { return currentUpdatePriority || DefaultEventPriority; },
|
|
65
|
+
resolveUpdatePriority() { return currentUpdatePriority || (getReconcilerConstants().DefaultEventPriority ?? 0); },
|
|
58
66
|
shouldAttemptEagerTransition() { return false; },
|
|
59
67
|
trackSchedulerEvent() {},
|
|
60
68
|
resolveEventType() { return ''; },
|
|
@@ -80,8 +88,9 @@ const hostConfig = {
|
|
|
80
88
|
preparePortalMount() {},
|
|
81
89
|
};
|
|
82
90
|
|
|
83
|
-
function createReconciler() {
|
|
84
|
-
const
|
|
91
|
+
export function createReconciler() {
|
|
92
|
+
const Reconciler = getReconcilerModule();
|
|
93
|
+
const create = typeof Reconciler === 'function' ? Reconciler : Reconciler.default;
|
|
85
94
|
return create(hostConfig);
|
|
86
95
|
}
|
|
87
96
|
|
|
@@ -100,8 +109,10 @@ function flush() {
|
|
|
100
109
|
drain();
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
//
|
|
104
|
-
|
|
112
|
+
// React act() environment — same pattern as React Testing Library.
|
|
113
|
+
// true inside act() → React processes updates and can warn about missing act.
|
|
114
|
+
// false outside act() → React doesn't warn about async state updates.
|
|
115
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false;
|
|
105
116
|
|
|
106
117
|
export function act(fn: () => void | Promise<void>): void {
|
|
107
118
|
const React = getReact();
|
|
@@ -112,22 +123,31 @@ export function act(fn: () => void | Promise<void>): void {
|
|
|
112
123
|
return;
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
const prev = (globalThis as any).IS_REACT_ACT_ENVIRONMENT;
|
|
127
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
reactAct(() => {
|
|
131
|
+
const result = fn();
|
|
132
|
+
if (result && typeof (result as any).then === 'function') {
|
|
133
|
+
let settled = false;
|
|
134
|
+
let error: any;
|
|
135
|
+
(result as Promise<void>).then(
|
|
136
|
+
() => { settled = true; },
|
|
137
|
+
(e: any) => { settled = true; error = e; }
|
|
138
|
+
);
|
|
125
139
|
drain();
|
|
140
|
+
if (error) throw error;
|
|
126
141
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
});
|
|
143
|
+
// Restore env BEFORE flush so async effects resolved by flush
|
|
144
|
+
// don't trigger "not wrapped in act" warnings
|
|
145
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = prev;
|
|
146
|
+
flush();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = prev;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
export function renderHook<T>(
|
package/src/index.ts
CHANGED
|
@@ -101,6 +101,7 @@ export function setupApiStore(
|
|
|
101
101
|
|
|
102
102
|
export const test = ht.test;
|
|
103
103
|
export const group = ht.group;
|
|
104
|
+
export const describe = ht.describe;
|
|
104
105
|
export const expect = ht.expect;
|
|
105
106
|
export const spy = ht.spy;
|
|
106
107
|
export const spyOn = ht.spyOn;
|
|
@@ -112,14 +113,11 @@ export const afterAll = ht.afterAll;
|
|
|
112
113
|
export const renderHook = ht.renderHook;
|
|
113
114
|
export const act = ht.act;
|
|
114
115
|
export const waitFor = ht.waitFor;
|
|
115
|
-
export const mockModule = ht.mockModule;
|
|
116
116
|
export const useMock = ht.useMock;
|
|
117
|
-
export const mockFetch = ht.mockFetch;
|
|
118
|
-
export const mockFetchUse = ht.mockFetchUse;
|
|
119
|
-
export const mockFetchReset = ht.mockFetchReset;
|
|
120
|
-
export const mockFetchClear = ht.mockFetchClear;
|
|
121
117
|
export const http = ht.http;
|
|
122
118
|
export const HttpResponse = ht.HttpResponse;
|
|
119
|
+
export const render = ht.render;
|
|
120
|
+
export const fireEvent = ht.fireEvent;
|
|
123
121
|
export const flushAsync = ht.flushAsync;
|
|
124
122
|
export const useFakeTimers = ht.useFakeTimers;
|
|
125
123
|
export const useRealTimers = ht.useRealTimers;
|
package/src/mock.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// useMock — patches module exports for test mocking
|
|
2
2
|
// Works by replacing the getter functions on ESM namespace objects
|
|
3
3
|
//
|
|
4
|
-
//
|
|
4
|
+
// mock() — jest.mock() equivalent: registers factory in global __HT_mocks
|
|
5
5
|
// so that externalized modules resolve through our mock layer at require() time.
|
|
6
6
|
|
|
7
7
|
import { spy, type Spy } from './spy';
|
|
@@ -9,7 +9,7 @@ import { spy, type Spy } from './spy';
|
|
|
9
9
|
type SavedDescriptor = { target: any; key: string; desc: PropertyDescriptor };
|
|
10
10
|
let savedDescriptors: SavedDescriptor[] = [];
|
|
11
11
|
|
|
12
|
-
// ---
|
|
12
|
+
// --- mock(): jest.mock() equivalent ---
|
|
13
13
|
// Registers a mock factory for a module path, scoped to the current test file.
|
|
14
14
|
// The bundler wraps mocked module exports in Proxies that check the per-file
|
|
15
15
|
// mock registry at access time. This allows multiple files to mock the same
|
|
@@ -21,7 +21,7 @@ const mockRegistry: Record<string, Record<string, any>> = (globalThis as any).__
|
|
|
21
21
|
const fileMocks: Record<string, Record<string, any>> =
|
|
22
22
|
(globalThis as any).__HT_file_mocks || ((globalThis as any).__HT_file_mocks = {});
|
|
23
23
|
|
|
24
|
-
// Track patches applied by
|
|
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
27
|
export function mockModule(
|
|
@@ -75,7 +75,7 @@ export function mockModule(
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// Called between test files to undo
|
|
78
|
+
// Called between test files to undo mock() patches and mark
|
|
79
79
|
// source modules for re-initialization (so they re-read from Proxies
|
|
80
80
|
// with the new __currentTestFile).
|
|
81
81
|
export function resetMockModulePatches(): void {
|