@unrift/core 0.3.0 → 0.5.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.
Files changed (64) hide show
  1. package/bin/unrift.mjs +2 -1
  2. package/dist/esm/cli.js +19 -1
  3. package/dist/esm/expect.d.ts +14 -0
  4. package/dist/esm/expect.d.ts.map +1 -1
  5. package/dist/esm/index.d.ts +3 -1
  6. package/dist/esm/index.d.ts.map +1 -1
  7. package/dist/esm/index.js +2 -0
  8. package/dist/esm/interface.d.ts +7 -3
  9. package/dist/esm/interface.d.ts.map +1 -1
  10. package/dist/esm/interface.js +9 -5
  11. package/dist/esm/matchers.d.ts.map +1 -1
  12. package/dist/esm/matchers.js +195 -1
  13. package/dist/esm/mock/auto.d.ts +27 -0
  14. package/dist/esm/mock/auto.d.ts.map +1 -0
  15. package/dist/esm/mock/auto.js +108 -0
  16. package/dist/esm/mock/fetch.d.ts +47 -0
  17. package/dist/esm/mock/fetch.d.ts.map +1 -0
  18. package/dist/esm/mock/fetch.js +149 -0
  19. package/dist/esm/mock/fs.d.ts +54 -0
  20. package/dist/esm/mock/fs.d.ts.map +1 -0
  21. package/dist/esm/mock/fs.js +125 -0
  22. package/dist/esm/mock/index.d.ts +70 -0
  23. package/dist/esm/mock/index.d.ts.map +1 -0
  24. package/dist/esm/mock/index.js +70 -0
  25. package/dist/esm/mock/module-loader.d.ts +66 -0
  26. package/dist/esm/mock/module-loader.d.ts.map +1 -0
  27. package/dist/esm/mock/module-loader.js +123 -0
  28. package/dist/esm/mock/module.d.ts +99 -0
  29. package/dist/esm/mock/module.d.ts.map +1 -0
  30. package/dist/esm/mock/module.js +198 -0
  31. package/dist/esm/mock/register-loader.d.ts +17 -0
  32. package/dist/esm/mock/register-loader.d.ts.map +1 -0
  33. package/dist/esm/mock/register-loader.js +45 -0
  34. package/dist/esm/mock/spy.d.ts +73 -0
  35. package/dist/esm/mock/spy.d.ts.map +1 -0
  36. package/dist/esm/mock/spy.js +307 -0
  37. package/dist/esm/mock/stub.d.ts +26 -0
  38. package/dist/esm/mock/stub.d.ts.map +1 -0
  39. package/dist/esm/mock/stub.js +82 -0
  40. package/dist/esm/mock/timers.d.ts +51 -0
  41. package/dist/esm/mock/timers.d.ts.map +1 -0
  42. package/dist/esm/mock/timers.js +581 -0
  43. package/dist/esm/run.d.ts +4 -7
  44. package/dist/esm/run.d.ts.map +1 -1
  45. package/dist/esm/run.js +6 -2
  46. package/dist/esm/runner.d.ts +1 -0
  47. package/dist/esm/runner.d.ts.map +1 -1
  48. package/dist/esm/runner.js +21 -10
  49. package/dist/esm/suite.d.ts +24 -6
  50. package/dist/esm/suite.d.ts.map +1 -1
  51. package/dist/esm/suite.js +114 -20
  52. package/dist/esm/test.d.ts +5 -1
  53. package/dist/esm/test.d.ts.map +1 -1
  54. package/dist/esm/test.js +20 -6
  55. package/dist/esm/utils/compareEngine.js +3 -3
  56. package/dist/esm/utils/defineConfig.d.ts +1 -0
  57. package/dist/esm/utils/defineConfig.d.ts.map +1 -1
  58. package/dist/esm/utils/discoverTestFiles.js +1 -1
  59. package/dist/esm/utils/helpers.d.ts +12 -0
  60. package/dist/esm/utils/helpers.d.ts.map +1 -1
  61. package/dist/esm/utils/helpers.js +20 -0
  62. package/dist/esm/utils/toThrow.d.ts.map +1 -1
  63. package/dist/esm/utils/toThrow.js +2 -1
  64. package/package.json +4 -3
@@ -0,0 +1,581 @@
1
+ /**
2
+ * Fake clock - controllable replacements for `setTimeout`, `setInterval`,
3
+ * `setImmediate`, `process.nextTick`, `queueMicrotask`, `Date`, `Date.now`,
4
+ * and `performance.now`.
5
+ *
6
+ * Tests opt in via `mock.useFakeTimers()` and drain queued tasks deterministically
7
+ * with `advanceTimersByTime(ms)` / `runAllTimers()` / `runOnlyPendingTimers()`.
8
+ *
9
+ * Restoration:
10
+ * - `mock.useRealTimers()` reverts every patched global to its original
11
+ * - `mock.restoreAll()` does the same as part of bulk cleanup
12
+ */
13
+ import { activeRestorers } from "./spy.js";
14
+ const PRIORITY = {
15
+ microtask: 0,
16
+ immediate: 1,
17
+ timeout: 2,
18
+ interval: 2,
19
+ };
20
+ // State
21
+ //
22
+ // NOTE: this is process-global singleton state. It is correct today because
23
+ // unrift runs every spec file sequentially in one process, and the
24
+ // afterEach(restoreAll) contract resets between tests. If parallel per-file
25
+ // execution lands (see ISSUES.md, parallel test file execution), the fake
26
+ // clock would need per-worker isolation instead of module-level state.
27
+ let installed = false;
28
+ let now = 0;
29
+ let nextId = 1;
30
+ let nextSeq = 0;
31
+ const queue = [];
32
+ let originals;
33
+ const realDate = globalThis.Date;
34
+ const realDateNow = realDate.now.bind(realDate);
35
+ // Queue operations
36
+ function compareTasks(a, b) {
37
+ if (a.fireAt !== b.fireAt)
38
+ return a.fireAt - b.fireAt;
39
+ const pa = PRIORITY[a.kind];
40
+ const pb = PRIORITY[b.kind];
41
+ if (pa !== pb)
42
+ return pa - pb;
43
+ return a.seq - b.seq;
44
+ }
45
+ function enqueue(task) {
46
+ /**
47
+ * Linear insertion keeps the queue sorted. Test workloads rarely have
48
+ * enough pending tasks for a heap to matter.
49
+ */
50
+ let i = 0;
51
+ while (i < queue.length && compareTasks(queue[i], task) <= 0)
52
+ i++;
53
+ queue.splice(i, 0, task);
54
+ }
55
+ function findIndexById(id) {
56
+ for (let i = 0; i < queue.length; i++) {
57
+ if (queue[i].id === id)
58
+ return i;
59
+ }
60
+ return -1;
61
+ }
62
+ // Patched timer functions
63
+ function fakeSetTimeout(callback, ms, ...args) {
64
+ const task = {
65
+ id: nextId++,
66
+ kind: "timeout",
67
+ fireAt: now + Math.max(0, ms ?? 0),
68
+ seq: nextSeq++,
69
+ callback,
70
+ args,
71
+ cancelled: false,
72
+ };
73
+ enqueue(task);
74
+ return task.id;
75
+ }
76
+ function fakeClearTimeout(id) {
77
+ if (id == null)
78
+ return;
79
+ const numericId = typeof id === "number" ? id : id.id;
80
+ if (numericId == null)
81
+ return;
82
+ const index = findIndexById(numericId);
83
+ if (index !== -1)
84
+ queue[index].cancelled = true;
85
+ }
86
+ function fakeSetInterval(callback, ms, ...args) {
87
+ const period = Math.max(1, ms ?? 0);
88
+ const task = {
89
+ id: nextId++,
90
+ kind: "interval",
91
+ fireAt: now + period,
92
+ seq: nextSeq++,
93
+ callback,
94
+ args,
95
+ periodMs: period,
96
+ cancelled: false,
97
+ };
98
+ enqueue(task);
99
+ return task.id;
100
+ }
101
+ function fakeSetImmediate(callback, ...args) {
102
+ const task = {
103
+ id: nextId++,
104
+ kind: "immediate",
105
+ fireAt: now,
106
+ seq: nextSeq++,
107
+ callback,
108
+ args,
109
+ cancelled: false,
110
+ };
111
+ enqueue(task);
112
+ return task.id;
113
+ }
114
+ function fakeQueueMicrotask(callback) {
115
+ const task = {
116
+ id: nextId++,
117
+ kind: "microtask",
118
+ fireAt: now,
119
+ seq: nextSeq++,
120
+ callback,
121
+ args: [],
122
+ cancelled: false,
123
+ };
124
+ enqueue(task);
125
+ }
126
+ // Date replacement
127
+ function makeFakeDate() {
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ const FakeDate = function (...args) {
130
+ if (!new.target) {
131
+ /**
132
+ * Date(...) without `new` returns a string of the *current* time -
133
+ * which when faked should reflect the fake clock.
134
+ */
135
+ return new realDate(now).toString();
136
+ }
137
+ if (args.length === 0) {
138
+ return Reflect.construct(realDate, [now], new.target);
139
+ }
140
+ return Reflect.construct(realDate, args, new.target);
141
+ };
142
+ /**
143
+ * Inherit static surface and instance prototype from the real Date.
144
+ * The cast is needed because `prototype` is typed as readonly on `typeof Date`
145
+ * even though it's actually writable on a function value.
146
+ */
147
+ Object.setPrototypeOf(FakeDate, realDate);
148
+ FakeDate.prototype =
149
+ realDate.prototype;
150
+ FakeDate.now = () => now;
151
+ FakeDate.parse = realDate.parse.bind(realDate);
152
+ FakeDate.UTC = realDate.UTC.bind(realDate);
153
+ return FakeDate;
154
+ }
155
+ // Drain
156
+ const MAX_DRAIN_ITERATIONS = 10_000;
157
+ // Pull and run the head task. Re-queues intervals. Skips cancelled tasks.
158
+ function pumpOne() {
159
+ while (queue.length > 0 && queue[0].cancelled)
160
+ queue.shift();
161
+ if (queue.length === 0)
162
+ return false;
163
+ const task = queue.shift();
164
+ now = Math.max(now, task.fireAt);
165
+ if (task.kind === "interval") {
166
+ /**
167
+ * Re-schedule before firing so a callback that calls clearInterval()
168
+ * can find the task in the queue.
169
+ */
170
+ const next = {
171
+ ...task,
172
+ seq: nextSeq++,
173
+ fireAt: now + (task.periodMs ?? 1),
174
+ };
175
+ enqueue(next);
176
+ }
177
+ task.callback(...task.args);
178
+ return true;
179
+ }
180
+ function advanceTo(target) {
181
+ let iterations = 0;
182
+ while (queue.length > 0) {
183
+ while (queue.length > 0 && queue[0].cancelled)
184
+ queue.shift();
185
+ if (queue.length === 0)
186
+ break;
187
+ if (queue[0].fireAt > target)
188
+ break;
189
+ pumpOne();
190
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
191
+ throw new Error("advanceTimersByTime exceeded the maximum drain iterations - " +
192
+ "is a timer scheduling itself in a loop?");
193
+ }
194
+ }
195
+ if (target > now)
196
+ now = target;
197
+ }
198
+ const ALL_FAKEABLE = [
199
+ "setTimeout",
200
+ "setInterval",
201
+ "setImmediate",
202
+ "queueMicrotask",
203
+ "process.nextTick",
204
+ "Date",
205
+ "performance",
206
+ ];
207
+ function resolveFakedSet(options) {
208
+ if (options.toFake !== undefined && options.doNotFake !== undefined) {
209
+ throw new Error("mock.useFakeTimers(): `toFake` and `doNotFake` are mutually exclusive");
210
+ }
211
+ if (options.toFake !== undefined) {
212
+ return new Set(options.toFake);
213
+ }
214
+ if (options.doNotFake !== undefined) {
215
+ const excluded = new Set(options.doNotFake);
216
+ return new Set(ALL_FAKEABLE.filter((api) => !excluded.has(api)));
217
+ }
218
+ return new Set(ALL_FAKEABLE);
219
+ }
220
+ // Tracks what was actually patched on the most recent useFakeTimers call,
221
+ // so useRealTimers knows which originals to restore.
222
+ let fakedApis = new Set();
223
+ // The own descriptor `performance` had for `now` before we shadowed it
224
+ // (undefined if `now` was inherited from the prototype).
225
+ let originalPerfNowDescriptor;
226
+ /**
227
+ * Patch `performance.now` to read the fake clock. Uses `defineProperty`
228
+ * rather than assignment because on Node 18 `performance.now` is a
229
+ * non-writable prototype method - plain assignment throws. Defining an own
230
+ * property on the instance shadows it safely.
231
+ *
232
+ * Returns false (without throwing) if the platform won't allow it, so a
233
+ * failure here never aborts the rest of the timer install.
234
+ */
235
+ function patchPerformanceNow() {
236
+ const perf = globalThis.performance;
237
+ if (!perf || typeof perf.now !== "function")
238
+ return false;
239
+ try {
240
+ originalPerfNowDescriptor = Object.getOwnPropertyDescriptor(perf, "now");
241
+ Object.defineProperty(perf, "now", {
242
+ configurable: true,
243
+ writable: true,
244
+ value: () => now,
245
+ });
246
+ return true;
247
+ }
248
+ catch {
249
+ return false;
250
+ }
251
+ }
252
+ /** Reverse `patchPerformanceNow`. Best-effort - never throws. */
253
+ function restorePerformanceNow() {
254
+ const perf = globalThis.performance;
255
+ if (!perf)
256
+ return;
257
+ try {
258
+ if (originalPerfNowDescriptor) {
259
+ Object.defineProperty(perf, "now", originalPerfNowDescriptor);
260
+ }
261
+ else {
262
+ // We added a shadow own property; deleting it reveals the real
263
+ // prototype method again.
264
+ delete perf.now;
265
+ }
266
+ }
267
+ catch {
268
+ // ignore - leaving the fake in place is better than throwing on cleanup
269
+ }
270
+ originalPerfNowDescriptor = undefined;
271
+ }
272
+ function ensureInstalled(method) {
273
+ if (!installed) {
274
+ throw new Error(`mock.${method}() requires fake timers - call mock.useFakeTimers() first`);
275
+ }
276
+ }
277
+ function toMillis(value) {
278
+ let ms;
279
+ if (value instanceof realDate)
280
+ ms = value.getTime();
281
+ else if (typeof value === "number")
282
+ ms = value;
283
+ else
284
+ ms = new realDate(value).getTime();
285
+ // Reject NaN (invalid Date / unparseable string) and ±Infinity up front.
286
+ // Letting a non-finite value become the fake clock would silently break
287
+ // every scheduling and drain comparison (now + ms, fireAt > target, …).
288
+ if (!Number.isFinite(ms)) {
289
+ const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
290
+ throw new TypeError(`mock fake-timers: invalid time value ${shown} - expected a valid Date, ` +
291
+ `a parseable date string, or finite epoch milliseconds`);
292
+ }
293
+ return ms;
294
+ }
295
+ export function useFakeTimers(options = {}) {
296
+ if (installed)
297
+ return;
298
+ // Resolve options that can throw BEFORE flipping `installed` or touching
299
+ // any global - so an invalid `toFake`/`doNotFake` combo or a bad `now`
300
+ // leaves useFakeTimers a clean no-op rather than a half-installed state.
301
+ const toFake = resolveFakedSet(options);
302
+ const initialNow = options.now !== undefined ? toMillis(options.now) : 0;
303
+ installed = true;
304
+ fakedApis = toFake;
305
+ originals = {
306
+ setTimeout: globalThis.setTimeout,
307
+ clearTimeout: globalThis.clearTimeout,
308
+ setInterval: globalThis.setInterval,
309
+ clearInterval: globalThis.clearInterval,
310
+ setImmediate: typeof globalThis.setImmediate !== "undefined"
311
+ ? globalThis.setImmediate
312
+ : undefined,
313
+ clearImmediate: typeof globalThis.clearImmediate !== "undefined"
314
+ ? globalThis.clearImmediate
315
+ : undefined,
316
+ queueMicrotask: globalThis.queueMicrotask,
317
+ processNextTick: typeof process !== "undefined" && process.nextTick
318
+ ? process.nextTick
319
+ : undefined,
320
+ Date: globalThis.Date,
321
+ performanceNow: typeof globalThis.performance !== "undefined" &&
322
+ typeof globalThis.performance.now === "function"
323
+ ? globalThis.performance.now.bind(globalThis.performance)
324
+ : undefined,
325
+ };
326
+ now = initialNow;
327
+ queue.length = 0;
328
+ // Register the restorer BEFORE patching anything, so even a mid-install
329
+ // failure leaves a globally-installed fake that restoreAll can undo.
330
+ // Without this, a throw between here and the end would strand the real
331
+ // timers permanently - see the Node 18 performance.now issue.
332
+ activeRestorers.add(useRealTimers);
333
+ // Patch globals - each pair gated on the resolved fake set.
334
+ if (toFake.has("setTimeout")) {
335
+ globalThis.setTimeout =
336
+ fakeSetTimeout;
337
+ globalThis.clearTimeout =
338
+ fakeClearTimeout;
339
+ }
340
+ if (toFake.has("setInterval")) {
341
+ globalThis.setInterval =
342
+ fakeSetInterval;
343
+ globalThis.clearInterval =
344
+ fakeClearTimeout;
345
+ }
346
+ if (toFake.has("setImmediate") && originals.setImmediate) {
347
+ globalThis.setImmediate =
348
+ fakeSetImmediate;
349
+ globalThis.clearImmediate =
350
+ fakeClearTimeout;
351
+ }
352
+ if (toFake.has("queueMicrotask")) {
353
+ globalThis.queueMicrotask = fakeQueueMicrotask;
354
+ }
355
+ if (toFake.has("process.nextTick") &&
356
+ originals.processNextTick &&
357
+ typeof process !== "undefined") {
358
+ /**
359
+ * process.nextTick has the highest priority in Node. schedule with
360
+ * microtask priority so it fires before macrotasks at the same time.
361
+ */
362
+ process.nextTick = ((callback, ...args) => {
363
+ const task = {
364
+ id: nextId++,
365
+ kind: "microtask",
366
+ fireAt: now,
367
+ seq: nextSeq++,
368
+ callback,
369
+ args,
370
+ cancelled: false,
371
+ };
372
+ enqueue(task);
373
+ });
374
+ }
375
+ if (toFake.has("Date")) {
376
+ globalThis.Date = makeFakeDate();
377
+ }
378
+ if (toFake.has("performance")) {
379
+ // If the platform won't let us patch performance.now (e.g. it's a
380
+ // non-configurable accessor), drop it from the faked set so restore
381
+ // doesn't try to undo something we never did.
382
+ if (!patchPerformanceNow()) {
383
+ fakedApis.delete("performance");
384
+ }
385
+ }
386
+ }
387
+ export function useRealTimers() {
388
+ if (!installed || !originals)
389
+ return;
390
+ // Only restore the APIs that were actually patched. Touching globals
391
+ // we didn't touch would clobber user-set values or other mocks.
392
+ if (fakedApis.has("setTimeout")) {
393
+ globalThis.setTimeout = originals.setTimeout;
394
+ globalThis.clearTimeout = originals.clearTimeout;
395
+ }
396
+ if (fakedApis.has("setInterval")) {
397
+ globalThis.setInterval = originals.setInterval;
398
+ globalThis.clearInterval = originals.clearInterval;
399
+ }
400
+ if (fakedApis.has("setImmediate")) {
401
+ if (originals.setImmediate)
402
+ globalThis.setImmediate = originals.setImmediate;
403
+ if (originals.clearImmediate)
404
+ globalThis.clearImmediate = originals.clearImmediate;
405
+ }
406
+ if (fakedApis.has("queueMicrotask")) {
407
+ globalThis.queueMicrotask = originals.queueMicrotask;
408
+ }
409
+ if (fakedApis.has("process.nextTick") &&
410
+ originals.processNextTick &&
411
+ typeof process !== "undefined") {
412
+ process.nextTick =
413
+ originals.processNextTick;
414
+ }
415
+ if (fakedApis.has("Date")) {
416
+ globalThis.Date = originals.Date;
417
+ }
418
+ if (fakedApis.has("performance")) {
419
+ restorePerformanceNow();
420
+ }
421
+ installed = false;
422
+ originals = undefined;
423
+ fakedApis = new Set();
424
+ queue.length = 0;
425
+ now = 0;
426
+ activeRestorers.delete(useRealTimers);
427
+ }
428
+ export function advanceTimersByTime(ms) {
429
+ ensureInstalled("advanceTimersByTime");
430
+ advanceTo(now + Math.max(0, ms));
431
+ }
432
+ export function runAllTimers() {
433
+ ensureInstalled("runAllTimers");
434
+ let iterations = 0;
435
+ while (queue.length > 0) {
436
+ while (queue.length > 0 && queue[0].cancelled)
437
+ queue.shift();
438
+ if (queue.length === 0)
439
+ break;
440
+ pumpOne();
441
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
442
+ throw new Error("runAllTimers exceeded the maximum drain iterations - " +
443
+ "is a timer scheduling itself in a loop?");
444
+ }
445
+ }
446
+ }
447
+ export function runOnlyPendingTimers() {
448
+ ensureInstalled("runOnlyPendingTimers");
449
+ // Snapshot by `seq`, not `id`: a re-fired interval re-enqueues itself with
450
+ // the SAME id but a fresh seq (see pumpOne). Snapshotting ids would keep the
451
+ // interval "in scope" forever and drain it every tick until the guard trips.
452
+ // seq is unique per enqueue, so the re-scheduled tick counts as newly
453
+ // scheduled and is left pending.
454
+ const targetSeqs = new Set(queue.map((t) => t.seq));
455
+ let iterations = 0;
456
+ while (queue.length > 0) {
457
+ while (queue.length > 0 && queue[0].cancelled)
458
+ queue.shift();
459
+ if (queue.length === 0)
460
+ break;
461
+ if (!targetSeqs.has(queue[0].seq))
462
+ break;
463
+ pumpOne();
464
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
465
+ throw new Error("runOnlyPendingTimers exceeded the maximum drain iterations");
466
+ }
467
+ }
468
+ }
469
+ /**
470
+ * Like `pumpOne`, but if the callback returns a thenable, awaits it before
471
+ * resolving, and yields to the real microtask queue afterward so any
472
+ * microtasks the callback queued can settle before the next pump.
473
+ *
474
+ * The `await Promise.resolve()` at the end is the key. It lets pending
475
+ * microtasks (resolved Promises, queued `.then` handlers) flush. Real
476
+ * `Promise.resolve()` is unaffected by `mock.useFakeTimers()` - the engine
477
+ * uses its internal microtask queue, not our fake `queueMicrotask`.
478
+ */
479
+ async function pumpOneAsync() {
480
+ while (queue.length > 0 && queue[0].cancelled)
481
+ queue.shift();
482
+ if (queue.length === 0)
483
+ return false;
484
+ const task = queue.shift();
485
+ now = Math.max(now, task.fireAt);
486
+ if (task.kind === "interval") {
487
+ const next = {
488
+ ...task,
489
+ seq: nextSeq++,
490
+ fireAt: now + (task.periodMs ?? 1),
491
+ };
492
+ enqueue(next);
493
+ }
494
+ const result = task.callback(...task.args);
495
+ if (result !== undefined &&
496
+ result !== null &&
497
+ typeof result.then === "function") {
498
+ await result;
499
+ }
500
+ // Yield once more so microtasks scheduled by the callback (after any
501
+ // awaited promise resolved) can run before we move on.
502
+ await Promise.resolve();
503
+ return true;
504
+ }
505
+ /**
506
+ * Like `advanceTimersByTime`, but awaits Promise return values from
507
+ * timer callbacks and yields to microtasks between pumps. Use this when
508
+ * the code under test does `await` work inside a `setTimeout` callback.
509
+ */
510
+ export async function advanceTimersByTimeAsync(ms) {
511
+ ensureInstalled("advanceTimersByTimeAsync");
512
+ const target = now + Math.max(0, ms);
513
+ let iterations = 0;
514
+ while (queue.length > 0) {
515
+ while (queue.length > 0 && queue[0].cancelled)
516
+ queue.shift();
517
+ if (queue.length === 0)
518
+ break;
519
+ if (queue[0].fireAt > target)
520
+ break;
521
+ await pumpOneAsync();
522
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
523
+ throw new Error("advanceTimersByTimeAsync exceeded the maximum drain iterations - " +
524
+ "is a timer scheduling itself in a loop?");
525
+ }
526
+ }
527
+ if (target > now)
528
+ now = target;
529
+ }
530
+ /** Async sibling of `runAllTimers`. */
531
+ export async function runAllTimersAsync() {
532
+ ensureInstalled("runAllTimersAsync");
533
+ let iterations = 0;
534
+ while (queue.length > 0) {
535
+ while (queue.length > 0 && queue[0].cancelled)
536
+ queue.shift();
537
+ if (queue.length === 0)
538
+ break;
539
+ await pumpOneAsync();
540
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
541
+ throw new Error("runAllTimersAsync exceeded the maximum drain iterations - " +
542
+ "is a timer scheduling itself in a loop?");
543
+ }
544
+ }
545
+ }
546
+ /** Async sibling of `runOnlyPendingTimers`. */
547
+ export async function runOnlyPendingTimersAsync() {
548
+ ensureInstalled("runOnlyPendingTimersAsync");
549
+ // Snapshot by `seq` for the same reason as the sync variant: a re-fired
550
+ // interval re-enqueues with the same id but a fresh seq, so id-based
551
+ // snapshotting would drain it every tick until the guard trips.
552
+ const targetSeqs = new Set(queue.map((t) => t.seq));
553
+ let iterations = 0;
554
+ while (queue.length > 0) {
555
+ while (queue.length > 0 && queue[0].cancelled)
556
+ queue.shift();
557
+ if (queue.length === 0)
558
+ break;
559
+ if (!targetSeqs.has(queue[0].seq))
560
+ break;
561
+ await pumpOneAsync();
562
+ if (++iterations > MAX_DRAIN_ITERATIONS) {
563
+ throw new Error("runOnlyPendingTimersAsync exceeded the maximum drain iterations");
564
+ }
565
+ }
566
+ }
567
+ export function getTimerCount() {
568
+ ensureInstalled("getTimerCount");
569
+ let count = 0;
570
+ for (const task of queue)
571
+ if (!task.cancelled)
572
+ count++;
573
+ return count;
574
+ }
575
+ export function setSystemTime(value) {
576
+ ensureInstalled("setSystemTime");
577
+ now = toMillis(value);
578
+ }
579
+ export function getRealSystemTime() {
580
+ return realDateNow();
581
+ }
package/dist/esm/run.d.ts CHANGED
@@ -1,20 +1,17 @@
1
- import { TaskStatus } from "./types";
1
+ import { type TestResult } from "./suite";
2
2
  type RunEngineOptions = {
3
3
  files: string[];
4
4
  timeoutMs?: number;
5
5
  bail?: boolean;
6
+ grep?: RegExp;
7
+ retries?: number;
6
8
  matchers?: string[];
7
9
  };
