You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: document the testing setup in CLAUDE.md #86exfn1e3
Four updates, all aligned with the doc's existing voice:
- Quick start: add yarn test / test:watch / test:e2e / typecheck to
the script list. Flip npm → yarn to match the actual lockfile.
- New ## Testing section before ## Verification checklist. Covers:
stack table (Vitest unit/component, Playwright e2e, tsc wrapper);
Vitest setup (chrome shim, postcss bypass rationale); Playwright
setup (extension fixture, fixtures server, fixture pages, no real
YouTube/Netflix); the critical Chromium flags with a "changing
them breaks CI silently" warning around --headless=new (forces
full Chromium, otherwise chrome-headless-shell skips extensions);
the CI verify gate step list with a heredoc-stub-or-mixpanel-throws
warning around .env.production; typecheck wrapper rationale (the
pilotui + dashboard-app suppression list); full test file map; and
totals (79 unit + 11 e2e, ~15s warm).
- ## Verification checklist: re-framed as "most of this is automated"
with each bullet now cross-referencing the spec file that pins it.
Residual manual items reduced to YouTube/Netflix subtitle behaviour
and the popup full-page lifecycle.
- ## Useful pointers: add Vitest config, tests/setup.ts, playwright
config, extension fixture, fixtures server, typecheck wrapper, and
the Vue 3 SFC ambient declaration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
yarn test:e2e # Playwright E2E against the loaded extension (requires dist/)
15
+
yarn typecheck # tsc --noEmit via the upstream-error filter
11
16
```
12
17
13
18
Load `dist/` as an unpacked extension at `chrome://extensions`. There is no separate dev server — the bundler writes straight to `dist/`, and Chrome reloads when you click the reload button on the extension card.
| E2E (real Chromium with the unpacked extension loaded) |`@playwright/test` + `chromium.launchPersistentContext({ args: ['--load-extension=dist'] })`|[tests/e2e/](tests/e2e/) — `*.spec.ts`|
247
+
| Static type |`tsc --noEmit` via [scripts/typecheck.mjs](scripts/typecheck.mjs)| (whole repo) |
248
+
249
+
The `.test.ts` / `.spec.ts` split keeps Vitest and Playwright from fighting over file ownership — Vitest's `exclude` config drops everything matching `**/*.spec.ts` and `tests/e2e/**`.
250
+
251
+
### Vitest setup
252
+
253
+
-[vitest.config.ts](vitest.config.ts) — happy-dom env. PostCSS is bypassed inline (the project's webpack-targeted [postcss.config.js](postcss.config.js) uses a custom `rem→px` plugin that Vite's loader rejects); unit tests don't import CSS so the bypass is invisible to component tests.
254
+
-[tests/setup.ts](tests/setup.ts) — hand-rolled in-memory `chrome.*` shim covering the surface the production code actually touches: `runtime.sendMessage` / `onMessage`, `storage.local` get/set, `storage.onChanged.addListener`, `tabs.query` / `sendMessage`, `i18n.getUILanguage`, `runtime.getURL`. Plus a module-level `vi.mock('mixpanel-browser', ...)` so analytics never fire, and a silenced `console.log`. Don't pull in `jest-chrome` / `sinon-chrome` — both are abandoned and over-engineered for this surface.
255
+
- Pinia stores get a fresh `createPinia()` per test in `beforeEach`. Cross-bundle bridge tests use real `window.dispatchEvent` (happy-dom provides a real DOM).
256
+
257
+
### Playwright E2E setup
258
+
259
+
-[playwright.config.ts](playwright.config.ts) — `webServer` auto-boots [tests/e2e/server.mjs](tests/e2e/server.mjs) (a ~30-line static-file server for fixture pages). Single worker (extensions don't parallelize cleanly under one persistent context). HTML report always emitted, uploaded as a CI artifact on every run.
260
+
-[tests/e2e/extension-fixture.ts](tests/e2e/extension-fixture.ts) — Playwright fixture that loads `dist/` as an unpacked extension and exposes `context`, `serviceWorker`, `extensionId`. Specs that need the extension import `{ test, expect }` from this file instead of `@playwright/test`. The `dist-artifacts.spec.ts` is fs-only and uses plain `@playwright/test`.
261
+
- Fixture pages live under [tests/e2e/fixtures/](tests/e2e/fixtures/): `index.html` (English, default 16px html font-size), `persian.html` (Persian RTL — regression target for the `btoa` / Latin1 bug class), `large-font.html` (24px html — regression target for the postcss `rem→px` rewrite).
262
+
- Don't run E2E against real `youtube.com` / `netflix.com` — flaky, slow, and breaks on selector changes outside our control. Nibble + ConsoleCrane both match `<all_urls>` in the manifest, so the local fixtures are enough for those flows.
263
+
264
+
### Critical Chromium flags
265
+
266
+
The fixture passes a specific args list to `launchPersistentContext`. **Changing them breaks CI silently.**
267
+
268
+
-`--headless=new` — forces the *full* Chromium binary in new-headless mode. Without it, Playwright defers to `chrome-headless-shell` on Linux runners, which **does not load extensions**. Every `toBeAttached` for `#subturtle-{nibble,console-crane}-root` will time out at 10s. macOS happens to do the right thing without this flag, which makes it easy to drop accidentally.
269
+
-`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage` — standard CI hygiene for Chromium under containerised runners. Harmless on macOS, sometimes required on Linux GitHub runners.
270
+
-`--disable-extensions-except=${dist}` + `--load-extension=${dist}` — load only our extension, nothing else.
271
+
272
+
### CI verify gate
273
+
274
+
[.github/workflows/release.yml](.github/workflows/release.yml) — a single workflow with two jobs.
275
+
276
+
The new `verify` job runs on `push` AND `pull_request` to `main` / `dev`. Step order matters:
277
+
278
+
1. Checkout + sibling `dashboard-app` clone (CI-only path; see Gotchas).
6.**Stub `.env.production`** — heredocs *non-empty* placeholder values for every key in `.env.example`. Do not regress this to `cp .env.example .env.production`: empty values cause `mixpanel.init("")` in [src/plugins/mixpanel.ts](src/plugins/mixpanel.ts) to throw synchronously during the content-script import chain, which silently halts every Vue mount before its top-level `log()` calls. Symptom: every browser-loading e2e test times out at `toBeAttached` for the content-script roots, with zero HTTP traffic in the trace after the page load.
284
+
7.**Build** — `yarn build`.
285
+
8.**E2E tests** — `yarn test:e2e`.
286
+
9.**Upload Playwright report** — runs on success or failure (gated by `hashFiles('playwright-report/**') != ''` so it skips silently when typecheck / unit tests fail before Playwright produces output). Pull with `gh run download -n playwright-report <run-id>`.
287
+
288
+
The existing `release` job is unchanged in body but now has `needs: verify` and `if: github.event_name == 'push'` so it skips on PRs and only fires after verify is green.
Wraps `tsc --noEmit` and suppresses two classes of upstream errors:
293
+
294
+
-`node_modules/pilotui/*` — pilotui's `package.json``exports.types` points at raw TS source, so tsc follows into `pilotui/src/vue.ts` which has a mismatched plugin signature against `vue3-perfect-scrollbar`.
295
+
-`../dashboard-app/*` — [src/stores/profile.ts](src/stores/profile.ts) walks the import chain into the sibling repo's frontend types, which re-export from server-side TS that depends on `mongoose` / `stripe` / `@modular-rest/server`. dashboard-app's own `node_modules` are usually present locally but are NOT installed in CI.
296
+
297
+
Real errors in our own code still print full tsc output and fail. Clean runs print a single summary line so GitHub's log parser doesn't surface the suppressed errors as red `Error:` annotations in the UI.
298
+
299
+
The Vue 3 SFC ambient declaration lives at [src/vue-shim.d.ts](src/vue-shim.d.ts); it must use `DefineComponent` (not Vue 2's default-export shape) or every `.vue` import in the routers gets typed as the bare `vue` module namespace.
300
+
301
+
### Test file map
302
+
303
+
```
304
+
tests/
305
+
setup.ts # chrome.* shim, mixpanel mock
306
+
route-params.test.ts # encode/decode Unicode round-trip + undefined edge case
dist-artifacts.spec.ts # fs check of dist/ shape (no browser)
320
+
nibble-flow.spec.ts # content script mounting + Persian emitOpen
321
+
console-crane-lifecycle.spec.ts # modal stays open while Nibble toggles off
322
+
translate-flow.spec.ts # full Persian translate-and-save with page.route stubs
323
+
visual-scale.spec.ts # rem→px rewrite regression net
324
+
```
325
+
326
+
### Test totals
327
+
328
+
79 unit / component tests across 9 files; 11 E2E specs across 5 files. Full suite runs in ~15s once Playwright's Chromium is warm.
329
+
232
330
## Verification checklist
233
331
234
-
When changes touch the bundle layout, content scripts, or shared CSS:
332
+
Most of this is automated by `yarn test` + `yarn test:e2e` — the items below are what the test suite already pins, with cross-references to the spec files. Re-run them by hand only if you're touching code the suite can't reach (the YouTube / Netflix subtitle path) or if you want a manual sanity pass on a real site.
333
+
334
+
Automated:
335
+
336
+
-`dist/` shape — entry files present, no orphan numeric chunks, manifest declares all four content scripts. → [tests/e2e/dist-artifacts.spec.ts](tests/e2e/dist-artifacts.spec.ts).
337
+
- Both content scripts mount their roots on a generic page; exactly one `#subturtle-console-crane-root`. → [tests/e2e/nibble-flow.spec.ts](tests/e2e/nibble-flow.spec.ts).
338
+
- Selection → Subturtle icon → translated card → Save → ConsoleCrane opens with WordDetail rendering Persian content. → [tests/e2e/translate-flow.spec.ts](tests/e2e/translate-flow.spec.ts).
339
+
- Toggling Nibble OFF for a host **while ConsoleCrane is open** does NOT close the modal or release the body scroll lock. → [tests/e2e/console-crane-lifecycle.spec.ts](tests/e2e/console-crane-lifecycle.spec.ts).
340
+
- Popup translate input: auto-focus on open, spinner while pending, re-submit different word resets, no double-fetch on enter mash. → [tests/translate-card.test.ts](tests/translate-card.test.ts).
341
+
- Per-host Nibble toggle persists and normalizes (`www.` strip, case fold, dedup). → [tests/settings-host.test.ts](tests/settings-host.test.ts).
342
+
- ConsoleCrane on Persian / CJK / emoji inputs throws no `InvalidCharacterError` from `btoa` — covered at the encode level, the bridge level, and the full select-and-save flow. → [tests/route-params.test.ts](tests/route-params.test.ts), [tests/e2e/nibble-flow.spec.ts](tests/e2e/nibble-flow.spec.ts), [tests/e2e/translate-flow.spec.ts](tests/e2e/translate-flow.spec.ts).
343
+
- Visual scale is consistent on default-html-fontsize and 24px-html-fontsize hosts (postcss `rem→px` rewrite regression net). → [tests/e2e/visual-scale.spec.ts](tests/e2e/visual-scale.spec.ts).
- On YouTube `/watch`: subtitle popup works; Nibble selection popup also works (all three content scripts run there). Exactly one `#subturtle-console-crane-root` in the DOM.
238
-
- On Wikipedia: only `nibble.js` and `console-crane.js` run; selection → icon → translation card → save flow opens ConsoleCrane.
239
-
- In the popup: per-site toggle reads/writes `nibbleDisabledDomains` and survives a popup re-open. Toggling Nibble OFF for a host **while ConsoleCrane is open** must NOT close the modal or lock page scroll — the modal lifecycle is decoupled from the Nibble per-host gate via the bridge.
240
-
- In the popup translate input: input is auto-focused on open; submitting renders the detailed result inline; logged-out users see "Login to save this phrase"; logged-in users get the bundle picker. Re-translating a different word resets the result. The button shows a spinner while pending.
241
-
- In ConsoleCrane on a non-Latin page (e.g. Persian / Chinese article): no `InvalidCharacterError` from `btoa`. Same check applies to the popup translate input — paste a Persian / CJK phrase and confirm no encoding error.
242
-
- Visual scale is consistent on a default-html-font-size site (YouTube) and a large-html-font-size site (typical WordPress blog).
347
+
- On YouTube `/watch`: subtitle popup wraps caption words, hover/anchor selection works, all three content scripts run side-by-side. The `main.js` URL match is locked to `youtube.com` and Netflix, so it can't be fixtured without a test-only manifest.
348
+
- On Netflix: same — subtitle wrapping behaviour on real Netflix.
349
+
- Popup full re-open lifecycle on the actual `chrome-extension://<id>/popup.html` page (the unit suite covers individual components but not the popup-page mount + nav transitions).
243
350
244
351
## Useful pointers
245
352
@@ -256,7 +363,14 @@ When changes touch the bundle layout, content scripts, or shared CSS:
0 commit comments