@zudojs/testing 0.1.0 → 1.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 +63 -15
- package/dist/assertions/deepEqual.core.d.ts +25 -0
- package/dist/assertions/deepEqual.core.js +194 -0
- package/dist/assertions/deepEqual.describe.d.ts +19 -0
- package/dist/assertions/deepEqual.describe.js +59 -0
- package/dist/assertions/errorAssertions.core.d.ts +4 -0
- package/dist/assertions/errorAssertions.core.js +28 -3
- package/dist/assertions/eventAssertions.core.d.ts +12 -0
- package/dist/assertions/eventAssertions.core.js +26 -4
- package/dist/assertions/httpAssertions.core.d.ts +12 -1
- package/dist/assertions/httpAssertions.core.js +28 -6
- package/dist/assertions/index.d.ts +7 -6
- package/dist/assertions/index.js +6 -6
- package/dist/cleanupManager/cleanupManager.core.d.ts +4 -0
- package/dist/cleanupManager/cleanupManager.core.js +24 -7
- package/dist/mocking/mockFn.core.d.ts +10 -1
- package/dist/mocking/mockFn.core.js +48 -39
- package/dist/mocking/spy.core.d.ts +11 -2
- package/dist/mocking/spy.core.js +54 -25
- package/dist/mocking/stub.core.d.ts +9 -6
- package/dist/mocking/stub.core.js +49 -12
- package/dist/spyLogger/index.d.ts +5 -4
- package/dist/spyLogger/index.js +3 -3
- package/dist/spyLogger/spyLogger.context.d.ts +27 -0
- package/dist/spyLogger/spyLogger.context.js +44 -0
- package/dist/spyLogger/spyLogger.core.d.ts +7 -1
- package/dist/spyLogger/spyLogger.core.js +10 -56
- package/dist/spyLogger/spyLogger.recording.d.ts +31 -0
- package/dist/spyLogger/spyLogger.recording.js +100 -0
- package/dist/testApplication/testApplication.core.js +6 -3
- package/dist/testClock/testClock.core.js +24 -2
- package/dist/testContext/testContext.recorder.core.js +3 -3
- package/dist/testEventBus/testEventBus.core.js +5 -2
- package/dist/testMessageBus/testMessageBus.core.js +5 -2
- package/dist/testQueue/testQueue.core.js +3 -1
- package/dist/testSerialization/serializationAssertions.core.js +19 -5
- package/dist/testStorage/testStorageMemory.core.d.ts +12 -4
- package/dist/testStorage/testStorageMemory.core.js +25 -12
- package/package.json +37 -26
- package/dist/.tsbuildinfo +0 -1
- package/dist/assertions/errorAssertions.core.d.ts.map +0 -1
- package/dist/assertions/errorAssertions.core.js.map +0 -1
- package/dist/assertions/eventAssertions.core.d.ts.map +0 -1
- package/dist/assertions/eventAssertions.core.js.map +0 -1
- package/dist/assertions/httpAssertions.core.d.ts.map +0 -1
- package/dist/assertions/httpAssertions.core.js.map +0 -1
- package/dist/assertions/index.d.ts.map +0 -1
- package/dist/assertions/index.js.map +0 -1
- package/dist/cleanupManager/cleanupManager.core.d.ts.map +0 -1
- package/dist/cleanupManager/cleanupManager.core.js.map +0 -1
- package/dist/cleanupManager/index.d.ts.map +0 -1
- package/dist/cleanupManager/index.js.map +0 -1
- package/dist/fixtures/eventFixtures.core.d.ts.map +0 -1
- package/dist/fixtures/eventFixtures.core.js.map +0 -1
- package/dist/fixtures/index.d.ts.map +0 -1
- package/dist/fixtures/index.js.map +0 -1
- package/dist/fixtures/messageFixtures.core.d.ts.map +0 -1
- package/dist/fixtures/messageFixtures.core.js.map +0 -1
- package/dist/httpTesting/httpRequest.core.d.ts.map +0 -1
- package/dist/httpTesting/httpRequest.core.js.map +0 -1
- package/dist/httpTesting/httpResponse.core.d.ts.map +0 -1
- package/dist/httpTesting/httpResponse.core.js.map +0 -1
- package/dist/httpTesting/httpResponse.helpers.d.ts.map +0 -1
- package/dist/httpTesting/httpResponse.helpers.js.map +0 -1
- package/dist/httpTesting/httpResponse.type.d.ts.map +0 -1
- package/dist/httpTesting/httpResponse.type.js.map +0 -1
- package/dist/httpTesting/httpStatusCode.type.d.ts.map +0 -1
- package/dist/httpTesting/httpStatusCode.type.js.map +0 -1
- package/dist/httpTesting/index.d.ts.map +0 -1
- package/dist/httpTesting/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mocking/index.d.ts.map +0 -1
- package/dist/mocking/index.js.map +0 -1
- package/dist/mocking/mockFn.core.d.ts.map +0 -1
- package/dist/mocking/mockFn.core.js.map +0 -1
- package/dist/mocking/spy.core.d.ts.map +0 -1
- package/dist/mocking/spy.core.js.map +0 -1
- package/dist/mocking/stub.core.d.ts.map +0 -1
- package/dist/mocking/stub.core.js.map +0 -1
- package/dist/spyLogger/index.d.ts.map +0 -1
- package/dist/spyLogger/index.js.map +0 -1
- package/dist/spyLogger/spyLogger.core.d.ts.map +0 -1
- package/dist/spyLogger/spyLogger.core.js.map +0 -1
- package/dist/spyLogger/spyLogger.type.d.ts.map +0 -1
- package/dist/spyLogger/spyLogger.type.js.map +0 -1
- package/dist/testApplication/index.d.ts.map +0 -1
- package/dist/testApplication/index.js.map +0 -1
- package/dist/testApplication/testApplication.core.d.ts.map +0 -1
- package/dist/testApplication/testApplication.core.js.map +0 -1
- package/dist/testClock/index.d.ts.map +0 -1
- package/dist/testClock/index.js.map +0 -1
- package/dist/testClock/testClock.core.d.ts.map +0 -1
- package/dist/testClock/testClock.core.js.map +0 -1
- package/dist/testConfig/index.d.ts.map +0 -1
- package/dist/testConfig/index.js.map +0 -1
- package/dist/testConfig/testConfig.core.d.ts.map +0 -1
- package/dist/testConfig/testConfig.core.js.map +0 -1
- package/dist/testContainer/index.d.ts.map +0 -1
- package/dist/testContainer/index.js.map +0 -1
- package/dist/testContainer/testContainer.core.d.ts.map +0 -1
- package/dist/testContainer/testContainer.core.js.map +0 -1
- package/dist/testContext/index.d.ts.map +0 -1
- package/dist/testContext/index.js.map +0 -1
- package/dist/testContext/testContext.core.d.ts.map +0 -1
- package/dist/testContext/testContext.core.js.map +0 -1
- package/dist/testContext/testContext.recorder.core.d.ts.map +0 -1
- package/dist/testContext/testContext.recorder.core.js.map +0 -1
- package/dist/testContext/testContext.recorder.type.d.ts.map +0 -1
- package/dist/testContext/testContext.recorder.type.js.map +0 -1
- package/dist/testEventBus/index.d.ts.map +0 -1
- package/dist/testEventBus/index.js.map +0 -1
- package/dist/testEventBus/testEventBus.core.d.ts.map +0 -1
- package/dist/testEventBus/testEventBus.core.js.map +0 -1
- package/dist/testMessageBus/index.d.ts.map +0 -1
- package/dist/testMessageBus/index.js.map +0 -1
- package/dist/testMessageBus/testMessageBus.core.d.ts.map +0 -1
- package/dist/testMessageBus/testMessageBus.core.js.map +0 -1
- package/dist/testQueue/index.d.ts.map +0 -1
- package/dist/testQueue/index.js.map +0 -1
- package/dist/testQueue/testQueue.core.d.ts.map +0 -1
- package/dist/testQueue/testQueue.core.js.map +0 -1
- package/dist/testSerialization/index.d.ts.map +0 -1
- package/dist/testSerialization/index.js.map +0 -1
- package/dist/testSerialization/serializationAssertions.core.d.ts.map +0 -1
- package/dist/testSerialization/serializationAssertions.core.js.map +0 -1
- package/dist/testStorage/index.d.ts.map +0 -1
- package/dist/testStorage/index.js.map +0 -1
- package/dist/testStorage/testStorageMemory.core.d.ts.map +0 -1
- package/dist/testStorage/testStorageMemory.core.js.map +0 -1
package/dist/assertions/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* HTTP, error, and event assertion utilities.
|
|
2
|
+
* @zudojs/testing — Assertions Barrel
|
|
5
3
|
*/
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
4
|
+
export { assertBadRequest, assertCreated, assertNoContent, assertNotFound, assertOK, assertResponseBody, assertResponseBodyContains, assertResponseHeader, assertResponseStatus, assertServerError, } from "./httpAssertions.core.js";
|
|
5
|
+
export { assertEventNotPublished, assertEventPayload, assertEventPublished, assertEventType, assertMessageDispatched, assertMessageNotDispatched, assertRecordedEventType, } from "./eventAssertions.core.js";
|
|
6
|
+
export { assertErrorCode, assertErrorMetadata, assertErrorType, assertRejects, assertThrows, } from "./errorAssertions.core.js";
|
|
7
|
+
export { deepEqual, findDifference } from "./deepEqual.core.js";
|
|
8
|
+
export { describeValue } from "./deepEqual.describe.js";
|
|
9
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -28,6 +28,10 @@ export interface CleanupManagerOptions {
|
|
|
28
28
|
* timers, database connections, and other resources leaking
|
|
29
29
|
* after tests complete.
|
|
30
30
|
*
|
|
31
|
+
* Every cleanup runs even if an earlier one fails, and `dispose()` then
|
|
32
|
+
* throws an `AggregateError` describing what failed. A leaked connection
|
|
33
|
+
* matters whether or not the other cleanups succeeded.
|
|
34
|
+
*
|
|
31
35
|
* @example
|
|
32
36
|
* ```ts
|
|
33
37
|
* const cleanup = createCleanupManager();
|
|
@@ -15,6 +15,7 @@ export function createCleanupManager(options = {}) {
|
|
|
15
15
|
const entries = [];
|
|
16
16
|
let nextId = 0;
|
|
17
17
|
let disposed = false;
|
|
18
|
+
let inFlight;
|
|
18
19
|
const register = (fn, label = `cleanup-${nextId}`) => {
|
|
19
20
|
if (disposed) {
|
|
20
21
|
throw new Error("Cannot register cleanup after manager has been disposed.");
|
|
@@ -25,11 +26,7 @@ export function createCleanupManager(options = {}) {
|
|
|
25
26
|
fn,
|
|
26
27
|
});
|
|
27
28
|
};
|
|
28
|
-
const
|
|
29
|
-
if (disposed) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
disposed = true;
|
|
29
|
+
const runCleanups = async () => {
|
|
33
30
|
const errors = [];
|
|
34
31
|
const reversed = [...entries].reverse();
|
|
35
32
|
for (const entry of reversed) {
|
|
@@ -41,11 +38,31 @@ export function createCleanupManager(options = {}) {
|
|
|
41
38
|
options.onError?.(error, entry);
|
|
42
39
|
}
|
|
43
40
|
}
|
|
41
|
+
const attempted = reversed.length;
|
|
44
42
|
entries.length = 0;
|
|
45
|
-
if (errors.length > 0
|
|
46
|
-
throw new
|
|
43
|
+
if (errors.length > 0) {
|
|
44
|
+
throw new AggregateError(errors.map((entry) => entry.error), `${errors.length} of ${attempted} cleanup functions failed: ${errors
|
|
45
|
+
.map((entry) => entry.entry.label)
|
|
46
|
+
.join(", ")}`);
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
/**
|
|
50
|
+
* Runs the cleanups once. A second call made while the first is still
|
|
51
|
+
* running shares its promise, so nobody is told "disposed" before the
|
|
52
|
+
* resources are actually released; a call after completion resolves
|
|
53
|
+
* immediately.
|
|
54
|
+
*/
|
|
55
|
+
const dispose = () => {
|
|
56
|
+
if (inFlight)
|
|
57
|
+
return inFlight;
|
|
58
|
+
if (disposed)
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
disposed = true;
|
|
61
|
+
inFlight = runCleanups().finally(() => {
|
|
62
|
+
inFlight = undefined;
|
|
63
|
+
});
|
|
64
|
+
return inFlight;
|
|
65
|
+
};
|
|
49
66
|
return {
|
|
50
67
|
get disposed() {
|
|
51
68
|
return disposed;
|
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
export interface MockFn<TArgs extends readonly unknown[] = unknown[], TResult = unknown> {
|
|
10
10
|
(...args: TArgs): TResult;
|
|
11
11
|
readonly calls: readonly TArgs[];
|
|
12
|
+
/**
|
|
13
|
+
* Result of each call, aligned index-for-index with {@link calls}. A
|
|
14
|
+
* call whose implementation threw occupies its slot with `undefined`;
|
|
15
|
+
* the thrown value is in {@link errors}.
|
|
16
|
+
*/
|
|
12
17
|
readonly results: readonly TResult[];
|
|
18
|
+
/** Values thrown by the implementation, in call order. */
|
|
19
|
+
readonly errors: readonly unknown[];
|
|
13
20
|
readonly invoked: boolean;
|
|
14
21
|
readonly callCount: number;
|
|
15
22
|
mockReturnValue: (value: TResult) => void;
|
|
16
|
-
|
|
23
|
+
/** Configures the mock to return a promise resolving to `value`. */
|
|
24
|
+
mockResolvedValue: (value: Awaited<TResult>) => void;
|
|
25
|
+
/** Configures the mock to return a promise rejecting with `error`. */
|
|
17
26
|
mockRejectedValue: (error: unknown) => void;
|
|
18
27
|
mockImplementation: (fn: (...args: TArgs) => TResult) => void;
|
|
19
28
|
mockReset: () => void;
|
|
@@ -23,30 +23,47 @@
|
|
|
23
23
|
export function createMockFn(defaultReturnValue) {
|
|
24
24
|
const calls = [];
|
|
25
25
|
const results = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
let
|
|
26
|
+
const errors = [];
|
|
27
|
+
const initialMode = arguments.length > 0
|
|
28
|
+
? { kind: "value", value: defaultReturnValue }
|
|
29
|
+
: { kind: "none" };
|
|
30
|
+
let mode = initialMode;
|
|
31
|
+
/**
|
|
32
|
+
* Produce the configured result.
|
|
33
|
+
*
|
|
34
|
+
* `undefined` is a legitimate configured value, so the mode is tracked
|
|
35
|
+
* explicitly rather than inferred from a `!== undefined` check — which
|
|
36
|
+
* silently ignored `mockReturnValue(undefined)`.
|
|
37
|
+
*/
|
|
38
|
+
const produce = (args) => {
|
|
39
|
+
switch (mode.kind) {
|
|
40
|
+
case "value":
|
|
41
|
+
return mode.value;
|
|
42
|
+
case "resolve":
|
|
43
|
+
return Promise.resolve(mode.value);
|
|
44
|
+
case "reject":
|
|
45
|
+
return Promise.reject(mode.error);
|
|
46
|
+
case "implementation":
|
|
47
|
+
return mode.fn(...args);
|
|
48
|
+
case "none":
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
31
52
|
const mock = ((...args) => {
|
|
32
53
|
calls.push(args);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (resolvedValue !== undefined) {
|
|
37
|
-
results.push(resolvedValue);
|
|
38
|
-
return resolvedValue;
|
|
54
|
+
let result;
|
|
55
|
+
try {
|
|
56
|
+
result = produce(args);
|
|
39
57
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Keep `results` aligned with `calls` even when the implementation
|
|
60
|
+
// throws; otherwise every later result shifts one index left.
|
|
61
|
+
results.push(undefined);
|
|
62
|
+
errors.push(error);
|
|
63
|
+
throw error;
|
|
44
64
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return returnValue;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
65
|
+
results.push(result);
|
|
66
|
+
return result;
|
|
50
67
|
});
|
|
51
68
|
Object.defineProperty(mock, "calls", {
|
|
52
69
|
get: () => calls,
|
|
@@ -56,6 +73,10 @@ export function createMockFn(defaultReturnValue) {
|
|
|
56
73
|
get: () => results,
|
|
57
74
|
enumerable: true,
|
|
58
75
|
});
|
|
76
|
+
Object.defineProperty(mock, "errors", {
|
|
77
|
+
get: () => errors,
|
|
78
|
+
enumerable: true,
|
|
79
|
+
});
|
|
59
80
|
Object.defineProperty(mock, "invoked", {
|
|
60
81
|
get: () => calls.length > 0,
|
|
61
82
|
enumerable: true,
|
|
@@ -65,39 +86,27 @@ export function createMockFn(defaultReturnValue) {
|
|
|
65
86
|
enumerable: true,
|
|
66
87
|
});
|
|
67
88
|
mock.mockReturnValue = (value) => {
|
|
68
|
-
|
|
69
|
-
resolvedValue = undefined;
|
|
70
|
-
shouldReject = false;
|
|
89
|
+
mode = { kind: "value", value };
|
|
71
90
|
};
|
|
72
91
|
mock.mockResolvedValue = (value) => {
|
|
73
|
-
|
|
74
|
-
returnValue = undefined;
|
|
75
|
-
shouldReject = false;
|
|
92
|
+
mode = { kind: "resolve", value };
|
|
76
93
|
};
|
|
77
94
|
mock.mockRejectedValue = (error) => {
|
|
78
|
-
|
|
79
|
-
shouldReject = true;
|
|
80
|
-
returnValue = undefined;
|
|
81
|
-
resolvedValue = undefined;
|
|
95
|
+
mode = { kind: "reject", error };
|
|
82
96
|
};
|
|
83
97
|
mock.mockImplementation = (fn) => {
|
|
84
|
-
|
|
85
|
-
returnValue = undefined;
|
|
86
|
-
resolvedValue = undefined;
|
|
87
|
-
shouldReject = false;
|
|
98
|
+
mode = { kind: "implementation", fn: fn };
|
|
88
99
|
};
|
|
89
100
|
mock.mockReset = () => {
|
|
90
101
|
calls.length = 0;
|
|
91
102
|
results.length = 0;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
resolvedValue = undefined;
|
|
95
|
-
rejectedValue = undefined;
|
|
96
|
-
shouldReject = false;
|
|
103
|
+
errors.length = 0;
|
|
104
|
+
mode = initialMode;
|
|
97
105
|
};
|
|
98
106
|
mock.mockClear = () => {
|
|
99
107
|
calls.length = 0;
|
|
100
108
|
results.length = 0;
|
|
109
|
+
errors.length = 0;
|
|
101
110
|
};
|
|
102
111
|
return mock;
|
|
103
112
|
}
|
|
@@ -11,12 +11,18 @@ export interface SpyFn<TArgs extends readonly unknown[] = unknown[], TResult = u
|
|
|
11
11
|
readonly original: (...args: TArgs) => TResult;
|
|
12
12
|
readonly calls: readonly TArgs[];
|
|
13
13
|
readonly results: readonly TResult[];
|
|
14
|
+
/** Errors thrown by the wrapped function, in call order. */
|
|
15
|
+
readonly errors: readonly unknown[];
|
|
14
16
|
readonly callCount: number;
|
|
15
|
-
|
|
17
|
+
/** Clears the recorded calls, results and errors. */
|
|
18
|
+
reset: () => void;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
21
|
* Creates a spy that wraps an existing function.
|
|
19
22
|
*
|
|
23
|
+
* The wrapper forwards its receiver, so a spy taken on a method still works
|
|
24
|
+
* when the method reads instance state.
|
|
25
|
+
*
|
|
20
26
|
* @param fn - The function to spy on.
|
|
21
27
|
* @returns A SpyFn instance.
|
|
22
28
|
*
|
|
@@ -29,7 +35,7 @@ export interface SpyFn<TArgs extends readonly unknown[] = unknown[], TResult = u
|
|
|
29
35
|
* expect(spy.calls).toHaveLength(1);
|
|
30
36
|
* expect(spy.calls[0]).toEqual([1, 2]);
|
|
31
37
|
*
|
|
32
|
-
* spy.
|
|
38
|
+
* spy.reset();
|
|
33
39
|
* ```
|
|
34
40
|
*/
|
|
35
41
|
export declare function createSpyFn<TArgs extends readonly unknown[] = unknown[], TResult = unknown>(fn: (...args: TArgs) => TResult): SpyFn<TArgs, TResult>;
|
|
@@ -40,7 +46,10 @@ export interface SpyMethod<TObj, TMethod extends keyof TObj> {
|
|
|
40
46
|
readonly object: TObj;
|
|
41
47
|
readonly property: TMethod;
|
|
42
48
|
readonly calls: readonly unknown[][];
|
|
49
|
+
readonly results: readonly unknown[];
|
|
50
|
+
readonly errors: readonly unknown[];
|
|
43
51
|
readonly callCount: number;
|
|
52
|
+
/** Reinstates the original method exactly as it was found. */
|
|
44
53
|
restore: () => void;
|
|
45
54
|
}
|
|
46
55
|
/**
|
package/dist/mocking/spy.core.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Creates a spy that wraps an existing function.
|
|
8
8
|
*
|
|
9
|
+
* The wrapper forwards its receiver, so a spy taken on a method still works
|
|
10
|
+
* when the method reads instance state.
|
|
11
|
+
*
|
|
9
12
|
* @param fn - The function to spy on.
|
|
10
13
|
* @returns A SpyFn instance.
|
|
11
14
|
*
|
|
@@ -18,38 +21,42 @@
|
|
|
18
21
|
* expect(spy.calls).toHaveLength(1);
|
|
19
22
|
* expect(spy.calls[0]).toEqual([1, 2]);
|
|
20
23
|
*
|
|
21
|
-
* spy.
|
|
24
|
+
* spy.reset();
|
|
22
25
|
* ```
|
|
23
26
|
*/
|
|
24
27
|
export function createSpyFn(fn) {
|
|
25
28
|
const calls = [];
|
|
26
29
|
const results = [];
|
|
27
|
-
const
|
|
30
|
+
const errors = [];
|
|
31
|
+
const spy = function (...args) {
|
|
28
32
|
calls.push(args);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
try {
|
|
34
|
+
const result = Reflect.apply(fn, this, args);
|
|
35
|
+
results.push(result);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
errors.push(error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
33
43
|
Object.defineProperty(spy, "original", {
|
|
34
44
|
value: fn,
|
|
35
45
|
writable: false,
|
|
36
46
|
enumerable: true,
|
|
37
47
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
enumerable: true
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
get: () => calls.length,
|
|
48
|
-
enumerable: true,
|
|
49
|
-
});
|
|
50
|
-
spy.restore = () => {
|
|
48
|
+
for (const [name, get] of [
|
|
49
|
+
["calls", () => calls],
|
|
50
|
+
["results", () => results],
|
|
51
|
+
["errors", () => errors],
|
|
52
|
+
["callCount", () => calls.length],
|
|
53
|
+
]) {
|
|
54
|
+
Object.defineProperty(spy, name, { get, enumerable: true });
|
|
55
|
+
}
|
|
56
|
+
spy.reset = () => {
|
|
51
57
|
calls.length = 0;
|
|
52
58
|
results.length = 0;
|
|
59
|
+
errors.length = 0;
|
|
53
60
|
};
|
|
54
61
|
return spy;
|
|
55
62
|
}
|
|
@@ -74,26 +81,48 @@ export function createSpyFn(fn) {
|
|
|
74
81
|
*/
|
|
75
82
|
export function createSpyMethod(object, property) {
|
|
76
83
|
const original = object[property];
|
|
77
|
-
const calls = [];
|
|
78
84
|
if (typeof original !== "function") {
|
|
79
85
|
throw new TypeError(`Property "${String(property)}" is not a function.`);
|
|
80
86
|
}
|
|
87
|
+
const key = property;
|
|
88
|
+
const target = object;
|
|
89
|
+
// Whether the method was the object's own or inherited from a prototype.
|
|
90
|
+
// Reassigning an inherited method and then "restoring" it by assignment
|
|
91
|
+
// leaves a permanent own property that changes enumeration and hasOwn.
|
|
92
|
+
const wasOwnProperty = Object.hasOwn(target, key);
|
|
81
93
|
const originalFn = original;
|
|
82
|
-
|
|
94
|
+
const calls = [];
|
|
95
|
+
const results = [];
|
|
96
|
+
const errors = [];
|
|
97
|
+
target[key] = function (...args) {
|
|
83
98
|
calls.push(args);
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
try {
|
|
100
|
+
const result = originalFn.apply(this ?? object, args);
|
|
101
|
+
results.push(result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
errors.push(error);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
86
109
|
return {
|
|
87
110
|
object,
|
|
88
111
|
property,
|
|
89
112
|
calls,
|
|
113
|
+
results,
|
|
114
|
+
errors,
|
|
90
115
|
get callCount() {
|
|
91
116
|
return calls.length;
|
|
92
117
|
},
|
|
93
118
|
restore: () => {
|
|
94
|
-
|
|
95
|
-
original;
|
|
119
|
+
if (wasOwnProperty)
|
|
120
|
+
target[key] = original;
|
|
121
|
+
else
|
|
122
|
+
delete target[key];
|
|
96
123
|
calls.length = 0;
|
|
124
|
+
results.length = 0;
|
|
125
|
+
errors.length = 0;
|
|
97
126
|
},
|
|
98
127
|
};
|
|
99
128
|
}
|
|
@@ -25,32 +25,35 @@
|
|
|
25
25
|
* });
|
|
26
26
|
*
|
|
27
27
|
* expect(await stub.find("123")).toEqual({ id: "123", name: "Test" });
|
|
28
|
-
* expect(stub.create).toBeUndefined();
|
|
29
28
|
* ```
|
|
30
29
|
*/
|
|
31
|
-
export declare function createStub<T extends
|
|
30
|
+
export declare function createStub<T extends object>(overrides?: Partial<T>): T;
|
|
32
31
|
/**
|
|
33
32
|
* Creates a stub class constructor.
|
|
34
33
|
*
|
|
34
|
+
* Instances keep the original prototype, so `instanceof` holds and methods
|
|
35
|
+
* that were not overridden exist as no-ops rather than being absent.
|
|
36
|
+
*
|
|
35
37
|
* @typeParam T - The class type to stub.
|
|
38
|
+
* @param OriginalClass - The class being stubbed.
|
|
36
39
|
* @param overrides - Optional property and method implementations.
|
|
37
40
|
* @returns A stub class constructor.
|
|
38
41
|
*
|
|
39
42
|
* @example
|
|
40
43
|
* ```ts
|
|
41
44
|
* class RealDatabase {
|
|
42
|
-
* async connect(): Promise<void> {
|
|
43
|
-
* async query(sql: string): Promise<unknown[]> {
|
|
45
|
+
* async connect(): Promise<void> {}
|
|
46
|
+
* async query(sql: string): Promise<unknown[]> { return []; }
|
|
44
47
|
* }
|
|
45
48
|
*
|
|
46
49
|
* const StubDatabase = createStubClass(RealDatabase, {
|
|
47
|
-
* connect: async () => {},
|
|
48
50
|
* query: async () => [],
|
|
49
51
|
* });
|
|
50
52
|
*
|
|
51
53
|
* const db = new StubDatabase();
|
|
54
|
+
* expect(db).toBeInstanceOf(RealDatabase);
|
|
52
55
|
* await db.connect();
|
|
53
56
|
* ```
|
|
54
57
|
*/
|
|
55
|
-
export declare function createStubClass<T>(
|
|
58
|
+
export declare function createStubClass<T extends object>(OriginalClass: new (...args: never[]) => T, overrides?: Partial<T>): new () => T;
|
|
56
59
|
//# sourceMappingURL=stub.core.d.ts.map
|
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Creates fake implementations of interfaces for testing.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Properties a stub must never answer with a function.
|
|
8
|
+
*
|
|
9
|
+
* `then` is the important one: answering it makes the stub a thenable, so
|
|
10
|
+
* `await stub` calls it expecting a promise callback. The fake never resolves,
|
|
11
|
+
* and the test hangs until it times out with no indication why.
|
|
12
|
+
*/
|
|
13
|
+
const NON_CALLABLE_KEYS = new Set([
|
|
14
|
+
"then",
|
|
15
|
+
"catch",
|
|
16
|
+
"finally",
|
|
17
|
+
Symbol.toPrimitive,
|
|
18
|
+
Symbol.iterator,
|
|
19
|
+
Symbol.asyncIterator,
|
|
20
|
+
Symbol.toStringTag,
|
|
21
|
+
]);
|
|
6
22
|
/**
|
|
7
23
|
* Creates a stub object from an interface.
|
|
8
24
|
*
|
|
@@ -25,53 +41,74 @@
|
|
|
25
41
|
* });
|
|
26
42
|
*
|
|
27
43
|
* expect(await stub.find("123")).toEqual({ id: "123", name: "Test" });
|
|
28
|
-
* expect(stub.create).toBeUndefined();
|
|
29
44
|
* ```
|
|
30
45
|
*/
|
|
31
46
|
export function createStub(overrides = {}) {
|
|
32
47
|
return new Proxy({}, {
|
|
33
48
|
get(_target, prop, _receiver) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
// `hasOwn`, not `in`: `in` walks the prototype chain, so `toString`,
|
|
50
|
+
// `constructor` and `valueOf` resolved to undefined instead of stubs.
|
|
51
|
+
if (Object.hasOwn(overrides, prop)) {
|
|
52
|
+
return overrides[prop];
|
|
37
53
|
}
|
|
38
|
-
if (
|
|
54
|
+
if (NON_CALLABLE_KEYS.has(prop))
|
|
55
|
+
return undefined;
|
|
56
|
+
if (typeof prop === "symbol")
|
|
39
57
|
return undefined;
|
|
40
|
-
}
|
|
41
58
|
return (..._args) => undefined;
|
|
42
59
|
},
|
|
60
|
+
has(_target, prop) {
|
|
61
|
+
return !NON_CALLABLE_KEYS.has(prop);
|
|
62
|
+
},
|
|
43
63
|
});
|
|
44
64
|
}
|
|
45
65
|
/**
|
|
46
66
|
* Creates a stub class constructor.
|
|
47
67
|
*
|
|
68
|
+
* Instances keep the original prototype, so `instanceof` holds and methods
|
|
69
|
+
* that were not overridden exist as no-ops rather than being absent.
|
|
70
|
+
*
|
|
48
71
|
* @typeParam T - The class type to stub.
|
|
72
|
+
* @param OriginalClass - The class being stubbed.
|
|
49
73
|
* @param overrides - Optional property and method implementations.
|
|
50
74
|
* @returns A stub class constructor.
|
|
51
75
|
*
|
|
52
76
|
* @example
|
|
53
77
|
* ```ts
|
|
54
78
|
* class RealDatabase {
|
|
55
|
-
* async connect(): Promise<void> {
|
|
56
|
-
* async query(sql: string): Promise<unknown[]> {
|
|
79
|
+
* async connect(): Promise<void> {}
|
|
80
|
+
* async query(sql: string): Promise<unknown[]> { return []; }
|
|
57
81
|
* }
|
|
58
82
|
*
|
|
59
83
|
* const StubDatabase = createStubClass(RealDatabase, {
|
|
60
|
-
* connect: async () => {},
|
|
61
84
|
* query: async () => [],
|
|
62
85
|
* });
|
|
63
86
|
*
|
|
64
87
|
* const db = new StubDatabase();
|
|
88
|
+
* expect(db).toBeInstanceOf(RealDatabase);
|
|
65
89
|
* await db.connect();
|
|
66
90
|
* ```
|
|
67
91
|
*/
|
|
68
|
-
export function createStubClass(
|
|
69
|
-
|
|
92
|
+
export function createStubClass(OriginalClass, overrides = {}) {
|
|
93
|
+
const prototype = OriginalClass.prototype;
|
|
94
|
+
const methodNames = Object.getOwnPropertyNames(prototype).filter((name) => {
|
|
95
|
+
if (name === "constructor")
|
|
96
|
+
return false;
|
|
97
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
98
|
+
return typeof descriptor?.value === "function";
|
|
99
|
+
});
|
|
100
|
+
class Stub {
|
|
70
101
|
constructor() {
|
|
102
|
+
for (const name of methodNames) {
|
|
103
|
+
this[name] = () => undefined;
|
|
104
|
+
}
|
|
71
105
|
for (const [key, value] of Object.entries(overrides)) {
|
|
72
106
|
this[key] = value;
|
|
73
107
|
}
|
|
74
108
|
}
|
|
75
|
-
}
|
|
109
|
+
}
|
|
110
|
+
Object.setPrototypeOf(Stub.prototype, prototype);
|
|
111
|
+
Object.setPrototypeOf(Stub, OriginalClass);
|
|
112
|
+
return Stub;
|
|
76
113
|
}
|
|
77
114
|
//# sourceMappingURL=stub.core.js.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zudojs/testing — Spy
|
|
3
|
-
*
|
|
4
|
-
* Records all log calls for assertion.
|
|
2
|
+
* @zudojs/testing — Spy Logger Barrel
|
|
5
3
|
*/
|
|
6
4
|
export { createSpyLogger } from "./spyLogger.core.js";
|
|
7
|
-
export
|
|
5
|
+
export { LEVELS, createRecordingLogger } from "./spyLogger.recording.js";
|
|
6
|
+
export type { DerivedOptions, Recorder } from "./spyLogger.recording.js";
|
|
7
|
+
export { deepMatches, mergeContext, mergeLoggerContext, } from "./spyLogger.context.js";
|
|
8
|
+
export type * from "./spyLogger.type.js";
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/spyLogger/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @zudojs/testing — Spy
|
|
3
|
-
*
|
|
4
|
-
* Records all log calls for assertion.
|
|
2
|
+
* @zudojs/testing — Spy Logger Barrel
|
|
5
3
|
*/
|
|
6
4
|
export { createSpyLogger } from "./spyLogger.core.js";
|
|
5
|
+
export { LEVELS, createRecordingLogger } from "./spyLogger.recording.js";
|
|
6
|
+
export { deepMatches, mergeContext, mergeLoggerContext, } from "./spyLogger.context.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — Logger context handling for the spy logger.
|
|
3
|
+
*
|
|
4
|
+
* @module spyLogger/spyLogger.context
|
|
5
|
+
*/
|
|
6
|
+
import type { LogMetadata, LoggerContext } from "@zudojs/logger";
|
|
7
|
+
/** Merges two logger contexts, preserving the nested shape. */
|
|
8
|
+
export declare function mergeLoggerContext(base: LoggerContext | undefined, next: LoggerContext): LoggerContext;
|
|
9
|
+
/**
|
|
10
|
+
* Flattens a logger context onto a call's metadata.
|
|
11
|
+
*
|
|
12
|
+
* `LoggerContext` nests identifiers and metadata, so it is flattened rather
|
|
13
|
+
* than spread wholesale: a test asserting on `tenantId` should find it at the
|
|
14
|
+
* top level, the way a real transport would render it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeContext(context: LoggerContext | undefined, metadata: LogMetadata | undefined): LogMetadata | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Structural comparison, so object metadata can actually be matched.
|
|
19
|
+
*
|
|
20
|
+
* Uses the same walker as the assertions. The previous
|
|
21
|
+
* `JSON.stringify` comparison matched any two `Map`s or `Set`s (both
|
|
22
|
+
* render as `{}`), dropped `undefined` values, and treated key order as
|
|
23
|
+
* significant, so `findByMetadata` could both match what it should not
|
|
24
|
+
* and miss what it should find.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deepMatches(actual: unknown, expected: unknown): boolean;
|
|
27
|
+
//# sourceMappingURL=spyLogger.context.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — Logger context handling for the spy logger.
|
|
3
|
+
*
|
|
4
|
+
* @module spyLogger/spyLogger.context
|
|
5
|
+
*/
|
|
6
|
+
import { deepEqual } from "../assertions/deepEqual.core.js";
|
|
7
|
+
/** Merges two logger contexts, preserving the nested shape. */
|
|
8
|
+
export function mergeLoggerContext(base, next) {
|
|
9
|
+
if (!base)
|
|
10
|
+
return next;
|
|
11
|
+
return {
|
|
12
|
+
identifiers: { ...base.identifiers, ...next.identifiers },
|
|
13
|
+
metadata: { ...base.metadata, ...next.metadata },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Flattens a logger context onto a call's metadata.
|
|
18
|
+
*
|
|
19
|
+
* `LoggerContext` nests identifiers and metadata, so it is flattened rather
|
|
20
|
+
* than spread wholesale: a test asserting on `tenantId` should find it at the
|
|
21
|
+
* top level, the way a real transport would render it.
|
|
22
|
+
*/
|
|
23
|
+
export function mergeContext(context, metadata) {
|
|
24
|
+
if (!context)
|
|
25
|
+
return metadata;
|
|
26
|
+
return {
|
|
27
|
+
...context.identifiers,
|
|
28
|
+
...context.metadata,
|
|
29
|
+
...metadata,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Structural comparison, so object metadata can actually be matched.
|
|
34
|
+
*
|
|
35
|
+
* Uses the same walker as the assertions. The previous
|
|
36
|
+
* `JSON.stringify` comparison matched any two `Map`s or `Set`s (both
|
|
37
|
+
* render as `{}`), dropped `undefined` values, and treated key order as
|
|
38
|
+
* significant, so `findByMetadata` could both match what it should not
|
|
39
|
+
* and miss what it should find.
|
|
40
|
+
*/
|
|
41
|
+
export function deepMatches(actual, expected) {
|
|
42
|
+
return deepEqual(actual, expected);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=spyLogger.context.js.map
|
|
@@ -8,8 +8,14 @@ import type { SpyLogger } from "./spyLogger.type.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* Creates a spy logger that records all log calls.
|
|
10
10
|
*
|
|
11
|
+
* A logger derived with `child()` or `withContext()` writes into the *same*
|
|
12
|
+
* recording as its parent, the way a real logger shares a transport. Giving
|
|
13
|
+
* the derived logger its own array meant code under test that called
|
|
14
|
+
* `logger.child({ module })` — the normal pattern — logged into an array
|
|
15
|
+
* nobody held, so assertions on the parent silently saw nothing.
|
|
16
|
+
*
|
|
11
17
|
* @param name - Logger name.
|
|
12
|
-
* @param level -
|
|
18
|
+
* @param level - Minimum level recorded. Defaults to TRACE (everything).
|
|
13
19
|
* @returns A SpyLogger instance.
|
|
14
20
|
*/
|
|
15
21
|
export declare function createSpyLogger(name?: string, level?: LoggerLevel): SpyLogger;
|