@yabbadabbadev/pepito 0.1.1 → 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 CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.1.1](https://github.com/yabbadabbadev/pepito/compare/v0.1.0...v0.1.1) (2026-08-20)
4
11
 
5
12
  Nothing changed for consumers of this package: no new API, no fix, no
package/README.md CHANGED
@@ -8,21 +8,20 @@ matchers to query it.
8
8
 
9
9
  ## 1. Install and start
10
10
 
11
- `react`, `react-dom`, `vitest`, `msw` and `vitest-browser-react` are
12
- **peerDependencies**: install them if your project doesn't already have
13
- them.
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.
14
18
 
15
19
  ```bash
16
- npm i -D @yabbadabbadev/pepito msw vitest-browser-react
20
+ npm i -D @yabbadabbadev/pepito msw
17
21
  npx msw init public --save
18
22
  ```
19
23
 
20
- `npx msw init public --save` generates the service worker MSW needs in
21
- browser mode (`public/mockServiceWorker.js`); it's a one-off, not a
22
- day-to-day step — it's only repeated when upgrading MSW's version.
23
-
24
- Call `setupNetwork` once, in a `setupFiles` file of `vitest.config` — never
25
- inside a test:
24
+ Call `setupNetwork` once, in a `setupFiles` file of `vitest.config`:
26
25
 
27
26
  ```ts
28
27
  // vitest.setup.ts
@@ -42,6 +41,70 @@ export default defineConfig({
42
41
  })
43
42
  ```
44
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
+
45
108
  `setupNetwork`'s second argument passes straight through to
46
109
  `worker.start()`, with no wrapper of its own — for example, to make a
47
110
  request with no handler fail the test instead of just warning on the
@@ -55,23 +118,26 @@ import { handlers } from './handlers'
55
118
  setupNetwork(handlers, { onUnhandledRequest: 'error' })
56
119
  ```
57
120
 
58
- Importing anything from `pepito` — here, `setupNetwork` — already brings the
59
- network matchers along as `expect` types: there's no separate type
60
- registration. If your test `tsconfig` doesn't include the setup file, `tsc`
61
- won't see the augmentation and `expect(...).toHaveBeenRequested()` will
62
- raise `TS2339` even though the test passes at runtime.
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.
63
127
 
64
128
  ## 2. Mount the application
65
129
 
66
- `mount` mounts with `vitest-browser-react` and returns its `screen`
67
- unwrapped. It requires `setupNetwork` to have run first (section 1), **even
68
- for a test with no network**: the coupling is deliberate `mount` also
69
- installs URL and storage cleanup between tests, not just the network — and
70
- if it's missing, it fails immediately with a fix instruction.
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.
71
136
 
72
137
  ```tsx
73
138
  import { http, HttpResponse } from 'msw'
74
- import { mount, get } from '@yabbadabbadev/pepito'
139
+ import { get } from '@yabbadabbadev/pepito'
140
+ import { mount } from '@yabbadabbadev/pepito/react'
75
141
  import { App } from '../src/App'
76
142
  import { ProductListMother } from '../test/mothers/product-list-mother'
77
143
 
@@ -102,7 +168,7 @@ route, and `setupNetwork()` undoes them afterwards in its `afterEach`.
102
168
  Neither option is required — `mount(<App />)` on its own just mounts:
103
169
 
104
170
  ```tsx
105
- import { mount } from '@yabbadabbadev/pepito'
171
+ import { mount } from '@yabbadabbadev/pepito/react'
106
172
  import { App } from '../src/App'
107
173
 
108
174
  test('mounts with no path or network of its own', async () => {
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 { mount } from './mount';
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
@@ -3,4 +3,4 @@ import './matcher-types';
3
3
  export * from './request-descriptors';
4
4
  export { setupNetwork } from './setup-network';
5
5
  export { network } from './network';
6
- export { mount } from './mount';
6
+ export { mountCore } from './mount-core';
@@ -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 `.claude/docs/references/measured-foundations.md`), may
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 `.claude/docs/references/measured-foundations.md`).
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 `.claude/docs/references/measured-foundations.md`), may
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 `.claude/docs/references/measured-foundations.md`).
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
@@ -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
+ }
@@ -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 `.claude/docs/references/measured-foundations.md`).
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
- * `.claude/docs/references/measured-foundations.md`).
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.
@@ -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 `.claude/docs/references/measured-foundations.md`).
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
- * `.claude/docs/references/measured-foundations.md`).
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
- * `.claude/docs/references/measured-foundations.md`).
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 `.claude/docs/references/measured-foundations.md`. The order
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
  *
@@ -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
- * `.claude/docs/references/measured-foundations.md`).
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 `.claude/docs/references/measured-foundations.md`. The order
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
  *
@@ -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 `.claude/docs/references/measured-foundations.md`) and are stored
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 `.claude/docs/references/measured-foundations.md`) and
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
- * `.claude/docs/references/measured-foundations.md`). Whoever needs to
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
@@ -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 `.claude/docs/references/measured-foundations.md`) and
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
- * `.claude/docs/references/measured-foundations.md`). Whoever needs to
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.1.1",
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
- "react": "^19.0.0",
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
- }