@webjsdev/mcp 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/resources/AGENTS.md +169 -77
  4. package/resources/SKILL.md +226 -0
  5. package/resources/references/auth-and-sessions.md +220 -0
  6. package/resources/references/built-ins.md +200 -0
  7. package/resources/references/client-router-and-streaming.md +204 -0
  8. package/resources/references/components.md +167 -0
  9. package/resources/references/data-and-actions.md +187 -0
  10. package/resources/references/muscle-memory-gotchas.md +170 -0
  11. package/resources/references/optimistic-ui.md +128 -0
  12. package/resources/references/routing-and-pages.md +158 -0
  13. package/resources/references/runtime.md +80 -0
  14. package/resources/references/service-worker.md +78 -0
  15. package/resources/references/styling.md +123 -0
  16. package/resources/references/testing.md +125 -0
  17. package/resources/references/typescript.md +148 -0
  18. package/src/mcp-docs.js +27 -14
  19. package/src/mcp-source.js +12 -2
  20. package/src/mcp.js +4 -3
  21. package/resources/agent-docs/advanced.md +0 -1251
  22. package/resources/agent-docs/built-ins.md +0 -379
  23. package/resources/agent-docs/components.md +0 -712
  24. package/resources/agent-docs/configuration.md +0 -284
  25. package/resources/agent-docs/framework-dev.md +0 -65
  26. package/resources/agent-docs/lit-muscle-memory-gotchas.md +0 -470
  27. package/resources/agent-docs/metadata.md +0 -334
  28. package/resources/agent-docs/recipes.md +0 -528
  29. package/resources/agent-docs/runtime.md +0 -65
  30. package/resources/agent-docs/service-worker.md +0 -100
  31. package/resources/agent-docs/styling.md +0 -279
  32. package/resources/agent-docs/testing.md +0 -438
  33. package/resources/agent-docs/typescript.md +0 -402
