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

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,251 @@
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
+ // What is *not* here is as deliberate. `uft.mock` intercepts a module before it
22
+ // is imported, which needs the loader rather than the runner, and uf's loader is
23
+ // `@uniflowed/host` — so it is a real piece of work rather than a wrapper, and
24
+ // it is not pretended at here. A missing binding throws with what it would take;
25
+ // a binding that silently did nothing would be worse than not having it.
26
+
27
+ import { clearAllMocks, fn, resetAllMocks, restoreAllMocks, spyOn } from "./spy.js";
28
+ import * as timers from "./timers.js";
29
+
30
+ /** Environment variables `stubEnv` replaced, and what they were. */
31
+ const stubbedEnv: Map<string, string | void> = new Map();
32
+
33
+ /** Globals `stubGlobal` replaced, and what they were. */
34
+ const stubbedGlobals: Map<string, { readonly owned: boolean, readonly value: mixed }> = new Map();
35
+
36
+ /**
37
+ * Raised for a `uft` namespace binding that is not implemented.
38
+ *
39
+ * Names what the binding needs rather than only that it is missing: `uft.mock`
40
+ * is absent because module interception belongs to the loader, and a reader who
41
+ * knows that can decide whether to wait or to restructure the test.
42
+ */
43
+ export class UnsupportedError extends Error {
44
+ /** The binding that was called. */
45
+ binding: string;
46
+
47
+ constructor(binding: string, reason: string) {
48
+ super(`uft.${binding} is not implemented yet: ${reason}`);
49
+ this.name = "UnsupportedError";
50
+ this.binding = binding;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Read the process environment, whichever host this is.
56
+ *
57
+ * Node, Deno and Bun all expose `process.env`; Deno also has `Deno.env`, and
58
+ * reaching for `process` first keeps one code path across the three.
59
+ */
60
+ function environment(): { [string]: string } | null {
61
+ const host = globalThis as $FlowFixMe;
62
+ return host.process?.env ?? null;
63
+ }
64
+
65
+ /**
66
+ * Replace an environment variable for the rest of the test.
67
+ *
68
+ * Undone by `unstubAllEnvs`, which the runner calls between files — a stub that
69
+ * outlived its test would be a test that passes alone and fails in a suite.
70
+ */
71
+ export function stubEnv(name: string, value: string | void): void {
72
+ const env = environment();
73
+ if (env == null) {
74
+ throw new UnsupportedError("stubEnv", "this host exposes no process environment");
75
+ }
76
+ if (!stubbedEnv.has(name)) {
77
+ stubbedEnv.set(name, Object.hasOwn(env, name) ? env[name] : undefined);
78
+ }
79
+ if (value === undefined) {
80
+ delete env[name];
81
+ } else {
82
+ env[name] = value;
83
+ }
84
+ }
85
+
86
+ /** Put every environment variable `stubEnv` replaced back. */
87
+ export function unstubAllEnvs(): void {
88
+ const env = environment();
89
+ if (env == null) {
90
+ stubbedEnv.clear();
91
+ return;
92
+ }
93
+ for (const [name, previous] of stubbedEnv) {
94
+ if (previous === undefined) {
95
+ delete env[name];
96
+ } else {
97
+ env[name] = previous;
98
+ }
99
+ }
100
+ stubbedEnv.clear();
101
+ }
102
+
103
+ /**
104
+ * Replace a global for the rest of the test.
105
+ *
106
+ * Whether the global was the object's own property is recorded, because putting
107
+ * back an inherited one by assignment would leave a copy that shadows whatever
108
+ * it was inherited from.
109
+ */
110
+ export function stubGlobal(name: string, value: mixed): void {
111
+ const host = globalThis as $FlowFixMe;
112
+ if (!stubbedGlobals.has(name)) {
113
+ stubbedGlobals.set(name, {
114
+ owned: Object.hasOwn(host, name),
115
+ value: host[name],
116
+ });
117
+ }
118
+ host[name] = value;
119
+ }
120
+
121
+ /** Put every global `stubGlobal` replaced back. */
122
+ export function unstubAllGlobals(): void {
123
+ const host = globalThis as $FlowFixMe;
124
+ for (const [name, previous] of stubbedGlobals) {
125
+ if (previous.owned) {
126
+ host[name] = previous.value;
127
+ } else {
128
+ delete host[name];
129
+ }
130
+ }
131
+ stubbedGlobals.clear();
132
+ }
133
+
134
+ /** How often `waitFor` re-runs its body while it is failing. */
135
+ const WAIT_INTERVAL_MS = 20;
136
+
137
+ /** How long `waitFor` keeps trying before giving up. */
138
+ const WAIT_TIMEOUT_MS = 1_000;
139
+
140
+ /**
141
+ * Run `body` until it stops throwing, or the timeout passes.
142
+ *
143
+ * The last failure is what is raised, not a timeout — "expected 2, got 1" says
144
+ * what went wrong, and "timed out" says only that something did.
145
+ */
146
+ export async function waitFor<T>(
147
+ body: () => T | Promise<T>,
148
+ options?: { readonly timeout?: number, readonly interval?: number },
149
+ ): Promise<T> {
150
+ const timeout = options?.timeout ?? WAIT_TIMEOUT_MS;
151
+ const interval = options?.interval ?? WAIT_INTERVAL_MS;
152
+ const deadline = Date.now() + timeout;
153
+ let last: mixed = null;
154
+
155
+ for (;;) {
156
+ try {
157
+ return await body();
158
+ } catch (thrown) {
159
+ last = thrown;
160
+ }
161
+ if (Date.now() >= deadline) {
162
+ throw last ?? new Error(`uft.waitFor: gave up after ${timeout}ms`);
163
+ }
164
+ await new Promise((resolve) => setTimeout(resolve, interval));
165
+ }
166
+ }
167
+
168
+ /** Run `body` until it returns something truthy, or the timeout passes. */
169
+ export async function waitUntil(
170
+ body: () => mixed | Promise<mixed>,
171
+ options?: { readonly timeout?: number, readonly interval?: number },
172
+ ): Promise<mixed> {
173
+ return waitFor(async () => {
174
+ const value = await body();
175
+ if (value == null || value === false) {
176
+ throw new Error("uft.waitUntil: the condition is not true yet");
177
+ }
178
+ return value;
179
+ }, options);
180
+ }
181
+
182
+ /**
183
+ * Hand a value back with its mock methods visible to the type checker.
184
+ *
185
+ * Purely a type-level convenience, exactly as in Vitest: at runtime it is the
186
+ * identity function, and its whole job is letting a test write
187
+ * `uft.mocked(client.send).mockReturnValue(…)` without a cast.
188
+ */
189
+ export function mocked<T>(value: T): $FlowFixMe {
190
+ return value;
191
+ }
192
+
193
+ /** Not implemented, and specific about what it would take. */
194
+ function unsupported(binding: string, reason: string): () => empty {
195
+ return () => {
196
+ throw new UnsupportedError(binding, reason);
197
+ };
198
+ }
199
+
200
+ /** The reason every module-interception binding is absent. */
201
+ const NEEDS_LOADER =
202
+ "intercepting a module before it is imported belongs to the loader " +
203
+ "(`@uniflowed/host`), not to the runner, and uf has not wired it yet";
204
+
205
+ /**
206
+ * The `uft` namespace.
207
+ *
208
+ * A frozen object rather than a class: it is a namespace, nothing about it is
209
+ * per-instance, and freezing it means a test cannot leave a monkey-patch behind
210
+ * for the next one.
211
+ */
212
+ export const uft: $FlowFixMe = Object.freeze({
213
+ fn,
214
+ spyOn,
215
+ mocked,
216
+
217
+ clearAllMocks,
218
+ resetAllMocks,
219
+ restoreAllMocks,
220
+
221
+ stubEnv,
222
+ unstubAllEnvs,
223
+ stubGlobal,
224
+ unstubAllGlobals,
225
+
226
+ waitFor,
227
+ waitUntil,
228
+
229
+ // The clock a test controls. A test about "after five minutes the session
230
+ // expires" should not take five minutes.
231
+ useFakeTimers: timers.useFakeTimers,
232
+ useRealTimers: timers.useRealTimers,
233
+ isFakeTimers: timers.isFaked,
234
+ advanceTimersByTime: timers.advanceTimersByTime,
235
+ advanceTimersByTimeAsync: timers.advanceTimersByTimeAsync,
236
+ advanceTimersToNextTimer: timers.advanceTimersToNextTimer,
237
+ runAllTimers: timers.runAllTimers,
238
+ runOnlyPendingTimers: timers.runOnlyPendingTimers,
239
+ getTimerCount: timers.getTimerCount,
240
+ setSystemTime: timers.setSystemTime,
241
+ getMockedSystemTime: timers.getMockedSystemTime,
242
+
243
+ // Module interception. Absent rather than faked; see `NEEDS_LOADER`.
244
+ mock: unsupported("mock", NEEDS_LOADER),
245
+ doMock: unsupported("doMock", NEEDS_LOADER),
246
+ unmock: unsupported("unmock", NEEDS_LOADER),
247
+ doUnmock: unsupported("doUnmock", NEEDS_LOADER),
248
+ importActual: unsupported("importActual", NEEDS_LOADER),
249
+ importMock: unsupported("importMock", NEEDS_LOADER),
250
+ resetModules: unsupported("resetModules", NEEDS_LOADER),
251
+ });
@@ -0,0 +1,260 @@
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` says which case is running so a chunk can be named.
25
+ // "Who printed this" is also state with a lifetime of its own — set around a
26
+ // case's body and hooks, cleared between them — exactly like the snapshot key
27
+ // in `snapshot.js`, and for the same reason it lives beside the thing it
28
+ // describes rather than inside either caller.
29
+ //
30
+ // # What "the test that printed it" means
31
+ //
32
+ // The name a chunk carries is the case the *worker* is running when the chunk
33
+ // arrives, which is not the same as the case whose code produced it. A
34
+ // `setTimeout` a test leaves behind prints while the next case is running and
35
+ // is filed under that one; a chunk from no case at all is filed under the
36
+ // file.
37
+ //
38
+ // Getting this exactly right needs the printing to be tied to the asynchronous
39
+ // context the case ran in — `AsyncLocalStorage` and everything under it — and
40
+ // that is a bigger change than this module, because it has to reach the
41
+ // scheduler that runs the cases. It is written down here rather than left to
42
+ // be discovered from a confusing report. See ubugeeei-prod/uf#207.
43
+
44
+ // # Bounds
45
+ //
46
+ // A test that prints in a loop must not be able to fill the pipe, the worker's
47
+ // memory or the report, so one file's captured output is bounded and the
48
+ // bound is announced rather than hidden: the chunk that reaches it says so and
49
+ // nothing after it is kept. The budget starts over for each file, so a chatty
50
+ // file does not silence the next one in the same worker.
51
+
52
+ import { format, inspect } from "node:util";
53
+
54
+ import { userFrames } from "./frames.js";
55
+
56
+ /** Which of the process's two streams a chunk was written to. */
57
+ export type OutputStream = "stdout" | "stderr";
58
+
59
+ /** One thing a test — or the file around it — printed. */
60
+ export type OutputChunk = {|
61
+ readonly stream: OutputStream,
62
+ /** Full name of the case that was running, or `null` when none was. */
63
+ readonly test: string | null,
64
+ /** The text as it would have reached the terminal, newline included. */
65
+ readonly text: string,
66
+ |};
67
+
68
+ /** Where captured output goes. */
69
+ export type OutputSink = (chunk: OutputChunk) => void;
70
+
71
+ /**
72
+ * Longest single write kept, in UTF-16 code units.
73
+ *
74
+ * A test that prints a megabyte-long serialised fixture meant to print
75
+ * something; the first few kilobytes of it are what says what happened, and
76
+ * the rest is not a report's to carry.
77
+ */
78
+ export const MAX_CHUNK_LENGTH: number = 8 * 1024;
79
+
80
+ /** Most output kept from one file, in UTF-16 code units. */
81
+ export const MAX_FILE_LENGTH: number = 128 * 1024;
82
+
83
+ /** Which stream each replaced `console` method writes to, as Node routes them. */
84
+ const CONSOLE_STREAMS: { readonly [string]: OutputStream } = {
85
+ log: "stdout",
86
+ info: "stdout",
87
+ debug: "stdout",
88
+ warn: "stderr",
89
+ error: "stderr",
90
+ };
91
+
92
+ /** Decoder for a `write` that was handed bytes rather than a string. */
93
+ const DECODER = new TextDecoder();
94
+
95
+ /** What a stream's `write` calls when it has taken the chunk. */
96
+ type WriteCallback = () => mixed;
97
+
98
+ let sink: OutputSink | null = null;
99
+ let raw: ((chunk: string) => void) | null = null;
100
+ let current: string | null = null;
101
+ let captured = 0;
102
+ let stopped = false;
103
+
104
+ /**
105
+ * The globals to patch, in one place.
106
+ *
107
+ * The one untyped expression in this module, and it is the trust boundary
108
+ * itself: `console` and `process.stdout` are the host's, their libdef types
109
+ * are read-only, and replacing them is exactly what this module is for.
110
+ */
111
+ function host(): $FlowFixMe {
112
+ return globalThis;
113
+ }
114
+
115
+ /**
116
+ * `args` rendered the way `console.log` renders them.
117
+ *
118
+ * Node's console hands its arguments to `util.format`, and this is that in two
119
+ * cases rather than one, because `format`'s first parameter is the template:
120
+ * with a leading string it substitutes `%s`, `%d`, `%o` and the rest and
121
+ * inspects whatever is left over, and with anything else there is no template
122
+ * to substitute into, so every argument is inspected and the results joined
123
+ * with a space. Both are what Node prints.
124
+ */
125
+ function formatArguments(args: $ReadOnlyArray<mixed>): string {
126
+ const [first, ...rest] = args;
127
+ if (typeof first === "string") {
128
+ return format(first, ...rest);
129
+ }
130
+ return args.map((value) => (typeof value === "string" ? value : inspect(value))).join(" ");
131
+ }
132
+
133
+ /** The text of one `write` argument, whether it arrived as bytes or a string. */
134
+ function textOf(chunk: mixed): string {
135
+ if (typeof chunk === "string") {
136
+ return chunk;
137
+ }
138
+ if (chunk instanceof Uint8Array) {
139
+ return DECODER.decode(chunk);
140
+ }
141
+ return String(chunk);
142
+ }
143
+
144
+ /**
145
+ * Record one piece of output, within the file's budget.
146
+ *
147
+ * Silently doing nothing when no sink is installed is deliberate: a test that
148
+ * prints must never fail because of how it was run.
149
+ */
150
+ function capture(stream: OutputStream, text: string): void {
151
+ const to = sink;
152
+ if (to == null || stopped || text === "") {
153
+ return;
154
+ }
155
+ let kept = text.length > MAX_CHUNK_LENGTH ? `${text.slice(0, MAX_CHUNK_LENGTH)}…\n` : text;
156
+ const room = MAX_FILE_LENGTH - captured;
157
+ if (kept.length >= room) {
158
+ kept = `${kept.slice(0, Math.max(room, 0))}\n[uf] output stopped after ${MAX_FILE_LENGTH} characters\n`;
159
+ stopped = true;
160
+ }
161
+ captured += kept.length;
162
+ to({ stream, test: current, text: kept });
163
+ }
164
+
165
+ /** A stand-in for `process.stdout.write` / `process.stderr.write`. */
166
+ function writer(
167
+ stream: OutputStream,
168
+ ): (chunk: mixed, encoding?: string | WriteCallback, callback?: WriteCallback) => boolean {
169
+ return (chunk, encoding, callback) => {
170
+ capture(stream, textOf(chunk));
171
+ // `write(chunk, callback)` and `write(chunk, encoding, callback)` are both
172
+ // real calls, and a caller that passed a callback is waiting for it.
173
+ //
174
+ // Deferred, because the real `Writable.write` never calls back before it
175
+ // returns: a caller that writes and then does something on the next line
176
+ // has that line run first, and one whose callback ran inline would see the
177
+ // two in the other order. `queueMicrotask` rather than `process.nextTick`
178
+ // so this holds on every host uf supports.
179
+ const done = typeof encoding === "function" ? encoding : callback;
180
+ if (done != null) {
181
+ queueMicrotask(done);
182
+ }
183
+ // The real method returns whether the stream has room for more. Nothing is
184
+ // buffered here, so it always has — and a caller told otherwise would wait
185
+ // for a `drain` that never comes.
186
+ return true;
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Route everything a test prints to `to` instead of to the process's streams,
192
+ * and return the real `process.stdout.write` that was replaced.
193
+ *
194
+ * The caller gets the raw stream because the caller is the protocol, and a
195
+ * protocol sharing its stream with the code it reports on is the bug this
196
+ * module exists for. Handing it back from here rather than letting the caller
197
+ * read it first is what makes "taken before anything could have replaced it"
198
+ * true by construction.
199
+ *
200
+ * Installed once, for the life of the worker: a worker runs many files, and
201
+ * restoring the real methods between them would leave a window in which a
202
+ * straggling `setTimeout` from the previous file writes into the protocol.
203
+ */
204
+ export function install(to: OutputSink): (chunk: string) => void {
205
+ const global = host();
206
+ const already = raw;
207
+ if (already != null) {
208
+ return already;
209
+ }
210
+ const stdout = global.process.stdout;
211
+ const real = stdout.write;
212
+ const protocol = (chunk: string) => {
213
+ real.call(stdout, chunk);
214
+ };
215
+ raw = protocol;
216
+ sink = to;
217
+ for (const method of Object.keys(CONSOLE_STREAMS)) {
218
+ const stream = CONSOLE_STREAMS[method];
219
+ global.console[method] = (...args: $ReadOnlyArray<mixed>) => {
220
+ capture(stream, `${formatArguments(args)}\n`);
221
+ };
222
+ }
223
+ global.console.trace = (...args: $ReadOnlyArray<mixed>) => {
224
+ // `console.trace` is a message *and* the stack under it, which is the
225
+ // whole reason to call it rather than `console.error`. `Error.stack`
226
+ // opens with `Trace: <message>`, so the trace Node prints is that string
227
+ // with this module's own frames taken off it.
228
+ const error = new Error(formatArguments(args));
229
+ error.name = "Trace";
230
+ capture("stderr", `${userFrames(error.stack) ?? `Trace: ${error.message}`}\n`);
231
+ };
232
+
233
+ global.process.stdout.write = writer("stdout");
234
+ global.process.stderr.write = writer("stderr");
235
+ return protocol;
236
+ }
237
+
238
+ /**
239
+ * Say which case is running, so what it prints can be named.
240
+ *
241
+ * The runner calls this around a case's body and hooks and clears it between
242
+ * them: output written while the module is being imported, from a `beforeAll`,
243
+ * or after the last case finished belongs to the file, not to whichever case
244
+ * happened to run last.
245
+ */
246
+ export function enterTest(name: string): void {
247
+ current = name;
248
+ }
249
+
250
+ /** Say that no case is running. */
251
+ export function exitTest(): void {
252
+ current = null;
253
+ }
254
+
255
+ /** Start one file's output budget over, with no case running. */
256
+ export function startFile(): void {
257
+ captured = 0;
258
+ stopped = false;
259
+ current = null;
260
+ }
@@ -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