@klondikemarlen/expect-http-response 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -1,6 +1,81 @@
1
1
  # @klondikemarlen/expect-http-response
2
2
 
3
- One `expect` can describe an HTTP response contract: its status and complete body. This dependency-free ESM matcher works with Jest's `expect` and Vitest. Register it explicitly with your existing test runner.
3
+ Readable HTTP response failures for Jest and Vitest: focused status errors and contextual body diffs, without unrelated transport internals. One `expect` describes the status and complete body. This dependency-free ESM matcher is registered explicitly with your existing test runner.
4
+
5
+ ## Why This Matcher Exists
6
+
7
+ `toHaveHttpResponse` expresses the response status and complete body as one HTTP contract, with focused diagnostics when that contract fails. Separate status and body assertions provide a useful comparison:
8
+
9
+ ```js
10
+ expect(response.status).toEqual(200)
11
+ expect(response.body).toEqual(expectedBody)
12
+ ```
13
+
14
+ Those assertions keep an incorrect status obvious and give the body its own native diff. A stock matcher can express the combined contract too:
15
+
16
+ ```js
17
+ expect(response).toEqual(expect.objectContaining({ status: 200, body: expectedBody }))
18
+ ```
19
+
20
+ However, a failure against the whole Supertest response can include unrelated headers, request metadata, and raw response text. `toMatchObject` is not an equivalent complete-body replacement: its partial matching can accept unexpected body fields.
21
+
22
+ `toHaveHttpResponse` combines the status and body checks while keeping their diagnostics focused:
23
+
24
+ ```js
25
+ expect(response).toHaveHttpResponse(200, expectedBody)
26
+ ```
27
+
28
+ - A status-only failure reports the differing statuses without printing the matching body.
29
+ - A body failure uses the active runner's contextual diff, exposing changed nested values rather than dumping both collections or hiding them as `[Object]`.
30
+ - If both differ, both failures are reported; separate assertions normally stop at the first failed status assertion.
31
+ - Failed negation explains that status and body both matched without printing the matching payload twice.
32
+
33
+ For example, a changed field among 100 response records remains visible in the diff (excerpt):
34
+
35
+ ```diff
36
+ "id": 50,
37
+ - "name": "Expected record 50",
38
+ + "name": "Record 50",
39
+ ```
40
+
41
+ This changes the diagnostic boundary, not the equality policy. Complete-body comparison, deliberate asymmetric matches, and the runner's custom equality testers retain their existing meaning.
42
+
43
+ ### Example: Whole-Response Failure
44
+
45
+ In a real controller test, the expected title of a returned notice was deliberately changed. On Vitest 4.1.11, the whole-response `toEqual(expect.objectContaining(...))` assertion above produced the following selected diff lines. Domain names and fixture values are anonymized; `...` marks omitted output:
46
+
47
+ ```diff
48
+ - "body": {
49
+ - "notices": [
50
+ - ObjectContaining {
51
+ - "id": 1,
52
+ - "title": "Scheduled maintenance",
53
+ ...
54
+ + "header": {
55
+ + "content-type": "application/json; charset=utf-8",
56
+ ...
57
+ + "req": {
58
+ + "method": "GET",
59
+ + "url": "http://127.0.0.1:<port>/notices",
60
+ ...
61
+ + "text": "{\"notices\":[...\"title\":\"Active notice\"...]}",
62
+ ```
63
+
64
+ The expected title appears under `body`, but the received title is embedded in escaped response text among unrelated transport details.
65
+
66
+ Using the same expected body with `toHaveHttpResponse` produced these selected lines instead:
67
+
68
+ ```diff
69
+ Body mismatch:
70
+ - Expected
71
+ + Received
72
+ ...
73
+ - "title": "Scheduled maintenance",
74
+ ...
75
+ + "title": "Active notice",
76
+ ```
77
+
78
+ Both titles are now part of the body comparison, without headers, request metadata, or raw response text. Other body fields can still appear according to the runner's native asymmetric-match formatting.
4
79
 
5
80
  ## Installation
6
81
 