8
10
  export type RunEngineResult = {
9
11
  isOnly: boolean;
10
12
  timeoutMs?: number;
11
13
  bail: boolean;
12
- results: Array<{
13
- description: string;
14
- status: TaskStatus;
15
- error?: Error;
16
- durationMs: number;
17
- }>;
14
+ results: TestResult[];
18
15
  };
19
16
  export declare function runEngine(options: RunEngineOptions): Promise<RunEngineResult>;
20
17
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAIA,OAAO,EAAY,UAAU,EAAE,MAAM,SAAS,CAAC;AAI/C,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,KAAK,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,UAAU,CAAC;QACnB,KAAK,CAAC,EAAE,KAAK,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ,CAAC;AAQF,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CA2D1B"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,SAAS,CAAC;AASjB,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB,CAAC;AAQF,wBAAsB,SAAS,CAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CAoE1B"}
package/dist/esm/run.js CHANGED
@@ -1,10 +1,11 @@
1
- import { clearContext, computeOnlyFlags, rootSuite, Suite } from "./suite.js";
1
+ import { clearContext, computeOnlyFlags, rootSuite, Suite, } from "./suite.js";
2
2
  import { ensureInternalMatchers } from "./matchers.js";
3
3
  import { RunState, unriftGlobalContext } from "./context.js";
