@klondikemarlen/expect-http-response 1.0.0 → 1.0.1

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,44 @@
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
+ The goal is readable failures, not satisfying an assertion-count quota. Separate status and body assertions are a useful baseline:
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` keeps the focused diagnostics of the two-expect approach while naming one HTTP response contract:
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.
4
42
 
5
43
  ## Installation
6
44
 
@@ -74,6 +112,8 @@ export default defineConfig({
74
112
 
75
113
  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
114
 
115
+ 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.
116
+
77
117
  ## TypeScript
78
118
 
79
119
  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 +214,17 @@ npm test
174
214
  npm pack --dry-run
175
215
  git switch main
176
216
  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
217
+ git tag v1.0.1
218
+ git push origin v1.0.1
219
+ gh release create v1.0.1 --title v1.0.1 --generate-notes
180
220
  npm publish --access public
181
- npm view @klondikemarlen/expect-http-response@1.0.0 version
221
+ npm view @klondikemarlen/expect-http-response@1.0.1 version
182
222
  ```
183
223
 
184
224
  In a separate consumer project, install the registry artifact:
185
225
 
186
226
  ```sh
187
- npm install --save-dev @klondikemarlen/expect-http-response@1.0.0
227
+ npm install --save-dev @klondikemarlen/expect-http-response@1.0.1
188
228
  ```
189
229
 
190
230
  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.1",
4
+ "description": "Focused HTTP response failure diagnostics for Jest and Vitest",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {