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

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,430 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/test`: standing a module in for another one.
4
+ //
5
+ // `uft.spyOn` replaces a method on an object a test can reach. This replaces a
6
+ // *module*, which is the only seam a great deal of a React application has: a
7
+ // component that imports a client and calls it at module scope, a page that
8
+ // imports `useRouter`, a `"use server"` module a client component calls. The
9
+ // mechanism belongs to the loader and lives in `@uniflowed/host`
10
+ // (`module-mocks.js`); what lives here is the API, the resolution of a
11
+ // specifier to the module it names, and the automatic stand-in.
12
+ //
13
+ // # When a mock takes effect
14
+ //
15
+ // **A mock is not hoisted.** Vitest lifts `vi.mock` above the importing file's
16
+ // `import` declarations with a Babel pass; uf has no Babel in its pipeline and
17
+ // `docs/architecture.md` says so on purpose, so inventing one for this would be
18
+ // a compiler pass whose only customer is a test helper. Bun answers the same
19
+ // question the same way, and it is the answer this package gives:
20
+ //
21
+ // * every `import` declaration in a file runs before the file's first
22
+ // statement, so a static import is *never* affected by a `uft.mock` written
23
+ // below it — it already holds the real module;
24
+ // * `uft.mock` takes effect for every import that begins after the promise it
25
+ // returns settles;
26
+ // * so `await import("./client.js")` — after the mock — is how a test reaches
27
+ // the stand-in.
28
+ //
29
+ // A synchronous factory is installed before `uft.mock` returns, which means the
30
+ // rule holds whether or not the promise is awaited. Awaiting is still the habit
31
+ // worth having, because an automatic stand-in has to read the real module first
32
+ // and cannot be installed synchronously at all.
33
+ //
34
+ // # What a mock does not do
35
+ //
36
+ // A module that has already been evaluated is not re-written. Node's registry
37
+ // hands out a namespace that is linked into every importer, and no loader hook
38
+ // can reach back into it — Bun's engine can, and having one API mean two things
39
+ // on two hosts is worse than having it mean the narrower one on both. So a mock
40
+ // affects the next import, not the last one, and `uft.resetModules` is how a
41
+ // test gets a module evaluated again.
42
+
43
+ import {
44
+ actualUrl,
45
+ defineModuleMock,
46
+ installInterception,
47
+ interceptionSupported,
48
+ removeModuleMock,
49
+ resetModuleMocks,
50
+ startModuleEpoch,
51
+ } from "@uniflowed/host/module-mocks";
52
+ import { createRequire } from "node:module";
53
+ import { pathToFileURL } from "node:url";
54
+
55
+ import { UnsupportedError } from "./unsupported.js";
56
+ import { frameFile, isInternalFrame } from "./frames.js";
57
+ import { fn } from "./spy.js";
58
+
59
+ /** The shape of a module's exports, as far as a type can say it. */
60
+ export type ModuleNamespace = { +[string]: mixed };
61
+
62
+ /**
63
+ * What a `uft.mock` factory hands back.
64
+ *
65
+ * `Partial<Module>` rather than `Module` because a partial mock is the common
66
+ * case — replace `send`, keep everything else — and rather than an unconstrained
67
+ * object because the whole point of a Flow-first toolchain doing this is that
68
+ * `{ send: 42 }` for a module whose `send` is a function is a type error at the
69
+ * call rather than a `TypeError` three tests later.
70
+ */
71
+ export type ModuleFactory<Module> = () => Partial<Module> | Promise<Partial<Module>>;
72
+
73
+ /** How deep an automatic stand-in follows nested objects. */
74
+ const AUTOMOCK_DEPTH = 5;
75
+
76
+ /**
77
+ * Why module interception is unavailable here, or `null` when it is available.
78
+ *
79
+ * Named as a reason rather than a boolean because the reason is what a reader
80
+ * needs: "this host has no synchronous module hooks" tells them the suite will
81
+ * work on Node, and "not implemented" tells them nothing.
82
+ */
83
+ export function moduleMockingUnavailable(): string | null {
84
+ return interceptionSupported() ? null : unsupportedReason(hostName());
85
+ }
86
+
87
+ /** The name of the host this process is. */
88
+ export function hostName(): string {
89
+ const host = globalThis as $FlowFixMe;
90
+ if (host.Deno != null) {
91
+ return "Deno";
92
+ }
93
+ if (host.process?.versions?.bun != null) {
94
+ return "Bun";
95
+ }
96
+ return "this host";
97
+ }
98
+
99
+ /** What to tell someone whose host cannot intercept a module. */
100
+ export function unsupportedReason(host: string): string {
101
+ return (
102
+ `replacing a module before it is imported needs synchronous module ` +
103
+ `customization hooks (\`node:module\`'s \`registerHooks\`), and ${host} does ` +
104
+ `not provide them; run the suite on Node, or replace what the module ` +
105
+ `hands out with \`uft.spyOn\` or \`@uniflowed/mock\` instead`
106
+ );
107
+ }
108
+
109
+ /** Raise unless this host can intercept a module. */
110
+ function requireInterception(binding: string): void {
111
+ const reason = moduleMockingUnavailable();
112
+ if (reason != null) {
113
+ throw new UnsupportedError(binding, reason);
114
+ }
115
+ installInterception();
116
+ }
117
+
118
+ /**
119
+ * The module that called into this one.
120
+ *
121
+ * A specifier is relative to the file it was written in, so that file has to be
122
+ * found, and a stack trace is the only place it is recorded. The frames are
123
+ * `--enable-source-maps`'d back to the author's own file, so what comes back is
124
+ * the path of the source rather than of the transform — which is exactly what a
125
+ * relative specifier in that source should resolve against.
126
+ */
127
+ function callerURL(binding: string): string {
128
+ const stack = new Error("uft").stack;
129
+ for (const frame of (stack ?? "").split("\n").slice(1)) {
130
+ if (isInternalFrame(frame)) {
131
+ continue;
132
+ }
133
+ const file = frameFile(frame);
134
+ if (file == null) {
135
+ continue;
136
+ }
137
+ if (file.startsWith("file:")) {
138
+ const url = new URL(file);
139
+ url.search = "";
140
+ url.hash = "";
141
+ return url.href;
142
+ }
143
+ if (file.startsWith("/")) {
144
+ return pathToFileURL(file).href;
145
+ }
146
+ }
147
+ throw new UnsupportedError(
148
+ binding,
149
+ "the calling file could not be read off the stack, so a relative " +
150
+ "specifier has nothing to resolve against; pass an absolute path or a " +
151
+ "`file:` URL",
152
+ );
153
+ }
154
+
155
+ /**
156
+ * The URL a specifier names, resolved from the file that wrote it.
157
+ *
158
+ * A path is resolved as a URL, which is what an ES module specifier is; a bare
159
+ * specifier goes through the package resolver, so `@uniflowed/router` reaches
160
+ * the same file an `import` of it would.
161
+ */
162
+ export function resolveSpecifier(specifier: string, parentURL: string): string {
163
+ if (specifier.startsWith("file:")) {
164
+ return specifier;
165
+ }
166
+ if (isPathSpecifier(specifier)) {
167
+ return new URL(specifier, parentURL).href;
168
+ }
169
+ try {
170
+ return pathToFileURL(createRequire(parentURL).resolve(specifier)).href;
171
+ } catch (error) {
172
+ throw new Error(`uft could not resolve "${specifier}" from ${parentURL}: ${String(error)}`);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Register a stand-in for the module `specifier` names.
178
+ *
179
+ * With a factory, the factory is what the module exports. Without one, the real
180
+ * module is read and every function it exports becomes a spy that records its
181
+ * calls and returns `undefined` — the automatic form, for a module whose shape
182
+ * a test wants to keep and whose behaviour it wants gone.
183
+ */
184
+ export function mock<Module: ModuleNamespace>(
185
+ specifier: string,
186
+ factory?: ModuleFactory<Module>,
187
+ ): Promise<void> {
188
+ requireInterception("mock");
189
+ const url = resolveSpecifier(specifier, callerURL("mock"));
190
+
191
+ if (factory == null) {
192
+ return importURL(actualUrl(url, isPathSpecifier(specifier))).then((actual) => {
193
+ defineModuleMock(url, automock(actual, AUTOMOCK_DEPTH, new Map()));
194
+ });
195
+ }
196
+
197
+ const produced = factory();
198
+ if (isThenable(produced)) {
199
+ return (produced: $FlowFixMe).then((namespace) => {
200
+ defineModuleMock(url, exportsOf(specifier, namespace));
201
+ });
202
+ }
203
+ defineModuleMock(url, exportsOf(specifier, produced));
204
+ return Promise.resolve();
205
+ }
206
+
207
+ /** Stop standing in for the module `specifier` names. */
208
+ export function unmock(specifier: string): void {
209
+ requireInterception("unmock");
210
+ removeModuleMock(resolveSpecifier(specifier, callerURL("unmock")));
211
+ }
212
+
213
+ /**
214
+ * The real module, whatever is registered for it.
215
+ *
216
+ * A separate instance from the one an ordinary import gets while the module is
217
+ * mocked — there is only one URL per module instance, and the mocked one is
218
+ * occupying the other. One instance per module epoch, so two calls in a row
219
+ * hand back the same module.
220
+ *
221
+ * It reaches past the mock for the module it *names* and for no other: the
222
+ * modules that one imports are resolved the way they would be anywhere else,
223
+ * stand-ins included. That is what makes a partial mock possible — the factory
224
+ * asks for the real module while its own stand-in is being built — and it is
225
+ * also why `importActual` of a module that imports a mocked one still sees the
226
+ * stand-in.
227
+ */
228
+ export function importActual<Module: ModuleNamespace>(specifier: string): Promise<Module> {
229
+ requireInterception("importActual");
230
+ const url = resolveSpecifier(specifier, callerURL("importActual"));
231
+ return importURL(actualUrl(url, isPathSpecifier(specifier)));
232
+ }
233
+
234
+ /**
235
+ * The module with every function it exports replaced by a spy.
236
+ *
237
+ * The automatic form of `mock`, without registering anything: what comes back
238
+ * is a stand-in the caller holds, and every other importer of that module still
239
+ * gets whatever it got before.
240
+ */
241
+ export function importMock<Module: ModuleNamespace>(specifier: string): Promise<Module> {
242
+ requireInterception("importMock");
243
+ const url = resolveSpecifier(specifier, callerURL("importMock"));
244
+ return importURL(actualUrl(url, isPathSpecifier(specifier))).then(
245
+ (actual) => (automock(actual, AUTOMOCK_DEPTH, new Map()): $FlowFixMe),
246
+ );
247
+ }
248
+
249
+ /**
250
+ * Evaluate modules again on the next import of them.
251
+ *
252
+ * "Modules" means the ones reached by a path — this project's own. A package is
253
+ * left alone: handing a second copy of `@uniflowed/test` to a file would give
254
+ * it a second registry and a second set of spies, and a reset that did that
255
+ * would break far more than it fixed.
256
+ *
257
+ * It does not run a mock's factory again. The factory ran when `uft.mock` was
258
+ * called; register the mock again to get new stand-ins out of it.
259
+ */
260
+ export function resetModules(): void {
261
+ requireInterception("resetModules");
262
+ startModuleEpoch();
263
+ }
264
+
265
+ /**
266
+ * Forget every mock and every reset.
267
+ *
268
+ * Called by the worker before each file, because a worker serves many files and
269
+ * a mock that outlived its file would be a suite that passes alone and fails
270
+ * beside another.
271
+ */
272
+ export function resetModuleState(): void {
273
+ resetModuleMocks();
274
+ }
275
+
276
+ /**
277
+ * Whether `specifier` names a file rather than a package.
278
+ *
279
+ * The same three prefixes `@uniflowed/host`'s resolve hook tests for, and
280
+ * deliberately not `file:`: a `file:` specifier names the URL it wants, and the
281
+ * one module that writes one — the generated stand-in, reaching back for its
282
+ * values — must land on the copy of the registry that put it there.
283
+ */
284
+ function isPathSpecifier(specifier: string): boolean {
285
+ return specifier.startsWith("./") || specifier.startsWith("../") || specifier.startsWith("/");
286
+ }
287
+
288
+ /** `import()`, in one place, so the marker parameter is never spelled twice. */
289
+ function importURL<Module>(url: string): Promise<Module> {
290
+ return (import(url): $FlowFixMe);
291
+ }
292
+
293
+ /** Whether `value` is a promise, or near enough for `then` to be meant. */
294
+ function isThenable(value: mixed): boolean {
295
+ return (
296
+ value != null &&
297
+ (typeof value === "object" || typeof value === "function") &&
298
+ typeof (value: $FlowFixMe).then === "function"
299
+ );
300
+ }
301
+
302
+ /**
303
+ * The exports a factory produced, as a plain object.
304
+ *
305
+ * Copied rather than kept, so that the export list the loader writes out is
306
+ * fixed at the moment the mock is registered: a factory that hands back an
307
+ * object it goes on adding keys to would otherwise produce a module whose
308
+ * exports depend on when it was first imported.
309
+ */
310
+ function exportsOf(specifier: string, produced: mixed): { [string]: mixed } {
311
+ if (produced == null || (typeof produced !== "object" && typeof produced !== "function")) {
312
+ throw new TypeError(
313
+ `uft.mock("${specifier}"): the factory must return the module's exports as an ` +
314
+ `object, and it returned ${produced === null ? "null" : typeof produced}`,
315
+ );
316
+ }
317
+ return { ...(produced: $FlowFixMe) };
318
+ }
319
+
320
+ /**
321
+ * `value` with its behaviour taken out and its shape left in.
322
+ *
323
+ * The rules, which are the ones a reader has to be able to predict:
324
+ *
325
+ * * a function becomes a spy that records its calls and returns `undefined`,
326
+ * keeping its name and its prototype's methods — so a class stays `new`-able
327
+ * and its methods are spies too;
328
+ * * an array becomes an empty array, because a fixture list a test did not
329
+ * write is a fixture list a test should not depend on;
330
+ * * a plain object is followed, key by key, to [`AUTOMOCK_DEPTH`];
331
+ * * a getter is kept as a getter rather than followed: reading one here would
332
+ * run code the module did not expect to run yet, and would fix an answer
333
+ * the getter exists to recompute;
334
+ * * everything else — a string, a number, a `Map`, a `Date`, a `RegExp` — is
335
+ * kept, because there is nothing in it to call.
336
+ */
337
+ function automock(value: mixed, depth: number, seen: Map<mixed, mixed>): mixed {
338
+ if (value == null || depth <= 0) {
339
+ return value;
340
+ }
341
+ const already = seen.get(value);
342
+ if (already !== undefined) {
343
+ return already;
344
+ }
345
+
346
+ if (typeof value === "function") {
347
+ const spy = fn().mockName((value: $FlowFixMe).name ?? "spy");
348
+ seen.set(value, spy);
349
+ copyProperties(value, spy, depth, seen, false);
350
+ const prototype = (value: $FlowFixMe).prototype;
351
+ if (prototype != null && typeof prototype === "object") {
352
+ // Every own name, not only the enumerable ones: a class's methods are
353
+ // non-enumerable own properties of its prototype, so `Object.keys` finds
354
+ // none of them and the stand-in of a class would have no methods at all.
355
+ copyProperties(prototype, spy.prototype, depth, seen, true);
356
+ }
357
+ return spy;
358
+ }
359
+
360
+ if (typeof value !== "object") {
361
+ return value;
362
+ }
363
+ if (Array.isArray(value)) {
364
+ const empty: Array<mixed> = [];
365
+ seen.set(value, empty);
366
+ return empty;
367
+ }
368
+ if (!isPlainish(value)) {
369
+ return value;
370
+ }
371
+
372
+ const copy: { [string]: mixed } = {};
373
+ seen.set(value, copy);
374
+ copyProperties(value, copy, depth, seen, false);
375
+ return copy;
376
+ }
377
+
378
+ /**
379
+ * Automock the properties of `from` onto `onto`.
380
+ *
381
+ * `hidden` is what separates a prototype from everything else: a class's
382
+ * methods are own but not enumerable, so a prototype needs every own name,
383
+ * while an object or a function's statics want the enumerable ones — the same
384
+ * set a spread or a module namespace would show.
385
+ */
386
+ function copyProperties(
387
+ from: mixed,
388
+ onto: mixed,
389
+ depth: number,
390
+ seen: Map<mixed, mixed>,
391
+ hidden: boolean,
392
+ ): void {
393
+ const source = (from: $FlowFixMe);
394
+ const target = (onto: $FlowFixMe);
395
+ const names = hidden ? Object.getOwnPropertyNames(source) : Object.keys(source);
396
+ for (const name of names) {
397
+ // `constructor` on a prototype points back at the function being mocked,
398
+ // and rewriting it would replace the spy with a spy of the spy.
399
+ if (name === "constructor") {
400
+ continue;
401
+ }
402
+ const descriptor = Object.getOwnPropertyDescriptor(source, name);
403
+ if (descriptor == null) {
404
+ continue;
405
+ }
406
+ if (!Object.hasOwn(descriptor, "value")) {
407
+ Object.defineProperty(target, name, descriptor);
408
+ continue;
409
+ }
410
+ target[name] = automock(descriptor.value, depth - 1, seen);
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Whether an object is one to look inside.
416
+ *
417
+ * A module namespace, an object literal and a `null`-prototype object are; a
418
+ * `Map`, a `Date`, a `RegExp` and anything else with behaviour of its own are
419
+ * not — replacing their methods with spies would produce something that claims
420
+ * to be a `Date` and cannot tell the time.
421
+ */
422
+ function isPlainish(value: mixed): boolean {
423
+ const prototype = Object.getPrototypeOf(value);
424
+ return prototype === null || prototype === Object.prototype || isNamespace(value);
425
+ }
426
+
427
+ /** Whether `value` is a module namespace object. */
428
+ function isNamespace(value: mixed): boolean {
429
+ return (value: $FlowFixMe)[Symbol.toStringTag] === "Module";
430
+ }
@@ -18,14 +18,28 @@
18
18
  // name — two very different things one dot apart. `uft` is three characters,
19
19
  // belongs to nothing else, and is what a reader types a hundred times a file.
20
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.
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.
26
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";
27
36
  import { clearAllMocks, fn, resetAllMocks, restoreAllMocks, spyOn } from "./spy.js";
28
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";
29
43
 
30
44
  /** Environment variables `stubEnv` replaced, and what they were. */
31
45
  const stubbedEnv: Map<string, string | void> = new Map();
@@ -33,29 +47,14 @@ const stubbedEnv: Map<string, string | void> = new Map();
33
47
  /** Globals `stubGlobal` replaced, and what they were. */
34
48
  const stubbedGlobals: Map<string, { readonly owned: boolean, readonly value: mixed }> = new Map();
35
49
 
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
50
  /**
55
51
  * Read the process environment, whichever host this is.
56
52
  *
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.
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.
59
58
  */
60
59
  function environment(): { [string]: string } | null {
61
60
  const host = globalThis as $FlowFixMe;
@@ -63,10 +62,15 @@ function environment(): { [string]: string } | null {
63
62
  }
64
63
 
65
64
  /**
66
- * Replace an environment variable for the rest of the test.
65
+ * Replace an environment variable for the rest of the file.
67
66
  *
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.
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.
70
74
  */
71
75
  export function stubEnv(name: string, value: string | void): void {
72
76
  const env = environment();
@@ -101,7 +105,11 @@ export function unstubAllEnvs(): void {
101
105
  }
102
106
 
103
107
  /**
104
- * 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.
105
113
  *
106
114
  * Whether the global was the object's own property is recorded, because putting
107
115
  * back an inherited one by assignment would leave a copy that shadows whatever
@@ -190,17 +198,54 @@ export function mocked<T>(value: T): $FlowFixMe {
190
198
  return value;
191
199
  }
192
200
 
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
- }
201
+ /**
202
+ * The `uft` namespace's type.
203
+ *
204
+ * Written out member by member rather than left as one `$FlowFixMe`, because
205
+ * the module-mocking half of it is the half a type can genuinely check: a
206
+ * factory that hands back the wrong shape for the module it is standing in for
207
+ * is an error at the call site, and that only works if `uft` has a type at all.
208
+ * The members that were already typed loosely keep the types they have —
209
+ * `typeof` reads them from their definitions, so this list cannot drift from
210
+ * them.
211
+ */
212
+ export type Uft = {
213
+ readonly fn: typeof fn,
214
+ readonly spyOn: typeof spyOn,
215
+ readonly mocked: typeof mocked,
216
+
217
+ readonly clearAllMocks: typeof clearAllMocks,
218
+ readonly resetAllMocks: typeof resetAllMocks,
219
+ readonly restoreAllMocks: typeof restoreAllMocks,
220
+
221
+ readonly stubEnv: typeof stubEnv,
222
+ readonly unstubAllEnvs: typeof unstubAllEnvs,
223
+ readonly stubGlobal: typeof stubGlobal,
224
+ readonly unstubAllGlobals: typeof unstubAllGlobals,
225
+
226
+ readonly waitFor: typeof waitFor,
227
+ readonly waitUntil: typeof waitUntil,
228
+
229
+ readonly useFakeTimers: typeof timers.installFakeClock,
230
+ readonly useRealTimers: typeof timers.restoreRealClock,
231
+ readonly isFakeTimers: typeof timers.isFaked,
232
+ readonly advanceTimersByTime: typeof timers.advanceTimersByTime,
233
+ readonly advanceTimersByTimeAsync: typeof timers.advanceTimersByTimeAsync,
234
+ readonly advanceTimersToNextTimer: typeof timers.advanceTimersToNextTimer,
235
+ readonly runAllTimers: typeof timers.runAllTimers,
236
+ readonly runOnlyPendingTimers: typeof timers.runOnlyPendingTimers,
237
+ readonly getTimerCount: typeof timers.getTimerCount,
238
+ readonly setSystemTime: typeof timers.setSystemTime,
239
+ readonly getMockedSystemTime: typeof timers.getMockedSystemTime,
199
240
 
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";
241
+ readonly mock: typeof mockModule,
242
+ readonly doMock: typeof mockModule,
243
+ readonly unmock: typeof unmockModule,
244
+ readonly doUnmock: typeof unmockModule,
245
+ readonly importActual: typeof importActualModule,
246
+ readonly importMock: typeof importMockModule,
247
+ readonly resetModules: typeof resetModulesNow,
248
+ };
204
249
 
205
250
  /**
206
251
  * The `uft` namespace.
@@ -209,7 +254,7 @@ const NEEDS_LOADER =
209
254
  * per-instance, and freezing it means a test cannot leave a monkey-patch behind
210
255
  * for the next one.
211
256
  */
212
- export const uft: $FlowFixMe = Object.freeze({
257
+ export const uft: Uft = Object.freeze({
213
258
  fn,
214
259
  spyOn,
215
260
  mocked,
@@ -228,8 +273,8 @@ export const uft: $FlowFixMe = Object.freeze({
228
273
 
229
274
  // The clock a test controls. A test about "after five minutes the session
230
275
  // expires" should not take five minutes.
231
- useFakeTimers: timers.useFakeTimers,
232
- useRealTimers: timers.useRealTimers,
276
+ useFakeTimers: timers.installFakeClock,
277
+ useRealTimers: timers.restoreRealClock,
233
278
  isFakeTimers: timers.isFaked,
234
279
  advanceTimersByTime: timers.advanceTimersByTime,
235
280
  advanceTimersByTimeAsync: timers.advanceTimersByTimeAsync,
@@ -240,12 +285,15 @@ export const uft: $FlowFixMe = Object.freeze({
240
285
  setSystemTime: timers.setSystemTime,
241
286
  getMockedSystemTime: timers.getMockedSystemTime,
242
287
 
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),
288
+ // Module interception. `doMock` is `mock` and `doUnmock` is `unmock`, under
289
+ // the names Vitest gives the un-hoisted forms: there is one form here,
290
+ // because uf hoists neither, and a `doMock` that was a different function
291
+ // would be claiming a difference that does not exist.
292
+ mock: mockModule,
293
+ doMock: mockModule,
294
+ unmock: unmockModule,
295
+ doUnmock: unmockModule,
296
+ importActual: importActualModule,
297
+ importMock: importMockModule,
298
+ resetModules: resetModulesNow,
251
299
  });