@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.
- package/LICENSE +21 -0
- package/README.md +63 -15
- package/dist/assertions/deepEqual.core.d.ts +25 -0
- package/dist/assertions/deepEqual.core.js +194 -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 +28 -3
- 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 +24 -7
- package/dist/mocking/mockFn.core.d.ts +10 -1
- package/dist/mocking/mockFn.core.js +48 -39
- 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 +27 -0
- package/dist/spyLogger/spyLogger.context.js +44 -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 +100 -0
- package/dist/testApplication/testApplication.core.js +6 -3
- package/dist/testClock/testClock.core.js +24 -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 +19 -5
- package/dist/testStorage/testStorageMemory.core.d.ts +12 -4
- package/dist/testStorage/testStorageMemory.core.js +25 -12
- package/package.json +37 -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,83 @@
|
|
|
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`, 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
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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
|