@uniflowed/test 0.0.0-alpha.8 → 0.1.0
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/app-browser.js +6 -0
- package/app.js +175 -0
- package/browser-worker.js +334 -0
- package/browser.js +59 -0
- package/bun/index.js +305 -0
- package/in-source.js +169 -0
- package/index.js +11 -1
- package/internal/axe.js +362 -0
- package/internal/browser/cdp.js +364 -0
- package/internal/browser/node.js +375 -0
- package/internal/browser/page-transport.js +36 -0
- package/internal/browser/page.js +258 -0
- package/internal/browser/screenshots.js +102 -0
- package/internal/browser/server.js +893 -0
- package/internal/browser/transport.js +2 -0
- package/internal/expect.js +432 -80
- package/internal/isolation.js +231 -0
- package/internal/namespace.js +22 -10
- package/internal/native-globals.js +14 -0
- package/internal/native-host.js +97 -0
- package/internal/output.js +25 -7
- package/internal/registry.js +54 -3
- package/internal/run.js +130 -11
- package/internal/timers.js +2 -2
- package/package.json +41 -5
- package/worker.js +98 -10
package/internal/run.js
CHANGED
|
@@ -13,11 +13,18 @@ import * as output from "./output.js";
|
|
|
13
13
|
import * as snapshot from "./snapshot.js";
|
|
14
14
|
import { AssertionError } from "./expect.js";
|
|
15
15
|
import { type Site, firstUserSite, siteInFile, userFrames } from "./frames.js";
|
|
16
|
-
import { type Body, type Case, type Suite, collected } from "./registry.js";
|
|
16
|
+
import { type BenchOptions, type Body, type Case, type Suite, collected } from "./registry.js";
|
|
17
17
|
|
|
18
18
|
/** How one case ended. */
|
|
19
19
|
export type Outcome =
|
|
20
|
-
| {|
|
|
20
|
+
| {|
|
|
21
|
+
readonly status: "passed",
|
|
22
|
+
/**
|
|
23
|
+
* One timing per measured call, in whole microseconds, for a benchmark
|
|
24
|
+
* run under `uf test --bench`.
|
|
25
|
+
*/
|
|
26
|
+
readonly samples?: $ReadOnlyArray<number>,
|
|
27
|
+
|}
|
|
21
28
|
| {|
|
|
22
29
|
readonly status: "failed",
|
|
23
30
|
readonly message: string,
|
|
@@ -27,7 +34,11 @@ export type Outcome =
|
|
|
27
34
|
/** Where the failing assertion was written, when the stack says. */
|
|
28
35
|
readonly site: {| readonly line: number, readonly column: number |} | null,
|
|
29
36
|
|}
|
|
30
|
-
| {|
|
|
37
|
+
| {|
|
|
38
|
+
readonly status: "skipped",
|
|
39
|
+
readonly reason: "explicit" | "not-only" | "filtered" | "bench" | "not-bench",
|
|
40
|
+
readonly message?: string | null,
|
|
41
|
+
|}
|
|
31
42
|
| {| readonly status: "todo" |};
|
|
32
43
|
|
|
33
44
|
/** One finished case, as the runner reports it. */
|
|
@@ -52,11 +63,31 @@ export type RunOptions = {|
|
|
|
52
63
|
* which file that is — a test's name alone does not locate it.
|
|
53
64
|
*/
|
|
54
65
|
readonly file?: string,
|
|
66
|
+
/**
|
|
67
|
+
* Run the benchmarks and report the tests skipped, rather than the other way
|
|
68
|
+
* round. `uf test --bench` sets it.
|
|
69
|
+
*/
|
|
70
|
+
readonly bench?: boolean,
|
|
55
71
|
|};
|
|
56
72
|
|
|
57
73
|
/** Default budget for one case, matching what most runners use. */
|
|
58
74
|
export const DEFAULT_TIMEOUT_MS: number = 5000;
|
|
59
75
|
|
|
76
|
+
/** Calls a benchmark makes and throws away before it times any, unless it says. */
|
|
77
|
+
export const DEFAULT_BENCH_WARMUP: number = 5;
|
|
78
|
+
|
|
79
|
+
/** Timed calls a benchmark makes, unless it says. */
|
|
80
|
+
export const DEFAULT_BENCH_ITERATIONS: number = 50;
|
|
81
|
+
|
|
82
|
+
/** Most timed calls one benchmark may ask for, and most samples `uf` keeps. */
|
|
83
|
+
export const MAX_BENCH_ITERATIONS: number = 100000;
|
|
84
|
+
|
|
85
|
+
/** Most untimed calls one benchmark may ask for first. */
|
|
86
|
+
export const MAX_BENCH_WARMUP: number = 10000;
|
|
87
|
+
|
|
88
|
+
/** The longest delay `setTimeout` honours; past it, the timer fires at once. */
|
|
89
|
+
const MAX_TIMER_MS = 2147483647;
|
|
90
|
+
|
|
60
91
|
/** The separator between a suite's name and its child's. */
|
|
61
92
|
export const NAME_SEPARATOR: string = " > ";
|
|
62
93
|
|
|
@@ -72,7 +103,9 @@ function fullName(path: $ReadOnlyArray<string>): string {
|
|
|
72
103
|
*/
|
|
73
104
|
function hasOnly(node: Suite | Case, inherited: boolean): boolean {
|
|
74
105
|
const marked = inherited || node.modifier === "only";
|
|
75
|
-
|
|
106
|
+
// A benchmark is a case like a test: `bench.only` restricts a file as
|
|
107
|
+
// `it.only` does.
|
|
108
|
+
if (node.kind !== "suite") {
|
|
76
109
|
return marked;
|
|
77
110
|
}
|
|
78
111
|
return node.children.some((child) => hasOnly(child, marked));
|
|
@@ -188,12 +221,17 @@ async function runCase(
|
|
|
188
221
|
});
|
|
189
222
|
};
|
|
190
223
|
|
|
191
|
-
if (test.modifier === "todo"
|
|
224
|
+
if (test.modifier === "todo") {
|
|
192
225
|
report({ status: "todo" });
|
|
193
226
|
return true;
|
|
194
227
|
}
|
|
195
228
|
if (context.skipped || test.modifier === "skip") {
|
|
196
|
-
report({ status: "skipped", reason: "explicit" });
|
|
229
|
+
report({ status: "skipped", reason: "explicit", message: test.skipReason });
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const body = test.body;
|
|
233
|
+
if (body == null) {
|
|
234
|
+
report({ status: "todo" });
|
|
197
235
|
return true;
|
|
198
236
|
}
|
|
199
237
|
if (!context.onlyPath) {
|
|
@@ -205,9 +243,19 @@ async function runCase(
|
|
|
205
243
|
report({ status: "skipped", reason: "filtered" });
|
|
206
244
|
return true;
|
|
207
245
|
}
|
|
246
|
+
// A run is the tests or the benchmarks, never both. A benchmark timed inside
|
|
247
|
+
// an ordinary run would slow every suite that has one, beside workers busy
|
|
248
|
+
// with other files; a test inside a run of benchmarks would be timed with
|
|
249
|
+
// them.
|
|
250
|
+
const benchmark = test.kind === "bench";
|
|
251
|
+
if (benchmark !== (options.bench === true)) {
|
|
252
|
+
report({ status: "skipped", reason: benchmark ? "bench" : "not-bench" });
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
208
255
|
|
|
209
256
|
const timeoutMs = test.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
210
257
|
let outcome: Outcome = { status: "passed" };
|
|
258
|
+
let samples: $ReadOnlyArray<number> | null = null;
|
|
211
259
|
// Setup, body and teardown run *inside* the case's output context, and that
|
|
212
260
|
// nesting is the whole of the fix for #207. What names a printed line is no
|
|
213
261
|
// longer where the runner had got to when the line arrived — which named the
|
|
@@ -222,7 +270,11 @@ async function runCase(
|
|
|
222
270
|
for (const hook of context.beforeEach) {
|
|
223
271
|
await withTimeout(hook, timeoutMs);
|
|
224
272
|
}
|
|
225
|
-
|
|
273
|
+
if (benchmark) {
|
|
274
|
+
samples = await measure(body, test.bench, timeoutMs);
|
|
275
|
+
} else {
|
|
276
|
+
await withTimeout(body, timeoutMs);
|
|
277
|
+
}
|
|
226
278
|
} catch (thrown) {
|
|
227
279
|
outcome = failure(thrown, options.file ?? null);
|
|
228
280
|
}
|
|
@@ -243,10 +295,74 @@ async function runCase(
|
|
|
243
295
|
// is the file's, and a snapshot taken outside one fails with something better
|
|
244
296
|
// than a key belonging to whichever test happened to run last.
|
|
245
297
|
snapshot.exitTest();
|
|
246
|
-
report(outcome);
|
|
298
|
+
report(outcome.status === "passed" && samples != null ? { status: "passed", samples } : outcome);
|
|
247
299
|
return outcome.status !== "failed";
|
|
248
300
|
}
|
|
249
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Time a benchmark's body: `warmup` calls thrown away, then `iterations` timed.
|
|
304
|
+
*
|
|
305
|
+
* The body is called directly and awaited, with nothing else between the two
|
|
306
|
+
* readings of the clock: a timer set around every call would be timed with
|
|
307
|
+
* it, and on a body that takes microseconds that is most of the number. The
|
|
308
|
+
* budget is held around the whole loop instead, at one call's budget per
|
|
309
|
+
* call, so a benchmark that hangs still fails rather than holding the worker.
|
|
310
|
+
*/
|
|
311
|
+
async function measure(
|
|
312
|
+
body: Body,
|
|
313
|
+
options: BenchOptions | null,
|
|
314
|
+
timeoutMs: number,
|
|
315
|
+
): Promise<$ReadOnlyArray<number>> {
|
|
316
|
+
const warmup = count("warmup", options?.warmup, DEFAULT_BENCH_WARMUP, 0, MAX_BENCH_WARMUP);
|
|
317
|
+
const iterations = count(
|
|
318
|
+
"iterations",
|
|
319
|
+
options?.iterations,
|
|
320
|
+
DEFAULT_BENCH_ITERATIONS,
|
|
321
|
+
1,
|
|
322
|
+
MAX_BENCH_ITERATIONS,
|
|
323
|
+
);
|
|
324
|
+
const rounds = warmup + iterations;
|
|
325
|
+
const samples: Array<number> = [];
|
|
326
|
+
await withTimeout(
|
|
327
|
+
async () => {
|
|
328
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
329
|
+
const started = performance.now();
|
|
330
|
+
await body();
|
|
331
|
+
if (round >= warmup) {
|
|
332
|
+
samples.push(Math.round((performance.now() - started) * 1000));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
Math.min(timeoutMs * rounds, MAX_TIMER_MS),
|
|
337
|
+
);
|
|
338
|
+
return samples;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* A benchmark option that has to be a whole number from `least` to `most`.
|
|
343
|
+
*
|
|
344
|
+
* Refused rather than clamped: `iterations: 0` or `warmup: 1e9` is a mistake,
|
|
345
|
+
* and a benchmark quietly run some other number of times reports numbers
|
|
346
|
+
* about a run nobody asked for.
|
|
347
|
+
*/
|
|
348
|
+
function count(
|
|
349
|
+
option: string,
|
|
350
|
+
value: ?number,
|
|
351
|
+
fallback: number,
|
|
352
|
+
least: number,
|
|
353
|
+
most: number,
|
|
354
|
+
): number {
|
|
355
|
+
if (value == null) {
|
|
356
|
+
return fallback;
|
|
357
|
+
}
|
|
358
|
+
if (!Number.isInteger(value) || value < least || value > most) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`bench \`${option}\` has to be a whole number from ${least} to ${most}, and was ${String(value)}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return value;
|
|
364
|
+
}
|
|
365
|
+
|
|
250
366
|
/**
|
|
251
367
|
* Walk one suite, running what it contains.
|
|
252
368
|
*
|
|
@@ -297,13 +413,16 @@ async function runSuite(
|
|
|
297
413
|
if (state.bail) {
|
|
298
414
|
break;
|
|
299
415
|
}
|
|
300
|
-
if (child.kind
|
|
416
|
+
if (child.kind !== "suite") {
|
|
301
417
|
const willRun =
|
|
302
418
|
!inner.skipped &&
|
|
303
419
|
child.modifier !== "skip" &&
|
|
304
420
|
child.modifier !== "todo" &&
|
|
305
421
|
child.body != null &&
|
|
306
|
-
(!onlyMode || inner.onlyPath || child.modifier === "only")
|
|
422
|
+
(!onlyMode || inner.onlyPath || child.modifier === "only") &&
|
|
423
|
+
// A benchmark in a run of the tests, or a test in a run of the
|
|
424
|
+
// benchmarks, is reported skipped and sets nothing up.
|
|
425
|
+
(child.kind === "bench") === (options.bench === true);
|
|
307
426
|
if (willRun) {
|
|
308
427
|
try {
|
|
309
428
|
await setUpOnce();
|
|
@@ -315,7 +434,7 @@ async function runSuite(
|
|
|
315
434
|
line: child.line,
|
|
316
435
|
column: child.column,
|
|
317
436
|
durationMicros: 0,
|
|
318
|
-
outcome: failure(thrown),
|
|
437
|
+
outcome: failure(thrown, options.file ?? null),
|
|
319
438
|
});
|
|
320
439
|
passed = false;
|
|
321
440
|
continue;
|
package/internal/timers.js
CHANGED
|
@@ -82,7 +82,7 @@ function host(): $FlowFixMe {
|
|
|
82
82
|
* formats a date sees a plausible one — and `setSystemTime` is how a test that
|
|
83
83
|
* cares says which.
|
|
84
84
|
*/
|
|
85
|
-
export function
|
|
85
|
+
export function installFakeClock(): void {
|
|
86
86
|
if (installed != null) {
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
@@ -120,7 +120,7 @@ export function useFakeTimers(): void {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** Put the real scheduling globals back. */
|
|
123
|
-
export function
|
|
123
|
+
export function restoreRealClock(): void {
|
|
124
124
|
if (installed == null) {
|
|
125
125
|
return;
|
|
126
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/test",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "The test API and worker for `uf test`: describe/it, a full matcher set, and the process uf fans test files out to.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -11,16 +11,52 @@
|
|
|
11
11
|
"directory": "packages/test"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".":
|
|
14
|
+
".": {
|
|
15
|
+
"uniflowed-bun-test": "./bun/index.js",
|
|
16
|
+
"default": "./index.js"
|
|
17
|
+
},
|
|
18
|
+
"./in-source": "./in-source.js",
|
|
15
19
|
"./worker": "./worker.js",
|
|
16
|
-
"./
|
|
20
|
+
"./browser-worker": "./browser-worker.js",
|
|
21
|
+
"./package.json": "./package.json",
|
|
22
|
+
"./app": {
|
|
23
|
+
"browser": "./app-browser.js",
|
|
24
|
+
"default": "./app.js"
|
|
25
|
+
},
|
|
26
|
+
"./browser": "./browser.js"
|
|
17
27
|
},
|
|
18
28
|
"files": [
|
|
29
|
+
"browser-worker.js",
|
|
30
|
+
"bun",
|
|
31
|
+
"in-source.js",
|
|
19
32
|
"index.js",
|
|
33
|
+
"internal",
|
|
20
34
|
"worker.js",
|
|
21
|
-
"
|
|
35
|
+
"app.js",
|
|
36
|
+
"browser.js",
|
|
37
|
+
"app-browser.js",
|
|
38
|
+
"!*.test.js"
|
|
22
39
|
],
|
|
23
40
|
"dependencies": {
|
|
24
|
-
"@uniflowed/host": "0.
|
|
41
|
+
"@uniflowed/host": "0.1.0",
|
|
42
|
+
"pixelmatch": "^7.1.0",
|
|
43
|
+
"pngjs": "^7.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"axe-core": ">=4"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"axe-core": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"browser": {
|
|
54
|
+
"node:async_hooks": "./internal/browser/node.js",
|
|
55
|
+
"node:fs": "./internal/browser/node.js",
|
|
56
|
+
"node:module": "./internal/browser/node.js",
|
|
57
|
+
"node:path": "./internal/browser/node.js",
|
|
58
|
+
"node:url": "./internal/browser/node.js",
|
|
59
|
+
"node:util": "./internal/browser/node.js",
|
|
60
|
+
"./internal/browser/transport.js": "./internal/browser/page-transport.js"
|
|
25
61
|
}
|
|
26
62
|
}
|
package/worker.js
CHANGED
|
@@ -49,14 +49,29 @@
|
|
|
49
49
|
|
|
50
50
|
import * as output from "./internal/output.js";
|
|
51
51
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
52
|
+
import process from "node:process";
|
|
52
53
|
import { writeChangedSnapshots } from "./internal/snapshot.js";
|
|
53
54
|
import { createInterface } from "node:readline";
|
|
54
55
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
55
56
|
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
57
|
+
import { installInSourceTests } from "./in-source.js";
|
|
58
|
+
import { restoreSharedState } from "./internal/isolation.js";
|
|
58
59
|
import { run } from "./internal/run.js";
|
|
59
60
|
|
|
61
|
+
// `process` is imported rather than read off the global because this file is
|
|
62
|
+
// the process entry point, and an entry point that names what it depends on is
|
|
63
|
+
// one a reader does not have to trust.
|
|
64
|
+
//
|
|
65
|
+
// It used to be put *on* the global as well. Deno 1.x exposed the object as
|
|
66
|
+
// `node:process` and nothing as `globalThis.process`, and every module this
|
|
67
|
+
// worker reaches that wants the process reads the global: `internal/output.js`
|
|
68
|
+
// replaces `process.stdout.write`, `internal/namespace.js` reads
|
|
69
|
+
// `process.env` for `stubEnv`, and `internal/axe.js` reads `UF_AXE`. The Deno
|
|
70
|
+
// uf starts is 2.8 or newer — `@uniflowed/host/deno-preload` is built on that
|
|
71
|
+
// release's `registerHooks`, and `uf test` refuses anything older — and Deno 2
|
|
72
|
+
// has the global like Node and Bun. A shim for a host uf no longer starts would
|
|
73
|
+
// be a line claiming to be load-bearing while holding nothing up.
|
|
74
|
+
|
|
60
75
|
/** What `uf` sends for one file. */
|
|
61
76
|
type Request = {|
|
|
62
77
|
readonly file: string,
|
|
@@ -137,18 +152,80 @@ function write(event: { readonly [string]: mixed }): void {
|
|
|
137
152
|
* the `serving` store this runs inside — it is what every event written from
|
|
138
153
|
* this file, or from anything this file leaves behind, is stamped with.
|
|
139
154
|
*/
|
|
155
|
+
/**
|
|
156
|
+
* Whether this is a run of the benchmarks, `uf test --bench`.
|
|
157
|
+
*
|
|
158
|
+
* Read from the environment, as snapshot updates are: it is a property of the
|
|
159
|
+
* run, set once on the worker by `uf`, and not something each request says.
|
|
160
|
+
*/
|
|
161
|
+
function benching(): boolean {
|
|
162
|
+
const value = (globalThis: $FlowFixMe).process?.env?.UF_TEST_BENCH;
|
|
163
|
+
return value != null && value !== "" && value !== "0";
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
async function runFile(request: Request, generation: number): Promise<void> {
|
|
141
167
|
const started = performance.now();
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
168
|
+
// Everything the previous file changed and this package shares with it goes
|
|
169
|
+
// back: the registry, the stubbed environment and globals, the clock, the
|
|
170
|
+
// module stand-ins, and the document. What each of those is and why it is on
|
|
171
|
+
// the list is `internal/isolation.js`, which is one place rather than five
|
|
172
|
+
// calls here — a worker serves many files out of one process, `uf test` fans
|
|
173
|
+
// files across workers by size, and a leak therefore makes the *result* of a
|
|
174
|
+
// suite a function of the timings file rather than of the code under test.
|
|
175
|
+
// See ubugeeei-prod/uf#417, #581 and #607, which are that sentence three
|
|
176
|
+
// times over.
|
|
177
|
+
//
|
|
178
|
+
// Before the import rather than after the run, so a file that throws while
|
|
179
|
+
// loading still hands the next one a clean process.
|
|
180
|
+
restoreSharedState();
|
|
181
|
+
// Not a restore, and so not on that list: this is the output budget for the
|
|
182
|
+
// file about to run, rather than something the previous file left behind.
|
|
148
183
|
output.startFile();
|
|
149
184
|
|
|
185
|
+
// In-source blocks in *this* file get uf's test API; the same blocks in
|
|
186
|
+
// every module this file imports get `undefined` and do not register. See
|
|
187
|
+
// `./in-source.js` for why the marker is a call rather than a constant.
|
|
188
|
+
const url = pathToFileURL(request.file).href;
|
|
189
|
+
const uninstallInSourceTests = installInSourceTests(url);
|
|
190
|
+
try {
|
|
191
|
+
await runImportedFile(request, generation, url, started);
|
|
192
|
+
} finally {
|
|
193
|
+
// For the whole file rather than only for its import. A block reads the
|
|
194
|
+
// marker at module scope, but a *case body* may read it too — `const
|
|
195
|
+
// { uft } = import.meta.uf.test` inside an `it` is an ordinary thing to
|
|
196
|
+
// write — and a marker that answered during the import and not during the
|
|
197
|
+
// run would be a value that changed under the file that read it.
|
|
198
|
+
//
|
|
199
|
+
// Taking it away earlier would not buy the isolation it looks like it
|
|
200
|
+
// buys: work a finished file left behind can register through a binding it
|
|
201
|
+
// captured just as easily as through this global, so the thing that keeps
|
|
202
|
+
// a straggler out of the next file is the registry reset at the top of
|
|
203
|
+
// this function and the generation stamped on every event, not the
|
|
204
|
+
// lifetime of one accessor.
|
|
205
|
+
uninstallInSourceTests();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The half of [`runFile`] that has a file to run.
|
|
211
|
+
*
|
|
212
|
+
* Split out so the caller's `finally` covers the import *and* the run without
|
|
213
|
+
* either of the two `return`s below escaping it.
|
|
214
|
+
*/
|
|
215
|
+
async function runImportedFile(
|
|
216
|
+
request: Request,
|
|
217
|
+
generation: number,
|
|
218
|
+
url: string,
|
|
219
|
+
started: number,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
let closeNative: (() => Promise<void>) | null = null;
|
|
150
222
|
try {
|
|
151
|
-
|
|
223
|
+
if (process.env.UF_TEST_TARGET === "react-native") {
|
|
224
|
+
const { loadNativeFile } = await import("./internal/native-host.js");
|
|
225
|
+
closeNative = await loadNativeFile(request.file);
|
|
226
|
+
} else {
|
|
227
|
+
await import(`${url}?uf-run=${generation}`);
|
|
228
|
+
}
|
|
152
229
|
} catch (thrown) {
|
|
153
230
|
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
154
231
|
write({
|
|
@@ -164,7 +241,12 @@ async function runFile(request: Request, generation: number): Promise<void> {
|
|
|
164
241
|
try {
|
|
165
242
|
const absolute = fileURLToPath(pathToFileURL(request.file).href);
|
|
166
243
|
await run(
|
|
167
|
-
{
|
|
244
|
+
{
|
|
245
|
+
filter: request.filter ?? null,
|
|
246
|
+
timeoutMs: request.timeoutMs,
|
|
247
|
+
file: absolute,
|
|
248
|
+
bench: benching(),
|
|
249
|
+
},
|
|
168
250
|
(result) => {
|
|
169
251
|
write({
|
|
170
252
|
event: "test",
|
|
@@ -180,12 +262,18 @@ async function runFile(request: Request, generation: number): Promise<void> {
|
|
|
180
262
|
// with forty snapshots would otherwise rewrite its snapshot file forty
|
|
181
263
|
// times, and a crash halfway through would leave a partial one.
|
|
182
264
|
writeChangedSnapshots();
|
|
265
|
+
await closeNative?.();
|
|
183
266
|
write({
|
|
184
267
|
event: "file",
|
|
185
268
|
status: "completed",
|
|
186
269
|
durationMicros: Math.round((performance.now() - started) * 1000),
|
|
187
270
|
});
|
|
188
271
|
} catch (thrown) {
|
|
272
|
+
try {
|
|
273
|
+
await closeNative?.();
|
|
274
|
+
} catch {
|
|
275
|
+
// Preserve the original run or teardown failure.
|
|
276
|
+
}
|
|
189
277
|
const error = thrown instanceof Error ? thrown : new Error(String(thrown));
|
|
190
278
|
write({
|
|
191
279
|
event: "file",
|