@unrift/core 0.3.0 → 0.4.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 +5 -0
- package/dist/esm/expect.d.ts +12 -0
- package/dist/esm/expect.d.ts.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/matchers.d.ts.map +1 -1
- package/dist/esm/matchers.js +161 -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/runner.d.ts.map +1 -1
- package/dist/esm/runner.js +13 -8
- package/dist/esm/test.d.ts.map +1 -1
- package/dist/esm/test.js +12 -5
- package/dist/esm/utils/compareEngine.js +3 -3
- package/dist/esm/utils/discoverTestFiles.js +1 -1
- package/dist/esm/utils/helpers.d.ts +10 -0
- package/dist/esm/utils/helpers.d.ts.map +1 -1
- package/dist/esm/utils/helpers.js +13 -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/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":"AAwBA,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"}
|
package/dist/esm/runner.js
CHANGED
|
@@ -8,6 +8,11 @@ import { runEngine } from "./run.js";
|
|
|
8
8
|
import { normalizePath } from "./utils/normalizePath.js";
|
|
9
9
|
import { safeRegExp, isGlobPattern, globToRegex } from "./utils/helpers.js";
|
|
10
10
|
import { TaskStatus } from "./types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Captured at module load so the runner's own timing measurements
|
|
13
|
+
* aren't affected by user mocks (e.g. mock.useFakeTimers patches performance.now).
|
|
14
|
+
*/
|
|
15
|
+
const realPerformanceNow = performance.now.bind(performance);
|
|
11
16
|
function debugLog(enabled, json, ...args) {
|
|
12
17
|
// Keep stdout clean for --json mode; use stderr for debug.
|
|
13
18
|
if (!enabled)
|
|
@@ -144,7 +149,7 @@ function extractTestName(description) {
|
|
|
144
149
|
}
|
|
145
150
|
export async function runTestsCLI(options = {}) {
|
|
146
151
|
regExpCache.clear();
|
|
147
|
-
const runStart =
|
|
152
|
+
const runStart = realPerformanceNow();
|
|
148
153
|
// Cache clean mode
|
|
149
154
|
if (options.cacheClean) {
|
|
150
155
|
const projectRoot = process.cwd();
|
|
@@ -159,7 +164,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
159
164
|
if (result.tempCacheDeleted)
|
|
160
165
|
lines.push("✔ deleted temp cache");
|
|
161
166
|
if (lines.length === 0)
|
|
162
|
-
lines.push("
|
|
167
|
+
lines.push("- nothing to clean");
|
|
163
168
|
console.log(lines.join("\n"));
|
|
164
169
|
}
|
|
165
170
|
return;
|
|
@@ -215,7 +220,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
215
220
|
testDir: dir,
|
|
216
221
|
configPath: resolvedConfigPath,
|
|
217
222
|
files: [],
|
|
218
|
-
durationMs:
|
|
223
|
+
durationMs: realPerformanceNow() - runStart,
|
|
219
224
|
results: [],
|
|
220
225
|
};
|
|
221
226
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -265,7 +270,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
265
270
|
});
|
|
266
271
|
}
|
|
267
272
|
const ok = failed === 0;
|
|
268
|
-
const durationMs =
|
|
273
|
+
const durationMs = realPerformanceNow() - runStart;
|
|
269
274
|
// JSON mode: stdout must be JSON only
|
|
270
275
|
if (options.json) {
|
|
271
276
|
const report = {
|
|
@@ -288,7 +293,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
288
293
|
process.exitCode = 1;
|
|
289
294
|
return;
|
|
290
295
|
}
|
|
291
|
-
//
|
|
296
|
+
// Pretty reporting
|
|
292
297
|
const groups = groupResultsByFile(results);
|
|
293
298
|
// File-by-file results
|
|
294
299
|
for (const group of groups) {
|
|
@@ -343,7 +348,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
343
348
|
}
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
|
-
//
|
|
351
|
+
// Failure details
|
|
347
352
|
if (failures.length > 0) {
|
|
348
353
|
console.log(`\n${horizontalRule(colors.boldRed(" FAILURES "))}\n`);
|
|
349
354
|
failures.forEach((r, i) => {
|
|
@@ -373,7 +378,7 @@ export async function runTestsCLI(options = {}) {
|
|
|
373
378
|
});
|
|
374
379
|
console.log(`\n${horizontalRule()}`);
|
|
375
380
|
}
|
|
376
|
-
//
|
|
381
|
+
// Summary
|
|
377
382
|
console.log("");
|
|
378
383
|
const summaryParts = [];
|
|
379
384
|
if (passed > 0)
|
|
@@ -461,7 +466,7 @@ export async function watchTestsCLI(options = {}) {
|
|
|
461
466
|
running = true;
|
|
462
467
|
const shortFile = shortenPath(filename);
|
|
463
468
|
console.log(`\n${colors.dim("─".repeat(Math.min(process.stdout.columns || 80, 80)))}`);
|
|
464
|
-
console.log(` ${colors.dim("↺")} ${colors.dim(`${shortFile} changed
|
|
469
|
+
console.log(` ${colors.dim("↺")} ${colors.dim(`${shortFile} changed - re-running…`)}\n`);
|
|
465
470
|
// Reset exit code so each run is evaluated independently
|
|
466
471
|
process.exitCode = 0;
|
|
467
472
|
try {
|
package/dist/esm/test.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AASlE,qBAAa,IAAI;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAK;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC;IAEd,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,EAAE,QAAQ,CAAC;IAEf,MAAM,EAAE,UAAU,CAAsB;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAGjB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,EACtB,IAAI,GAAE,QAA2B,EACjC,SAAS,CAAC,EAAE,MAAM;IAQd,GAAG,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAyDnD"}
|