@@ -74,6 +149,8 @@ export default defineConfig({
74
149
 
75
150
  Import the matcher from the package root and extend the active runner's `expect` instance explicitly. The matcher relies on the runner's equality function and custom matcher context; it does not bundle or select a runner.
76
151
 
152
+ Focused failures use the runner's `utils.printDiffOrStringify`, which Jest and Vitest supply through `expect.extend`. Other compatible runners need a Jest- or Vitest-compatible version of that utility for contextual diffs; without it, only basic expected/received rendering is available.
153
+
77
154
  ## TypeScript
78
155
 
79
156
  The package root resolves to `index.d.mts`. Add a declaration augmentation for each runner you actually install. For Jest's `expect` package:
@@ -174,17 +251,17 @@ npm test
174
251
  npm pack --dry-run
175
252
  git switch main
176
253
  git pull --ff-only origin main
177
- git tag v1.0.0
178
- git push origin v1.0.0
179
- gh release create v1.0.0 --title v1.0.0 --generate-notes
254
+ git tag v1.0.2
255
+ git push origin v1.0.2
256
+ gh release create v1.0.2 --title v1.0.2 --generate-notes
180
257
  npm publish --access public
181
- npm view @klondikemarlen/expect-http-response@1.0.0 version
258
+ npm view @klondikemarlen/expect-http-response@1.0.2 version
182
259
  ```
183
260
 
184
261
  In a separate consumer project, install the registry artifact:
185
262
 
186
263
  ```sh
187
- npm install --save-dev @klondikemarlen/expect-http-response@1.0.0
264
+ npm install --save-dev @klondikemarlen/expect-http-response@1.0.2
188
265
  ```
189
266
 
190
267
  After publishing, use the actual registry installation in a fresh consumer project and run its matcher and TypeScript registration checks. Do not claim npm availability until the publish and registry install have been observed.
package/index.d.mts CHANGED
@@ -12,7 +12,22 @@ interface HttpResponseLike {
12
12
  readonly body?: unknown
13
13
  }
14
14
 
15
+ type JestBodyFormatter = (
16
+ expected: unknown,
17
+ received: unknown,
18
+ expectedLabel: string,
19
+ receivedLabel: string,
20
+ expand: boolean
21
+ ) => string
22
+
23
+ type VitestBodyFormatter = (
24
+ received: unknown,
25
+ expected: unknown,
26
+ options?: { expand?: boolean }
27
+ ) => string | undefined
28
+
15
29
  interface MatcherUtils {
30
+ printDiffOrStringify?: JestBodyFormatter | VitestBodyFormatter
16
31
  matcherHint?: (
17
32
  matcherName: string,
18
33
  received?: string,
package/index.mjs CHANGED
@@ -58,10 +58,8 @@ function formatMessage(context, result) {
58
58
  hint,
59
59
  "",
60
60
  "Expected the response not to match this HTTP contract:",
61
- `Expected status: ${printValue(context, "printExpected", result.expectedStatus)}`,
62
- `Received status: ${printValue(context, "printReceived", result.receivedStatus)}`,
63
- `Expected body: ${printValue(context, "printExpected", result.expectedBody)}`,
64
- `Received body: ${printValue(context, "printReceived", result.receivedBody)}`,
61
+ `Matched status: ${printValue(context, "printReceived", result.receivedStatus)}`,
62
+ "The status and body both matched.",
65
63
  ].join("\n")
66
64
  }
67
65
 
@@ -77,14 +75,36 @@ function formatMessage(context, result) {
77
75
  lines.push(
78
76
  "",
79
77
  "Body mismatch:",
80
- `Expected body: ${printValue(context, "printExpected", result.expectedBody)}`,
81
- `Received body: ${printValue(context, "printReceived", result.receivedBody)}`
78
+ formatBodyDifference(context, result.expectedBody, result.receivedBody)
82
79
  )
83
80
  }
84
81
 
85
82
  return lines.join("\n")
86
83
  }
87
84
 
85
+ function formatBodyDifference(context, expectedBody, receivedBody) {
86
+ const formatter = context.utils?.printDiffOrStringify
87
+ let difference
88
+
89
+ if (typeof formatter === "function") {
90
+ // Jest uses five positional arguments; Vitest uses three or four, with reversed values.
91
+ if (formatter.length >= 5) {
92
+ difference = formatter(expectedBody, receivedBody, "Expected body", "Received body", false)
93
+ } else {
94
+ difference = formatter(receivedBody, expectedBody, { expand: false })
95
+ }
96
+ }
97
+
98
+ if (typeof difference === "string") {
99
+ return difference
100
+ }
101
+
102
+ return [
103
+ `Expected body: ${printValue(context, "printExpected", expectedBody)}`,
104
+ `Received body: ${printValue(context, "printReceived", receivedBody)}`,
105
+ ].join("\n")
106
+ }
107
+
88
108
  function matcherHint(context) {
89
109
  if (typeof context.utils?.matcherHint === "function") {
90
110
  return context.utils.matcherHint(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@klondikemarlen/expect-http-response",
3
- "version": "1.0.0",
4
- "description": "One-expect HTTP response contracts for Jest and Vitest",
3
+ "version": "1.0.2",
4
+ "description": "Focused HTTP response failure diagnostics for Jest and Vitest",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {