@uniflowed/test 0.0.0-alpha.1 → 0.0.0-alpha.11

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.
@@ -0,0 +1,287 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/test`: the `uft` namespace.
4
+ //
5
+ // The operations Vitest groups under `vi`, under uf's own name. The *shape* is
6
+ // what a migration needs — `fn`, `spyOn`, `stubEnv`, `useFakeTimers` doing what
7
+ // they do elsewhere — and borrowing another tool's brand for it would be
8
+ // claiming something uf has not earned.
9
+ //
10
+ // A namespace rather than loose named exports, because several of these names
11
+ // are generic enough to collide in a test file: `@uniflowed/testing` re-exports
12
+ // both this package and `@uniflowed/react-testing`, and both have a `waitFor`.
13
+ //
14
+ // `uft` rather than `uf`, and rather than `uf.test`. A bare `uf` is the command
15
+ // and the project, and a test file would be using the name for something much
16
+ // smaller than everything else in the toolchain answers to. `uf.test` reads as
17
+ // the `test` function this package also exports, which is `it` under another
18
+ // name — two very different things one dot apart. `uft` is three characters,
19
+ // belongs to nothing else, and is what a reader types a hundred times a file.
20
+ //
21
+ // `uft.mock` and the six names beside it intercept a module before it is
22
+ // imported, which is the loader's job rather than the runner's: by the time the
23
+ // runner sees an `import`, the module has been fetched, linked and evaluated.
24
+ // So the mechanism is `@uniflowed/host`'s (`module-mocks.js`) and the API is
25
+ // `./modules.js`'s, and this file only names them. A host that cannot provide
26
+ // synchronous module hooks gets an `UnsupportedError` that says which host it
27
+ // is and what to do instead — never a binding that silently does nothing.
28
+
29
+ import {
30
+ importActual as importActualModule,
31
+ importMock as importMockModule,
32
+ mock as mockModule,
33
+ resetModules as resetModulesNow,
34
+ unmock as unmockModule,
35
+ } from "./modules.js";
36
+ import { clearAllMocks, fn, resetAllMocks, restoreAllMocks, spyOn } from "./spy.js";
37
+ import * as timers from "./timers.js";
38
+ import { UnsupportedError } from "./unsupported.js";
39
+
40
+ // Declared in `./unsupported.js` rather than here, because `./modules.js` needs
41
+ // it too and a class both halves of a pair reach for is a third module.
42
+ export { UnsupportedError } from "./unsupported.js";
43
+
44
+ /** Environment variables `stubEnv` replaced, and what they were. */
45
+ const stubbedEnv: Map<string, string | void> = new Map();
46
+
47
+ /** Globals `stubGlobal` replaced, and what they were. */
48
+ const stubbedGlobals: Map<string, { readonly owned: boolean, readonly value: mixed }> = new Map();
49
+
50
+ /**
51
+ * Read the process environment, whichever host this is.
52
+ *
53
+ * Node, Deno and Bun all expose `process.env`; Deno also has `Deno.env`, and
54
+ * reaching for `process` first keeps one code path across the three.
55
+ */
56
+ function environment(): { [string]: string } | null {
57
+ const host = globalThis as $FlowFixMe;
58
+ return host.process?.env ?? null;
59
+ }
60
+
61
+ /**
62
+ * Replace an environment variable for the rest of the test.
63
+ *
64
+ * Undone by `unstubAllEnvs`, which the runner calls between files — a stub that
65
+ * outlived its test would be a test that passes alone and fails in a suite.
66
+ */
67
+ export function stubEnv(name: string, value: string | void): void {
68
+ const env = environment();
69
+ if (env == null) {
70
+ throw new UnsupportedError("stubEnv", "this host exposes no process environment");
71
+ }
72
+ if (!stubbedEnv.has(name)) {
73
+ stubbedEnv.set(name, Object.hasOwn(env, name) ? env[name] : undefined);
74
+ }
75
+ if (value === undefined) {
76
+ delete env[name];
77
+ } else {
78
+ env[name] = value;
79
+ }
80
+ }
81
+
82
+ /** Put every environment variable `stubEnv` replaced back. */
83
+ export function unstubAllEnvs(): void {
84
+ const env = environment();
85
+ if (env == null) {
86
+ stubbedEnv.clear();
87
+ return;
88
+ }
89
+ for (const [name, previous] of stubbedEnv) {
90
+ if (previous === undefined) {
91
+ delete env[name];
92
+ } else {
93
+ env[name] = previous;
94
+ }
95
+ }
96
+ stubbedEnv.clear();
97
+ }
98
+
99
+ /**
100
+ * Replace a global for the rest of the test.
101
+ *
102
+ * Whether the global was the object's own property is recorded, because putting
103
+ * back an inherited one by assignment would leave a copy that shadows whatever
104
+ * it was inherited from.
105
+ */
106
+ export function stubGlobal(name: string, value: mixed): void {
107
+ const host = globalThis as $FlowFixMe;
108
+ if (!stubbedGlobals.has(name)) {
109
+ stubbedGlobals.set(name, {
110
+ owned: Object.hasOwn(host, name),
111
+ value: host[name],
112
+ });
113
+ }
114
+ host[name] = value;
115
+ }
116
+
117
+ /** Put every global `stubGlobal` replaced back. */
118
+ export function unstubAllGlobals(): void {
119
+ const host = globalThis as $FlowFixMe;
120
+ for (const [name, previous] of stubbedGlobals) {
121
+ if (previous.owned) {
122
+ host[name] = previous.value;
123
+ } else {
124
+ delete host[name];
125
+ }
126
+ }
127
+ stubbedGlobals.clear();
128
+ }
129
+
130
+ /** How often `waitFor` re-runs its body while it is failing. */
131
+ const WAIT_INTERVAL_MS = 20;
132
+
133
+ /** How long `waitFor` keeps trying before giving up. */
134
+ const WAIT_TIMEOUT_MS = 1_000;
135
+
136
+ /**
137
+ * Run `body` until it stops throwing, or the timeout passes.
138
+ *
139
+ * The last failure is what is raised, not a timeout — "expected 2, got 1" says
140
+ * what went wrong, and "timed out" says only that something did.
141
+ */
142
+ export async function waitFor<T>(
143
+ body: () => T | Promise<T>,
144
+ options?: { readonly timeout?: number, readonly interval?: number },
145
+ ): Promise<T> {
146
+ const timeout = options?.timeout ?? WAIT_TIMEOUT_MS;
147
+ const interval = options?.interval ?? WAIT_INTERVAL_MS;
148
+ const deadline = Date.now() + timeout;
149
+ let last: mixed = null;
150
+
151
+ for (;;) {
152
+ try {
153
+ return await body();
154
+ } catch (thrown) {
155
+ last = thrown;
156
+ }
157
+ if (Date.now() >= deadline) {
158
+ throw last ?? new Error(`uft.waitFor: gave up after ${timeout}ms`);
159
+ }
160
+ await new Promise((resolve) => setTimeout(resolve, interval));
161
+ }
162
+ }
163
+
164
+ /** Run `body` until it returns something truthy, or the timeout passes. */
165
+ export async function waitUntil(
166
+ body: () => mixed | Promise<mixed>,
167
+ options?: { readonly timeout?: number, readonly interval?: number },
168
+ ): Promise<mixed> {
169
+ return waitFor(async () => {
170
+ const value = await body();
171
+ if (value == null || value === false) {
172
+ throw new Error("uft.waitUntil: the condition is not true yet");
173
+ }
174
+ return value;
175
+ }, options);
176
+ }
177
+
178
+ /**
179
+ * Hand a value back with its mock methods visible to the type checker.
180
+ *
181
+ * Purely a type-level convenience, exactly as in Vitest: at runtime it is the
182
+ * identity function, and its whole job is letting a test write
183
+ * `uft.mocked(client.send).mockReturnValue(…)` without a cast.
184
+ */
185
+ export function mocked<T>(value: T): $FlowFixMe {
186
+ return value;
187
+ }
188
+
189
+ /**
190
+ * The `uft` namespace's type.
191
+ *
192
+ * Written out member by member rather than left as one `$FlowFixMe`, because
193
+ * the module-mocking half of it is the half a type can genuinely check: a
194
+ * factory that hands back the wrong shape for the module it is standing in for
195
+ * is an error at the call site, and that only works if `uft` has a type at all.
196
+ * The members that were already typed loosely keep the types they have —
197
+ * `typeof` reads them from their definitions, so this list cannot drift from
198
+ * them.
199
+ */
200
+ export type Uft = {
201
+ readonly fn: typeof fn,
202
+ readonly spyOn: typeof spyOn,
203
+ readonly mocked: typeof mocked,
204
+
205
+ readonly clearAllMocks: typeof clearAllMocks,
206
+ readonly resetAllMocks: typeof resetAllMocks,
207
+ readonly restoreAllMocks: typeof restoreAllMocks,
208
+
209
+ readonly stubEnv: typeof stubEnv,
210
+ readonly unstubAllEnvs: typeof unstubAllEnvs,
211
+ readonly stubGlobal: typeof stubGlobal,
212
+ readonly unstubAllGlobals: typeof unstubAllGlobals,
213
+
214
+ readonly waitFor: typeof waitFor,
215
+ readonly waitUntil: typeof waitUntil,
216
+
217
+ readonly useFakeTimers: typeof timers.useFakeTimers,
218
+ readonly useRealTimers: typeof timers.useRealTimers,
219
+ readonly isFakeTimers: typeof timers.isFaked,
220
+ readonly advanceTimersByTime: typeof timers.advanceTimersByTime,
221
+ readonly advanceTimersByTimeAsync: typeof timers.advanceTimersByTimeAsync,
222
+ readonly advanceTimersToNextTimer: typeof timers.advanceTimersToNextTimer,
223
+ readonly runAllTimers: typeof timers.runAllTimers,
224
+ readonly runOnlyPendingTimers: typeof timers.runOnlyPendingTimers,
225
+ readonly getTimerCount: typeof timers.getTimerCount,
226
+ readonly setSystemTime: typeof timers.setSystemTime,
227
+ readonly getMockedSystemTime: typeof timers.getMockedSystemTime,
228
+
229
+ readonly mock: typeof mockModule,
230
+ readonly doMock: typeof mockModule,
231
+ readonly unmock: typeof unmockModule,
232
+ readonly doUnmock: typeof unmockModule,
233
+ readonly importActual: typeof importActualModule,
234
+ readonly importMock: typeof importMockModule,
235
+ readonly resetModules: typeof resetModulesNow,
236
+ };
237
+
238
+ /**
239
+ * The `uft` namespace.
240
+ *
241
+ * A frozen object rather than a class: it is a namespace, nothing about it is
242
+ * per-instance, and freezing it means a test cannot leave a monkey-patch behind
243
+ * for the next one.
244
+ */
245
+ export const uft: Uft = Object.freeze({
246
+ fn,
247
+ spyOn,
248
+ mocked,
249
+
250
+ clearAllMocks,
251
+ resetAllMocks,
252
+ restoreAllMocks,
253
+
254
+ stubEnv,
255
+ unstubAllEnvs,
256
+ stubGlobal,
257
+ unstubAllGlobals,
258
+
259
+ waitFor,
260
+ waitUntil,
261
+
262
+ // The clock a test controls. A test about "after five minutes the session
263
+ // expires" should not take five minutes.
264
+ useFakeTimers: timers.useFakeTimers,
265
+ useRealTimers: timers.useRealTimers,
266
+ isFakeTimers: timers.isFaked,
267
+ advanceTimersByTime: timers.advanceTimersByTime,
268
+ advanceTimersByTimeAsync: timers.advanceTimersByTimeAsync,
269
+ advanceTimersToNextTimer: timers.advanceTimersToNextTimer,
270
+ runAllTimers: timers.runAllTimers,
271
+ runOnlyPendingTimers: timers.runOnlyPendingTimers,
272
+ getTimerCount: timers.getTimerCount,
273
+ setSystemTime: timers.setSystemTime,
274
+ getMockedSystemTime: timers.getMockedSystemTime,
275
+
276
+ // Module interception. `doMock` is `mock` and `doUnmock` is `unmock`, under
277
+ // the names Vitest gives the un-hoisted forms: there is one form here,
278
+ // because uf hoists neither, and a `doMock` that was a different function
279
+ // would be claiming a difference that does not exist.
280
+ mock: mockModule,
281
+ doMock: mockModule,
282
+ unmock: unmockModule,
283
+ doUnmock: unmockModule,
284
+ importActual: importActualModule,
285
+ importMock: importMockModule,
286
+ resetModules: resetModulesNow,
287
+ });
@@ -0,0 +1,309 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/test`: what a test printed, attributed to the test
4
+ // that printed it.
5
+ //
6
+ // The worker and the tests it runs share one stdout, and the worker's protocol
7
+ // is one JSON object per line on it. A `console.log` in a test therefore wrote
8
+ // a line into the middle of that protocol, `uf` read it as an event it could
9
+ // not parse, and the whole file died — a green suite killed by a debugging
10
+ // statement somebody left in.
11
+ //
12
+ // So the stream stops being shared. This module takes over `console` and the
13
+ // two `write` methods a test can reach directly, turns everything written
14
+ // through them into an ordinary protocol event, and hands the caller the real
15
+ // `process.stdout.write` it took — the protocol keeps the raw stream, and it
16
+ // is the only thing that has it. A test that prints something shaped exactly
17
+ // like a protocol line is then a string inside a JSON field, which is the
18
+ // point: the escaping is what makes the channel robust, not a filter that
19
+ // tries to recognise the imposter.
20
+ //
21
+ // # Why this is its own module
22
+ //
23
+ // Two callers need it and neither owns it: `worker.js` installs the capture at
24
+ // start-up, and `run.js` runs each case inside the ownership kept here, so a
25
+ // chunk can be named. "Who printed this" is also state with a lifetime of its
26
+ // own — one case's hooks and body — exactly like the snapshot key in
27
+ // `snapshot.js`, and for the same reason it lives beside the thing it describes
28
+ // rather than inside either caller.
29
+ //
30
+ // # What "the test that printed it" means
31
+ //
32
+ // The case whose *asynchronous context* the write happened in, which is the
33
+ // case whose code produced it.
34
+ //
35
+ // The obvious answer was a module-level variable the runner set before a case
36
+ // and cleared after it, and it was wrong in one shape that matters: the name a
37
+ // chunk carried was whatever the worker happened to be running when the chunk
38
+ // arrived. A `setTimeout` a test left behind fires while the *next* case is
39
+ // running, so the line it printed was reported under that next case — a test
40
+ // accused of printing something it never printed, which is worse than not
41
+ // naming it at all, because a reader chasing the message finds it under code
42
+ // that does not contain it. See ubugeeei-prod/uf#207.
43
+ //
44
+ // So the owner is an `AsyncLocalStorage`, and what `run.js` calls is
45
+ // `runInTest` rather than an `enterTest` / `exitTest` pair: the store is only
46
+ // carried by work started *inside* the case, so the case's setup, body and
47
+ // teardown have to run within it. Everything they schedule inherits it,
48
+ // whenever it eventually runs.
49
+ //
50
+ // # When there is no owner
51
+ //
52
+ // `getStore()` answers nothing outside a case, and a chunk with no owner is
53
+ // filed under the file — which is right for an import, a `beforeAll`, or a
54
+ // straggler from a case that is long gone.
55
+ //
56
+ // It is also the answer on a host whose storage does not reach the callback.
57
+ // Deno 1.31 has `AsyncLocalStorage` and propagates it across `await`, but not
58
+ // through `setTimeout`, so a detached callback there is filed under the file
59
+ // rather than under the case that scheduled it. That degradation is the point:
60
+ // of the two ways to be less than exact, naming the file says less, and naming
61
+ // the next case says something false.
62
+ //
63
+ // `node:async_hooks` itself is not guarded for, because a guard could not run.
64
+ // Node, Deno and Bun all provide it under the `node:` specifier, and a host
65
+ // that had no `node:` builtins could not start this worker at all — `node:util`
66
+ // is imported below, `node:readline` and `node:url` by `worker.js`. A `typeof`
67
+ // check around the constructor would only ever execute on a host where this
68
+ // module had already linked.
69
+ //
70
+ // The one thing this does not answer is a straggler that outlives its *file*:
71
+ // the worker runs the next file in the same process, and a chunk still carrying
72
+ // a name from the file before is a name the next file's report has no test for.
73
+ // The host files it under the file it arrived in, which is honest but not
74
+ // exact, and closing it properly needs the file's generation in the protocol.
75
+ // That is ubugeeei-prod/uf#203, and it is not this module's to fix.
76
+
77
+ // # Bounds
78
+ //
79
+ // A test that prints in a loop must not be able to fill the pipe, the worker's
80
+ // memory or the report, so one file's captured output is bounded and the
81
+ // bound is announced rather than hidden: the chunk that reaches it says so and
82
+ // nothing after it is kept. The budget starts over for each file, so a chatty
83
+ // file does not silence the next one in the same worker.
84
+
85
+ import { AsyncLocalStorage } from "node:async_hooks";
86
+ import { format, inspect } from "node:util";
87
+
88
+ import { userFrames } from "./frames.js";
89
+
90
+ /** Which of the process's two streams a chunk was written to. */
91
+ export type OutputStream = "stdout" | "stderr";
92
+
93
+ /** One thing a test — or the file around it — printed. */
94
+ export type OutputChunk = {|
95
+ readonly stream: OutputStream,
96
+ /** Full name of the case that was running, or `null` when none was. */
97
+ readonly test: string | null,
98
+ /** The text as it would have reached the terminal, newline included. */
99
+ readonly text: string,
100
+ |};
101
+
102
+ /** Where captured output goes. */
103
+ export type OutputSink = (chunk: OutputChunk) => void;
104
+
105
+ /**
106
+ * Longest single write kept, in UTF-16 code units.
107
+ *
108
+ * A test that prints a megabyte-long serialised fixture meant to print
109
+ * something; the first few kilobytes of it are what says what happened, and
110
+ * the rest is not a report's to carry.
111
+ */
112
+ export const MAX_CHUNK_LENGTH: number = 8 * 1024;
113
+
114
+ /** Most output kept from one file, in UTF-16 code units. */
115
+ export const MAX_FILE_LENGTH: number = 128 * 1024;
116
+
117
+ /** Which stream each replaced `console` method writes to, as Node routes them. */
118
+ const CONSOLE_STREAMS: { readonly [string]: OutputStream } = {
119
+ log: "stdout",
120
+ info: "stdout",
121
+ debug: "stdout",
122
+ warn: "stderr",
123
+ error: "stderr",
124
+ };
125
+
126
+ /** Decoder for a `write` that was handed bytes rather than a string. */
127
+ const DECODER = new TextDecoder();
128
+
129
+ /** What a stream's `write` calls when it has taken the chunk. */
130
+ type WriteCallback = () => mixed;
131
+
132
+ /**
133
+ * The case a write belongs to, kept in the asynchronous context it ran in.
134
+ *
135
+ * Full names rather than a record, because that is the whole of what a chunk
136
+ * needs to say and the protocol carries it as a string either way.
137
+ */
138
+ const owner: AsyncLocalStorage<string> = new AsyncLocalStorage();
139
+
140
+ let sink: OutputSink | null = null;
141
+ let raw: ((chunk: string) => void) | null = null;
142
+ let captured = 0;
143
+ let stopped = false;
144
+
145
+ /**
146
+ * The globals to patch, in one place.
147
+ *
148
+ * The one untyped expression in this module, and it is the trust boundary
149
+ * itself: `console` and `process.stdout` are the host's, their libdef types
150
+ * are read-only, and replacing them is exactly what this module is for.
151
+ */
152
+ function host(): $FlowFixMe {
153
+ return globalThis;
154
+ }
155
+
156
+ /**
157
+ * `args` rendered the way `console.log` renders them.
158
+ *
159
+ * Node's console hands its arguments to `util.format`, and this is that in two
160
+ * cases rather than one, because `format`'s first parameter is the template:
161
+ * with a leading string it substitutes `%s`, `%d`, `%o` and the rest and
162
+ * inspects whatever is left over, and with anything else there is no template
163
+ * to substitute into, so every argument is inspected and the results joined
164
+ * with a space. Both are what Node prints.
165
+ */
166
+ function formatArguments(args: $ReadOnlyArray<mixed>): string {
167
+ const [first, ...rest] = args;
168
+ if (typeof first === "string") {
169
+ return format(first, ...rest);
170
+ }
171
+ return args.map((value) => (typeof value === "string" ? value : inspect(value))).join(" ");
172
+ }
173
+
174
+ /** The text of one `write` argument, whether it arrived as bytes or a string. */
175
+ function textOf(chunk: mixed): string {
176
+ if (typeof chunk === "string") {
177
+ return chunk;
178
+ }
179
+ if (chunk instanceof Uint8Array) {
180
+ return DECODER.decode(chunk);
181
+ }
182
+ return String(chunk);
183
+ }
184
+
185
+ /**
186
+ * Record one piece of output, within the file's budget.
187
+ *
188
+ * Silently doing nothing when no sink is installed is deliberate: a test that
189
+ * prints must never fail because of how it was run.
190
+ */
191
+ function capture(stream: OutputStream, text: string): void {
192
+ const to = sink;
193
+ if (to == null || stopped || text === "") {
194
+ return;
195
+ }
196
+ let kept = text.length > MAX_CHUNK_LENGTH ? `${text.slice(0, MAX_CHUNK_LENGTH)}…\n` : text;
197
+ const room = MAX_FILE_LENGTH - captured;
198
+ if (kept.length >= room) {
199
+ kept = `${kept.slice(0, Math.max(room, 0))}\n[uf] output stopped after ${MAX_FILE_LENGTH} characters\n`;
200
+ stopped = true;
201
+ }
202
+ captured += kept.length;
203
+ to({ stream, test: owner.getStore() ?? null, text: kept });
204
+ }
205
+
206
+ /** A stand-in for `process.stdout.write` / `process.stderr.write`. */
207
+ function writer(
208
+ stream: OutputStream,
209
+ ): (chunk: mixed, encoding?: string | WriteCallback, callback?: WriteCallback) => boolean {
210
+ return (chunk, encoding, callback) => {
211
+ capture(stream, textOf(chunk));
212
+ // `write(chunk, callback)` and `write(chunk, encoding, callback)` are both
213
+ // real calls, and a caller that passed a callback is waiting for it.
214
+ //
215
+ // Deferred, because the real `Writable.write` never calls back before it
216
+ // returns: a caller that writes and then does something on the next line
217
+ // has that line run first, and one whose callback ran inline would see the
218
+ // two in the other order. `queueMicrotask` rather than `process.nextTick`
219
+ // so this holds on every host uf supports.
220
+ const done = typeof encoding === "function" ? encoding : callback;
221
+ if (done != null) {
222
+ queueMicrotask(done);
223
+ }
224
+ // The real method returns whether the stream has room for more. Nothing is
225
+ // buffered here, so it always has — and a caller told otherwise would wait
226
+ // for a `drain` that never comes.
227
+ return true;
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Route everything a test prints to `to` instead of to the process's streams,
233
+ * and return the real `process.stdout.write` that was replaced.
234
+ *
235
+ * The caller gets the raw stream because the caller is the protocol, and a
236
+ * protocol sharing its stream with the code it reports on is the bug this
237
+ * module exists for. Handing it back from here rather than letting the caller
238
+ * read it first is what makes "taken before anything could have replaced it"
239
+ * true by construction.
240
+ *
241
+ * Installed once, for the life of the worker: a worker runs many files, and
242
+ * restoring the real methods between them would leave a window in which a
243
+ * straggling `setTimeout` from the previous file writes into the protocol.
244
+ */
245
+ export function install(to: OutputSink): (chunk: string) => void {
246
+ const global = host();
247
+ const already = raw;
248
+ if (already != null) {
249
+ return already;
250
+ }
251
+ const stdout = global.process.stdout;
252
+ const real = stdout.write;
253
+ const protocol = (chunk: string) => {
254
+ real.call(stdout, chunk);
255
+ };
256
+ raw = protocol;
257
+ sink = to;
258
+ for (const method of Object.keys(CONSOLE_STREAMS)) {
259
+ const stream = CONSOLE_STREAMS[method];
260
+ global.console[method] = (...args: $ReadOnlyArray<mixed>) => {
261
+ capture(stream, `${formatArguments(args)}\n`);
262
+ };
263
+ }
264
+ global.console.trace = (...args: $ReadOnlyArray<mixed>) => {
265
+ // `console.trace` is a message *and* the stack under it, which is the
266
+ // whole reason to call it rather than `console.error`. `Error.stack`
267
+ // opens with `Trace: <message>`, so the trace Node prints is that string
268
+ // with this module's own frames taken off it.
269
+ const error = new Error(formatArguments(args));
270
+ error.name = "Trace";
271
+ capture("stderr", `${userFrames(error.stack) ?? `Trace: ${error.message}`}\n`);
272
+ };
273
+
274
+ global.process.stdout.write = writer("stdout");
275
+ global.process.stderr.write = writer("stderr");
276
+ return protocol;
277
+ }
278
+
279
+ /**
280
+ * Run `body` as `name`, so what it prints — and what it leaves behind to print
281
+ * later — is filed under that case.
282
+ *
283
+ * The runner wraps one case's `beforeEach`, body and `afterEach` in a single
284
+ * call, because those are the one case's work. Whatever `body` returns is
285
+ * returned unchanged, so an `await` on this is an `await` on the case.
286
+ *
287
+ * Nothing here needs an "and now nothing is running" counterpart. Output from
288
+ * an import, a `beforeAll` or a case that has already been reported was never
289
+ * inside this call, so it has no owner and is the file's — which is the
290
+ * property the previous module-level variable had to be reset to keep, and
291
+ * kept only for as long as nothing straggled.
292
+ */
293
+ export function runInTest<T>(name: string, body: () => T): T {
294
+ return owner.run(name, body);
295
+ }
296
+
297
+ /**
298
+ * Start one file's output budget over.
299
+ *
300
+ * Only the budget: there is no current case to clear, because a case's
301
+ * ownership lives in the callbacks it started rather than in this module. A
302
+ * straggler from the file before still carries the name it was written under,
303
+ * which the host cannot match to a test of the new file and files under that
304
+ * file instead. See ubugeeei-prod/uf#203.
305
+ */
306
+ export function startFile(): void {
307
+ captured = 0;
308
+ stopped = false;
309
+ }
@@ -32,27 +32,27 @@ export type Modifier = "none" | "only" | "skip" | "todo";
32
32
 
33
33
  /** One registered test case. */
34
34
  export type Case = {|
35
- +kind: "test",
36
- +name: string,
37
- +body: Body | null,
38
- +modifier: Modifier,
39
- +timeoutMs: number | null,
40
- +line: number,
41
- +column: number,
35
+ readonly kind: "test",
36
+ readonly name: string,
37
+ readonly body: Body | null,
38
+ readonly modifier: Modifier,
39
+ readonly timeoutMs: number | null,
40
+ readonly line: number,
41
+ readonly column: number,
42
42
  |};
43
43
 
44
44
  /** One `describe` and everything inside it. */
45
45
  export type Suite = {|
46
- +kind: "suite",
47
- +name: string,
48
- +modifier: Modifier,
49
- +children: Array<Suite | Case>,
50
- +beforeAll: Array<Body>,
51
- +afterAll: Array<Body>,
52
- +beforeEach: Array<Body>,
53
- +afterEach: Array<Body>,
54
- +line: number,
55
- +column: number,
46
+ readonly kind: "suite",
47
+ readonly name: string,
48
+ readonly modifier: Modifier,
49
+ readonly children: Array<Suite | Case>,
50
+ readonly beforeAll: Array<Body>,
51
+ readonly afterAll: Array<Body>,
52
+ readonly beforeEach: Array<Body>,
53
+ readonly afterEach: Array<Body>,
54
+ readonly line: number,
55
+ readonly column: number,
56
56
  |};
57
57
 
58
58
  function suite(name: string, modifier: Modifier, line: number, column: number): Suite {
@@ -101,7 +101,7 @@ export function collected(): Suite {
101
101
  * not in a shape we understand, the position is `0`, which every consumer
102
102
  * treats as "unknown" rather than as line one.
103
103
  */
104
- function callSite(): {| +line: number, +column: number |} {
104
+ function callSite(): {| readonly line: number, readonly column: number |} {
105
105
  return firstUserSite(new Error("position").stack) ?? { line: 0, column: 0 };
106
106
  }
107
107
 
@@ -118,7 +118,12 @@ function addSuite(name: string, body: Body, modifier: Modifier): void {
118
118
  }
119
119
  }
120
120
 
121
- function addCase(name: string, body: Body | null, modifier: Modifier, timeoutMs: number | null): void {
121
+ function addCase(
122
+ name: string,
123
+ body: Body | null,
124
+ modifier: Modifier,
125
+ timeoutMs: number | null,
126
+ ): void {
122
127
  const position = callSite();
123
128
  current.children.push({
124
129
  kind: "test",
@@ -132,7 +137,7 @@ function addCase(name: string, body: Body | null, modifier: Modifier, timeoutMs:
132
137
  }
133
138
 
134
139
  /** Options a single test may carry. */
135
- export type TestOptions = {| +timeout?: number |};
140
+ export type TestOptions = {| readonly timeout?: number |};
136
141
 
137
142
  /**
138
143
  * The `describe` API, and its modifiers.
@@ -155,13 +160,11 @@ function suiteApi(): $FlowFixMe {
155
160
  api.todo = (name: string, body?: Body) => {
156
161
  addSuite(name, body ?? (() => {}), "todo");
157
162
  };
158
- api.each =
159
- (table: $ReadOnlyArray<mixed>) =>
160
- (name: string, body: (row: mixed) => mixed) => {
161
- for (const row of table) {
162
- addSuite(formatRow(name, row), () => body(row), "none");
163
- }
164
- };
163
+ api.each = (table: $ReadOnlyArray<mixed>) => (name: string, body: (row: mixed) => mixed) => {
164
+ for (const row of table) {
165
+ addSuite(formatRow(name, row), () => body(row), "none");
166
+ }
167
+ };
165
168
  return api;
166
169
  }
167
170