@uniflowed/test 0.0.0-alpha.9 → 0.2.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 +506 -100
- package/internal/frames.js +178 -0
- 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 +102 -17
- package/internal/run.js +159 -24
- package/internal/timers.js +2 -2
- package/package.json +41 -5
- package/worker.js +115 -10
package/internal/registry.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// * `.only` anywhere in the file restricts the file to marked cases and their
|
|
20
20
|
// ancestors; everything else is reported skipped, never silently dropped.
|
|
21
21
|
|
|
22
|
-
import { firstUserSite } from "./frames.js";
|
|
22
|
+
import { callerSite, firstUserSite } from "./frames.js";
|
|
23
23
|
|
|
24
24
|
/** The placeholders `it.each` substitutes a row into. */
|
|
25
25
|
const ROW_TOKEN = /%[sjdi]/g;
|
|
@@ -27,16 +27,42 @@ const ROW_TOKEN = /%[sjdi]/g;
|
|
|
27
27
|
/** What a test or hook body may return. */
|
|
28
28
|
export type Body = () => mixed | Promise<mixed>;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* One `beforeAll`, `afterAll`, `beforeEach` or `afterEach`.
|
|
32
|
+
*
|
|
33
|
+
* `timeoutMs` is the hook's own budget when its registration named one, and
|
|
34
|
+
* `null` when the hook runs under the budget of the case it belongs to. A hook
|
|
35
|
+
* that starts a process or a server needs longer than the cases it sets up for,
|
|
36
|
+
* and without a budget of its own the only lever was the file-wide timeout.
|
|
37
|
+
*/
|
|
38
|
+
export type Hook = {| readonly body: Body, readonly timeoutMs: number | null |};
|
|
39
|
+
|
|
30
40
|
/** The suffix written on a registration call. */
|
|
31
41
|
export type Modifier = "none" | "only" | "skip" | "todo";
|
|
32
42
|
|
|
33
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* How a benchmark runs under `uf test --bench`.
|
|
45
|
+
*
|
|
46
|
+
* `warmup` calls are made and their times thrown away, then `iterations` calls
|
|
47
|
+
* are each timed. `timeout` is the budget for one call, in milliseconds, and
|
|
48
|
+
* the benchmark as a whole is held to that budget once per call.
|
|
49
|
+
*/
|
|
50
|
+
export type BenchOptions = {|
|
|
51
|
+
readonly warmup?: number,
|
|
52
|
+
readonly iterations?: number,
|
|
53
|
+
readonly timeout?: number,
|
|
54
|
+
|};
|
|
55
|
+
|
|
56
|
+
/** One registered test case, or one benchmark. */
|
|
34
57
|
export type Case = {|
|
|
35
|
-
readonly kind: "test",
|
|
58
|
+
readonly kind: "test" | "bench",
|
|
36
59
|
readonly name: string,
|
|
37
60
|
readonly body: Body | null,
|
|
38
61
|
readonly modifier: Modifier,
|
|
62
|
+
readonly skipReason: string | null,
|
|
39
63
|
readonly timeoutMs: number | null,
|
|
64
|
+
/** How to run it, for a benchmark; `null` for a test. */
|
|
65
|
+
readonly bench: BenchOptions | null,
|
|
40
66
|
readonly line: number,
|
|
41
67
|
readonly column: number,
|
|
42
68
|
|};
|
|
@@ -47,10 +73,10 @@ export type Suite = {|
|
|
|
47
73
|
readonly name: string,
|
|
48
74
|
readonly modifier: Modifier,
|
|
49
75
|
readonly children: Array<Suite | Case>,
|
|
50
|
-
readonly beforeAll: Array<
|
|
51
|
-
readonly afterAll: Array<
|
|
52
|
-
readonly beforeEach: Array<
|
|
53
|
-
readonly afterEach: Array<
|
|
76
|
+
readonly beforeAll: Array<Hook>,
|
|
77
|
+
readonly afterAll: Array<Hook>,
|
|
78
|
+
readonly beforeEach: Array<Hook>,
|
|
79
|
+
readonly afterEach: Array<Hook>,
|
|
54
80
|
readonly line: number,
|
|
55
81
|
readonly column: number,
|
|
56
82
|
|};
|
|
@@ -102,6 +128,10 @@ export function collected(): Suite {
|
|
|
102
128
|
* treats as "unknown" rather than as line one.
|
|
103
129
|
*/
|
|
104
130
|
function callSite(): {| readonly line: number, readonly column: number |} {
|
|
131
|
+
const site = callerSite(callSite);
|
|
132
|
+
if (site !== undefined) {
|
|
133
|
+
return site ?? { line: 0, column: 0 };
|
|
134
|
+
}
|
|
105
135
|
return firstUserSite(new Error("position").stack) ?? { line: 0, column: 0 };
|
|
106
136
|
}
|
|
107
137
|
|
|
@@ -123,14 +153,18 @@ function addCase(
|
|
|
123
153
|
body: Body | null,
|
|
124
154
|
modifier: Modifier,
|
|
125
155
|
timeoutMs: number | null,
|
|
156
|
+
skipReason: string | null = null,
|
|
157
|
+
bench: BenchOptions | null = null,
|
|
126
158
|
): void {
|
|
127
159
|
const position = callSite();
|
|
128
160
|
current.children.push({
|
|
129
|
-
kind: "test",
|
|
161
|
+
kind: bench == null ? "test" : "bench",
|
|
130
162
|
name,
|
|
131
163
|
body,
|
|
132
164
|
modifier,
|
|
165
|
+
skipReason,
|
|
133
166
|
timeoutMs,
|
|
167
|
+
bench,
|
|
134
168
|
line: position.line,
|
|
135
169
|
column: position.column,
|
|
136
170
|
});
|
|
@@ -179,6 +213,9 @@ function caseApi(): $FlowFixMe {
|
|
|
179
213
|
api.skip = (name: string, body?: Body) => {
|
|
180
214
|
addCase(name, body ?? null, "skip", null);
|
|
181
215
|
};
|
|
216
|
+
api.skipBecause = (name: string, reason: string, body?: Body) => {
|
|
217
|
+
addCase(name, body ?? null, "skip", null, reason);
|
|
218
|
+
};
|
|
182
219
|
api.todo = (name: string, body?: Body) => {
|
|
183
220
|
addCase(name, body ?? null, "todo", null);
|
|
184
221
|
};
|
|
@@ -211,6 +248,34 @@ export const it: $FlowFixMe = caseApi();
|
|
|
211
248
|
/** `test` is `it`, for people who write it that way. */
|
|
212
249
|
export const test: $FlowFixMe = it;
|
|
213
250
|
|
|
251
|
+
/** The `bench` API, and its modifiers. See [`suiteApi`] for the shape. */
|
|
252
|
+
function benchApi(): $FlowFixMe {
|
|
253
|
+
const api: $FlowFixMe = (name: string, body: Body, options?: BenchOptions) => {
|
|
254
|
+
addCase(name, body, "none", options?.timeout ?? null, null, options ?? {});
|
|
255
|
+
};
|
|
256
|
+
api.only = (name: string, body: Body, options?: BenchOptions) => {
|
|
257
|
+
addCase(name, body, "only", options?.timeout ?? null, null, options ?? {});
|
|
258
|
+
};
|
|
259
|
+
api.skip = (name: string, body?: Body) => {
|
|
260
|
+
addCase(name, body ?? null, "skip", null, null, {});
|
|
261
|
+
};
|
|
262
|
+
api.todo = (name: string) => {
|
|
263
|
+
addCase(name, null, "todo", null, null, {});
|
|
264
|
+
};
|
|
265
|
+
return api;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Register one benchmark.
|
|
270
|
+
*
|
|
271
|
+
* `uf test` reports a benchmark as skipped, so a suite does not pay for timing
|
|
272
|
+
* one, and `uf test --bench` runs the benchmarks in place of the tests and
|
|
273
|
+
* reports how long each call took. `bench.only`, `bench.skip` and `bench.todo`
|
|
274
|
+
* do what `it`'s do. See [`BenchOptions`] for `warmup`, `iterations` and
|
|
275
|
+
* `timeout`.
|
|
276
|
+
*/
|
|
277
|
+
export const bench: $FlowFixMe = benchApi();
|
|
278
|
+
|
|
214
279
|
/**
|
|
215
280
|
* Substitute a row into a name, the way every runner spells it: `%s` for the
|
|
216
281
|
* value, `%j` for its JSON.
|
|
@@ -225,22 +290,42 @@ function formatRow(name: string, row: mixed): string {
|
|
|
225
290
|
});
|
|
226
291
|
}
|
|
227
292
|
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
/**
|
|
294
|
+
* A hook's own budget, from the second argument of its registration.
|
|
295
|
+
*
|
|
296
|
+
* `{ timeout }` is what `it` takes, and a bare number is what Jest and Vitest
|
|
297
|
+
* take — a suite moved from either writes `beforeAll(start, 30_000)`, and a
|
|
298
|
+
* budget that was silently dropped would fail as a timeout the file never
|
|
299
|
+
* asked for.
|
|
300
|
+
*/
|
|
301
|
+
function hookTimeout(options: ?(TestOptions | number)): number | null {
|
|
302
|
+
if (typeof options === "number") {
|
|
303
|
+
return options;
|
|
304
|
+
}
|
|
305
|
+
return options?.timeout ?? null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Run once before the first test in this suite that runs.
|
|
310
|
+
*
|
|
311
|
+
* When it fails, every case it was setting up for fails with its error: none
|
|
312
|
+
* of them runs against a setup that did not happen.
|
|
313
|
+
*/
|
|
314
|
+
export function beforeAll(body: Body, options?: TestOptions | number): void {
|
|
315
|
+
current.beforeAll.push({ body, timeoutMs: hookTimeout(options) });
|
|
231
316
|
}
|
|
232
317
|
|
|
233
318
|
/** Run once after the last test in this suite that ran. */
|
|
234
|
-
export function afterAll(body: Body): void {
|
|
235
|
-
current.afterAll.push(body);
|
|
319
|
+
export function afterAll(body: Body, options?: TestOptions | number): void {
|
|
320
|
+
current.afterAll.push({ body, timeoutMs: hookTimeout(options) });
|
|
236
321
|
}
|
|
237
322
|
|
|
238
323
|
/** Run before every test in this suite and its children. */
|
|
239
|
-
export function beforeEach(body: Body): void {
|
|
240
|
-
current.beforeEach.push(body);
|
|
324
|
+
export function beforeEach(body: Body, options?: TestOptions | number): void {
|
|
325
|
+
current.beforeEach.push({ body, timeoutMs: hookTimeout(options) });
|
|
241
326
|
}
|
|
242
327
|
|
|
243
328
|
/** Run after every test in this suite and its children, including failures. */
|
|
244
|
-
export function afterEach(body: Body): void {
|
|
245
|
-
current.afterEach.push(body);
|
|
329
|
+
export function afterEach(body: Body, options?: TestOptions | number): void {
|
|
330
|
+
current.afterEach.push({ body, timeoutMs: hookTimeout(options) });
|
|
246
331
|
}
|
package/internal/run.js
CHANGED
|
@@ -13,11 +13,25 @@ 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 {
|
|
16
|
+
import {
|
|
17
|
+
type BenchOptions,
|
|
18
|
+
type Body,
|
|
19
|
+
type Case,
|
|
20
|
+
type Hook,
|
|
21
|
+
type Suite,
|
|
22
|
+
collected,
|
|
23
|
+
} from "./registry.js";
|
|
17
24
|
|
|
18
25
|
/** How one case ended. */
|
|
19
26
|
export type Outcome =
|
|
20
|
-
| {|
|
|
27
|
+
| {|
|
|
28
|
+
readonly status: "passed",
|
|
29
|
+
/**
|
|
30
|
+
* One timing per measured call, in whole microseconds, for a benchmark
|
|
31
|
+
* run under `uf test --bench`.
|
|
32
|
+
*/
|
|
33
|
+
readonly samples?: $ReadOnlyArray<number>,
|
|
34
|
+
|}
|
|
21
35
|
| {|
|
|
22
36
|
readonly status: "failed",
|
|
23
37
|
readonly message: string,
|
|
@@ -27,7 +41,11 @@ export type Outcome =
|
|
|
27
41
|
/** Where the failing assertion was written, when the stack says. */
|
|
28
42
|
readonly site: {| readonly line: number, readonly column: number |} | null,
|
|
29
43
|
|}
|
|
30
|
-
| {|
|
|
44
|
+
| {|
|
|
45
|
+
readonly status: "skipped",
|
|
46
|
+
readonly reason: "explicit" | "not-only" | "filtered" | "bench" | "not-bench",
|
|
47
|
+
readonly message?: string | null,
|
|
48
|
+
|}
|
|
31
49
|
| {| readonly status: "todo" |};
|
|
32
50
|
|
|
33
51
|
/** One finished case, as the runner reports it. */
|
|
@@ -52,11 +70,31 @@ export type RunOptions = {|
|
|
|
52
70
|
* which file that is — a test's name alone does not locate it.
|
|
53
71
|
*/
|
|
54
72
|
readonly file?: string,
|
|
73
|
+
/**
|
|
74
|
+
* Run the benchmarks and report the tests skipped, rather than the other way
|
|
75
|
+
* round. `uf test --bench` sets it.
|
|
76
|
+
*/
|
|
77
|
+
readonly bench?: boolean,
|
|
55
78
|
|};
|
|
56
79
|
|
|
57
80
|
/** Default budget for one case, matching what most runners use. */
|
|
58
81
|
export const DEFAULT_TIMEOUT_MS: number = 5000;
|
|
59
82
|
|
|
83
|
+
/** Calls a benchmark makes and throws away before it times any, unless it says. */
|
|
84
|
+
export const DEFAULT_BENCH_WARMUP: number = 5;
|
|
85
|
+
|
|
86
|
+
/** Timed calls a benchmark makes, unless it says. */
|
|
87
|
+
export const DEFAULT_BENCH_ITERATIONS: number = 50;
|
|
88
|
+
|
|
89
|
+
/** Most timed calls one benchmark may ask for, and most samples `uf` keeps. */
|
|
90
|
+
export const MAX_BENCH_ITERATIONS: number = 100000;
|
|
91
|
+
|
|
92
|
+
/** Most untimed calls one benchmark may ask for first. */
|
|
93
|
+
export const MAX_BENCH_WARMUP: number = 10000;
|
|
94
|
+
|
|
95
|
+
/** The longest delay `setTimeout` honours; past it, the timer fires at once. */
|
|
96
|
+
const MAX_TIMER_MS = 2147483647;
|
|
97
|
+
|
|
60
98
|
/** The separator between a suite's name and its child's. */
|
|
61
99
|
export const NAME_SEPARATOR: string = " > ";
|
|
62
100
|
|
|
@@ -72,7 +110,9 @@ function fullName(path: $ReadOnlyArray<string>): string {
|
|
|
72
110
|
*/
|
|
73
111
|
function hasOnly(node: Suite | Case, inherited: boolean): boolean {
|
|
74
112
|
const marked = inherited || node.modifier === "only";
|
|
75
|
-
|
|
113
|
+
// A benchmark is a case like a test: `bench.only` restricts a file as
|
|
114
|
+
// `it.only` does.
|
|
115
|
+
if (node.kind !== "suite") {
|
|
76
116
|
return marked;
|
|
77
117
|
}
|
|
78
118
|
return node.children.some((child) => hasOnly(child, marked));
|
|
@@ -153,8 +193,8 @@ function failure(thrown: mixed, file: string | null): Outcome {
|
|
|
153
193
|
/** Everything one case needs from the suites above it. */
|
|
154
194
|
type Context = {|
|
|
155
195
|
readonly path: $ReadOnlyArray<string>,
|
|
156
|
-
readonly beforeEach: $ReadOnlyArray<
|
|
157
|
-
readonly afterEach: $ReadOnlyArray<
|
|
196
|
+
readonly beforeEach: $ReadOnlyArray<Hook>,
|
|
197
|
+
readonly afterEach: $ReadOnlyArray<Hook>,
|
|
158
198
|
readonly skipped: boolean,
|
|
159
199
|
readonly onlyPath: boolean,
|
|
160
200
|
|};
|
|
@@ -188,12 +228,17 @@ async function runCase(
|
|
|
188
228
|
});
|
|
189
229
|
};
|
|
190
230
|
|
|
191
|
-
if (test.modifier === "todo"
|
|
231
|
+
if (test.modifier === "todo") {
|
|
192
232
|
report({ status: "todo" });
|
|
193
233
|
return true;
|
|
194
234
|
}
|
|
195
235
|
if (context.skipped || test.modifier === "skip") {
|
|
196
|
-
report({ status: "skipped", reason: "explicit" });
|
|
236
|
+
report({ status: "skipped", reason: "explicit", message: test.skipReason });
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
const body = test.body;
|
|
240
|
+
if (body == null) {
|
|
241
|
+
report({ status: "todo" });
|
|
197
242
|
return true;
|
|
198
243
|
}
|
|
199
244
|
if (!context.onlyPath) {
|
|
@@ -205,9 +250,19 @@ async function runCase(
|
|
|
205
250
|
report({ status: "skipped", reason: "filtered" });
|
|
206
251
|
return true;
|
|
207
252
|
}
|
|
253
|
+
// A run is the tests or the benchmarks, never both. A benchmark timed inside
|
|
254
|
+
// an ordinary run would slow every suite that has one, beside workers busy
|
|
255
|
+
// with other files; a test inside a run of benchmarks would be timed with
|
|
256
|
+
// them.
|
|
257
|
+
const benchmark = test.kind === "bench";
|
|
258
|
+
if (benchmark !== (options.bench === true)) {
|
|
259
|
+
report({ status: "skipped", reason: benchmark ? "bench" : "not-bench" });
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
208
262
|
|
|
209
263
|
const timeoutMs = test.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
210
264
|
let outcome: Outcome = { status: "passed" };
|
|
265
|
+
let samples: $ReadOnlyArray<number> | null = null;
|
|
211
266
|
// Setup, body and teardown run *inside* the case's output context, and that
|
|
212
267
|
// nesting is the whole of the fix for #207. What names a printed line is no
|
|
213
268
|
// longer where the runner had got to when the line arrived — which named the
|
|
@@ -220,9 +275,13 @@ async function runCase(
|
|
|
220
275
|
await output.runInTest(name, async () => {
|
|
221
276
|
try {
|
|
222
277
|
for (const hook of context.beforeEach) {
|
|
223
|
-
await withTimeout(hook, timeoutMs);
|
|
278
|
+
await withTimeout(hook.body, hook.timeoutMs ?? timeoutMs);
|
|
279
|
+
}
|
|
280
|
+
if (benchmark) {
|
|
281
|
+
samples = await measure(body, test.bench, timeoutMs);
|
|
282
|
+
} else {
|
|
283
|
+
await withTimeout(body, timeoutMs);
|
|
224
284
|
}
|
|
225
|
-
await withTimeout(test.body, timeoutMs);
|
|
226
285
|
} catch (thrown) {
|
|
227
286
|
outcome = failure(thrown, options.file ?? null);
|
|
228
287
|
}
|
|
@@ -230,7 +289,7 @@ async function runCase(
|
|
|
230
289
|
// when the body had not already failed.
|
|
231
290
|
for (const hook of context.afterEach) {
|
|
232
291
|
try {
|
|
233
|
-
await withTimeout(hook, timeoutMs);
|
|
292
|
+
await withTimeout(hook.body, hook.timeoutMs ?? timeoutMs);
|
|
234
293
|
} catch (thrown) {
|
|
235
294
|
if (outcome.status === "passed") {
|
|
236
295
|
outcome = failure(thrown, options.file ?? null);
|
|
@@ -243,10 +302,74 @@ async function runCase(
|
|
|
243
302
|
// is the file's, and a snapshot taken outside one fails with something better
|
|
244
303
|
// than a key belonging to whichever test happened to run last.
|
|
245
304
|
snapshot.exitTest();
|
|
246
|
-
report(outcome);
|
|
305
|
+
report(outcome.status === "passed" && samples != null ? { status: "passed", samples } : outcome);
|
|
247
306
|
return outcome.status !== "failed";
|
|
248
307
|
}
|
|
249
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Time a benchmark's body: `warmup` calls thrown away, then `iterations` timed.
|
|
311
|
+
*
|
|
312
|
+
* The body is called directly and awaited, with nothing else between the two
|
|
313
|
+
* readings of the clock: a timer set around every call would be timed with
|
|
314
|
+
* it, and on a body that takes microseconds that is most of the number. The
|
|
315
|
+
* budget is held around the whole loop instead, at one call's budget per
|
|
316
|
+
* call, so a benchmark that hangs still fails rather than holding the worker.
|
|
317
|
+
*/
|
|
318
|
+
async function measure(
|
|
319
|
+
body: Body,
|
|
320
|
+
options: BenchOptions | null,
|
|
321
|
+
timeoutMs: number,
|
|
322
|
+
): Promise<$ReadOnlyArray<number>> {
|
|
323
|
+
const warmup = count("warmup", options?.warmup, DEFAULT_BENCH_WARMUP, 0, MAX_BENCH_WARMUP);
|
|
324
|
+
const iterations = count(
|
|
325
|
+
"iterations",
|
|
326
|
+
options?.iterations,
|
|
327
|
+
DEFAULT_BENCH_ITERATIONS,
|
|
328
|
+
1,
|
|
329
|
+
MAX_BENCH_ITERATIONS,
|
|
330
|
+
);
|
|
331
|
+
const rounds = warmup + iterations;
|
|
332
|
+
const samples: Array<number> = [];
|
|
333
|
+
await withTimeout(
|
|
334
|
+
async () => {
|
|
335
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
336
|
+
const started = performance.now();
|
|
337
|
+
await body();
|
|
338
|
+
if (round >= warmup) {
|
|
339
|
+
samples.push(Math.round((performance.now() - started) * 1000));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
Math.min(timeoutMs * rounds, MAX_TIMER_MS),
|
|
344
|
+
);
|
|
345
|
+
return samples;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* A benchmark option that has to be a whole number from `least` to `most`.
|
|
350
|
+
*
|
|
351
|
+
* Refused rather than clamped: `iterations: 0` or `warmup: 1e9` is a mistake,
|
|
352
|
+
* and a benchmark quietly run some other number of times reports numbers
|
|
353
|
+
* about a run nobody asked for.
|
|
354
|
+
*/
|
|
355
|
+
function count(
|
|
356
|
+
option: string,
|
|
357
|
+
value: ?number,
|
|
358
|
+
fallback: number,
|
|
359
|
+
least: number,
|
|
360
|
+
most: number,
|
|
361
|
+
): number {
|
|
362
|
+
if (value == null) {
|
|
363
|
+
return fallback;
|
|
364
|
+
}
|
|
365
|
+
if (!Number.isInteger(value) || value < least || value > most) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`bench \`${option}\` has to be a whole number from ${least} to ${most}, and was ${String(value)}`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return value;
|
|
371
|
+
}
|
|
372
|
+
|
|
250
373
|
/**
|
|
251
374
|
* Walk one suite, running what it contains.
|
|
252
375
|
*
|
|
@@ -280,16 +403,25 @@ async function runSuite(
|
|
|
280
403
|
// every `beforeAll` in an ordinary file — one where the tests live inside a
|
|
281
404
|
// `describe` — never running at all, silently. The chain is walked outermost
|
|
282
405
|
// first, so an inner suite's setup sees what the outer one did.
|
|
406
|
+
//
|
|
407
|
+
// Once, and remembered either way. The outcome is kept rather than a "done"
|
|
408
|
+
// flag, because a flag raised before the hooks ran let every case after the
|
|
409
|
+
// first go ahead when setup had failed: the first case reported the hook's
|
|
410
|
+
// error and the rest ran against a fixture that was never made, failing — or
|
|
411
|
+
// worse, passing — for reasons that had nothing to do with them.
|
|
283
412
|
let setUp = false;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
413
|
+
let setup: Promise<void> | null = null;
|
|
414
|
+
const setUpOnce = (): Promise<void> => {
|
|
415
|
+
if (setup == null) {
|
|
416
|
+
setUp = true;
|
|
417
|
+
setup = (async () => {
|
|
418
|
+
await setUpAncestors();
|
|
419
|
+
for (const hook of node.beforeAll) {
|
|
420
|
+
await withTimeout(hook.body, hook.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
292
423
|
}
|
|
424
|
+
return setup;
|
|
293
425
|
};
|
|
294
426
|
|
|
295
427
|
let passed = true;
|
|
@@ -297,13 +429,16 @@ async function runSuite(
|
|
|
297
429
|
if (state.bail) {
|
|
298
430
|
break;
|
|
299
431
|
}
|
|
300
|
-
if (child.kind
|
|
432
|
+
if (child.kind !== "suite") {
|
|
301
433
|
const willRun =
|
|
302
434
|
!inner.skipped &&
|
|
303
435
|
child.modifier !== "skip" &&
|
|
304
436
|
child.modifier !== "todo" &&
|
|
305
437
|
child.body != null &&
|
|
306
|
-
(!onlyMode || inner.onlyPath || child.modifier === "only")
|
|
438
|
+
(!onlyMode || inner.onlyPath || child.modifier === "only") &&
|
|
439
|
+
// A benchmark in a run of the tests, or a test in a run of the
|
|
440
|
+
// benchmarks, is reported skipped and sets nothing up.
|
|
441
|
+
(child.kind === "bench") === (options.bench === true);
|
|
307
442
|
if (willRun) {
|
|
308
443
|
try {
|
|
309
444
|
await setUpOnce();
|
|
@@ -315,7 +450,7 @@ async function runSuite(
|
|
|
315
450
|
line: child.line,
|
|
316
451
|
column: child.column,
|
|
317
452
|
durationMicros: 0,
|
|
318
|
-
outcome: failure(thrown),
|
|
453
|
+
outcome: failure(thrown, options.file ?? null),
|
|
319
454
|
});
|
|
320
455
|
passed = false;
|
|
321
456
|
continue;
|
|
@@ -336,7 +471,7 @@ async function runSuite(
|
|
|
336
471
|
if (setUp) {
|
|
337
472
|
for (const hook of node.afterAll) {
|
|
338
473
|
try {
|
|
339
|
-
await withTimeout(hook, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
474
|
+
await withTimeout(hook.body, hook.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
340
475
|
} catch {
|
|
341
476
|
// A teardown failure cannot fail a test that already reported, and
|
|
342
477
|
// there is nothing left to attach it to; the file's own status carries
|
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.2.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.2.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
|
}
|