4
4
  import { normalizePath } from "./utils/normalizePath.js";
5
5
  import { TaskMode } from "./types.js";
6
6
  import { toError } from "./utils/toError.js";
7
7
  import { toImportUrl } from "./utils/transform.js";
8
+ import { assertValidRetries } from "./utils/helpers.js";
8
9
  async function loadMatcherModules(specifiers) {
9
10
  for (const spec of specifiers ?? []) {
10
11
  await import(spec);
@@ -12,6 +13,9 @@ async function loadMatcherModules(specifiers) {
12
13
  }
13
14
  export async function runEngine(options) {
14
15
  const { files } = options;
16
+ // Validate before touching global state. A bad value (e.g. "3" from a JSON
17
+ // config) would otherwise turn into "31" attempts or zero.
18
+ assertValidRetries(options.retries, "runEngine");
15
19
  clearContext();
16
20
  ensureInternalMatchers();
17
21
  await loadMatcherModules(options.matchers);
@@ -40,7 +44,7 @@ export async function runEngine(options) {
40
44
  }
41
45
  const isOnly = computeOnlyFlags(rootSuite);
42
46
  unriftGlobalContext.state = RunState.Run;
43
- await rootSuite.run({ timeoutMs, bail }, { bailed: false }, isOnly, false);
47
+ await rootSuite.run({ timeoutMs, bail, grep: options.grep, retries: options.retries }, { bailed: false }, isOnly, false);
44
48
  const results = rootSuite.getResults();
45
49
  return {
46
50
  isOnly,
@@ -2,6 +2,7 @@ interface RunnerOptions {
2
2
  configPath?: string;
3
3
  testDir?: string;
4
4
  pattern?: RegExp;
5
+ grep?: RegExp;
5
6
  timeoutMs?: number;
6
7
  bail?: boolean;
7
8
  debug?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAkBA,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA0ND,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,iBA8T5D;AA+DD,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,iBAyD9D"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAyBA,UAAU,aAAa;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AA+MD,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,iBAwU5D;AA+DD,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,iBAyD9D"}