@yabbadabbadev/pepito 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-08-14
4
+
5
+ First release: `setupNetwork`, `mount`, request descriptors (`get`, `post`,
6
+ `put`, `patch`, `del`, `query`, `request`) and the matchers
7
+ `toHaveBeenRequested`, `toHaveBeenRequestedTimes`, `toHaveBeenIntercepted`,
8
+ `toHaveRespondedWith` and `toHaveNoUnhandledRequests`, plus `network.log()`
9
+ and `network.idle()`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yabbadabbadev
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 ADDED
@@ -0,0 +1,512 @@
1
+ # pepito
2
+
3
+ Network test utilities for Vitest browser mode: mount the application with
4
+ `mount` and assert on the traffic observed by MSW with `expect` matchers. It
5
+ doesn't abstract MSW — handlers are still declared with the usual
6
+ `http.get(...)` — it only provides the startup, the traffic registry and the
7
+ matchers to query it.
8
+
9
+ Published on npm as `@yabbadabbadev/pepito`; `pepito` remains the project's
10
+ code-name and this directory's name. See `CONTRIBUTING.md` to run the tests
11
+ and publish a version, and `ROADMAP.md` for what's out of scope for this
12
+ version.
13
+
14
+ ## 1. Install and start
15
+
16
+ `react`, `react-dom`, `vitest`, `msw` and `vitest-browser-react` are
17
+ **peerDependencies**: install them if your project doesn't already have
18
+ them.
19
+
20
+ ```bash
21
+ npm i -D @yabbadabbadev/pepito msw vitest-browser-react
22
+ npx msw init public --save
23
+ ```
24
+
25
+ `npx msw init public --save` generates the service worker MSW needs in
26
+ browser mode (`public/mockServiceWorker.js`); it's a one-off, not a
27
+ day-to-day step — it's only repeated when upgrading MSW's version.
28
+
29
+ Call `setupNetwork` once, in a `setupFiles` file of `vitest.config` — never
30
+ inside a test:
31
+
32
+ ```ts
33
+ // vitest.setup.ts
34
+ import { setupNetwork } from '@yabbadabbadev/pepito'
35
+ import { handlers } from './handlers'
36
+
37
+ setupNetwork(handlers)
38
+ ```
39
+
40
+ ```ts
41
+ // vitest.config.ts
42
+ export default defineConfig({
43
+ test: {
44
+ setupFiles: ['./vitest.setup.ts'],
45
+ browser: {/* … */},
46
+ },
47
+ })
48
+ ```
49
+
50
+ `setupNetwork`'s second argument passes straight through to
51
+ `worker.start()`, with no wrapper of its own — for example, to make a
52
+ request with no handler fail the test instead of just warning on the
53
+ console:
54
+
55
+ ```ts
56
+ // vitest.setup.ts
57
+ import { setupNetwork } from '@yabbadabbadev/pepito'
58
+ import { handlers } from './handlers'
59
+
60
+ setupNetwork(handlers, { onUnhandledRequest: 'error' })
61
+ ```
62
+
63
+ Importing anything from `pepito` — here, `setupNetwork` — already brings the
64
+ network matchers along as `expect` types: there's no separate type
65
+ registration. If your test `tsconfig` doesn't include the setup file, `tsc`
66
+ won't see the augmentation and `expect(...).toHaveBeenRequested()` will
67
+ raise `TS2339` even though the test passes at runtime.
68
+
69
+ ## 2. Mount the application
70
+
71
+ `mount` mounts with `vitest-browser-react` and returns its `screen`
72
+ unwrapped. It requires `setupNetwork` to have run first (section 1), **even
73
+ for a test with no network**: the coupling is deliberate — `mount` also
74
+ installs URL and storage cleanup between tests, not just the network — and
75
+ if it's missing, it fails immediately with a fix instruction.
76
+
77
+ ```tsx
78
+ import { http, HttpResponse } from 'msw'
79
+ import { mount, get } from '@yabbadabbadev/pepito'
80
+ import { App } from '../src/App'
81
+ import { ProductListMother } from '../test/mothers/product-list-mother'
82
+
83
+ test('the catalog page renders the URL filter', async () => {
84
+ const screen = await mount(<App />, {
85
+ path: '/products?filter=bread',
86
+ network: [
87
+ http.get('/api/products', () =>
88
+ HttpResponse.json(ProductListMother.catalog()),
89
+ ),
90
+ ],
91
+ })
92
+
93
+ await expect.element(screen.getByText('filter: bread')).toBeVisible()
94
+ await expect(get('/api/products')).toHaveBeenRequested()
95
+ })
96
+ ```
97
+
98
+ `path` is a **complete** same-origin URI starting with `/` — query and hash
99
+ included — because your application's router (`BrowserRouter` or whichever)
100
+ reads it from the document's real URL, not from a `MemoryRouter`. A
101
+ different origin isn't a valid `path`: it's mocked in the `handlers`, not in
102
+ the mount — see section 7.
103
+
104
+ `network` are this test's own MSW handlers: they're installed with
105
+ `worker.use()` before render, so they win over the suite's for the same
106
+ route, and `setupNetwork()` undoes them afterwards in its `afterEach`.
107
+ Neither option is required — `mount(<App />)` on its own just mounts:
108
+
109
+ ```tsx
110
+ import { mount } from '@yabbadabbadev/pepito'
111
+ import { App } from '../src/App'
112
+
113
+ test('mounts with no path or network of its own', async () => {
114
+ const screen = await mount(<App />)
115
+
116
+ await expect.element(screen.getByText('Product catalog')).toBeVisible()
117
+ })
118
+ ```
119
+
120
+ `path` can carry query and hash together, because both flow through the
121
+ router the same as the rest of the URI:
122
+
123
+ ```tsx
124
+ const screen = await mount(<App />, { path: '/products?filter=bread#detail' })
125
+
126
+ await expect.element(screen.getByText('filter: bread')).toBeVisible()
127
+ await expect.element(screen.getByText('hash: #detail')).toBeVisible()
128
+ ```
129
+
130
+ ## 3. Assert a request
131
+
132
+ ```ts
133
+ import { get, post } from '@yabbadabbadev/pepito'
134
+
135
+ await expect(get('/api/products')).toHaveBeenRequested()
136
+ await expect(get('/api/products')).toHaveBeenRequestedTimes(2)
137
+ await expect(post('/api/products')).not.toHaveBeenRequested()
138
+ ```
139
+
140
+ `get`, `post`, `put`, `patch`, `del` and `query` describe the expected
141
+ request — all six are shortcuts for `request(method, path)` with the method
142
+ already fixed. For any other method use `request(method, path)` directly:
143
+ it's the escape hatch that covers even the ones MSW 2.15 still doesn't
144
+ expose as a handler helper:
145
+
146
+ ```ts
147
+ import { del, patch, put, query, request } from '@yabbadabbadev/pepito'
148
+
149
+ await expect(
150
+ put('/api/products/1', { body: { product_name: 'Whole milk' } }),
151
+ ).toHaveBeenRequested()
152
+ await expect(
153
+ patch('/api/products/1', { body: { stock: 3 } }),
154
+ ).toHaveBeenRequested()
155
+ await expect(del('/api/products/1')).toHaveBeenRequested()
156
+ await expect(
157
+ query('/api/products', { searchParams: { filter: 'bread' } }),
158
+ ).toHaveBeenRequested()
159
+ await expect(request('OPTIONS', '/api/products')).toHaveBeenRequested()
160
+ ```
161
+
162
+ Body and `searchParams` match by **subset**:
163
+
164
+ ```ts
165
+ import { get, post } from '@yabbadabbadev/pepito'
166
+
167
+ await expect(
168
+ get('/api/products', { searchParams: { filter: 'bread' } }),
169
+ ).toHaveBeenRequested()
170
+ await expect(
171
+ post('/api/products', { body: { product_name: 'Whole milk' } }),
172
+ ).toHaveBeenRequested()
173
+ ```
174
+
175
+ `post('/api/products', { body: { product_name: 'Whole milk' } })` matches
176
+ even if the real request also carries `id`. Pass `{ exact: true }` when you
177
+ need strict equality of the whole object, not just the keys you list in
178
+ `body`:
179
+
180
+ ```ts
181
+ await expect(
182
+ post('/api/products', {
183
+ body: { product_name: 'Whole milk' },
184
+ exact: true,
185
+ }),
186
+ ).toHaveBeenRequested()
187
+ ```
188
+
189
+ A `searchParams` key repeated in the real URL (`?tag=a&tag=b`) collapses to
190
+ its last value before comparing — the matcher can't tell that request apart
191
+ from one where the key appears only once.
192
+
193
+ The matchers retry because a request is an effect that follows the
194
+ interaction, just like `expect.element` — there's no need to wrap them in a
195
+ `waitFor`. `.not.toHaveBeenRequested()` and `toHaveBeenRequestedTimes` are
196
+ the exception: before deciding, they wait for the network to settle, so as
197
+ not to confuse a request that hasn't arrived yet with one that never
198
+ happened.
199
+
200
+ ## 4. Intercepted or escaped
201
+
202
+ `toHaveBeenRequested` and `toHaveBeenIntercepted` assert different things:
203
+
204
+ | Matcher | What it asserts |
205
+ | ----------------------- | ------------------------------------------ |
206
+ | `toHaveBeenRequested` | The application made the request |
207
+ | `toHaveBeenIntercepted` | One of your handlers produced the response |
208
+
209
+ A handler with `passthrough()` satisfies the first and not the second: the
210
+ response came from the real network, not from your mock.
211
+
212
+ ```ts
213
+ import { http, passthrough } from 'msw'
214
+ import { get } from '@yabbadabbadev/pepito'
215
+
216
+ // handler: http.get('/api/legacy', () => passthrough())
217
+
218
+ await fetch('/api/legacy')
219
+
220
+ await expect(get('/api/legacy')).toHaveBeenRequested() // passes
221
+ await expect(get('/api/legacy')).toHaveBeenIntercepted() // fails
222
+ ```
223
+
224
+ To catch what doesn't even have a handler, the suite-wide guardrail:
225
+
226
+ ```ts
227
+ await expect.network().toHaveNoUnhandledRequests()
228
+ ```
229
+
230
+ `toHaveNoUnhandledRequests` hangs off `expect.network()`, not off a request
231
+ descriptor, because it doesn't describe one specific request but all the
232
+ observed traffic.
233
+
234
+ ## 5. What the mock responded
235
+
236
+ ```ts
237
+ import { get } from '@yabbadabbadev/pepito'
238
+
239
+ await expect(get('/api/products')).toHaveRespondedWith(500)
240
+
241
+ await expect(get('/api/products')).toHaveRespondedWith({
242
+ status: 200,
243
+ body: { total: 2 },
244
+ })
245
+ ```
246
+
247
+ A bare number is the shorthand for `{ status }`. The `body`, if given,
248
+ matches by subset the same way as in request descriptors — `{ exact: true }`
249
+ for strict equality:
250
+
251
+ ```ts
252
+ import { get } from '@yabbadabbadev/pepito'
253
+ import { ProductListMother } from '../test/mothers/product-list-mother'
254
+
255
+ await expect(get('/api/products')).toHaveRespondedWith({
256
+ status: 200,
257
+ body: ProductListMother.catalog(),
258
+ exact: true,
259
+ })
260
+ ```
261
+
262
+ `toHaveRespondedWith` also requires the request to have been intercepted: a
263
+ real response via `passthrough()` never counts, even if the status happens
264
+ to match.
265
+
266
+ ## 6. Debug a failure
267
+
268
+ Failure messages carry the full observed traffic and a colored diff
269
+ (Vitest's `this.utils`, the same one native matchers use — no new
270
+ dependencies):
271
+
272
+ ```
273
+ expect(received).toHaveBeenRequested(expected)
274
+
275
+ Expected: Object {
276
+ "body": undefined,
277
+ "method": "GET",
278
+ "path": "/api/products",
279
+ "searchParams": Object {
280
+ "filter": "chocolate",
281
+ },
282
+ }
283
+
284
+ - Expected
285
+ + Received
286
+
287
+ {
288
+ "body": undefined,
289
+ "searchParams": {
290
+ - "filter": "chocolate",
291
+ + "filter": "bread",
292
+ },
293
+ }
294
+
295
+ Observed traffic:
296
+ GET /api/products?filter=bread → 200 [matched/mocked]
297
+ ```
298
+
299
+ (real output, captured without color; in your terminal `- Expected`/`+
300
+ Received` arrive in green and red)
301
+
302
+ To look at the traffic without anything failing, in the middle of a test
303
+ you're debugging:
304
+
305
+ ```ts
306
+ import { network } from '@yabbadabbadev/pepito'
307
+
308
+ await fetch('/api/products')
309
+ await network.log() // dumps method, path, body and status to the console
310
+ ```
311
+
312
+ If a failure message doesn't explain what you expected, that's a matcher
313
+ defect, not something to work around by hand: failure messages are part of
314
+ the product and are tested like any other output (see `CONTRIBUTING.md`).
315
+
316
+ ## 7. Recipes
317
+
318
+ **Different responses across successive calls.** No `pepito` API is needed
319
+ for this: it's plain MSW, with chained handlers. The first one that matches
320
+ wins, and if it carries `{ once: true }` it deactivates after that first
321
+ time, so the request falls through to the next handler in the list:
322
+
323
+ ```tsx
324
+ import { http, HttpResponse } from 'msw'
325
+ import { get, mount } from '@yabbadabbadev/pepito'
326
+ import { App } from '../src/App'
327
+ import { ProductListMother } from '../test/mothers/product-list-mother'
328
+
329
+ test('the second visit already sees the cached catalog', async () => {
330
+ await mount(<App />, {
331
+ network: [
332
+ http.get(
333
+ '/api/products',
334
+ () => HttpResponse.json(ProductListMother.empty()),
335
+ { once: true },
336
+ ),
337
+ http.get('/api/products', () =>
338
+ HttpResponse.json(ProductListMother.catalog()),
339
+ ),
340
+ ],
341
+ })
342
+
343
+ await fetch('/api/products') // 1st: the once handler responds and is spent
344
+ await fetch('/api/products') // 2nd: falls through to the handler below
345
+ await fetch('/api/products') // 3rd: the one below isn't once, keeps responding
346
+
347
+ await expect(get('/api/products')).toHaveBeenRequestedTimes(3)
348
+ await expect(get('/api/products')).toHaveRespondedWith({
349
+ status: 200,
350
+ body: ProductListMother.empty(),
351
+ })
352
+ await expect(get('/api/products')).toHaveRespondedWith({
353
+ status: 200,
354
+ body: ProductListMother.catalog(),
355
+ })
356
+ })
357
+ ```
358
+
359
+ Verified in this repo: first call → empty catalog, second and third → full
360
+ catalog (X→Y→Y). `setupNetwork()` undoes the test handlers in its
361
+ `afterEach` with `worker.resetHandlers()`, so every following test sees a
362
+ fresh `once` again, with no exhaustion carrying over between tests.
363
+
364
+ Each `toHaveRespondedWith` above finds its own entry in the registry — one
365
+ matches the empty response, the other the catalog — regardless of the order
366
+ the `expect`s are written in. What `pepito` doesn't have yet is a matcher
367
+ that asserts the ORDER between two identical requests (for example, "the
368
+ empty one before the catalog"): it's in `ROADMAP.md`, because the registry
369
+ is already chronological and only needs exposing.
370
+
371
+ For sequences odder than `once` can express well (three different
372
+ responses, or a condition that isn't "the first time"), a counter closure
373
+ declared inside the test itself is the alternative — with no changes to
374
+ `pepito`:
375
+
376
+ ```ts
377
+ test('three different responses for the same route', async () => {
378
+ let callCount = 0
379
+
380
+ await mount(<App />, {
381
+ network: [
382
+ http.get('/api/products', () => {
383
+ callCount += 1
384
+ if (callCount === 1) return HttpResponse.json([], { status: 202 })
385
+ if (callCount === 2) return HttpResponse.json([], { status: 500 })
386
+ return HttpResponse.json(ProductListMother.catalog())
387
+ }),
388
+ ],
389
+ })
390
+
391
+ await fetch('/api/products')
392
+ await fetch('/api/products')
393
+ await fetch('/api/products')
394
+
395
+ await expect(get('/api/products')).toHaveRespondedWith(202)
396
+ await expect(get('/api/products')).toHaveRespondedWith(500)
397
+ })
398
+ ```
399
+
400
+ **Seeding `localStorage`/cookies before mounting.** The browser's storage is
401
+ real; no `mount` option is needed for this, a plain `setItem` before `await
402
+ mount(...)` is enough:
403
+
404
+ ```ts
405
+ test('the catalog respects the saved filter', async () => {
406
+ localStorage.setItem('favoriteFilter', 'bread')
407
+ document.cookie = 'session=abc'
408
+
409
+ const screen = await mount(<App />)
410
+
411
+ await expect.element(screen.getByText('filter: bread')).toBeVisible()
412
+ })
413
+ ```
414
+
415
+ **Fixtures with Mothers.** Mocked response payloads follow the house's
416
+ Mother/Builder pattern — each Mother models the shape of one endpoint's
417
+ response, with named factories for its variants; no loose literals and no
418
+ `structuredClone` with mutation, not even in examples copied from here:
419
+
420
+ ```ts
421
+ // test/mothers/product-list-mother.ts
422
+ interface ProductResponse {
423
+ id: number
424
+ product_name: string
425
+ }
426
+
427
+ const milk = { id: 1, product_name: 'Whole milk' } satisfies ProductResponse
428
+ const bread = { id: 2, product_name: 'Country bread' } satisfies ProductResponse
429
+
430
+ export const ProductListMother = {
431
+ catalog: (): ProductResponse[] => [milk, bread],
432
+ empty: (): ProductResponse[] => [],
433
+ }
434
+ ```
435
+
436
+ ```ts
437
+ http.get('/api/products', () => HttpResponse.json(ProductListMother.catalog()))
438
+ ```
439
+
440
+ An incidental payload that isn't an entity (`{ ok: true }`) doesn't need a
441
+ Mother. `pepito` doesn't export fixture infrastructure — Mothers belong to
442
+ each project's own domain.
443
+
444
+ **The API's host goes in the handler, not in the configuration.** There's no
445
+ `defaultHost`: the service worker intercepts cross-origin the same as
446
+ same-origin, so the host is a detail of the handler's URL.
447
+
448
+ ```ts
449
+ http.get('https://api.example.com/products', () =>
450
+ HttpResponse.json(ProductListMother.catalog()),
451
+ )
452
+ // or with a wildcard:
453
+ http.get('*/products', () => HttpResponse.json(ProductListMother.catalog()))
454
+ ```
455
+
456
+ `mount({ path })`, by contrast, does require same-origin — a different
457
+ origin there throws with an instruction, because `path` moves the test
458
+ document's URL, not the request you want to mock.
459
+
460
+ **Waiting for the network to settle before capturing.**
461
+ `toMatchScreenshot`'s native stabilizer waits for the frame to stop
462
+ changing, but a static `<p>Loading…</p>` is also a stable capture: without
463
+ waiting for the network, the baseline is the loading screen, not the real
464
+ content. Measured in the evaluation repo's
465
+ `docs/knowledge/regresion-visual-browser-mode.md`: 0 failures across 17
466
+ local runs + 3 in CI waiting for calm this way, with
467
+ baselines byte-identical to the ones anchored by `expect.element` to a
468
+ specific text. `network.idle()` waits with the same mechanism the network
469
+ matchers use, without asserting anything:
470
+
471
+ ```tsx
472
+ import { mount, network } from '@yabbadabbadev/pepito'
473
+ import { App } from '../src/App'
474
+
475
+ test('the catalog does not change visually', async () => {
476
+ const screen = await mount(<App />)
477
+
478
+ await network.idle()
479
+
480
+ await expect.element(screen.getByRole('main')).toMatchScreenshot('catalog')
481
+ })
482
+ ```
483
+
484
+ ## 8. Cheat sheet
485
+
486
+ ```ts
487
+ request('QUERY', '/api/products') // any method, including ones with no shortcut
488
+ get('/api/products', { searchParams: { filter: 'bread' } }) // subset of searchParams
489
+ post('/api/products', { body: { product_name: 'Whole milk' } }) // subset of body
490
+ put('/api/products/1', { body: { stock: 3 }, exact: true }) // strict equality
491
+ patch('/api/products/1', { body: { stock: 3 } })
492
+ del('/api/products/1')
493
+ query('/api/products', { searchParams: { filter: 'bread' } })
494
+
495
+ await expect(get('/api/products')).toHaveBeenRequested() // the app made the request
496
+ await expect(get('/api/products')).not.toHaveBeenRequested() // waits for calm first
497
+ await expect(get('/api/products')).toHaveBeenRequestedTimes(2) // exact count, network settled
498
+ await expect(get('/api/products')).toHaveBeenIntercepted() // one of your handlers responded
499
+ await expect(get('/api/products')).toHaveRespondedWith(500) // shorthand for { status: 500 }
500
+ await expect(get('/api/products')).toHaveRespondedWith({
501
+ status: 200,
502
+ body: { total: 2 }, // subset
503
+ })
504
+ await expect(get('/api/products')).toHaveRespondedWith({
505
+ status: 200,
506
+ body: { total: 2 },
507
+ exact: true, // strict equality
508
+ })
509
+ await expect.network().toHaveNoUnhandledRequests() // guardrail: nothing without a handler
510
+ await network.log() // dumps the observed traffic, without asserting
511
+ await network.idle() // waits for calm, without asserting — before capturing in visual regression
512
+ ```
@@ -0,0 +1,59 @@
1
+ import type { MatcherState } from '@vitest/expect';
2
+ import type { ExpectedResponse } from './matcher-types';
3
+ import type { RequestSpec } from './request-descriptors';
4
+ import type { ResolvedRequest } from './traffic-registry';
5
+ /** Dumps the observed traffic, one line per entry, to embed in a failure message. */
6
+ export declare function formatTraffic(traffic: ResolvedRequest[]): string;
7
+ /**
8
+ * Composes the network matchers' failure message: the matcher's hint, what
9
+ * was expected, a diff against the closest candidate (same method and path,
10
+ * even if it doesn't fully match) if one exists, and the full dump of the
11
+ * observed traffic.
12
+ */
13
+ export declare function requestFailureMessage(messageContext: {
14
+ utils: MatcherState['utils'];
15
+ matcherName: string;
16
+ spec: RequestSpec;
17
+ traffic: ResolvedRequest[];
18
+ isNot: boolean;
19
+ }): string;
20
+ /**
21
+ * Composes the `toHaveBeenRequestedTimes` failure message: there's no
22
+ * "closest candidate" to show like in `requestFailureMessage`, but an
23
+ * expected count against the one actually observed, plus the full dump.
24
+ */
25
+ export declare function requestCountFailureMessage(messageContext: {
26
+ utils: MatcherState['utils'];
27
+ spec: RequestSpec;
28
+ expectedCount: number;
29
+ foundCount: number;
30
+ traffic: ResolvedRequest[];
31
+ isNot: boolean;
32
+ }): string;
33
+ /**
34
+ * Composes the `toHaveNoUnhandledRequests` failure message: which requests
35
+ * arrived without a handler (method and path, the minimum needed to locate
36
+ * them in the code) followed by the full traffic dump, in case the missing
37
+ * handler becomes obvious when seen alongside the rest.
38
+ */
39
+ export declare function noUnhandledRequestsFailureMessage(messageContext: {
40
+ utils: MatcherState['utils'];
41
+ unhandledEntries: ResolvedRequest[];
42
+ traffic: ResolvedRequest[];
43
+ isNot: boolean;
44
+ }): string;
45
+ /**
46
+ * Composes the `toHaveRespondedWith` failure message: the matcher's hint,
47
+ * the expected response, a diff against the closest intercepted-and-matched
48
+ * candidate (even if it doesn't match on status or body) if one exists, and
49
+ * the full dump of the observed traffic. The candidate requires `matched &&
50
+ * mocked` because a passthrough entry can match the request's spec without
51
+ * ever having been intercepted: showing it as "close" would be misleading.
52
+ */
53
+ export declare function respondedWithFailureMessage(messageContext: {
54
+ utils: MatcherState['utils'];
55
+ spec: RequestSpec;
56
+ expected: ExpectedResponse;
57
+ traffic: ResolvedRequest[];
58
+ isNot: boolean;
59
+ }): string;