@znight/testing 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +441 -2
- package/dist/index.d.mts +697 -0
- package/dist/index.mjs +1561 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -4
- package/src/config.ts +214 -0
- package/src/define-metadata.ts +255 -0
- package/src/extensions.ts +109 -0
- package/src/index.ts +58 -0
- package/src/metadata-index.ts +161 -0
- package/src/mock-fn.ts +83 -0
- package/src/mock-registry.ts +118 -0
- package/src/mock.ts +270 -0
- package/src/overrides.ts +54 -0
- package/src/spy.ts +246 -0
- package/src/test-app.ts +551 -0
- package/src/testbed-builder.ts +366 -0
- package/src/testbed.ts +97 -0
- package/src/types.ts +107 -0
- package/src/utils.ts +37 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1561 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Application, Container, ErrorCode, InstantiationError, METADATA_VERSION, ZnightError, applyInterceptors, createConsoleLogger, describeToken, displayName, formatPath, isProviderDefinition } from "@znight/core";
|
|
3
|
+
// src/utils.ts
|
|
4
|
+
/** Internal helpers. */
|
|
5
|
+
/**
|
|
6
|
+
* Materialises a `Map`, an iterable of `[key, value]` pairs or a record into an array of pairs (an
|
|
7
|
+
* iterable such as a generator can only be read once).
|
|
8
|
+
*/
|
|
9
|
+
function entriesOf(values) {
|
|
10
|
+
if (typeof values[Symbol.iterator] === "function") {
|
|
11
|
+
const entries = [];
|
|
12
|
+
for (const entry of values) {
|
|
13
|
+
if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError("[znight] Expected [key, value] pairs (a Map, an array of pairs or a record)");
|
|
14
|
+
entries.push(entry);
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
18
|
+
return Object.entries(values);
|
|
19
|
+
}
|
|
20
|
+
/** Whether `value` is a promise-like object (has a callable `then`). */
|
|
21
|
+
function isThenable(value) {
|
|
22
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
23
|
+
}
|
|
24
|
+
/** Describes a provider key (class, token...) for messages. */
|
|
25
|
+
function describeKey(key) {
|
|
26
|
+
if (typeof key === "function") return `class ${key.name || "<anonymous>"}`;
|
|
27
|
+
if (typeof key === "string") return `"${key}"`;
|
|
28
|
+
return String(key);
|
|
29
|
+
}
|
|
30
|
+
// src/define-metadata.ts
|
|
31
|
+
/**
|
|
32
|
+
* Hand-written metadata with the exact shape the compiler generates (version, resolution order...),
|
|
33
|
+
* for tests that do not run the compiler (plugins, libraries, edge cases).
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* const metadata = createMetadata({
|
|
37
|
+
* 'src/clock.ts#Clock': { scope: 'singleton', class: Clock, deps: [], factory: () => new Clock() },
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @throws ZnightError ZNIGHT005 for a malformed provider definition.
|
|
42
|
+
*/
|
|
43
|
+
function createMetadata(providers, options = {}) {
|
|
44
|
+
if (providers === null || typeof providers !== "object") throw new TypeError("[znight] createMetadata() expects a record of provider definitions");
|
|
45
|
+
for (const [token, definition] of Object.entries(providers)) if (!isProviderDefinition(definition)) throw new ZnightError(ErrorCode.InvalidDecorator, `Invalid provider definition for ${describeToken(token)}: expected { scope, deps, factory }`);
|
|
46
|
+
const externals = options.externals?.map((external) => typeof external === "string" ? { token: external } : external);
|
|
47
|
+
return {
|
|
48
|
+
version: METADATA_VERSION,
|
|
49
|
+
providers,
|
|
50
|
+
resolutionOrder: options.resolutionOrder ?? topologicalOrder(providers),
|
|
51
|
+
...options.aliases === void 0 ? {} : { aliases: options.aliases },
|
|
52
|
+
...externals === void 0 ? {} : { externals },
|
|
53
|
+
...options.extensions === void 0 ? {} : { extensions: options.extensions }
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const DEFAULT_PREFIX = "test";
|
|
57
|
+
/** The token {@link defineTestMetadata} derives from a class: `<prefix>#<ClassName>`. */
|
|
58
|
+
function testToken(cls, prefix = DEFAULT_PREFIX) {
|
|
59
|
+
const name = typeof cls === "function" ? cls.name : void 0;
|
|
60
|
+
if (typeof name !== "string" || name === "") throw new TypeError("[znight] testToken(): expected a named class");
|
|
61
|
+
return `${prefix}#${name}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Class-first hand-written metadata: tokens are derived from class names (`test#UserService`),
|
|
65
|
+
* dependencies may be classes, factories default to `new Class(...deps)` and the resolution order
|
|
66
|
+
* is computed.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* const metadata = defineTestMetadata({
|
|
70
|
+
* providers: [
|
|
71
|
+
* PgUserRepository,
|
|
72
|
+
* { class: UserService, deps: [PgUserRepository, '@znight/observability#Logger'] },
|
|
73
|
+
* ],
|
|
74
|
+
* aliases: [[UserRepository, PgUserRepository]],
|
|
75
|
+
* externals: ['@znight/observability#Logger'],
|
|
76
|
+
* })
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @throws ZnightError ZNIGHT004 for duplicate tokens; TypeError for a provider without class nor
|
|
80
|
+
* token/factory.
|
|
81
|
+
*/
|
|
82
|
+
function defineTestMetadata(spec = {}) {
|
|
83
|
+
const prefix = spec.tokenPrefix ?? DEFAULT_PREFIX;
|
|
84
|
+
const providers = (spec.providers ?? []).map((entry) => typeof entry === "function" ? { class: entry } : entry);
|
|
85
|
+
const classTokens = /* @__PURE__ */ new Map();
|
|
86
|
+
const tokens = providers.map((provider) => {
|
|
87
|
+
if (provider === null || typeof provider !== "object") throw new TypeError("[znight] defineTestMetadata(): a provider must be a class or { class?, token?, deps?, factory? }");
|
|
88
|
+
const token = provider.token ?? (provider.class === void 0 ? void 0 : testToken(provider.class, prefix));
|
|
89
|
+
if (token === void 0) throw new TypeError("[znight] defineTestMetadata(): a provider needs a class or a token");
|
|
90
|
+
if (provider.class !== void 0 && !classTokens.has(provider.class)) classTokens.set(provider.class, token);
|
|
91
|
+
return token;
|
|
92
|
+
});
|
|
93
|
+
const toToken = (key) => {
|
|
94
|
+
if (typeof key === "string") return key;
|
|
95
|
+
return classTokens.get(key) ?? testToken(key, prefix);
|
|
96
|
+
};
|
|
97
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
98
|
+
providers.forEach((provider, position) => {
|
|
99
|
+
const token = tokens[position];
|
|
100
|
+
if (definitions.has(token)) throw new ZnightError(ErrorCode.DuplicateProvider, `defineTestMetadata(): duplicate provider ${describeToken(token)}`, { hints: ["Give one of them an explicit token"] });
|
|
101
|
+
definitions.set(token, toDefinition(token, provider, toToken));
|
|
102
|
+
});
|
|
103
|
+
let aliases;
|
|
104
|
+
if (spec.aliases !== void 0) {
|
|
105
|
+
const pairs = entriesOf(spec.aliases).map(([alias, target]) => [toToken(alias), toToken(target)]);
|
|
106
|
+
aliases = Object.fromEntries(pairs);
|
|
107
|
+
}
|
|
108
|
+
return createMetadata(Object.fromEntries(definitions), {
|
|
109
|
+
...aliases === void 0 ? {} : { aliases },
|
|
110
|
+
...spec.externals === void 0 ? {} : { externals: spec.externals },
|
|
111
|
+
...spec.extensions === void 0 ? {} : { extensions: spec.extensions },
|
|
112
|
+
...spec.resolutionOrder === void 0 ? {} : { resolutionOrder: spec.resolutionOrder }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function toDefinition(token, provider, toToken) {
|
|
116
|
+
const cls = provider.class;
|
|
117
|
+
let factory = provider.factory;
|
|
118
|
+
if (factory === void 0) {
|
|
119
|
+
if (cls === void 0) throw new TypeError(`[znight] defineTestMetadata(): ${token} needs a class or a factory`);
|
|
120
|
+
const constructible = cls;
|
|
121
|
+
factory = (deps) => new constructible(...deps);
|
|
122
|
+
}
|
|
123
|
+
const deps = (provider.deps ?? []).map((dep) => toDependency(dep, toToken));
|
|
124
|
+
return {
|
|
125
|
+
scope: provider.scope ?? "singleton",
|
|
126
|
+
deps,
|
|
127
|
+
factory,
|
|
128
|
+
...cls === void 0 ? {} : { class: cls },
|
|
129
|
+
...provider.async === void 0 ? {} : { async: provider.async },
|
|
130
|
+
...provider.interceptors === void 0 ? {} : { interceptors: provider.interceptors },
|
|
131
|
+
...provider.dispose === void 0 ? {} : { dispose: provider.dispose },
|
|
132
|
+
...provider.name === void 0 ? {} : { name: provider.name },
|
|
133
|
+
...provider.primary === void 0 ? {} : { primary: provider.primary },
|
|
134
|
+
...provider.source === void 0 ? {} : { source: provider.source }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function toDependency(dep, toToken) {
|
|
138
|
+
if (typeof dep === "string" || typeof dep === "function") return toToken(dep);
|
|
139
|
+
if (dep !== null && typeof dep === "object") {
|
|
140
|
+
if ("multi" in dep && Array.isArray(dep.multi)) return { multi: dep.multi.map(toToken) };
|
|
141
|
+
if ("lazy" in dep && dep.lazy !== void 0) return { lazy: toToken(dep.lazy) };
|
|
142
|
+
if ("token" in dep && dep.optional === true) return {
|
|
143
|
+
token: toToken(dep.token),
|
|
144
|
+
optional: true
|
|
145
|
+
};
|
|
146
|
+
if ("prototype" in dep) return toToken(dep);
|
|
147
|
+
}
|
|
148
|
+
throw new TypeError(`[znight] defineTestMetadata(): invalid dependency ${String(dep)}`);
|
|
149
|
+
}
|
|
150
|
+
/** Hard dependencies of a definition (lazy ones excluded: they never constrain the order). */
|
|
151
|
+
function hardDependencies(definition) {
|
|
152
|
+
const tokens = [];
|
|
153
|
+
for (const ref of definition.deps) if (typeof ref === "string") tokens.push(ref);
|
|
154
|
+
else if ("multi" in ref) tokens.push(...ref.multi);
|
|
155
|
+
else if ("token" in ref) tokens.push(ref.token);
|
|
156
|
+
return tokens;
|
|
157
|
+
}
|
|
158
|
+
/** Deterministic topological order (dependencies first). Cycles are left for the container to report. */
|
|
159
|
+
function topologicalOrder(providers) {
|
|
160
|
+
const order = [];
|
|
161
|
+
const state = /* @__PURE__ */ new Map();
|
|
162
|
+
const visit = (token) => {
|
|
163
|
+
if (state.has(token) || !Object.hasOwn(providers, token)) return;
|
|
164
|
+
state.set(token, "visiting");
|
|
165
|
+
for (const dep of hardDependencies(providers[token])) visit(dep);
|
|
166
|
+
state.set(token, "done");
|
|
167
|
+
order.push(token);
|
|
168
|
+
};
|
|
169
|
+
for (const token of Object.keys(providers)) visit(token);
|
|
170
|
+
return order;
|
|
171
|
+
}
|
|
172
|
+
// src/spy.ts
|
|
173
|
+
const DEFAULT_NAME = "spy";
|
|
174
|
+
/** Global call counter shared by every built-in spy (`mock.invocationCallOrder`). */
|
|
175
|
+
let callOrder = 0;
|
|
176
|
+
const internalsBySpy = /* @__PURE__ */ new WeakMap();
|
|
177
|
+
function internalsOf(spy) {
|
|
178
|
+
const internals = typeof spy === "function" ? internalsBySpy.get(spy) : void 0;
|
|
179
|
+
if (internals === void 0) throw new TypeError("[znight] Not a spy created by createSpy()");
|
|
180
|
+
return internals;
|
|
181
|
+
}
|
|
182
|
+
function createState() {
|
|
183
|
+
const calls = [];
|
|
184
|
+
return {
|
|
185
|
+
calls,
|
|
186
|
+
results: [],
|
|
187
|
+
instances: [],
|
|
188
|
+
contexts: [],
|
|
189
|
+
invocationCallOrder: [],
|
|
190
|
+
get lastCall() {
|
|
191
|
+
return calls[calls.length - 1];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function clearState(state) {
|
|
196
|
+
state.calls.length = 0;
|
|
197
|
+
state.results.length = 0;
|
|
198
|
+
state.instances.length = 0;
|
|
199
|
+
state.contexts.length = 0;
|
|
200
|
+
state.invocationCallOrder.length = 0;
|
|
201
|
+
}
|
|
202
|
+
function isObjectLike$1(value) {
|
|
203
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
204
|
+
}
|
|
205
|
+
function invoke(internals, self, args, isNew) {
|
|
206
|
+
const state = internals.state;
|
|
207
|
+
state.calls.push(args);
|
|
208
|
+
state.contexts.push(self);
|
|
209
|
+
state.instances.push(self);
|
|
210
|
+
state.invocationCallOrder.push(++callOrder);
|
|
211
|
+
const result = {
|
|
212
|
+
type: "incomplete",
|
|
213
|
+
value: void 0
|
|
214
|
+
};
|
|
215
|
+
state.results.push(result);
|
|
216
|
+
const implementation = internals.once.shift() ?? internals.implementation;
|
|
217
|
+
try {
|
|
218
|
+
const value = implementation === void 0 ? void 0 : implementation.apply(self, args);
|
|
219
|
+
const returned = isNew && !isObjectLike$1(value) ? self : value;
|
|
220
|
+
result.type = "return";
|
|
221
|
+
result.value = returned;
|
|
222
|
+
return returned;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
result.type = "throw";
|
|
225
|
+
result.value = error;
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function returning(value) {
|
|
230
|
+
return () => value;
|
|
231
|
+
}
|
|
232
|
+
function resolving(value) {
|
|
233
|
+
return () => Promise.resolve(value);
|
|
234
|
+
}
|
|
235
|
+
function rejecting(error) {
|
|
236
|
+
return () => Promise.reject(error);
|
|
237
|
+
}
|
|
238
|
+
function returnThis() {
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
/** Methods shared by every built-in spy (the prototype of the spy functions). */
|
|
242
|
+
const spyPrototype = Object.create(Function.prototype, {
|
|
243
|
+
_isMockFunction: { value: true },
|
|
244
|
+
mock: { get() {
|
|
245
|
+
return internalsOf(this).state;
|
|
246
|
+
} },
|
|
247
|
+
mockImplementation: { value(implementation) {
|
|
248
|
+
internalsOf(this).implementation = implementation;
|
|
249
|
+
return this;
|
|
250
|
+
} },
|
|
251
|
+
mockImplementationOnce: { value(implementation) {
|
|
252
|
+
internalsOf(this).once.push(implementation);
|
|
253
|
+
return this;
|
|
254
|
+
} },
|
|
255
|
+
mockReturnValue: { value(value) {
|
|
256
|
+
internalsOf(this).implementation = returning(value);
|
|
257
|
+
return this;
|
|
258
|
+
} },
|
|
259
|
+
mockReturnValueOnce: { value(value) {
|
|
260
|
+
internalsOf(this).once.push(returning(value));
|
|
261
|
+
return this;
|
|
262
|
+
} },
|
|
263
|
+
mockResolvedValue: { value(value) {
|
|
264
|
+
internalsOf(this).implementation = resolving(value);
|
|
265
|
+
return this;
|
|
266
|
+
} },
|
|
267
|
+
mockResolvedValueOnce: { value(value) {
|
|
268
|
+
internalsOf(this).once.push(resolving(value));
|
|
269
|
+
return this;
|
|
270
|
+
} },
|
|
271
|
+
mockRejectedValue: { value(error) {
|
|
272
|
+
internalsOf(this).implementation = rejecting(error);
|
|
273
|
+
return this;
|
|
274
|
+
} },
|
|
275
|
+
mockRejectedValueOnce: { value(error) {
|
|
276
|
+
internalsOf(this).once.push(rejecting(error));
|
|
277
|
+
return this;
|
|
278
|
+
} },
|
|
279
|
+
mockReturnThis: { value() {
|
|
280
|
+
internalsOf(this).implementation = returnThis;
|
|
281
|
+
return this;
|
|
282
|
+
} },
|
|
283
|
+
mockName: { value(name) {
|
|
284
|
+
internalsOf(this).name = String(name);
|
|
285
|
+
return this;
|
|
286
|
+
} },
|
|
287
|
+
getMockName: { value() {
|
|
288
|
+
return internalsOf(this).name;
|
|
289
|
+
} },
|
|
290
|
+
getMockImplementation: { value() {
|
|
291
|
+
return internalsOf(this).implementation;
|
|
292
|
+
} },
|
|
293
|
+
mockClear: { value() {
|
|
294
|
+
clearState(internalsOf(this).state);
|
|
295
|
+
return this;
|
|
296
|
+
} },
|
|
297
|
+
mockReset: { value() {
|
|
298
|
+
const internals = internalsOf(this);
|
|
299
|
+
clearState(internals.state);
|
|
300
|
+
internals.once.length = 0;
|
|
301
|
+
internals.implementation = internals.initial;
|
|
302
|
+
return this;
|
|
303
|
+
} },
|
|
304
|
+
mockRestore: { value() {
|
|
305
|
+
const internals = internalsOf(this);
|
|
306
|
+
clearState(internals.state);
|
|
307
|
+
internals.once.length = 0;
|
|
308
|
+
internals.implementation = internals.initial;
|
|
309
|
+
} }
|
|
310
|
+
});
|
|
311
|
+
/**
|
|
312
|
+
* Creates the built-in, dependency-free, Jest-compatible spy. It records `mock.calls`,
|
|
313
|
+
* `mock.results`, `mock.instances`, `mock.contexts`, `mock.invocationCallOrder` and `mock.lastCall`,
|
|
314
|
+
* supports `mockReturnValue(Once)`, `mockResolvedValue(Once)`, `mockRejectedValue(Once)`,
|
|
315
|
+
* `mockImplementation(Once)`, `mockReturnThis`, `mockName`, `mockClear`, `mockReset` and
|
|
316
|
+
* `mockRestore`, and is recognised by Jest's and Vitest's `expect` (`_isMockFunction`).
|
|
317
|
+
*
|
|
318
|
+
* `mockReset()` restores the implementation given to `createSpy()` (Vitest semantics).
|
|
319
|
+
*
|
|
320
|
+
* ```ts
|
|
321
|
+
* const find = createSpy((id: string) => ({ id }))
|
|
322
|
+
* find.mockReturnValueOnce({ id: 'x' })
|
|
323
|
+
* find('1')
|
|
324
|
+
* find.mock.calls // [['1']]
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
function createSpy(implementation) {
|
|
328
|
+
if (implementation !== void 0 && typeof implementation !== "function") throw new TypeError("[znight] createSpy() expects a function implementation");
|
|
329
|
+
const initial = implementation;
|
|
330
|
+
const internals = {
|
|
331
|
+
name: DEFAULT_NAME,
|
|
332
|
+
implementation: initial,
|
|
333
|
+
initial,
|
|
334
|
+
once: [],
|
|
335
|
+
state: createState()
|
|
336
|
+
};
|
|
337
|
+
const spy = function(...args) {
|
|
338
|
+
return invoke(internals, this, args, new.target !== void 0);
|
|
339
|
+
};
|
|
340
|
+
Object.setPrototypeOf(spy, spyPrototype);
|
|
341
|
+
internalsBySpy.set(spy, internals);
|
|
342
|
+
return spy;
|
|
343
|
+
}
|
|
344
|
+
/** Whether `value` is a spy created by {@link createSpy}. */
|
|
345
|
+
function isBuiltinSpy(value) {
|
|
346
|
+
return typeof value === "function" && internalsBySpy.has(value);
|
|
347
|
+
}
|
|
348
|
+
// src/mock-fn.ts
|
|
349
|
+
const BUILTIN = Object.freeze({
|
|
350
|
+
source: "builtin",
|
|
351
|
+
create: createSpy
|
|
352
|
+
});
|
|
353
|
+
let defaultOption = "auto";
|
|
354
|
+
/** `mock` of `bun:test`: `undefined` until probed, `null` when unavailable. */
|
|
355
|
+
let bunMock;
|
|
356
|
+
function assertOption(option, where) {
|
|
357
|
+
if (typeof option === "function" || option === "auto" || option === "builtin") return;
|
|
358
|
+
throw new TypeError(`[znight] ${where}: mockFn must be 'auto', 'builtin' or a function such as vi.fn`);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Sets the spy factory used when `mockFn` is not passed explicitly (e.g. in a Vitest setup file:
|
|
362
|
+
* `setDefaultMockFn(vi.fn)`). Without argument, restores auto-detection.
|
|
363
|
+
*/
|
|
364
|
+
function setDefaultMockFn(option = "auto") {
|
|
365
|
+
assertOption(option, "setDefaultMockFn()");
|
|
366
|
+
defaultOption = option;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Resolves a {@link MockFnOption} (default: the option set with {@link setDefaultMockFn}, `'auto'`
|
|
370
|
+
* initially) to a spy factory.
|
|
371
|
+
*/
|
|
372
|
+
function resolveMockFn(option) {
|
|
373
|
+
const effective = option ?? defaultOption;
|
|
374
|
+
assertOption(effective, "resolveMockFn()");
|
|
375
|
+
if (typeof effective === "function") return {
|
|
376
|
+
source: "custom",
|
|
377
|
+
create: effective
|
|
378
|
+
};
|
|
379
|
+
if (effective === "builtin") return BUILTIN;
|
|
380
|
+
return detectMockFn();
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Auto-detects the spy factory of the running test framework: Vitest (`globalThis.vi.fn`, with
|
|
384
|
+
* `globals: true`), Jest (`globalThis.jest.fn`), `bun:test` (`mock`, loaded only when running on
|
|
385
|
+
* Bun), otherwise the built-in spy ({@link createSpy}).
|
|
386
|
+
*/
|
|
387
|
+
function detectMockFn() {
|
|
388
|
+
const scope = globalThis;
|
|
389
|
+
const vitest = scope.vi?.fn;
|
|
390
|
+
if (typeof vitest === "function") return {
|
|
391
|
+
source: "vitest",
|
|
392
|
+
create: vitest
|
|
393
|
+
};
|
|
394
|
+
const jest = scope.jest?.fn;
|
|
395
|
+
if (typeof jest === "function") return {
|
|
396
|
+
source: "jest",
|
|
397
|
+
create: jest
|
|
398
|
+
};
|
|
399
|
+
const bun = loadBunMock();
|
|
400
|
+
if (bun !== void 0) return {
|
|
401
|
+
source: "bun",
|
|
402
|
+
create: bun
|
|
403
|
+
};
|
|
404
|
+
return BUILTIN;
|
|
405
|
+
}
|
|
406
|
+
/** Whether the code runs on Bun. */
|
|
407
|
+
function isBunRuntime() {
|
|
408
|
+
return typeof globalThis.Bun !== "undefined";
|
|
409
|
+
}
|
|
410
|
+
function loadBunMock() {
|
|
411
|
+
bunMock ??= isBunRuntime() ? requireBunMock() : null;
|
|
412
|
+
return bunMock ?? void 0;
|
|
413
|
+
}
|
|
414
|
+
function requireBunMock() {
|
|
415
|
+
try {
|
|
416
|
+
const specifier = ["bun", "test"].join(":");
|
|
417
|
+
const bunTest = createRequire(import.meta.url)(specifier);
|
|
418
|
+
return typeof bunTest.mock === "function" ? bunTest.mock : null;
|
|
419
|
+
} catch {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// src/mock.ts
|
|
424
|
+
/**
|
|
425
|
+
* Property names never auto-mocked (they read as `undefined` unless set explicitly). Libraries probe
|
|
426
|
+
* them to detect promises (`then`), JSON values (`toJSON`), asymmetric matchers (`asymmetricMatch`),
|
|
427
|
+
* React elements (`$$typeof`), DOM nodes (`nodeType`, `tagName`), ES module namespaces
|
|
428
|
+
* (`__esModule`) or mock functions (`_isMockFunction`). Names starting with `@@` (Immutable.js
|
|
429
|
+
* sentinels) are reserved too. Symbol-keyed properties are never auto-mocked.
|
|
430
|
+
*/
|
|
431
|
+
const RESERVED = /* @__PURE__ */ new Set([
|
|
432
|
+
"then",
|
|
433
|
+
"toJSON",
|
|
434
|
+
"asymmetricMatch",
|
|
435
|
+
"$$typeof",
|
|
436
|
+
"nodeType",
|
|
437
|
+
"tagName",
|
|
438
|
+
"__esModule",
|
|
439
|
+
"_isMockFunction"
|
|
440
|
+
]);
|
|
441
|
+
const TREE = Symbol("znight.mock");
|
|
442
|
+
const mocks = /* @__PURE__ */ new WeakSet();
|
|
443
|
+
function isReserved(key) {
|
|
444
|
+
return RESERVED.has(key) || key.startsWith("@@");
|
|
445
|
+
}
|
|
446
|
+
function createMock(first, second, third) {
|
|
447
|
+
if (typeof first === "function") return mockObject(first, second, third);
|
|
448
|
+
return mockObject(void 0, first, second);
|
|
449
|
+
}
|
|
450
|
+
/** {@link createMock} with an optional class (internal). */
|
|
451
|
+
function mockObject(cls, partial, options = {}) {
|
|
452
|
+
if (partial !== void 0 && (partial === null || typeof partial !== "object")) throw new TypeError("[znight] createMock(): the partial implementation must be an object");
|
|
453
|
+
const tree = {
|
|
454
|
+
create: resolveMockFn(options.mockFn).create,
|
|
455
|
+
deep: options.deep === true,
|
|
456
|
+
spies: []
|
|
457
|
+
};
|
|
458
|
+
const className = typeof cls === "function" ? cls.name : void 0;
|
|
459
|
+
return createNode(options.name ?? (typeof className === "string" && className !== "" ? className : "mock"), tree, cls, partial);
|
|
460
|
+
}
|
|
461
|
+
/** Whether `value` is a mock (or one of its spies) created by {@link createMock}. */
|
|
462
|
+
function isMock(value) {
|
|
463
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && mocks.has(value);
|
|
464
|
+
}
|
|
465
|
+
/** Forgets the calls recorded by every spy of `mock` (`mockClear()`); implementations are kept. */
|
|
466
|
+
function clearMock(mock) {
|
|
467
|
+
for (const { spy } of treeOf(mock, "clearMock").spies) spy.mockClear?.();
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Resets every spy of `mock` to its initial state: recorded calls and programmed implementations
|
|
471
|
+
* are forgotten, implementations given to {@link createMock} (and deep returns) are restored.
|
|
472
|
+
*/
|
|
473
|
+
function resetMock(mock) {
|
|
474
|
+
for (const { spy, implementation } of treeOf(mock, "resetMock").spies) {
|
|
475
|
+
spy.mockReset?.();
|
|
476
|
+
if (implementation !== void 0) spy.mockImplementation?.(implementation);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function treeOf(mock, where) {
|
|
480
|
+
const tree = isMock(mock) ? mock[TREE] : void 0;
|
|
481
|
+
if (tree === void 0) throw new TypeError(`[znight] ${where}(): expected a mock created by createMock()`);
|
|
482
|
+
return tree;
|
|
483
|
+
}
|
|
484
|
+
/** An object mock (the root of a mock, or the value returned by a deep spy). */
|
|
485
|
+
function createNode(name, tree, cls, partial) {
|
|
486
|
+
const target = Object.create(typeof cls === "function" && isObjectLike(cls.prototype) ? cls.prototype : Object.prototype);
|
|
487
|
+
const accessors = typeof cls === "function" ? accessorKeys(cls.prototype) : void 0;
|
|
488
|
+
const children = /* @__PURE__ */ new Map();
|
|
489
|
+
if (partial !== void 0) for (const key of Reflect.ownKeys(partial)) {
|
|
490
|
+
const value = partial[key];
|
|
491
|
+
if (typeof key === "string" && typeof value === "function") children.set(key, createSpyNode(`${name}.${key}`, tree, value));
|
|
492
|
+
else defineValue(target, key, value);
|
|
493
|
+
}
|
|
494
|
+
const proxy = new Proxy(target, {
|
|
495
|
+
get(object, key, receiver) {
|
|
496
|
+
if (typeof key === "symbol") {
|
|
497
|
+
if (key === TREE) return tree;
|
|
498
|
+
return Object.hasOwn(object, key) ? object[key] : void 0;
|
|
499
|
+
}
|
|
500
|
+
if (Object.hasOwn(object, key)) return object[key];
|
|
501
|
+
if (key === "constructor") return Reflect.get(object, key, receiver);
|
|
502
|
+
if (isReserved(key)) return void 0;
|
|
503
|
+
if (Object.hasOwn(Object.prototype, key)) return Reflect.get(Object.prototype, key, receiver);
|
|
504
|
+
if (accessors?.has(key) === true) return void 0;
|
|
505
|
+
let child = children.get(key);
|
|
506
|
+
if (child === void 0) {
|
|
507
|
+
child = createSpyNode(`${name}.${key}`, tree, void 0);
|
|
508
|
+
children.set(key, child);
|
|
509
|
+
}
|
|
510
|
+
return child;
|
|
511
|
+
},
|
|
512
|
+
set(object, key, value) {
|
|
513
|
+
defineValue(object, key, value);
|
|
514
|
+
return true;
|
|
515
|
+
},
|
|
516
|
+
deleteProperty(object, key) {
|
|
517
|
+
if (typeof key === "string") children.delete(key);
|
|
518
|
+
return Reflect.deleteProperty(object, key);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
mocks.add(proxy);
|
|
522
|
+
return proxy;
|
|
523
|
+
}
|
|
524
|
+
/** A spy whose unknown properties are deep spies (`spy.nested.method`). */
|
|
525
|
+
function createSpyNode(path, tree, implementation) {
|
|
526
|
+
const effective = implementation ?? (tree.deep ? deepReturn(path, tree) : void 0);
|
|
527
|
+
const spy = tree.create(effective);
|
|
528
|
+
if (typeof spy !== "function") throw new TypeError(`[znight] The mockFn factory returned ${typeof spy} instead of a function`);
|
|
529
|
+
if (typeof spy.mockName === "function") spy.mockName(path);
|
|
530
|
+
makeDeep(spy, path, tree);
|
|
531
|
+
tree.spies.push({
|
|
532
|
+
spy,
|
|
533
|
+
implementation: effective
|
|
534
|
+
});
|
|
535
|
+
mocks.add(spy);
|
|
536
|
+
return spy;
|
|
537
|
+
}
|
|
538
|
+
/** The implementation of a deep spy: returns the same deep mock on every call. */
|
|
539
|
+
function deepReturn(path, tree) {
|
|
540
|
+
let result;
|
|
541
|
+
return () => {
|
|
542
|
+
result ??= createNode(`${path}()`, tree, void 0, void 0);
|
|
543
|
+
return result;
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Inserts a proxy between `spy` and its prototype: reading a property the spy does not have (own or
|
|
548
|
+
* inherited) creates a child spy. The spy itself stays a genuine spy of its framework, which
|
|
549
|
+
* `bun:test`'s `expect` requires (it rejects proxies).
|
|
550
|
+
*/
|
|
551
|
+
function makeDeep(spy, path, tree) {
|
|
552
|
+
const original = Object.getPrototypeOf(spy);
|
|
553
|
+
if (!isObjectLike(original)) return;
|
|
554
|
+
const children = /* @__PURE__ */ new Map();
|
|
555
|
+
const prototype = new Proxy(original, { get(object, key, receiver) {
|
|
556
|
+
if (typeof key === "symbol" || key in object || isReserved(key)) return Reflect.get(object, key, receiver);
|
|
557
|
+
let child = children.get(key);
|
|
558
|
+
if (child === void 0) {
|
|
559
|
+
child = createSpyNode(`${path}.${key}`, tree, void 0);
|
|
560
|
+
children.set(key, child);
|
|
561
|
+
}
|
|
562
|
+
return child;
|
|
563
|
+
} });
|
|
564
|
+
try {
|
|
565
|
+
Object.setPrototypeOf(spy, prototype);
|
|
566
|
+
} catch {}
|
|
567
|
+
}
|
|
568
|
+
function defineValue(object, key, value) {
|
|
569
|
+
Object.defineProperty(object, key, {
|
|
570
|
+
value,
|
|
571
|
+
writable: true,
|
|
572
|
+
enumerable: true,
|
|
573
|
+
configurable: true
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
/** Names of the accessors (getters/setters) declared along a prototype chain, `Object.prototype` excluded. */
|
|
577
|
+
function accessorKeys(prototype) {
|
|
578
|
+
const keys = /* @__PURE__ */ new Set();
|
|
579
|
+
let current = prototype;
|
|
580
|
+
while (isObjectLike(current) && current !== Object.prototype && current !== Function.prototype) {
|
|
581
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
582
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
583
|
+
if (descriptor !== void 0 && (descriptor.get !== void 0 || descriptor.set !== void 0)) keys.add(key);
|
|
584
|
+
}
|
|
585
|
+
current = Object.getPrototypeOf(current);
|
|
586
|
+
}
|
|
587
|
+
return keys;
|
|
588
|
+
}
|
|
589
|
+
function isObjectLike(value) {
|
|
590
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
591
|
+
}
|
|
592
|
+
// src/overrides.ts
|
|
593
|
+
/**
|
|
594
|
+
* A type-checked override: the value must match the type designated by the class or typed token.
|
|
595
|
+
*
|
|
596
|
+
* ```ts
|
|
597
|
+
* await TestApp.create({ metadata, overrides: [override(UserRepository, new InMemoryUserRepository())] })
|
|
598
|
+
* ```
|
|
599
|
+
*/
|
|
600
|
+
function override(key, value) {
|
|
601
|
+
return [key, value];
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Replaces a provider with a factory whose dependencies are resolved by the container (or, in
|
|
605
|
+
* `TestBed`, like the target's dependencies).
|
|
606
|
+
*
|
|
607
|
+
* ```ts
|
|
608
|
+
* overrideFactory(UserRepository, ([db]) => new InMemoryUserRepository(db), { deps: ['src/db.ts#Database'] })
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
function overrideFactory(key, factory, options = {}) {
|
|
612
|
+
if (typeof factory !== "function") throw new TypeError("[znight] overrideFactory() expects a factory function");
|
|
613
|
+
return [key, {
|
|
614
|
+
scope: options.scope ?? "singleton",
|
|
615
|
+
deps: options.deps ?? [],
|
|
616
|
+
factory,
|
|
617
|
+
...options.async === void 0 ? {} : { async: options.async },
|
|
618
|
+
...options.dispose === void 0 ? {} : { dispose: options.dispose }
|
|
619
|
+
}];
|
|
620
|
+
}
|
|
621
|
+
// src/extensions.ts
|
|
622
|
+
function isRecord(value) {
|
|
623
|
+
return value !== null && typeof value === "object";
|
|
624
|
+
}
|
|
625
|
+
function arrayOf(value) {
|
|
626
|
+
return Array.isArray(value) ? value : [];
|
|
627
|
+
}
|
|
628
|
+
function extension(metadata, name) {
|
|
629
|
+
if (!isRecord(metadata)) return void 0;
|
|
630
|
+
const extensions = metadata.extensions;
|
|
631
|
+
if (!isRecord(extensions)) return void 0;
|
|
632
|
+
const value = extensions[name];
|
|
633
|
+
return isRecord(value) ? value : void 0;
|
|
634
|
+
}
|
|
635
|
+
function tokensOf(value) {
|
|
636
|
+
return arrayOf(value).filter((token) => typeof token === "string");
|
|
637
|
+
}
|
|
638
|
+
/** Configuration classes declared by `@znight/config` (`extensions.config.configs`), by token. */
|
|
639
|
+
function readConfigClasses(metadata) {
|
|
640
|
+
const result = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const entry of arrayOf(extension(metadata, "config")?.["configs"])) {
|
|
642
|
+
if (!isRecord(entry) || typeof entry["token"] !== "string") continue;
|
|
643
|
+
const fields = [];
|
|
644
|
+
for (const field of arrayOf(entry["fields"])) {
|
|
645
|
+
if (!isRecord(field) || typeof field["property"] !== "string") continue;
|
|
646
|
+
fields.push({
|
|
647
|
+
property: field["property"],
|
|
648
|
+
env: typeof field["env"] === "string" ? field["env"] : void 0,
|
|
649
|
+
type: typeof field["type"] === "string" ? field["type"] : void 0,
|
|
650
|
+
hasDefault: Object.hasOwn(field, "default") && field["default"] !== void 0,
|
|
651
|
+
default: field["default"],
|
|
652
|
+
required: field["required"] === true
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
const prefix = entry["prefix"];
|
|
656
|
+
result.set(entry["token"], {
|
|
657
|
+
token: entry["token"],
|
|
658
|
+
prefix: typeof prefix === "string" && prefix !== "" ? prefix : void 0,
|
|
659
|
+
fields
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Tokens an HTTP application needs at boot: controllers (`extensions.http.controllers[].token`) and
|
|
666
|
+
* the guards and interceptors they and their routes reference.
|
|
667
|
+
*/
|
|
668
|
+
function readHttpRoots(metadata) {
|
|
669
|
+
const roots = [];
|
|
670
|
+
for (const controller of arrayOf(extension(metadata, "http")?.["controllers"])) {
|
|
671
|
+
if (!isRecord(controller)) continue;
|
|
672
|
+
if (typeof controller["token"] === "string") roots.push(controller["token"]);
|
|
673
|
+
roots.push(...tokensOf(controller["guards"]), ...tokensOf(controller["interceptors"]));
|
|
674
|
+
for (const route of arrayOf(controller["routes"])) if (isRecord(route)) roots.push(...tokensOf(route["guards"]), ...tokensOf(route["interceptors"]));
|
|
675
|
+
}
|
|
676
|
+
return roots;
|
|
677
|
+
}
|
|
678
|
+
/** Message consumers (`extensions.messaging.consumers[].token`). */
|
|
679
|
+
function readMessagingRoots(metadata) {
|
|
680
|
+
const roots = [];
|
|
681
|
+
for (const consumer of arrayOf(extension(metadata, "messaging")?.["consumers"])) if (isRecord(consumer) && typeof consumer["token"] === "string") roots.push(consumer["token"]);
|
|
682
|
+
return roots;
|
|
683
|
+
}
|
|
684
|
+
/** Hosts of scheduled jobs (`extensions.schedule.jobs[].token`), disabled jobs included (they can be triggered). */
|
|
685
|
+
function readScheduleRoots(metadata) {
|
|
686
|
+
const roots = [];
|
|
687
|
+
for (const job of arrayOf(extension(metadata, "schedule")?.["jobs"])) if (isRecord(job) && typeof job["token"] === "string") roots.push(job["token"]);
|
|
688
|
+
return roots;
|
|
689
|
+
}
|
|
690
|
+
// src/metadata-index.ts
|
|
691
|
+
/**
|
|
692
|
+
* Read-only index over compiled metadata: provider definitions, aliases, externals and the key
|
|
693
|
+
* lookups shared by `TestBed`, `mocks.get()` and the testing helpers (class, token, alias or display
|
|
694
|
+
* name → canonical token). Internal.
|
|
695
|
+
*/
|
|
696
|
+
var MetadataIndex = class {
|
|
697
|
+
metadata;
|
|
698
|
+
#providers = /* @__PURE__ */ new Map();
|
|
699
|
+
#aliases = /* @__PURE__ */ new Map();
|
|
700
|
+
#externals = /* @__PURE__ */ new Map();
|
|
701
|
+
constructor(metadata) {
|
|
702
|
+
assertMetadata(metadata);
|
|
703
|
+
this.metadata = metadata;
|
|
704
|
+
for (const [token, definition] of Object.entries(metadata.providers)) this.#providers.set(token, definition);
|
|
705
|
+
for (const [alias, target] of Object.entries(metadata.aliases ?? {})) this.#aliases.set(alias, target);
|
|
706
|
+
for (const external of metadata.externals ?? []) if (typeof external?.token === "string") this.#externals.set(external.token, external);
|
|
707
|
+
}
|
|
708
|
+
/** Every provider token, in declaration order. */
|
|
709
|
+
providerTokens() {
|
|
710
|
+
return this.#providers.keys();
|
|
711
|
+
}
|
|
712
|
+
definition(token) {
|
|
713
|
+
return this.#providers.get(token);
|
|
714
|
+
}
|
|
715
|
+
isProvider(token) {
|
|
716
|
+
return this.#providers.has(token);
|
|
717
|
+
}
|
|
718
|
+
external(token) {
|
|
719
|
+
return this.#externals.get(token);
|
|
720
|
+
}
|
|
721
|
+
/** Whether `token` is a provider, an alias or an external of the metadata. */
|
|
722
|
+
isKnown(token) {
|
|
723
|
+
return this.#providers.has(token) || this.#aliases.has(token) || this.#externals.has(token);
|
|
724
|
+
}
|
|
725
|
+
/** Provider, alias and external tokens. */
|
|
726
|
+
knownTokens() {
|
|
727
|
+
return /* @__PURE__ */ new Set([
|
|
728
|
+
...this.#providers.keys(),
|
|
729
|
+
...this.#aliases.keys(),
|
|
730
|
+
...this.#externals.keys()
|
|
731
|
+
]);
|
|
732
|
+
}
|
|
733
|
+
/** The class of a provider, when it is a class provider. */
|
|
734
|
+
classOf(token) {
|
|
735
|
+
return this.#providers.get(token)?.class;
|
|
736
|
+
}
|
|
737
|
+
/** Follows aliases (cycle-guarded) until a provider token; returns `token` itself otherwise. */
|
|
738
|
+
canonical(token) {
|
|
739
|
+
let current = token;
|
|
740
|
+
const seen = /* @__PURE__ */ new Set();
|
|
741
|
+
while (!this.#providers.has(current) && this.#aliases.has(current) && !seen.has(current)) {
|
|
742
|
+
seen.add(current);
|
|
743
|
+
current = this.#aliases.get(current);
|
|
744
|
+
}
|
|
745
|
+
return current;
|
|
746
|
+
}
|
|
747
|
+
/** Alias entries (alias → target), for filtering and display. */
|
|
748
|
+
aliases() {
|
|
749
|
+
return this.#aliases.entries();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Finds the canonical token designated by `key` among `universe`:
|
|
753
|
+
* - a string: a token of `universe`, an alias resolving into it, or a unique display name
|
|
754
|
+
* (`'UserService'`) of a token or alias;
|
|
755
|
+
* - a class: the token whose provider class is `key`, else the unique token whose class extends
|
|
756
|
+
* it, else a unique display name equal to the class name (externals, interfaces).
|
|
757
|
+
*
|
|
758
|
+
* @throws ZnightError ZNIGHT008 when a display name is ambiguous.
|
|
759
|
+
*/
|
|
760
|
+
find(key, universe) {
|
|
761
|
+
if (typeof key === "string") {
|
|
762
|
+
if (universe.has(key)) return key;
|
|
763
|
+
const canonical = this.canonical(key);
|
|
764
|
+
if (universe.has(canonical)) return canonical;
|
|
765
|
+
return this.#findByName(key, universe, void 0);
|
|
766
|
+
}
|
|
767
|
+
if (typeof key !== "function") return void 0;
|
|
768
|
+
const cls = key;
|
|
769
|
+
const exact = [...universe].filter((token) => this.classOf(token) === cls);
|
|
770
|
+
if (exact.length > 0) return exact[0];
|
|
771
|
+
const derived = [...universe].filter((token) => extendsClass(this.classOf(token), cls));
|
|
772
|
+
if (derived.length === 1) return derived[0];
|
|
773
|
+
if (derived.length > 1) throw ambiguousError(cls.name, derived);
|
|
774
|
+
const name = cls.name;
|
|
775
|
+
return typeof name === "string" && name !== "" ? this.#findByName(name, universe, cls) : void 0;
|
|
776
|
+
}
|
|
777
|
+
#findByName(name, universe, cls) {
|
|
778
|
+
const matches = /* @__PURE__ */ new Set();
|
|
779
|
+
const add = (token) => {
|
|
780
|
+
const canonical = this.canonical(token);
|
|
781
|
+
if (universe.has(canonical)) matches.add(canonical);
|
|
782
|
+
else if (universe.has(token)) matches.add(token);
|
|
783
|
+
};
|
|
784
|
+
for (const token of universe) if (displayName(token) === name) add(token);
|
|
785
|
+
for (const [alias] of this.#aliases) if (displayName(alias) === name) add(alias);
|
|
786
|
+
const candidates = [...matches].filter((token) => {
|
|
787
|
+
if (cls === void 0) return true;
|
|
788
|
+
const own = this.classOf(token);
|
|
789
|
+
return own === void 0 || own === cls || extendsClass(own, cls);
|
|
790
|
+
});
|
|
791
|
+
if (candidates.length === 1) return candidates[0];
|
|
792
|
+
if (candidates.length > 1) throw ambiguousError(name, candidates);
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
function extendsClass(candidate, base) {
|
|
796
|
+
if (candidate === void 0 || candidate === base) return false;
|
|
797
|
+
const prototype = base.prototype;
|
|
798
|
+
return typeof prototype === "object" && prototype !== null && candidate.prototype instanceof base;
|
|
799
|
+
}
|
|
800
|
+
function ambiguousError(name, tokens) {
|
|
801
|
+
return new ZnightError(ErrorCode.AmbiguousInjection, `"${name}" is ambiguous: it matches ${tokens.map(describeToken).join(", ")}`, { hints: ["Use the full token (or the class) instead of the display name"] });
|
|
802
|
+
}
|
|
803
|
+
function assertMetadata(metadata) {
|
|
804
|
+
if (metadata === null || typeof metadata !== "object" || metadata.providers === null || typeof metadata.providers !== "object") throw new ZnightError(ErrorCode.MissingProvider, "No compiled metadata: expected the generated __ZNIGHT_METADATA__", { hints: ["Pass { metadata: __ZNIGHT_METADATA__ } imported from src/.znight/container.generated.ts", "Generate it before running tests: `znight generate`, the Bun preload `@znight/compiler/bun` or the Vite plugin under Vitest"] });
|
|
805
|
+
if (metadata.version !== METADATA_VERSION) throw new ZnightError(ErrorCode.VersionMismatch, `Unsupported metadata version ${String(metadata.version)} (expected ${METADATA_VERSION})`, { hints: ["Regenerate the metadata with the @znight/compiler version matching @znight/testing"] });
|
|
806
|
+
}
|
|
807
|
+
// src/test-app.ts
|
|
808
|
+
/** Name of the HTTP plugin `TestApp.inject()` delegates to (`HttpPlugin.configure()` from `@znight/http`). */
|
|
809
|
+
const HTTP_PLUGIN_NAME = "http";
|
|
810
|
+
/**
|
|
811
|
+
* End-to-end harness: a started {@link Application} without signal handlers, overrides applied last
|
|
812
|
+
* and only what the tested entry points need instantiated.
|
|
813
|
+
*
|
|
814
|
+
* ```ts
|
|
815
|
+
* await using app = await TestApp.create({
|
|
816
|
+
* metadata: __ZNIGHT_METADATA__,
|
|
817
|
+
* plugins: [HttpPlugin.configure({ adapter: fastify() })],
|
|
818
|
+
* overrides: [[UserRepository, fakeRepository]],
|
|
819
|
+
* })
|
|
820
|
+
* const response = await app.inject({ method: 'GET', url: '/users/1' })
|
|
821
|
+
* expect(response.status).toBe(200)
|
|
822
|
+
* expect(response.json()).toEqual({ id: '1' })
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
var TestApp = class TestApp {
|
|
826
|
+
/** The application under test. */
|
|
827
|
+
app;
|
|
828
|
+
#closed = false;
|
|
829
|
+
constructor(app) {
|
|
830
|
+
this.app = app;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Creates and (by default) starts the application.
|
|
834
|
+
*
|
|
835
|
+
* Plugins are used as configured: give the servers they start a free port or none in tests
|
|
836
|
+
* (`HttpPlugin.configure({ port: 0 })`, `ObservabilityPlugin.configure({ metrics: { server: false } })`);
|
|
837
|
+
* a start failing on a port already in use (`EADDRINUSE`) gets a hint saying so.
|
|
838
|
+
*
|
|
839
|
+
* @throws the error of `Application.create()` / `app.start()` (everything is rolled back).
|
|
840
|
+
*/
|
|
841
|
+
static async create(options) {
|
|
842
|
+
if (options === null || typeof options !== "object") throw new TypeError("[znight] TestApp.create({ metadata }) requires options with the generated metadata");
|
|
843
|
+
const index = new MetadataIndex(options.metadata);
|
|
844
|
+
const providers = resolveKeys(options.providers, index);
|
|
845
|
+
const overrides = resolveKeys(options.overrides, index);
|
|
846
|
+
const init = initOptions(options, index, overrides);
|
|
847
|
+
try {
|
|
848
|
+
const app = await Application.create({
|
|
849
|
+
metadata: options.metadata,
|
|
850
|
+
plugins: options.plugins ?? [],
|
|
851
|
+
...providers === void 0 ? {} : { providers },
|
|
852
|
+
...overrides === void 0 ? {} : { overrides },
|
|
853
|
+
logger: options.logger ?? createConsoleLogger({ level: "warn" }),
|
|
854
|
+
shutdown: {
|
|
855
|
+
...options.shutdown,
|
|
856
|
+
signals: false
|
|
857
|
+
},
|
|
858
|
+
init
|
|
859
|
+
});
|
|
860
|
+
if (options.start !== false) await app.start();
|
|
861
|
+
return new TestApp(app);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
addHints(error, init.only !== void 0);
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/** The application container. */
|
|
868
|
+
get container() {
|
|
869
|
+
return this.app.container;
|
|
870
|
+
}
|
|
871
|
+
get(key) {
|
|
872
|
+
return this.app.container.get(key);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Dispatches a request in memory through the HTTP plugin (`HttpPlugin`): routing, guards,
|
|
876
|
+
* interceptors, validation, serialisation, without opening a socket. Send uploads as a
|
|
877
|
+
* `FormData` body; read downloads from `response.bytes`. Send cookies with `cookies` (and
|
|
878
|
+
* `signedCookies`), read those the response sets from `response.cookies`.
|
|
879
|
+
*
|
|
880
|
+
* @throws ZnightError ZNIGHT001 when the application has no HTTP plugin, ZNIGHT021 once closed.
|
|
881
|
+
*/
|
|
882
|
+
inject(request) {
|
|
883
|
+
try {
|
|
884
|
+
if (this.#closed) throw new ZnightError(ErrorCode.LifecycleFailed, "TestApp.inject(): the application is closed");
|
|
885
|
+
if (request === null || typeof request !== "object" || typeof request.url !== "string") throw new TypeError("[znight] TestApp.inject() expects { method?, url, headers?, body?, query?, cookies?, signedCookies? }");
|
|
886
|
+
const plugin = this.app.getPlugin(HTTP_PLUGIN_NAME);
|
|
887
|
+
if (plugin === void 0 || typeof plugin.inject !== "function") throw new ZnightError(ErrorCode.MissingProvider, "TestApp.inject() requires the HTTP plugin", { hints: ["Add HttpPlugin.configure({ adapter }) from @znight/http to TestApp.create({ plugins })"] });
|
|
888
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
889
|
+
return Promise.resolve(plugin.inject({
|
|
890
|
+
...request,
|
|
891
|
+
method
|
|
892
|
+
})).then(complete);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
return Promise.reject(error);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Dispatches a request in memory and resolves at the response headers; the body is read as it is
|
|
899
|
+
* produced (`response.body`). Cancelling the body (or aborting `signal`) disconnects the client.
|
|
900
|
+
*
|
|
901
|
+
* @throws ZnightError ZNIGHT001 when the HTTP plugin does not support streaming, ZNIGHT021 once closed.
|
|
902
|
+
*/
|
|
903
|
+
injectStream(request) {
|
|
904
|
+
return this.#stream("injectStream", request);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Opens an event stream (Server-Sent Events) in memory and reads it like `EventSource`:
|
|
908
|
+
*
|
|
909
|
+
* ```ts
|
|
910
|
+
* await using stream = await app.injectSse({ url: '/orders/o-1/events' })
|
|
911
|
+
* const event = await stream.next() // rejects after 2 000 ms
|
|
912
|
+
* expect(event.json()).toMatchObject({ status: 'paid' })
|
|
913
|
+
* const resumed = await stream.reconnect() // last-event-id, like EventSource
|
|
914
|
+
* ```
|
|
915
|
+
*
|
|
916
|
+
* Leaving the `await using` block (or `close()`) disconnects: the server sees `ctx.signal` abort.
|
|
917
|
+
*
|
|
918
|
+
* @throws ZnightError ZNIGHT001 when the HTTP plugin does not support event streams, ZNIGHT021 once closed.
|
|
919
|
+
*/
|
|
920
|
+
injectSse(request) {
|
|
921
|
+
return this.#stream("injectSse", request);
|
|
922
|
+
}
|
|
923
|
+
#stream(method, request) {
|
|
924
|
+
try {
|
|
925
|
+
if (this.#closed) throw new ZnightError(ErrorCode.LifecycleFailed, `TestApp.${method}(): the application is closed`);
|
|
926
|
+
if (request === null || typeof request !== "object" || typeof request.url !== "string") throw new TypeError(`[znight] TestApp.${method}() expects { method?, url, headers?, body?, query?, cookies?, signedCookies?, signal? }`);
|
|
927
|
+
const plugin = this.app.getPlugin(HTTP_PLUGIN_NAME);
|
|
928
|
+
const dispatch = plugin?.[method];
|
|
929
|
+
if (typeof dispatch !== "function") throw new ZnightError(ErrorCode.MissingProvider, `TestApp.${method}() requires the HTTP plugin with streaming support`, { hints: [plugin === void 0 ? "Add HttpPlugin.configure({ adapter }) from @znight/http to TestApp.create({ plugins })" : `The plugin named 'http' does not implement ${method}(): register HttpPlugin.configure({ adapter }) from @znight/http ≥ 0.1.0 (the same release as @znight/testing)`] });
|
|
930
|
+
const sent = {
|
|
931
|
+
...request,
|
|
932
|
+
method: (request.method ?? "GET").toUpperCase()
|
|
933
|
+
};
|
|
934
|
+
return Promise.resolve(dispatch.call(plugin, sent));
|
|
935
|
+
} catch (error) {
|
|
936
|
+
return Promise.reject(error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/** Stops the application (idempotent): plugins `onStop`, providers `onStop` and disposal, plugins `onDispose`. */
|
|
940
|
+
close() {
|
|
941
|
+
this.#closed = true;
|
|
942
|
+
return this.app.stop();
|
|
943
|
+
}
|
|
944
|
+
/** {@link close}, for `await using app = await TestApp.create(...)`. */
|
|
945
|
+
[Symbol.asyncDispose]() {
|
|
946
|
+
return this.close();
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const ENCODER = new TextEncoder();
|
|
950
|
+
const NO_COOKIES = Object.freeze({});
|
|
951
|
+
/**
|
|
952
|
+
* The plugin response, as is when it is complete; otherwise with `bytes` derived from `body` and
|
|
953
|
+
* no `cookies` when the plugin did not provide them.
|
|
954
|
+
*/
|
|
955
|
+
function complete(response) {
|
|
956
|
+
const hasBytes = response.bytes instanceof Uint8Array;
|
|
957
|
+
const hasCookies = response.cookies !== null && typeof response.cookies === "object";
|
|
958
|
+
if (hasBytes && hasCookies) return response;
|
|
959
|
+
return {
|
|
960
|
+
status: response.status,
|
|
961
|
+
headers: response.headers,
|
|
962
|
+
body: response.body,
|
|
963
|
+
bytes: hasBytes ? response.bytes : ENCODER.encode(response.body),
|
|
964
|
+
cookies: hasCookies ? response.cookies : NO_COOKIES,
|
|
965
|
+
json: () => response.json()
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Materialises `providers`/`overrides` and maps string keys that are not tokens of the metadata to
|
|
970
|
+
* the token they designate (alias or unique display name: `'UserRepository'`), like `TestBed` does.
|
|
971
|
+
* Unknown strings are kept (tokens registered at runtime by plugins); classes are resolved by the
|
|
972
|
+
* container.
|
|
973
|
+
*/
|
|
974
|
+
function resolveKeys(values, index) {
|
|
975
|
+
if (values === void 0) return void 0;
|
|
976
|
+
const known = index.knownTokens();
|
|
977
|
+
return entriesOf(values).map(([key, value]) => {
|
|
978
|
+
if (typeof key !== "string" || known.has(key)) return [key, value];
|
|
979
|
+
return [index.find(key, known) ?? key, value];
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function initOptions(options, index, overrides) {
|
|
983
|
+
const only = options.only;
|
|
984
|
+
if (only === "all") return {};
|
|
985
|
+
let roots;
|
|
986
|
+
if (only === void 0) {
|
|
987
|
+
const metadata = options.metadata;
|
|
988
|
+
roots = [
|
|
989
|
+
...readHttpRoots(metadata),
|
|
990
|
+
...readMessagingRoots(metadata),
|
|
991
|
+
...readScheduleRoots(metadata)
|
|
992
|
+
].filter((token) => index.isProvider(token));
|
|
993
|
+
if (roots.length === 0) return {};
|
|
994
|
+
} else {
|
|
995
|
+
if (!Array.isArray(only)) throw new TypeError("[znight] TestApp.create(): only must be 'all' or an array of keys");
|
|
996
|
+
roots = only;
|
|
997
|
+
}
|
|
998
|
+
const extra = (overrides ?? []).map(([key]) => key);
|
|
999
|
+
const plugins = pluginRoots(options.plugins ?? [], { metadata: options.metadata }, index);
|
|
1000
|
+
return { only: unique([
|
|
1001
|
+
...roots,
|
|
1002
|
+
...plugins,
|
|
1003
|
+
...extra
|
|
1004
|
+
]) };
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Providers the plugins need at boot (`Plugin.initRoots()`, {@link PluginInitRoots}), as provider
|
|
1008
|
+
* tokens of the metadata. Other keys (classes without provider, unknown or ambiguous names) are
|
|
1009
|
+
* left to the plugin, which reports them with its own message.
|
|
1010
|
+
*/
|
|
1011
|
+
function pluginRoots(plugins, context, index) {
|
|
1012
|
+
const providers = new Set(index.providerTokens());
|
|
1013
|
+
const roots = [];
|
|
1014
|
+
const frozen = Object.freeze({ metadata: context.metadata });
|
|
1015
|
+
for (const plugin of plugins) {
|
|
1016
|
+
const initRoots = plugin?.initRoots;
|
|
1017
|
+
if (typeof initRoots !== "function") continue;
|
|
1018
|
+
const keys = initRoots.call(plugin, frozen);
|
|
1019
|
+
if (!Array.isArray(keys)) throw new TypeError(`[znight] TestApp.create(): initRoots() of the plugin "${String(plugin.name)}" must return an array of classes or tokens`);
|
|
1020
|
+
for (const key of keys) {
|
|
1021
|
+
const token = findProvider(key, providers, index);
|
|
1022
|
+
if (token !== void 0) roots.push(token);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return roots;
|
|
1026
|
+
}
|
|
1027
|
+
function findProvider(key, providers, index) {
|
|
1028
|
+
try {
|
|
1029
|
+
return index.find(key, providers);
|
|
1030
|
+
} catch {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const ONLY_HINT = "With TestApp: list it in TestApp.create({ only: [...] }) (or pass only: 'all') to create it at boot";
|
|
1035
|
+
/** Hint added to start failures caused by a port already in use (`EADDRINUSE`). */
|
|
1036
|
+
const PORT_IN_USE_HINT = "With TestApp, servers need no fixed port (inject() sends requests in memory): configure HttpPlugin with { port: 0 } and ObservabilityPlugin with { metrics: { server: false } } (or server: { port: 0 }) in tests";
|
|
1037
|
+
/**
|
|
1038
|
+
* Adds TestApp hints to a failed `TestApp.create()`, along the `cause` chain:
|
|
1039
|
+
* - the TestApp form of the container hint to "cannot create on demand: its factory is async"
|
|
1040
|
+
* errors (ZNIGHT015, hint `init({ only })`) of a restricted boot, which `TestApp` exposes as its
|
|
1041
|
+
* `only` option;
|
|
1042
|
+
* - when a server could not listen because its port is taken (`EADDRINUSE`: a fixed port, such as
|
|
1043
|
+
* the default `9464` of the observability metrics server, shared by test files running in
|
|
1044
|
+
* parallel or by `znight dev`), the options giving it a free port or none, on the outermost error
|
|
1045
|
+
* carrying hints (the `ZnightError` the application wraps plugin failures in).
|
|
1046
|
+
*/
|
|
1047
|
+
function addHints(error, restricted) {
|
|
1048
|
+
const chain = [];
|
|
1049
|
+
let current = error;
|
|
1050
|
+
while (current !== null && typeof current === "object" && !chain.includes(current)) {
|
|
1051
|
+
chain.push(current);
|
|
1052
|
+
current = current.cause;
|
|
1053
|
+
}
|
|
1054
|
+
if (restricted) {
|
|
1055
|
+
for (const item of chain) if (item.code === ErrorCode.InstantiationFailed && Array.isArray(item.hints) && item.hints.some((hint) => typeof hint === "string" && hint.includes("init({ only })"))) addHint(item, ONLY_HINT);
|
|
1056
|
+
}
|
|
1057
|
+
if (chain.some((item) => item.code === "EADDRINUSE")) {
|
|
1058
|
+
const outermost = chain.find((item) => Array.isArray(item.hints));
|
|
1059
|
+
if (outermost !== void 0) addHint(outermost, PORT_IN_USE_HINT);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function addHint(error, hint) {
|
|
1063
|
+
const hints = error.hints;
|
|
1064
|
+
if (!hints.includes(hint)) Reflect.set(error, "hints", [...hints, hint]);
|
|
1065
|
+
}
|
|
1066
|
+
function unique(values) {
|
|
1067
|
+
return [...new Set(values)];
|
|
1068
|
+
}
|
|
1069
|
+
// src/config.ts
|
|
1070
|
+
/**
|
|
1071
|
+
* Resolves {@link ConfigValues} to per-token values, validating class keys and property names.
|
|
1072
|
+
*
|
|
1073
|
+
* @throws ZnightError ZNIGHT012 for an unknown configuration class or property.
|
|
1074
|
+
*/
|
|
1075
|
+
function normalizeConfigValues(values, index, configs) {
|
|
1076
|
+
const result = /* @__PURE__ */ new Map();
|
|
1077
|
+
if (values === void 0) return result;
|
|
1078
|
+
const universe = new Set(configs.keys());
|
|
1079
|
+
for (const [key, properties] of entriesOf(values)) {
|
|
1080
|
+
const token = index.find(key, universe) ?? findByPrefix(key, configs);
|
|
1081
|
+
if (token === void 0) {
|
|
1082
|
+
const available = [...configs.values()].map(describeConfig);
|
|
1083
|
+
throw new ZnightError(ErrorCode.InvalidConfig, `config: ${describeKey(key)} is not a configuration class`, { hints: [available.length === 0 ? "The metadata declares no configuration class (extensions.config.configs)" : `Configuration classes: ${available.join(", ")}`] });
|
|
1084
|
+
}
|
|
1085
|
+
const info = configs.get(token);
|
|
1086
|
+
if (properties === null || typeof properties !== "object") throw new ZnightError(ErrorCode.InvalidConfig, `config: the values of ${displayName(token)} must be an object`);
|
|
1087
|
+
const known = new Set(info.fields.map((field) => field.property));
|
|
1088
|
+
for (const property of Object.keys(properties)) if (!known.has(property)) throw new ZnightError(ErrorCode.InvalidConfig, `config: ${displayName(token)} has no configuration property "${property}"`, { hints: [`Properties: ${[...known].join(", ") || "(none)"}`] });
|
|
1089
|
+
result.set(token, {
|
|
1090
|
+
...result.get(token),
|
|
1091
|
+
...properties
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Builds a real configuration instance like `@znight/config` without any source would:
|
|
1098
|
+
* `new ConfigClass()` (field initializers run), then the declared `@Default` values (which win over
|
|
1099
|
+
* initializers, as at runtime, converted to the field type, see {@link convertDefault}), then
|
|
1100
|
+
* `values` (as given). Required fields without value stay `undefined`.
|
|
1101
|
+
*/
|
|
1102
|
+
function buildConfigInstance(info, cls, values) {
|
|
1103
|
+
let instance;
|
|
1104
|
+
try {
|
|
1105
|
+
instance = cls === void 0 ? {} : new cls();
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
throw new InstantiationError(info.token, [info.token], error);
|
|
1108
|
+
}
|
|
1109
|
+
for (const field of info.fields) if (values !== void 0 && Object.hasOwn(values, field.property)) instance[field.property] = values[field.property];
|
|
1110
|
+
else if (field.hasDefault) instance[field.property] = convertDefault(field.default, field.type);
|
|
1111
|
+
return instance;
|
|
1112
|
+
}
|
|
1113
|
+
function findByPrefix(key, configs) {
|
|
1114
|
+
if (typeof key !== "string") return void 0;
|
|
1115
|
+
const matches = [...configs.values()].filter((info) => info.prefix === key);
|
|
1116
|
+
return matches.length === 1 ? matches[0]?.token : void 0;
|
|
1117
|
+
}
|
|
1118
|
+
function describeConfig(info) {
|
|
1119
|
+
const name = displayName(info.token);
|
|
1120
|
+
return info.prefix === void 0 ? name : `${name} (prefix "${info.prefix}")`;
|
|
1121
|
+
}
|
|
1122
|
+
const INVALID = Symbol("invalid");
|
|
1123
|
+
/** Strict decimal numbers, as accepted by `@znight/config` (`42`, `-1.5`, `1e3`; not `0x10`). */
|
|
1124
|
+
const DECIMAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
1125
|
+
const TRUE = /* @__PURE__ */ new Set([
|
|
1126
|
+
"true",
|
|
1127
|
+
"1",
|
|
1128
|
+
"yes",
|
|
1129
|
+
"on"
|
|
1130
|
+
]);
|
|
1131
|
+
const FALSE = /* @__PURE__ */ new Set([
|
|
1132
|
+
"false",
|
|
1133
|
+
"0",
|
|
1134
|
+
"no",
|
|
1135
|
+
"off"
|
|
1136
|
+
]);
|
|
1137
|
+
/**
|
|
1138
|
+
* A declared default converted to the field type the way `@znight/config` converts it at runtime
|
|
1139
|
+
* (`loadConfig`), so the instance has the same shape in tests as in the application: `'8080'` → `8080`
|
|
1140
|
+
* (`number`), `'yes'` → `true`, `'a, b'` → `['a', 'b']` (lists), an absolute URL string → a `URL`
|
|
1141
|
+
* (`url`), a JSON string → its value (`json`). `@znight/testing` does not depend on `@znight/config`,
|
|
1142
|
+
* hence this small mirror of its coercion rules. A value that does not convert (the compiler already
|
|
1143
|
+
* rejects invalid defaults) or an unknown type is kept as is (copied).
|
|
1144
|
+
*/
|
|
1145
|
+
function convertDefault(value, type) {
|
|
1146
|
+
const converted = convert(value, type);
|
|
1147
|
+
return converted === INVALID ? copy(value) : converted;
|
|
1148
|
+
}
|
|
1149
|
+
function convert(value, type) {
|
|
1150
|
+
switch (type) {
|
|
1151
|
+
case "string": return toText(value);
|
|
1152
|
+
case "number": return toNumber(value);
|
|
1153
|
+
case "boolean": return toBoolean(value);
|
|
1154
|
+
case "string[]": return toList(value, toText);
|
|
1155
|
+
case "number[]": return toList(value, toNumber);
|
|
1156
|
+
case "url": return toUrl(value);
|
|
1157
|
+
case "json": return toJson(value);
|
|
1158
|
+
default: return INVALID;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function toText(value) {
|
|
1162
|
+
if (typeof value === "string") return value;
|
|
1163
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
1164
|
+
return INVALID;
|
|
1165
|
+
}
|
|
1166
|
+
function toNumber(value) {
|
|
1167
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : INVALID;
|
|
1168
|
+
if (typeof value !== "string" || !DECIMAL.test(value.trim())) return INVALID;
|
|
1169
|
+
const number = Number(value.trim());
|
|
1170
|
+
return Number.isFinite(number) ? number : INVALID;
|
|
1171
|
+
}
|
|
1172
|
+
function toBoolean(value) {
|
|
1173
|
+
if (typeof value === "boolean") return value;
|
|
1174
|
+
if (value === 1 || value === 0) return value === 1;
|
|
1175
|
+
if (typeof value !== "string") return INVALID;
|
|
1176
|
+
const text = value.trim().toLowerCase();
|
|
1177
|
+
if (TRUE.has(text)) return true;
|
|
1178
|
+
return FALSE.has(text) ? false : INVALID;
|
|
1179
|
+
}
|
|
1180
|
+
function toList(value, item) {
|
|
1181
|
+
let items;
|
|
1182
|
+
if (Array.isArray(value)) items = value;
|
|
1183
|
+
else if (typeof value === "string") items = value.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
1184
|
+
else return INVALID;
|
|
1185
|
+
const result = [];
|
|
1186
|
+
for (const entry of items) {
|
|
1187
|
+
const converted = item(entry);
|
|
1188
|
+
if (converted === INVALID) return INVALID;
|
|
1189
|
+
result.push(converted);
|
|
1190
|
+
}
|
|
1191
|
+
return result;
|
|
1192
|
+
}
|
|
1193
|
+
function toUrl(value) {
|
|
1194
|
+
if (value instanceof URL) return new URL(value.href);
|
|
1195
|
+
return typeof value === "string" && URL.canParse(value.trim()) ? new URL(value.trim()) : INVALID;
|
|
1196
|
+
}
|
|
1197
|
+
function toJson(value) {
|
|
1198
|
+
if (typeof value !== "string") return copy(value);
|
|
1199
|
+
try {
|
|
1200
|
+
return JSON.parse(value);
|
|
1201
|
+
} catch {
|
|
1202
|
+
return INVALID;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
/** Defaults are static values: copy them so that tests never share mutable defaults. */
|
|
1206
|
+
function copy(value) {
|
|
1207
|
+
if (value === null || typeof value !== "object") return value;
|
|
1208
|
+
try {
|
|
1209
|
+
return structuredClone(value);
|
|
1210
|
+
} catch {
|
|
1211
|
+
return value;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
// src/mock-registry.ts
|
|
1215
|
+
const REASONS = {
|
|
1216
|
+
provided: "its value was passed with `provide`",
|
|
1217
|
+
override: "it was replaced with `overrides`",
|
|
1218
|
+
config: "it is a configuration class (a real instance is injected; set values with `config`)",
|
|
1219
|
+
optional: "it is an optional dependency (`undefined` is injected unless provided)",
|
|
1220
|
+
interceptor: "it is an interceptor (not applied unless `aop: \"real\"`)",
|
|
1221
|
+
real: "it was built for real (aop: \"real\")"
|
|
1222
|
+
};
|
|
1223
|
+
/** Default {@link MockRegistry} implementation (internal). */
|
|
1224
|
+
var TestMocks = class {
|
|
1225
|
+
#mocks;
|
|
1226
|
+
#others;
|
|
1227
|
+
#index;
|
|
1228
|
+
#owner;
|
|
1229
|
+
constructor(mocks, others, index, owner) {
|
|
1230
|
+
this.#mocks = mocks;
|
|
1231
|
+
this.#others = others;
|
|
1232
|
+
this.#index = index;
|
|
1233
|
+
this.#owner = owner;
|
|
1234
|
+
}
|
|
1235
|
+
get(key) {
|
|
1236
|
+
const token = this.#index.find(key, new Set(this.#mocks.keys()));
|
|
1237
|
+
if (token !== void 0) return this.#mocks.get(token);
|
|
1238
|
+
throw this.#notMockedError(key);
|
|
1239
|
+
}
|
|
1240
|
+
has(key) {
|
|
1241
|
+
return this.#index.find(key, new Set(this.#mocks.keys())) !== void 0;
|
|
1242
|
+
}
|
|
1243
|
+
tokens() {
|
|
1244
|
+
return [...this.#mocks.keys()];
|
|
1245
|
+
}
|
|
1246
|
+
clearAll() {
|
|
1247
|
+
for (const mock of this.#mocks.values()) clearMock(mock);
|
|
1248
|
+
}
|
|
1249
|
+
resetAll() {
|
|
1250
|
+
for (const mock of this.#mocks.values()) resetMock(mock);
|
|
1251
|
+
}
|
|
1252
|
+
#notMockedError(key) {
|
|
1253
|
+
const owner = displayName(this.#owner);
|
|
1254
|
+
let other;
|
|
1255
|
+
try {
|
|
1256
|
+
other = this.#index.find(key, new Set(this.#others.keys()));
|
|
1257
|
+
} catch {
|
|
1258
|
+
other = void 0;
|
|
1259
|
+
}
|
|
1260
|
+
if (other !== void 0) {
|
|
1261
|
+
const kind = this.#others.get(other);
|
|
1262
|
+
return new ZnightError(ErrorCode.MissingProvider, `${describeToken(other)} is not mocked: ${REASONS[kind]}`);
|
|
1263
|
+
}
|
|
1264
|
+
const mocked = [...this.#mocks.keys()].map(displayName);
|
|
1265
|
+
return new ZnightError(ErrorCode.MissingProvider, `No mock for ${describeKey(key)}: it is not a direct dependency of ${owner}`, { hints: [mocked.length === 0 ? `${owner} has no auto-mocked dependency` : `Auto-mocked: ${mocked.join(", ")}`, "TestBed only mocks direct dependencies; regenerate the metadata if the constructor changed"] });
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
// src/testbed-builder.ts
|
|
1269
|
+
const AOP_MODES = /* @__PURE__ */ new Set([
|
|
1270
|
+
void 0,
|
|
1271
|
+
"none",
|
|
1272
|
+
"real"
|
|
1273
|
+
]);
|
|
1274
|
+
/**
|
|
1275
|
+
* Builds the target of `TestBed.create()`: resolves keys, injects auto-mocks / provided values /
|
|
1276
|
+
* configuration instances, runs the factory and assembles the result container. Internal.
|
|
1277
|
+
*/
|
|
1278
|
+
var Builder = class {
|
|
1279
|
+
#index;
|
|
1280
|
+
#options;
|
|
1281
|
+
#mockFn;
|
|
1282
|
+
#configs;
|
|
1283
|
+
#configValues;
|
|
1284
|
+
#provided;
|
|
1285
|
+
#overrides;
|
|
1286
|
+
/** Values injected into the target (and into built dependencies), by canonical token. */
|
|
1287
|
+
#values = /* @__PURE__ */ new Map();
|
|
1288
|
+
/** Real interceptors (aop: 'real'), by canonical token. */
|
|
1289
|
+
#interceptors = /* @__PURE__ */ new Map();
|
|
1290
|
+
#kinds = /* @__PURE__ */ new Map();
|
|
1291
|
+
#mocks = /* @__PURE__ */ new Map();
|
|
1292
|
+
/** Providers being built (cycle guard). */
|
|
1293
|
+
#building = /* @__PURE__ */ new Set();
|
|
1294
|
+
#known;
|
|
1295
|
+
constructor(options) {
|
|
1296
|
+
if (options === null || typeof options !== "object") throw new TypeError("[znight] TestBed.create(target, { metadata }) requires options with the generated metadata");
|
|
1297
|
+
if (!AOP_MODES.has(options.aop)) throw new TypeError(`[znight] TestBed.create(): aop must be 'none' or 'real', got ${String(options.aop)}`);
|
|
1298
|
+
this.#options = options;
|
|
1299
|
+
this.#index = new MetadataIndex(options.metadata);
|
|
1300
|
+
this.#mockFn = resolveMockFn(options.mockFn).create;
|
|
1301
|
+
this.#configs = readConfigClasses(options.metadata);
|
|
1302
|
+
this.#configValues = normalizeConfigValues(options.config, this.#index, this.#configs);
|
|
1303
|
+
this.#provided = this.#normalize(options.provide, "provide");
|
|
1304
|
+
this.#overrides = this.#normalize(options.overrides, "overrides");
|
|
1305
|
+
}
|
|
1306
|
+
async build(target) {
|
|
1307
|
+
const token = this.#resolveTarget(target);
|
|
1308
|
+
let definition = this.#index.definition(token);
|
|
1309
|
+
if (this.#overrides.has(token)) {
|
|
1310
|
+
const replacement = this.#overrides.get(token);
|
|
1311
|
+
if (!isProviderDefinition(replacement)) throw new ZnightError(ErrorCode.InvalidDecorator, `overrides: the target ${displayName(token)} can only be replaced by a provider definition { scope, deps, factory }`);
|
|
1312
|
+
definition = replacement;
|
|
1313
|
+
}
|
|
1314
|
+
const instance = await this.#construct(token, definition, [token]);
|
|
1315
|
+
const container = await this.#createContainer(token, definition, instance);
|
|
1316
|
+
const others = new Map(this.#kinds);
|
|
1317
|
+
return {
|
|
1318
|
+
instance,
|
|
1319
|
+
mocks: new TestMocks(this.#mocks, others, this.#index, token),
|
|
1320
|
+
container
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
#resolveTarget(target) {
|
|
1324
|
+
const token = this.#index.find(target, new Set(this.#index.providerTokens()));
|
|
1325
|
+
if (token !== void 0) return token;
|
|
1326
|
+
throw new ZnightError(ErrorCode.MissingProvider, `TestBed: ${describeKey(target)} is not a provider of the metadata`, { hints: ["Check that the class is decorated (@Injectable(), @Controller()...) and exported", "Regenerate the metadata before running tests: `znight generate`, the Bun preload `@znight/compiler/bun` or the Vite plugin"] });
|
|
1327
|
+
}
|
|
1328
|
+
/** Every token a `provide`/`overrides` key may designate (canonical). */
|
|
1329
|
+
#knownTokens() {
|
|
1330
|
+
if (this.#known !== void 0) return this.#known;
|
|
1331
|
+
const index = this.#index;
|
|
1332
|
+
const known = /* @__PURE__ */ new Set();
|
|
1333
|
+
const add = (token) => {
|
|
1334
|
+
if (typeof token === "string") known.add(index.canonical(token));
|
|
1335
|
+
};
|
|
1336
|
+
for (const token of index.providerTokens()) {
|
|
1337
|
+
known.add(token);
|
|
1338
|
+
for (const ref of index.definition(token).deps) for (const dep of tokensOfRef(ref)) add(dep);
|
|
1339
|
+
}
|
|
1340
|
+
for (const external of index.metadata.externals ?? []) add(external?.token);
|
|
1341
|
+
for (const [alias] of index.aliases()) add(alias);
|
|
1342
|
+
this.#known = known;
|
|
1343
|
+
return known;
|
|
1344
|
+
}
|
|
1345
|
+
#normalize(values, option) {
|
|
1346
|
+
const result = /* @__PURE__ */ new Map();
|
|
1347
|
+
if (values === void 0) return result;
|
|
1348
|
+
const known = this.#knownTokens();
|
|
1349
|
+
for (const [key, value] of entriesOf(values)) {
|
|
1350
|
+
const token = this.#index.find(key, known);
|
|
1351
|
+
if (token === void 0) throw new ZnightError(ErrorCode.MissingProvider, `${option}: ${describeKey(key)} matches no provider, external or dependency of the metadata`, { hints: ["Use a class, a token, an alias or a unique display name (e.g. \"UserRepository\")"] });
|
|
1352
|
+
if (result.has(token)) throw new ZnightError(ErrorCode.DuplicateProvider, `${option}: several values for ${describeToken(token)}`);
|
|
1353
|
+
result.set(token, value);
|
|
1354
|
+
}
|
|
1355
|
+
return result;
|
|
1356
|
+
}
|
|
1357
|
+
#isExplicit(token) {
|
|
1358
|
+
const canonical = this.#index.canonical(token);
|
|
1359
|
+
return this.#overrides.has(canonical) || this.#provided.has(canonical);
|
|
1360
|
+
}
|
|
1361
|
+
async #construct(token, definition, path) {
|
|
1362
|
+
if (this.#building.has(token)) throw new ZnightError(ErrorCode.CircularDependency, `Circular dependency: ${formatPath([...path])}`);
|
|
1363
|
+
this.#building.add(token);
|
|
1364
|
+
try {
|
|
1365
|
+
const interceptorDeps = interceptorIndices(definition);
|
|
1366
|
+
const deps = new Array(definition.deps.length);
|
|
1367
|
+
for (let index = 0; index < definition.deps.length; index++) {
|
|
1368
|
+
const ref = definition.deps[index];
|
|
1369
|
+
if (!interceptorDeps.has(index)) deps[index] = await this.#resolve(ref, path);
|
|
1370
|
+
else if (this.#options.aop === "real") deps[index] = await this.#interceptor(ref, path);
|
|
1371
|
+
else for (const dep of tokensOfRef(ref)) this.#record(this.#index.canonical(dep), "interceptor");
|
|
1372
|
+
}
|
|
1373
|
+
let instance;
|
|
1374
|
+
try {
|
|
1375
|
+
instance = definition.factory(deps);
|
|
1376
|
+
if (isThenable(instance)) instance = await instance;
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
throw new InstantiationError(token, path, error, definition.source);
|
|
1379
|
+
}
|
|
1380
|
+
if (this.#options.aop === "real" && definition.interceptors !== void 0) try {
|
|
1381
|
+
applyInterceptors(instance, definition.interceptors, deps, displayName(token));
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
throw new InstantiationError(token, path, error, definition.source);
|
|
1384
|
+
}
|
|
1385
|
+
return instance;
|
|
1386
|
+
} finally {
|
|
1387
|
+
this.#building.delete(token);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
async #resolve(ref, path) {
|
|
1391
|
+
if (typeof ref === "string") return this.#value(ref, path);
|
|
1392
|
+
if (ref !== null && typeof ref === "object") {
|
|
1393
|
+
if ("multi" in ref && Array.isArray(ref.multi)) {
|
|
1394
|
+
const values = [];
|
|
1395
|
+
for (const token of ref.multi) if (this.#isExplicit(token)) values.push(await this.#value(token, path));
|
|
1396
|
+
return values;
|
|
1397
|
+
}
|
|
1398
|
+
if ("lazy" in ref && typeof ref.lazy === "string") {
|
|
1399
|
+
const value = await this.#value(ref.lazy, path);
|
|
1400
|
+
return { get: () => value };
|
|
1401
|
+
}
|
|
1402
|
+
if ("token" in ref && typeof ref.token === "string") {
|
|
1403
|
+
if (this.#isExplicit(ref.token)) return this.#value(ref.token, path);
|
|
1404
|
+
this.#record(this.#index.canonical(ref.token), "optional");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
throw new ZnightError(ErrorCode.InvalidDecorator, `Invalid dependency of ${displayName(path[path.length - 1])}: ${safeJson(ref)}`, { hints: ["Expected a token, { token, optional: true }, { multi: [...] } or { lazy: token }"] });
|
|
1409
|
+
}
|
|
1410
|
+
/** The value injected for `token` (cached: the same dependency always gets the same value). */
|
|
1411
|
+
async #value(token, path) {
|
|
1412
|
+
const canonical = this.#index.canonical(token);
|
|
1413
|
+
if (this.#values.has(canonical)) return this.#values.get(canonical);
|
|
1414
|
+
let value;
|
|
1415
|
+
if (this.#overrides.has(canonical)) {
|
|
1416
|
+
value = await this.#explicit(canonical, this.#overrides.get(canonical), path);
|
|
1417
|
+
this.#record(canonical, "override");
|
|
1418
|
+
} else if (this.#provided.has(canonical)) {
|
|
1419
|
+
value = this.#provided.get(canonical);
|
|
1420
|
+
this.#record(canonical, "provided");
|
|
1421
|
+
} else if (this.#configs.has(canonical)) {
|
|
1422
|
+
value = buildConfigInstance(this.#configs.get(canonical), this.#index.classOf(canonical), this.#configValues.get(canonical));
|
|
1423
|
+
this.#record(canonical, "config");
|
|
1424
|
+
} else value = this.#autoMock(canonical, path);
|
|
1425
|
+
this.#values.set(canonical, value);
|
|
1426
|
+
return value;
|
|
1427
|
+
}
|
|
1428
|
+
/** An override (a value, or a definition built with its dependencies resolved like the target's). */
|
|
1429
|
+
#explicit(token, value, path) {
|
|
1430
|
+
return isProviderDefinition(value) ? this.#construct(token, value, [...path, token]) : value;
|
|
1431
|
+
}
|
|
1432
|
+
#autoMock(token, path) {
|
|
1433
|
+
const definition = this.#index.definition(token);
|
|
1434
|
+
if (definition !== void 0 && definition.class === void 0 && definition.name !== void 0) throw qualifiedValueError(token, definition.name, path);
|
|
1435
|
+
const mock = mockObject(definition?.class, void 0, {
|
|
1436
|
+
name: displayName(token),
|
|
1437
|
+
deep: this.#options.deep === true,
|
|
1438
|
+
mockFn: this.#mockFn
|
|
1439
|
+
});
|
|
1440
|
+
this.#mocks.set(token, mock);
|
|
1441
|
+
this.#kinds.delete(token);
|
|
1442
|
+
return mock;
|
|
1443
|
+
}
|
|
1444
|
+
/** A real interceptor (aop: 'real'): explicit value, or built from its compiled definition. */
|
|
1445
|
+
async #interceptor(ref, path) {
|
|
1446
|
+
if (typeof ref !== "string") return this.#resolve(ref, path);
|
|
1447
|
+
const token = this.#index.canonical(ref);
|
|
1448
|
+
if (this.#interceptors.has(token)) return this.#interceptors.get(token);
|
|
1449
|
+
let value;
|
|
1450
|
+
if (this.#overrides.has(token)) {
|
|
1451
|
+
value = await this.#explicit(token, this.#overrides.get(token), path);
|
|
1452
|
+
this.#record(token, "override");
|
|
1453
|
+
} else if (this.#provided.has(token)) {
|
|
1454
|
+
value = this.#provided.get(token);
|
|
1455
|
+
this.#record(token, "provided");
|
|
1456
|
+
} else {
|
|
1457
|
+
const definition = this.#index.definition(token);
|
|
1458
|
+
if (definition === void 0) {
|
|
1459
|
+
const providedBy = this.#index.external(token)?.providedBy;
|
|
1460
|
+
throw new ZnightError(ErrorCode.MissingProvider, `aop: 'real' needs the interceptor ${describeToken(token)}, supplied at runtime by ${providedBy ?? "a plugin"}`, { hints: [`Pass an instance: provide: [[${JSON.stringify(token)}, interceptor]]`] });
|
|
1461
|
+
}
|
|
1462
|
+
value = await this.#construct(token, definition, [...path, token]);
|
|
1463
|
+
this.#record(token, "real");
|
|
1464
|
+
}
|
|
1465
|
+
this.#interceptors.set(token, value);
|
|
1466
|
+
return value;
|
|
1467
|
+
}
|
|
1468
|
+
#record(token, kind) {
|
|
1469
|
+
if (!this.#mocks.has(token)) this.#kinds.set(token, kind);
|
|
1470
|
+
}
|
|
1471
|
+
async #createContainer(target, definition, instance) {
|
|
1472
|
+
const bound = /* @__PURE__ */ new Map([[target, instance]]);
|
|
1473
|
+
for (const [token, value] of this.#values) if (value !== void 0 && !bound.has(token)) bound.set(token, value);
|
|
1474
|
+
for (const [token, value] of this.#interceptors) if (value !== void 0 && !bound.has(token)) bound.set(token, value);
|
|
1475
|
+
const providers = [...bound.keys()].map((token) => {
|
|
1476
|
+
const cls = token === target ? definition.class ?? this.#index.classOf(token) : this.#index.classOf(token);
|
|
1477
|
+
return [token, {
|
|
1478
|
+
scope: "singleton",
|
|
1479
|
+
deps: [],
|
|
1480
|
+
factory: () => void 0,
|
|
1481
|
+
...cls === void 0 ? {} : { class: cls }
|
|
1482
|
+
}];
|
|
1483
|
+
});
|
|
1484
|
+
const aliases = [];
|
|
1485
|
+
for (const [alias] of this.#index.aliases()) {
|
|
1486
|
+
const canonical = this.#index.canonical(alias);
|
|
1487
|
+
if (bound.has(canonical) && !bound.has(alias)) aliases.push([alias, canonical]);
|
|
1488
|
+
}
|
|
1489
|
+
const metadata = {
|
|
1490
|
+
version: METADATA_VERSION,
|
|
1491
|
+
providers: Object.fromEntries(providers),
|
|
1492
|
+
resolutionOrder: [...bound.keys()],
|
|
1493
|
+
aliases: Object.fromEntries(aliases)
|
|
1494
|
+
};
|
|
1495
|
+
const container = Container.create(metadata);
|
|
1496
|
+
for (const [token, value] of bound) container.provide(token, value);
|
|
1497
|
+
await container.init();
|
|
1498
|
+
return container;
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
/** Indices of `definition.deps` used only by interceptor chains. */
|
|
1502
|
+
function interceptorIndices(definition) {
|
|
1503
|
+
const indices = /* @__PURE__ */ new Set();
|
|
1504
|
+
for (const interception of Object.values(definition.interceptors ?? {})) for (const binding of interception.chain) indices.add(binding.dep);
|
|
1505
|
+
return indices;
|
|
1506
|
+
}
|
|
1507
|
+
function tokensOfRef(ref) {
|
|
1508
|
+
if (typeof ref === "string") return [ref];
|
|
1509
|
+
if (ref === null || typeof ref !== "object") return [];
|
|
1510
|
+
if ("multi" in ref && Array.isArray(ref.multi)) return ref.multi;
|
|
1511
|
+
if ("lazy" in ref && typeof ref.lazy === "string") return [ref.lazy];
|
|
1512
|
+
if ("token" in ref && typeof ref.token === "string") return [ref.token];
|
|
1513
|
+
return [];
|
|
1514
|
+
}
|
|
1515
|
+
function qualifiedValueError(token, name, path) {
|
|
1516
|
+
const owner = displayName(path[path.length - 1]);
|
|
1517
|
+
const key = displayName(token);
|
|
1518
|
+
return new ZnightError(ErrorCode.MissingProvider, `TestBed cannot auto-mock ${describeToken(token)}, the value named "${name}" injected into ${owner}: qualified values may be primitives, provide one`, { hints: [`TestBed.create(${owner}, { metadata, provide: { ${JSON.stringify(key)}: value } })`] });
|
|
1519
|
+
}
|
|
1520
|
+
function safeJson(value) {
|
|
1521
|
+
try {
|
|
1522
|
+
return JSON.stringify(value) ?? String(value);
|
|
1523
|
+
} catch {
|
|
1524
|
+
return String(value);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
// src/testbed.ts
|
|
1528
|
+
/**
|
|
1529
|
+
* Unit-test harness: builds one provider with its real factory and auto-mocks its **direct**
|
|
1530
|
+
* dependencies, by kind:
|
|
1531
|
+
*
|
|
1532
|
+
* | Dependency | Injected |
|
|
1533
|
+
* | -------------------------------------------- | --------------------------------------------------- |
|
|
1534
|
+
* | class / interface / `@Provides` / external | deep auto-mock |
|
|
1535
|
+
* | configuration class (`@Config`) | real instance: defaults + `config` values |
|
|
1536
|
+
* | qualified value (`@Inject('x')`, primitives) | the `provide` value (an error otherwise) |
|
|
1537
|
+
* | optional (`@Optional()`, `T \| undefined`) | `undefined` (or the `provide` value) |
|
|
1538
|
+
* | multi (`T[]`) | `[]` (or the provided members) |
|
|
1539
|
+
* | `Lazy<T>` | `{ get: () => mock }` |
|
|
1540
|
+
* | interceptor (`@Intercept`) | nothing, not applied (unless `aop: 'real'`) |
|
|
1541
|
+
*
|
|
1542
|
+
* ```ts
|
|
1543
|
+
* const { instance, mocks } = await TestBed.create(UserService, { metadata: __ZNIGHT_METADATA__ })
|
|
1544
|
+
* mocks.get(UserRepository).findById.mockResolvedValue({ id: '1' })
|
|
1545
|
+
* await instance.findById('1')
|
|
1546
|
+
* expect(mocks.get(UserRepository).findById).toHaveBeenCalledWith('1')
|
|
1547
|
+
* ```
|
|
1548
|
+
*/
|
|
1549
|
+
var TestBed = class {
|
|
1550
|
+
constructor() {}
|
|
1551
|
+
static create(target, options) {
|
|
1552
|
+
try {
|
|
1553
|
+
return new Builder(options).build(target);
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
return Promise.reject(error);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
export { HTTP_PLUGIN_NAME, TestApp, TestBed, clearMock, createMetadata, createMock, createSpy, defineTestMetadata, detectMockFn, isBuiltinSpy, isMock, override, overrideFactory, resetMock, resolveMockFn, setDefaultMockFn, testToken };
|
|
1560
|
+
|
|
1561
|
+
//# sourceMappingURL=index.mjs.map
|