@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.
- package/bin/unrift.mjs +2 -1
- package/dist/esm/cli.js +19 -1
- package/dist/esm/expect.d.ts +14 -0
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/interface.d.ts +7 -3
- package/dist/esm/interface.d.ts.map +1 -1
- package/dist/esm/interface.js +9 -5
- package/dist/esm/matchers.d.ts.map +1 -1
- package/dist/esm/matchers.js +195 -1
- package/dist/esm/mock/auto.d.ts +27 -0
- package/dist/esm/mock/auto.d.ts.map +1 -0
- package/dist/esm/mock/auto.js +108 -0
- package/dist/esm/mock/fetch.d.ts +47 -0
- package/dist/esm/mock/fetch.d.ts.map +1 -0
- package/dist/esm/mock/fetch.js +149 -0
- package/dist/esm/mock/fs.d.ts +54 -0
- package/dist/esm/mock/fs.d.ts.map +1 -0
- package/dist/esm/mock/fs.js +125 -0
- package/dist/esm/mock/index.d.ts +70 -0
- package/dist/esm/mock/index.d.ts.map +1 -0
- package/dist/esm/mock/index.js +70 -0
- package/dist/esm/mock/module-loader.d.ts +66 -0
- package/dist/esm/mock/module-loader.d.ts.map +1 -0
- package/dist/esm/mock/module-loader.js +123 -0
- package/dist/esm/mock/module.d.ts +99 -0
- package/dist/esm/mock/module.d.ts.map +1 -0
- package/dist/esm/mock/module.js +198 -0
- package/dist/esm/mock/register-loader.d.ts +17 -0
- package/dist/esm/mock/register-loader.d.ts.map +1 -0
- package/dist/esm/mock/register-loader.js +45 -0
- package/dist/esm/mock/spy.d.ts +73 -0
- package/dist/esm/mock/spy.d.ts.map +1 -0
- package/dist/esm/mock/spy.js +307 -0
- package/dist/esm/mock/stub.d.ts +26 -0
- package/dist/esm/mock/stub.d.ts.map +1 -0
- package/dist/esm/mock/stub.js +82 -0
- package/dist/esm/mock/timers.d.ts +51 -0
- package/dist/esm/mock/timers.d.ts.map +1 -0
- package/dist/esm/mock/timers.js +581 -0
- package/dist/esm/run.d.ts +4 -7
- package/dist/esm/run.d.ts.map +1 -1
- package/dist/esm/run.js +6 -2
- package/dist/esm/runner.d.ts +1 -0
- package/dist/esm/runner.d.ts.map +1 -1
- package/dist/esm/runner.js +21 -10
- package/dist/esm/suite.d.ts +24 -6
- package/dist/esm/suite.d.ts.map +1 -1
- package/dist/esm/suite.js +114 -20
- package/dist/esm/test.d.ts +5 -1
- package/dist/esm/test.d.ts.map +1 -1
- package/dist/esm/test.js +20 -6
- package/dist/esm/utils/compareEngine.js +3 -3
- package/dist/esm/utils/defineConfig.d.ts +1 -0
- package/dist/esm/utils/defineConfig.d.ts.map +1 -1
- package/dist/esm/utils/discoverTestFiles.js +1 -1
- package/dist/esm/utils/helpers.d.ts +12 -0
- package/dist/esm/utils/helpers.d.ts.map +1 -1
- package/dist/esm/utils/helpers.js +20 -0
- package/dist/esm/utils/toThrow.d.ts.map +1 -1
- package/dist/esm/utils/toThrow.js +2 -1
- 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 {
|
|
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:
|
|
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 {};
|
package/dist/esm/run.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run.ts"],"names":[],"mappings":"
|
|
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,
|
package/dist/esm/runner.d.ts
CHANGED
package/dist/esm/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"
|
|
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"}
|