@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,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module mocking - main-thread registry and public API (Tier A).
|
|
3
|
+
*
|
|
4
|
+
* Tier A scope:
|
|
5
|
+
* - `mock.doMock(spec, factory)` registers a mock for an external module
|
|
6
|
+
* specifier (bare package name or `node:*` built-in).
|
|
7
|
+
* - Subsequent `await import(spec)` calls return the factory's result.
|
|
8
|
+
* - Static imports are NOT supported in Tier A; see the docs / ISSUES.md
|
|
9
|
+
* for the planned Tier B (hoisted `mock.module`) work.
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* - bin/unrift.mjs registers `module-loader.js` via `module.register()` and
|
|
13
|
+
* pipes a MessagePort into the loader thread.
|
|
14
|
+
* - The main-side port is stashed on `globalThis.__unriftLoaderPort` before
|
|
15
|
+
* the test runner starts.
|
|
16
|
+
* - `doMock()` invokes the factory once, caches the result on
|
|
17
|
+
* `globalThis.__unriftMocks`, and posts a `register` message with the
|
|
18
|
+
* spec + export-key list to the loader.
|
|
19
|
+
* - The loader (see module-loader.ts) drains pending messages on every
|
|
20
|
+
* resolve() and short-circuits matched specs to a synthetic module that
|
|
21
|
+
* reads back from `globalThis.__unriftMocks`.
|
|
22
|
+
*
|
|
23
|
+
* Future tiers will reuse the same registry shape and IPC protocol -
|
|
24
|
+
* adding `register-hoisted` (Tier B) and `register-relative` (Tier C)
|
|
25
|
+
* message types without breaking the existing surface.
|
|
26
|
+
*/
|
|
27
|
+
import { activeRestorers, spy } from "./spy.js";
|
|
28
|
+
// The mock registry and loader port below live on globalThis, which is
|
|
29
|
+
// process-global. This is correct under unrift's sequential single-process
|
|
30
|
+
// execution; parallel per-file execution (see ISSUES.md) would need the
|
|
31
|
+
// registry keyed/isolated per worker.
|
|
32
|
+
/**
|
|
33
|
+
* Sentinel field name on globalThis. Loader-generated synthetic source
|
|
34
|
+
* reads back through this same name, so it must stay stable.
|
|
35
|
+
*/
|
|
36
|
+
export const MOCKS_GLOBAL_KEY = "__unriftMocks";
|
|
37
|
+
/** Sentinel field name for the IPC port set by bin/unrift.mjs. */
|
|
38
|
+
export const LOADER_PORT_GLOBAL_KEY = "__unriftLoaderPort";
|
|
39
|
+
/** URL prefix the loader uses for mocked modules. Public so tests can assert. */
|
|
40
|
+
export const MOCK_URL_PREFIX = "unrift-mock:";
|
|
41
|
+
function globals() {
|
|
42
|
+
return globalThis;
|
|
43
|
+
}
|
|
44
|
+
function ensureMocksMap() {
|
|
45
|
+
const slots = globals();
|
|
46
|
+
let map = slots[MOCKS_GLOBAL_KEY];
|
|
47
|
+
if (!map) {
|
|
48
|
+
map = new Map();
|
|
49
|
+
slots[MOCKS_GLOBAL_KEY] = map;
|
|
50
|
+
}
|
|
51
|
+
return map;
|
|
52
|
+
}
|
|
53
|
+
function getLoaderPort() {
|
|
54
|
+
return globals()[LOADER_PORT_GLOBAL_KEY];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* JS identifier check. ESM `export const <key>` requires a valid identifier;
|
|
58
|
+
* keys that fail this check are skipped with a console.warn so the user
|
|
59
|
+
* isn't surprised by a silently-missing export.
|
|
60
|
+
*/
|
|
61
|
+
const IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
62
|
+
function partitionExportKeys(spec, result) {
|
|
63
|
+
const validKeys = [];
|
|
64
|
+
const skippedKeys = [];
|
|
65
|
+
for (const key of Object.keys(result)) {
|
|
66
|
+
if (key === "default" || IDENT_RE.test(key)) {
|
|
67
|
+
validKeys.push(key);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
skippedKeys.push(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (skippedKeys.length > 0) {
|
|
74
|
+
console.warn(`[unrift] mock.doMock("${spec}"): factory returned ${skippedKeys.length} key(s) that are not valid ` +
|
|
75
|
+
`JS identifiers and will not be exported: ${skippedKeys.join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
return { validKeys, skippedKeys };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Track every spec that's currently mocked so `unmock()` can find them
|
|
81
|
+
* and `restoreAll()` can clean them all up at once.
|
|
82
|
+
*/
|
|
83
|
+
const mockedSpecs = new Set();
|
|
84
|
+
/**
|
|
85
|
+
* Monotonically increasing counter - every `doMock()` call gets a unique
|
|
86
|
+
* ID embedded in its synthetic module URL, defeating Node's module cache
|
|
87
|
+
* across re-registrations.
|
|
88
|
+
*/
|
|
89
|
+
let nextRegistrationId = 0;
|
|
90
|
+
/**
|
|
91
|
+
* Restore hook registered with the spy/stub bulk-restore registry so
|
|
92
|
+
* `mock.restoreAll()` cleans up module mocks too.
|
|
93
|
+
*/
|
|
94
|
+
const restoreAllMockedSpecs = () => {
|
|
95
|
+
for (const spec of [...mockedSpecs]) {
|
|
96
|
+
unmock(spec);
|
|
97
|
+
}
|
|
98
|
+
activeRestorers.delete(restoreAllMockedSpecs);
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Register a mock for `spec`. Subsequent `await import(spec)` calls will
|
|
102
|
+
* resolve to the factory's return value instead of the real module.
|
|
103
|
+
*
|
|
104
|
+
* Tier A constraints (planned to be lifted in Tier B and beyond):
|
|
105
|
+
* - Only **dynamic** imports (`await import(spec)`) hit the mock; static
|
|
106
|
+
* `import { x } from "spec"` is hoisted by ESM above this call and
|
|
107
|
+
* will see the real module.
|
|
108
|
+
* - Only **external** specifiers work - bare package names (`"react"`)
|
|
109
|
+
* and Node built-ins (`"node:fs"`). Relative specs (`"./bar"`) are
|
|
110
|
+
* bundled inline by esbuild and can't be intercepted at runtime.
|
|
111
|
+
* - The factory is invoked **once** at registration time. Its return
|
|
112
|
+
* value is reused for every subsequent import.
|
|
113
|
+
*
|
|
114
|
+
* Throws if the Unrift CLI loader isn't attached (i.e., tests were run
|
|
115
|
+
* with raw `node` instead of `unrift`).
|
|
116
|
+
*/
|
|
117
|
+
export function doMock(spec, factory) {
|
|
118
|
+
if (typeof spec !== "string" || spec.length === 0) {
|
|
119
|
+
throw new TypeError("mock.doMock() requires a non-empty string specifier");
|
|
120
|
+
}
|
|
121
|
+
if (typeof factory !== "function") {
|
|
122
|
+
throw new TypeError("mock.doMock() requires a factory function returning an exports object");
|
|
123
|
+
}
|
|
124
|
+
const port = getLoaderPort();
|
|
125
|
+
if (!port) {
|
|
126
|
+
throw new Error("mock.doMock() requires the Unrift CLI loader to be installed. " +
|
|
127
|
+
"Run tests via the `unrift` binary (or `node bin/unrift.mjs`), not raw `node`.");
|
|
128
|
+
}
|
|
129
|
+
const result = factory();
|
|
130
|
+
if (result === null || typeof result !== "object") {
|
|
131
|
+
throw new TypeError(`mock.doMock("${spec}"): factory must return an object; got ${typeof result}`);
|
|
132
|
+
}
|
|
133
|
+
const { validKeys } = partitionExportKeys(spec, result);
|
|
134
|
+
ensureMocksMap().set(spec, result);
|
|
135
|
+
mockedSpecs.add(spec);
|
|
136
|
+
activeRestorers.add(restoreAllMockedSpecs);
|
|
137
|
+
port.postMessage({
|
|
138
|
+
type: "register",
|
|
139
|
+
spec,
|
|
140
|
+
registrationId: ++nextRegistrationId,
|
|
141
|
+
exportKeys: validKeys,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Remove a previously registered mock. Subsequent `await import(spec)`
|
|
146
|
+
* calls fall through to the real module.
|
|
147
|
+
*/
|
|
148
|
+
export function unmock(spec) {
|
|
149
|
+
const port = getLoaderPort();
|
|
150
|
+
if (port) {
|
|
151
|
+
port.postMessage({ type: "unregister", spec });
|
|
152
|
+
}
|
|
153
|
+
ensureMocksMap().delete(spec);
|
|
154
|
+
mockedSpecs.delete(spec);
|
|
155
|
+
}
|
|
156
|
+
/** Returns the current set of mocked specs. Useful for tests / debugging. */
|
|
157
|
+
export function listMockedSpecs() {
|
|
158
|
+
return [...mockedSpecs];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Convenience helper: imports the real module, builds a version with every
|
|
162
|
+
* function export wrapped in a spy, registers it via `mock.doMock`, and
|
|
163
|
+
* returns the mocked exports.
|
|
164
|
+
*
|
|
165
|
+
* Non-function exports are passed through unchanged. Tests can customize
|
|
166
|
+
* individual spies with `.mockReturnValue`, `.mockImplementation`, etc.
|
|
167
|
+
*
|
|
168
|
+
* Subject to the same Tier A limits as `doMock` - only affects
|
|
169
|
+
* `await import(spec)` after the call. Static imports of `spec` see the
|
|
170
|
+
* real module.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* const fs = await mock.fromModule("node:fs");
|
|
174
|
+
* fs.readFileSync.mockReturnValue("fake content");
|
|
175
|
+
* // ... test code that does `await import("node:fs")` now gets the spies
|
|
176
|
+
*/
|
|
177
|
+
export async function fromModule(spec) {
|
|
178
|
+
if (typeof spec !== "string" || spec.length === 0) {
|
|
179
|
+
throw new TypeError("mock.fromModule() requires a non-empty string specifier");
|
|
180
|
+
}
|
|
181
|
+
const real = (await import(spec));
|
|
182
|
+
const mocked = {};
|
|
183
|
+
for (const key of Object.keys(real)) {
|
|
184
|
+
const value = real[key];
|
|
185
|
+
if (typeof value === "function") {
|
|
186
|
+
// Auto-mock: create a spy with NO implementation, so unconfigured
|
|
187
|
+
// exports record calls and return undefined rather than executing the
|
|
188
|
+
// real function (which could fire real side effects). Tests opt in to
|
|
189
|
+
// behavior per export via .mockReturnValue / .mockImplementation.
|
|
190
|
+
mocked[key] = spy();
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
mocked[key] = value;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
doMock(spec, () => mocked);
|
|
197
|
+
return mocked;
|
|
198
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the Node module-customization hook that powers module mocking
|
|
3
|
+
* (mock.doMock / mock.fs / mock.fromModule).
|
|
4
|
+
*
|
|
5
|
+
* This is called once, early in the CLI startup (see cli.ts) - before any
|
|
6
|
+
* test file is dynamically imported. Because it lives in @unrift/core next to
|
|
7
|
+
* the loader, both the `@unrift/core` bin and the `unrift` wrapper bin get it
|
|
8
|
+
* "for free" by going through the shared CLI; neither bin needs its own
|
|
9
|
+
* bootstrap logic.
|
|
10
|
+
*
|
|
11
|
+
* The loader runs in a worker thread and receives a MessagePort over which
|
|
12
|
+
* the main thread posts register/unregister messages. The main-side port is
|
|
13
|
+
* stashed on globalThis so mock.doMock() can reach it without a circular
|
|
14
|
+
* import back into the CLI.
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerModuleLoader(): void;
|
|
17
|
+
//# sourceMappingURL=register-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-loader.d.ts","sourceRoot":"","sources":["../../../src/mock/register-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,wBAAgB,oBAAoB,IAAI,IAAI,CAyB3C"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the Node module-customization hook that powers module mocking
|
|
3
|
+
* (mock.doMock / mock.fs / mock.fromModule).
|
|
4
|
+
*
|
|
5
|
+
* This is called once, early in the CLI startup (see cli.ts) - before any
|
|
6
|
+
* test file is dynamically imported. Because it lives in @unrift/core next to
|
|
7
|
+
* the loader, both the `@unrift/core` bin and the `unrift` wrapper bin get it
|
|
8
|
+
* "for free" by going through the shared CLI; neither bin needs its own
|
|
9
|
+
* bootstrap logic.
|
|
10
|
+
*
|
|
11
|
+
* The loader runs in a worker thread and receives a MessagePort over which
|
|
12
|
+
* the main thread posts register/unregister messages. The main-side port is
|
|
13
|
+
* stashed on globalThis so mock.doMock() can reach it without a circular
|
|
14
|
+
* import back into the CLI.
|
|
15
|
+
*/
|
|
16
|
+
import { register } from "node:module";
|
|
17
|
+
import { MessageChannel } from "node:worker_threads";
|
|
18
|
+
import { LOADER_PORT_GLOBAL_KEY } from "./module.js";
|
|
19
|
+
let done = false;
|
|
20
|
+
export function registerModuleLoader() {
|
|
21
|
+
if (done)
|
|
22
|
+
return;
|
|
23
|
+
done = true;
|
|
24
|
+
// `module.register()` was added in 18.19.0 / 20.6.0. On older Node, module
|
|
25
|
+
// mocking simply isn't available - doMock() throws a clear error if used.
|
|
26
|
+
if (typeof register !== "function")
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
const { port1, port2 } = new MessageChannel();
|
|
30
|
+
globalThis[LOADER_PORT_GLOBAL_KEY] = port1;
|
|
31
|
+
// Resolve the loader relative to this module - it's a sibling file, so no
|
|
32
|
+
// package export or cross-package specifier is involved.
|
|
33
|
+
register("./module-loader.js", import.meta.url, {
|
|
34
|
+
data: { port: port2 },
|
|
35
|
+
transferList: [port2],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
// Best-effort: a failure here must not crash test runs that don't use
|
|
40
|
+
// module mocking.
|
|
41
|
+
if (process.env.UNRIFT_DEBUG === "1") {
|
|
42
|
+
console.warn("[unrift] module loader registration failed:", err);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spy / Mock function primitive.
|
|
3
|
+
*
|
|
4
|
+
* Both `spy()` and `mock.fn()` produce the same object - a callable that
|
|
5
|
+
* records every invocation, supports configurable implementations, and can
|
|
6
|
+
* be restored to the original (when created via `spyOn`).
|
|
7
|
+
*/
|
|
8
|
+
export declare const SPY_BRAND: unique symbol;
|
|
9
|
+
export type AnyFn = (...args: any[]) => any;
|
|
10
|
+
export interface MockResult {
|
|
11
|
+
type: "return" | "throw";
|
|
12
|
+
value: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface MockState {
|
|
15
|
+
calls: unknown[][];
|
|
16
|
+
results: MockResult[];
|
|
17
|
+
contexts: unknown[];
|
|
18
|
+
instances: unknown[];
|
|
19
|
+
lastCall: unknown[] | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface Spy<T extends AnyFn = AnyFn> {
|
|
22
|
+
(...args: Parameters<T>): ReturnType<T>;
|
|
23
|
+
mock: MockState;
|
|
24
|
+
mockName(name: string): Spy<T>;
|
|
25
|
+
getMockName(): string;
|
|
26
|
+
mockImplementation(impl: T): Spy<T>;
|
|
27
|
+
mockImplementationOnce(impl: T): Spy<T>;
|
|
28
|
+
mockReturnValue(value: ReturnType<T>): Spy<T>;
|
|
29
|
+
mockReturnValueOnce(value: ReturnType<T>): Spy<T>;
|
|
30
|
+
mockResolvedValue(value: Awaited<ReturnType<T>>): Spy<T>;
|
|
31
|
+
mockResolvedValueOnce(value: Awaited<ReturnType<T>>): Spy<T>;
|
|
32
|
+
mockRejectedValue(error: unknown): Spy<T>;
|
|
33
|
+
mockRejectedValueOnce(error: unknown): Spy<T>;
|
|
34
|
+
mockClear(): Spy<T>;
|
|
35
|
+
mockReset(): Spy<T>;
|
|
36
|
+
mockRestore(): void;
|
|
37
|
+
readonly [SPY_BRAND]: true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Internal registries. Used by `mock.clearAll()`, `mock.resetAll()`,
|
|
41
|
+
* and `mock.restoreAll()` to operate on every spy created so far.
|
|
42
|
+
*/
|
|
43
|
+
export declare const activeSpies: Set<Spy<AnyFn>>;
|
|
44
|
+
export declare const activeRestorers: Set<() => void>;
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if the value is a spy created by `spy()` or `spyOn()`.
|
|
47
|
+
* Used by mock matchers to validate their `received` argument.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isSpy(value: unknown): value is Spy;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a standalone spy / mock function. Records every invocation and
|
|
52
|
+
* supports configurable behavior via `.mockReturnValue`, `.mockImplementation`, etc.
|
|
53
|
+
*/
|
|
54
|
+
export declare function spy<T extends AnyFn = AnyFn>(impl?: T): Spy<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Replaces `obj[key]` with a spy that wraps the original method. Calling
|
|
57
|
+
* `.mockRestore()` (or `mock.restoreAll()`) puts the original back.
|
|
58
|
+
*
|
|
59
|
+
* The original is the default implementation - calls pass through unless
|
|
60
|
+
* the user provides an override via `.mockReturnValue` / `.mockImplementation`.
|
|
61
|
+
*
|
|
62
|
+
* The 3-arg form `spyOn(obj, "prop", "get")` / `spyOn(obj, "prop", "set")`
|
|
63
|
+
* wraps the accessor instead of a method. Setter spies record the assigned
|
|
64
|
+
* value as the first call argument; getter spies record the returned value
|
|
65
|
+
* in `mock.results`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function spyOn<T extends object, K extends keyof T & (string | symbol)>(obj: T, key: K): T[K] extends AnyFn ? Spy<T[K]> : never;
|
|
68
|
+
export declare function spyOn<T extends object, K extends keyof T & (string | symbol)>(obj: T, key: K, accessor: "get"): Spy<() => T[K]>;
|
|
69
|
+
export declare function spyOn<T extends object, K extends keyof T & (string | symbol)>(obj: T, key: K, accessor: "set"): Spy<(value: T[K]) => void>;
|
|
70
|
+
export declare function clearAllMocks(): void;
|
|
71
|
+
export declare function resetAllMocks(): void;
|
|
72
|
+
export declare function restoreAllMocks(): void;
|
|
73
|
+
//# sourceMappingURL=spy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spy.d.ts","sourceRoot":"","sources":["../../../src/mock/spy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spy / Mock function primitive.
|
|
3
|
+
*
|
|
4
|
+
* Both `spy()` and `mock.fn()` produce the same object - a callable that
|
|
5
|
+
* records every invocation, supports configurable implementations, and can
|
|
6
|
+
* be restored to the original (when created via `spyOn`).
|
|
7
|
+
*/
|
|
8
|
+
export const SPY_BRAND = Symbol.for("unrift.spy");
|
|
9
|
+
/**
|
|
10
|
+
* Internal registries. Used by `mock.clearAll()`, `mock.resetAll()`,
|
|
11
|
+
* and `mock.restoreAll()` to operate on every spy created so far.
|
|
12
|
+
*/
|
|
13
|
+
export const activeSpies = new Set();
|
|
14
|
+
export const activeRestorers = new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Returns true if the value is a spy created by `spy()` or `spyOn()`.
|
|
17
|
+
* Used by mock matchers to validate their `received` argument.
|
|
18
|
+
*/
|
|
19
|
+
export function isSpy(value) {
|
|
20
|
+
return (typeof value === "function" &&
|
|
21
|
+
value[SPY_BRAND] === true);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* True if `fn` can be used with `new` / `Reflect.construct`. Arrow functions,
|
|
25
|
+
* object/class methods, async and generator functions, and most bound
|
|
26
|
+
* functions are NOT constructable.
|
|
27
|
+
*
|
|
28
|
+
* Probes by using `fn` as the `newTarget` of a no-op construct - this
|
|
29
|
+
* validates constructability without ever invoking `fn`'s body.
|
|
30
|
+
*/
|
|
31
|
+
function isConstructor(fn) {
|
|
32
|
+
try {
|
|
33
|
+
Reflect.construct(function () { }, [], fn);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function createSpy(config) {
|
|
41
|
+
let mockName = "spy";
|
|
42
|
+
let currentImpl = config.initialImpl;
|
|
43
|
+
const onceImpls = [];
|
|
44
|
+
const state = {
|
|
45
|
+
calls: [],
|
|
46
|
+
results: [],
|
|
47
|
+
contexts: [],
|
|
48
|
+
instances: [],
|
|
49
|
+
lastCall: undefined,
|
|
50
|
+
};
|
|
51
|
+
// The callable. Defined as a regular `function` (not arrow) so it has its own
|
|
52
|
+
// `this` binding when invoked as a method, and so `new spy(...)` works.
|
|
53
|
+
function spyImpl(...args) {
|
|
54
|
+
state.calls.push(args);
|
|
55
|
+
state.contexts.push(this);
|
|
56
|
+
state.lastCall = args;
|
|
57
|
+
/**
|
|
58
|
+
* Resolution order:
|
|
59
|
+
* 1. queued one-shot implementations (FIFO)
|
|
60
|
+
* 2. current permanent implementation
|
|
61
|
+
* 3. original (spyOn) implementation
|
|
62
|
+
* 4. no-op returning undefined
|
|
63
|
+
*/
|
|
64
|
+
const impl = onceImpls.shift() ?? currentImpl ?? config.originalImpl;
|
|
65
|
+
let isNewTarget = false;
|
|
66
|
+
try {
|
|
67
|
+
/**
|
|
68
|
+
* `new.target` would be cleaner, but inside this regular function we
|
|
69
|
+
* detect construction by checking whether `this` is an instance whose
|
|
70
|
+
* prototype matches the spy's prototype.
|
|
71
|
+
*/
|
|
72
|
+
isNewTarget =
|
|
73
|
+
new.target !== undefined ||
|
|
74
|
+
(this !== undefined &&
|
|
75
|
+
this !== null &&
|
|
76
|
+
Object.getPrototypeOf(this) === spyImpl.prototype);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
isNewTarget = false;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
let value;
|
|
83
|
+
if (isNewTarget) {
|
|
84
|
+
// `new spy()` is always valid, regardless of the implementation.
|
|
85
|
+
if (impl && isConstructor(impl)) {
|
|
86
|
+
// Constructable impl: run it as the constructor with the spy's
|
|
87
|
+
// prototype (via newTarget), yielding its own instance.
|
|
88
|
+
value = Reflect.construct(impl, args, spyImpl);
|
|
89
|
+
}
|
|
90
|
+
else if (impl) {
|
|
91
|
+
// Non-constructable impl (arrow, method, async, …): can't
|
|
92
|
+
// Reflect.construct it. Run it against the object JS already created
|
|
93
|
+
// for us; the instance is that object, unless the impl explicitly
|
|
94
|
+
// returns one (constructor return semantics).
|
|
95
|
+
const ret = impl.apply(this, args);
|
|
96
|
+
value = typeof ret === "object" && ret !== null ? ret : this;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// No impl: the instance is the object JS created for `new spy()`.
|
|
100
|
+
value = this;
|
|
101
|
+
}
|
|
102
|
+
state.instances.push(value);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
value = impl ? impl.apply(this, args) : undefined;
|
|
106
|
+
}
|
|
107
|
+
state.results.push({ type: "return", value });
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
state.results.push({ type: "throw", value: err });
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Attach state + methods.
|
|
116
|
+
const spy = spyImpl;
|
|
117
|
+
Object.defineProperty(spy, SPY_BRAND, {
|
|
118
|
+
value: true,
|
|
119
|
+
enumerable: false,
|
|
120
|
+
configurable: false,
|
|
121
|
+
writable: false,
|
|
122
|
+
});
|
|
123
|
+
Object.defineProperty(spy, "mock", {
|
|
124
|
+
value: state,
|
|
125
|
+
enumerable: false,
|
|
126
|
+
configurable: true,
|
|
127
|
+
writable: true,
|
|
128
|
+
});
|
|
129
|
+
spy.mockName = (name) => {
|
|
130
|
+
mockName = name;
|
|
131
|
+
return spy;
|
|
132
|
+
};
|
|
133
|
+
spy.getMockName = () => mockName;
|
|
134
|
+
spy.mockImplementation = (impl) => {
|
|
135
|
+
currentImpl = impl;
|
|
136
|
+
return spy;
|
|
137
|
+
};
|
|
138
|
+
spy.mockImplementationOnce = (impl) => {
|
|
139
|
+
onceImpls.push(impl);
|
|
140
|
+
return spy;
|
|
141
|
+
};
|
|
142
|
+
spy.mockReturnValue = (value) => spy.mockImplementation((() => value));
|
|
143
|
+
spy.mockReturnValueOnce = (value) => spy.mockImplementationOnce((() => value));
|
|
144
|
+
spy.mockResolvedValue = (value) => spy.mockImplementation((() => Promise.resolve(value)));
|
|
145
|
+
spy.mockResolvedValueOnce = (value) => spy.mockImplementationOnce((() => Promise.resolve(value)));
|
|
146
|
+
spy.mockRejectedValue = (error) => spy.mockImplementation((() => Promise.reject(error)));
|
|
147
|
+
spy.mockRejectedValueOnce = (error) => spy.mockImplementationOnce((() => Promise.reject(error)));
|
|
148
|
+
spy.mockClear = () => {
|
|
149
|
+
state.calls.length = 0;
|
|
150
|
+
state.results.length = 0;
|
|
151
|
+
state.contexts.length = 0;
|
|
152
|
+
state.instances.length = 0;
|
|
153
|
+
state.lastCall = undefined;
|
|
154
|
+
return spy;
|
|
155
|
+
};
|
|
156
|
+
spy.mockReset = () => {
|
|
157
|
+
spy.mockClear();
|
|
158
|
+
currentImpl = undefined;
|
|
159
|
+
onceImpls.length = 0;
|
|
160
|
+
return spy;
|
|
161
|
+
};
|
|
162
|
+
spy.mockRestore = () => {
|
|
163
|
+
spy.mockReset();
|
|
164
|
+
if (config.restoreFn) {
|
|
165
|
+
config.restoreFn();
|
|
166
|
+
activeRestorers.delete(config.restoreFn);
|
|
167
|
+
}
|
|
168
|
+
activeSpies.delete(spy);
|
|
169
|
+
};
|
|
170
|
+
activeSpies.add(spy);
|
|
171
|
+
return spy;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Creates a standalone spy / mock function. Records every invocation and
|
|
175
|
+
* supports configurable behavior via `.mockReturnValue`, `.mockImplementation`, etc.
|
|
176
|
+
*/
|
|
177
|
+
export function spy(impl) {
|
|
178
|
+
return createSpy({ initialImpl: impl });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Walks the prototype chain to find the descriptor for `key` - needed
|
|
182
|
+
* because accessor properties (getter/setter) usually live on a prototype,
|
|
183
|
+
* not on the instance itself.
|
|
184
|
+
*/
|
|
185
|
+
function findDescriptor(obj, key) {
|
|
186
|
+
let current = obj;
|
|
187
|
+
while (current !== null) {
|
|
188
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
189
|
+
if (descriptor)
|
|
190
|
+
return { host: current, descriptor };
|
|
191
|
+
current = Object.getPrototypeOf(current);
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
export function spyOn(obj, key, accessor) {
|
|
196
|
+
// Accessor (getter/setter) path - 3-arg form.
|
|
197
|
+
if (accessor === "get" || accessor === "set") {
|
|
198
|
+
const found = findDescriptor(obj, key);
|
|
199
|
+
if (!found) {
|
|
200
|
+
throw new Error(`spyOn() could not find property "${String(key)}" on the target or its prototype chain`);
|
|
201
|
+
}
|
|
202
|
+
const { host, descriptor } = found;
|
|
203
|
+
const original = descriptor[accessor];
|
|
204
|
+
// Repeat spyOn on an already-spied accessor returns the existing spy.
|
|
205
|
+
// Wrapping a spy in a spy would capture the first spy as the "original",
|
|
206
|
+
// and restoring would then put the first spy back instead of the real
|
|
207
|
+
// accessor - leaking a mock past restoreAll().
|
|
208
|
+
if (isSpy(original)) {
|
|
209
|
+
return original;
|
|
210
|
+
}
|
|
211
|
+
if (descriptor.configurable === false) {
|
|
212
|
+
throw new Error(`spyOn() cannot wrap non-configurable property "${String(key)}"`);
|
|
213
|
+
}
|
|
214
|
+
if (typeof original !== "function") {
|
|
215
|
+
throw new Error(`spyOn(..., "${accessor}") requires "${String(key)}" to have a ${accessor}ter`);
|
|
216
|
+
}
|
|
217
|
+
// If the spy is on a prototype, restoring needs to put back the
|
|
218
|
+
// *prototype's* descriptor at the *prototype* level - not on `obj`.
|
|
219
|
+
// Idempotent: a second run (mockRestore + restoreAll) is a no-op.
|
|
220
|
+
let restored = false;
|
|
221
|
+
const restore = () => {
|
|
222
|
+
if (restored)
|
|
223
|
+
return;
|
|
224
|
+
restored = true;
|
|
225
|
+
Object.defineProperty(host, key, descriptor);
|
|
226
|
+
};
|
|
227
|
+
const accessorSpy = createSpy({
|
|
228
|
+
originalImpl: original,
|
|
229
|
+
restoreFn: restore,
|
|
230
|
+
});
|
|
231
|
+
const newDescriptor = {
|
|
232
|
+
configurable: true,
|
|
233
|
+
enumerable: descriptor.enumerable,
|
|
234
|
+
[accessor]: accessorSpy,
|
|
235
|
+
};
|
|
236
|
+
// Preserve the other half of the pair (the unspied accessor stays put).
|
|
237
|
+
if (accessor === "get" && descriptor.set)
|
|
238
|
+
newDescriptor.set = descriptor.set;
|
|
239
|
+
if (accessor === "set" && descriptor.get)
|
|
240
|
+
newDescriptor.get = descriptor.get;
|
|
241
|
+
Object.defineProperty(host, key, newDescriptor);
|
|
242
|
+
activeRestorers.add(restore);
|
|
243
|
+
return accessorSpy;
|
|
244
|
+
}
|
|
245
|
+
// Method path - original 2-arg behavior.
|
|
246
|
+
const original = obj[key];
|
|
247
|
+
// Repeat spyOn on an already-spied method returns the existing spy rather
|
|
248
|
+
// than wrapping it. A spy-wrapping-a-spy would capture the first spy as its
|
|
249
|
+
// "original", so restore order could put the first spy back after the real
|
|
250
|
+
// method - leaking a mock past restoreAll().
|
|
251
|
+
if (isSpy(original)) {
|
|
252
|
+
return original;
|
|
253
|
+
}
|
|
254
|
+
if (typeof original !== "function") {
|
|
255
|
+
throw new Error(`spyOn() requires a function property; got ${typeof original} for key "${String(key)}"`);
|
|
256
|
+
}
|
|
257
|
+
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
|
258
|
+
if (descriptor && descriptor.configurable === false) {
|
|
259
|
+
throw new Error(`spyOn() cannot wrap non-configurable property "${String(key)}"`);
|
|
260
|
+
}
|
|
261
|
+
// Idempotent: mockRestore() followed by restoreAll() must not re-apply.
|
|
262
|
+
let restored = false;
|
|
263
|
+
const restore = () => {
|
|
264
|
+
if (restored)
|
|
265
|
+
return;
|
|
266
|
+
restored = true;
|
|
267
|
+
if (descriptor) {
|
|
268
|
+
Object.defineProperty(obj, key, descriptor);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
delete obj[key];
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const spy = createSpy({
|
|
275
|
+
originalImpl: original,
|
|
276
|
+
restoreFn: restore,
|
|
277
|
+
});
|
|
278
|
+
Object.defineProperty(obj, key, {
|
|
279
|
+
value: spy,
|
|
280
|
+
writable: true,
|
|
281
|
+
configurable: true,
|
|
282
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
283
|
+
});
|
|
284
|
+
activeRestorers.add(restore);
|
|
285
|
+
return spy;
|
|
286
|
+
}
|
|
287
|
+
// Clear call history on every active spy. Implementations are preserved.
|
|
288
|
+
export function clearAllMocks() {
|
|
289
|
+
for (const spy of activeSpies)
|
|
290
|
+
spy.mockClear();
|
|
291
|
+
}
|
|
292
|
+
// Clear call history AND remove all configured implementations on every active spy.
|
|
293
|
+
export function resetAllMocks() {
|
|
294
|
+
for (const spy of activeSpies)
|
|
295
|
+
spy.mockReset();
|
|
296
|
+
}
|
|
297
|
+
// Restore originals for every spyOn / stub created so far, then clear all spy state.
|
|
298
|
+
export function restoreAllMocks() {
|
|
299
|
+
// Iterate over a snapshot - restorers mutate the set as they run.
|
|
300
|
+
for (const restore of [...activeRestorers]) {
|
|
301
|
+
restore();
|
|
302
|
+
activeRestorers.delete(restore);
|
|
303
|
+
}
|
|
304
|
+
for (const spy of [...activeSpies])
|
|
305
|
+
spy.mockReset();
|
|
306
|
+
activeSpies.clear();
|
|
307
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mock.stub` - replace any property on any object (function or value),
|
|
3
|
+
* with restoration tracked centrally so `mock.restoreAll()` can undo it.
|
|
4
|
+
*
|
|
5
|
+
* Use `spyOn` when you want a wrapped function spy. Use `stub` when you
|
|
6
|
+
* want to swap in a fixed value or non-function replacement.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Replace `target[key]` with `value`. Returns a function that restores
|
|
10
|
+
* the original property descriptor (or removes the key entirely if it
|
|
11
|
+
* didn't exist before).
|
|
12
|
+
*
|
|
13
|
+
* Stubbing the same property again before restoring updates the value but
|
|
14
|
+
* keeps the first stub's original - so restore always yields the real value,
|
|
15
|
+
* no matter how many times the property was re-stubbed.
|
|
16
|
+
*
|
|
17
|
+
* The restorer is also registered with the global `mock.restoreAll()`
|
|
18
|
+
* pool, so test cleanup can reset every stub at once.
|
|
19
|
+
*/
|
|
20
|
+
export declare function stub<T extends object, K extends keyof T>(target: T, key: K, value: T[K]): () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Convenience wrapper: `mock.stub(globalThis, key, value)`. Useful for
|
|
23
|
+
* swapping `fetch`, `crypto`, `console`, etc. during a test.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mockGlobal<K extends string>(key: K, value: unknown): () => void;
|
|
26
|
+
//# sourceMappingURL=stub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stub.d.ts","sourceRoot":"","sources":["../../../src/mock/stub.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAYH;;;;;;;;;;;GAWG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EACtD,MAAM,EAAE,CAAC,EACT,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,GACV,MAAM,IAAI,CAuDZ;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,EACzC,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,OAAO,GACb,MAAM,IAAI,CAEZ"}
|