@webjsdev/cli 0.10.10 → 0.10.12
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/bin/webjs.js +6 -4
- package/lib/create.js +16 -2
- package/lib/mcp-docs.js +400 -0
- package/lib/mcp-source.js +244 -0
- package/lib/mcp.js +167 -18
- package/package.json +7 -2
- package/resources/AGENTS.md +404 -0
- package/resources/agent-docs/advanced.md +1090 -0
- package/resources/agent-docs/built-ins.md +367 -0
- package/resources/agent-docs/components.md +486 -0
- package/resources/agent-docs/configuration.md +207 -0
- package/resources/agent-docs/framework-dev.md +65 -0
- package/resources/agent-docs/lit-muscle-memory-gotchas.md +456 -0
- package/resources/agent-docs/metadata.md +334 -0
- package/resources/agent-docs/recipes.md +440 -0
- package/resources/agent-docs/service-worker.md +100 -0
- package/resources/agent-docs/ssr-partial-nav-design.md +214 -0
- package/resources/agent-docs/styling.md +235 -0
- package/resources/agent-docs/testing.md +372 -0
- package/resources/agent-docs/typescript.md +334 -0
- package/templates/.dockerignore +6 -4
- package/templates/AGENTS.md +25 -10
- package/templates/CONVENTIONS.md +18 -1
|
@@ -0,0 +1,372 @@
|
|
|
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`.
|