@webjsdev/cli 0.10.12 → 0.10.14

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.
@@ -1,372 +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
- ts-plugin/test/plugin/ tsserver plugin tests (.test.mjs)
75
- test/ cross-package only
76
- ssr/ SSR pipeline (core + server)
77
- actions/ expose() (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
- ### In-repo app tests in CI (#342)
112
-
113
- Each in-repo app (`website`, `examples/blog`) carries its own test suite under
114
- its `test/` dir and runs it through its own `webjs test` script (the website's
115
- `test` runs node + browser; the blog's `test` is node-only). The root runners do
116
- not discover these, so a dedicated `.github/workflows/ci.yml` job, **In-repo app
117
- tests (website + blog)**, runs `npm test --workspace=@webjsdev/website` and
118
- `npm test --workspace=@webjsdev/example-blog` (with Playwright installed for the
119
- website browser tests and the Prisma DB prepared for the blog, the same setup
120
- the `unit` + `e2e` jobs use). It is a required status check, so a regression in
121
- an app's tests gates the merge. The app test dirs are not walked by the root
122
- runner, so the framework-package tests never double-run. `docs` and the
123
- ui-website ship no test suite yet, so they are not in the job.
124
-
125
- ### Adding a new test
126
-
127
- 1. Find the feature folder that matches what you're testing
128
- (`signals`, `routing`, `cache`, `auth`, etc.).
129
- 2. If none exists, create one under the right scope (package vs
130
- root) with the feature's natural name.
131
- 3. Drop the test in directly when it's a node test; nest it
132
- inside `browser/` / `e2e/` / `smoke/` when it's that kind.
133
- 4. Use `.test.js` for ESM packages, `.test.mjs` when the
134
- surrounding package is CJS (e.g. `@webjsdev/ts-plugin`).
135
-
136
- ---
137
-
138
- ## Component test helpers (`@webjsdev/core/testing`)
139
-
140
- `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.
141
-
142
- ### `fixture()` vs `ssrFixture()`
143
-
144
- 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:
145
-
146
- - **`fixture(template)`** waits two macrotasks. Use it for a quick mount where the SSR-then-hydrate distinction does not matter.
147
- - **`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()`).
148
-
149
- `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.
150
-
151
- ```js
152
- import { html } from '@webjsdev/core';
153
- import { ssrFixture, waitForUpdate } from '@webjsdev/core/testing';
154
-
155
- const el = await ssrFixture(html`<my-counter count="5"></my-counter>`);
156
- assert.ok(el.innerHTML.includes('5')); // post-hydration DOM
157
-
158
- el.count = 10;
159
- await waitForUpdate(el); // awaits the real cycle
160
- assert.ok(el.innerHTML.includes('10'));
161
- ```
162
-
163
- **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`.
164
-
165
- ### `assertNoA11yViolations(el, opts?)` (opt-in)
166
-
167
- 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.
168
-
169
- 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`.
170
-
171
- 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 } } }`).
172
-
173
- ```js
174
- import { ssrFixture, assertNoA11yViolations } from '@webjsdev/core/testing';
175
-
176
- const el = await ssrFixture(html`<my-form></my-form>`);
177
- await assertNoA11yViolations(el); // passes a clean subtree
178
-
179
- // a <button> with no accessible name, an <input> with no label, an <img>
180
- // with no alt: each throws a named violation. Worked both-direction tests
181
- // live in packages/core/test/testing/browser/a11y.test.js.
182
- ```
183
-
184
- ---
185
-
186
- ## The handle() test harness (`@webjsdev/server/testing`)
187
-
188
- `createRequestHandler({ appDir }).handle(request)` drives the FULL request
189
- pipeline (middleware, routing, SSR, page actions, server-action RPC, auth +
190
- CSRF) and returns a native `Response`. It is the same entry the framework's own
191
- suite uses, so the most realistic way to test an app is to fire a `Request`
192
- through it and assert on the `Response`, no spawned process and no network.
193
-
194
- `@webjsdev/server/testing` ships THIN builders over that `handle()`. They are
195
- not a test framework: each is a few lines over native `Request` / `Response`,
196
- and they reuse the REAL cookie / header names and the REAL wire serializer (so a
197
- test exercises the production contract, never a parallel fake).
198
-
199
- ```js
200
- import { createRequestHandler } from '@webjsdev/server';
201
- import { testRequest, getCsrf, invokeActionForTest, loginAndGetCookies, withSessionCookie }
202
- from '@webjsdev/server/testing';
203
-
204
- const app = await createRequestHandler({ appDir: process.cwd(), dev: true });
205
- ```
206
-
207
- ### testRequest: fire a request, get the Response
208
-
209
- ```js
210
- const res = await testRequest(app.handle, '/about');
211
- assert.equal(res.status, 200);
212
- assert.match(await res.text(), /About/);
213
- ```
214
-
215
- A bare path (`/about`) is prefixed with a dummy origin (the pipeline only reads
216
- `pathname` + `search`); a full URL string or a pre-built `Request` works too.
217
- The optional third arg is a standard `RequestInit` (method, headers, body).
218
-
219
- ### getCsrf + the auth/session helpers
220
-
221
- The action RPC endpoint requires a `x-webjs-csrf` header matching the
222
- `webjs_csrf` cookie issued on the first SSR response. `getCsrf(handle)` does the
223
- initial GET and returns `{ token, cookie, header }` so a test can send a
224
- CSRF-valid request. `loginAndGetCookies(handle, { email, password })` drives the
225
- REAL credentials login through `handle()` (the `createAuth` route handler) and
226
- captures the genuine signed session `Set-Cookie`, so a follow-up request can hit
227
- a protected route as the logged-in user:
228
-
229
- ```js
230
- // unauthenticated protected route is gated
231
- const gated = await testRequest(app.handle, '/dashboard');
232
- assert.equal(gated.status, 302); // -> /login
233
-
234
- // real login, then reuse the captured cookie
235
- const { cookies } = await loginAndGetCookies(app.handle, { email, password });
236
- const dash = await testRequest(app.handle, '/dashboard', withSessionCookie({}, cookies));
237
- assert.equal(dash.status, 200);
238
- ```
239
-
240
- The session cookie is the production cookie, captured from a real login, never a
241
- hand-built shape. (The default login path is `/api/auth/signin/credentials`, the
242
- route `createAuth`'s handler routes a credentials login through; override
243
- `opts.loginPath` / `opts.body` for a different wiring.)
244
-
245
- ### invokeActionForTest: round-trip an action through the REAL endpoint
246
-
247
- ```js
248
- // modules/posts/actions/create.server.ts exports createPost
249
- const out = await invokeActionForTest(app, 'modules/posts/actions/create.server.ts', 'createPost', [input]);
250
- ```
251
-
252
- `invokeActionForTest` serializes `args` with the webjs serializer (exactly as
253
- the generated client stub does), POSTs them to the REAL
254
- `/__webjs/action/<hash>/<fn>` endpoint with a valid CSRF cookie + header, and
255
- parses the response with the serializer. The action is addressed by the SHA-256
256
- hash of its `.server.{js,ts}` file path (absolute or appDir-relative) plus the
257
- function name, the same scheme the stub uses (`actionEndpoint(appDir, file, fn)`
258
- returns that path if you need it directly).
259
-
260
- **Prefer this over a direct import of the action.** A direct import calls the
261
- function in-process and bypasses three production concerns the endpoint
262
- enforces:
263
-
264
- - **the wire serializer** (a `Date` / `Map` / `BigInt` arg or return is
265
- genuinely encoded + decoded, not passed by reference),
266
- - **CSRF** (a missing token is a 403),
267
- - **prod error sanitization** (a thrown error surfaces as a sanitized
268
- message-only payload, never the stack or extra error fields).
269
-
270
- So `invokeActionForTest` catches a serializer / CSRF / error-sanitization
271
- regression a direct import cannot see. For the negative cases (assert a 403 on
272
- missing CSRF, or inspect a sanitized 500 body), `rawActionRequest(...)` returns
273
- the raw `Response` and never throws on a non-2xx; pass `{ omitCsrf: true }` to
274
- deliberately drop the CSRF pair.
275
-
276
- The saas scaffold's `test/auth/auth.test.ts` is a worked example: it drives the
277
- unauthenticated-redirect gate, then a real signup -> login -> dashboard flow
278
- through `handle()` using these helpers.
279
-
280
- ---
281
-
282
- ## App layout (what users get)
283
-
284
- A scaffolded webjs app has one `test/` directory at its root,
285
- shaped the same way:
286
-
287
- ```
288
- test/
289
- auth/
290
- auth.test.ts # signup / login / currentUser
291
- password.test.ts # scrypt hash + verify
292
- browser/login-form.test.js # only if exercising DOM
293
- posts/
294
- posts.test.ts
295
- browser/post-editor.test.js
296
- hello/
297
- hello.test.ts # the scaffold's starter test
298
- browser/hello.test.js
299
- e2e/hello.test.ts
300
- ```
301
-
302
- App-side runners:
303
-
304
- - `webjs test` → node tests (everything not under `browser/` or `e2e/`).
305
- - `webjs test --browser` → web-test-runner against `test/**/browser/**`.
306
- - `WEBJS_E2E=1 webjs test` adds e2e.
307
-
308
- App AI agents read this convention through the scaffold's
309
- `AGENTS.md` and `CONVENTIONS.md`. The scaffold's
310
- `web-test-runner.config.js` globs `test/**/browser/**/*.test.js`.
311
-
312
- ---
313
-
314
- ## Choosing where a test goes
315
-
316
- A short decision flow:
317
-
318
- 1. **Does it boot more than one webjs package?**
319
- - Yes → root `test/<feature>/`.
320
- - No → `packages/<the-one-package>/test/<feature>/`.
321
- 2. **Does it need a browser?**
322
- - Yes → `…/<feature>/browser/<name>.test.js`.
323
- - No → `…/<feature>/<name>.test.{js,ts,mjs}`.
324
- 3. **Does it spawn a real process (server, CLI subprocess)?**
325
- - Yes → `…/<feature>/e2e/<name>.test.{ts,mjs}` (and gate
326
- behind `WEBJS_E2E=1` if it's slow).
327
- 4. **Is it a fast post-deploy "does the surface still work" check?**
328
- - Yes → `…/<feature>/smoke/<name>.test.{js,ts}`.
329
-
330
- If the answer to "what feature is this?" is "framework
331
- internals" or "misc", pick the user-facing concern instead
332
- (`routing`, `serializer`, `slots`). If you genuinely cannot pick
333
- a feature, the test is probably testing too many things at once.
334
-
335
- ---
336
-
337
- ## What NOT to do
338
-
339
- - **Don't recreate the old `test/{unit,browser,e2e}/` shape.**
340
- Kind is a child of feature, not the other way around.
341
- - **Don't create empty kind folders.** If `e2e/` has no tests
342
- yet, leave it absent.
343
- - **Don't put package-only tests under root `test/`.** It hurts
344
- the per-package `npm test --workspace=…` workflow.
345
- - **Don't import from another package's `test/` directory.**
346
- Test code is not a public surface.
347
- - **Don't add `.unit` / `.integration` filename suffixes.** The
348
- folder tells you the kind; the filename should match what it
349
- tests.
350
-
351
- ---
352
-
353
- ## Verifying UI and theming changes
354
-
355
- For anything visual (layout, components, themes), a passing unit test or a
356
- clean-looking code diff is not enough. Render it in a real browser and look.
357
-
358
- - **Test both light AND dark mode.** Light mode passing proves nothing about
359
- dark mode. The scaffold drives dark mode through two signals (a `data-theme`
360
- attribute for the editorial chrome and a `.dark` class for the ui-* kit, see
361
- `agent-docs/styling.md`), and when neither is set both default to a
362
- coincidentally matching light, so a desync only shows once dark is active.
363
- Emulate dark (Playwright `newContext({ colorScheme: 'dark' })` or flip the
364
- theme toggle) and inspect a component's **computed** `background-color` /
365
- `color`, not just the page chrome.
366
- - **Read the screenshot.** Capture `page.screenshot({ fullPage: true })` and
367
- open the PNG; white-on-white or a stray light box is obvious visually and
368
- invisible in the markup.
369
- - **Guard the wiring in a fast test where you can.** A runtime cascade bug
370
- needs a browser, but the mechanism that triggers it (e.g. the theme toggle
371
- setting `.dark`) can be asserted cheaply. See the dark-mode assertions in
372
- `test/scaffolds/scaffold-integration.test.js`.