@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.
Files changed (3) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +24 -204
  3. 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
- **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
- })
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
- await fetch('/api/products')
392
- await fetch('/api/products')
393
- await fetch('/api/products')
323
+ ## About the name
394
324
 
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
- ```
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
- ## 8. Cheat sheet
330
+ ## License
485
331
 
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
- ```
332
+ MIT, see [`LICENSE`](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yabbadabbadev/pepito",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Network test utilities for Vitest browser mode: application mounting and matchers over the traffic observed by MSW",
5
5
  "type": "module",
6
6
  "license": "MIT",