@uniflowed/test 0.0.0-alpha.13 → 0.0.0-alpha.14

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.
@@ -289,29 +289,36 @@ function matchesThrown(thrown: mixed, expected: mixed): boolean {
289
289
  * Every entry returns a [`Verdict`] rather than throwing, which is what lets
290
290
  * `.not` reuse all of them.
291
291
  *
292
- * # The `any` in the indexer
292
+ * # Every entry takes `mixed`, and that is what makes the indexer sayable
293
293
  *
294
- * The entries do not agree about their arguments — `toBe` takes a `mixed`,
295
- * `toHaveLength` takes a `number`, `toBeCloseTo` takes two — and [`bind`]
296
- * applies whichever one it was asked for to a `$ReadOnlyArray<mixed>` it
297
- * collected from a caller. Parameters are contravariant, so one indexer cannot
298
- * describe both ends: `(...args: $ReadOnlyArray<mixed>)` rejects every entry
299
- * that wants a `number`, and `(...args: $ReadOnlyArray<empty>)` accepts every
300
- * entry and rejects the call.
294
+ * [`bind`] reaches an entry by a computed key and applies it to the
295
+ * `$ReadOnlyArray<mixed>` it collected from the caller, so the indexer has to
296
+ * describe a function that will accept those arguments. Parameters are
297
+ * contravariant, so an entry that demanded a `number` could not be described
298
+ * by one — `(...args: $ReadOnlyArray<mixed>)` rejects it, and
299
+ * `(...args: $ReadOnlyArray<empty>)` accepts it and rejects the call. That
300
+ * disagreement is why this indexer used to be written `$ReadOnlyArray<any>`,
301
+ * with a `flow/unclear-type` suppression on it.
301
302
  *
302
- * That is unchanged, and it is now the last of it. Where a caller used to meet
303
- * this indexer through an unbroken chain of `$FlowFixMe`, the published
304
- * surface is [`Expectation`] and the disagreement the indexer papers over is
305
- * written down there, one signature per name. What survives is a table reached
306
- * by a computed key inside this module, between `verdicts` and `bind` — two
307
- * functions in one file, neither of which a consumer can see — and narrowing
308
- * it means writing `bind`'s forty-one wrappers out by hand to avoid the
309
- * lookup. That is a second copy of the listing to keep in step with this one,
310
- * which is a worse trade than the suppression it removes.
303
+ * So the disagreement is gone instead of papered over: every entry here takes
304
+ * `mixed` and coerces what it needs, the way most of them — `toBe`,
305
+ * `toBeGreaterThan`, `toHaveAttribute` — already did. Nothing a caller can see
306
+ * got wider: `toHaveLength` still refuses a string and `toBeTypeOf` still
307
+ * refuses a word `typeof` never says, because those are [`Matchers`]'s
308
+ * signatures and [`Matchers`] is the published type. What changed is that the
309
+ * table behind them stopped claiming a narrower argument than the one `bind`
310
+ * can hand it, which is a claim that was never true. The `String(…)` and
311
+ * `Number(…)` calls that appeared with it are the coercion the runtime was
312
+ * already doing, said out loud, and each produces the same message the implicit
313
+ * one did for the same input.
314
+ *
315
+ * The alternative — narrowing the indexer by writing `bind`'s forty-one
316
+ * wrappers out to avoid the computed lookup — is a second copy of the listing
317
+ * to keep in step with [`Matchers`], and is a worse trade than either. See
318
+ * ubugeeei-prod/uf#402.
311
319
  */
312
320
  function verdicts(received: mixed): {
313
- // uf-lint-disable-next-line flow/unclear-type
314
- readonly [string]: (...args: $ReadOnlyArray<any>) => Verdict,
321
+ readonly [string]: (...args: $ReadOnlyArray<mixed>) => Verdict,
315
322
  } {
316
323
  const shown = () => render(received);
317
324
  const simple = (pass: boolean, what: string, expected?: mixed): Verdict => ({
@@ -386,14 +393,15 @@ function verdicts(received: mixed): {
386
393
  `to be at most ${render(expected)}`,
387
394
  expected,
388
395
  ),
389
- toBeCloseTo: (expected: number, digits?: number) => {
390
- const places = digits ?? 2;
396
+ toBeCloseTo: (expected: mixed, digits?: mixed) => {
397
+ const target = Number(expected);
398
+ const places = digits === undefined ? 2 : Number(digits);
391
399
  const tolerance = 10 ** -places / 2;
392
- const difference = Math.abs((received as $FlowFixMe) - expected);
400
+ const difference = Math.abs((received as $FlowFixMe) - target);
393
401
  return simple(
394
402
  difference < tolerance,
395
- `to be within ${tolerance} of ${expected}, but it is off by ${difference}`,
396
- expected,
403
+ `to be within ${tolerance} of ${target}, but it is off by ${difference}`,
404
+ target,
397
405
  );
398
406
  },
399
407
  toContain: (expected: mixed) => {
@@ -419,22 +427,23 @@ function verdicts(received: mixed): {
419
427
  expected,
420
428
  );
421
429
  },
422
- toHaveLength: (expected: number) => {
430
+ toHaveLength: (expected: mixed) => {
423
431
  const length = received == null ? undefined : (received as $FlowFixMe).length;
424
432
  return simple(
425
433
  length === expected,
426
- `to have length ${expected}, not ${render(length)}`,
434
+ `to have length ${String(expected)}, not ${render(length)}`,
427
435
  expected,
428
436
  );
429
437
  },
430
- toHaveProperty: (path: string, ...rest: $ReadOnlyArray<mixed>) => {
431
- const found = propertyAt(received, path);
438
+ toHaveProperty: (path: mixed, ...rest: $ReadOnlyArray<mixed>) => {
439
+ const at = String(path);
440
+ const found = propertyAt(received, at);
432
441
  if (rest.length === 0) {
433
- return simple(found.found, `to have a property at \`${path}\``);
442
+ return simple(found.found, `to have a property at \`${at}\``);
434
443
  }
435
444
  return simple(
436
445
  found.found && equals(found.value, rest[0]),
437
- `to have \`${path}\` equal to ${render(rest[0])}, not ${render(found.value)}`,
446
+ `to have \`${at}\` equal to ${render(rest[0])}, not ${render(found.value)}`,
438
447
  rest[0],
439
448
  );
440
449
  },
@@ -454,16 +463,24 @@ function verdicts(received: mixed): {
454
463
  `to be an instance of ${render(expected)}`,
455
464
  expected,
456
465
  ),
457
- toBeTypeOf: (expected: string) =>
466
+ toBeTypeOf: (expected: mixed) => {
467
+ const name = String(expected);
468
+ return simple(
469
+ typeof received === name,
470
+ `to be of type ${name}, not ${typeof received}`,
471
+ name,
472
+ );
473
+ },
474
+ toSatisfy: (predicate: mixed) =>
458
475
  simple(
459
- typeof received === expected,
460
- `to be of type ${expected}, not ${typeof received}`,
461
- expected,
476
+ typeof predicate === "function" && predicate(received) === true,
477
+ "to satisfy the predicate",
462
478
  ),
463
- toSatisfy: (predicate: (value: mixed) => boolean) =>
464
- simple(predicate(received) === true, "to satisfy the predicate"),
465
- toMatchSnapshot: (hint?: string): Verdict => {
466
- const verdict = snapshot.matchSnapshot(received, hint);
479
+ toMatchSnapshot: (hint?: mixed): Verdict => {
480
+ const verdict = snapshot.matchSnapshot(
481
+ received,
482
+ hint === undefined ? undefined : String(hint),
483
+ );
467
484
  return {
468
485
  pass: verdict.pass,
469
486
  expected: verdict.expected ?? "(no snapshot yet)",
@@ -477,8 +494,11 @@ function verdicts(received: mixed): {
477
494
  negatedFailure: () => "expected the value not to match its snapshot",
478
495
  };
479
496
  },
480
- toMatchInlineSnapshot: (expected?: string): Verdict => {
481
- const verdict = snapshot.matchInlineSnapshot(received, expected);
497
+ toMatchInlineSnapshot: (expected?: mixed): Verdict => {
498
+ const verdict = snapshot.matchInlineSnapshot(
499
+ received,
500
+ expected === undefined ? undefined : String(expected),
501
+ );
482
502
  return {
483
503
  pass: verdict.pass,
484
504
  expected: verdict.expected ?? "(no inline snapshot yet)",
@@ -529,10 +549,14 @@ function verdicts(received: mixed): {
529
549
  requireSpy("toHaveBeenCalled");
530
550
  return simple(spyCalls().length > 0, "to have been called");
531
551
  },
532
- toHaveBeenCalledTimes: (count: number) => {
552
+ toHaveBeenCalledTimes: (count: mixed) => {
533
553
  requireSpy("toHaveBeenCalledTimes");
534
554
  const actual = spyCalls().length;
535
- return simple(actual === count, `to have been called ${count} times, not ${actual}`, count);
555
+ return simple(
556
+ actual === count,
557
+ `to have been called ${String(count)} times, not ${actual}`,
558
+ count,
559
+ );
536
560
  },
537
561
  toHaveBeenCalledWith: (...args: $ReadOnlyArray<mixed>) => {
538
562
  requireSpy("toHaveBeenCalledWith");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/test",
3
- "version": "0.0.0-alpha.13",
3
+ "version": "0.0.0-alpha.14",
4
4
  "description": "The test API and worker for `uf test`: describe/it, a full matcher set, and the process uf fans test files out to.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,6 +21,6 @@
21
21
  "internal"
22
22
  ],
23
23
  "dependencies": {
24
- "@uniflowed/host": "0.0.0-alpha.13"
24
+ "@uniflowed/host": "0.0.0-alpha.14"
25
25
  }
26
26
  }
package/worker.js CHANGED
@@ -57,6 +57,14 @@ import { reset } from "./internal/registry.js";
57
57
  import { resetModuleState } from "./internal/modules.js";
58
58
  import { run } from "./internal/run.js";
59
59
  import { unstubAllEnvs, unstubAllGlobals } from "./internal/namespace.js";
60
+ // Renamed at the door, for two reasons that agree. It reads as the resets
61
+ // beside it do — `reset`, `unstubAllEnvs`, `resetModuleState` are all
62
+ // verb-first, and so is what this does to the clock. And `useRealTimers` is
63
+ // not a React hook: it is uf's own timer control, which happens to be named
64
+ // the way every runner names it, and calling it bare in a plain function is
65
+ // a `react/hooks-rules` error on the name alone. A suppression would assert
66
+ // something about this call; the name is simply accurate.
67
+ import { useRealTimers as restoreRealClock } from "./internal/timers.js";
60
68
 
61
69
  /** What `uf` sends for one file. */
62
70
  type Request = {|
@@ -151,6 +159,22 @@ async function runFile(request: Request, generation: number): Promise<void> {
151
159
  // wrote it. See ubugeeei-prod/uf#417.
152
160
  unstubAllEnvs();
153
161
  unstubAllGlobals();
162
+ // And the clock goes back, whatever the previous file did with it. A spy
163
+ // lives in the registry `reset` clears; a fake clock is a write to the
164
+ // scheduling globals the whole process shares, so `uft.useFakeTimers()` in
165
+ // one file is still installed when the next one imports.
166
+ //
167
+ // Worse than a leaked value, and worse in a way that hides it. A leaked stub
168
+ // makes the next file read something wrong, which arrives as an assertion
169
+ // naming the value. A leaked clock makes the next file's `setTimeout` never
170
+ // fire — including the one `withTimeout` races each case against — so the
171
+ // file hangs with nothing on screen until `uf`'s own deadline kills the
172
+ // worker, and the report names the file that waited rather than the file
173
+ // that stopped time. See ubugeeei-prod/uf#581.
174
+ //
175
+ // Before the import rather than after the run, so a file that throws while
176
+ // loading still hands the next one a real clock.
177
+ restoreRealClock();
154
178
  // Every module this file stood in for goes back, before the next file can
155
179
  // import one of them and be handed the previous file's stand-in. A worker
156
180
  // serves many files out of one module registry, so this is the difference