@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
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,83 @@
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`, even when the implementation throws
70
+ (the slot holds `undefined` and the thrown value lands in `errors`).
71
+ - Spies forward their receiver, so a method reading `this` still works.
72
+ - `assertThrows` is for synchronous code. Handing it an async function throws
73
+ "use assertRejects" instead of a misleading "did not throw", and the
74
+ rejection is handled rather than leaked.
75
+ - `findByMetadata` and the structural assertions compare `Set` members and
76
+ `Map` keys by value, so `new Set([{ id: 1 }])` matches `new Set([{ id: 1 }])`.
29
77
 
30
78
  ## Use Cases
31
79
 
32
- - Unit testing Zudojs components
33
- - Integration testing
34
- - Mocking dependencies
35
- - Test fixtures
80
+ - Unit and integration testing
81
+ - Dependency injection in tests
82
+ - Deterministic time and randomness
83
+ - 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,194 @@
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
+ // Keys are matched by identity first and structurally second, so a
91
+ // Map keyed by objects compares by value like everything else here.
92
+ const unmatched = [...actual.keys()];
93
+ for (const [key, value] of expected) {
94
+ const actualKey = actual.has(key)
95
+ ? key
96
+ : findStructuralMatch(unmatched, key, seen);
97
+ if (actualKey === NO_MATCH) {
98
+ return { path, reason: `missing key ${describeValue(key)}` };
99
+ }
100
+ unmatched.splice(unmatched.indexOf(actualKey), 1);
101
+ const found = diff(actual.get(actualKey), value, `${path}[${describeValue(key)}]`, seen);
102
+ if (found)
103
+ return found;
104
+ }
105
+ return undefined;
106
+ }
107
+ if (actual instanceof Set && expected instanceof Set) {
108
+ if (actual.size !== expected.size) {
109
+ return {
110
+ path,
111
+ reason: `expected ${expected.size} items, received ${actual.size}`,
112
+ };
113
+ }
114
+ // `Set.has` is identity-based, so two Sets holding equal but distinct
115
+ // objects compared unequal. Each expected entry is matched against a
116
+ // still-unmatched actual entry structurally instead.
117
+ const unmatched = [...actual];
118
+ for (const entry of expected) {
119
+ const match = actual.has(entry)
120
+ ? entry
121
+ : findStructuralMatch(unmatched, entry, seen);
122
+ if (match === NO_MATCH) {
123
+ return { path, reason: `missing item ${describeValue(entry)}` };
124
+ }
125
+ unmatched.splice(unmatched.indexOf(match), 1);
126
+ }
127
+ return undefined;
128
+ }
129
+ if (ArrayBuffer.isView(actual) && ArrayBuffer.isView(expected)) {
130
+ const a = new Uint8Array(actual.buffer, actual.byteOffset, actual.byteLength);
131
+ const b = new Uint8Array(expected.buffer, expected.byteOffset, expected.byteLength);
132
+ if (a.length !== b.length) {
133
+ return {
134
+ path,
135
+ reason: `expected ${b.length} bytes, received ${a.length}`,
136
+ };
137
+ }
138
+ for (let index = 0; index < a.length; index++) {
139
+ if (a[index] !== b[index]) {
140
+ return { path, reason: `bytes differ at offset ${index}` };
141
+ }
142
+ }
143
+ return undefined;
144
+ }
145
+ const actualKeys = Object.keys(actual).sort();
146
+ const expectedKeys = Object.keys(expected).sort();
147
+ for (const key of expectedKeys) {
148
+ if (!actualKeys.includes(key)) {
149
+ return { path: `${path}.${key}`, reason: "missing from received value" };
150
+ }
151
+ }
152
+ for (const key of actualKeys) {
153
+ if (!expectedKeys.includes(key)) {
154
+ return { path: `${path}.${key}`, reason: "unexpected in received value" };
155
+ }
156
+ }
157
+ for (const key of expectedKeys) {
158
+ const found = diff(actual[key], expected[key], `${path}.${key}`, seen);
159
+ if (found)
160
+ return found;
161
+ }
162
+ return undefined;
163
+ }
164
+ const NO_MATCH = Symbol("deepEqual.noMatch");
165
+ /**
166
+ * Finds a candidate structurally equal to `expected`. Each probe walks a
167
+ * copy of `seen`: the cycle guard records a pair as "assumed equal" before
168
+ * comparing it, and a probe that fails must not leave that assumption
169
+ * behind for the next candidate.
170
+ */
171
+ function findStructuralMatch(candidates, expected, seen) {
172
+ for (const candidate of candidates) {
173
+ if (diff(candidate, expected, "", new Map(seen)) === undefined) {
174
+ return candidate;
175
+ }
176
+ }
177
+ return NO_MATCH;
178
+ }
179
+ /**
180
+ * Find the first structural difference between two values.
181
+ *
182
+ * @param actual - The observed value.
183
+ * @param expected - The value it should equal.
184
+ * @param rootPath - Label for the root in the reported path.
185
+ * @returns The first difference, or undefined when the values are equal.
186
+ */
187
+ export function findDifference(actual, expected, rootPath = "value") {
188
+ return diff(actual, expected, rootPath, new Map());
189
+ }
190
+ /** Whether two values are structurally equal. */
191
+ export function deepEqual(actual, expected) {
192
+ return findDifference(actual, expected) === undefined;
193
+ }
194
+ //# 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
  *
@@ -11,8 +13,9 @@
11
13
  * @returns The thrown error.
12
14
  */
13
15
  export function assertThrows(fn, expectedMessage) {
16
+ let returned;
14
17
  try {
15
- fn();
18
+ returned = fn();
16
19
  }
17
20
  catch (error) {
18
21
  if (expectedMessage !== undefined) {
@@ -23,8 +26,21 @@ export function assertThrows(fn, expectedMessage) {
23
26
  }
24
27
  return error instanceof Error ? error : new Error(String(error));
25
28
  }
29
+ if (isThenable(returned)) {
30
+ // An async function never throws synchronously; its failure is a
31
+ // rejection. Reporting "did not throw" was misleading, and the
32
+ // rejected promise nobody awaited surfaced as an unhandled rejection
33
+ // blamed on whichever test happened to be running.
34
+ void returned.then(undefined, () => undefined);
35
+ throw new Error("assertThrows received a function that returned a promise; use assertRejects for async functions.");
36
+ }
26
37
  throw new Error("Expected function to throw, but it did not.");
27
38
  }
39
+ function isThenable(value) {
40
+ return (value !== null &&
41
+ (typeof value === "object" || typeof value === "function") &&
42
+ typeof value.then === "function");
43
+ }
28
44
  /**
29
45
  * Asserts that an async function rejects.
30
46
  *
@@ -75,8 +91,17 @@ export function assertErrorMetadata(error, key, value) {
75
91
  }
76
92
  const baseError = error;
77
93
  const actual = baseError.metadata?.[key];
78
- if (actual !== value) {
79
- throw new Error(`Expected error metadata "${key}" to be "${value}", got "${actual}".`);
94
+ const difference = findDifference(actual, value, `metadata.${key}`);
95
+ if (difference) {
96
+ throw new Error(`Expected error metadata "${key}" to be ${describeValue(value)}, got ${describeValue(actual)}.`);
97
+ }
98
+ }
99
+ /**
100
+ * Asserts that an error is an instance of a specific class.
101
+ */
102
+ export function assertErrorType(error, type) {
103
+ if (!(error instanceof type)) {
104
+ throw new Error(`Expected error to be a ${type.name}, got ${error instanceof Error ? error.constructor.name : typeof error}.`);
80
105
  }
81
106
  }
82
107
  //# 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