@zudojs/testing 0.1.0 → 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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -15
  3. package/dist/assertions/deepEqual.core.d.ts +25 -0
  4. package/dist/assertions/deepEqual.core.js +164 -0
  5. package/dist/assertions/deepEqual.describe.d.ts +19 -0
  6. package/dist/assertions/deepEqual.describe.js +59 -0
  7. package/dist/assertions/errorAssertions.core.d.ts +4 -0
  8. package/dist/assertions/errorAssertions.core.js +13 -2
  9. package/dist/assertions/eventAssertions.core.d.ts +12 -0
  10. package/dist/assertions/eventAssertions.core.js +26 -4
  11. package/dist/assertions/httpAssertions.core.d.ts +12 -1
  12. package/dist/assertions/httpAssertions.core.js +28 -6
  13. package/dist/assertions/index.d.ts +7 -6
  14. package/dist/assertions/index.js +6 -6
  15. package/dist/cleanupManager/cleanupManager.core.d.ts +4 -0
  16. package/dist/cleanupManager/cleanupManager.core.js +5 -2
  17. package/dist/mocking/mockFn.core.d.ts +4 -1
  18. package/dist/mocking/mockFn.core.js +33 -41
  19. package/dist/mocking/spy.core.d.ts +11 -2
  20. package/dist/mocking/spy.core.js +54 -25
  21. package/dist/mocking/stub.core.d.ts +9 -6
  22. package/dist/mocking/stub.core.js +49 -12
  23. package/dist/spyLogger/index.d.ts +5 -4
  24. package/dist/spyLogger/index.js +3 -3
  25. package/dist/spyLogger/spyLogger.context.d.ts +19 -0
  26. package/dist/spyLogger/spyLogger.context.js +41 -0
  27. package/dist/spyLogger/spyLogger.core.d.ts +7 -1
  28. package/dist/spyLogger/spyLogger.core.js +10 -56
  29. package/dist/spyLogger/spyLogger.recording.d.ts +31 -0
  30. package/dist/spyLogger/spyLogger.recording.js +89 -0
  31. package/dist/testApplication/testApplication.core.js +6 -3
  32. package/dist/testClock/testClock.core.js +19 -2
  33. package/dist/testContext/testContext.recorder.core.js +3 -3
  34. package/dist/testEventBus/testEventBus.core.js +5 -2
  35. package/dist/testMessageBus/testMessageBus.core.js +5 -2
  36. package/dist/testQueue/testQueue.core.js +3 -1
  37. package/dist/testSerialization/serializationAssertions.core.js +13 -4
  38. package/dist/testStorage/testStorageMemory.core.d.ts +12 -4
  39. package/dist/testStorage/testStorageMemory.core.js +25 -12
  40. package/package.json +33 -26
  41. package/dist/.tsbuildinfo +0 -1
  42. package/dist/assertions/errorAssertions.core.d.ts.map +0 -1
  43. package/dist/assertions/errorAssertions.core.js.map +0 -1
  44. package/dist/assertions/eventAssertions.core.d.ts.map +0 -1
  45. package/dist/assertions/eventAssertions.core.js.map +0 -1
  46. package/dist/assertions/httpAssertions.core.d.ts.map +0 -1
  47. package/dist/assertions/httpAssertions.core.js.map +0 -1
  48. package/dist/assertions/index.d.ts.map +0 -1
  49. package/dist/assertions/index.js.map +0 -1
  50. package/dist/cleanupManager/cleanupManager.core.d.ts.map +0 -1
  51. package/dist/cleanupManager/cleanupManager.core.js.map +0 -1
  52. package/dist/cleanupManager/index.d.ts.map +0 -1
  53. package/dist/cleanupManager/index.js.map +0 -1
  54. package/dist/fixtures/eventFixtures.core.d.ts.map +0 -1
  55. package/dist/fixtures/eventFixtures.core.js.map +0 -1
  56. package/dist/fixtures/index.d.ts.map +0 -1
  57. package/dist/fixtures/index.js.map +0 -1
  58. package/dist/fixtures/messageFixtures.core.d.ts.map +0 -1
  59. package/dist/fixtures/messageFixtures.core.js.map +0 -1
  60. package/dist/httpTesting/httpRequest.core.d.ts.map +0 -1
  61. package/dist/httpTesting/httpRequest.core.js.map +0 -1
  62. package/dist/httpTesting/httpResponse.core.d.ts.map +0 -1
  63. package/dist/httpTesting/httpResponse.core.js.map +0 -1
  64. package/dist/httpTesting/httpResponse.helpers.d.ts.map +0 -1
  65. package/dist/httpTesting/httpResponse.helpers.js.map +0 -1
  66. package/dist/httpTesting/httpResponse.type.d.ts.map +0 -1
  67. package/dist/httpTesting/httpResponse.type.js.map +0 -1
  68. package/dist/httpTesting/httpStatusCode.type.d.ts.map +0 -1
  69. package/dist/httpTesting/httpStatusCode.type.js.map +0 -1
  70. package/dist/httpTesting/index.d.ts.map +0 -1
  71. package/dist/httpTesting/index.js.map +0 -1
  72. package/dist/index.d.ts.map +0 -1
  73. package/dist/index.js.map +0 -1
  74. package/dist/mocking/index.d.ts.map +0 -1
  75. package/dist/mocking/index.js.map +0 -1
  76. package/dist/mocking/mockFn.core.d.ts.map +0 -1
  77. package/dist/mocking/mockFn.core.js.map +0 -1
  78. package/dist/mocking/spy.core.d.ts.map +0 -1
  79. package/dist/mocking/spy.core.js.map +0 -1
  80. package/dist/mocking/stub.core.d.ts.map +0 -1
  81. package/dist/mocking/stub.core.js.map +0 -1
  82. package/dist/spyLogger/index.d.ts.map +0 -1
  83. package/dist/spyLogger/index.js.map +0 -1
  84. package/dist/spyLogger/spyLogger.core.d.ts.map +0 -1
  85. package/dist/spyLogger/spyLogger.core.js.map +0 -1
  86. package/dist/spyLogger/spyLogger.type.d.ts.map +0 -1
  87. package/dist/spyLogger/spyLogger.type.js.map +0 -1
  88. package/dist/testApplication/index.d.ts.map +0 -1
  89. package/dist/testApplication/index.js.map +0 -1
  90. package/dist/testApplication/testApplication.core.d.ts.map +0 -1
  91. package/dist/testApplication/testApplication.core.js.map +0 -1
  92. package/dist/testClock/index.d.ts.map +0 -1
  93. package/dist/testClock/index.js.map +0 -1
  94. package/dist/testClock/testClock.core.d.ts.map +0 -1
  95. package/dist/testClock/testClock.core.js.map +0 -1
  96. package/dist/testConfig/index.d.ts.map +0 -1
  97. package/dist/testConfig/index.js.map +0 -1
  98. package/dist/testConfig/testConfig.core.d.ts.map +0 -1
  99. package/dist/testConfig/testConfig.core.js.map +0 -1
  100. package/dist/testContainer/index.d.ts.map +0 -1
  101. package/dist/testContainer/index.js.map +0 -1
  102. package/dist/testContainer/testContainer.core.d.ts.map +0 -1
  103. package/dist/testContainer/testContainer.core.js.map +0 -1
  104. package/dist/testContext/index.d.ts.map +0 -1
  105. package/dist/testContext/index.js.map +0 -1
  106. package/dist/testContext/testContext.core.d.ts.map +0 -1
  107. package/dist/testContext/testContext.core.js.map +0 -1
  108. package/dist/testContext/testContext.recorder.core.d.ts.map +0 -1
  109. package/dist/testContext/testContext.recorder.core.js.map +0 -1
  110. package/dist/testContext/testContext.recorder.type.d.ts.map +0 -1
  111. package/dist/testContext/testContext.recorder.type.js.map +0 -1
  112. package/dist/testEventBus/index.d.ts.map +0 -1
  113. package/dist/testEventBus/index.js.map +0 -1
  114. package/dist/testEventBus/testEventBus.core.d.ts.map +0 -1
  115. package/dist/testEventBus/testEventBus.core.js.map +0 -1
  116. package/dist/testMessageBus/index.d.ts.map +0 -1
  117. package/dist/testMessageBus/index.js.map +0 -1
  118. package/dist/testMessageBus/testMessageBus.core.d.ts.map +0 -1
  119. package/dist/testMessageBus/testMessageBus.core.js.map +0 -1
  120. package/dist/testQueue/index.d.ts.map +0 -1
  121. package/dist/testQueue/index.js.map +0 -1
  122. package/dist/testQueue/testQueue.core.d.ts.map +0 -1
  123. package/dist/testQueue/testQueue.core.js.map +0 -1
  124. package/dist/testSerialization/index.d.ts.map +0 -1
  125. package/dist/testSerialization/index.js.map +0 -1
  126. package/dist/testSerialization/serializationAssertions.core.d.ts.map +0 -1
  127. package/dist/testSerialization/serializationAssertions.core.js.map +0 -1
  128. package/dist/testStorage/index.d.ts.map +0 -1
  129. package/dist/testStorage/index.js.map +0 -1
  130. package/dist/testStorage/testStorageMemory.core.d.ts.map +0 -1
  131. 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
- let implementation;
27
- let returnValue = defaultReturnValue;
28
- let resolvedValue;
29
- let rejectedValue;
30
- let shouldReject = false;
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
- if (shouldReject) {
34
- throw rejectedValue;
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
- returnValue = value;
69
- resolvedValue = undefined;
70
- shouldReject = false;
74
+ mode = { kind: "value", value };
71
75
  };
72
76
  mock.mockResolvedValue = (value) => {
73
- resolvedValue = value;
74
- returnValue = undefined;
75
- shouldReject = false;
77
+ mode = { kind: "resolve", value };
76
78
  };
77
79
  mock.mockRejectedValue = (error) => {
78
- rejectedValue = error;
79
- shouldReject = true;
80
- returnValue = undefined;
81
- resolvedValue = undefined;
80
+ mode = { kind: "reject", error };
82
81
  };
83
82
  mock.mockImplementation = (fn) => {
84
- implementation = fn;
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
- implementation = undefined;
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
- restore: () => void;
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.restore();
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
  /**
@@ -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.restore();
24
+ * spy.reset();
22
25
  * ```
23
26
  */
24
27
  export function createSpyFn(fn) {
25
28
  const calls = [];
26
29
  const results = [];
27
- const spy = ((...args) => {
30
+ const errors = [];
31
+ const spy = function (...args) {
28
32
  calls.push(args);
29
- const result = fn(...args);
30
- results.push(result);
31
- return result;
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
- Object.defineProperty(spy, "calls", {
39
- get: () => calls,
40
- enumerable: true,
41
- });
42
- Object.defineProperty(spy, "results", {
43
- get: () => results,
44
- enumerable: true,
45
- });
46
- Object.defineProperty(spy, "callCount", {
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
- object[property] = ((...args) => {
94
+ const calls = [];
95
+ const results = [];
96
+ const errors = [];
97
+ target[key] = function (...args) {
83
98
  calls.push(args);
84
- return originalFn(...args);
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
- object[property] =
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 Record<string, unknown>>(overrides?: Partial<T>): T;
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> { /* real implementation *\/ }
43
- * async query(sql: string): Promise<unknown[]> { /* real implementation *\/ }
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>(_OriginalClass: new (...args: unknown[]) => T, overrides?: Partial<T>): new () => 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
- const key = prop;
35
- if (key in overrides) {
36
- return overrides[key];
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 (typeof prop === "symbol") {
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> { /* real implementation *\/ }
56
- * async query(sql: string): Promise<unknown[]> { /* real implementation *\/ }
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(_OriginalClass, overrides = {}) {
69
- return class {
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 logger for testing.
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 type { LogCall, SpyLogger } from "./spyLogger.type.js";
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
@@ -1,7 +1,7 @@
1
1
  /**
2
- * @zudojs/testing — Spy logger for testing.
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 - Optional minimum level (defaults to TRACE = all).
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
- function recordCall(calls, method, level, message, metadata) {
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 - Optional minimum level (defaults to TRACE = all).
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 = 5) {
17
- const calls = [];
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