@c9up/helix 0.1.4 → 0.1.5
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/dist/runtime/suite.d.ts +26 -0
- package/dist/runtime/suite.d.ts.map +1 -1
- package/dist/runtime/suite.js +21 -18
- package/dist/runtime/suite.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +2 -2
- package/src/cli/coverage/aggregate.ts +0 -231
- package/src/cli/coverage/collect.ts +0 -63
- package/src/cli/coverage/diff/base.ts +0 -46
- package/src/cli/coverage/diff/index.ts +0 -160
- package/src/cli/coverage/diff/overlay.ts +0 -62
- package/src/cli/coverage/diff/parse.ts +0 -121
- package/src/cli/coverage/diff/reporters.ts +0 -82
- package/src/cli/coverage/diff/types.ts +0 -46
- package/src/cli/coverage/filter.ts +0 -71
- package/src/cli/coverage/glob.ts +0 -0
- package/src/cli/coverage/index.ts +0 -126
- package/src/cli/coverage/reporters/json.ts +0 -40
- package/src/cli/coverage/reporters/lcov.ts +0 -54
- package/src/cli/coverage/reporters/text.ts +0 -48
- package/src/cli/coverage/thresholds.ts +0 -73
- package/src/cli/coverage/types.ts +0 -93
- package/src/cli/discover.ts +0 -174
- package/src/cli/native.ts +0 -104
- package/src/cli/pool.ts +0 -486
- package/src/cli/reporter.ts +0 -155
- package/src/cli/run.ts +0 -440
- package/src/cli/summary.ts +0 -42
- package/src/cli/watch/loop.ts +0 -159
- package/src/cli/watch/types.ts +0 -22
- package/src/cli/watch/watcher.ts +0 -145
- package/src/container/index.ts +0 -16
- package/src/container/override.ts +0 -86
- package/src/container/spy.ts +0 -25
- package/src/index.ts +0 -42
- package/src/runtime/assertion-error.ts +0 -38
- package/src/runtime/cli-worker.ts +0 -140
- package/src/runtime/equals.ts +0 -400
- package/src/runtime/expect.ts +0 -173
- package/src/runtime/index.ts +0 -50
- package/src/runtime/lifecycle.ts +0 -17
- package/src/runtime/matchers.ts +0 -452
- package/src/runtime/run.ts +0 -573
- package/src/runtime/suite.ts +0 -310
- package/src/runtime/test-context.ts +0 -59
- package/src/runtime/vi/fake-timers.ts +0 -410
- package/src/runtime/vi/index.ts +0 -254
- package/src/runtime/vi/spy.ts +0 -224
- package/src/runtime/vi/spyOn.ts +0 -155
- package/src/runtime/vi/system-time.ts +0 -121
- package/src/runtime/worker.ts +0 -239
- package/src/time/freeze.ts +0 -229
- package/src/time/index.ts +0 -16
package/src/runtime/vi/spy.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spy core — a callable function that records every call and exposes a
|
|
3
|
-
* Vitest-shape `.mock` surface. Wears the `__helixIsSpy` brand so the
|
|
4
|
-
* matchers in `matchers.ts` (`toHaveBeenCalled*`) recognise it.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface MockCallResult<Return = unknown> {
|
|
8
|
-
/** `"incomplete"` is pushed synchronously at call entry and patched when the call resolves — matches Vitest so `mock.results.length === mock.calls.length` even for in-flight async. */
|
|
9
|
-
type: "return" | "throw" | "incomplete";
|
|
10
|
-
value: Return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface MockInternals<
|
|
14
|
-
Args extends readonly unknown[] = readonly unknown[],
|
|
15
|
-
Return = unknown,
|
|
16
|
-
> {
|
|
17
|
-
calls: Args[];
|
|
18
|
-
results: MockCallResult<Return>[];
|
|
19
|
-
/** The most recent call's arguments. Mirrors Vitest. */
|
|
20
|
-
lastCall: Args | undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type AnyFn = (...args: unknown[]) => unknown;
|
|
24
|
-
|
|
25
|
-
type CallSignature<Fn extends AnyFn> = (
|
|
26
|
-
...args: Parameters<Fn>
|
|
27
|
-
) => ReturnType<Fn>;
|
|
28
|
-
|
|
29
|
-
interface SpyMethods<Fn extends AnyFn> {
|
|
30
|
-
readonly __helixIsSpy: true;
|
|
31
|
-
readonly mock: MockInternals<Parameters<Fn>, ReturnType<Fn>>;
|
|
32
|
-
readonly calls: Parameters<Fn>[];
|
|
33
|
-
readonly callCount: number;
|
|
34
|
-
|
|
35
|
-
mockImplementation(fn: Impl<Fn>): Spy<Fn>;
|
|
36
|
-
mockImplementationOnce(fn: Impl<Fn>): Spy<Fn>;
|
|
37
|
-
mockReturnValue(value: ReturnType<Fn>): Spy<Fn>;
|
|
38
|
-
mockReturnValueOnce(value: ReturnType<Fn>): Spy<Fn>;
|
|
39
|
-
mockResolvedValue(value: Awaited<ReturnType<Fn>>): Spy<Fn>;
|
|
40
|
-
mockResolvedValueOnce(value: Awaited<ReturnType<Fn>>): Spy<Fn>;
|
|
41
|
-
mockRejectedValue(reason: unknown): Spy<Fn>;
|
|
42
|
-
mockRejectedValueOnce(reason: unknown): Spy<Fn>;
|
|
43
|
-
mockClear(): Spy<Fn>;
|
|
44
|
-
mockReset(): Spy<Fn>;
|
|
45
|
-
mockRestore(): Spy<Fn>;
|
|
46
|
-
|
|
47
|
-
/** Internal: spyOn installs a restore callback here. Undefined for `vi.fn`. */
|
|
48
|
-
__setRestore(fn: () => void): void;
|
|
49
|
-
/** Internal: true iff `__setRestore` was called (= created by spyOn). */
|
|
50
|
-
readonly __isSpyOn: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type Spy<Fn extends AnyFn = AnyFn> = CallSignature<Fn> & SpyMethods<Fn>;
|
|
54
|
-
|
|
55
|
-
type Impl<Fn extends AnyFn> = (...args: Parameters<Fn>) => ReturnType<Fn>;
|
|
56
|
-
|
|
57
|
-
export interface CreateSpyOptions<Fn extends AnyFn> {
|
|
58
|
-
name?: string;
|
|
59
|
-
defaultImplementation?: Fn;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Factory used by `vi.fn` and `vi.spyOn`. The returned function is both
|
|
64
|
-
* callable (executing the current implementation) and an object carrying
|
|
65
|
-
* `.mock`, the Helix brand, and the `mockFoo()` mutators.
|
|
66
|
-
*/
|
|
67
|
-
export function createSpy<Fn extends AnyFn = AnyFn>(
|
|
68
|
-
options: CreateSpyOptions<Fn> = {},
|
|
69
|
-
): Spy<Fn> {
|
|
70
|
-
const onceQueue: Array<Impl<Fn> | Fn> = [];
|
|
71
|
-
let currentImpl: Fn | Impl<Fn> | undefined = options.defaultImplementation;
|
|
72
|
-
let restoreFn: (() => void) | undefined;
|
|
73
|
-
|
|
74
|
-
const mock: MockInternals<Parameters<Fn>, ReturnType<Fn>> = {
|
|
75
|
-
calls: [],
|
|
76
|
-
results: [],
|
|
77
|
-
lastCall: undefined,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const call: CallSignature<Fn> = function (
|
|
81
|
-
this: unknown,
|
|
82
|
-
...args: Parameters<Fn>
|
|
83
|
-
): ReturnType<Fn> {
|
|
84
|
-
mock.calls.push(args);
|
|
85
|
-
mock.lastCall = args;
|
|
86
|
-
// Push an incomplete result synchronously so `mock.results.length`
|
|
87
|
-
// always matches `mock.calls.length` (Vitest parity).
|
|
88
|
-
const result: MockCallResult<ReturnType<Fn>> = {
|
|
89
|
-
type: "incomplete",
|
|
90
|
-
value: undefined as ReturnType<Fn>,
|
|
91
|
-
};
|
|
92
|
-
mock.results.push(result);
|
|
93
|
-
const impl = onceQueue.shift() ?? currentImpl;
|
|
94
|
-
if (!impl) {
|
|
95
|
-
result.type = "return";
|
|
96
|
-
return undefined as ReturnType<Fn>;
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
const value: ReturnType<Fn> = (impl as Impl<Fn>).apply(this, args);
|
|
100
|
-
result.type = "return";
|
|
101
|
-
result.value = value;
|
|
102
|
-
return value;
|
|
103
|
-
} catch (err) {
|
|
104
|
-
result.type = "throw";
|
|
105
|
-
result.value = err as ReturnType<Fn>;
|
|
106
|
-
throw err;
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// `calls` and `callCount` must be LIVE accessors. Defining them in an
|
|
111
|
-
// object literal and passing through `Object.assign` would snapshot the
|
|
112
|
-
// getter's current value (Object.assign invokes [[Get]] on the source).
|
|
113
|
-
const methods: Omit<SpyMethods<Fn>, "calls" | "callCount" | "__isSpyOn"> = {
|
|
114
|
-
__helixIsSpy: true,
|
|
115
|
-
mock,
|
|
116
|
-
mockImplementation(fn) {
|
|
117
|
-
currentImpl = fn;
|
|
118
|
-
return spy;
|
|
119
|
-
},
|
|
120
|
-
mockImplementationOnce(fn) {
|
|
121
|
-
onceQueue.push(fn);
|
|
122
|
-
return spy;
|
|
123
|
-
},
|
|
124
|
-
mockReturnValue(value) {
|
|
125
|
-
currentImpl = () => value;
|
|
126
|
-
return spy;
|
|
127
|
-
},
|
|
128
|
-
mockReturnValueOnce(value) {
|
|
129
|
-
onceQueue.push(() => value);
|
|
130
|
-
return spy;
|
|
131
|
-
},
|
|
132
|
-
mockResolvedValue(value) {
|
|
133
|
-
currentImpl = () => Promise.resolve(value) as ReturnType<Fn>;
|
|
134
|
-
return spy;
|
|
135
|
-
},
|
|
136
|
-
mockResolvedValueOnce(value) {
|
|
137
|
-
onceQueue.push(() => Promise.resolve(value) as ReturnType<Fn>);
|
|
138
|
-
return spy;
|
|
139
|
-
},
|
|
140
|
-
mockRejectedValue(reason) {
|
|
141
|
-
currentImpl = () => Promise.reject(reason) as ReturnType<Fn>;
|
|
142
|
-
return spy;
|
|
143
|
-
},
|
|
144
|
-
mockRejectedValueOnce(reason) {
|
|
145
|
-
onceQueue.push(() => Promise.reject(reason) as ReturnType<Fn>);
|
|
146
|
-
return spy;
|
|
147
|
-
},
|
|
148
|
-
mockClear() {
|
|
149
|
-
mock.calls.length = 0;
|
|
150
|
-
mock.results.length = 0;
|
|
151
|
-
mock.lastCall = undefined;
|
|
152
|
-
return spy;
|
|
153
|
-
},
|
|
154
|
-
mockReset() {
|
|
155
|
-
spy.mockClear();
|
|
156
|
-
onceQueue.length = 0;
|
|
157
|
-
// On a `vi.fn` (no `restoreFn`) Vitest clears the implementation
|
|
158
|
-
// entirely. On a `spyOn` spy, restoring to the call-through
|
|
159
|
-
// `defaultImplementation` is the right behaviour.
|
|
160
|
-
currentImpl = restoreFn ? options.defaultImplementation : undefined;
|
|
161
|
-
return spy;
|
|
162
|
-
},
|
|
163
|
-
mockRestore() {
|
|
164
|
-
// Atomic order: swap `restoreFn` out of the closure BEFORE calling
|
|
165
|
-
// it, so a re-entrant `mockRestore` during the user's restore hook
|
|
166
|
-
// doesn't double-fire. If the hook throws, the spy is considered
|
|
167
|
-
// restored (best we can do) and the error propagates.
|
|
168
|
-
const hook = restoreFn;
|
|
169
|
-
restoreFn = undefined;
|
|
170
|
-
spy.mockReset();
|
|
171
|
-
hook?.();
|
|
172
|
-
return spy;
|
|
173
|
-
},
|
|
174
|
-
__setRestore(fn) {
|
|
175
|
-
restoreFn = fn;
|
|
176
|
-
isSpyOn = true;
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// Sticky flag: once a spy is created via spyOn (`__setRestore` called),
|
|
181
|
-
// it stays "spyOn-flavoured" even after `mockRestore()` clears
|
|
182
|
-
// `restoreFn`. This matters for `restoreAllMocks` which must distinguish
|
|
183
|
-
// pass-through spies from bare `vi.fn` spies independently of their
|
|
184
|
-
// restoration state.
|
|
185
|
-
let isSpyOn = false;
|
|
186
|
-
|
|
187
|
-
const withMethods = Object.assign(call, methods);
|
|
188
|
-
Object.defineProperties(withMethods, {
|
|
189
|
-
calls: {
|
|
190
|
-
configurable: true,
|
|
191
|
-
enumerable: false,
|
|
192
|
-
get() {
|
|
193
|
-
return mock.calls;
|
|
194
|
-
},
|
|
195
|
-
},
|
|
196
|
-
callCount: {
|
|
197
|
-
configurable: true,
|
|
198
|
-
enumerable: false,
|
|
199
|
-
get() {
|
|
200
|
-
return mock.calls.length;
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
__isSpyOn: {
|
|
204
|
-
configurable: true,
|
|
205
|
-
enumerable: false,
|
|
206
|
-
get() {
|
|
207
|
-
return isSpyOn;
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
});
|
|
211
|
-
const spy = withMethods as Spy<Fn>;
|
|
212
|
-
Object.defineProperty(spy, "name", {
|
|
213
|
-
value: options.name ?? "spy",
|
|
214
|
-
configurable: true,
|
|
215
|
-
});
|
|
216
|
-
return spy;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function isSpy(value: unknown): value is Spy {
|
|
220
|
-
if (!value || (typeof value !== "function" && typeof value !== "object")) {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
return (value as { __helixIsSpy?: unknown }).__helixIsSpy === true;
|
|
224
|
-
}
|
package/src/runtime/vi/spyOn.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `vi.spyOn(obj, key)` — replaces `obj[key]` with a spy that calls through
|
|
3
|
-
* to the original. `spy.mockRestore()` puts the original back.
|
|
4
|
-
*
|
|
5
|
-
* Supports:
|
|
6
|
-
* - data properties whose value is a function (most methods);
|
|
7
|
-
* - accessor properties (`get` / `set`);
|
|
8
|
-
* - methods inherited via the prototype chain (the spy is installed as an
|
|
9
|
-
* own property on the target, and `mockRestore()` deletes the own
|
|
10
|
-
* property so inheritance resumes).
|
|
11
|
-
*
|
|
12
|
-
* `this` binding: the spy's default implementation uses a `function` form
|
|
13
|
-
* (not arrow) so the caller's receiver is forwarded to the original. This
|
|
14
|
-
* is essential when spying on a prototype method that depends on `this`.
|
|
15
|
-
*
|
|
16
|
-
* Double-spy detection: throws if the property is already a Helix spy, to
|
|
17
|
-
* avoid the subtle double-counting bug where the inner spy's `calls` is
|
|
18
|
-
* incremented by pass-through from the outer one.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { type AnyFn, createSpy, isSpy, type Spy } from "./spy.js";
|
|
22
|
-
|
|
23
|
-
type AccessorKind = "get" | "set";
|
|
24
|
-
|
|
25
|
-
export interface SpyOnOptions {
|
|
26
|
-
/** For accessor properties, which side to spy on. Default: `"get"`. */
|
|
27
|
-
accessor?: AccessorKind;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function findDescriptor(
|
|
31
|
-
obj: object,
|
|
32
|
-
key: PropertyKey,
|
|
33
|
-
): { descriptor: PropertyDescriptor; onPrototype: boolean } {
|
|
34
|
-
let cursor: object | null = obj;
|
|
35
|
-
while (cursor !== null) {
|
|
36
|
-
const desc = Object.getOwnPropertyDescriptor(cursor, key);
|
|
37
|
-
if (desc) {
|
|
38
|
-
return { descriptor: desc, onPrototype: cursor !== obj };
|
|
39
|
-
}
|
|
40
|
-
cursor = Object.getPrototypeOf(cursor);
|
|
41
|
-
}
|
|
42
|
-
throw new Error(
|
|
43
|
-
`vi.spyOn: property "${String(key)}" does not exist on the target`,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function buildRestore(
|
|
48
|
-
obj: object,
|
|
49
|
-
key: PropertyKey,
|
|
50
|
-
ownDescBefore: PropertyDescriptor | undefined,
|
|
51
|
-
onPrototype: boolean,
|
|
52
|
-
): () => void {
|
|
53
|
-
return () => {
|
|
54
|
-
if (onPrototype && !ownDescBefore) {
|
|
55
|
-
Reflect.deleteProperty(obj, key);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (ownDescBefore) {
|
|
59
|
-
Object.defineProperty(obj, key, ownDescBefore);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function spyOn<Obj extends object, Key extends keyof Obj>(
|
|
65
|
-
obj: Obj,
|
|
66
|
-
key: Key,
|
|
67
|
-
options: SpyOnOptions = {},
|
|
68
|
-
): Spy {
|
|
69
|
-
const label = String(key);
|
|
70
|
-
const { descriptor, onPrototype } = findDescriptor(obj, key);
|
|
71
|
-
const ownDescBefore = Object.getOwnPropertyDescriptor(obj, key);
|
|
72
|
-
|
|
73
|
-
// Double-spy guard: refuse to spy on an already-spied value.
|
|
74
|
-
if (descriptor.value !== undefined && isSpy(descriptor.value)) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
`vi.spyOn: "${label}" is already a spy. Call mockRestore() before spying again.`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const restore = buildRestore(obj, key, ownDescBefore, onPrototype);
|
|
81
|
-
|
|
82
|
-
if (descriptor.get || descriptor.set) {
|
|
83
|
-
const kind: AccessorKind = options.accessor ?? "get";
|
|
84
|
-
const originalGet = descriptor.get;
|
|
85
|
-
const originalSet = descriptor.set;
|
|
86
|
-
if (kind === "get") {
|
|
87
|
-
if (!originalGet) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
`vi.spyOn: "${label}" has no getter (pass { accessor: "set" } to spy on the setter)`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
// Use `function` so the shim + spy forward the caller's `this`.
|
|
93
|
-
const spy = createSpy({
|
|
94
|
-
name: label,
|
|
95
|
-
defaultImplementation: function (this: unknown) {
|
|
96
|
-
return originalGet.call(this);
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
Object.defineProperty(obj, key, {
|
|
100
|
-
configurable: true,
|
|
101
|
-
enumerable: descriptor.enumerable ?? true,
|
|
102
|
-
get(this: unknown): unknown {
|
|
103
|
-
return spy.call(this);
|
|
104
|
-
},
|
|
105
|
-
set: originalSet,
|
|
106
|
-
});
|
|
107
|
-
spy.__setRestore(restore);
|
|
108
|
-
return spy;
|
|
109
|
-
}
|
|
110
|
-
if (!originalSet) {
|
|
111
|
-
throw new Error(`vi.spyOn: "${label}" has no setter`);
|
|
112
|
-
}
|
|
113
|
-
const spy = createSpy<(v: unknown) => void>({
|
|
114
|
-
name: label,
|
|
115
|
-
defaultImplementation: function (this: unknown, v: unknown) {
|
|
116
|
-
originalSet.call(this, v);
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
Object.defineProperty(obj, key, {
|
|
120
|
-
configurable: true,
|
|
121
|
-
enumerable: descriptor.enumerable ?? true,
|
|
122
|
-
get: originalGet,
|
|
123
|
-
set(this: unknown, v: unknown) {
|
|
124
|
-
spy.call(this, v);
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
spy.__setRestore(restore);
|
|
128
|
-
return spy;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const originalValue = descriptor.value;
|
|
132
|
-
if (typeof originalValue !== "function") {
|
|
133
|
-
throw new Error(
|
|
134
|
-
`vi.spyOn: "${label}" is not a function (got ${typeof originalValue}). Pass { accessor } for accessors.`,
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
const originalFn = originalValue as AnyFn;
|
|
138
|
-
const spy = createSpy({
|
|
139
|
-
name: label,
|
|
140
|
-
defaultImplementation: function (this: unknown, ...args: unknown[]) {
|
|
141
|
-
// `this` is the call-site receiver (e.g. the class instance); the
|
|
142
|
-
// original method is forwarded its real receiver. Fixes the bug
|
|
143
|
-
// where `spyOn(Proto, 'method')` used to hard-bind `this = Proto`.
|
|
144
|
-
return originalFn.apply(this, args);
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
Object.defineProperty(obj, key, {
|
|
148
|
-
configurable: true,
|
|
149
|
-
enumerable: descriptor.enumerable ?? true,
|
|
150
|
-
writable: descriptor.writable ?? true,
|
|
151
|
-
value: spy,
|
|
152
|
-
});
|
|
153
|
-
spy.__setRestore(restore);
|
|
154
|
-
return spy;
|
|
155
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* System-time control — pins `Date.now()` and `new Date()` to a fake
|
|
3
|
-
* epoch. Each `ViContext` owns its own `SystemClock` instance; the Date
|
|
4
|
-
* shim read-through to `context.fakeEpoch`, so concurrent test files
|
|
5
|
-
* never share wall-clock state (AC 6 isolation).
|
|
6
|
-
*
|
|
7
|
-
* The shim is installed globally on first activation across ANY context
|
|
8
|
-
* (Date is a singleton per realm), but the shim ALWAYS reads from the
|
|
9
|
-
* currently-active context via the `readContext` resolver passed in. When
|
|
10
|
-
* all contexts deactivate, the shim uninstalls.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const OriginalDate = globalThis.Date;
|
|
14
|
-
const originalNow = OriginalDate.now.bind(OriginalDate);
|
|
15
|
-
|
|
16
|
-
export interface SystemClock {
|
|
17
|
-
/** Fake epoch in ms, or `null` if this context is on real time. */
|
|
18
|
-
fakeEpoch: number | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function createSystemClock(): SystemClock {
|
|
22
|
-
return { fakeEpoch: null };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Register one context's interest in the Date shim. Returns an
|
|
27
|
-
* unregister function.
|
|
28
|
-
*/
|
|
29
|
-
type ContextResolver = () => SystemClock | undefined;
|
|
30
|
-
|
|
31
|
-
const resolvers = new Set<ContextResolver>();
|
|
32
|
-
let installed = false;
|
|
33
|
-
|
|
34
|
-
function resolveEpoch(): number | null {
|
|
35
|
-
for (const resolve of resolvers) {
|
|
36
|
-
const ctx = resolve();
|
|
37
|
-
if (ctx?.fakeEpoch !== null && ctx?.fakeEpoch !== undefined) {
|
|
38
|
-
return ctx.fakeEpoch;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function registerSystemClockContext(
|
|
45
|
-
resolver: ContextResolver,
|
|
46
|
-
): () => void {
|
|
47
|
-
resolvers.add(resolver);
|
|
48
|
-
install();
|
|
49
|
-
return () => {
|
|
50
|
-
resolvers.delete(resolver);
|
|
51
|
-
if (resolvers.size === 0) restore();
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function setFakeEpoch(clock: SystemClock, time: Date | number): void {
|
|
56
|
-
let epoch: number;
|
|
57
|
-
if (time instanceof OriginalDate) {
|
|
58
|
-
epoch = time.getTime();
|
|
59
|
-
} else if (typeof time === "number") {
|
|
60
|
-
epoch = time;
|
|
61
|
-
} else {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`vi.setSystemTime: expected Date or number, got ${typeof time}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
if (!Number.isFinite(epoch)) {
|
|
67
|
-
throw new Error(`vi.setSystemTime: expected a finite epoch, got ${epoch}`);
|
|
68
|
-
}
|
|
69
|
-
clock.fakeEpoch = epoch;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function clearFakeEpoch(clock: SystemClock): void {
|
|
73
|
-
clock.fakeEpoch = null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function buildShim(): object {
|
|
77
|
-
const shim = function DateShim(this: unknown, ...args: unknown[]): unknown {
|
|
78
|
-
const epoch = resolveEpoch();
|
|
79
|
-
// Called without `new`: return a toString of the pinned (or real) time.
|
|
80
|
-
if (!new.target) {
|
|
81
|
-
return epoch !== null
|
|
82
|
-
? new OriginalDate(epoch).toString()
|
|
83
|
-
: new OriginalDate().toString();
|
|
84
|
-
}
|
|
85
|
-
if (args.length === 0 && epoch !== null) {
|
|
86
|
-
return new OriginalDate(epoch);
|
|
87
|
-
}
|
|
88
|
-
return Reflect.construct(OriginalDate, args);
|
|
89
|
-
};
|
|
90
|
-
shim.prototype = OriginalDate.prototype;
|
|
91
|
-
Object.defineProperty(shim, "name", { value: "Date", configurable: true });
|
|
92
|
-
Reflect.set(shim, "now", () => {
|
|
93
|
-
const epoch = resolveEpoch();
|
|
94
|
-
return epoch !== null ? epoch : originalNow();
|
|
95
|
-
});
|
|
96
|
-
Reflect.set(shim, "parse", OriginalDate.parse.bind(OriginalDate));
|
|
97
|
-
Reflect.set(shim, "UTC", OriginalDate.UTC.bind(OriginalDate));
|
|
98
|
-
return shim;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function install(): void {
|
|
102
|
-
if (installed) return;
|
|
103
|
-
installed = true;
|
|
104
|
-
Reflect.set(globalThis, "Date", buildShim());
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function restore(): void {
|
|
108
|
-
if (!installed) return;
|
|
109
|
-
installed = false;
|
|
110
|
-
Reflect.set(globalThis, "Date", OriginalDate);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Get the original `Date.now` even after the shim is installed. */
|
|
114
|
-
export function getRealNow(): number {
|
|
115
|
-
return originalNow();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Get the original `Date` constructor. */
|
|
119
|
-
export function getRealDate(): DateConstructor {
|
|
120
|
-
return OriginalDate;
|
|
121
|
-
}
|