@uniflowed/test 0.0.0-alpha.1 → 0.0.0-alpha.11
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/index.js +21 -2
- package/internal/asymmetric.js +184 -0
- package/internal/equality.js +79 -19
- package/internal/expect.js +323 -81
- package/internal/frames.js +96 -4
- package/internal/modules.js +430 -0
- package/internal/namespace.js +287 -0
- package/internal/output.js +309 -0
- package/internal/registry.js +30 -27
- package/internal/run.js +94 -42
- package/internal/snapshot.js +306 -0
- package/internal/spy.js +236 -0
- package/internal/timers.js +362 -0
- package/internal/unsupported.js +30 -0
- package/package.json +5 -2
- package/worker.js +163 -25
package/internal/spy.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: the spy behind `fn` and `uft.spyOn`.
|
|
4
|
+
//
|
|
5
|
+
// Shaped after Vitest's, because a project moving to uf should not have to
|
|
6
|
+
// rewrite its assertions. That means `mock.calls`, `mock.results`,
|
|
7
|
+
// `mock.lastCall`, the `Once` variants, and `mockReset` and `mockRestore`
|
|
8
|
+
// meaning the two different things they mean there.
|
|
9
|
+
//
|
|
10
|
+
// The three reset verbs are easy to conflate and are genuinely different:
|
|
11
|
+
//
|
|
12
|
+
// * `mockClear` forgets the calls, and keeps the implementation.
|
|
13
|
+
// * `mockReset` forgets the calls *and* the implementation, leaving the
|
|
14
|
+
// original one a `spyOn` captured — or nothing, for a bare `fn`.
|
|
15
|
+
// * `mockRestore` does what `mockReset` does and then puts the real method
|
|
16
|
+
// back on the object, which only a `spyOn` has to put back.
|
|
17
|
+
//
|
|
18
|
+
// Every spy is registered, so `uft.clearAllMocks` and its siblings can reach the
|
|
19
|
+
// ones a test never held a reference to.
|
|
20
|
+
|
|
21
|
+
/** One call: what went in, and what came out. */
|
|
22
|
+
export type SpyCall = {
|
|
23
|
+
readonly args: $ReadOnlyArray<mixed>,
|
|
24
|
+
readonly returned?: mixed,
|
|
25
|
+
readonly threw?: mixed,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** One call's outcome, in the shape Vitest reports it. */
|
|
29
|
+
export type SpyResult =
|
|
30
|
+
| { readonly type: "return", readonly value: mixed }
|
|
31
|
+
| { readonly type: "throw", readonly value: mixed };
|
|
32
|
+
|
|
33
|
+
/** Every spy made in this process, so the `All` verbs can reach them. */
|
|
34
|
+
const registry: Array<$FlowFixMe> = [];
|
|
35
|
+
|
|
36
|
+
/** How a spy puts back what it replaced, when it replaced something. */
|
|
37
|
+
type Restore = null | (() => void);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A spy, optionally standing in for something it can put back.
|
|
41
|
+
*
|
|
42
|
+
* `restore` is what separates `fn()` from `spyOn(object, "method")`: the second
|
|
43
|
+
* took something off an object and owes it back.
|
|
44
|
+
*/
|
|
45
|
+
function makeSpy(implementation: mixed, restore: Restore, name: string): $FlowFixMe {
|
|
46
|
+
const calls: Array<SpyCall> = [];
|
|
47
|
+
const results: Array<SpyResult> = [];
|
|
48
|
+
const instances: Array<mixed> = [];
|
|
49
|
+
// Implementations queued by the `Once` variants, taken from the front.
|
|
50
|
+
const queued: Array<mixed> = [];
|
|
51
|
+
|
|
52
|
+
const original = implementation;
|
|
53
|
+
let current = implementation;
|
|
54
|
+
let mockName = name;
|
|
55
|
+
|
|
56
|
+
const spy: $FlowFixMe = function (...args: $ReadOnlyArray<mixed>) {
|
|
57
|
+
// `this` is recorded because a spy on a method is often called as one, and
|
|
58
|
+
// `mock.instances` is how a test asserts on the receiver.
|
|
59
|
+
instances.push(this);
|
|
60
|
+
const body = queued.length > 0 ? queued.shift() : current;
|
|
61
|
+
try {
|
|
62
|
+
const returned = typeof body === "function" ? body.apply(this, args) : undefined;
|
|
63
|
+
calls.push({ args, returned });
|
|
64
|
+
results.push({ type: "return", value: returned });
|
|
65
|
+
return returned;
|
|
66
|
+
} catch (thrown) {
|
|
67
|
+
calls.push({ args, threw: thrown });
|
|
68
|
+
results.push({ type: "throw", value: thrown });
|
|
69
|
+
throw thrown;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
spy.mock = {
|
|
74
|
+
calls,
|
|
75
|
+
results,
|
|
76
|
+
instances,
|
|
77
|
+
get lastCall(): $ReadOnlyArray<mixed> | void {
|
|
78
|
+
return calls.length === 0 ? undefined : calls[calls.length - 1].args;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
spy.mockClear = () => {
|
|
83
|
+
calls.length = 0;
|
|
84
|
+
results.length = 0;
|
|
85
|
+
instances.length = 0;
|
|
86
|
+
return spy;
|
|
87
|
+
};
|
|
88
|
+
spy.mockReset = () => {
|
|
89
|
+
spy.mockClear();
|
|
90
|
+
queued.length = 0;
|
|
91
|
+
current = original;
|
|
92
|
+
return spy;
|
|
93
|
+
};
|
|
94
|
+
spy.mockRestore = () => {
|
|
95
|
+
spy.mockReset();
|
|
96
|
+
if (restore != null) {
|
|
97
|
+
restore();
|
|
98
|
+
}
|
|
99
|
+
return spy;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
spy.mockImplementation = (next: mixed) => {
|
|
103
|
+
current = next;
|
|
104
|
+
return spy;
|
|
105
|
+
};
|
|
106
|
+
spy.mockImplementationOnce = (next: mixed) => {
|
|
107
|
+
queued.push(next);
|
|
108
|
+
return spy;
|
|
109
|
+
};
|
|
110
|
+
spy.withImplementation = (next: mixed, body: () => mixed) => {
|
|
111
|
+
const previous = current;
|
|
112
|
+
current = next;
|
|
113
|
+
try {
|
|
114
|
+
const out = body();
|
|
115
|
+
// An async body has to put the implementation back when it settles, not
|
|
116
|
+
// when it starts, or the next test runs against this one's stand-in.
|
|
117
|
+
if (out != null && typeof (out: $FlowFixMe).then === "function") {
|
|
118
|
+
return (out: $FlowFixMe).finally(() => {
|
|
119
|
+
current = previous;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
current = previous;
|
|
123
|
+
return out;
|
|
124
|
+
} catch (thrown) {
|
|
125
|
+
current = previous;
|
|
126
|
+
throw thrown;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
spy.mockReturnValue = (value: mixed) => spy.mockImplementation(() => value);
|
|
131
|
+
spy.mockReturnValueOnce = (value: mixed) => spy.mockImplementationOnce(() => value);
|
|
132
|
+
spy.mockResolvedValue = (value: mixed) => spy.mockImplementation(() => Promise.resolve(value));
|
|
133
|
+
spy.mockResolvedValueOnce = (value: mixed) =>
|
|
134
|
+
spy.mockImplementationOnce(() => Promise.resolve(value));
|
|
135
|
+
spy.mockRejectedValue = (reason: mixed) => spy.mockImplementation(() => Promise.reject(reason));
|
|
136
|
+
spy.mockRejectedValueOnce = (reason: mixed) =>
|
|
137
|
+
spy.mockImplementationOnce(() => Promise.reject(reason));
|
|
138
|
+
spy.mockReturnThis = () =>
|
|
139
|
+
spy.mockImplementation(function () {
|
|
140
|
+
return this;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
spy.mockName = (next: string) => {
|
|
144
|
+
mockName = next;
|
|
145
|
+
return spy;
|
|
146
|
+
};
|
|
147
|
+
spy.getMockName = () => mockName;
|
|
148
|
+
|
|
149
|
+
registry.push(spy);
|
|
150
|
+
return spy;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A spy with no original behind it.
|
|
155
|
+
*
|
|
156
|
+
* `fn()` records and returns `undefined`; `fn(body)` records and runs `body`.
|
|
157
|
+
*/
|
|
158
|
+
export function fn(implementation?: mixed): $FlowFixMe {
|
|
159
|
+
return makeSpy(implementation, null, "spy");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Replace `object[method]` with a spy that calls through to it.
|
|
164
|
+
*
|
|
165
|
+
* Calls through by default, which is what makes `spyOn` an observation rather
|
|
166
|
+
* than a replacement — a test that only wants to know a method was called does
|
|
167
|
+
* not have to reimplement it. `mockImplementation` is how a test says it wants
|
|
168
|
+
* the other thing.
|
|
169
|
+
*
|
|
170
|
+
* The original is put back by `mockRestore`, and by `uft.restoreAllMocks`.
|
|
171
|
+
*/
|
|
172
|
+
export function spyOn(object: mixed, method: string): $FlowFixMe {
|
|
173
|
+
if (object == null || (typeof object !== "object" && typeof object !== "function")) {
|
|
174
|
+
throw new TypeError(`uft.spyOn: cannot spy on ${describe(object)}`);
|
|
175
|
+
}
|
|
176
|
+
const target = object as $FlowFixMe;
|
|
177
|
+
const original = target[method];
|
|
178
|
+
if (typeof original !== "function") {
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
`uft.spyOn: ${method} is ${describe(original)}, and only a method can be spied on`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const owned = Object.hasOwn(target, method);
|
|
185
|
+
const spy = makeSpy(
|
|
186
|
+
original,
|
|
187
|
+
() => {
|
|
188
|
+
// Deleting rather than reassigning when the method was inherited: writing
|
|
189
|
+
// the original onto the instance would leave a copy the prototype no longer
|
|
190
|
+
// controls, and the next change to the prototype would not be seen.
|
|
191
|
+
if (owned) {
|
|
192
|
+
target[method] = original;
|
|
193
|
+
} else {
|
|
194
|
+
delete target[method];
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
method,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
target[method] = spy;
|
|
201
|
+
return spy;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Whether `value` is one of these spies. */
|
|
205
|
+
export function isSpy(value: mixed): boolean {
|
|
206
|
+
return typeof value === "function" && (value: $FlowFixMe).mock != null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Forget every spy's calls, keeping their implementations. */
|
|
210
|
+
export function clearAllMocks(): void {
|
|
211
|
+
for (const spy of registry) {
|
|
212
|
+
spy.mockClear();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Forget every spy's calls and implementations. */
|
|
217
|
+
export function resetAllMocks(): void {
|
|
218
|
+
for (const spy of registry) {
|
|
219
|
+
spy.mockReset();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Put back everything `spyOn` replaced. */
|
|
224
|
+
export function restoreAllMocks(): void {
|
|
225
|
+
for (const spy of registry) {
|
|
226
|
+
spy.mockRestore();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A readable name for a value, for an error message. */
|
|
231
|
+
function describe(value: mixed): string {
|
|
232
|
+
if (value === null) {
|
|
233
|
+
return "null";
|
|
234
|
+
}
|
|
235
|
+
return typeof value;
|
|
236
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: a clock a test controls.
|
|
4
|
+
//
|
|
5
|
+
// A test about "after five minutes the session expires" should not take five
|
|
6
|
+
// minutes, and a test that waits a real 50ms to see a debounce is a test that
|
|
7
|
+
// fails on a loaded machine. Fake timers replace the scheduling globals with a
|
|
8
|
+
// queue the test advances by hand, so elapsed time becomes an input rather than
|
|
9
|
+
// something to wait for.
|
|
10
|
+
//
|
|
11
|
+
// # What is faked
|
|
12
|
+
//
|
|
13
|
+
// `setTimeout`, `setInterval`, `setImmediate` and their `clear` partners, plus
|
|
14
|
+
// `Date.now` and `new Date()` with no arguments. Not `queueMicrotask` and not
|
|
15
|
+
// promises: a microtask is not scheduled *in time*, it runs at the end of the
|
|
16
|
+
// current turn, and pretending otherwise would let a test claim a promise
|
|
17
|
+
// resolved "after 100ms" when the two have nothing to do with each other. The
|
|
18
|
+
// async advance methods are the honest way to let microtasks run in between.
|
|
19
|
+
//
|
|
20
|
+
// # Why the ids are numbers
|
|
21
|
+
//
|
|
22
|
+
// Node's `setTimeout` returns a `Timeout` object and a browser's returns a
|
|
23
|
+
// number, and code written for either passes what it got straight to
|
|
24
|
+
// `clearTimeout`. A number works in both places — `clearTimeout` only ever
|
|
25
|
+
// looks the value up — and a test that stores one in a `Map` keyed by number
|
|
26
|
+
// keeps working.
|
|
27
|
+
|
|
28
|
+
/** One scheduled callback. */
|
|
29
|
+
type Task = {
|
|
30
|
+
readonly id: number,
|
|
31
|
+
/** When it is due, on the fake clock. */
|
|
32
|
+
due: number,
|
|
33
|
+
/** How often it repeats, or `null` for a one-shot. */
|
|
34
|
+
readonly every: number | null,
|
|
35
|
+
readonly body: (...args: $ReadOnlyArray<mixed>) => mixed,
|
|
36
|
+
readonly args: $ReadOnlyArray<mixed>,
|
|
37
|
+
/** Ordering among tasks due at the same instant: first scheduled, first run. */
|
|
38
|
+
readonly sequence: number,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The globals a clock replaces, so they can be put back exactly. */
|
|
42
|
+
type Saved = {
|
|
43
|
+
readonly setTimeout: mixed,
|
|
44
|
+
readonly clearTimeout: mixed,
|
|
45
|
+
readonly setInterval: mixed,
|
|
46
|
+
readonly clearInterval: mixed,
|
|
47
|
+
readonly setImmediate: mixed,
|
|
48
|
+
readonly clearImmediate: mixed,
|
|
49
|
+
readonly Date: mixed,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* How many times a run loop will fire a timer before deciding it will not stop.
|
|
54
|
+
*
|
|
55
|
+
* An interval reschedules itself, so `runAllTimers` on one would never finish.
|
|
56
|
+
* A bound turns an unbounded hang into a failure that names the problem, which
|
|
57
|
+
* is the difference between a test suite that stops and one that has to be
|
|
58
|
+
* killed.
|
|
59
|
+
*/
|
|
60
|
+
const RUNAWAY_LIMIT = 10_000;
|
|
61
|
+
|
|
62
|
+
let installed: Saved | null = null;
|
|
63
|
+
let now = 0;
|
|
64
|
+
let nextId = 1;
|
|
65
|
+
let sequence = 0;
|
|
66
|
+
let tasks: Array<Task> = [];
|
|
67
|
+
|
|
68
|
+
/** Whether a clock is currently installed. */
|
|
69
|
+
export function isFaked(): boolean {
|
|
70
|
+
return installed != null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The globals to patch, in one place so install and restore cannot diverge. */
|
|
74
|
+
function host(): $FlowFixMe {
|
|
75
|
+
return globalThis;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Replace the scheduling globals with a clock this module controls.
|
|
80
|
+
*
|
|
81
|
+
* `now` starts at the real current time rather than at zero, so a test that
|
|
82
|
+
* formats a date sees a plausible one — and `setSystemTime` is how a test that
|
|
83
|
+
* cares says which.
|
|
84
|
+
*/
|
|
85
|
+
export function useFakeTimers(): void {
|
|
86
|
+
if (installed != null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const global = host();
|
|
90
|
+
installed = {
|
|
91
|
+
setTimeout: global.setTimeout,
|
|
92
|
+
clearTimeout: global.clearTimeout,
|
|
93
|
+
setInterval: global.setInterval,
|
|
94
|
+
clearInterval: global.clearInterval,
|
|
95
|
+
setImmediate: global.setImmediate,
|
|
96
|
+
clearImmediate: global.clearImmediate,
|
|
97
|
+
Date: global.Date,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
now = Date.now();
|
|
101
|
+
tasks = [];
|
|
102
|
+
nextId = 1;
|
|
103
|
+
sequence = 0;
|
|
104
|
+
|
|
105
|
+
global.setTimeout = (body: $FlowFixMe, delay?: number, ...args: $ReadOnlyArray<mixed>) =>
|
|
106
|
+
schedule(body, delay ?? 0, null, args);
|
|
107
|
+
global.setInterval = (body: $FlowFixMe, delay?: number, ...args: $ReadOnlyArray<mixed>) =>
|
|
108
|
+
// A zero-delay interval would be scheduled at the same instant forever, so
|
|
109
|
+
// it advances by one tick — which is what every runtime does with it.
|
|
110
|
+
schedule(body, delay ?? 0, Math.max(delay ?? 0, 1), args);
|
|
111
|
+
// `setImmediate` is "before any timer, after this turn", which on a fake
|
|
112
|
+
// clock is a zero-delay timer that sorts ahead by having been scheduled at
|
|
113
|
+
// the current instant.
|
|
114
|
+
global.setImmediate = (body: $FlowFixMe, ...args: $ReadOnlyArray<mixed>) =>
|
|
115
|
+
schedule(body, 0, null, args);
|
|
116
|
+
global.clearTimeout = cancel;
|
|
117
|
+
global.clearInterval = cancel;
|
|
118
|
+
global.clearImmediate = cancel;
|
|
119
|
+
global.Date = fakeDate(installed.Date as $FlowFixMe);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Put the real scheduling globals back. */
|
|
123
|
+
export function useRealTimers(): void {
|
|
124
|
+
if (installed == null) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const global = host();
|
|
128
|
+
global.setTimeout = installed.setTimeout;
|
|
129
|
+
global.clearTimeout = installed.clearTimeout;
|
|
130
|
+
global.setInterval = installed.setInterval;
|
|
131
|
+
global.clearInterval = installed.clearInterval;
|
|
132
|
+
global.setImmediate = installed.setImmediate;
|
|
133
|
+
global.clearImmediate = installed.clearImmediate;
|
|
134
|
+
global.Date = installed.Date;
|
|
135
|
+
installed = null;
|
|
136
|
+
tasks = [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A `Date` whose "now" is the fake clock's.
|
|
141
|
+
*
|
|
142
|
+
* A proxy over the real constructor rather than a subclass of it, and the
|
|
143
|
+
* difference is three bugs rather than a preference:
|
|
144
|
+
*
|
|
145
|
+
* * `Date()` — called as a function, with no `new` — returns a string in every
|
|
146
|
+
* runtime. A `class` cannot be called that way at all, so a subclass turned
|
|
147
|
+
* every such call into a `TypeError`, under fake timers only.
|
|
148
|
+
* * `before instanceof Date`, for a date built *before* the clock was faked,
|
|
149
|
+
* was false: the object's prototype chain runs through the real `Date` and a
|
|
150
|
+
* subclass's prototype is not in it. Anything branching on that — including
|
|
151
|
+
* `setSystemTime` below, once — silently took the wrong branch. A proxy has
|
|
152
|
+
* no prototype of its own, so the check is the real one.
|
|
153
|
+
* * `Date.name` was `"FakeDate"`, and a subclass's own `toString` reads as
|
|
154
|
+
* class source rather than native code.
|
|
155
|
+
*
|
|
156
|
+
* Only `now` is replaced. `parse`, `UTC`, and every prototype method are the
|
|
157
|
+
* real ones, reached through the proxy.
|
|
158
|
+
*/
|
|
159
|
+
function fakeDate(Real: $FlowFixMe): $FlowFixMe {
|
|
160
|
+
return new Proxy(Real, {
|
|
161
|
+
// `Date(anything)` ignores its arguments and returns the current time as a
|
|
162
|
+
// string, which on this clock is the fake one.
|
|
163
|
+
apply(): string {
|
|
164
|
+
return new Real(now).toString();
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
construct(target: $FlowFixMe, args: $ReadOnlyArray<mixed>, newTarget: $FlowFixMe) {
|
|
168
|
+
// Only the no-argument form reads the clock; every other form is
|
|
169
|
+
// constructing a specific date and has nothing to do with "now".
|
|
170
|
+
const actual = args.length === 0 ? [now] : args;
|
|
171
|
+
// `newTarget` rather than `Real`, so a subclass of the faked `Date` gets
|
|
172
|
+
// its own prototype instead of the real one's.
|
|
173
|
+
return Reflect.construct(Real, actual, newTarget === undefined ? Real : newTarget);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
get(target: $FlowFixMe, property: $FlowFixMe, receiver: $FlowFixMe): $FlowFixMe {
|
|
177
|
+
if (property === "now") {
|
|
178
|
+
return fakeNow;
|
|
179
|
+
}
|
|
180
|
+
return Reflect.get(target, property, receiver);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `Date.now`, as its own function so the proxy hands back a stable identity. */
|
|
186
|
+
function fakeNow(): number {
|
|
187
|
+
return now;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Put a task on the queue and hand back its id. */
|
|
191
|
+
function schedule(
|
|
192
|
+
body: $FlowFixMe,
|
|
193
|
+
delay: number,
|
|
194
|
+
every: number | null,
|
|
195
|
+
args: $ReadOnlyArray<mixed>,
|
|
196
|
+
): number {
|
|
197
|
+
const id = nextId;
|
|
198
|
+
nextId += 1;
|
|
199
|
+
sequence += 1;
|
|
200
|
+
tasks.push({
|
|
201
|
+
id,
|
|
202
|
+
due: now + Math.max(delay, 0),
|
|
203
|
+
every,
|
|
204
|
+
body,
|
|
205
|
+
args,
|
|
206
|
+
sequence,
|
|
207
|
+
});
|
|
208
|
+
return id;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Take a task off the queue. Unknown ids are ignored, as the real ones are. */
|
|
212
|
+
function cancel(id: mixed): void {
|
|
213
|
+
tasks = tasks.filter((task) => task.id !== id);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** How many timers are waiting. */
|
|
217
|
+
export function getTimerCount(): number {
|
|
218
|
+
return tasks.length;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The next task due, or `null`. Ties break by scheduling order. */
|
|
222
|
+
function nextTask(before: number): Task | null {
|
|
223
|
+
let best: Task | null = null;
|
|
224
|
+
for (const task of tasks) {
|
|
225
|
+
if (task.due > before) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
best == null ||
|
|
230
|
+
task.due < best.due ||
|
|
231
|
+
(task.due === best.due && task.sequence < best.sequence)
|
|
232
|
+
) {
|
|
233
|
+
best = task;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return best;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Run one task, rescheduling it when it repeats. */
|
|
240
|
+
function fire(task: Task): void {
|
|
241
|
+
now = task.due;
|
|
242
|
+
if (task.every == null) {
|
|
243
|
+
cancel(task.id);
|
|
244
|
+
} else {
|
|
245
|
+
task.due = now + task.every;
|
|
246
|
+
}
|
|
247
|
+
task.body(...task.args);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Raised when a run loop will not terminate. */
|
|
251
|
+
export class RunawayTimersError extends Error {
|
|
252
|
+
constructor(method: string) {
|
|
253
|
+
super(
|
|
254
|
+
`uf.${method}: still firing after ${RUNAWAY_LIMIT} timers. ` +
|
|
255
|
+
"A timer that reschedules itself never drains — advance the clock by a " +
|
|
256
|
+
"fixed amount instead.",
|
|
257
|
+
);
|
|
258
|
+
this.name = "RunawayTimersError";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Move the clock forward, firing everything that comes due. */
|
|
263
|
+
export function advanceTimersByTime(millis: number): void {
|
|
264
|
+
const target = now + Math.max(millis, 0);
|
|
265
|
+
let fired = 0;
|
|
266
|
+
for (;;) {
|
|
267
|
+
const task = nextTask(target);
|
|
268
|
+
if (task == null) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
fired += 1;
|
|
272
|
+
if (fired > RUNAWAY_LIMIT) {
|
|
273
|
+
throw new RunawayTimersError("advanceTimersByTime");
|
|
274
|
+
}
|
|
275
|
+
fire(task);
|
|
276
|
+
}
|
|
277
|
+
// Land on the requested instant even when nothing was due there, so two
|
|
278
|
+
// advances of 50ms are the same as one of 100ms.
|
|
279
|
+
now = target;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** The same, yielding to the microtask queue between timers. */
|
|
283
|
+
export async function advanceTimersByTimeAsync(millis: number): Promise<void> {
|
|
284
|
+
const target = now + Math.max(millis, 0);
|
|
285
|
+
let fired = 0;
|
|
286
|
+
for (;;) {
|
|
287
|
+
const task = nextTask(target);
|
|
288
|
+
if (task == null) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
fired += 1;
|
|
292
|
+
if (fired > RUNAWAY_LIMIT) {
|
|
293
|
+
throw new RunawayTimersError("advanceTimersByTimeAsync");
|
|
294
|
+
}
|
|
295
|
+
fire(task);
|
|
296
|
+
// The point of the async form: a callback that awaited something gets to
|
|
297
|
+
// continue before the next timer fires.
|
|
298
|
+
await Promise.resolve();
|
|
299
|
+
}
|
|
300
|
+
now = target;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Fire the next timer due, whenever it is due. */
|
|
304
|
+
export function advanceTimersToNextTimer(): void {
|
|
305
|
+
const task = nextTask(Number.POSITIVE_INFINITY);
|
|
306
|
+
if (task != null) {
|
|
307
|
+
fire(task);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Fire everything until the queue is empty. */
|
|
312
|
+
export function runAllTimers(): void {
|
|
313
|
+
let fired = 0;
|
|
314
|
+
for (;;) {
|
|
315
|
+
const task = nextTask(Number.POSITIVE_INFINITY);
|
|
316
|
+
if (task == null) {
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
fired += 1;
|
|
320
|
+
if (fired > RUNAWAY_LIMIT) {
|
|
321
|
+
throw new RunawayTimersError("runAllTimers");
|
|
322
|
+
}
|
|
323
|
+
fire(task);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Fire only what is already queued, not what those callbacks schedule.
|
|
329
|
+
*
|
|
330
|
+
* The way to drain an interval once: `runAllTimers` on one never terminates,
|
|
331
|
+
* and this fires each waiting task exactly once.
|
|
332
|
+
*/
|
|
333
|
+
export function runOnlyPendingTimers(): void {
|
|
334
|
+
const pending = [...tasks].sort((a, b) => a.due - b.due || a.sequence - b.sequence);
|
|
335
|
+
for (const task of pending) {
|
|
336
|
+
// A callback may have cancelled a later one, so check it is still queued.
|
|
337
|
+
if (tasks.some((queued) => queued.id === task.id)) {
|
|
338
|
+
fire(task);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Move the wall clock without firing anything.
|
|
345
|
+
*
|
|
346
|
+
* Duck-typed rather than `instanceof Date`, and deliberately: `Date` here is
|
|
347
|
+
* whatever is installed, so the check would be asking about the *fake* one
|
|
348
|
+
* while the caller may be holding a date made before the clock was faked, or
|
|
349
|
+
* one from another realm. Either answered `false`, fell through to the number
|
|
350
|
+
* branch, and set the clock to an object — after which every comparison
|
|
351
|
+
* against it was `false` and no timer ever came due.
|
|
352
|
+
*
|
|
353
|
+
* Anything that can say what instant it is, is an instant.
|
|
354
|
+
*/
|
|
355
|
+
export function setSystemTime(time: number | Date): void {
|
|
356
|
+
now = typeof time === "number" ? time : Number((time as $FlowFixMe).getTime());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** What the fake clock currently reads. */
|
|
360
|
+
export function getMockedSystemTime(): Date | null {
|
|
361
|
+
return installed == null ? null : new (installed.Date as $FlowFixMe)(now);
|
|
362
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// Internal to `@uniflowed/test`: the error a `uft` binding raises when this
|
|
4
|
+
// host cannot give it.
|
|
5
|
+
//
|
|
6
|
+
// Its own module because two of them need it — the namespace, and the module
|
|
7
|
+
// mocking in `./modules.js` that the namespace imports — and a class that both
|
|
8
|
+
// of a pair of modules reaches for is a third module, not an import cycle.
|
|
9
|
+
//
|
|
10
|
+
// The class is exported from the package, so a test can catch it by name
|
|
11
|
+
// rather than by matching on a message.
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Raised for a `uft` binding this host cannot provide.
|
|
15
|
+
*
|
|
16
|
+
* Names what the binding needs rather than only that it is missing: a reader
|
|
17
|
+
* who is told that module interception needs synchronous module hooks can
|
|
18
|
+
* decide whether to run the suite on another host or to restructure the test,
|
|
19
|
+
* and neither is a decision a bare "not implemented" supports.
|
|
20
|
+
*/
|
|
21
|
+
export class UnsupportedError extends Error {
|
|
22
|
+
/** The binding that was called. */
|
|
23
|
+
binding: string;
|
|
24
|
+
|
|
25
|
+
constructor(binding: string, reason: string) {
|
|
26
|
+
super(`uft.${binding} is not available: ${reason}`);
|
|
27
|
+
this.name = "UnsupportedError";
|
|
28
|
+
this.binding = binding;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/test",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.11",
|
|
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",
|
|
@@ -19,5 +19,8 @@
|
|
|
19
19
|
"index.js",
|
|
20
20
|
"worker.js",
|
|
21
21
|
"internal"
|
|
22
|
-
]
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@uniflowed/host": "0.0.0-alpha.11"
|
|
25
|
+
}
|
|
23
26
|
}
|