@@ -1,438 +0,0 @@
1
- # Testing in webjs
2
-
3
- This doc covers two audiences:
4
-
5
- 1. **Framework agents** working inside this monorepo (`packages/*`,
6
- the root `test/`, the e2e suite, the dev server).
7
- 2. **App agents** working inside a webjs app scaffolded with
8
- `webjs create`. App-side conventions also live in the
9
- scaffold's `CONVENTIONS.md` (which is the authoritative,
10
- user-customisable copy); this section here is the
11
- framework-level reference.
12
-
13
- Both audiences follow the same shape: **feature folders are
14
- primary, test kind is a subfolder inside the feature only when
15
- that kind is actually present**. The layout mirrors webjs's own
16
- `modules/<feature>/` convention in apps (feature first, kind
17
- second), so the mental model transfers between framework work and
18
- app work.
19
-
20
- ---
21
-
22
- ## Test kinds
23
-
24
- | Kind | Location | What it does | Runner |
25
- |---|---|---|---|
26
- | **unit + integration** (node) | `test/<feature>/<name>.test.{js,ts,mjs}` | Imports modules and asserts. No spawned process, no network. Includes both true unit tests and in-process integration. | `node --test` |
27
- | **browser** | `test/<feature>/browser/<name>.test.js` | Runs in real Chromium with Playwright. Real DOM, events, `adoptedStyleSheets`, `IntersectionObserver`, shadow / light DOM. | web-test-runner (`wtr`) |
28
- | **e2e** | `test/<feature>/e2e/<name>.test.{ts,mjs}` | Boots a real process (dev server, CLI binary) and drives it through its public interface (HTTP, browser, stdout). Opt in with `WEBJS_E2E=1`. | `node --test` (driver) |
29
- | **smoke** | `test/<feature>/smoke/<name>.test.{js,ts}` | Fast deploy-time sanity check. A subset of e2e in spirit, kept separate so it runs on every deploy. | `node --test` |
30
-
31
- Some packages will only have one or two of these kinds (e.g.
32
- `@webjsdev/core` is a library, no `e2e/` or `smoke/`). Only create
33
- a kind subfolder when there's at least one test of that kind.
34
- Empty `e2e/` folders are anti-patterns.
35
-
36
- ---
37
-
38
- ## Framework layout (this repo)
39
-
40
- ```
41
- packages/
42
- core/test/
43
- signals/ signal primitive, computed, watcher
44
- signal.test.js
45
- signal-spec-conformance.test.js
46
- signal-ssr.test.js
47
- browser/ SignalWatcher + DOM
48
- signal-component.test.js
49
- signal-hydration.test.js
50
- watch-directive.test.js
51
- rendering/
52
- render-server.test.js
53
- render-client.test.js
54
- browser/
55
- render-client.test.js
56
- slots/
57
- browser/
58
- slot.test.js
59
- slot-projection-cycle.test.js
60
- directives/
61
- directives.test.js
62
- browser/
63
- directives-cache.test.js
64
- directives-ref.test.js
65
- …
66
- lifecycle/, context/, suspense/, task/, …
67
- server/test/
68
- routing/ api/ actions/ auth/
69
- cache/ check/ csrf/ session/
70
- …
71
- cli/test/ scaffold validation only
72
- ui/test/ schema, resolver, project-detect, components
73
- components/browser/ real-browser tests for the kit
74
- intellisense/test/plugin/ tsserver plugin tests (.test.mjs)
75
- test/ cross-package only
76
- ssr/ SSR pipeline (core + server)
77
- actions/ server actions (core + server)
78
- serialization/ json-negotiation (core + server)
79
- scaffolds/ full app boots from CLI scaffolds
80
- blog/ examples/blog smoke + browser e2e
81
- smoke/blog-smoke.test.js
82
- browser/blog.test.js
83
- e2e/ puppeteer-driven full-app journeys
84
- docs/ docs site validation
85
- types/ type-check fixtures
86
- ```
87
-
88
- Rule of thumb for **package vs root**: if the test imports from
89
- `@webjsdev/server` (or any other package) at all, it's
90
- cross-package and stays at root. If it only imports from one
91
- package, it goes in that package's `test/`.
92
-
93
- ### Drivers
94
-
95
- - `npm test` → `scripts/run-node-tests.js`: enumerates every
96
- `*.test.{js,mjs}` under `packages/*/test/` and `test/`,
97
- excluding anything under a `browser/` or `e2e/` segment. Those
98
- run via their own scripts.
99
- - `npm run test:browser` → `wtr`: globs
100
- `packages/*/test/**/browser/**/*.test.js` and
101
- `test/**/browser/**/*.test.js`. The blog browser e2e is
102
- excluded (it needs the blog dev server up first).
103
- - `npm run test:e2e` → `WEBJS_E2E=1 node --test test/e2e/e2e.test.mjs`.
104
- - `npm run test:all` runs node + browser (not e2e).
105
-
106
- The root drivers above ONLY discover the framework packages and the root
107
- cross-package suite (`packages/*/test/` + `test/`). They do NOT walk the in-repo
108
- apps' own test dirs (`website/test/`, `examples/blog/test/`), so those run from
109
- each app's OWN `webjs test` script, not the root runner.
110
-
111
- ### The Bun test matrix (#509)
112
-
113
- webjs runs on Node 24+ or Bun (#508). The **Node suite (`npm test`) is the
114
- source of truth**; a separate, additive **Bun matrix** re-runs the
115
- runtime-sensitive suite under Bun to catch the long tail of cross-runtime
116
- incompatibilities (a `node:*` API Bun implements differently, a crypto/stream
117
- edge case, an error-message-format quirk).
118
-
119
- **Bun parity is part of the definition of done, not an afterthought.** A change
120
- to a runtime-sensitive surface (the serializer, the node:http vs `Bun.serve`
121
- listener + request path, SSR / action / CSRF dispatch, streams, `node:crypto`,
122
- the TS stripper, auth / session / cors) is NOT done until you have run the Bun
123
- matrix green AND added or updated a `test/bun/<feature>.mjs` cross-runtime
124
- assertion for the surface. This is enforced at commit time by
125
- `.claude/hooks/require-bun-parity-with-runtime-src.sh`, which BLOCKS a commit
126
- that stages runtime-sensitive `packages/*/src` with no `test/bun/**` test (the
127
- escape hatch `WEBJS_BUN_VERIFIED=1` is only for when an existing `test/bun`
128
- script already covers the surface AND you ran it under Bun).
129
-
130
- - `node scripts/run-bun-tests.js` (needs `bun` on PATH; `BUN=…` overrides) runs
131
- the `node:test` files under `test/`, `packages/core/test/`, and
132
- `packages/server/test/` (excluding `browser/`, `e2e/`, the network-bound
133
- `vendor/`) file by file via `bun test <file>`, and CLASSIFIES each result:
134
- **pass**, **skip(node-only)** (a documented file that asserts Node-only
135
- behavior or trips a Bun-test-runner quirk, listed with a reason in the
136
- script's `DENYLIST`, its Bun behavior covered elsewhere), **skip(harness)**
137
- (a Bun `node:test` compat gap, auto-detected by error signature),
138
- **skip(env)** (needs Redis / a DOM), and **genuine fail** (a real Bun
139
- failure, which fails the job). Set `WEBJS_BUN_TESTS=<substr,…>` to scope a
140
- local run.
141
- - CI runs it in the `bun` job alongside `test/bun/smoke.mjs` (#508),
142
- `test/bun/listener.mjs` (#511, the listener-shell parity), and
143
- `test/bun/timeouts.mjs` (#663, the `Bun.serve` `idleTimeout` wiring; the pure
144
- ms-to-seconds mapping is unit-tested under the matrix too).
145
- - Two cross-runtime test scripts also run under BOTH runtimes: `test/bun/smoke.mjs`
146
- (boot + SSR + TS strip + a server-action RPC) and `test/bun/listener.mjs`
147
- (`startServer` over a real socket: SSR + route + SSE + WebSocket). Plain assert
148
- scripts (not `node:test`) so the same file runs identically on each runtime.
149
-
150
- **Dev hot-reload of server modules (cross-runtime, #514).** webjs's dev server
151
- re-imports a `route.ts` / `.server.ts` / page module per request with a
152
- `?t=<timestamp>` query cache-bust to pick up edits. Node honors that query and
153
- re-imports, so under `node --watch` an edit is picked up live. Bun's ESM loader
154
- IGNORES the query string and exposes no module-eviction API, so that mechanism
155
- alone leaves a server-side edit stale on Bun. The CLI closes the gap by
156
- re-execing `webjs dev` under `bun --hot` on Bun (vs `node --watch` on Node),
157
- whose file-watching cache invalidation makes the next re-import fresh with no
158
- restart (`Bun.serve` is reused across hot reloads, so the listener is not
159
- duplicated). The cross-runtime test `test/bun/dev-hot-reload.mjs` proves the
160
- edit-picked-up behaviour on BOTH runtimes (and runs as a dedicated CI step on
161
- Bun). The `api` dev-cache-bust UNIT test asserts the bare server-level `?t=`
162
- mechanism directly (no supervisor), which Bun ignores by design, so it stays on
163
- the Bun-matrix denylist; the user-facing hot reload it underpins is covered on
164
- Bun by the dev-hot-reload script instead. A `--no-hot` flag opts out of the
165
- supervisor on either runtime (run the server in-process, edits need a manual
166
- restart). Component / page / layout SOURCE edits already hot-reload on both
167
- runtimes (the served `.ts`/`.js` is read from disk per request, not imported).
168
-
169
- ### In-repo app tests in CI (#342)
170
-
171
- Each in-repo app (`website`, `examples/blog`) carries its own test suite under
172
- its `test/` dir and runs it through its own `webjs test` script (the website's
173
- `test` runs node + browser; the blog's `test` is node-only). The root runners do
174
- not discover these, so a dedicated `.github/workflows/ci.yml` job, **In-repo app
175
- tests (website + blog)**, runs `npm test --workspace=@webjsdev/website` and
176
- `npm test --workspace=@webjsdev/example-blog` (with Playwright installed for the
177
- website browser tests and the SQLite DB prepared for the blog, the same setup
178
- the `unit` + `e2e` jobs use). It is a required status check, so a regression in
179
- an app's tests gates the merge. The app test dirs are not walked by the root
180
- runner, so the framework-package tests never double-run. `docs` and the
181
- ui-website ship no test suite yet, so they are not in the job.
182
-
183
- ### Adding a new test
184
-
185
- 1. Find the feature folder that matches what you're testing
186
- (`signals`, `routing`, `cache`, `auth`, etc.).
187
- 2. If none exists, create one under the right scope (package vs
188
- root) with the feature's natural name.
189
- 3. Drop the test in directly when it's a node test; nest it
190
- inside `browser/` / `e2e/` / `smoke/` when it's that kind.
191
- 4. Use `.test.js` for ESM packages, `.test.mjs` when the
192
- surrounding package is CJS (e.g. `@webjsdev/intellisense`).
193
-
194
- ---
195
-
196
- ## Component test helpers (`@webjsdev/core/testing`)
197
-
198
- `import { fixture, ssrFixture, waitForUpdate, assertNoA11yViolations, click, shadowQuery, shadowQueryAll } from '@webjsdev/core/testing'`. The mount + hydrate + a11y helpers run in the WTR Chromium session (real DOM), thin wrappers over the browser already running.
199
-
200
- ### `fixture()` vs `ssrFixture()`
201
-
202
- Both server-render an `html\`…\`` template (via `renderToString`, with DSD) and set the markup into a container so the browser upgrades the custom element. The difference is how they wait:
203
-
204
- - **`fixture(template)`** waits two macrotasks. Use it for a quick mount where the SSR-then-hydrate distinction does not matter.
205
- - **`ssrFixture(template)`** awaits the element's NATIVE `updateComplete` promise (the real render-cycle resolution), not a timer, so the post-hydration DOM is observable deterministically. It is the documented SSR + hydrate entry. Its contract: the SSR'd markup and the post-hydration DOM agree, so a hydration mismatch (server renders one thing, client another) is observable by comparing the SSR'd inner HTML against `el.innerHTML` / `el.shadowRoot.innerHTML` after it resolves. The component class must already be registered (the test imports its module, same as `fixture()`).
206
-
207
- `waitForUpdate(el)` now also awaits the native `updateComplete` when present (falling back to a macrotask flush for a plain element), so a re-render after a property assignment or signal `set()` settles deterministically.
208
-
209
- ```js
210
- import { html } from '@webjsdev/core';
211
- import { ssrFixture, waitForUpdate } from '@webjsdev/core/testing';
212
-
213
- const el = await ssrFixture(html`<my-counter count="5"></my-counter>`);
214
- assert.ok(el.innerHTML.includes('5')); // post-hydration DOM
215
-
216
- el.count = 10;
217
- await waitForUpdate(el); // awaits the real cycle
218
- assert.ok(el.innerHTML.includes('10'));
219
- ```
220
-
221
- **Hydration-mismatch pattern.** To assert SSR and the hydrated DOM agree, normalise the SSR string (strip the `<!--webjs-hydrate-->` marker, `data-webjs-prop-*` attributes, part comments) and compare against the live `el.innerHTML`. The counterfactual is a component whose `render()` is non-deterministic across the SSR call and the hydration render; `ssrFixture` returns the live hydrated element, so the divergence is detectable. The worked tests live in `packages/core/test/testing/browser/ssr-fixture.test.js`, alongside the broader SSR-vs-client parity corpus in `packages/core/test/rendering/browser/ssr-client-parity.test.js`.
222
-
223
- **Testing async render + streaming + error isolation (#469 / #471 / #473).** Split by layer:
224
- - **SSR async render + error isolation** (unit, node, `renderToString`): assert an `async render()` bakes the resolved DATA into the HTML with no fallback markup, and that a throwing component renders a component-scoped error state while siblings render (worked in `packages/core/test/suspense/async-render-ssr.test.js`).
225
- - **`<webjs-suspense>` streaming** (unit, node, `renderToStream` + a `suspenseCtx`): collect the stream and assert the fallback flushes first, then the `<template data-webjs-resolve>` boundaries, and that multiple boundaries fetch concurrently via a timing assertion that fails if serial (worked in `packages/core/test/suspense/webjs-suspense-ssr.test.js`).
226
- - **Client async render** (browser, WTR): mount a component and `await el.updateComplete`, then assert stale-while-revalidate keeps prior content during a gated re-fetch, `renderFallback()` shows only on a re-fetch (never first paint), a rejected render commits `renderError()`, and the race guard drops a superseded resolution. Use a gate promise inside `render()` to control timing (worked in `packages/core/test/suspense/browser/async-render-client.test.js`).
227
- - **Progressive soft-nav streaming** (e2e, `WEBJS_E2E=1`): soft-navigate to a streamed page and assert the fallback is live in the DOM at the moment the URL advances (a buffered swap would only advance the URL once the boundary already resolved), then that the boundary streams in. The DOM-free reader helpers are unit-tested in `packages/core/test/routing/progressive-stream.test.js`.
228
-
229
- ### `assertNoA11yViolations(el, opts?)` (opt-in)
230
-
231
- An OPT-IN accessibility assertion that runs the standard axe-core engine against an element's subtree in the WTR Chromium session. Nothing calls it for you, it is never a forced gate.
232
-
233
- axe-core is a TEST-ONLY peer, imported dynamically by the helper, so it is NOT a hard dependency of `@webjsdev/core`. Install it where you run the test (`npm install -D axe-core`; the scaffold and this repo already ship it). If it is missing, the helper throws a clear message: `assertNoA11yViolations needs axe-core. Install it: npm install -D axe-core`.
234
-
235
- On zero violations it resolves; on a violation it throws an Error whose message lists each violation's id, impact, a short help string, and the failing nodes' selectors, so the failure is actionable. `opts` passes through to `axe.run` (e.g. `{ rules: { 'color-contrast': { enabled: false } } }`).
236
-
237
- ```js
238
- import { ssrFixture, assertNoA11yViolations } from '@webjsdev/core/testing';
239
-
240
- const el = await ssrFixture(html`<my-form></my-form>`);
241
- await assertNoA11yViolations(el); // passes a clean subtree
242
-
243
- // a <button> with no accessible name, an <input> with no label, an <img>
244
- // with no alt: each throws a named violation. Worked both-direction tests
245
- // live in packages/core/test/testing/browser/a11y.test.js.
246
- ```
247
-
248
- ---
249
-
250
- ## The handle() test harness (`@webjsdev/server/testing`)
251
-
252
- `createRequestHandler({ appDir }).handle(request)` drives the FULL request
253
- pipeline (middleware, routing, SSR, page actions, server-action RPC, auth +
254
- CSRF) and returns a native `Response`. It is the same entry the framework's own
255
- suite uses, so the most realistic way to test an app is to fire a `Request`
256
- through it and assert on the `Response`, no spawned process and no network.
257
-
258
- `@webjsdev/server/testing` ships THIN builders over that `handle()`. They are
259
- not a test framework: each is a few lines over native `Request` / `Response`,
260
- and they reuse the REAL cookie / header names and the REAL wire serializer (so a
261
- test exercises the production contract, never a parallel fake).
262
-
263
- ```js
264
- import { createRequestHandler } from '@webjsdev/server';
265
- import { testRequest, invokeActionForTest, rawActionRequest, loginAndGetCookies, withSessionCookie }
266
- from '@webjsdev/server/testing';
267
-
268
- const app = await createRequestHandler({ appDir: process.cwd(), dev: true });
269
- ```
270
-
271
- ### testRequest: fire a request, get the Response
272
-
273
- ```js
274
- const res = await testRequest(app.handle, '/about');
275
- assert.equal(res.status, 200);
276
- assert.match(await res.text(), /About/);
277
- ```
278
-
279
- A bare path (`/about`) is prefixed with a dummy origin (the pipeline only reads
280
- `pathname` + `search`); a full URL string or a pre-built `Request` works too.
281
- The optional third arg is a standard `RequestInit` (method, headers, body).
282
-
283
- ### The auth/session helpers
284
-
285
- Server-action CSRF is an Origin / `Sec-Fetch-Site` check, so a test needs no
286
- CSRF setup: `invokeActionForTest` models a same-origin browser POST and passes
287
- the check automatically, and `rawActionRequest(app, file, fn, args, {
288
- crossOrigin: true })` models a cross-site request to assert the 403.
289
- `loginAndGetCookies(handle, { email, password })` drives the
290
- REAL credentials login through `handle()` (the `createAuth` route handler) and
291
- captures the genuine signed session `Set-Cookie`, so a follow-up request can hit
292
- a protected route as the logged-in user:
293
-
294
- ```js
295
- // unauthenticated protected route is gated
296
- const gated = await testRequest(app.handle, '/dashboard');
297
- assert.equal(gated.status, 302); // -> /login
298
-
299
- // real login, then reuse the captured cookie
300
- const { cookies } = await loginAndGetCookies(app.handle, { email, password });
301
- const dash = await testRequest(app.handle, '/dashboard', withSessionCookie({}, cookies));
302
- assert.equal(dash.status, 200);
303
- ```
304
-
305
- The session cookie is the production cookie, captured from a real login, never a
306
- hand-built shape. (The default login path is `/api/auth/signin/credentials`, the
307
- route `createAuth`'s handler routes a credentials login through; override
308
- `opts.loginPath` / `opts.body` for a different wiring.)
309
-
310
- ### invokeActionForTest: round-trip an action through the REAL endpoint
311
-
312
- ```js
313
- // modules/posts/actions/create.server.ts exports createPost
314
- const out = await invokeActionForTest(app, 'modules/posts/actions/create.server.ts', 'createPost', [input]);
315
- ```
316
-
317
- `invokeActionForTest` serializes `args` with the webjs serializer (exactly as
318
- the generated client stub does), POSTs them to the REAL
319
- `/__webjs/action/<hash>/<fn>` endpoint as a same-origin request (so it passes
320
- the Origin / Sec-Fetch-Site CSRF check), and parses the response with the
321
- serializer. The action is addressed by the SHA-256
322
- hash of its `.server.{js,ts}` file path (absolute or appDir-relative) plus the
323
- function name, the same scheme the stub uses (`actionEndpoint(appDir, file, fn)`
324
- returns that path if you need it directly).
325
-
326
- **Prefer this over a direct import of the action.** A direct import calls the
327
- function in-process and bypasses three production concerns the endpoint
328
- enforces:
329
-
330
- - **the wire serializer** (a `Date` / `Map` / `BigInt` arg or return is
331
- genuinely encoded + decoded, not passed by reference),
332
- - **CSRF** (a missing token is a 403),
333
- - **prod error sanitization** (a thrown error surfaces as a sanitized
334
- message-only payload, never the stack or extra error fields).
335
-
336
- So `invokeActionForTest` catches a serializer / CSRF / error-sanitization
337
- regression a direct import cannot see. For the negative cases (assert a 403 on
338
- missing CSRF, or inspect a sanitized 500 body), `rawActionRequest(...)` returns
339
- the raw `Response` and never throws on a non-2xx; pass `{ omitCsrf: true }` to
340
- deliberately drop the CSRF pair.
341
-
342
- The saas scaffold's `test/auth/auth.test.ts` is a worked example: it drives the
343
- unauthenticated-redirect gate, then a real signup -> login -> dashboard flow
344
- through `handle()` using these helpers.
345
-
346
- ---
347
-
348
- ## App layout (what users get)
349
-
350
- A scaffolded webjs app has one `test/` directory at its root,
351
- shaped the same way:
352
-
353
- ```
354
- test/
355
- auth/
356
- auth.test.ts # signup / login / currentUser
357
- password.test.ts # scrypt hash + verify
358
- browser/login-form.test.js # only if exercising DOM
359
- posts/
360
- posts.test.ts
361
- browser/post-editor.test.js
362
- hello/
363
- hello.test.ts # the scaffold's starter test
364
- browser/hello.test.js
365
- e2e/hello.test.ts
366
- ```
367
-
368
- App-side runners:
369
-
370
- - `webjs test` → the runtime's test runner over everything not under `browser/` or `e2e/` (#570): `node --test` on Node, `bun test` on Bun (dispatched on `process.versions.bun`, since `bun --test` is invalid).
371
- - `webjs test --browser` → web-test-runner against `test/**/browser/**` (the app's resolved `wtr` bin, spawned with the current runtime, no `npx`).
372
- - `WEBJS_E2E=1 webjs test` adds e2e.
373
-
374
- App AI agents read this convention through the scaffold's
375
- `AGENTS.md` and `CONVENTIONS.md`. The scaffold's
376
- `web-test-runner.config.js` globs `test/**/browser/**/*.test.js`.
377
-
378
- ---
379
-
380
- ## Choosing where a test goes
381
-
382
- A short decision flow:
383
-
384
- 1. **Does it boot more than one webjs package?**
385
- - Yes → root `test/<feature>/`.
386
- - No → `packages/<the-one-package>/test/<feature>/`.
387
- 2. **Does it need a browser?**
388
- - Yes → `…/<feature>/browser/<name>.test.js`.
389
- - No → `…/<feature>/<name>.test.{js,ts,mjs}`.
390
- 3. **Does it spawn a real process (server, CLI subprocess)?**
391
- - Yes → `…/<feature>/e2e/<name>.test.{ts,mjs}` (and gate
392
- behind `WEBJS_E2E=1` if it's slow).
393
- 4. **Is it a fast post-deploy "does the surface still work" check?**
394
- - Yes → `…/<feature>/smoke/<name>.test.{js,ts}`.
395
-
396
- If the answer to "what feature is this?" is "framework
397
- internals" or "misc", pick the user-facing concern instead
398
- (`routing`, `serializer`, `slots`). If you genuinely cannot pick
399
- a feature, the test is probably testing too many things at once.
400
-
401
- ---
402
-
403
- ## What NOT to do
404
-
405
- - **Don't recreate the old `test/{unit,browser,e2e}/` shape.**
406
- Kind is a child of feature, not the other way around.
407
- - **Don't create empty kind folders.** If `e2e/` has no tests
408
- yet, leave it absent.
409
- - **Don't put package-only tests under root `test/`.** It hurts
410
- the per-package `npm test --workspace=…` workflow.
411
- - **Don't import from another package's `test/` directory.**
412
- Test code is not a public surface.
413
- - **Don't add `.unit` / `.integration` filename suffixes.** The
414
- folder tells you the kind; the filename should match what it
415
- tests.
416
-
417
- ---
418
-
419
- ## Verifying UI and theming changes
420
-
421
- For anything visual (layout, components, themes), a passing unit test or a
422
- clean-looking code diff is not enough. Render it in a real browser and look.
423
-
424
- - **Test both light AND dark mode.** Light mode passing proves nothing about
425
- dark mode. The scaffold drives dark mode through two signals (a `data-theme`
426
- attribute for the editorial chrome and a `.dark` class for the ui-* kit, see
427
- `agent-docs/styling.md`), and when neither is set both default to a
428
- coincidentally matching light, so a desync only shows once dark is active.
429
- Emulate dark (Playwright `newContext({ colorScheme: 'dark' })` or flip the
430
- theme toggle) and inspect a component's **computed** `background-color` /
431
- `color`, not just the page chrome.
432
- - **Read the screenshot.** Capture `page.screenshot({ fullPage: true })` and
433
- open the PNG; white-on-white or a stray light box is obvious visually and
434
- invisible in the markup.
435
- - **Guard the wiring in a fast test where you can.** A runtime cascade bug
436
- needs a browser, but the mechanism that triggers it (e.g. the theme toggle
437
- setting `.dark`) can be asserted cheaply. See the dark-mode assertions in
438
- `test/scaffolds/scaffold-integration.test.js`.