@kasperpeulen/vitest-plugin-nextjs 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -2
- package/dist/_virtual/_rolldown/runtime.js +14 -0
- package/dist/action-ids.d.ts +18 -0
- package/dist/action-ids.js +44 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.js +12 -0
- package/dist/app-requests.d.ts +63 -0
- package/dist/app-requests.js +253 -0
- package/dist/browser-context.d.ts +24 -0
- package/dist/browser-context.js +157 -0
- package/dist/build-output.d.ts +68 -0
- package/dist/build-output.js +216 -0
- package/dist/build.d.ts +26 -0
- package/dist/build.js +39 -0
- package/dist/built-files.d.ts +9 -0
- package/dist/built-files.js +22 -0
- package/dist/cache.d.ts +44 -0
- package/dist/cache.js +99 -0
- package/dist/client-components.d.ts +3 -0
- package/dist/client-components.js +4 -0
- package/dist/client-file-key.d.ts +5 -0
- package/dist/client-file-key.js +5 -0
- package/dist/client-files.d.ts +83 -0
- package/dist/client-files.js +186 -0
- package/dist/client-graph.d.ts +94 -0
- package/dist/client-graph.js +266 -0
- package/dist/client-modules.d.ts +10 -0
- package/dist/client-modules.js +43 -0
- package/dist/client.d.ts +42 -0
- package/dist/client.js +263 -0
- package/dist/commonjs.d.ts +44 -0
- package/dist/commonjs.js +111 -0
- package/dist/compat/browser/client.d.ts +19 -0
- package/dist/compat/browser/client.js +13 -0
- package/dist/compat/build.d.ts +145 -0
- package/dist/compat/build.js +149 -0
- package/dist/compat/checks.d.ts +16 -0
- package/dist/compat/checks.js +148 -0
- package/dist/compat/patches.d.ts +13 -0
- package/dist/compat/patches.js +30 -0
- package/dist/compat/paths.d.ts +74 -0
- package/dist/compat/paths.js +160 -0
- package/dist/compat/rsc/handlers.d.ts +2 -0
- package/dist/compat/rsc/handlers.js +2 -0
- package/dist/compat/rsc/prerender.d.ts +10 -0
- package/dist/compat/rsc/prerender.js +9 -0
- package/dist/compat/server/headers.d.ts +3 -0
- package/dist/compat/server/headers.js +3 -0
- package/dist/compat/ssr/build-output.d.ts +4 -0
- package/dist/compat/ssr/build-output.js +3 -0
- package/dist/compat/ssr/cache.d.ts +6 -0
- package/dist/compat/ssr/cache.js +6 -0
- package/dist/compat/ssr/handler.d.ts +8 -0
- package/dist/compat/ssr/handler.js +8 -0
- package/dist/compat/ssr/node-server.d.ts +5 -0
- package/dist/compat/ssr/node-server.js +4 -0
- package/dist/compat/ssr/render-errors.d.ts +4 -0
- package/dist/compat/ssr/render-errors.js +4 -0
- package/dist/compat/ssr/senders.d.ts +6 -0
- package/dist/compat/ssr/senders.js +6 -0
- package/dist/compat/types.d.ts +8 -0
- package/dist/compat/types.js +1 -0
- package/dist/compile.d.ts +39 -0
- package/dist/compile.js +175 -0
- package/dist/disk.d.ts +16 -0
- package/dist/disk.js +13 -0
- package/dist/document-loads.d.ts +15 -0
- package/dist/document-loads.js +53 -0
- package/dist/document.d.ts +79 -0
- package/dist/document.js +330 -0
- package/dist/downloads.d.ts +15 -0
- package/dist/downloads.js +71 -0
- package/dist/esm-require.d.ts +11 -0
- package/dist/esm-require.js +32 -0
- package/dist/fast-crypto/index.js +1093 -0
- package/dist/fast-crypto.d.ts +20 -0
- package/dist/fast-crypto.js +151 -0
- package/dist/flight.d.ts +19 -0
- package/dist/flight.js +42 -0
- package/dist/form-submission.d.ts +15 -0
- package/dist/form-submission.js +27 -0
- package/dist/from-page.d.ts +5 -0
- package/dist/from-page.js +44 -0
- package/dist/globals.d.ts +1 -0
- package/dist/globals.js +197 -0
- package/dist/host-document.d.ts +26 -0
- package/dist/host-document.js +34 -0
- package/dist/host-return-shared.d.ts +5 -0
- package/dist/host-return-shared.js +5 -0
- package/dist/host-return.d.ts +5 -0
- package/dist/host-return.js +16 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.js +281 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +3 -0
- package/dist/leftovers.d.ts +11 -0
- package/dist/leftovers.js +55 -0
- package/dist/mode.d.ts +169 -0
- package/dist/mode.js +31 -0
- package/dist/node/LICENSES.md +3540 -0
- package/dist/node/assert/strict.js +29 -0
- package/dist/node/assert.js +3044 -0
- package/dist/node/buffer.js +1499 -0
- package/dist/node/child_process.js +45 -0
- package/dist/node/cluster.js +85 -0
- package/dist/node/console.js +96 -0
- package/dist/node/constants.js +495 -0
- package/dist/node/crypto.js +1388 -0
- package/dist/node/dgram.js +78 -0
- package/dist/node/diagnostics_channel.js +122 -0
- package/dist/node/dns/promises.js +129 -0
- package/dist/node/dns.js +137 -0
- package/dist/node/domain.js +43 -0
- package/dist/node/events.js +1155 -0
- package/dist/node/fs/promises.js +40 -0
- package/dist/node/fs.js +10375 -0
- package/dist/node/http.js +326 -0
- package/dist/node/http2.js +305 -0
- package/dist/node/https.js +57 -0
- package/dist/node/inspector/promises.js +66 -0
- package/dist/node/inspector.js +76 -0
- package/dist/node/module.js +205 -0
- package/dist/node/net.js +392 -0
- package/dist/node/os.js +259 -0
- package/dist/node/path/posix.js +25 -0
- package/dist/node/path/win32.js +25 -0
- package/dist/node/path.js +384 -0
- package/dist/node/perf_hooks.js +313 -0
- package/dist/node/process.js +452 -0
- package/dist/node/punycode.js +178 -0
- package/dist/node/querystring.js +934 -0
- package/dist/node/readline/promises.js +71 -0
- package/dist/node/readline.js +56 -0
- package/dist/node/repl.js +44 -0
- package/dist/node/sqlite.js +22 -0
- package/dist/node/stream/consumers.js +47 -0
- package/dist/node/stream/promises.js +6 -0
- package/dist/node/stream/web.js +52 -0
- package/dist/node/stream.js +4323 -0
- package/dist/node/string_decoder.js +276 -0
- package/dist/node/sys.js +6 -0
- package/dist/node/timers/promises.js +71 -0
- package/dist/node/timers.js +133 -0
- package/dist/node/tls.js +122 -0
- package/dist/node/trace_events.js +23 -0
- package/dist/node/tty.js +69 -0
- package/dist/node/url.js +1426 -0
- package/dist/node/util/types.js +1456 -0
- package/dist/node/util.js +4807 -0
- package/dist/node/v8.js +160 -0
- package/dist/node/vm.js +88 -0
- package/dist/node/wasi.js +16 -0
- package/dist/node/worker_threads.js +123 -0
- package/dist/node/zlib.js +1396 -0
- package/dist/node-builtins.d.ts +40 -0
- package/dist/node-builtins.js +74 -0
- package/dist/node-on-page.d.ts +46 -0
- package/dist/node-on-page.js +101 -0
- package/dist/node-platform.d.ts +36 -0
- package/dist/node-platform.js +152 -0
- package/dist/node-rendered.d.ts +21 -0
- package/dist/node-rendered.js +26 -0
- package/dist/node-server.d.ts +46 -0
- package/dist/node-server.js +193 -0
- package/dist/not-found-page.d.ts +19 -0
- package/dist/not-found-page.js +74 -0
- package/dist/optimizer-cache-key.d.ts +26 -0
- package/dist/optimizer-cache-key.js +73 -0
- package/dist/own-package.d.ts +1 -0
- package/dist/own-package.js +6 -0
- package/dist/page-store.d.ts +33 -0
- package/dist/page-store.js +80 -0
- package/dist/page-timers.d.ts +22 -0
- package/dist/page-timers.js +47 -0
- package/dist/page.d.ts +112 -0
- package/dist/page.js +549 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +47 -0
- package/dist/plugin.d.ts +23 -0
- package/dist/plugin.js +1006 -0
- package/dist/prerender-verdict.d.ts +8 -0
- package/dist/prerender-verdict.js +67 -0
- package/dist/project/context.d.ts +546 -0
- package/dist/project/context.js +111 -0
- package/dist/project/entries.d.ts +42 -0
- package/dist/project/entries.js +189 -0
- package/dist/project/env-worker.d.ts +1 -0
- package/dist/project/env-worker.js +9 -0
- package/dist/project/env.d.ts +36 -0
- package/dist/project/env.js +82 -0
- package/dist/project/layers.d.ts +31 -0
- package/dist/project/layers.js +150 -0
- package/dist/project/loaders.d.ts +71 -0
- package/dist/project/loaders.js +300 -0
- package/dist/project/metadata.d.ts +8 -0
- package/dist/project/metadata.js +34 -0
- package/dist/project/module-exports.d.ts +29 -0
- package/dist/project/module-exports.js +77 -0
- package/dist/project/routes.d.ts +19 -0
- package/dist/project/routes.js +92 -0
- package/dist/project/routing.d.ts +46 -0
- package/dist/project/routing.js +280 -0
- package/dist/project/transform.d.ts +8 -0
- package/dist/project/transform.js +110 -0
- package/dist/project/webpack.d.ts +65 -0
- package/dist/project/webpack.js +191 -0
- package/dist/project.d.ts +269 -0
- package/dist/project.js +58 -0
- package/dist/registry-global.d.ts +5 -0
- package/dist/registry-global.js +5 -0
- package/dist/registry.d.ts +241 -0
- package/dist/registry.js +30 -0
- package/dist/route-chunks.d.ts +13 -0
- package/dist/route-chunks.js +32 -0
- package/dist/route-lookup.d.ts +34 -0
- package/dist/route-lookup.js +207 -0
- package/dist/routes-ahead-shared.d.ts +17 -0
- package/dist/routes-ahead-shared.js +9 -0
- package/dist/routes-ahead.d.ts +10 -0
- package/dist/routes-ahead.js +54 -0
- package/dist/rsc.d.ts +19 -0
- package/dist/rsc.js +171 -0
- package/dist/server-action.d.ts +9 -0
- package/dist/server-action.js +15 -0
- package/dist/server-code.d.ts +176 -0
- package/dist/server-code.js +357 -0
- package/dist/server-in-front.d.ts +37 -0
- package/dist/server-in-front.js +90 -0
- package/dist/server-layers.d.ts +22 -0
- package/dist/server-layers.js +33 -0
- package/dist/setup-browser.d.ts +1 -0
- package/dist/setup-browser.js +8 -0
- package/dist/setup-node.d.ts +1 -0
- package/dist/setup-node.js +6 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +62 -0
- package/dist/speed-features.d.ts +13 -0
- package/dist/speed-features.js +95 -0
- package/dist/ssr.d.ts +70 -0
- package/dist/ssr.js +480 -0
- package/dist/static-build.d.ts +36 -0
- package/dist/static-build.js +92 -0
- package/dist/storage.d.ts +31 -0
- package/dist/storage.js +94 -0
- package/dist/styles-shared.d.ts +137 -0
- package/dist/styles-shared.js +124 -0
- package/dist/styles.d.ts +102 -0
- package/dist/styles.js +730 -0
- package/dist/stylesheet-versions.d.ts +17 -0
- package/dist/stylesheet-versions.js +43 -0
- package/dist/tester.html +10 -0
- package/dist/use-cache.d.ts +15 -0
- package/dist/use-cache.js +93 -0
- package/dist/vitest-version.d.ts +6 -0
- package/dist/vitest-version.js +12 -0
- package/package.json +125 -3
package/README.md
CHANGED
|
@@ -1,3 +1,250 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Storybook for Pages
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> End-to-end stories and tests of the pages of a framework app, with as little mocking as possible. Next.js first.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/storybookjs/storybook-for-pages/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/storybookjs/storybook-for-pages/blob/main/LICENSE)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](https://pnpm.io/)
|
|
9
|
+
|
|
10
|
+
A page of your app runs with the framework's own runtime: in a Vitest test, in `vite dev` and in a Storybook story. Next.js comes first. React Router, TanStack Start, SvelteKit and Nuxt come later, each as a Vite plugin with a Storybook framework on top.
|
|
11
|
+
|
|
12
|
+
| Package | What it is |
|
|
13
|
+
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| [`@kasperpeulen/vitest-plugin-nextjs`](https://github.com/storybookjs/storybook-for-pages/blob/main/packages/vitest-plugin-nextjs) | The Next.js plugin for Vite: a page of a Next.js app in Vitest Browser Mode, `vite dev` and Storybook. |
|
|
15
|
+
| [`@kasperpeulen/storybook-nextjs-vite-rsc`](https://github.com/storybookjs/storybook-for-pages/blob/main/packages/storybook-nextjs-vite-rsc) | The Storybook framework on top of it: stories of whole pages and of Server Components. |
|
|
16
|
+
| [`@kasperpeulen/vitest-plugin-rsc`](https://github.com/storybookjs/storybook-for-pages/blob/main/packages/vitest-plugin-rsc) | The core, without a framework: React Server Components in one browser tab, server, Flight and hydration. |
|
|
17
|
+
| [`@kasperpeulen/dom-locators`](https://github.com/storybookjs/storybook-for-pages/blob/main/packages/dom-locators) | Vitest Browser Mode's page locators answered in the document, also in a DOM of Node.js. |
|
|
18
|
+
|
|
19
|
+
All of them are alphas: `npm install` without a tag gets the last one.
|
|
20
|
+
|
|
21
|
+
Write a test, render a page of your Next.js app, click around, check the database. No dev server, no deployment, no mocks of Next.js.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
test("archive a note", async () => {
|
|
25
|
+
await signedInAs(testUser);
|
|
26
|
+
await db.insert(notes).values({ ownerId: testUser.id, title: "Inbox triage" });
|
|
27
|
+
|
|
28
|
+
await renderServer({ url: "/notes" });
|
|
29
|
+
await expect.element(page.getByText("Inbox triage")).toBeVisible();
|
|
30
|
+
|
|
31
|
+
await page.getByRole("button", { name: "Archive Inbox triage" }).click();
|
|
32
|
+
await expect.element(page.getByText("Inbox triage")).not.toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That test seeds a database, renders the `/notes` route on the server, hydrates it in a real browser, runs a Server Action and checks the result. All in one browser tab.
|
|
37
|
+
|
|
38
|
+
The trick: the plugin runs your server code _in the test's browser tab_. Next's own request handler renders the route, Next's own client hydrates it, and the `db` your test seeds is the very same module your Server Components read. In the notes demo, that database is Postgres compiled to WebAssembly.
|
|
39
|
+
|
|
40
|
+
## Why
|
|
41
|
+
|
|
42
|
+
Server Components have been hard to test. Rendering one in a unit test has been [an open problem since 2023](https://github.com/testing-library/react-testing-library/issues/1209), so they've mostly been left to E2E tests. And E2E tests are slow, share a server and a database, and get flaky. That's why validation errors, roles, empty states, feature flags and time-dependent code usually go untested.
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<img src="https://raw.githubusercontent.com/storybookjs/storybook-for-pages/main/docs/assets/test-pyramid.svg" alt="Test pyramid: a small Playwright E2E layer above a wider Vitest unit, component, and integration layer" width="760" />
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
This plugin moves that coverage down to the fast part of the pyramid:
|
|
49
|
+
|
|
50
|
+
- **Everything lives in the tab.** The file system is in memory, the database is a [real engine in WebAssembly](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/databases.md), and outgoing requests go to [MSW](https://mswjs.io/docs/recipes/vitest-browser-mode).
|
|
51
|
+
- **Test files run in parallel.** Every Vitest worker gets its own tab, with its own server, database, disk, cookies and storage.
|
|
52
|
+
- **Every test starts clean.** Cookies, storage, the disk and Next's cache are reset before each test.
|
|
53
|
+
|
|
54
|
+
That also makes it a nice loop for coding agents: edit, run, fix, repeat.
|
|
55
|
+
|
|
56
|
+
### Is it really Next.js?
|
|
57
|
+
|
|
58
|
+
Yes. The plugin doesn't reimplement Next. It swaps the bundler for Vite and runs Next's runtime unchanged. To keep that honest, the repo runs Next's own e2e tests against the plugin: **967 of the 1,139 that apply pass (85%)** on `next@16.4.0`. Every failure is listed with a reason in the [conformance report](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/internals/next-conformance.md). CI also tests against `next@latest` and `next@canary`.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
You need:
|
|
63
|
+
|
|
64
|
+
- Vite 8+ and Vitest 5.0.4+ in [Browser Mode](https://vitest.dev/guide/browser/) (the examples use Playwright).
|
|
65
|
+
- A Next.js App Router app on `next@16.4` or later.
|
|
66
|
+
|
|
67
|
+
### 1. Install
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm install -D @kasperpeulen/vitest-plugin-rsc @kasperpeulen/vitest-plugin-nextjs vitest @vitest/browser-playwright playwright @types/node@24
|
|
71
|
+
# Next's route resolution, pinned to your `next` version
|
|
72
|
+
npm install -D -E @next/routing@$(node -p "require('next/package.json').version")
|
|
73
|
+
npx playwright install chromium
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A few notes:
|
|
77
|
+
|
|
78
|
+
- The plugins are in **alpha**. The API can still change from one release to the next.
|
|
79
|
+
- `@kasperpeulen/vitest-plugin-nextjs` is built on `@kasperpeulen/vitest-plugin-rsc`, which it has as a peer dependency: install both, at the same release.
|
|
80
|
+
- `@next/routing` must match your `next` version exactly. Upgrade them together.
|
|
81
|
+
- Vitest 5 needs `@types/node` 22+, and create-next-app installs 20, hence the bump.
|
|
82
|
+
|
|
83
|
+
### 2. Configure Vitest
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// vitest.config.mts
|
|
87
|
+
import { playwright } from "@vitest/browser-playwright";
|
|
88
|
+
import { defineConfig } from "vitest/config";
|
|
89
|
+
import { vitestPluginRSC } from "@kasperpeulen/vitest-plugin-rsc";
|
|
90
|
+
import { vitestPluginNext } from "@kasperpeulen/vitest-plugin-nextjs";
|
|
91
|
+
|
|
92
|
+
export default defineConfig({
|
|
93
|
+
plugins: [vitestPluginRSC(), vitestPluginNext()],
|
|
94
|
+
test: {
|
|
95
|
+
browser: {
|
|
96
|
+
enabled: true,
|
|
97
|
+
headless: true,
|
|
98
|
+
provider: playwright(),
|
|
99
|
+
instances: [{ browser: "chromium" }],
|
|
100
|
+
},
|
|
101
|
+
include: ["**/*.test.{ts,tsx}"],
|
|
102
|
+
isolate: false,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- No setup file needed: the plugin cleans up around every test.
|
|
108
|
+
- `isolate: false` loads your app's server once per worker instead of once per file. It does mean the test files in a worker share modules, so put mocks in a [setup file](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/nextjs-guide.md#mocks).
|
|
109
|
+
- `.mts` makes the config load as ESM, which matters if your `package.json` has no `"type": "module"`.
|
|
110
|
+
- `include` keeps Playwright's `*.spec.ts` files out.
|
|
111
|
+
- Add `.vitest/` to `.gitignore`. That's where screenshots of failed tests go.
|
|
112
|
+
|
|
113
|
+
### 3. Write a test
|
|
114
|
+
|
|
115
|
+
A Server Component that reads a cookie and bumps it from a Server Action:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
// app/likes.tsx
|
|
119
|
+
import { cookies } from "next/headers";
|
|
120
|
+
|
|
121
|
+
export async function Likes() {
|
|
122
|
+
const likes = Number((await cookies()).get("likes")?.value ?? 0);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<form
|
|
126
|
+
action={async () => {
|
|
127
|
+
"use server";
|
|
128
|
+
(await cookies()).set("likes", String(likes + 1));
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<button>Likes: {likes}</button>
|
|
132
|
+
</form>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
And its test:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// app/likes.test.tsx
|
|
141
|
+
import { expect, test } from "vitest";
|
|
142
|
+
import { page } from "vitest/browser";
|
|
143
|
+
import { renderServer } from "@kasperpeulen/vitest-plugin-nextjs/testing-library";
|
|
144
|
+
import { Likes } from "./likes";
|
|
145
|
+
|
|
146
|
+
test("likes from a Server Action", async () => {
|
|
147
|
+
await renderServer(<Likes />, { headers: { cookie: "likes=41" } });
|
|
148
|
+
|
|
149
|
+
await page.getByRole("button", { name: "Likes: 41" }).click();
|
|
150
|
+
await expect.element(page.getByRole("button", { name: "Likes: 42" })).toBeVisible();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("opens the home page", async () => {
|
|
154
|
+
const { response } = await renderServer({ url: "/" });
|
|
155
|
+
|
|
156
|
+
expect(response.status).toBe(200);
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The first test renders a single component, Testing Library style, but on Next's server. The click runs the Server Action, and Next renders again with the new cookie. The second test opens your home page like a browser would.
|
|
161
|
+
|
|
162
|
+
### 4. Run it
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npx vitest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Vitest starts Chromium and watches your files. Use `npx vitest run` in CI. The first test in each worker is a bit slower, because it loads Next's runtime.
|
|
169
|
+
|
|
170
|
+
## What you can test
|
|
171
|
+
|
|
172
|
+
Pretty much everything Next does. Your files go through Next's own compiler, so `next/font`, `next/image`, `next/dynamic` and styled-jsx just work, and a mistake that would break `next build` breaks your test too. Routes, layouts, `loading.tsx`, error boundaries, redirects, cookies, Server Actions, route handlers, caching and `proxy.ts` all behave as they do in Next.
|
|
173
|
+
|
|
174
|
+
Head to the **[Next.js guide](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/nextjs-guide.md)** for an example of each.
|
|
175
|
+
|
|
176
|
+
For a quicker feedback loop, `VITEST_PLUGIN_RSC_MODE=fast npx vitest` runs in [fast mode](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/nextjs-guide.md#fast-mode): the pages of a test file render in one app, without HTML from the server, prefetching or page loads. Keep CI in fidelity mode, the default. [Fast testing patterns](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/fast-testing-patterns.md) has what each test costs, and what else makes it cheaper.
|
|
177
|
+
|
|
178
|
+
### Databases
|
|
179
|
+
|
|
180
|
+
Browsers have no TCP sockets, so the database runs in the tab: a real engine in WebAssembly. Your app keeps its ORM, queries and migrations. Tests mock just one module, `db.ts`.
|
|
181
|
+
|
|
182
|
+
| | Drizzle | Prisma | Kysely | SQL |
|
|
183
|
+
| -------------------------------- | ------- | ------ | ------ | --- |
|
|
184
|
+
| Postgres, on PGlite | ✅ | ✅ | ✅ | ✅ |
|
|
185
|
+
| SQLite, on `@libsql/client-wasm` | ✅ | ✅ | ✅ | ✅ |
|
|
186
|
+
| MySQL, on MariaDB in WebAssembly | ✅ | ✅ | ✅ | ✅ |
|
|
187
|
+
|
|
188
|
+
There's a Redis recipe for `ioredis` too. See [Databases](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/databases.md).
|
|
189
|
+
|
|
190
|
+
### Storybook
|
|
191
|
+
|
|
192
|
+
[`@kasperpeulen/storybook-nextjs-vite-rsc`](https://github.com/storybookjs/storybook-for-pages/blob/main/packages/storybook-nextjs-vite-rsc) brings the same thing to Storybook. A story can render a whole page of your app, with the same mocks as your tests. Even a static, deployed Storybook carries its database along, so forms really submit.
|
|
193
|
+
|
|
194
|
+
### Without Next.js
|
|
195
|
+
|
|
196
|
+
`vitestPluginRSC()` on its own renders Server Components for any React app. See [RSC without Next.js](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/rsc-without-nextjs.md).
|
|
197
|
+
|
|
198
|
+
## Good to know
|
|
199
|
+
|
|
200
|
+
It's Next's real runtime, but in a browser tab. A few things follow from that:
|
|
201
|
+
|
|
202
|
+
- **No `test.concurrent`.** Tests in one file run one after another. Files still run in parallel.
|
|
203
|
+
- **No native addons or TCP sockets.** Use a [database recipe](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/databases.md), a WebAssembly alternative, or MSW.
|
|
204
|
+
- **Development React by default.** A Server Component's error message reaches the client, where a deployment would send a digest. Use [`build: "production"`](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/nextjs-guide.md#development-or-production) to match a deployment.
|
|
205
|
+
- **Some Next features aren't supported yet**, like `opengraph-image` image rendering, the React Compiler and most custom `webpack` config.
|
|
206
|
+
|
|
207
|
+
The full list is in [Limitations](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/limitations.md).
|
|
208
|
+
|
|
209
|
+
## Documentation
|
|
210
|
+
|
|
211
|
+
**Guides**
|
|
212
|
+
|
|
213
|
+
- [Testing a Next.js app](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/nextjs-guide.md): the main guide.
|
|
214
|
+
- [Testing Client Components](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/use-client-test-files.md) in a `"use client"` test file.
|
|
215
|
+
- [Databases](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/databases.md): Postgres, SQLite, MySQL and Redis in the tab.
|
|
216
|
+
- [Fast testing patterns](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/fast-testing-patterns.md): how to make a suite fast, and what each pattern costs and gives up.
|
|
217
|
+
- [Fast tests](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/fast-tests.md): the measurements behind them, and the quickest feedback loop.
|
|
218
|
+
- [Node.js in the browser](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/nodejs-in-the-browser.md): which Node APIs work, and the in-memory disk.
|
|
219
|
+
- [Static builds](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/static-build.md): `vite build`, and how to serve the result.
|
|
220
|
+
- [RSC without Next.js](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/rsc-without-nextjs.md).
|
|
221
|
+
|
|
222
|
+
**Reference**
|
|
223
|
+
|
|
224
|
+
- [API reference](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/api.md)
|
|
225
|
+
- [Limitations](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/limitations.md)
|
|
226
|
+
|
|
227
|
+
**Under the hood**
|
|
228
|
+
|
|
229
|
+
- [Internals](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/internals/README.md): how it all works, the Next.js conformance suite, benchmarks and releasing. Detailed, and mostly for contributors.
|
|
230
|
+
|
|
231
|
+
## Publishing
|
|
232
|
+
|
|
233
|
+
Release Please and `.github/workflows/release.yml` release the four packages, each with a version of its own: see [Releasing](https://github.com/storybookjs/storybook-for-pages/blob/main/docs/internals/releasing.md). A release goes to npm with npm's trusted publishing, an OIDC token of the workflow, without an npm token.
|
|
234
|
+
|
|
235
|
+
That is set up for all four packages:
|
|
236
|
+
|
|
237
|
+
- Each package exists on npm, from a first publish by hand.
|
|
238
|
+
- Each has a trusted publisher on npmjs.com: GitHub Actions, organization `storybookjs`, repository `storybook-for-pages`, workflow `release.yml`.
|
|
239
|
+
|
|
240
|
+
To check it for a package:
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
npm trust list @kasperpeulen/vitest-plugin-nextjs
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
npm makes no provenance for a package of a private repository, so the workflow publishes without `--provenance`.
|
|
247
|
+
|
|
248
|
+
## Contributing
|
|
249
|
+
|
|
250
|
+
See [CONTRIBUTING.md](https://github.com/storybookjs/storybook-for-pages/blob/main/CONTRIBUTING.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import "node:module";
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __exportAll = (all, no_symbols) => {
|
|
5
|
+
let target = {};
|
|
6
|
+
for (var name in all) __defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true
|
|
9
|
+
});
|
|
10
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
11
|
+
return target;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { __exportAll };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ServerRequest } from "./registry.js";
|
|
2
|
+
|
|
3
|
+
//#region src/action-ids.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The ids of the Server Actions that `request` names. Next's router names the
|
|
6
|
+
* one it calls in the `next-action` header. A form that the browser posts
|
|
7
|
+
* itself, as React renders one for a Server Action, to work before the app
|
|
8
|
+
* has hydrated or without JavaScript, names them in its fields, which Next's
|
|
9
|
+
* `decodeAction()` and `areAllActionIdsValid()` read: a field
|
|
10
|
+
* `$ACTION_ID_<id>`, or `$ACTION_REF_<n>` with the id in the field
|
|
11
|
+
* `$ACTION_<n>:0`, for an action with bound arguments.
|
|
12
|
+
*
|
|
13
|
+
* Next reads the whole body of such a request first, and so does this: from
|
|
14
|
+
* a copy, which leaves `request` a body of its own.
|
|
15
|
+
*/
|
|
16
|
+
declare function actionIdsOf(request: ServerRequest): Promise<string[]>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { actionIdsOf };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { registry } from "./registry.js";
|
|
2
|
+
//#region src/action-ids.ts
|
|
3
|
+
const actionIdField = "$ACTION_ID_";
|
|
4
|
+
const actionReferenceField = "$ACTION_REF_";
|
|
5
|
+
/**
|
|
6
|
+
* The ids of the Server Actions that `request` names. Next's router names the
|
|
7
|
+
* one it calls in the `next-action` header. A form that the browser posts
|
|
8
|
+
* itself, as React renders one for a Server Action, to work before the app
|
|
9
|
+
* has hydrated or without JavaScript, names them in its fields, which Next's
|
|
10
|
+
* `decodeAction()` and `areAllActionIdsValid()` read: a field
|
|
11
|
+
* `$ACTION_ID_<id>`, or `$ACTION_REF_<n>` with the id in the field
|
|
12
|
+
* `$ACTION_<n>:0`, for an action with bound arguments.
|
|
13
|
+
*
|
|
14
|
+
* Next reads the whole body of such a request first, and so does this: from
|
|
15
|
+
* a copy, which leaves `request` a body of its own.
|
|
16
|
+
*/
|
|
17
|
+
async function actionIdsOf(request) {
|
|
18
|
+
if (request.method !== "POST") return [];
|
|
19
|
+
const header = request.headers.get("next-action");
|
|
20
|
+
if (header !== null) return [header];
|
|
21
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
22
|
+
if (!/^multipart\/form-data\b/i.test(contentType)) return [];
|
|
23
|
+
let { body } = request;
|
|
24
|
+
if (body instanceof ReadableStream) [body, request.body] = body.tee();
|
|
25
|
+
let form;
|
|
26
|
+
try {
|
|
27
|
+
form = await new registry.Response(body, { headers: { "content-type": contentType } }).formData();
|
|
28
|
+
} catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const ids = [];
|
|
32
|
+
for (const name of form.keys()) {
|
|
33
|
+
if (name.startsWith(actionIdField)) ids.push(name.slice(11));
|
|
34
|
+
if (!name.startsWith(actionReferenceField)) continue;
|
|
35
|
+
const reference = form.get(`$ACTION_${name.slice(12)}:0`);
|
|
36
|
+
try {
|
|
37
|
+
const { id } = JSON.parse(typeof reference === "string" ? reference : "");
|
|
38
|
+
if (typeof id === "string") ids.push(id);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
return ids;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { actionIdsOf };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NextAdapter } from "next";
|
|
2
|
+
|
|
3
|
+
//#region src/adapter.d.ts
|
|
4
|
+
/** Who waits for the outcome of a build, by the build directory it made up for it. */
|
|
5
|
+
declare const receivers: Map<any, any>;
|
|
6
|
+
declare const adapter: NextAdapter;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { adapter as default, receivers };
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/adapter.ts
|
|
2
|
+
const scope = globalThis;
|
|
3
|
+
/** Who waits for the outcome of a build, by the build directory it made up for it. */
|
|
4
|
+
const receivers = scope.__vitest_plugin_rsc_next_adapter__ ??= /* @__PURE__ */ new Map();
|
|
5
|
+
const adapter = {
|
|
6
|
+
name: "vitest-plugin-rsc",
|
|
7
|
+
onBuildComplete(context) {
|
|
8
|
+
receivers.get(context.distDir)?.(context);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { adapter as default, receivers };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Opened } from "./registry.js";
|
|
2
|
+
import { PageStore } from "./page-store.js";
|
|
3
|
+
|
|
4
|
+
//#region src/app-requests.d.ts
|
|
5
|
+
/** Says which page of the app is open, by its signal, which aborts as it is left. */
|
|
6
|
+
declare function setOpenPage(signal: AbortSignal | undefined): void;
|
|
7
|
+
/** Says whether the router of the pages outlives them, from now on. */
|
|
8
|
+
declare function setRouterOutlivesPage(outlives: boolean): void;
|
|
9
|
+
declare function leftTheApp(url: URL): Error;
|
|
10
|
+
declare function unlessAborted<T>(answer: Promise<T>, signal: AbortSignal, abandon: (answer: Promise<T>) => void): Promise<T>;
|
|
11
|
+
type Sending<Passed = never> = {
|
|
12
|
+
/**
|
|
13
|
+
* The store of the page, as it was when the request started: what it sends
|
|
14
|
+
* besides its own headers, and where the cookies of its responses go. None
|
|
15
|
+
* for a request the server makes to itself while it handles another. That
|
|
16
|
+
* one carries the headers it was given, cookies included, and does not wait
|
|
17
|
+
* for the request it is part of.
|
|
18
|
+
*/
|
|
19
|
+
from: PageStore | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Whether it sends the cookies of the store and keeps the ones it gets: not
|
|
22
|
+
* with `credentials: "omit"`. A `cookie` header of its own it sends either way.
|
|
23
|
+
*/
|
|
24
|
+
credentials?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* A page load, which leaves the app when it is redirected to another
|
|
27
|
+
* origin: one that the test opens, or one of the app's own, sent while the
|
|
28
|
+
* page before it is still open (see `navigate()` in page.ts). That one
|
|
29
|
+
* opens nothing of what the test opened, also after a redirect, and
|
|
30
|
+
* starts no files of its route, which leaving that page would take back.
|
|
31
|
+
* What it opens is its `opened`, after a redirect too: from a page of fast
|
|
32
|
+
* mode's kept app, a page without HTML.
|
|
33
|
+
*/
|
|
34
|
+
navigation?: "test" | "app"; /** Who answers a request that the server has nothing for: see `HandleOptions.unrouted`. */
|
|
35
|
+
network?: () => Promise<Response | NoInfer<Passed>>; /** What the request opens, not after a redirect: see `HandleOptions.opened`. */
|
|
36
|
+
opened?: Opened;
|
|
37
|
+
};
|
|
38
|
+
declare function sendRequest<Passed extends undefined = never>(request: Request, {
|
|
39
|
+
from,
|
|
40
|
+
credentials,
|
|
41
|
+
navigation,
|
|
42
|
+
network,
|
|
43
|
+
opened
|
|
44
|
+
}: Sending<Passed>): Promise<Response | NoInfer<Passed>>;
|
|
45
|
+
type Network<Passed> = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response | Passed>;
|
|
46
|
+
/**
|
|
47
|
+
* The browser's `fetch` while a browser context of the app is open: what
|
|
48
|
+
* Next's client router and Client Components call. `store()` is the store of
|
|
49
|
+
* the context, and `network()` the `fetch` the document had before it, which
|
|
50
|
+
* gets every request that is not the app's, or, where the DOM hands over the
|
|
51
|
+
* page's requests, one that answers none, for the DOM to send. Without a
|
|
52
|
+
* context, `network()` gets every request: code that took this `fetch` in a
|
|
53
|
+
* context has the document's own after it.
|
|
54
|
+
*
|
|
55
|
+
* Next marks the requests it sends for a page, with an `rsc` or a
|
|
56
|
+
* `next-action` header. They belong to the page that is open as they are
|
|
57
|
+
* sent. Its router is there only once its page is, so without a page that
|
|
58
|
+
* page was left, and it sends nothing more. A test asks with
|
|
59
|
+
* `handleRequest()`.
|
|
60
|
+
*/
|
|
61
|
+
declare function pageFetch<Passed extends undefined = never>(store: () => PageStore | undefined, network: () => Network<NoInfer<Passed>>): Network<Passed>;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Sending, leftTheApp, pageFetch, sendRequest, setOpenPage, setRouterOutlivesPage, unlessAborted };
|