@uniflowed/test 0.0.0-alpha.34 → 0.0.0-alpha.37
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/bun/index.js +305 -0
- package/index.js +9 -1
- package/internal/axe.js +84 -1
- package/internal/isolation.js +41 -6
- package/internal/registry.js +48 -3
- package/internal/run.js +118 -8
- package/package.json +7 -3
- package/worker.js +29 -20
package/bun/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: `bun:test` has no Flow library definition, and the only
|
|
4
|
+
// thing that ever imports this is a `bun test` that `uf test` started.
|
|
5
|
+
//
|
|
6
|
+
// `@uniflowed/test`, when the suite is run by `bun test`.
|
|
7
|
+
//
|
|
8
|
+
// A project that writes `test: { runner: "bun" }` in `uf.config.js` has its
|
|
9
|
+
// suite run by `bun test` instead of uf's own runner, and its test files do not
|
|
10
|
+
// change: they still import `@uniflowed/test`. `uf test` starts Bun with the
|
|
11
|
+
// `uniflowed-bun-test` export condition, and this package's `exports` sends
|
|
12
|
+
// `@uniflowed/test` here under that condition and to `../index.js` under every
|
|
13
|
+
// other. uf's own runner never sets it, on Node or on Bun, so the only process
|
|
14
|
+
// that meets this file is one `uf test` started to hand a suite to Bun.
|
|
15
|
+
//
|
|
16
|
+
// # Why a condition rather than a plugin
|
|
17
|
+
//
|
|
18
|
+
// A `Bun.plugin` `onResolve` is not consulted for a bare specifier that
|
|
19
|
+
// resolves to an installed package, and `@uniflowed/test` is installed in every
|
|
20
|
+
// project that uses it. Tried on Bun 1.3.13, the test file imported uf's real
|
|
21
|
+
// API, registered its cases where Bun could not see them, and `bun test`
|
|
22
|
+
// printed "Ran 0 tests across 1 file" and exited 0 — a green run over a suite
|
|
23
|
+
// that ran nothing. Resolution is the one place the choice is made before a
|
|
24
|
+
// test file's imports run. See ubugeeei-prod/uf#942.
|
|
25
|
+
//
|
|
26
|
+
// # What maps, and what refuses
|
|
27
|
+
//
|
|
28
|
+
// The registration API, the hooks and `expect` are Bun's own, with the
|
|
29
|
+
// modifiers both runners give them: `.only`, `.skip`, `.todo` and `.each`
|
|
30
|
+
// (whose `%s %j %d %i` placeholders both runners format). `fn`, `spyOn`, and
|
|
31
|
+
// the `uft` members Bun has an equivalent for — the fake clock and the mock
|
|
32
|
+
// resets — are Bun's. uf's host-agnostic helpers are uf's: `uft.waitFor` and
|
|
33
|
+
// `uft.waitUntil` poll with `setTimeout` under either runner, `uft.mocked` is
|
|
34
|
+
// the identity function, and `equals` and `render` are pure.
|
|
35
|
+
//
|
|
36
|
+
// Everything else raises `UnsupportedError` at the moment it is used, naming
|
|
37
|
+
// what was called and why there is nothing here to give it — never an
|
|
38
|
+
// approximation that would report a different result:
|
|
39
|
+
//
|
|
40
|
+
// * `it.skipBecause`: Bun's skip carries no reason into its report, and a skip
|
|
41
|
+
// whose reason quietly disappears is what `skipBecause` exists to prevent.
|
|
42
|
+
// * uf's DOM matchers and `toHaveNoAxeViolations`: Bun's `expect` has none.
|
|
43
|
+
// * `uft.stubEnv`, `uft.stubGlobal` and their `unstubAll` pairs: uf puts a stub
|
|
44
|
+
// back before the next file, and `bun test` runs every file in one process
|
|
45
|
+
// where nothing would.
|
|
46
|
+
// * Module mocking: Bun's `mock.module` rewrites modules that have already
|
|
47
|
+
// imported the real one, which is a different promise from `uft.mock`'s.
|
|
48
|
+
// * `uft.advanceTimersByTimeAsync`: Bun's fake clock has no asynchronous
|
|
49
|
+
// advance, and flushing microtasks between timers is the whole of what it
|
|
50
|
+
// promises.
|
|
51
|
+
// * `AssertionError` and `RunawayTimersError`: Bun throws errors of its own, so
|
|
52
|
+
// a test comparing a failure against uf's class would be comparing it against
|
|
53
|
+
// a class nothing throws.
|
|
54
|
+
|
|
55
|
+
import * as bun from "bun:test";
|
|
56
|
+
|
|
57
|
+
import { equals, render } from "../internal/equality.js";
|
|
58
|
+
import { mocked, waitFor, waitUntil } from "../internal/namespace.js";
|
|
59
|
+
import { DEFAULT_TIMEOUT_MS, NAME_SEPARATOR } from "../internal/run.js";
|
|
60
|
+
import { UnsupportedError } from "../internal/unsupported.js";
|
|
61
|
+
|
|
62
|
+
export { UnsupportedError, equals, render, DEFAULT_TIMEOUT_MS, NAME_SEPARATOR };
|
|
63
|
+
export { afterAll, afterEach, beforeAll, beforeEach } from "bun:test";
|
|
64
|
+
|
|
65
|
+
/** What every refusal here says about where the suite is running. */
|
|
66
|
+
const UNDER_BUN = "the suite is being run by `bun test` (`test.runner` in uf.config.js)";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The error for something that is not a `uft` member.
|
|
70
|
+
*
|
|
71
|
+
* Still an `UnsupportedError`, because that is the class a test catches;
|
|
72
|
+
* only the message changes, since `UnsupportedError`'s is written for `uft`
|
|
73
|
+
* members and `it.skipBecause` or a matcher is not one.
|
|
74
|
+
*/
|
|
75
|
+
class RunnerUnsupportedError extends UnsupportedError {
|
|
76
|
+
constructor(binding, reason) {
|
|
77
|
+
super(binding, reason);
|
|
78
|
+
this.message = `${binding} is not available: ${reason}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A function that refuses, for a binding with no equivalent under Bun. */
|
|
83
|
+
function refusing(binding, reason, Refusal = RunnerUnsupportedError) {
|
|
84
|
+
return () => {
|
|
85
|
+
throw new Refusal(binding, `${UNDER_BUN}, and ${reason}`);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A class that refuses to be constructed or compared against. */
|
|
90
|
+
function refusingClass(name, reason) {
|
|
91
|
+
const refuse = refusing(name, reason);
|
|
92
|
+
return class {
|
|
93
|
+
constructor() {
|
|
94
|
+
refuse();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static [Symbol.hasInstance]() {
|
|
98
|
+
return refuse();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The placeholders uf's `.each` substitutes a row into. */
|
|
104
|
+
const ROW_TOKEN = /%[sjdi]/g;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A row's case name, written the way uf's own runner writes it.
|
|
108
|
+
*
|
|
109
|
+
* Bun's `.each` differs from uf's twice over: it spreads an array row into the
|
|
110
|
+
* body's arguments where uf hands the body the row whole, and its JUnit report
|
|
111
|
+
* records the name with the placeholder still in it (`formats row %s`, once
|
|
112
|
+
* per row). A suite whose cases are named differently by the two runners is a
|
|
113
|
+
* suite whose results cannot be compared between them, so `.each` here is
|
|
114
|
+
* uf's, registering one case per row through Bun's plain registration.
|
|
115
|
+
* Mirrors `formatRow` in `../internal/registry.js`.
|
|
116
|
+
*/
|
|
117
|
+
function formatRow(name, row) {
|
|
118
|
+
const values = Array.isArray(row) ? row : [row];
|
|
119
|
+
let index = 0;
|
|
120
|
+
return name.replace(ROW_TOKEN, (token) => {
|
|
121
|
+
const value = values[index];
|
|
122
|
+
index += 1;
|
|
123
|
+
return token === "%j" ? (JSON.stringify(value) ?? "undefined") : String(value);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* One of uf's registration functions, forwarding to Bun's.
|
|
129
|
+
*
|
|
130
|
+
* Forwarded call by call rather than re-exported, so that a modifier uf has
|
|
131
|
+
* and Bun does not — `it.skipBecause` — can be attached without reaching into
|
|
132
|
+
* Bun's own function object.
|
|
133
|
+
*/
|
|
134
|
+
function registration(bunApi) {
|
|
135
|
+
const api = (...args) => bunApi(...args);
|
|
136
|
+
api.only = (...args) => bunApi.only(...args);
|
|
137
|
+
api.skip = (...args) => bunApi.skip(...args);
|
|
138
|
+
api.todo = (...args) => bunApi.todo(...args);
|
|
139
|
+
api.each = (table) => (name, body, options) => {
|
|
140
|
+
for (const row of table) {
|
|
141
|
+
bunApi(formatRow(name, row), () => body(row), options);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
return api;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const describe = registration(bun.describe);
|
|
148
|
+
export const it = registration(bun.it);
|
|
149
|
+
it.skipBecause = refusing(
|
|
150
|
+
"it.skipBecause",
|
|
151
|
+
'Bun\'s skip carries no reason into its report; write `it.skip`, or run this file with `runner: "uf"`',
|
|
152
|
+
);
|
|
153
|
+
export const test = it;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `bench`, which `bun:test` has no counterpart for.
|
|
157
|
+
*
|
|
158
|
+
* Its modifiers refuse too, by name, rather than failing on `undefined`.
|
|
159
|
+
*/
|
|
160
|
+
const refuseBench = refusing(
|
|
161
|
+
"bench",
|
|
162
|
+
'`bun:test` has no benchmarks; run them with `runner: "uf"` and `uf test --bench`',
|
|
163
|
+
);
|
|
164
|
+
export const bench = (..._args) => refuseBench();
|
|
165
|
+
bench.only = refuseBench;
|
|
166
|
+
bench.skip = refuseBench;
|
|
167
|
+
bench.todo = refuseBench;
|
|
168
|
+
|
|
169
|
+
/** uf's matchers that Bun's `expect` has no counterpart for. */
|
|
170
|
+
const MATCHERS_BUN_LACKS = [
|
|
171
|
+
"toBeChecked",
|
|
172
|
+
"toBeDisabled",
|
|
173
|
+
"toBeEnabled",
|
|
174
|
+
"toBeInTheDocument",
|
|
175
|
+
"toBeRequired",
|
|
176
|
+
"toBeVisible",
|
|
177
|
+
"toHaveAttribute",
|
|
178
|
+
"toHaveClass",
|
|
179
|
+
"toHaveFocus",
|
|
180
|
+
"toHaveTextContent",
|
|
181
|
+
"toHaveValue",
|
|
182
|
+
"toHaveNoAxeViolations",
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
bun.expect.extend(
|
|
186
|
+
Object.fromEntries(
|
|
187
|
+
MATCHERS_BUN_LACKS.map((matcher) => [
|
|
188
|
+
matcher,
|
|
189
|
+
refusing(
|
|
190
|
+
`expect(…).${matcher}`,
|
|
191
|
+
"Bun's `expect` has no such matcher; assert on the element's properties directly, or run this file with `runner: \"uf\"`",
|
|
192
|
+
),
|
|
193
|
+
]),
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* uf's snapshot matchers.
|
|
199
|
+
*
|
|
200
|
+
* Bun has matchers by these names, and they are still not the same matchers:
|
|
201
|
+
* both runners write `__snapshots__/<file>.snap` in Jest's format, but each
|
|
202
|
+
* keys an entry by its own spelling of the test's name and serialises the value
|
|
203
|
+
* its own way. Passed through, a suite recorded by uf would either fail against
|
|
204
|
+
* its own snapshots or quietly write a second set beside them — and a snapshot
|
|
205
|
+
* that was rewritten rather than compared is the failure snapshots exist to
|
|
206
|
+
* catch.
|
|
207
|
+
*/
|
|
208
|
+
const SNAPSHOT_MATCHERS = ["toMatchSnapshot", "toMatchInlineSnapshot"];
|
|
209
|
+
|
|
210
|
+
bun.expect.extend(
|
|
211
|
+
Object.fromEntries(
|
|
212
|
+
SNAPSHOT_MATCHERS.map((matcher) => [
|
|
213
|
+
matcher,
|
|
214
|
+
refusing(
|
|
215
|
+
`expect(…).${matcher}`,
|
|
216
|
+
'uf and Bun key and serialise snapshots differently, so this would compare against — or rewrite — a snapshot uf did not write; run snapshot tests with `runner: "uf"`',
|
|
217
|
+
),
|
|
218
|
+
]),
|
|
219
|
+
),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
export const expect = bun.expect;
|
|
223
|
+
|
|
224
|
+
export const fn = (...args) => bun.jest.fn(...args);
|
|
225
|
+
export const spyOn = (...args) => bun.spyOn(...args);
|
|
226
|
+
|
|
227
|
+
export const AssertionError = refusingClass(
|
|
228
|
+
"AssertionError",
|
|
229
|
+
"Bun throws its own assertion errors, so nothing a failure throws is an instance of uf's",
|
|
230
|
+
);
|
|
231
|
+
export const RunawayTimersError = refusingClass(
|
|
232
|
+
"RunawayTimersError",
|
|
233
|
+
"Bun's fake clock throws its own error for a timer loop that never ends",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
/** A `uft` member with no equivalent under Bun. */
|
|
237
|
+
const refusingMember = (member, reason) => refusing(member, reason, UnsupportedError);
|
|
238
|
+
|
|
239
|
+
const MODULE_MOCKING =
|
|
240
|
+
"Bun's `mock.module` rewrites modules that have already imported the real one, which is a different promise from `uft.mock`'s (see guide/testing, \"Which hosts\")";
|
|
241
|
+
|
|
242
|
+
const STUBS =
|
|
243
|
+
"uf puts a stub back before the next file, and `bun test` runs every file in one process where nothing would";
|
|
244
|
+
|
|
245
|
+
export const uft = Object.freeze({
|
|
246
|
+
fn,
|
|
247
|
+
spyOn,
|
|
248
|
+
mocked,
|
|
249
|
+
|
|
250
|
+
clearAllMocks: () => bun.jest.clearAllMocks(),
|
|
251
|
+
resetAllMocks: () => bun.jest.resetAllMocks(),
|
|
252
|
+
restoreAllMocks: () => bun.jest.restoreAllMocks(),
|
|
253
|
+
|
|
254
|
+
stubEnv: refusingMember("stubEnv", STUBS),
|
|
255
|
+
unstubAllEnvs: refusingMember("unstubAllEnvs", STUBS),
|
|
256
|
+
stubGlobal: refusingMember("stubGlobal", STUBS),
|
|
257
|
+
unstubAllGlobals: refusingMember("unstubAllGlobals", STUBS),
|
|
258
|
+
|
|
259
|
+
waitFor,
|
|
260
|
+
waitUntil,
|
|
261
|
+
|
|
262
|
+
useFakeTimers: () => {
|
|
263
|
+
bun.jest.useFakeTimers();
|
|
264
|
+
},
|
|
265
|
+
useRealTimers: () => {
|
|
266
|
+
bun.jest.useRealTimers();
|
|
267
|
+
},
|
|
268
|
+
isFakeTimers: () => bun.jest.isFakeTimers(),
|
|
269
|
+
advanceTimersByTime: (milliseconds) => {
|
|
270
|
+
bun.jest.advanceTimersByTime(milliseconds);
|
|
271
|
+
},
|
|
272
|
+
advanceTimersByTimeAsync: refusingMember(
|
|
273
|
+
"advanceTimersByTimeAsync",
|
|
274
|
+
"Bun's fake clock has no asynchronous advance; await between `uft.advanceTimersByTime` calls instead",
|
|
275
|
+
),
|
|
276
|
+
advanceTimersToNextTimer: (steps) => {
|
|
277
|
+
bun.jest.advanceTimersToNextTimer(steps);
|
|
278
|
+
},
|
|
279
|
+
runAllTimers: () => {
|
|
280
|
+
bun.jest.runAllTimers();
|
|
281
|
+
},
|
|
282
|
+
runOnlyPendingTimers: () => {
|
|
283
|
+
bun.jest.runOnlyPendingTimers();
|
|
284
|
+
},
|
|
285
|
+
getTimerCount: () => bun.jest.getTimerCount(),
|
|
286
|
+
// uf's clock only moves while it is faked, and a time set before
|
|
287
|
+
// `useFakeTimers` is forgotten when the clock is installed. Bun's
|
|
288
|
+
// `setSystemTime` moves the real `Date` whether or not anything is faked, so
|
|
289
|
+
// it is only forwarded while Bun's clock is faked — outside that it does
|
|
290
|
+
// what uf's does, which is nothing a test can observe.
|
|
291
|
+
setSystemTime: (time) => {
|
|
292
|
+
if (bun.jest.isFakeTimers()) {
|
|
293
|
+
bun.setSystemTime(time);
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
getMockedSystemTime: () => (bun.jest.isFakeTimers() ? new Date() : null),
|
|
297
|
+
|
|
298
|
+
mock: refusingMember("mock", MODULE_MOCKING),
|
|
299
|
+
doMock: refusingMember("doMock", MODULE_MOCKING),
|
|
300
|
+
unmock: refusingMember("unmock", MODULE_MOCKING),
|
|
301
|
+
doUnmock: refusingMember("doUnmock", MODULE_MOCKING),
|
|
302
|
+
importActual: refusingMember("importActual", MODULE_MOCKING),
|
|
303
|
+
importMock: refusingMember("importMock", MODULE_MOCKING),
|
|
304
|
+
resetModules: refusingMember("resetModules", MODULE_MOCKING),
|
|
305
|
+
});
|
package/index.js
CHANGED
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
export type { Expect, Expectation, Matchers } from "./internal/expect.js";
|
|
16
16
|
export type { InSourceTests } from "./in-source.js";
|
|
17
|
-
export type {
|
|
17
|
+
export type {
|
|
18
|
+
BenchOptions,
|
|
19
|
+
Body as TestBody,
|
|
20
|
+
Case,
|
|
21
|
+
Modifier,
|
|
22
|
+
Suite,
|
|
23
|
+
TestOptions,
|
|
24
|
+
} from "./internal/registry.js";
|
|
18
25
|
export type { ModuleFactory, ModuleNamespace } from "./internal/modules.js";
|
|
19
26
|
export type { Uft } from "./internal/namespace.js";
|
|
20
27
|
export type { Outcome, Result, RunOptions } from "./internal/run.js";
|
|
@@ -27,6 +34,7 @@ export {
|
|
|
27
34
|
afterEach,
|
|
28
35
|
beforeAll,
|
|
29
36
|
beforeEach,
|
|
37
|
+
bench,
|
|
30
38
|
describe,
|
|
31
39
|
it,
|
|
32
40
|
test,
|
package/internal/axe.js
CHANGED
|
@@ -207,6 +207,87 @@ function excerpt(html: string): string {
|
|
|
207
207
|
return line.length > MAX_NODE_CHARS ? `${line.slice(0, MAX_NODE_CHARS)}…` : line;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The audit this process started last, settled or not.
|
|
212
|
+
*
|
|
213
|
+
* axe-core runs one audit at a time per process and refuses a second outright
|
|
214
|
+
* ("Axe is already running") rather than queueing it. One file never starts two
|
|
215
|
+
* on purpose, but a worker serves many files: a case that timed out in the
|
|
216
|
+
* middle of an audit leaves that audit running, and the next file's first audit
|
|
217
|
+
* used to fail on it — reported under a file that had done nothing wrong.
|
|
218
|
+
*/
|
|
219
|
+
let running: Promise<mixed> = Promise.resolve();
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Run `audit` once the audit already running has settled, however it settles.
|
|
223
|
+
*
|
|
224
|
+
* Waiting rather than failing is safe because the audit being waited on always
|
|
225
|
+
* ends — axe-core walks a finite tree — and a case that waits too long is still
|
|
226
|
+
* bounded by its own timeout.
|
|
227
|
+
*/
|
|
228
|
+
function afterTheAuditRunning<T>(audit: () => Promise<T>): Promise<T> {
|
|
229
|
+
const turn = running.then(audit, audit);
|
|
230
|
+
running = turn.then(
|
|
231
|
+
() => undefined,
|
|
232
|
+
() => undefined,
|
|
233
|
+
);
|
|
234
|
+
return turn;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** How long to wait between looks at an audit this module did not start. */
|
|
238
|
+
const FOREIGN_AUDIT_POLL_MS = 10;
|
|
239
|
+
|
|
240
|
+
/** How long an audit this module did not start may hold axe-core. */
|
|
241
|
+
const FOREIGN_AUDIT_LIMIT_MS = 10000;
|
|
242
|
+
|
|
243
|
+
// The timer and the clock as they were when this module loaded, so a file that
|
|
244
|
+
// fakes timers cannot stall a wait for a real audit to end.
|
|
245
|
+
const pause: typeof setTimeout = setTimeout;
|
|
246
|
+
const now: () => number = Date.now.bind(Date);
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Whether axe-core refused to start because an audit is already running.
|
|
250
|
+
*
|
|
251
|
+
* `running` queues every audit this module starts, so a refusal means an audit
|
|
252
|
+
* started somewhere else. `uf dev`'s overlay runs axe-core itself, and stopping
|
|
253
|
+
* the overlay cannot recall an audit already walking the page: its own test can
|
|
254
|
+
* leave one finishing into the next file in the worker.
|
|
255
|
+
*/
|
|
256
|
+
function isAlreadyRunning(error: mixed): boolean {
|
|
257
|
+
return error instanceof Error && error.message.startsWith("Axe is already running");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Run the audit once axe-core is free of an audit this module did not start.
|
|
262
|
+
*
|
|
263
|
+
* Looks again every `FOREIGN_AUDIT_POLL_MS` until axe-core takes it, and after
|
|
264
|
+
* `FOREIGN_AUDIT_LIMIT_MS` says what held the engine rather than waiting on an
|
|
265
|
+
* audit that is not going to end.
|
|
266
|
+
*/
|
|
267
|
+
function runOnceAxeIsFree(
|
|
268
|
+
axe: $FlowFixMe,
|
|
269
|
+
node: mixed,
|
|
270
|
+
options: mixed,
|
|
271
|
+
since: number = now(),
|
|
272
|
+
): Promise<mixed> {
|
|
273
|
+
return Promise.resolve()
|
|
274
|
+
.then(() => axe.run(node, options))
|
|
275
|
+
.catch((error: mixed) => {
|
|
276
|
+
if (!isAlreadyRunning(error)) {
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
if (now() - since >= FOREIGN_AUDIT_LIMIT_MS) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
"axe-core has been running an audit uf did not start for ten seconds, such as " +
|
|
282
|
+
"`uf dev`'s overlay or a direct `axe.run`; this assertion could not start its own.",
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return new Promise((resolve) => {
|
|
286
|
+
pause(resolve, FOREIGN_AUDIT_POLL_MS);
|
|
287
|
+
}).then(() => runOnceAxeIsFree(axe, node, options, since));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
210
291
|
/**
|
|
211
292
|
* Audit `node` and report what it found, weakest results already dropped.
|
|
212
293
|
*
|
|
@@ -229,7 +310,9 @@ export async function auditElement(
|
|
|
229
310
|
}
|
|
230
311
|
const axe = await axeEngine();
|
|
231
312
|
const options = resolveOptions(overrides);
|
|
232
|
-
const results = await
|
|
313
|
+
const results = await afterTheAuditRunning(() =>
|
|
314
|
+
runOnceAxeIsFree(axe, node, axeRunOptions(options)),
|
|
315
|
+
);
|
|
233
316
|
const found: Array<AxeViolation> = [];
|
|
234
317
|
for (const raw of arrayAt(results, "violations")) {
|
|
235
318
|
const violation: AxeViolation = {
|
package/internal/isolation.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// the process, outlives the file, and is handed to whichever file the schedule
|
|
9
9
|
// puts next in that worker.
|
|
10
10
|
//
|
|
11
|
-
// That second list is what this module is. It has been discovered
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// That second list is what this module is. It has been discovered four times,
|
|
12
|
+
// and each time the same way — a suite that passed alone and failed beside
|
|
13
|
+
// another, naming the file that read the value rather than the file that wrote
|
|
14
|
+
// it:
|
|
15
15
|
//
|
|
16
16
|
// * ubugeeei-prod/uf#417, `uft.stubEnv("NODE_ENV", …)` still set for the
|
|
17
17
|
// next file;
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
// fired and the file hung with nothing on screen;
|
|
21
21
|
// * ubugeeei-prod/uf#607, `document.body` still holding the markup a
|
|
22
22
|
// hydration test wrote into it, so the next file's "there is one image on
|
|
23
|
-
// the page" found six
|
|
23
|
+
// the page" found six;
|
|
24
|
+
// * ubugeeei-prod/uf#944, the window around that body: a `matchMedia` a file
|
|
25
|
+
// removed, and the document's `FormData` left in place of Node's. That one is not on the list below, because this package does not
|
|
26
|
+
// make the window — `@uniflowed/react-testing` does, and registers how to
|
|
27
|
+
// put it back (see `registeredElsewhere`).
|
|
24
28
|
//
|
|
25
29
|
// Which files share a worker is decided by `.uf/test-timings.json`, so a leak
|
|
26
30
|
// makes the *result* of a suite depend on how the machine was loaded the last
|
|
@@ -168,6 +172,37 @@ function restoreDocument(): void {
|
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Where other packages register process-wide state of their own.
|
|
177
|
+
*
|
|
178
|
+
* A package this one does not depend on can still install something a file
|
|
179
|
+
* changes and the next file reads: `@uniflowed/react-testing` installs a window
|
|
180
|
+
* on the first render and keeps it for the process. Only that package knows
|
|
181
|
+
* what the window looked like when it made it, so it registers how to put it
|
|
182
|
+
* back — a name for the message below, and a function — in a `Map` under this
|
|
183
|
+
* symbol, and every entry runs after the list above.
|
|
184
|
+
*
|
|
185
|
+
* A symbol from the global registry rather than an export, so the package that
|
|
186
|
+
* registers needs no import of this one: under another runner the entry is
|
|
187
|
+
* never read and costs nothing.
|
|
188
|
+
*/
|
|
189
|
+
const SHARED_STATE: symbol = Symbol.for("@uniflowed/test/shared-state");
|
|
190
|
+
|
|
191
|
+
/** Every entry another package registered, as this module's own entries are shaped. */
|
|
192
|
+
function registeredElsewhere(): $ReadOnlyArray<Shared> {
|
|
193
|
+
const registry: mixed = Reflect.get(globalThis, SHARED_STATE);
|
|
194
|
+
if (!(registry instanceof Map)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const entries: Array<Shared> = [];
|
|
198
|
+
for (const [what, restore] of registry) {
|
|
199
|
+
if (typeof what === "string" && typeof restore === "function") {
|
|
200
|
+
entries.push({ what, restore: () => void restore() });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return entries;
|
|
204
|
+
}
|
|
205
|
+
|
|
171
206
|
/**
|
|
172
207
|
* Put back everything the file that just ran may have changed.
|
|
173
208
|
*
|
|
@@ -185,7 +220,7 @@ function restoreDocument(): void {
|
|
|
185
220
|
*/
|
|
186
221
|
export function restoreSharedState(): void {
|
|
187
222
|
let failure: { readonly what: string, readonly thrown: mixed } | null = null;
|
|
188
|
-
for (const shared of SHARED) {
|
|
223
|
+
for (const shared of [...SHARED, ...registeredElsewhere()]) {
|
|
189
224
|
try {
|
|
190
225
|
shared.restore();
|
|
191
226
|
} catch (thrown) {
|
package/internal/registry.js
CHANGED
|
@@ -30,14 +30,29 @@ export type Body = () => mixed | Promise<mixed>;
|
|
|
30
30
|
/** The suffix written on a registration call. */
|
|
31
31
|
export type Modifier = "none" | "only" | "skip" | "todo";
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* How a benchmark runs under `uf test --bench`.
|
|
35
|
+
*
|
|
36
|
+
* `warmup` calls are made and their times thrown away, then `iterations` calls
|
|
37
|
+
* are each timed. `timeout` is the budget for one call, in milliseconds, and
|
|
38
|
+
* the benchmark as a whole is held to that budget once per call.
|
|
39
|
+
*/
|
|
40
|
+
export type BenchOptions = {|
|
|
41
|
+
readonly warmup?: number,
|
|
42
|
+
readonly iterations?: number,
|
|
43
|
+
readonly timeout?: number,
|
|
44
|
+
|};
|
|
45
|
+
|
|
46
|
+
/** One registered test case, or one benchmark. */
|
|
34
47
|
export type Case = {|
|
|
35
|
-
readonly kind: "test",
|
|
48
|
+
readonly kind: "test" | "bench",
|
|
36
49
|
readonly name: string,
|
|
37
50
|
readonly body: Body | null,
|
|
38
51
|
readonly modifier: Modifier,
|
|
39
52
|
readonly skipReason: string | null,
|
|
40
53
|
readonly timeoutMs: number | null,
|
|
54
|
+
/** How to run it, for a benchmark; `null` for a test. */
|
|
55
|
+
readonly bench: BenchOptions | null,
|
|
41
56
|
readonly line: number,
|
|
42
57
|
readonly column: number,
|
|
43
58
|
|};
|
|
@@ -125,15 +140,17 @@ function addCase(
|
|
|
125
140
|
modifier: Modifier,
|
|
126
141
|
timeoutMs: number | null,
|
|
127
142
|
skipReason: string | null = null,
|
|
143
|
+
bench: BenchOptions | null = null,
|
|
128
144
|
): void {
|
|
129
145
|
const position = callSite();
|
|
130
146
|
current.children.push({
|
|
131
|
-
kind: "test",
|
|
147
|
+
kind: bench == null ? "test" : "bench",
|
|
132
148
|
name,
|
|
133
149
|
body,
|
|
134
150
|
modifier,
|
|
135
151
|
skipReason,
|
|
136
152
|
timeoutMs,
|
|
153
|
+
bench,
|
|
137
154
|
line: position.line,
|
|
138
155
|
column: position.column,
|
|
139
156
|
});
|
|
@@ -217,6 +234,34 @@ export const it: $FlowFixMe = caseApi();
|
|
|
217
234
|
/** `test` is `it`, for people who write it that way. */
|
|
218
235
|
export const test: $FlowFixMe = it;
|
|
219
236
|
|
|
237
|
+
/** The `bench` API, and its modifiers. See [`suiteApi`] for the shape. */
|
|
238
|
+
function benchApi(): $FlowFixMe {
|
|
239
|
+
const api: $FlowFixMe = (name: string, body: Body, options?: BenchOptions) => {
|
|
240
|
+
addCase(name, body, "none", options?.timeout ?? null, null, options ?? {});
|
|
241
|
+
};
|
|
242
|
+
api.only = (name: string, body: Body, options?: BenchOptions) => {
|
|
243
|
+
addCase(name, body, "only", options?.timeout ?? null, null, options ?? {});
|
|
244
|
+
};
|
|
245
|
+
api.skip = (name: string, body?: Body) => {
|
|
246
|
+
addCase(name, body ?? null, "skip", null, null, {});
|
|
247
|
+
};
|
|
248
|
+
api.todo = (name: string) => {
|
|
249
|
+
addCase(name, null, "todo", null, null, {});
|
|
250
|
+
};
|
|
251
|
+
return api;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Register one benchmark.
|
|
256
|
+
*
|
|
257
|
+
* `uf test` reports a benchmark as skipped, so a suite does not pay for timing
|
|
258
|
+
* one, and `uf test --bench` runs the benchmarks in place of the tests and
|
|
259
|
+
* reports how long each call took. `bench.only`, `bench.skip` and `bench.todo`
|
|
260
|
+
* do what `it`'s do. See [`BenchOptions`] for `warmup`, `iterations` and
|
|
261
|
+
* `timeout`.
|
|
262
|
+
*/
|
|
263
|
+
export const bench: $FlowFixMe = benchApi();
|
|
264
|
+
|
|
220
265
|
/**
|
|
221
266
|
* Substitute a row into a name, the way every runner spells it: `%s` for the
|
|
222
267
|
* value, `%j` for its JSON.
|
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,
|
|
@@ -29,7 +36,7 @@ export type Outcome =
|
|
|
29
36
|
|}
|
|
30
37
|
| {|
|
|
31
38
|
readonly status: "skipped",
|
|
32
|
-
readonly reason: "explicit" | "not-only" | "filtered",
|
|
39
|
+
readonly reason: "explicit" | "not-only" | "filtered" | "bench" | "not-bench",
|
|
33
40
|
readonly message?: string | null,
|
|
34
41
|
|}
|
|
35
42
|
| {| readonly status: "todo" |};
|
|
@@ -56,11 +63,31 @@ export type RunOptions = {|
|
|
|
56
63
|
* which file that is — a test's name alone does not locate it.
|
|
57
64
|
*/
|
|
58
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,
|
|
59
71
|
|};
|
|
60
72
|
|
|
61
73
|
/** Default budget for one case, matching what most runners use. */
|
|
62
74
|
export const DEFAULT_TIMEOUT_MS: number = 5000;
|
|
63
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
|
+
|
|
64
91
|
/** The separator between a suite's name and its child's. */
|
|
65
92
|
export const NAME_SEPARATOR: string = " > ";
|
|
66
93
|
|
|
@@ -76,7 +103,9 @@ function fullName(path: $ReadOnlyArray<string>): string {
|
|
|
76
103
|
*/
|
|
77
104
|
function hasOnly(node: Suite | Case, inherited: boolean): boolean {
|
|
78
105
|
const marked = inherited || node.modifier === "only";
|
|
79
|
-
|
|
106
|
+
// A benchmark is a case like a test: `bench.only` restricts a file as
|
|
107
|
+
// `it.only` does.
|
|
108
|
+
if (node.kind !== "suite") {
|
|
80
109
|
return marked;
|
|
81
110
|
}
|
|
82
111
|
return node.children.some((child) => hasOnly(child, marked));
|
|
@@ -214,9 +243,19 @@ async function runCase(
|
|
|
214
243
|
report({ status: "skipped", reason: "filtered" });
|
|
215
244
|
return true;
|
|
216
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
|
+
}
|
|
217
255
|
|
|
218
256
|
const timeoutMs = test.timeoutMs ?? options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
219
257
|
let outcome: Outcome = { status: "passed" };
|
|
258
|
+
let samples: $ReadOnlyArray<number> | null = null;
|
|
220
259
|
// Setup, body and teardown run *inside* the case's output context, and that
|
|
221
260
|
// nesting is the whole of the fix for #207. What names a printed line is no
|
|
222
261
|
// longer where the runner had got to when the line arrived — which named the
|
|
@@ -231,7 +270,11 @@ async function runCase(
|
|
|
231
270
|
for (const hook of context.beforeEach) {
|
|
232
271
|
await withTimeout(hook, timeoutMs);
|
|
233
272
|
}
|
|
234
|
-
|
|
273
|
+
if (benchmark) {
|
|
274
|
+
samples = await measure(body, test.bench, timeoutMs);
|
|
275
|
+
} else {
|
|
276
|
+
await withTimeout(body, timeoutMs);
|
|
277
|
+
}
|
|
235
278
|
} catch (thrown) {
|
|
236
279
|
outcome = failure(thrown, options.file ?? null);
|
|
237
280
|
}
|
|
@@ -252,10 +295,74 @@ async function runCase(
|
|
|
252
295
|
// is the file's, and a snapshot taken outside one fails with something better
|
|
253
296
|
// than a key belonging to whichever test happened to run last.
|
|
254
297
|
snapshot.exitTest();
|
|
255
|
-
report(outcome);
|
|
298
|
+
report(outcome.status === "passed" && samples != null ? { status: "passed", samples } : outcome);
|
|
256
299
|
return outcome.status !== "failed";
|
|
257
300
|
}
|
|
258
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
|
+
|
|
259
366
|
/**
|
|
260
367
|
* Walk one suite, running what it contains.
|
|
261
368
|
*
|
|
@@ -306,13 +413,16 @@ async function runSuite(
|
|
|
306
413
|
if (state.bail) {
|
|
307
414
|
break;
|
|
308
415
|
}
|
|
309
|
-
if (child.kind
|
|
416
|
+
if (child.kind !== "suite") {
|
|
310
417
|
const willRun =
|
|
311
418
|
!inner.skipped &&
|
|
312
419
|
child.modifier !== "skip" &&
|
|
313
420
|
child.modifier !== "todo" &&
|
|
314
421
|
child.body != null &&
|
|
315
|
-
(!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);
|
|
316
426
|
if (willRun) {
|
|
317
427
|
try {
|
|
318
428
|
await setUpOnce();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/test",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.37",
|
|
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,7 +11,10 @@
|
|
|
11
11
|
"directory": "packages/test"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".":
|
|
14
|
+
".": {
|
|
15
|
+
"uniflowed-bun-test": "./bun/index.js",
|
|
16
|
+
"default": "./index.js"
|
|
17
|
+
},
|
|
15
18
|
"./in-source": "./in-source.js",
|
|
16
19
|
"./worker": "./worker.js",
|
|
17
20
|
"./browser-worker": "./browser-worker.js",
|
|
@@ -19,6 +22,7 @@
|
|
|
19
22
|
},
|
|
20
23
|
"files": [
|
|
21
24
|
"browser-worker.js",
|
|
25
|
+
"bun",
|
|
22
26
|
"in-source.js",
|
|
23
27
|
"index.js",
|
|
24
28
|
"internal",
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
"!*.test.js"
|
|
27
31
|
],
|
|
28
32
|
"dependencies": {
|
|
29
|
-
"@uniflowed/host": "0.0.0-alpha.
|
|
33
|
+
"@uniflowed/host": "0.0.0-alpha.37"
|
|
30
34
|
},
|
|
31
35
|
"peerDependencies": {
|
|
32
36
|
"axe-core": ">=4"
|
package/worker.js
CHANGED
|
@@ -58,26 +58,19 @@ import { installInSourceTests } from "./in-source.js";
|
|
|
58
58
|
import { restoreSharedState } from "./internal/isolation.js";
|
|
59
59
|
import { run } from "./internal/run.js";
|
|
60
60
|
|
|
61
|
-
// `process` is imported rather than read off the global
|
|
62
|
-
// the
|
|
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.
|
|
63
64
|
//
|
|
64
|
-
//
|
|
65
|
-
// `
|
|
66
|
-
//
|
|
67
|
-
// process
|
|
68
|
-
// `process.
|
|
69
|
-
//
|
|
70
|
-
// `
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// So the entry point installs it, once, before anything below runs. `??=`
|
|
75
|
-
// rather than `=`: on Node and Bun the global is already the real object and
|
|
76
|
-
// this must not replace it with a second view of the same thing.
|
|
77
|
-
//
|
|
78
|
-
// It is a shim of the host, in the one file that is a process entry point, and
|
|
79
|
-
// it stops here — nothing else in `@uniflowed/test` may do this.
|
|
80
|
-
globalThis.process ??= process;
|
|
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.
|
|
81
74
|
|
|
82
75
|
/** What `uf` sends for one file. */
|
|
83
76
|
type Request = {|
|
|
@@ -159,6 +152,17 @@ function write(event: { readonly [string]: mixed }): void {
|
|
|
159
152
|
* the `serving` store this runs inside — it is what every event written from
|
|
160
153
|
* this file, or from anything this file leaves behind, is stamped with.
|
|
161
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
|
+
|
|
162
166
|
async function runFile(request: Request, generation: number): Promise<void> {
|
|
163
167
|
const started = performance.now();
|
|
164
168
|
// Everything the previous file changed and this package shares with it goes
|
|
@@ -231,7 +235,12 @@ async function runImportedFile(
|
|
|
231
235
|
try {
|
|
232
236
|
const absolute = fileURLToPath(pathToFileURL(request.file).href);
|
|
233
237
|
await run(
|
|
234
|
-
{
|
|
238
|
+
{
|
|
239
|
+
filter: request.filter ?? null,
|
|
240
|
+
timeoutMs: request.timeoutMs,
|
|
241
|
+
file: absolute,
|
|
242
|
+
bench: benching(),
|
|
243
|
+
},
|
|
235
244
|
(result) => {
|
|
236
245
|
write({
|
|
237
246
|
event: "test",
|