@uniflowed/test 0.0.0-alpha.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,231 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/test`: what a file is allowed to leave behind.
4
+ //
5
+ // A worker serves many files out of one process, one at a time (`../worker.js`
6
+ // says why). Everything a file registers lives in this package and is cleared
7
+ // with it; everything a file *reaches around* the package to change belongs to
8
+ // the process, outlives the file, and is handed to whichever file the schedule
9
+ // puts next in that worker.
10
+ //
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
+ //
16
+ // * ubugeeei-prod/uf#417, `uft.stubEnv("NODE_ENV", …)` still set for the
17
+ // next file;
18
+ // * ubugeeei-prod/uf#581, a fake clock still installed, so the next file's
19
+ // `setTimeout` — including the one each case is raced against — never
20
+ // fired and the file hung with nothing on screen;
21
+ // * ubugeeei-prod/uf#607, `document.body` still holding the markup a
22
+ // hydration test wrote into it, so the next file's "there is one image on
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`).
28
+ //
29
+ // Which files share a worker is decided by `.uf/test-timings.json`, so a leak
30
+ // makes the *result* of a suite depend on how the machine was loaded the last
31
+ // time it ran. That is the failure this module exists to end, and the reason
32
+ // it is a list in one place rather than three calls in `../worker.js`: the
33
+ // question "what else does a file share with the next one" now has somewhere
34
+ // to be answered, and a fourth answer is one entry rather than one more thing
35
+ // to remember.
36
+ //
37
+ // # What belongs here
38
+ //
39
+ // State that (a) the process shares, (b) a test can change from inside a file,
40
+ // and (c) nothing else puts back. Anything a file merely *reads* does not
41
+ // belong here, and neither does anything the registry already clears — a spy
42
+ // is not on this list because `reset()` is what a spy lives in.
43
+ //
44
+ // # When it runs
45
+ //
46
+ // Before the next file is imported, not after the previous one is run. A file
47
+ // that throws while loading is still a file that has run code, and it still
48
+ // hands the next one whatever that code changed; putting things back on the
49
+ // way *in* covers the load failure and the crash as well as the ordinary end.
50
+ // It also means the first file in a worker starts from the same state as the
51
+ // tenth.
52
+
53
+ import { reset } from "./registry.js";
54
+ import { resetModuleState } from "./modules.js";
55
+ import { unstubAllEnvs, unstubAllGlobals } from "./namespace.js";
56
+ // Renamed at the door, for two reasons that agree. It reads as the resets
57
+ // beside it do — `reset`, `unstubAllEnvs`, `resetModuleState` are all
58
+ // verb-first, and so is what this does to the clock.
59
+ import { restoreRealClock } from "./timers.js";
60
+
61
+ /**
62
+ * One piece of process-wide state a file can change, and how to put it back.
63
+ *
64
+ * `what` is written for a person reading this list rather than for any code:
65
+ * nothing branches on it, and it is here because a list of five bare function
66
+ * references is a list nobody can check against the paragraph above it.
67
+ */
68
+ type Shared = {|
69
+ readonly what: string,
70
+ readonly restore: () => void,
71
+ |};
72
+
73
+ /**
74
+ * Everything a file shares with the file after it, in the order it goes back.
75
+ *
76
+ * The order matters in one place and is harmless everywhere else: the clock
77
+ * goes back before the module stand-ins do, because a stand-in's factory runs
78
+ * on the next import and a factory that schedules anything under a leaked fake
79
+ * clock would schedule it into a clock nobody is going to advance.
80
+ */
81
+ const SHARED: $ReadOnlyArray<Shared> = [
82
+ {
83
+ what: "the tests, hooks and spies this package registered",
84
+ restore: reset,
85
+ },
86
+ {
87
+ // `process.env` belongs to the process. `uft.stubEnv("NODE_ENV",
88
+ // "production")` in one file is still set when the next one imports, and
89
+ // the file that fails is the one that read it.
90
+ what: "environment variables `uft.stubEnv` replaced",
91
+ restore: unstubAllEnvs,
92
+ },
93
+ {
94
+ // `globalThis` likewise, and worse: a stubbed `fetch` makes the next file
95
+ // talk to a stand-in that does not know about it.
96
+ what: "globals `uft.stubGlobal` replaced",
97
+ restore: unstubAllGlobals,
98
+ },
99
+ {
100
+ // Worse than a leaked value, and worse in a way that hides it. A leaked
101
+ // stub makes the next file read something wrong, which arrives as an
102
+ // assertion naming the value. A leaked clock makes the next file's
103
+ // `setTimeout` never fire — including the one `withTimeout` races each
104
+ // case against — so the file hangs with nothing on screen until `uf`'s own
105
+ // deadline kills the worker, and the report names the file that waited
106
+ // rather than the file that stopped time.
107
+ what: "the clock, whatever `uft.useFakeTimers` did to it",
108
+ restore: restoreRealClock,
109
+ },
110
+ {
111
+ // A worker serves many files out of one module registry, so this is the
112
+ // difference between "one file at a time" and "one file's mocks at a
113
+ // time".
114
+ what: "modules `uft.mock` stood in for",
115
+ restore: resetModuleState,
116
+ },
117
+ {
118
+ what: "the document, if this process has one",
119
+ restore: restoreDocument,
120
+ },
121
+ ];
122
+
123
+ /**
124
+ * Hand the next file a document nobody else has written to.
125
+ *
126
+ * The document is process-wide in a way that is easy to miss, because nothing
127
+ * in this package installs it: `@uniflowed/react-testing` puts one on the
128
+ * global object the first time a test renders and deliberately keeps it for
129
+ * the life of the process — replacing it would strand every React root already
130
+ * mounted in the old one. So one document serves every file a worker runs, and
131
+ * what a file leaves in it is what the next file queries.
132
+ *
133
+ * Its *contents* are put back rather than the document itself, and "back"
134
+ * means empty: a file is handed the body it would have had if it had installed
135
+ * the document itself. `cleanup()` already unmounts what `render` mounted, and
136
+ * that is not the leak — the leak is markup a test wrote into the body by
137
+ * hand, which a hydration test must do because hydration is React attaching to
138
+ * markup that is already there. `rsc-split.test.js` and `streaming.test.js`
139
+ * both `replaceChildren` into the body, and the file after them in that worker
140
+ * started with somebody else's page.
141
+ *
142
+ * Read through `globalThis` and guarded by `typeof`, because a worker running
143
+ * a suite that never renders has no `document` at all and must not pay for
144
+ * one — and because on a host that *is* a browser this is the page, whose body
145
+ * a run of `uf test` has no business emptying. It only ever clears a body that
146
+ * a test process is using as scratch space, which is every case this can
147
+ * reach: the shim's document, or a page the project chose to run its own tests
148
+ * in.
149
+ */
150
+ function restoreDocument(): void {
151
+ if (typeof globalThis.document === "undefined") {
152
+ return;
153
+ }
154
+ const body = globalThis.document.body;
155
+ if (body == null) {
156
+ // A document a parser produced need not have one, and a document with no
157
+ // body is a document with nothing to put back.
158
+ return;
159
+ }
160
+ body.replaceChildren();
161
+ // The attributes too, and not for tidiness: `<body class="dark">` is how a
162
+ // theme test says what it is testing, and a file that leaves one behind
163
+ // makes the next file's "the page is in light mode" false for a reason it
164
+ // cannot see. Read into an array first — removing an attribute while
165
+ // iterating a live list is the loop that skips every other entry.
166
+ for (const name of [...body.getAttributeNames()]) {
167
+ body.removeAttribute(name);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Where other packages register process-wide state of their own.
173
+ *
174
+ * A package this one does not depend on can still install something a file
175
+ * changes and the next file reads: `@uniflowed/react-testing` installs a window
176
+ * on the first render and keeps it for the process. Only that package knows
177
+ * what the window looked like when it made it, so it registers how to put it
178
+ * back — a name for the message below, and a function — in a `Map` under this
179
+ * symbol, and every entry runs after the list above.
180
+ *
181
+ * A symbol from the global registry rather than an export, so the package that
182
+ * registers needs no import of this one: under another runner the entry is
183
+ * never read and costs nothing.
184
+ */
185
+ const SHARED_STATE: symbol = Symbol.for("@uniflowed/test/shared-state");
186
+
187
+ /** Every entry another package registered, as this module's own entries are shaped. */
188
+ function registeredElsewhere(): $ReadOnlyArray<Shared> {
189
+ const registry: mixed = Reflect.get(globalThis, SHARED_STATE);
190
+ if (!(registry instanceof Map)) {
191
+ return [];
192
+ }
193
+ const entries: Array<Shared> = [];
194
+ for (const [what, restore] of registry) {
195
+ if (typeof what === "string" && typeof restore === "function") {
196
+ entries.push({ what, restore: () => void restore() });
197
+ }
198
+ }
199
+ return entries;
200
+ }
201
+
202
+ /**
203
+ * Put back everything the file that just ran may have changed.
204
+ *
205
+ * Called by `../worker.js` before it imports the next file. Nothing here
206
+ * reports a file for having changed any of this: a file is *allowed* to — that
207
+ * is what the `uft` namespace is for — and the contract is that the change
208
+ * does not outlive the file, not that it never happened.
209
+ *
210
+ * Every entry is attempted even after one has thrown, and the first failure is
211
+ * raised afterwards under the name of what it could not put back. Stopping at
212
+ * the first would leave the four entries behind it un-restored, which is the
213
+ * defect this module exists to prevent arriving by a new route — and a bare
214
+ * throw from one of these used to say only that something in the runner failed
215
+ * between two files.
216
+ */
217
+ export function restoreSharedState(): void {
218
+ let failure: { readonly what: string, readonly thrown: mixed } | null = null;
219
+ for (const shared of [...SHARED, ...registeredElsewhere()]) {
220
+ try {
221
+ shared.restore();
222
+ } catch (thrown) {
223
+ failure ??= { what: shared.what, thrown };
224
+ }
225
+ }
226
+ if (failure != null) {
227
+ const cause = failure.thrown;
228
+ const message = cause instanceof Error ? cause.message : String(cause);
229
+ throw new Error(`@uniflowed/test could not put back ${failure.what}: ${message}`);
230
+ }
231
+ }
@@ -50,8 +50,11 @@ const stubbedGlobals: Map<string, { readonly owned: boolean, readonly value: mix
50
50
  /**
51
51
  * Read the process environment, whichever host this is.
52
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.
53
+ * Node and Bun expose `process` as a global; **Deno does not**, and has the
54
+ * same object under `node:process`. `../worker.js` installs it on the global
55
+ * before anything here runs, which is what keeps this one code path across the
56
+ * three — and is why the `null` branch below is still reachable, for a host
57
+ * that is neither.
55
58
  */
56
59
  function environment(): { [string]: string } | null {
57
60
  const host = globalThis as $FlowFixMe;
@@ -59,10 +62,15 @@ function environment(): { [string]: string } | null {
59
62
  }
60
63
 
61
64
  /**
62
- * Replace an environment variable for the rest of the test.
65
+ * Replace an environment variable for the rest of the file.
63
66
  *
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.
67
+ * The file, and not the test: `process.env` belongs to the process, so a stub
68
+ * stands until something puts it back. `./worker.js` does that between files,
69
+ * beside the spy registry it clears for the same reason — a worker serves many
70
+ * files, and a stub that outlived its file would be a test that passes because
71
+ * of another one, in a suite where which files share a worker is decided by a
72
+ * timings file. A case that wants a narrower scope calls `unstubAllEnvs` in an
73
+ * `afterEach`, which is also what makes the scope visible to a reader.
66
74
  */
67
75
  export function stubEnv(name: string, value: string | void): void {
68
76
  const env = environment();
@@ -97,7 +105,11 @@ export function unstubAllEnvs(): void {
97
105
  }
98
106
 
99
107
  /**
100
- * Replace a global for the rest of the test.
108
+ * Replace a global for the rest of the file.
109
+ *
110
+ * Undone by `unstubAllGlobals`, which `./worker.js` calls between files, for
111
+ * the reason [`stubEnv`] above gives: `globalThis` outlives every file that
112
+ * writes to it.
101
113
  *
102
114
  * Whether the global was the object's own property is recorded, because putting
103
115
  * back an inherited one by assignment would leave a copy that shadows whatever
@@ -214,8 +226,8 @@ export type Uft = {
214
226
  readonly waitFor: typeof waitFor,
215
227
  readonly waitUntil: typeof waitUntil,
216
228
 
217
- readonly useFakeTimers: typeof timers.useFakeTimers,
218
- readonly useRealTimers: typeof timers.useRealTimers,
229
+ readonly useFakeTimers: typeof timers.installFakeClock,
230
+ readonly useRealTimers: typeof timers.restoreRealClock,
219
231
  readonly isFakeTimers: typeof timers.isFaked,
220
232
  readonly advanceTimersByTime: typeof timers.advanceTimersByTime,
221
233
  readonly advanceTimersByTimeAsync: typeof timers.advanceTimersByTimeAsync,
@@ -261,8 +273,8 @@ export const uft: Uft = Object.freeze({
261
273
 
262
274
  // The clock a test controls. A test about "after five minutes the session
263
275
  // expires" should not take five minutes.
264
- useFakeTimers: timers.useFakeTimers,
265
- useRealTimers: timers.useRealTimers,
276
+ useFakeTimers: timers.installFakeClock,
277
+ useRealTimers: timers.restoreRealClock,
266
278
  isFakeTimers: timers.isFaked,
267
279
  advanceTimersByTime: timers.advanceTimersByTime,
268
280
  advanceTimersByTimeAsync: timers.advanceTimersByTimeAsync,
@@ -0,0 +1,14 @@
1
+ // @flow
2
+ // The facade returns the outer uf worker's registry, not another test runner.
3
+ const environment: $FlowFixMe = globalThis;
4
+ const api = environment.__UF_NATIVE_TEST_API__;
5
+ export const test = api.test;
6
+ export const it = api.it;
7
+ export const describe = api.describe;
8
+ export const beforeAll = api.beforeAll;
9
+ export const afterAll = api.afterAll;
10
+ export const beforeEach = api.beforeEach;
11
+ export const afterEach = api.afterEach;
12
+ export const expect = api.expect;
13
+ export const fn = api.fn;
14
+ export const spyOn = api.spyOn;
@@ -0,0 +1,97 @@
1
+ // @flow
2
+ // Jest's module environment executes RN's own mocks. uf still owns discovery,
3
+ // scheduling, hooks, timeouts, isolation and the result protocol.
4
+
5
+ import { createRequire } from "node:module";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import * as tests from "../index.js";
9
+
10
+ // Optional, application-owned environment modules cross one dynamic boundary.
11
+ type EnvironmentModule = $FlowFixMe;
12
+
13
+ export async function loadNativeFile(file: string): Promise<() => Promise<void>> {
14
+ const rootDir = process.cwd();
15
+ const require = createRequire(path.join(rootDir, "package.json"));
16
+ const load = (name: string): EnvironmentModule => {
17
+ try {
18
+ return require(name);
19
+ } catch (cause) {
20
+ throw new Error(
21
+ `uf test (react-native): install ${name} in the application; the native environment could not load it`,
22
+ { cause },
23
+ );
24
+ }
25
+ };
26
+ const Runtime = load("jest-runtime").default;
27
+ const { readConfig } = load("jest-config");
28
+ const { createScriptTransformer } = load("@jest/transform");
29
+ const { jestExpect } = load("@jest/expect");
30
+ const { projectConfig: config, globalConfig } = await readConfig(
31
+ {
32
+ config: JSON.stringify({
33
+ rootDir,
34
+ preset: "@react-native/jest-preset",
35
+ cacheDirectory: path.join(rootDir, ".uf", "native-test-cache"),
36
+ moduleNameMapper: {
37
+ "^@uniflowed/test$": fileURLToPath(new URL("./native-globals.js", import.meta.url).href),
38
+ },
39
+ transform: {
40
+ "^.+\\.[jt]sx?$": require.resolve("@uniflowed/react-native/test-transformer.cjs"),
41
+ },
42
+ transformIgnorePatterns: [
43
+ "node_modules/(?!((jest-)?react-native|@react-native(-community)?|@react-navigation|@uniflowed|react-native-screens|react-native-safe-area-context)/)",
44
+ ],
45
+ }),
46
+ },
47
+ rootDir,
48
+ );
49
+ const context = await Runtime.createContext(config, {
50
+ console,
51
+ maxWorkers: 1,
52
+ watch: false,
53
+ watchman: false,
54
+ });
55
+ const Environment = load(config.testEnvironment);
56
+ const environment = new (Environment.TestEnvironment ?? Environment)(
57
+ { projectConfig: config, globalConfig },
58
+ { console, testPath: file, docblockPragmas: {} },
59
+ );
60
+ await environment.setup();
61
+ let runtime: EnvironmentModule = null;
62
+ let closed = false;
63
+ const close = async () => {
64
+ if (closed) return;
65
+ closed = true;
66
+ try {
67
+ runtime?.leaveTestCode();
68
+ runtime?.teardown();
69
+ } finally {
70
+ await environment.teardown();
71
+ }
72
+ };
73
+ try {
74
+ const transformer = await createScriptTransformer(config, new Map());
75
+ runtime = new Runtime(
76
+ config,
77
+ environment,
78
+ context.resolver,
79
+ transformer,
80
+ new Map(),
81
+ { collectCoverage: false, collectCoverageFrom: [], coverageProvider: "v8" },
82
+ file,
83
+ globalConfig,
84
+ );
85
+ const api = { ...tests, expect: jestExpect };
86
+ for (const [key, value] of Object.entries(api)) environment.global[key] = value;
87
+ environment.global.__UF_NATIVE_TEST_API__ = api;
88
+ runtime.setGlobalsForRuntime(api);
89
+ runtime.enterTestCode();
90
+ for (const setup of config.setupFiles) runtime.requireModule(setup);
91
+ runtime.requireModule(file);
92
+ return close;
93
+ } catch (error) {
94
+ await close();
95
+ throw error;
96
+ }
97
+ }
@@ -241,6 +241,19 @@ function writer(
241
241
  * Installed once, for the life of the worker: a worker runs many files, and
242
242
  * restoring the real methods between them would leave a window in which a
243
243
  * straggling `setTimeout` from the previous file writes into the protocol.
244
+ *
245
+ * # A page has no stream to take
246
+ *
247
+ * `uf test --browser` runs this same capture inside a page
248
+ * (`./browser/page.js`), and there the whole premise of the returned value is
249
+ * absent: there is no `process.stdout`, so there is nothing for a test to
250
+ * write into by accident, and the protocol's channel is a separate HTTP
251
+ * request rather than a stream anything else can reach. So the two `write`
252
+ * methods are only replaced when there are two `write` methods, and the
253
+ * "raw stream" handed back is a no-op nobody has a use for.
254
+ *
255
+ * Deliberately not a `typeof process` check at each use: the question is asked
256
+ * once, here, because the answer cannot change under a running host.
244
257
  */
245
258
  export function install(to: OutputSink): (chunk: string) => void {
246
259
  const global = host();
@@ -248,11 +261,14 @@ export function install(to: OutputSink): (chunk: string) => void {
248
261
  if (already != null) {
249
262
  return already;
250
263
  }
251
- const stdout = global.process.stdout;
252
- const real = stdout.write;
253
- const protocol = (chunk: string) => {
254
- real.call(stdout, chunk);
255
- };
264
+ const stdout = global.process?.stdout;
265
+ const real = stdout?.write;
266
+ const protocol =
267
+ real == null
268
+ ? (_chunk: string) => {}
269
+ : (chunk: string) => {
270
+ real.call(stdout, chunk);
271
+ };
256
272
  raw = protocol;
257
273
  sink = to;
258
274
  for (const method of Object.keys(CONSOLE_STREAMS)) {
@@ -271,8 +287,10 @@ export function install(to: OutputSink): (chunk: string) => void {
271
287
  capture("stderr", `${userFrames(error.stack) ?? `Trace: ${error.message}`}\n`);
272
288
  };
273
289
 
274
- global.process.stdout.write = writer("stdout");
275
- global.process.stderr.write = writer("stderr");
290
+ if (global.process?.stdout != null) {
291
+ global.process.stdout.write = writer("stdout");
292
+ global.process.stderr.write = writer("stderr");
293
+ }
276
294
  return protocol;
277
295
  }
278
296
 
@@ -30,13 +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
- /** One registered test case. */
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,
52
+ readonly skipReason: string | null,
39
53
  readonly timeoutMs: number | null,
54
+ /** How to run it, for a benchmark; `null` for a test. */
55
+ readonly bench: BenchOptions | null,
40
56
  readonly line: number,
41
57
  readonly column: number,
42
58
  |};
@@ -123,14 +139,18 @@ function addCase(
123
139
  body: Body | null,
124
140
  modifier: Modifier,
125
141
  timeoutMs: number | null,
142
+ skipReason: string | null = null,
143
+ bench: BenchOptions | null = null,
126
144
  ): void {
127
145
  const position = callSite();
128
146
  current.children.push({
129
- kind: "test",
147
+ kind: bench == null ? "test" : "bench",
130
148
  name,
131
149
  body,
132
150
  modifier,
151
+ skipReason,
133
152
  timeoutMs,
153
+ bench,
134
154
  line: position.line,
135
155
  column: position.column,
136
156
  });
@@ -179,6 +199,9 @@ function caseApi(): $FlowFixMe {
179
199
  api.skip = (name: string, body?: Body) => {
180
200
  addCase(name, body ?? null, "skip", null);
181
201
  };
202
+ api.skipBecause = (name: string, reason: string, body?: Body) => {
203
+ addCase(name, body ?? null, "skip", null, reason);
204
+ };
182
205
  api.todo = (name: string, body?: Body) => {
183
206
  addCase(name, body ?? null, "todo", null);
184
207
  };
@@ -211,6 +234,34 @@ export const it: $FlowFixMe = caseApi();
211
234
  /** `test` is `it`, for people who write it that way. */
212
235
  export const test: $FlowFixMe = it;
213
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
+
214
265
  /**
215
266
  * Substitute a row into a name, the way every runner spells it: `%s` for the
216
267
  * value, `%j` for its JSON.