@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.
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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,35 +1,77 @@
1
1
  # @zudojs/testing
2
2
 
3
- Test helpers, fixtures, mocks, and utilities for testing Zudojs applications.
3
+ Test helpers, fixtures, mocks, and assertions for Zudojs applications.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @zudojs/testing
8
+ npm install --save-dev @zudojs/testing
9
9
  ```
10
10
 
11
11
  ## Quick Start
12
12
 
13
13
  ```typescript
14
- import { createMockContainer, createTestLogger } from "@zudojs/testing";
14
+ import {
15
+ createCleanupManager,
16
+ createMockFn,
17
+ createSpyLogger,
18
+ createTestClock,
19
+ createTestContainer,
20
+ } from "@zudojs/testing";
15
21
 
16
- const container = createMockContainer();
17
- container.register("database", mockDatabase);
22
+ const container = createTestContainer({
23
+ overrides: [{ token: databaseToken, useValue: fakeDatabase }],
24
+ });
18
25
 
19
- const logger = createTestLogger();
26
+ const logger = createSpyLogger("test");
27
+ const clock = createTestClock(0); // pinned to the epoch
28
+ const cleanup = createCleanupManager();
29
+
30
+ const findUser = createMockFn<[string], Promise<User>>();
31
+ findUser.mockResolvedValue({ id: "u_1" });
32
+
33
+ clock.advance(60_000);
34
+ cleanup.register(() => container.dispose(), "container");
35
+
36
+ // Rejects with an AggregateError if any cleanup fails.
37
+ await cleanup.dispose();
38
+ ```
39
+
40
+ Assertions compare structurally, so they can actually fail:
41
+
42
+ ```typescript
43
+ import { assertResponseStatus, assertResponseBody } from "@zudojs/testing";
44
+
45
+ assertResponseStatus(response, 201);
46
+ assertResponseBody(response, { id: "u_1", roles: new Set(["admin"]) });
20
47
  ```
21
48
 
22
49
  ## Features
23
50
 
24
- - Mock container for testing
25
- - Test fixtures and factories
26
- - In-memory adapters for testing
27
- - Test logger with assertions
28
- - Snapshot testing utilities
51
+ - Test container, config, clock, and application context
52
+ - Spy logger that records child and context loggers too
53
+ - Mock functions, spies, and stubs
54
+ - Recording event bus, message bus, and queue
55
+ - HTTP request/response builders
56
+ - Structural assertions for responses, events, errors, and serialization
57
+ - Cleanup manager that reports what failed
58
+
59
+ ## Safety Notes
60
+
61
+ - Assertions compare structurally, not by `JSON.stringify`. `Map`, `Set`,
62
+ `Date`, `BigInt`, `undefined` values and key order are all handled, and a
63
+ circular value reports a mismatch instead of throwing a `TypeError`.
64
+ - `createStub()` answers `then` with `undefined`, so awaiting a stub — or
65
+ returning one from an async factory — resolves rather than hanging.
66
+ - `cleanup.dispose()` rejects with an `AggregateError` when any cleanup fails,
67
+ after running them all.
68
+ - `mockResolvedValue` and `mockRejectedValue` return promises; `results` stays
69
+ aligned index-for-index with `calls`.
70
+ - Spies forward their receiver, so a method reading `this` still works.
29
71
 
30
72
  ## Use Cases
31
73
 
32
- - Unit testing Zudojs components
33
- - Integration testing
34
- - Mocking dependencies
35
- - Test fixtures
74
+ - Unit and integration testing
75
+ - Dependency injection in tests
76
+ - Deterministic time and randomness
77
+ - Asserting on events, messages, and HTTP responses
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @zudojs/testing — Structural equality for assertions.
3
+ *
4
+ * Assertions previously compared `JSON.stringify(actual) !== JSON.stringify(expected)`,
5
+ * which is not equality in either direction. `Map`, `Set`, functions and
6
+ * `undefined` values all stringify to nothing, so structurally different
7
+ * values compared equal and an assertion could not fail; key order is
8
+ * significant, so equal values compared different; and circular or `BigInt`
9
+ * input threw a `TypeError` out of the assertion itself.
10
+ *
11
+ * @module assertions/deepEqual
12
+ */
13
+ import type { Difference } from "./deepEqual.describe.js";
14
+ /**
15
+ * Find the first structural difference between two values.
16
+ *
17
+ * @param actual - The observed value.
18
+ * @param expected - The value it should equal.
19
+ * @param rootPath - Label for the root in the reported path.
20
+ * @returns The first difference, or undefined when the values are equal.
21
+ */
22
+ export declare function findDifference(actual: unknown, expected: unknown, rootPath?: string): Difference | undefined;
23
+ /** Whether two values are structurally equal. */
24
+ export declare function deepEqual(actual: unknown, expected: unknown): boolean;
25
+ //# sourceMappingURL=deepEqual.core.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @zudojs/testing — Structural equality for assertions.
3
+ *
4
+ * Assertions previously compared `JSON.stringify(actual) !== JSON.stringify(expected)`,
5
+ * which is not equality in either direction. `Map`, `Set`, functions and
6
+ * `undefined` values all stringify to nothing, so structurally different
7
+ * values compared equal and an assertion could not fail; key order is
8
+ * significant, so equal values compared different; and circular or `BigInt`
9
+ * input threw a `TypeError` out of the assertion itself.
10
+ *
11
+ * @module assertions/deepEqual
12
+ */
13
+ import { describeValue } from "./deepEqual.describe.js";
14
+ /** The constructor-level kind of a value, used to reject cross-type matches. */
15
+ function kindOf(value) {
16
+ if (value === null)
17
+ return "null";
18
+ if (Array.isArray(value))
19
+ return "array";
20
+ if (value instanceof Date)
21
+ return "date";
22
+ if (value instanceof RegExp)
23
+ return "regexp";
24
+ if (value instanceof Map)
25
+ return "map";
26
+ if (value instanceof Set)
27
+ return "set";
28
+ if (ArrayBuffer.isView(value))
29
+ return "typedarray";
30
+ return typeof value;
31
+ }
32
+ /** Finds the first structural difference between two values. */
33
+ function diff(actual, expected, path, seen) {
34
+ if (Object.is(actual, expected))
35
+ return undefined;
36
+ const actualKind = kindOf(actual);
37
+ const expectedKind = kindOf(expected);
38
+ if (actualKind !== expectedKind) {
39
+ return {
40
+ path,
41
+ reason: `expected ${expectedKind}, received ${actualKind}`,
42
+ };
43
+ }
44
+ if (actual instanceof Date && expected instanceof Date) {
45
+ return actual.getTime() === expected.getTime()
46
+ ? undefined
47
+ : {
48
+ path,
49
+ reason: `expected ${describeValue(expected)}, received ${describeValue(actual)}`,
50
+ };
51
+ }
52
+ if (actual instanceof RegExp && expected instanceof RegExp) {
53
+ return String(actual) === String(expected)
54
+ ? undefined
55
+ : { path, reason: `expected ${expected}, received ${actual}` };
56
+ }
57
+ if (typeof actual !== "object" || actual === null) {
58
+ return {
59
+ path,
60
+ reason: `expected ${describeValue(expected)}, received ${describeValue(actual)}`,
61
+ };
62
+ }
63
+ // Guard against cycles: a pair already being compared is assumed equal
64
+ // until proven otherwise elsewhere in the walk.
65
+ const previous = seen.get(actual);
66
+ if (previous === expected)
67
+ return undefined;
68
+ seen.set(actual, expected);
69
+ if (Array.isArray(actual) && Array.isArray(expected)) {
70
+ if (actual.length !== expected.length) {
71
+ return {
72
+ path,
73
+ reason: `expected length ${expected.length}, received ${actual.length}`,
74
+ };
75
+ }
76
+ for (let index = 0; index < actual.length; index++) {
77
+ const found = diff(actual[index], expected[index], `${path}[${index}]`, seen);
78
+ if (found)
79
+ return found;
80
+ }
81
+ return undefined;
82
+ }
83
+ if (actual instanceof Map && expected instanceof Map) {
84
+ if (actual.size !== expected.size) {
85
+ return {
86
+ path,
87
+ reason: `expected ${expected.size} entries, received ${actual.size}`,
88
+ };
89
+ }
90
+ for (const [key, value] of expected) {
91
+ if (!actual.has(key)) {
92
+ return { path, reason: `missing key ${describeValue(key)}` };
93
+ }
94
+ const found = diff(actual.get(key), value, `${path}[${describeValue(key)}]`, seen);
95
+ if (found)
96
+ return found;
97
+ }
98
+ return undefined;
99
+ }
100
+ if (actual instanceof Set && expected instanceof Set) {
101
+ if (actual.size !== expected.size) {
102
+ return {
103
+ path,
104
+ reason: `expected ${expected.size} items, received ${actual.size}`,
105
+ };
106
+ }
107
+ for (const entry of expected) {
108
+ if (!actual.has(entry)) {
109
+ return { path, reason: `missing item ${describeValue(entry)}` };
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ if (ArrayBuffer.isView(actual) && ArrayBuffer.isView(expected)) {
115
+ const a = new Uint8Array(actual.buffer, actual.byteOffset, actual.byteLength);
116
+ const b = new Uint8Array(expected.buffer, expected.byteOffset, expected.byteLength);
117
+ if (a.length !== b.length) {
118
+ return {
119
+ path,
120
+ reason: `expected ${b.length} bytes, received ${a.length}`,
121
+ };
122
+ }
123
+ for (let index = 0; index < a.length; index++) {
124
+ if (a[index] !== b[index]) {
125
+ return { path, reason: `bytes differ at offset ${index}` };
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+ const actualKeys = Object.keys(actual).sort();
131
+ const expectedKeys = Object.keys(expected).sort();
132
+ for (const key of expectedKeys) {
133
+ if (!actualKeys.includes(key)) {
134
+ return { path: `${path}.${key}`, reason: "missing from received value" };
135
+ }
136
+ }
137
+ for (const key of actualKeys) {
138
+ if (!expectedKeys.includes(key)) {
139
+ return { path: `${path}.${key}`, reason: "unexpected in received value" };
140
+ }
141
+ }
142
+ for (const key of expectedKeys) {
143
+ const found = diff(actual[key], expected[key], `${path}.${key}`, seen);
144
+ if (found)
145
+ return found;
146
+ }
147
+ return undefined;
148
+ }
149
+ /**
150
+ * Find the first structural difference between two values.
151
+ *
152
+ * @param actual - The observed value.
153
+ * @param expected - The value it should equal.
154
+ * @param rootPath - Label for the root in the reported path.
155
+ * @returns The first difference, or undefined when the values are equal.
156
+ */
157
+ export function findDifference(actual, expected, rootPath = "value") {
158
+ return diff(actual, expected, rootPath, new Map());
159
+ }
160
+ /** Whether two values are structurally equal. */
161
+ export function deepEqual(actual, expected) {
162
+ return findDifference(actual, expected) === undefined;
163
+ }
164
+ //# sourceMappingURL=deepEqual.core.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @zudojs/testing — Rendering values for assertion messages.
3
+ *
4
+ * Assertion failures name the path and the difference, so both sides have to
5
+ * render without throwing — including the circular and BigInt values that
6
+ * `JSON.stringify` refuses.
7
+ *
8
+ * @module assertions/deepEqual.describe
9
+ */
10
+ /** Where two values first differed. */
11
+ export interface Difference {
12
+ /** Dotted path to the differing node. */
13
+ readonly path: string;
14
+ /** Human-readable description of the mismatch. */
15
+ readonly reason: string;
16
+ }
17
+ /** Renders a value for an assertion message without throwing. */
18
+ export declare function describeValue(value: unknown, depth?: number): string;
19
+ //# sourceMappingURL=deepEqual.describe.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @zudojs/testing — Rendering values for assertion messages.
3
+ *
4
+ * Assertion failures name the path and the difference, so both sides have to
5
+ * render without throwing — including the circular and BigInt values that
6
+ * `JSON.stringify` refuses.
7
+ *
8
+ * @module assertions/deepEqual.describe
9
+ */
10
+ /** Renders a value for an assertion message without throwing. */
11
+ export function describeValue(value, depth = 0) {
12
+ if (value === null)
13
+ return "null";
14
+ if (value === undefined)
15
+ return "undefined";
16
+ switch (typeof value) {
17
+ case "string":
18
+ return JSON.stringify(value);
19
+ case "bigint":
20
+ return `${value}n`;
21
+ case "number":
22
+ case "boolean":
23
+ return String(value);
24
+ case "symbol":
25
+ return value.toString();
26
+ case "function":
27
+ return `[Function ${value.name || "anonymous"}]`;
28
+ }
29
+ if (depth > 2)
30
+ return "…";
31
+ if (value instanceof Date)
32
+ return `Date(${value.toISOString()})`;
33
+ if (value instanceof RegExp)
34
+ return String(value);
35
+ if (value instanceof Map) {
36
+ return `Map(${value.size}) {${[...value]
37
+ .slice(0, 4)
38
+ .map(([k, v]) => ` ${describeValue(k, depth + 1)} => ${describeValue(v, depth + 1)}`)
39
+ .join(",")} }`;
40
+ }
41
+ if (value instanceof Set) {
42
+ return `Set(${value.size}) {${[...value]
43
+ .slice(0, 4)
44
+ .map((entry) => ` ${describeValue(entry, depth + 1)}`)
45
+ .join(",")} }`;
46
+ }
47
+ if (Array.isArray(value)) {
48
+ return `[${value
49
+ .slice(0, 6)
50
+ .map((entry) => describeValue(entry, depth + 1))
51
+ .join(", ")}${value.length > 6 ? ", …" : ""}]`;
52
+ }
53
+ const keys = Object.keys(value);
54
+ return `{${keys
55
+ .slice(0, 6)
56
+ .map((key) => ` ${key}: ${describeValue(value[key], depth + 1)}`)
57
+ .join(",")}${keys.length > 6 ? ", …" : ""} }`;
58
+ }
59
+ //# sourceMappingURL=deepEqual.describe.js.map
@@ -34,4 +34,8 @@ export declare function assertErrorCode(error: unknown, code: string): void;
34
34
  * @param value - Expected metadata value.
35
35
  */
36
36
  export declare function assertErrorMetadata(error: unknown, key: string, value: unknown): void;
37
+ /**
38
+ * Asserts that an error is an instance of a specific class.
39
+ */
40
+ export declare function assertErrorType<T extends Error>(error: unknown, type: new (...args: never[]) => T): asserts error is T;
37
41
  //# sourceMappingURL=errorAssertions.core.d.ts.map
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Assert errors, error types, and error messages.
5
5
  */
6
+ import { findDifference } from "./deepEqual.core.js";
7
+ import { describeValue } from "./deepEqual.describe.js";
6
8
  /**
7
9
  * Asserts that a function throws an error.
8
10
  *
@@ -75,8 +77,17 @@ export function assertErrorMetadata(error, key, value) {
75
77
  }
76
78
  const baseError = error;
77
79
  const actual = baseError.metadata?.[key];
78
- if (actual !== value) {
79
- throw new Error(`Expected error metadata "${key}" to be "${value}", got "${actual}".`);
80
+ const difference = findDifference(actual, value, `metadata.${key}`);
81
+ if (difference) {
82
+ throw new Error(`Expected error metadata "${key}" to be ${describeValue(value)}, got ${describeValue(actual)}.`);
83
+ }
84
+ }
85
+ /**
86
+ * Asserts that an error is an instance of a specific class.
87
+ */
88
+ export function assertErrorType(error, type) {
89
+ if (!(error instanceof type)) {
90
+ throw new Error(`Expected error to be a ${type.name}, got ${error instanceof Error ? error.constructor.name : typeof error}.`);
80
91
  }
81
92
  }
82
93
  //# sourceMappingURL=errorAssertions.core.js.map
@@ -41,4 +41,16 @@ export declare function assertEventPublished<TPayload>(events: readonly Recorded
41
41
  * @param type - Expected message type.
42
42
  */
43
43
  export declare function assertMessageDispatched<TPayload>(messages: readonly RecordedMessage<TPayload>[], type: string): void;
44
+ /**
45
+ * Asserts that no event of a given type was published.
46
+ *
47
+ * The negative counterpart matters as much as the positive one: "this request
48
+ * must not emit a refund event" is a real assertion, and without this helper
49
+ * it gets written as a hand-rolled `.some()` that is easy to invert.
50
+ */
51
+ export declare function assertEventNotPublished<TPayload>(events: readonly RecordedEvent<TPayload>[], type: string): void;
52
+ /**
53
+ * Asserts that no message of a given type was dispatched.
54
+ */
55
+ export declare function assertMessageNotDispatched<TPayload>(messages: readonly RecordedMessage<TPayload>[], type: string): void;
44
56
  //# sourceMappingURL=eventAssertions.core.d.ts.map
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Assert events, event types, and event payloads.
5
5
  */
6
+ import { findDifference } from "./deepEqual.core.js";
6
7
  /**
7
8
  * Asserts that an event has a specific type.
8
9
  *
@@ -21,10 +22,9 @@ export function assertEventType(event, type) {
21
22
  * @param expected - Expected payload.
22
23
  */
23
24
  export function assertEventPayload(event, expected) {
24
- const actual = JSON.stringify(event.payload);
25
- const expectedJson = JSON.stringify(expected);
26
- if (actual !== expectedJson) {
27
- throw new Error(`Expected event payload ${expectedJson}, got ${actual}.`);
25
+ const difference = findDifference(event.payload, expected, "payload");
26
+ if (difference) {
27
+ throw new Error(`Event payload mismatch at ${difference.path}: ${difference.reason}.`);
28
28
  }
29
29
  }
30
30
  /**
@@ -60,4 +60,26 @@ export function assertMessageDispatched(messages, type) {
60
60
  throw new Error(`Expected message "${type}" to be dispatched, but it was not found.`);
61
61
  }
62
62
  }
63
+ /**
64
+ * Asserts that no event of a given type was published.
65
+ *
66
+ * The negative counterpart matters as much as the positive one: "this request
67
+ * must not emit a refund event" is a real assertion, and without this helper
68
+ * it gets written as a hand-rolled `.some()` that is easy to invert.
69
+ */
70
+ export function assertEventNotPublished(events, type) {
71
+ const found = events.filter((e) => e.event.type === type);
72
+ if (found.length > 0) {
73
+ throw new Error(`Expected event "${type}" not to be published, but it was published ${found.length} time(s).`);
74
+ }
75
+ }
76
+ /**
77
+ * Asserts that no message of a given type was dispatched.
78
+ */
79
+ export function assertMessageNotDispatched(messages, type) {
80
+ const found = messages.filter((m) => m.message.type === type);
81
+ if (found.length > 0) {
82
+ throw new Error(`Expected message "${type}" not to be dispatched, but it was dispatched ${found.length} time(s).`);
83
+ }
84
+ }
63
85
  //# sourceMappingURL=eventAssertions.core.js.map
@@ -21,12 +21,23 @@ export declare function assertResponseStatus(response: TestHTTPResponse, expecte
21
21
  */
22
22
  export declare function assertResponseHeader(response: TestHTTPResponse, header: string, value: string): void;
23
23
  /**
24
- * Asserts that a response body matches expected JSON.
24
+ * Asserts that a response body matches the expected value.
25
+ *
26
+ * Compares structurally rather than by serializing both sides: `Map`, `Set`,
27
+ * `undefined` values and functions all disappear under `JSON.stringify`, so a
28
+ * serialized comparison passes on values that are not equal at all.
25
29
  *
26
30
  * @param response - The test response.
27
31
  * @param expected - Expected body.
28
32
  */
29
33
  export declare function assertResponseBody(response: TestHTTPResponse, expected: unknown): void;
34
+ /**
35
+ * Asserts that a response body contains at least the expected properties.
36
+ *
37
+ * @param response - The test response.
38
+ * @param expected - Properties the body must contain.
39
+ */
40
+ export declare function assertResponseBodyContains(response: TestHTTPResponse, expected: Readonly<Record<string, unknown>>): void;
30
41
  /**
31
42
  * Asserts that a response is 200 OK.
32
43
  */
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Assert HTTP responses, status codes, headers, and bodies.
5
5
  */
6
+ import { findDifference } from "./deepEqual.core.js";
7
+ import { describeValue } from "./deepEqual.describe.js";
6
8
  /**
7
9
  * Asserts that a response has a specific status code.
8
10
  *
@@ -28,17 +30,37 @@ export function assertResponseHeader(response, header, value) {
28
30
  }
29
31
  }
30
32
  /**
31
- * Asserts that a response body matches expected JSON.
33
+ * Asserts that a response body matches the expected value.
34
+ *
35
+ * Compares structurally rather than by serializing both sides: `Map`, `Set`,
36
+ * `undefined` values and functions all disappear under `JSON.stringify`, so a
37
+ * serialized comparison passes on values that are not equal at all.
32
38
  *
33
39
  * @param response - The test response.
34
40
  * @param expected - Expected body.
35
41
  */
36
42
  export function assertResponseBody(response, expected) {
37
- const actual = response.body;
38
- const actualJson = JSON.stringify(actual);
39
- const expectedJson = JSON.stringify(expected);
40
- if (actualJson !== expectedJson) {
41
- throw new Error(`Expected body ${expectedJson}, got ${actualJson}.`);
43
+ const difference = findDifference(response.body, expected, "body");
44
+ if (difference) {
45
+ throw new Error(`Response body mismatch at ${difference.path}: ${difference.reason}.`);
46
+ }
47
+ }
48
+ /**
49
+ * Asserts that a response body contains at least the expected properties.
50
+ *
51
+ * @param response - The test response.
52
+ * @param expected - Properties the body must contain.
53
+ */
54
+ export function assertResponseBodyContains(response, expected) {
55
+ const body = response.body;
56
+ if (typeof body !== "object" || body === null) {
57
+ throw new Error(`Expected an object body, got ${describeValue(body)}.`);
58
+ }
59
+ for (const [key, value] of Object.entries(expected)) {
60
+ const difference = findDifference(body[key], value, `body.${key}`);
61
+ if (difference) {
62
+ throw new Error(`Response body mismatch at ${difference.path}: ${difference.reason}.`);
63
+ }
42
64
  }
43
65
  }
44
66
  /**
@@ -1,9 +1,10 @@
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
+ export type { Difference } from "./deepEqual.describe.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -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();
@@ -41,9 +41,12 @@ export function createCleanupManager(options = {}) {
41
41
  options.onError?.(error, entry);
42
42
  }
43
43
  }
44
+ const attempted = reversed.length;
44
45
  entries.length = 0;
45
- if (errors.length > 0 && errors.length === entries.length) {
46
- throw new Error(`All ${errors.length} cleanup functions failed.`);
46
+ if (errors.length > 0) {
47
+ throw new AggregateError(errors.map((entry) => entry.error), `${errors.length} of ${attempted} cleanup functions failed: ${errors
48
+ .map((entry) => entry.entry.label)
49
+ .join(", ")}`);
47
50
  }
48
51
  };
49
52
  return {
@@ -9,11 +9,14 @@
9
9
  export interface MockFn<TArgs extends readonly unknown[] = unknown[], TResult = unknown> {
10
10
  (...args: TArgs): TResult;
11
11
  readonly calls: readonly TArgs[];
12
+ /** Result of each call, aligned index-for-index with {@link calls}. */
12
13
  readonly results: readonly TResult[];
13
14
  readonly invoked: boolean;
14
15
  readonly callCount: number;
15
16
  mockReturnValue: (value: TResult) => void;
16
- mockResolvedValue: (value: TResult) => void;
17
+ /** Configures the mock to return a promise resolving to `value`. */
18
+ mockResolvedValue: (value: Awaited<TResult>) => void;
19
+ /** Configures the mock to return a promise rejecting with `error`. */
17
20
  mockRejectedValue: (error: unknown) => void;
18
21
  mockImplementation: (fn: (...args: TArgs) => TResult) => void;
19
22
  mockReset: () => void;