@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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -15
  3. package/dist/assertions/deepEqual.core.d.ts +25 -0
  4. package/dist/assertions/deepEqual.core.js +194 -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 +28 -3
  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 +24 -7
  17. package/dist/mocking/mockFn.core.d.ts +10 -1
  18. package/dist/mocking/mockFn.core.js +48 -39
  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 +27 -0
  26. package/dist/spyLogger/spyLogger.context.js +44 -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 +100 -0
  31. package/dist/testApplication/testApplication.core.js +6 -3
  32. package/dist/testClock/testClock.core.js +24 -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 +19 -5
  38. package/dist/testStorage/testStorageMemory.core.d.ts +12 -4
  39. package/dist/testStorage/testStorageMemory.core.js +25 -12
  40. package/package.json +37 -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
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Assertion helpers.
3
- *
4
- * HTTP, error, and event assertion utilities.
2
+ * @zudojs/testing — Assertions Barrel
5
3
  */
6
- export { assertResponseStatus, assertResponseHeader, assertResponseBody, assertOK, assertCreated, assertNoContent, assertBadRequest, assertNotFound, assertServerError, } from "./httpAssertions.core.js";
7
- export { assertThrows, assertRejects, assertErrorCode, assertErrorMetadata, } from "./errorAssertions.core.js";
8
- export { assertEventType, assertEventPayload, assertRecordedEventType, assertEventPublished, assertMessageDispatched, } from "./eventAssertions.core.js";
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 dispose = async () => {
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 && errors.length === entries.length) {
46
- throw new Error(`All ${errors.length} cleanup functions failed.`);
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
- mockResolvedValue: (value: TResult) => void;
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
- let implementation;
27
- let returnValue = defaultReturnValue;
28
- let resolvedValue;
29
- let rejectedValue;
30
- let shouldReject = false;
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
- if (shouldReject) {
34
- throw rejectedValue;
35
- }
36
- if (resolvedValue !== undefined) {
37
- results.push(resolvedValue);
38
- return resolvedValue;
54
+ let result;
55
+ try {
56
+ result = produce(args);
39
57
  }
40
- if (implementation) {
41
- const result = implementation(...args);
42
- results.push(result);
43
- return result;
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
- if (returnValue !== undefined) {
46
- results.push(returnValue);
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
- returnValue = value;
69
- resolvedValue = undefined;
70
- shouldReject = false;
89
+ mode = { kind: "value", value };
71
90
  };
72
91
  mock.mockResolvedValue = (value) => {
73
- resolvedValue = value;
74
- returnValue = undefined;
75
- shouldReject = false;
92
+ mode = { kind: "resolve", value };
76
93
  };
77
94
  mock.mockRejectedValue = (error) => {
78
- rejectedValue = error;
79
- shouldReject = true;
80
- returnValue = undefined;
81
- resolvedValue = undefined;
95
+ mode = { kind: "reject", error };
82
96
  };
83
97
  mock.mockImplementation = (fn) => {
84
- implementation = fn;
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
- implementation = undefined;
93
- returnValue = defaultReturnValue;
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
- 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,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 - 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;