@yabbadabbadev/pepito 0.1.0 → 0.1.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/CHANGELOG.md +10 -0
- package/README.md +24 -204
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.1](https://github.com/yabbadabbadev/pepito/compare/v0.1.0...v0.1.1) (2026-08-20)
|
|
4
|
+
|
|
5
|
+
Nothing changed for consumers of this package: no new API, no fix, no
|
|
6
|
+
behaviour change. `0.1.1` exists to exercise the publishing path end to end,
|
|
7
|
+
and it is the first release published from a public repository, so it is the
|
|
8
|
+
first to carry a provenance attestation.
|
|
9
|
+
|
|
10
|
+
If you are on `0.1.0`, there is no reason to upgrade beyond wanting the
|
|
11
|
+
attestation.
|
|
12
|
+
|
|
3
13
|
## 0.1.0 — 2026-08-14
|
|
4
14
|
|
|
5
15
|
First release: `setupNetwork`, `mount`, request descriptors (`get`, `post`,
|
package/README.md
CHANGED
|
@@ -6,11 +6,6 @@ doesn't abstract MSW — handlers are still declared with the usual
|
|
|
6
6
|
`http.get(...)` — it only provides the startup, the traffic registry and the
|
|
7
7
|
matchers to query it.
|
|
8
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
9
|
## 1. Install and start
|
|
15
10
|
|
|
16
11
|
`react`, `react-dom`, `vitest`, `msw` and `vitest-browser-react` are
|
|
@@ -129,6 +124,16 @@ await expect.element(screen.getByText('hash: #detail')).toBeVisible()
|
|
|
129
124
|
|
|
130
125
|
## 3. Assert a request
|
|
131
126
|
|
|
127
|
+
The five matchers, at a glance:
|
|
128
|
+
|
|
129
|
+
| Matcher | What it asserts |
|
|
130
|
+
| --------------------------- | ----------------------------------------------------------------------- |
|
|
131
|
+
| `toHaveBeenRequested` | The application made the request |
|
|
132
|
+
| `toHaveBeenRequestedTimes` | Exactly `count` matching requests were made |
|
|
133
|
+
| `toHaveBeenIntercepted` | One of your handlers produced the response |
|
|
134
|
+
| `toHaveRespondedWith` | The intercepted response has the expected status/body |
|
|
135
|
+
| `toHaveNoUnhandledRequests` | No observed request was left without a handler (via `expect.network()`) |
|
|
136
|
+
|
|
132
137
|
```ts
|
|
133
138
|
import { get, post } from '@yabbadabbadev/pepito'
|
|
134
139
|
|
|
@@ -199,14 +204,8 @@ happened.
|
|
|
199
204
|
|
|
200
205
|
## 4. Intercepted or escaped
|
|
201
206
|
|
|
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
|
|
207
|
+
`toHaveBeenRequested` and `toHaveBeenIntercepted` assert different things
|
|
208
|
+
(see the table in section 3). A handler with `passthrough()` satisfies the first and not the second: the
|
|
210
209
|
response came from the real network, not from your mock.
|
|
211
210
|
|
|
212
211
|
```ts
|
|
@@ -315,198 +314,19 @@ the product and are tested like any other output (see `CONTRIBUTING.md`).
|
|
|
315
314
|
|
|
316
315
|
## 7. Recipes
|
|
317
316
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
})
|
|
317
|
+
Task-oriented answers for what `pepito` deliberately doesn't wrap in its own
|
|
318
|
+
API: chaining handlers for successive responses, seeding storage before
|
|
319
|
+
mounting, the Mothers pattern for fixtures, cross-origin handlers, and
|
|
320
|
+
waiting for the network to settle before a screenshot. See
|
|
321
|
+
[`docs/recipes.md`](docs/recipes.md).
|
|
390
322
|
|
|
391
|
-
|
|
392
|
-
await fetch('/api/products')
|
|
393
|
-
await fetch('/api/products')
|
|
323
|
+
## About the name
|
|
394
324
|
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
```
|
|
325
|
+
Published on npm as `@yabbadabbadev/pepito`; `pepito` remains the project's
|
|
326
|
+
code-name and this directory's name. See `CONTRIBUTING.md` to run the tests
|
|
327
|
+
and publish a version, and `ROADMAP.md` for what's out of scope for this
|
|
328
|
+
version.
|
|
483
329
|
|
|
484
|
-
##
|
|
330
|
+
## License
|
|
485
331
|
|
|
486
|
-
|
|
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
|
-
```
|
|
332
|
+
MIT, see [`LICENSE`](LICENSE).
|
package/package.json
CHANGED