@yabbadabbadev/pepito 0.1.0 → 0.2.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 +17 -0
- package/README.md +112 -226
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mount-core.d.ts +38 -0
- package/dist/mount-core.js +44 -0
- package/dist/network.d.ts +2 -2
- package/dist/network.js +2 -2
- package/dist/react.d.ts +28 -0
- package/dist/react.js +29 -0
- package/dist/setup-network.d.ts +2 -2
- package/dist/setup-network.js +2 -2
- package/dist/storage-cleanup.d.ts +2 -2
- package/dist/storage-cleanup.js +2 -2
- package/dist/svelte.d.ts +23 -0
- package/dist/svelte.js +25 -0
- package/dist/traffic-registry.d.ts +3 -3
- package/dist/traffic-registry.js +2 -2
- package/dist/vue.d.ts +23 -0
- package/dist/vue.js +25 -0
- package/package.json +20 -6
- package/dist/mount.d.ts +0 -45
- package/dist/mount.js +0 -61
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/yabbadabbadev/pepito/compare/v0.1.1...v0.2.0) (2026-09-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* framework-agnostic core + subpath adapters for /react, /vue, /svelte ([#9](https://github.com/yabbadabbadev/pepito/issues/9)) ([2a1ab74](https://github.com/yabbadabbadev/pepito/commit/2a1ab74274d8c8ac03980f368cde8260c7330f3a))
|
|
9
|
+
|
|
10
|
+
## [0.1.1](https://github.com/yabbadabbadev/pepito/compare/v0.1.0...v0.1.1) (2026-08-20)
|
|
11
|
+
|
|
12
|
+
Nothing changed for consumers of this package: no new API, no fix, no
|
|
13
|
+
behaviour change. `0.1.1` exists to exercise the publishing path end to end,
|
|
14
|
+
and it is the first release published from a public repository, so it is the
|
|
15
|
+
first to carry a provenance attestation.
|
|
16
|
+
|
|
17
|
+
If you are on `0.1.0`, there is no reason to upgrade beyond wanting the
|
|
18
|
+
attestation.
|
|
19
|
+
|
|
3
20
|
## 0.1.0 — 2026-08-14
|
|
4
21
|
|
|
5
22
|
First release: `setupNetwork`, `mount`, request descriptors (`get`, `post`,
|
package/README.md
CHANGED
|
@@ -6,28 +6,22 @@ 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
|
-
`
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
`vitest` and `msw` are **peerDependencies**: install them if your project
|
|
12
|
+
doesn't already have them.
|
|
13
|
+
|
|
14
|
+
### Core (any framework)
|
|
15
|
+
|
|
16
|
+
The core — `setupNetwork`, matchers, request descriptors, `network.log()` —
|
|
17
|
+
works without any framework adapter.
|
|
19
18
|
|
|
20
19
|
```bash
|
|
21
|
-
npm i -D @yabbadabbadev/pepito msw
|
|
20
|
+
npm i -D @yabbadabbadev/pepito msw
|
|
22
21
|
npx msw init public --save
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
`
|
|
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:
|
|
24
|
+
Call `setupNetwork` once, in a `setupFiles` file of `vitest.config`:
|
|
31
25
|
|
|
32
26
|
```ts
|
|
33
27
|
// vitest.setup.ts
|
|
@@ -47,6 +41,70 @@ export default defineConfig({
|
|
|
47
41
|
})
|
|
48
42
|
```
|
|
49
43
|
|
|
44
|
+
### React
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm i -D @yabbadabbadev/pepito msw vitest-browser-react
|
|
48
|
+
npx msw init public --save
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { mount } from '@yabbadabbadev/pepito/react'
|
|
53
|
+
import { App } from '../src/App'
|
|
54
|
+
|
|
55
|
+
const screen = await mount(<App />, { path: '/products' })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Vue
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm i -D @yabbadabbadev/pepito msw vitest-browser-vue
|
|
62
|
+
npx msw init public --save
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { mount } from '@yabbadabbadev/pepito/vue'
|
|
67
|
+
import App from '../src/App.vue'
|
|
68
|
+
|
|
69
|
+
const screen = await mount(App, { path: '/products' })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Svelte
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm i -D @yabbadabbadev/pepito msw vitest-browser-svelte
|
|
76
|
+
npx msw init public --save
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { mount } from '@yabbadabbadev/pepito/svelte'
|
|
81
|
+
import App from '../src/App.svelte'
|
|
82
|
+
|
|
83
|
+
const screen = await mount(App, { path: '/products' })
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Custom framework adapter
|
|
87
|
+
|
|
88
|
+
If you use a framework without an official subpath (Lit, Preact, Angular,
|
|
89
|
+
Solid, …), build your own adapter with `mountCore`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { render } from 'vitest-browser-lit'
|
|
93
|
+
import { mountCore } from '@yabbadabbadev/pepito'
|
|
94
|
+
|
|
95
|
+
export function mount(
|
|
96
|
+
component: unknown,
|
|
97
|
+
options?: Parameters<typeof mountCore>[2],
|
|
98
|
+
) {
|
|
99
|
+
return mountCore(component, (c) => render(c as any), options)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`mountCore` applies `pushState` for routing and registers test-specific
|
|
104
|
+
MSW handlers before calling your `render` function. Its return type is
|
|
105
|
+
generic: it infers the full typed result from whatever your `render`
|
|
106
|
+
returns.
|
|
107
|
+
|
|
50
108
|
`setupNetwork`'s second argument passes straight through to
|
|
51
109
|
`worker.start()`, with no wrapper of its own — for example, to make a
|
|
52
110
|
request with no handler fail the test instead of just warning on the
|
|
@@ -60,23 +118,26 @@ import { handlers } from './handlers'
|
|
|
60
118
|
setupNetwork(handlers, { onUnhandledRequest: 'error' })
|
|
61
119
|
```
|
|
62
120
|
|
|
63
|
-
Importing anything from `pepito` —
|
|
64
|
-
network matchers along as `expect` types: there's no separate
|
|
65
|
-
registration. If your test `tsconfig` doesn't include the setup
|
|
66
|
-
won't see the augmentation and
|
|
67
|
-
raise `TS2339` even though the
|
|
121
|
+
Importing anything from `pepito` — even just `setupNetwork` — already
|
|
122
|
+
brings the network matchers along as `expect` types: there's no separate
|
|
123
|
+
type registration. If your test `tsconfig` doesn't include the setup
|
|
124
|
+
file, `tsc` won't see the augmentation and
|
|
125
|
+
`expect(...).toHaveBeenRequested()` will raise `TS2339` even though the
|
|
126
|
+
test passes at runtime.
|
|
68
127
|
|
|
69
128
|
## 2. Mount the application
|
|
70
129
|
|
|
71
|
-
`mount`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
130
|
+
`mount` (from `@yabbadabbadev/pepito/react`) mounts with
|
|
131
|
+
`vitest-browser-react` and returns its `screen` unwrapped. It requires
|
|
132
|
+
`setupNetwork` to have run first (section 1), **even for a test with no
|
|
133
|
+
network**: the coupling is deliberate — `mount` also installs URL and
|
|
134
|
+
storage cleanup between tests, not just the network — and if it's missing,
|
|
135
|
+
it fails immediately with a fix instruction.
|
|
76
136
|
|
|
77
137
|
```tsx
|
|
78
138
|
import { http, HttpResponse } from 'msw'
|
|
79
|
-
import {
|
|
139
|
+
import { get } from '@yabbadabbadev/pepito'
|
|
140
|
+
import { mount } from '@yabbadabbadev/pepito/react'
|
|
80
141
|
import { App } from '../src/App'
|
|
81
142
|
import { ProductListMother } from '../test/mothers/product-list-mother'
|
|
82
143
|
|
|
@@ -107,7 +168,7 @@ route, and `setupNetwork()` undoes them afterwards in its `afterEach`.
|
|
|
107
168
|
Neither option is required — `mount(<App />)` on its own just mounts:
|
|
108
169
|
|
|
109
170
|
```tsx
|
|
110
|
-
import { mount } from '@yabbadabbadev/pepito'
|
|
171
|
+
import { mount } from '@yabbadabbadev/pepito/react'
|
|
111
172
|
import { App } from '../src/App'
|
|
112
173
|
|
|
113
174
|
test('mounts with no path or network of its own', async () => {
|
|
@@ -129,6 +190,16 @@ await expect.element(screen.getByText('hash: #detail')).toBeVisible()
|
|
|
129
190
|
|
|
130
191
|
## 3. Assert a request
|
|
131
192
|
|
|
193
|
+
The five matchers, at a glance:
|
|
194
|
+
|
|
195
|
+
| Matcher | What it asserts |
|
|
196
|
+
| --------------------------- | ----------------------------------------------------------------------- |
|
|
197
|
+
| `toHaveBeenRequested` | The application made the request |
|
|
198
|
+
| `toHaveBeenRequestedTimes` | Exactly `count` matching requests were made |
|
|
199
|
+
| `toHaveBeenIntercepted` | One of your handlers produced the response |
|
|
200
|
+
| `toHaveRespondedWith` | The intercepted response has the expected status/body |
|
|
201
|
+
| `toHaveNoUnhandledRequests` | No observed request was left without a handler (via `expect.network()`) |
|
|
202
|
+
|
|
132
203
|
```ts
|
|
133
204
|
import { get, post } from '@yabbadabbadev/pepito'
|
|
134
205
|
|
|
@@ -199,14 +270,8 @@ happened.
|
|
|
199
270
|
|
|
200
271
|
## 4. Intercepted or escaped
|
|
201
272
|
|
|
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
|
|
273
|
+
`toHaveBeenRequested` and `toHaveBeenIntercepted` assert different things
|
|
274
|
+
(see the table in section 3). A handler with `passthrough()` satisfies the first and not the second: the
|
|
210
275
|
response came from the real network, not from your mock.
|
|
211
276
|
|
|
212
277
|
```ts
|
|
@@ -315,198 +380,19 @@ the product and are tested like any other output (see `CONTRIBUTING.md`).
|
|
|
315
380
|
|
|
316
381
|
## 7. Recipes
|
|
317
382
|
|
|
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
|
|
383
|
+
Task-oriented answers for what `pepito` deliberately doesn't wrap in its own
|
|
384
|
+
API: chaining handlers for successive responses, seeding storage before
|
|
385
|
+
mounting, the Mothers pattern for fixtures, cross-origin handlers, and
|
|
386
|
+
waiting for the network to settle before a screenshot. See
|
|
387
|
+
[`docs/recipes.md`](docs/recipes.md).
|
|
346
388
|
|
|
347
|
-
|
|
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 />)
|
|
389
|
+
## About the name
|
|
477
390
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
```
|
|
391
|
+
Published on npm as `@yabbadabbadev/pepito`; `pepito` remains the project's
|
|
392
|
+
code-name and this directory's name. See `CONTRIBUTING.md` to run the tests
|
|
393
|
+
and publish a version, and `ROADMAP.md` for what's out of scope for this
|
|
394
|
+
version.
|
|
483
395
|
|
|
484
|
-
##
|
|
396
|
+
## License
|
|
485
397
|
|
|
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
|
-
```
|
|
398
|
+
MIT, see [`LICENSE`](LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ import './matcher-types';
|
|
|
3
3
|
export * from './request-descriptors';
|
|
4
4
|
export { setupNetwork } from './setup-network';
|
|
5
5
|
export { network } from './network';
|
|
6
|
-
export {
|
|
7
|
-
export type { MountOptions } from './mount';
|
|
6
|
+
export { mountCore } from './mount-core';
|
|
7
|
+
export type { MountOptions, MountResult } from './mount-core';
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RequestHandler } from 'msw';
|
|
2
|
+
/** Options for {@link mountCore}. Both are optional. */
|
|
3
|
+
export interface MountOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Same-origin URI starting with `/`, query and hash included (for example
|
|
6
|
+
* `/products?filter=bread#detail`). Applied with `history.pushState`
|
|
7
|
+
* before render so the application's router reads it on mount.
|
|
8
|
+
*/
|
|
9
|
+
path?: string;
|
|
10
|
+
/**
|
|
11
|
+
* MSW handlers of this test's own. Installed with `worker.use()` before
|
|
12
|
+
* render, so they take priority over the suite's for the same route, and
|
|
13
|
+
* `setupNetwork()` undoes them in its `afterEach`.
|
|
14
|
+
*/
|
|
15
|
+
network?: RequestHandler[];
|
|
16
|
+
}
|
|
17
|
+
/** The minimal contract every `vitest-browser-*` render result satisfies. */
|
|
18
|
+
export interface MountResult {
|
|
19
|
+
container: Element;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Framework-agnostic mount: applies `pushState` (if `path`), registers
|
|
23
|
+
* test-specific MSW handlers (if `network`), then calls `render(component)`.
|
|
24
|
+
*
|
|
25
|
+
* The `render` function is provided by the caller — each framework subpath
|
|
26
|
+
* (`/react`, `/vue`, `/svelte`) wraps this with its own `vitest-browser-*`
|
|
27
|
+
* render. See the "Custom framework adapter" section in the README for
|
|
28
|
+
* building your own.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { render } from 'vitest-browser-lit'
|
|
33
|
+
* import { mountCore } from '@yabbadabbadev/pepito'
|
|
34
|
+
*
|
|
35
|
+
* const screen = await mountCore(MyComponent, (c) => render(c as any))
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function mountCore<T extends MountResult>(component: unknown, render: (component: unknown) => T | Promise<T>, options?: MountOptions): Promise<T>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { requireNetworkContext } from './network-singleton';
|
|
2
|
+
function isSameOriginPath(path) {
|
|
3
|
+
if (!path.startsWith('/'))
|
|
4
|
+
return false;
|
|
5
|
+
try {
|
|
6
|
+
return new URL(path, location.origin).origin === location.origin;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Framework-agnostic mount: applies `pushState` (if `path`), registers
|
|
14
|
+
* test-specific MSW handlers (if `network`), then calls `render(component)`.
|
|
15
|
+
*
|
|
16
|
+
* The `render` function is provided by the caller — each framework subpath
|
|
17
|
+
* (`/react`, `/vue`, `/svelte`) wraps this with its own `vitest-browser-*`
|
|
18
|
+
* render. See the "Custom framework adapter" section in the README for
|
|
19
|
+
* building your own.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { render } from 'vitest-browser-lit'
|
|
24
|
+
* import { mountCore } from '@yabbadabbadev/pepito'
|
|
25
|
+
*
|
|
26
|
+
* const screen = await mountCore(MyComponent, (c) => render(c as any))
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function mountCore(component, render, options = {}) {
|
|
30
|
+
const { worker } = requireNetworkContext('mountCore');
|
|
31
|
+
const { path, network: testHandlers } = options;
|
|
32
|
+
if (path !== undefined && !isSameOriginPath(path)) {
|
|
33
|
+
throw new Error(`pepito: path must be a same-origin URI that starts with '/'; ` +
|
|
34
|
+
`received: ${path}. A different origin is mocked in the MSW ` +
|
|
35
|
+
`handlers, not in the mount.`);
|
|
36
|
+
}
|
|
37
|
+
if (path !== undefined) {
|
|
38
|
+
history.pushState({}, '', path);
|
|
39
|
+
}
|
|
40
|
+
if (testHandlers !== undefined && testHandlers.length > 0) {
|
|
41
|
+
worker.use(...testHandlers);
|
|
42
|
+
}
|
|
43
|
+
return await Promise.resolve(render(component));
|
|
44
|
+
}
|
package/dist/network.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const network: {
|
|
|
34
34
|
* absence of future traffic: the same practical blind window remains as
|
|
35
35
|
* for the rest of the mechanism — a request fired in the same tick as the
|
|
36
36
|
* call, before crossing the real round trip to the service worker (1–6 ms
|
|
37
|
-
* measured; see `.
|
|
37
|
+
* measured; see `.agents/docs/references/measured-foundations.md`), may
|
|
38
38
|
* not be in the registry yet when `network.idle()` resolves.
|
|
39
39
|
*
|
|
40
40
|
* Meant for visual regression: capturing right after mounting, with a
|
|
@@ -42,7 +42,7 @@ export declare const network: {
|
|
|
42
42
|
* native stabilizer of `toMatchScreenshot` doesn't catch it because
|
|
43
43
|
* "Loading…" is also a capture that stops changing between frames
|
|
44
44
|
* (measured: 0 failures across 17 local runs + 3 in CI waiting for calm
|
|
45
|
-
* this way — see `.
|
|
45
|
+
* this way — see `.agents/docs/references/measured-foundations.md`).
|
|
46
46
|
* Before this, the only public way to wait for calm on its own was to
|
|
47
47
|
* divert `expect.network().toHaveNoUnhandledRequests()` from its actual
|
|
48
48
|
* purpose (detecting traffic without a handler). It works just as well as a
|
package/dist/network.js
CHANGED
|
@@ -45,7 +45,7 @@ export const network = {
|
|
|
45
45
|
* absence of future traffic: the same practical blind window remains as
|
|
46
46
|
* for the rest of the mechanism — a request fired in the same tick as the
|
|
47
47
|
* call, before crossing the real round trip to the service worker (1–6 ms
|
|
48
|
-
* measured; see `.
|
|
48
|
+
* measured; see `.agents/docs/references/measured-foundations.md`), may
|
|
49
49
|
* not be in the registry yet when `network.idle()` resolves.
|
|
50
50
|
*
|
|
51
51
|
* Meant for visual regression: capturing right after mounting, with a
|
|
@@ -53,7 +53,7 @@ export const network = {
|
|
|
53
53
|
* native stabilizer of `toMatchScreenshot` doesn't catch it because
|
|
54
54
|
* "Loading…" is also a capture that stops changing between frames
|
|
55
55
|
* (measured: 0 failures across 17 local runs + 3 in CI waiting for calm
|
|
56
|
-
* this way — see `.
|
|
56
|
+
* this way — see `.agents/docs/references/measured-foundations.md`).
|
|
57
57
|
* Before this, the only public way to wait for calm on its own was to
|
|
58
58
|
* divert `expect.network().toHaveNoUnhandledRequests()` from its actual
|
|
59
59
|
* purpose (detecting traffic without a handler). It works just as well as a
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
|
+
import { type RenderResult } from 'vitest-browser-react';
|
|
3
|
+
import type { MountOptions } from './mount-core';
|
|
4
|
+
/**
|
|
5
|
+
* Mounts a React element with `vitest-browser-react`, optionally on a real
|
|
6
|
+
* document route and with test-specific MSW handlers.
|
|
7
|
+
*
|
|
8
|
+
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
9
|
+
* applied with `history.pushState` BEFORE render because the application's
|
|
10
|
+
* `BrowserRouter` reads the URL on mount and only listens to `popstate`
|
|
11
|
+
* afterwards. The URI can carry query and hash: they flow through the router
|
|
12
|
+
* the same as the path. `setupNetwork()` restores the original URL in its
|
|
13
|
+
* `afterEach`, so every test starts from the same route regardless of what
|
|
14
|
+
* the previous one mounted.
|
|
15
|
+
*
|
|
16
|
+
* `network`, if given, is registered with `worker.use()` before render: its
|
|
17
|
+
* handlers take priority over the suite's for this request, with the same
|
|
18
|
+
* resolution rules as MSW.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { mount } from '@yabbadabbadev/pepito/react'
|
|
23
|
+
*
|
|
24
|
+
* const screen = await mount(<App />, { path: '/products?filter=bread' })
|
|
25
|
+
* await expect.element(screen.getByText('filter: bread')).toBeVisible()
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function mount(ui: ReactElement, options?: MountOptions): Promise<RenderResult>;
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render } from 'vitest-browser-react';
|
|
2
|
+
import { mountCore } from './mount-core';
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a React element with `vitest-browser-react`, optionally on a real
|
|
5
|
+
* document route and with test-specific MSW handlers.
|
|
6
|
+
*
|
|
7
|
+
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
8
|
+
* applied with `history.pushState` BEFORE render because the application's
|
|
9
|
+
* `BrowserRouter` reads the URL on mount and only listens to `popstate`
|
|
10
|
+
* afterwards. The URI can carry query and hash: they flow through the router
|
|
11
|
+
* the same as the path. `setupNetwork()` restores the original URL in its
|
|
12
|
+
* `afterEach`, so every test starts from the same route regardless of what
|
|
13
|
+
* the previous one mounted.
|
|
14
|
+
*
|
|
15
|
+
* `network`, if given, is registered with `worker.use()` before render: its
|
|
16
|
+
* handlers take priority over the suite's for this request, with the same
|
|
17
|
+
* resolution rules as MSW.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { mount } from '@yabbadabbadev/pepito/react'
|
|
22
|
+
*
|
|
23
|
+
* const screen = await mount(<App />, { path: '/products?filter=bread' })
|
|
24
|
+
* await expect.element(screen.getByText('filter: bread')).toBeVisible()
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export async function mount(ui, options) {
|
|
28
|
+
return mountCore(ui, (c) => render(c), options);
|
|
29
|
+
}
|
package/dist/setup-network.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { SetupWorker, StartOptions } from 'msw/browser';
|
|
|
4
4
|
* Starts the MSW worker and hooks up the traffic registry, leaving the
|
|
5
5
|
* between-test cleanup installed in `afterEach`: registry, hot handlers,
|
|
6
6
|
* document URL and origin storage all return to their pre-test state
|
|
7
|
-
* (measured — see `.
|
|
7
|
+
* (measured — see `.agents/docs/references/measured-foundations.md`).
|
|
8
8
|
*
|
|
9
9
|
* Called once per test file, typically from a `setupFiles` entry in
|
|
10
10
|
* `vitest.config`, never from inside a test.
|
|
@@ -14,7 +14,7 @@ import type { SetupWorker, StartOptions } from 'msw/browser';
|
|
|
14
14
|
* survives between tests — see `clearOriginStorage` in storage-cleanup.ts.
|
|
15
15
|
* A cookie without an explicit `path` does get cleared, including one set
|
|
16
16
|
* while `mount` was simulating being on a nested route (measured — see
|
|
17
|
-
* `.
|
|
17
|
+
* `.agents/docs/references/measured-foundations.md`).
|
|
18
18
|
*
|
|
19
19
|
* @param handlers - Initial MSW handlers, the same ones `setupWorker` would take.
|
|
20
20
|
* @param startOptions - Passed through to `worker.start()` as-is; no wrapper of its own.
|
package/dist/setup-network.js
CHANGED
|
@@ -8,7 +8,7 @@ import { resetTraffic } from './traffic-registry';
|
|
|
8
8
|
* Starts the MSW worker and hooks up the traffic registry, leaving the
|
|
9
9
|
* between-test cleanup installed in `afterEach`: registry, hot handlers,
|
|
10
10
|
* document URL and origin storage all return to their pre-test state
|
|
11
|
-
* (measured — see `.
|
|
11
|
+
* (measured — see `.agents/docs/references/measured-foundations.md`).
|
|
12
12
|
*
|
|
13
13
|
* Called once per test file, typically from a `setupFiles` entry in
|
|
14
14
|
* `vitest.config`, never from inside a test.
|
|
@@ -18,7 +18,7 @@ import { resetTraffic } from './traffic-registry';
|
|
|
18
18
|
* survives between tests — see `clearOriginStorage` in storage-cleanup.ts.
|
|
19
19
|
* A cookie without an explicit `path` does get cleared, including one set
|
|
20
20
|
* while `mount` was simulating being on a nested route (measured — see
|
|
21
|
-
* `.
|
|
21
|
+
* `.agents/docs/references/measured-foundations.md`).
|
|
22
22
|
*
|
|
23
23
|
* @param handlers - Initial MSW handlers, the same ones `setupWorker` would take.
|
|
24
24
|
* @param startOptions - Passed through to `worker.start()` as-is; no wrapper of its own.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* land on the same worker: `localStorage`, `sessionStorage` and cookies
|
|
4
4
|
* belong to the origin, not the document, so they survive per-file
|
|
5
5
|
* isolation (measured — see
|
|
6
|
-
* `.
|
|
6
|
+
* `.agents/docs/references/measured-foundations.md`).
|
|
7
7
|
*
|
|
8
8
|
* Each cookie is expired twice per name: once with `path=/` (the one most
|
|
9
9
|
* application code sets) and once with no `path` attribute, in case one was
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Measured that it doesn't, in this harness (Chromium via Playwright,
|
|
18
18
|
* `vitest@4.1.10`): the order has no observable effect today — full
|
|
19
|
-
* evidence in `.
|
|
19
|
+
* evidence in `.agents/docs/references/measured-foundations.md`. The order
|
|
20
20
|
* is kept anyway, at no cost, in case some future runner or browser does
|
|
21
21
|
* follow the simulated URL.
|
|
22
22
|
*
|
package/dist/storage-cleanup.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* land on the same worker: `localStorage`, `sessionStorage` and cookies
|
|
4
4
|
* belong to the origin, not the document, so they survive per-file
|
|
5
5
|
* isolation (measured — see
|
|
6
|
-
* `.
|
|
6
|
+
* `.agents/docs/references/measured-foundations.md`).
|
|
7
7
|
*
|
|
8
8
|
* Each cookie is expired twice per name: once with `path=/` (the one most
|
|
9
9
|
* application code sets) and once with no `path` attribute, in case one was
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Measured that it doesn't, in this harness (Chromium via Playwright,
|
|
18
18
|
* `vitest@4.1.10`): the order has no observable effect today — full
|
|
19
|
-
* evidence in `.
|
|
19
|
+
* evidence in `.agents/docs/references/measured-foundations.md`. The order
|
|
20
20
|
* is kept anyway, at no cost, in case some future runner or browser does
|
|
21
21
|
* follow the simulated URL.
|
|
22
22
|
*
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render, type RenderResult } from 'vitest-browser-svelte';
|
|
2
|
+
import type { MountOptions } from './mount-core';
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a Svelte component with `vitest-browser-svelte`, optionally on a
|
|
5
|
+
* real document route and with test-specific MSW handlers.
|
|
6
|
+
*
|
|
7
|
+
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
8
|
+
* applied with `history.pushState` BEFORE render so the application's
|
|
9
|
+
* router reads it on mount. `setupNetwork()` restores the original URL
|
|
10
|
+
* in its `afterEach`.
|
|
11
|
+
*
|
|
12
|
+
* `network`, if given, is registered with `worker.use()` before render.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { mount } from '@yabbadabbadev/pepito/svelte'
|
|
17
|
+
* import App from './App.svelte'
|
|
18
|
+
*
|
|
19
|
+
* const screen = await mount(App, { path: '/products' })
|
|
20
|
+
* await expect.element(screen.getByText('Products')).toBeVisible()
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function mount(component: Parameters<typeof render>[0], options?: MountOptions): Promise<RenderResult<any>>;
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render } from 'vitest-browser-svelte';
|
|
2
|
+
import { mountCore } from './mount-core';
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a Svelte component with `vitest-browser-svelte`, optionally on a
|
|
5
|
+
* real document route and with test-specific MSW handlers.
|
|
6
|
+
*
|
|
7
|
+
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
8
|
+
* applied with `history.pushState` BEFORE render so the application's
|
|
9
|
+
* router reads it on mount. `setupNetwork()` restores the original URL
|
|
10
|
+
* in its `afterEach`.
|
|
11
|
+
*
|
|
12
|
+
* `network`, if given, is registered with `worker.use()` before render.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { mount } from '@yabbadabbadev/pepito/svelte'
|
|
17
|
+
* import App from './App.svelte'
|
|
18
|
+
*
|
|
19
|
+
* const screen = await mount(App, { path: '/products' })
|
|
20
|
+
* await expect.element(screen.getByText('Products')).toBeVisible()
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export async function mount(component, options) {
|
|
24
|
+
return mountCore(component, (c) => render(c), options);
|
|
25
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Traffic entry as it lives in the registry while the test runs: `body` and
|
|
3
3
|
* `responseBody` are promises because they can only be read once (measured
|
|
4
|
-
* — see `.
|
|
4
|
+
* — see `.agents/docs/references/measured-foundations.md`) and are stored
|
|
5
5
|
* without awaiting, so as not to block the `request:start` listener that
|
|
6
6
|
* opens them.
|
|
7
7
|
*/
|
|
@@ -26,7 +26,7 @@ export interface ResolvedRequest extends Omit<ObservedRequest, 'body' | 'respons
|
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Opens a traffic entry with what can only be read at `request:start`
|
|
29
|
-
* (measured — see `.
|
|
29
|
+
* (measured — see `.agents/docs/references/measured-foundations.md`) and
|
|
30
30
|
* marks it in flight.
|
|
31
31
|
* `watchNetwork` (msw-events.ts) is the only caller.
|
|
32
32
|
*/
|
|
@@ -73,7 +73,7 @@ export declare const QUIESCENCE_TIMEOUT_MS = 4000;
|
|
|
73
73
|
* in the same tick as the call hasn't yet crossed the real round trip to
|
|
74
74
|
* the service worker that registers its `request:start`, so this function
|
|
75
75
|
* reads the counter at zero by construction, not by actual absence (see
|
|
76
|
-
* `.
|
|
76
|
+
* `.agents/docs/references/measured-foundations.md`). Whoever needs to
|
|
77
77
|
* assert an absence or count with precision must go through
|
|
78
78
|
* `snapshotAfterIdle` in `pepito/src/matchers.ts`, which closes that window
|
|
79
79
|
* with a two-observation stability condition; calling this function
|
package/dist/traffic-registry.js
CHANGED
|
@@ -6,7 +6,7 @@ const traffic = new Map();
|
|
|
6
6
|
const pending = new Set();
|
|
7
7
|
/**
|
|
8
8
|
* Opens a traffic entry with what can only be read at `request:start`
|
|
9
|
-
* (measured — see `.
|
|
9
|
+
* (measured — see `.agents/docs/references/measured-foundations.md`) and
|
|
10
10
|
* marks it in flight.
|
|
11
11
|
* `watchNetwork` (msw-events.ts) is the only caller.
|
|
12
12
|
*/
|
|
@@ -99,7 +99,7 @@ export const QUIESCENCE_TIMEOUT_MS = 4000;
|
|
|
99
99
|
* in the same tick as the call hasn't yet crossed the real round trip to
|
|
100
100
|
* the service worker that registers its `request:start`, so this function
|
|
101
101
|
* reads the counter at zero by construction, not by actual absence (see
|
|
102
|
-
* `.
|
|
102
|
+
* `.agents/docs/references/measured-foundations.md`). Whoever needs to
|
|
103
103
|
* assert an absence or count with precision must go through
|
|
104
104
|
* `snapshotAfterIdle` in `pepito/src/matchers.ts`, which closes that window
|
|
105
105
|
* with a two-observation stability condition; calling this function
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { render, type RenderResult } from 'vitest-browser-vue';
|
|
2
|
+
import type { MountOptions } from './mount-core';
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a Vue component with `vitest-browser-vue`, optionally on a real
|
|
5
|
+
* document route and with test-specific MSW handlers.
|
|
6
|
+
*
|
|
7
|
+
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
8
|
+
* applied with `history.pushState` BEFORE render so the application's
|
|
9
|
+
* router reads it on mount. `setupNetwork()` restores the original URL
|
|
10
|
+
* in its `afterEach`.
|
|
11
|
+
*
|
|
12
|
+
* `network`, if given, is registered with `worker.use()` before render.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { mount } from '@yabbadabbadev/pepito/vue'
|
|
17
|
+
* import App from './App.vue'
|
|
18
|
+
*
|
|
19
|
+
* const screen = await mount(App, { path: '/products' })
|
|
20
|
+
* await expect.element(screen.getByText('Products')).toBeVisible()
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function mount(component: Parameters<typeof render>[0], options?: MountOptions): Promise<RenderResult<Record<string, unknown>>>;
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { render } from 'vitest-browser-vue';
|
|
2
|
+
import { mountCore } from './mount-core';
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a Vue component with `vitest-browser-vue`, optionally on a real
|
|
5
|
+
* document route and with test-specific MSW handlers.
|
|
6
|
+
*
|
|
7
|
+
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
8
|
+
* applied with `history.pushState` BEFORE render so the application's
|
|
9
|
+
* router reads it on mount. `setupNetwork()` restores the original URL
|
|
10
|
+
* in its `afterEach`.
|
|
11
|
+
*
|
|
12
|
+
* `network`, if given, is registered with `worker.use()` before render.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { mount } from '@yabbadabbadev/pepito/vue'
|
|
17
|
+
* import App from './App.vue'
|
|
18
|
+
*
|
|
19
|
+
* const screen = await mount(App, { path: '/products' })
|
|
20
|
+
* await expect.element(screen.getByText('Products')).toBeVisible()
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export async function mount(component, options) {
|
|
24
|
+
return mountCore(component, (c) => render(c), options);
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yabbadabbadev/pepito",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -26,6 +26,18 @@
|
|
|
26
26
|
".": {
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./react": {
|
|
31
|
+
"types": "./dist/react.d.ts",
|
|
32
|
+
"default": "./dist/react.js"
|
|
33
|
+
},
|
|
34
|
+
"./vue": {
|
|
35
|
+
"types": "./dist/vue.d.ts",
|
|
36
|
+
"default": "./dist/vue.js"
|
|
37
|
+
},
|
|
38
|
+
"./svelte": {
|
|
39
|
+
"types": "./dist/svelte.d.ts",
|
|
40
|
+
"default": "./dist/svelte.js"
|
|
29
41
|
}
|
|
30
42
|
},
|
|
31
43
|
"files": [
|
|
@@ -51,13 +63,11 @@
|
|
|
51
63
|
},
|
|
52
64
|
"peerDependencies": {
|
|
53
65
|
"msw": "^2.15.0",
|
|
54
|
-
"
|
|
55
|
-
"react-dom": "^19.0.0",
|
|
56
|
-
"vitest": "^4.0.0",
|
|
57
|
-
"vitest-browser-react": "^2.0.0"
|
|
66
|
+
"vitest": "^4.0.0"
|
|
58
67
|
},
|
|
59
68
|
"devDependencies": {
|
|
60
69
|
"@eslint/js": "^9.39.5",
|
|
70
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
61
71
|
"@types/react": "^19.2.18",
|
|
62
72
|
"@types/react-dom": "^19.2.4",
|
|
63
73
|
"@typescript-eslint/eslint-plugin": "^8.67.0",
|
|
@@ -80,9 +90,13 @@
|
|
|
80
90
|
"react": "^19.2.8",
|
|
81
91
|
"react-dom": "^19.2.8",
|
|
82
92
|
"react-router": "^8.3.0",
|
|
93
|
+
"svelte": "^5.57.0",
|
|
83
94
|
"typescript": "^6.0.3",
|
|
84
95
|
"vitest": "^4.1.10",
|
|
85
|
-
"vitest-browser-react": "^2.2.0"
|
|
96
|
+
"vitest-browser-react": "^2.2.0",
|
|
97
|
+
"vitest-browser-svelte": "^3.0.0",
|
|
98
|
+
"vitest-browser-vue": "^2.1.0",
|
|
99
|
+
"vue": "^3.5.42"
|
|
86
100
|
},
|
|
87
101
|
"msw": {
|
|
88
102
|
"workerDirectory": [
|
package/dist/mount.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ReactElement } from 'react';
|
|
2
|
-
import type { RequestHandler } from 'msw';
|
|
3
|
-
import { type RenderResult } from 'vitest-browser-react';
|
|
4
|
-
/** Options for {@link mount}. Both are optional: `mount(<App />)` alone just mounts. */
|
|
5
|
-
export interface MountOptions {
|
|
6
|
-
/**
|
|
7
|
-
* Same-origin URI starting with `/`, query and hash included (for example
|
|
8
|
-
* `/products?filter=bread#detail`). Applied with `history.pushState`
|
|
9
|
-
* before render so the application's router reads it on mount.
|
|
10
|
-
*/
|
|
11
|
-
path?: string;
|
|
12
|
-
/**
|
|
13
|
-
* MSW handlers of this test's own. Installed with `worker.use()` before
|
|
14
|
-
* render, so they take priority over the suite's for the same route, and
|
|
15
|
-
* `setupNetwork()` undoes them in its `afterEach`.
|
|
16
|
-
*/
|
|
17
|
-
network?: RequestHandler[];
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Mounts `ui` with `vitest-browser-react`, optionally on a real document
|
|
21
|
-
* route and with test-specific MSW handlers.
|
|
22
|
-
*
|
|
23
|
-
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
24
|
-
* applied with `history.pushState` BEFORE render because the application's
|
|
25
|
-
* `BrowserRouter` reads the URL on mount and only listens to `popstate`
|
|
26
|
-
* afterwards (measured — see
|
|
27
|
-
* `.claude/docs/references/measured-foundations.md`). The URI can carry
|
|
28
|
-
* query and hash: they flow through the router the same as the path.
|
|
29
|
-
* `setupNetwork()` restores the original URL in its `afterEach`, so every
|
|
30
|
-
* test starts from the same route regardless of what the previous one
|
|
31
|
-
* mounted.
|
|
32
|
-
*
|
|
33
|
-
* `network`, if given, is registered with `worker.use()` before render: its
|
|
34
|
-
* handlers take priority over the suite's for this request, with the same
|
|
35
|
-
* resolution rules as MSW.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```tsx
|
|
39
|
-
* import { mount } from '@yabbadabbadev/pepito'
|
|
40
|
-
*
|
|
41
|
-
* const screen = await mount(<App />, { path: '/products?filter=bread' })
|
|
42
|
-
* await expect.element(screen.getByText('filter: bread')).toBeVisible()
|
|
43
|
-
* ```
|
|
44
|
-
*/
|
|
45
|
-
export declare function mount(ui: ReactElement, options?: MountOptions): Promise<RenderResult>;
|
package/dist/mount.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { render } from 'vitest-browser-react';
|
|
2
|
-
import { requireNetworkContext } from './network-singleton';
|
|
3
|
-
// The '/' prefix alone isn't enough: '//evil.com' is protocol-relative and
|
|
4
|
-
// WHATWG also treats '/\' as introducing an authority, so both would pass
|
|
5
|
-
// it and pushState would be the one throwing the raw SecurityError further
|
|
6
|
-
// down.
|
|
7
|
-
function isSameOriginPath(path) {
|
|
8
|
-
if (!path.startsWith('/'))
|
|
9
|
-
return false;
|
|
10
|
-
try {
|
|
11
|
-
return new URL(path, location.origin).origin === location.origin;
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Mounts `ui` with `vitest-browser-react`, optionally on a real document
|
|
19
|
-
* route and with test-specific MSW handlers.
|
|
20
|
-
*
|
|
21
|
-
* `path`, if given, has to be a same-origin URI starting with `/`: it's
|
|
22
|
-
* applied with `history.pushState` BEFORE render because the application's
|
|
23
|
-
* `BrowserRouter` reads the URL on mount and only listens to `popstate`
|
|
24
|
-
* afterwards (measured — see
|
|
25
|
-
* `.claude/docs/references/measured-foundations.md`). The URI can carry
|
|
26
|
-
* query and hash: they flow through the router the same as the path.
|
|
27
|
-
* `setupNetwork()` restores the original URL in its `afterEach`, so every
|
|
28
|
-
* test starts from the same route regardless of what the previous one
|
|
29
|
-
* mounted.
|
|
30
|
-
*
|
|
31
|
-
* `network`, if given, is registered with `worker.use()` before render: its
|
|
32
|
-
* handlers take priority over the suite's for this request, with the same
|
|
33
|
-
* resolution rules as MSW.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* ```tsx
|
|
37
|
-
* import { mount } from '@yabbadabbadev/pepito'
|
|
38
|
-
*
|
|
39
|
-
* const screen = await mount(<App />, { path: '/products?filter=bread' })
|
|
40
|
-
* await expect.element(screen.getByText('filter: bread')).toBeVisible()
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export async function mount(ui, options = {}) {
|
|
44
|
-
const { worker } = requireNetworkContext('mount');
|
|
45
|
-
const { path, network: testHandlers } = options;
|
|
46
|
-
if (path !== undefined && !isSameOriginPath(path)) {
|
|
47
|
-
throw new Error(`pepito: path must be a same-origin URI that starts with '/'; ` +
|
|
48
|
-
`received: ${path}. A different origin is mocked in the MSW ` +
|
|
49
|
-
`handlers, not in the mount.`);
|
|
50
|
-
}
|
|
51
|
-
// Before render: the app's router reads the URL on mount and only
|
|
52
|
-
// listens to popstate afterwards. See
|
|
53
|
-
// docs/knowledge/url-navegacion-browser-mode.md.
|
|
54
|
-
if (path !== undefined) {
|
|
55
|
-
history.pushState({}, '', path);
|
|
56
|
-
}
|
|
57
|
-
if (testHandlers !== undefined && testHandlers.length > 0) {
|
|
58
|
-
worker.use(...testHandlers);
|
|
59
|
-
}
|
|
60
|
-
return render(ui);
|
|
61
|
-
}
|