@zudojs/testing 0.0.1 → 1.0.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 +57 -15
- package/dist/assertions/deepEqual.core.d.ts +25 -0
- package/dist/assertions/deepEqual.core.js +164 -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 +13 -2
- 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 +5 -2
- package/dist/mocking/mockFn.core.d.ts +4 -1
- package/dist/mocking/mockFn.core.js +33 -41
- 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 +19 -0
- package/dist/spyLogger/spyLogger.context.js +41 -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 +89 -0
- package/dist/testApplication/testApplication.core.js +6 -3
- package/dist/testClock/testClock.core.js +19 -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 +13 -4
- package/dist/testStorage/testStorageMemory.core.d.ts +12 -4
- package/dist/testStorage/testStorageMemory.core.js +25 -12
- package/package.json +33 -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
|
@@ -23,30 +23,36 @@
|
|
|
23
23
|
export function createMockFn(defaultReturnValue) {
|
|
24
24
|
const calls = [];
|
|
25
25
|
const results = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let
|
|
30
|
-
|
|
26
|
+
const initialMode = arguments.length > 0
|
|
27
|
+
? { kind: "value", value: defaultReturnValue }
|
|
28
|
+
: { kind: "none" };
|
|
29
|
+
let mode = initialMode;
|
|
30
|
+
/**
|
|
31
|
+
* Produce the configured result.
|
|
32
|
+
*
|
|
33
|
+
* `undefined` is a legitimate configured value, so the mode is tracked
|
|
34
|
+
* explicitly rather than inferred from a `!== undefined` check — which
|
|
35
|
+
* silently ignored `mockReturnValue(undefined)`.
|
|
36
|
+
*/
|
|
37
|
+
const produce = (args) => {
|
|
38
|
+
switch (mode.kind) {
|
|
39
|
+
case "value":
|
|
40
|
+
return mode.value;
|
|
41
|
+
case "resolve":
|
|
42
|
+
return Promise.resolve(mode.value);
|
|
43
|
+
case "reject":
|
|
44
|
+
return Promise.reject(mode.error);
|
|
45
|
+
case "implementation":
|
|
46
|
+
return mode.fn(...args);
|
|
47
|
+
case "none":
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
31
51
|
const mock = ((...args) => {
|
|
32
52
|
calls.push(args);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (resolvedValue !== undefined) {
|
|
37
|
-
results.push(resolvedValue);
|
|
38
|
-
return resolvedValue;
|
|
39
|
-
}
|
|
40
|
-
if (implementation) {
|
|
41
|
-
const result = implementation(...args);
|
|
42
|
-
results.push(result);
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
if (returnValue !== undefined) {
|
|
46
|
-
results.push(returnValue);
|
|
47
|
-
return returnValue;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
53
|
+
const result = produce(args);
|
|
54
|
+
results.push(result);
|
|
55
|
+
return result;
|
|
50
56
|
});
|
|
51
57
|
Object.defineProperty(mock, "calls", {
|
|
52
58
|
get: () => calls,
|
|
@@ -65,35 +71,21 @@ export function createMockFn(defaultReturnValue) {
|
|
|
65
71
|
enumerable: true,
|
|
66
72
|
});
|
|
67
73
|
mock.mockReturnValue = (value) => {
|
|
68
|
-
|
|
69
|
-
resolvedValue = undefined;
|
|
70
|
-
shouldReject = false;
|
|
74
|
+
mode = { kind: "value", value };
|
|
71
75
|
};
|
|
72
76
|
mock.mockResolvedValue = (value) => {
|
|
73
|
-
|
|
74
|
-
returnValue = undefined;
|
|
75
|
-
shouldReject = false;
|
|
77
|
+
mode = { kind: "resolve", value };
|
|
76
78
|
};
|
|
77
79
|
mock.mockRejectedValue = (error) => {
|
|
78
|
-
|
|
79
|
-
shouldReject = true;
|
|
80
|
-
returnValue = undefined;
|
|
81
|
-
resolvedValue = undefined;
|
|
80
|
+
mode = { kind: "reject", error };
|
|
82
81
|
};
|
|
83
82
|
mock.mockImplementation = (fn) => {
|
|
84
|
-
|
|
85
|
-
returnValue = undefined;
|
|
86
|
-
resolvedValue = undefined;
|
|
87
|
-
shouldReject = false;
|
|
83
|
+
mode = { kind: "implementation", fn: fn };
|
|
88
84
|
};
|
|
89
85
|
mock.mockReset = () => {
|
|
90
86
|
calls.length = 0;
|
|
91
87
|
results.length = 0;
|
|
92
|
-
|
|
93
|
-
returnValue = defaultReturnValue;
|
|
94
|
-
resolvedValue = undefined;
|
|
95
|
-
rejectedValue = undefined;
|
|
96
|
-
shouldReject = false;
|
|
88
|
+
mode = initialMode;
|
|
97
89
|
};
|
|
98
90
|
mock.mockClear = () => {
|
|
99
91
|
calls.length = 0;
|
|
@@ -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,19 @@
|
|
|
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
|
+
/** Structural comparison, so object metadata can actually be matched. */
|
|
18
|
+
export declare function deepMatches(actual: unknown, expected: unknown): boolean;
|
|
19
|
+
//# sourceMappingURL=spyLogger.context.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — Logger context handling for the spy logger.
|
|
3
|
+
*
|
|
4
|
+
* @module spyLogger/spyLogger.context
|
|
5
|
+
*/
|
|
6
|
+
/** Merges two logger contexts, preserving the nested shape. */
|
|
7
|
+
export function mergeLoggerContext(base, next) {
|
|
8
|
+
if (!base)
|
|
9
|
+
return next;
|
|
10
|
+
return {
|
|
11
|
+
identifiers: { ...base.identifiers, ...next.identifiers },
|
|
12
|
+
metadata: { ...base.metadata, ...next.metadata },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Flattens a logger context onto a call's metadata.
|
|
17
|
+
*
|
|
18
|
+
* `LoggerContext` nests identifiers and metadata, so it is flattened rather
|
|
19
|
+
* than spread wholesale: a test asserting on `tenantId` should find it at the
|
|
20
|
+
* top level, the way a real transport would render it.
|
|
21
|
+
*/
|
|
22
|
+
export function mergeContext(context, metadata) {
|
|
23
|
+
if (!context)
|
|
24
|
+
return metadata;
|
|
25
|
+
return {
|
|
26
|
+
...context.identifiers,
|
|
27
|
+
...context.metadata,
|
|
28
|
+
...metadata,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Structural comparison, so object metadata can actually be matched. */
|
|
32
|
+
export function deepMatches(actual, expected) {
|
|
33
|
+
if (Object.is(actual, expected))
|
|
34
|
+
return true;
|
|
35
|
+
if (typeof actual !== "object" || typeof expected !== "object")
|
|
36
|
+
return false;
|
|
37
|
+
if (actual === null || expected === null)
|
|
38
|
+
return false;
|
|
39
|
+
return JSON.stringify(actual) === JSON.stringify(expected);
|
|
40
|
+
}
|
|
41
|
+
//# 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;
|
|
@@ -3,67 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Records all log calls for assertion without side effects.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
calls.push({ method, level, message, metadata, timestamp: new Date() });
|
|
8
|
-
}
|
|
6
|
+
import { LEVELS, createRecordingLogger } from "./spyLogger.recording.js";
|
|
9
7
|
/**
|
|
10
8
|
* Creates a spy logger that records all log calls.
|
|
11
9
|
*
|
|
10
|
+
* A logger derived with `child()` or `withContext()` writes into the *same*
|
|
11
|
+
* recording as its parent, the way a real logger shares a transport. Giving
|
|
12
|
+
* the derived logger its own array meant code under test that called
|
|
13
|
+
* `logger.child({ module })` — the normal pattern — logged into an array
|
|
14
|
+
* nobody held, so assertions on the parent silently saw nothing.
|
|
15
|
+
*
|
|
12
16
|
* @param name - Logger name.
|
|
13
|
-
* @param level -
|
|
17
|
+
* @param level - Minimum level recorded. Defaults to TRACE (everything).
|
|
14
18
|
* @returns A SpyLogger instance.
|
|
15
19
|
*/
|
|
16
|
-
export function createSpyLogger(name = "test", level =
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
get name() {
|
|
20
|
-
return name;
|
|
21
|
-
},
|
|
22
|
-
get level() {
|
|
23
|
-
return level;
|
|
24
|
-
},
|
|
25
|
-
get enabled() {
|
|
26
|
-
return true;
|
|
27
|
-
},
|
|
28
|
-
get calls() {
|
|
29
|
-
return calls;
|
|
30
|
-
},
|
|
31
|
-
fatal: (message, metadata) => {
|
|
32
|
-
recordCall(calls, "fatal", 0, message, metadata);
|
|
33
|
-
},
|
|
34
|
-
error: (message, metadata) => {
|
|
35
|
-
recordCall(calls, "error", 1, message, metadata);
|
|
36
|
-
},
|
|
37
|
-
warn: (message, metadata) => {
|
|
38
|
-
recordCall(calls, "warn", 2, message, metadata);
|
|
39
|
-
},
|
|
40
|
-
info: (message, metadata) => {
|
|
41
|
-
recordCall(calls, "info", 3, message, metadata);
|
|
42
|
-
},
|
|
43
|
-
debug: (message, metadata) => {
|
|
44
|
-
recordCall(calls, "debug", 4, message, metadata);
|
|
45
|
-
},
|
|
46
|
-
trace: (message, metadata) => {
|
|
47
|
-
recordCall(calls, "trace", 5, message, metadata);
|
|
48
|
-
},
|
|
49
|
-
log: (logLevel, message, options) => {
|
|
50
|
-
recordCall(calls, "log", logLevel, message, options?.metadata);
|
|
51
|
-
},
|
|
52
|
-
child: (_options) => createSpyLogger(`${name}.child`, level),
|
|
53
|
-
withContext: (_context) => createSpyLogger(name, level),
|
|
54
|
-
setLevel: (newLevel) => {
|
|
55
|
-
level = newLevel;
|
|
56
|
-
},
|
|
57
|
-
enable: () => { },
|
|
58
|
-
disable: () => { },
|
|
59
|
-
flush: async () => { },
|
|
60
|
-
close: async () => { },
|
|
61
|
-
clear: () => {
|
|
62
|
-
calls.length = 0;
|
|
63
|
-
},
|
|
64
|
-
findByMethod: (method) => calls.filter((c) => c.method === method),
|
|
65
|
-
findByMessage: (substring) => calls.filter((c) => c.message.includes(substring)),
|
|
66
|
-
findByMetadata: (key, value) => calls.filter((c) => c.metadata !== undefined && c.metadata[key] === value),
|
|
67
|
-
};
|
|
20
|
+
export function createSpyLogger(name = "test", level = LEVELS.trace) {
|
|
21
|
+
return createRecordingLogger({ calls: [] }, { name }, level, true);
|
|
68
22
|
}
|
|
69
23
|
//# sourceMappingURL=spyLogger.core.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — Recording logger construction.
|
|
3
|
+
*
|
|
4
|
+
* Split from the factory so a derived logger — `child()` or `withContext()` —
|
|
5
|
+
* is built the same way as its parent and shares the same recording.
|
|
6
|
+
*
|
|
7
|
+
* @module spyLogger/spyLogger.recording
|
|
8
|
+
*/
|
|
9
|
+
import type { LoggerLevel, LoggerContext } from "@zudojs/logger";
|
|
10
|
+
import type { LogCall, SpyLogger } from "./spyLogger.type.js";
|
|
11
|
+
/** Numeric level for each method, ascending in verbosity. */
|
|
12
|
+
export declare const LEVELS: {
|
|
13
|
+
readonly fatal: 0;
|
|
14
|
+
readonly error: 1;
|
|
15
|
+
readonly warn: 2;
|
|
16
|
+
readonly info: 3;
|
|
17
|
+
readonly debug: 4;
|
|
18
|
+
readonly trace: 5;
|
|
19
|
+
};
|
|
20
|
+
/** Shared recording state, so derived loggers report to their parent. */
|
|
21
|
+
export interface Recorder {
|
|
22
|
+
readonly calls: LogCall[];
|
|
23
|
+
}
|
|
24
|
+
/** Options controlling a derived logger's identity. */
|
|
25
|
+
export interface DerivedOptions {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly context?: LoggerContext;
|
|
28
|
+
}
|
|
29
|
+
/** Builds a logger over an existing recording. */
|
|
30
|
+
export declare function createRecordingLogger(recorder: Recorder, derived: DerivedOptions, initialLevel: LoggerLevel, initiallyEnabled: boolean): SpyLogger;
|
|
31
|
+
//# sourceMappingURL=spyLogger.recording.d.ts.map
|