@zudojs/testing 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -15
- package/dist/assertions/deepEqual.core.d.ts +25 -0
- package/dist/assertions/deepEqual.core.js +164 -0
- package/dist/assertions/deepEqual.describe.d.ts +19 -0
- package/dist/assertions/deepEqual.describe.js +59 -0
- package/dist/assertions/errorAssertions.core.d.ts +4 -0
- package/dist/assertions/errorAssertions.core.js +13 -2
- package/dist/assertions/eventAssertions.core.d.ts +12 -0
- package/dist/assertions/eventAssertions.core.js +26 -4
- package/dist/assertions/httpAssertions.core.d.ts +12 -1
- package/dist/assertions/httpAssertions.core.js +28 -6
- package/dist/assertions/index.d.ts +7 -6
- package/dist/assertions/index.js +6 -6
- package/dist/cleanupManager/cleanupManager.core.d.ts +4 -0
- package/dist/cleanupManager/cleanupManager.core.js +5 -2
- package/dist/mocking/mockFn.core.d.ts +4 -1
- package/dist/mocking/mockFn.core.js +33 -41
- package/dist/mocking/spy.core.d.ts +11 -2
- package/dist/mocking/spy.core.js +54 -25
- package/dist/mocking/stub.core.d.ts +9 -6
- package/dist/mocking/stub.core.js +49 -12
- package/dist/spyLogger/index.d.ts +5 -4
- package/dist/spyLogger/index.js +3 -3
- package/dist/spyLogger/spyLogger.context.d.ts +19 -0
- package/dist/spyLogger/spyLogger.context.js +41 -0
- package/dist/spyLogger/spyLogger.core.d.ts +7 -1
- package/dist/spyLogger/spyLogger.core.js +10 -56
- package/dist/spyLogger/spyLogger.recording.d.ts +31 -0
- package/dist/spyLogger/spyLogger.recording.js +89 -0
- package/dist/testApplication/testApplication.core.js +6 -3
- package/dist/testClock/testClock.core.js +19 -2
- package/dist/testContext/testContext.recorder.core.js +3 -3
- package/dist/testEventBus/testEventBus.core.js +5 -2
- package/dist/testMessageBus/testMessageBus.core.js +5 -2
- package/dist/testQueue/testQueue.core.js +3 -1
- package/dist/testSerialization/serializationAssertions.core.js +13 -4
- package/dist/testStorage/testStorageMemory.core.d.ts +12 -4
- package/dist/testStorage/testStorageMemory.core.js +25 -12
- package/package.json +33 -26
- package/dist/.tsbuildinfo +0 -1
- package/dist/assertions/errorAssertions.core.d.ts.map +0 -1
- package/dist/assertions/errorAssertions.core.js.map +0 -1
- package/dist/assertions/eventAssertions.core.d.ts.map +0 -1
- package/dist/assertions/eventAssertions.core.js.map +0 -1
- package/dist/assertions/httpAssertions.core.d.ts.map +0 -1
- package/dist/assertions/httpAssertions.core.js.map +0 -1
- package/dist/assertions/index.d.ts.map +0 -1
- package/dist/assertions/index.js.map +0 -1
- package/dist/cleanupManager/cleanupManager.core.d.ts.map +0 -1
- package/dist/cleanupManager/cleanupManager.core.js.map +0 -1
- package/dist/cleanupManager/index.d.ts.map +0 -1
- package/dist/cleanupManager/index.js.map +0 -1
- package/dist/fixtures/eventFixtures.core.d.ts.map +0 -1
- package/dist/fixtures/eventFixtures.core.js.map +0 -1
- package/dist/fixtures/index.d.ts.map +0 -1
- package/dist/fixtures/index.js.map +0 -1
- package/dist/fixtures/messageFixtures.core.d.ts.map +0 -1
- package/dist/fixtures/messageFixtures.core.js.map +0 -1
- package/dist/httpTesting/httpRequest.core.d.ts.map +0 -1
- package/dist/httpTesting/httpRequest.core.js.map +0 -1
- package/dist/httpTesting/httpResponse.core.d.ts.map +0 -1
- package/dist/httpTesting/httpResponse.core.js.map +0 -1
- package/dist/httpTesting/httpResponse.helpers.d.ts.map +0 -1
- package/dist/httpTesting/httpResponse.helpers.js.map +0 -1
- package/dist/httpTesting/httpResponse.type.d.ts.map +0 -1
- package/dist/httpTesting/httpResponse.type.js.map +0 -1
- package/dist/httpTesting/httpStatusCode.type.d.ts.map +0 -1
- package/dist/httpTesting/httpStatusCode.type.js.map +0 -1
- package/dist/httpTesting/index.d.ts.map +0 -1
- package/dist/httpTesting/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mocking/index.d.ts.map +0 -1
- package/dist/mocking/index.js.map +0 -1
- package/dist/mocking/mockFn.core.d.ts.map +0 -1
- package/dist/mocking/mockFn.core.js.map +0 -1
- package/dist/mocking/spy.core.d.ts.map +0 -1
- package/dist/mocking/spy.core.js.map +0 -1
- package/dist/mocking/stub.core.d.ts.map +0 -1
- package/dist/mocking/stub.core.js.map +0 -1
- package/dist/spyLogger/index.d.ts.map +0 -1
- package/dist/spyLogger/index.js.map +0 -1
- package/dist/spyLogger/spyLogger.core.d.ts.map +0 -1
- package/dist/spyLogger/spyLogger.core.js.map +0 -1
- package/dist/spyLogger/spyLogger.type.d.ts.map +0 -1
- package/dist/spyLogger/spyLogger.type.js.map +0 -1
- package/dist/testApplication/index.d.ts.map +0 -1
- package/dist/testApplication/index.js.map +0 -1
- package/dist/testApplication/testApplication.core.d.ts.map +0 -1
- package/dist/testApplication/testApplication.core.js.map +0 -1
- package/dist/testClock/index.d.ts.map +0 -1
- package/dist/testClock/index.js.map +0 -1
- package/dist/testClock/testClock.core.d.ts.map +0 -1
- package/dist/testClock/testClock.core.js.map +0 -1
- package/dist/testConfig/index.d.ts.map +0 -1
- package/dist/testConfig/index.js.map +0 -1
- package/dist/testConfig/testConfig.core.d.ts.map +0 -1
- package/dist/testConfig/testConfig.core.js.map +0 -1
- package/dist/testContainer/index.d.ts.map +0 -1
- package/dist/testContainer/index.js.map +0 -1
- package/dist/testContainer/testContainer.core.d.ts.map +0 -1
- package/dist/testContainer/testContainer.core.js.map +0 -1
- package/dist/testContext/index.d.ts.map +0 -1
- package/dist/testContext/index.js.map +0 -1
- package/dist/testContext/testContext.core.d.ts.map +0 -1
- package/dist/testContext/testContext.core.js.map +0 -1
- package/dist/testContext/testContext.recorder.core.d.ts.map +0 -1
- package/dist/testContext/testContext.recorder.core.js.map +0 -1
- package/dist/testContext/testContext.recorder.type.d.ts.map +0 -1
- package/dist/testContext/testContext.recorder.type.js.map +0 -1
- package/dist/testEventBus/index.d.ts.map +0 -1
- package/dist/testEventBus/index.js.map +0 -1
- package/dist/testEventBus/testEventBus.core.d.ts.map +0 -1
- package/dist/testEventBus/testEventBus.core.js.map +0 -1
- package/dist/testMessageBus/index.d.ts.map +0 -1
- package/dist/testMessageBus/index.js.map +0 -1
- package/dist/testMessageBus/testMessageBus.core.d.ts.map +0 -1
- package/dist/testMessageBus/testMessageBus.core.js.map +0 -1
- package/dist/testQueue/index.d.ts.map +0 -1
- package/dist/testQueue/index.js.map +0 -1
- package/dist/testQueue/testQueue.core.d.ts.map +0 -1
- package/dist/testQueue/testQueue.core.js.map +0 -1
- package/dist/testSerialization/index.d.ts.map +0 -1
- package/dist/testSerialization/index.js.map +0 -1
- package/dist/testSerialization/serializationAssertions.core.d.ts.map +0 -1
- package/dist/testSerialization/serializationAssertions.core.js.map +0 -1
- package/dist/testStorage/index.d.ts.map +0 -1
- package/dist/testStorage/index.js.map +0 -1
- package/dist/testStorage/testStorageMemory.core.d.ts.map +0 -1
- package/dist/testStorage/testStorageMemory.core.js.map +0 -1
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
|
|
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 {
|
|
14
|
+
import {
|
|
15
|
+
createCleanupManager,
|
|
16
|
+
createMockFn,
|
|
17
|
+
createSpyLogger,
|
|
18
|
+
createTestClock,
|
|
19
|
+
createTestContainer,
|
|
20
|
+
} from "@zudojs/testing";
|
|
15
21
|
|
|
16
|
-
const container =
|
|
17
|
-
|
|
22
|
+
const container = createTestContainer({
|
|
23
|
+
overrides: [{ token: databaseToken, useValue: fakeDatabase }],
|
|
24
|
+
});
|
|
18
25
|
|
|
19
|
-
const logger =
|
|
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
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
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
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* HTTP, error, and event assertion utilities.
|
|
2
|
+
* @zudojs/testing — Assertions Barrel
|
|
5
3
|
*/
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
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
|
package/dist/assertions/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* HTTP, error, and event assertion utilities.
|
|
2
|
+
* @zudojs/testing — Assertions Barrel
|
|
5
3
|
*/
|
|
6
|
-
export {
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
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
|
|
46
|
-
throw new
|
|
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
|
-
|
|
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;
|