Skip to content

Commit ddeb5e4

Browse files
navidshadclaude
andcommitted
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>
1 parent e63f078 commit ddeb5e4

1 file changed

Lines changed: 126 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ Operating manual for working inside this repo. For product overview / supported
55
## Quick start
66

77
```bash
8-
npm install
9-
npm run dev # webpack --watch, writes dist/
10-
npm run build # NODE_ENV=production webpack --mode=production
8+
yarn install
9+
yarn dev # webpack --watch, writes dist/
10+
yarn build # NODE_ENV=production webpack --mode=production
11+
12+
yarn test # Vitest one-shot (unit + component)
13+
yarn test:watch # Vitest watch mode
14+
yarn test:e2e # Playwright E2E against the loaded extension (requires dist/)
15+
yarn typecheck # tsc --noEmit via the upstream-error filter
1116
```
1217

1318
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.
@@ -229,17 +234,119 @@ GITHUB_TOKEN=$(gh auth token) yarn release:dry
229234

230235
This prints the version + notes that would be generated without writing anything or creating a release.
231236

237+
## Testing
238+
239+
Three test layers, all wired into a single CI verify gate that blocks releases on a red.
240+
241+
### Stack
242+
243+
| Layer | Tool | Where |
244+
| --- | --- | --- |
245+
| Unit / component | Vitest + happy-dom + `@vue/test-utils` + `@pinia/testing` | [tests/](tests/)`*.test.ts` |
246+
| 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).
279+
2. `yarn install --frozen-lockfile`.
280+
3. Cache + install Playwright Chromium (`~/.cache/ms-playwright` keyed on `yarn.lock` hash).
281+
4. **Type check**`yarn typecheck`.
282+
5. **Unit tests**`yarn test`.
283+
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.
289+
290+
### Typecheck wrapper ([scripts/typecheck.mjs](scripts/typecheck.mjs))
291+
292+
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
307+
console-crane-bridge.test.ts # window CustomEvent emit/listen contracts
308+
console-crane-store.test.ts # toggleConsoleCrane / goBack / canGoBack / isOnMainPage
309+
translate.service.test.ts # cache hit/miss + 24h TTL eviction (vi.useFakeTimers)
310+
settings-host.test.ts # nibbleDisabledDomains normalize / toggle
311+
language-detection.test.ts # RTL detection, title lookup, supported codes
312+
selection-popup.test.ts # @mousedown.prevent.stop regression
313+
nibble-surface.test.ts # bridge-driven hide/show
314+
translate-card.test.ts # popup translate input flow
315+
e2e/
316+
extension-fixture.ts # chromium.launchPersistentContext + extension load
317+
server.mjs # static fixtures HTTP server
318+
fixtures/ # index.html, persian.html, large-font.html
319+
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+
232330
## Verification checklist
233331

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).
344+
345+
Still manual:
235346

236-
- `dist/` contains exactly: `background.js`, `main.js`, `nibble.js`, `console-crane.js`, `popup.js`, `popup.html`, `manifest.json`, `assets/` (no orphan numeric chunks).
237-
- 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).
243350

244351
## Useful pointers
245352

@@ -256,7 +363,14 @@ When changes touch the bundle layout, content scripts, or shared CSS:
256363
- Settings store: [src/common/store/settings.ts](src/common/store/settings.ts)
257364
- Marker store: [src/stores/marker.ts](src/stores/marker.ts)
258365
- Translate service: [src/common/services/translate.service.ts](src/common/services/translate.service.ts)
259-
- Release workflow: [.github/workflows/release.yml](.github/workflows/release.yml)
366+
- Release + verify workflow: [.github/workflows/release.yml](.github/workflows/release.yml)
260367
- semantic-release config: [release.config.cjs](release.config.cjs)
261368
- Changelogs: [CHANGELOG.md](CHANGELOG.md) (stable), [CHANGELOG-DEV.md](CHANGELOG-DEV.md) (prerelease)
262369
- Version-bump helpers: [scripts/next-version.mjs](scripts/next-version.mjs), [scripts/sync-manifest-version.mjs](scripts/sync-manifest-version.mjs)
370+
- Vitest config: [vitest.config.ts](vitest.config.ts)
371+
- Vitest setup (chrome shim, mixpanel mock): [tests/setup.ts](tests/setup.ts)
372+
- Playwright config: [playwright.config.ts](playwright.config.ts)
373+
- Playwright extension fixture: [tests/e2e/extension-fixture.ts](tests/e2e/extension-fixture.ts)
374+
- Playwright fixtures server: [tests/e2e/server.mjs](tests/e2e/server.mjs)
375+
- Typecheck wrapper (with upstream-error filter): [scripts/typecheck.mjs](scripts/typecheck.mjs)
376+
- Vue 3 SFC ambient declaration: [src/vue-shim.d.ts](src/vue-shim.d.ts)

0 commit comments

Comments
 (0)