Playwright e2e: foundation + smoke suite #150

Closed
opened 2026-06-19 20:48:12 +00:00 by james · 0 comments
Owner

Goal

Carol has no browser-level e2e today — coverage stops at the vitest API/DB/unit layer. Nothing exercises the assembled product (the Expo RN-Web SPA served by the Next.js API against a real DB) the way a user does, so regressions in routing, auth redirects, or the SPA↔API contract can slip through CI green.

This umbrella issue covers standing up a real Playwright harness that boots the whole app and drives Chromium, plus a thin smoke path that proves the stack end to end. Broader coverage is split into child tickets.

What this slice delivers (closed by the foundation PR)

  • A new apps/e2e/ workspace (@carol/e2e, private) holding the Playwright harness — kept out of @carol/api / @carol/client so browser deps don't leak into the shipped packages.
  • App boot via Playwright webServer: a build-aware scripts/serve.sh builds the client web bundle (pnpm -F @carol/client export:web) and the API (pnpm -F @carol/api build) if missing, then runs pnpm -F @carol/api start against a throwaway file-backed SQLite under apps/e2e/.tmp/ (wiped each boot), with SPA_BUNDLE_PATH pointed at apps/client/dist. Readiness gate is GET /api/health. A fresh DB per boot means the first register becomes admin and lands authenticated — a clean entry with no invite/approval. (A file, not :memory:: next start migrates from the instrumentation hook, whose module graph is separate from the API routes in a production build, so a :memory: DB isn't shared across the two.)
  • One smoke spec (tests/smoke.spec.ts), ordered: register a uniqueEmail() user → land authenticated at /notes → assert the sidebar renders and navigate ≥2 nav items, asserting the route changes → create a note and assert it appears → log out → assert redirect to /login → log back in and assert authenticated.
  • A CI gate: an e2e job in .forgejo/workflows/pr.yml (SQLite + Chromium) that builds the bundle, installs Chromium, boots the app, and runs the smoke spec on every PR.

Conventions chosen

  • Selectors are roles/aria first. Inputs via getByPlaceholder / getByLabel; buttons via getByRole('button', { name }); nav by visible label text. Button/nav names are imported from the i18n catalog (packages/i18n/messages/en.json) through fixtures/strings.ts, never hardcoded — robust to copy changes and aligned with the no-hardcoded-strings rule.
  • Minimal testIDs. RN-Web maps testIDdata-testid. Added only where a role/name is genuinely ambiguous (the note list row). Primary action buttons that lacked an accessibilityRole got accessibilityRole="button" so they expose a button role to assistive tech and Playwright alike — an accessibility win, not test-only scaffolding.
  • CI: mirrors the existing job scaffold (catthehacker image, the same SHA-pinned actions/checkout + actions/setup-node, corepack enable, pnpm install --frozen-lockfile --ignore-scripts, pnpm rebuild). SQLite + Chromium only.

Acceptance criteria

  • apps/e2e/ workspace with a Playwright config that boots the assembled app and gates readiness on /api/health.
  • One smoke spec covering register → navigate → create note → log out → log back in, passing headless Chromium.
  • Selectors use roles/aria + i18n catalog values; new testIDs are minimal and justified.
  • An e2e CI job on PRs (SQLite + Chromium) that builds the bundle and runs the spec.
  • No new runtime env var (so no README Configuration-table change).

Follow-ups (child tickets)

  • #325 — per-domain critical-path coverage (profile, projects, experience/jobs, network, account/PAT, admin invites/approvals).
  • #326 — test-infra hardening: per-test DB reset + a logged-in storageState fixture.
  • #327 — cross-browser (Firefox/WebKit) + mobile-viewport / drawer-nav variants.

This issue stays the umbrella; the foundation PR closes it.

## Goal Carol has no browser-level e2e today — coverage stops at the vitest API/DB/unit layer. Nothing exercises the assembled product (the Expo RN-Web SPA served by the Next.js API against a real DB) the way a user does, so regressions in routing, auth redirects, or the SPA↔API contract can slip through CI green. This umbrella issue covers standing up a real Playwright harness that boots the whole app and drives Chromium, plus a thin smoke path that proves the stack end to end. Broader coverage is split into child tickets. ## What this slice delivers (closed by the foundation PR) - A new `apps/e2e/` workspace (`@carol/e2e`, private) holding the Playwright harness — kept out of `@carol/api` / `@carol/client` so browser deps don't leak into the shipped packages. - **App boot via Playwright `webServer`**: a build-aware `scripts/serve.sh` builds the client web bundle (`pnpm -F @carol/client export:web`) and the API (`pnpm -F @carol/api build`) if missing, then runs `pnpm -F @carol/api start` against a throwaway **file-backed** SQLite under `apps/e2e/.tmp/` (wiped each boot), with `SPA_BUNDLE_PATH` pointed at `apps/client/dist`. Readiness gate is `GET /api/health`. A fresh DB per boot means the first `register` becomes admin and lands authenticated — a clean entry with no invite/approval. (A file, not `:memory:`: `next start` migrates from the instrumentation hook, whose module graph is separate from the API routes in a production build, so a `:memory:` DB isn't shared across the two.) - **One smoke spec** (`tests/smoke.spec.ts`), ordered: register a `uniqueEmail()` user → land authenticated at `/notes` → assert the sidebar renders and navigate ≥2 nav items, asserting the route changes → create a note and assert it appears → log out → assert redirect to `/login` → log back in and assert authenticated. - **A CI gate**: an `e2e` job in `.forgejo/workflows/pr.yml` (SQLite + Chromium) that builds the bundle, installs Chromium, boots the app, and runs the smoke spec on every PR. ## Conventions chosen - **Selectors are roles/aria first.** Inputs via `getByPlaceholder` / `getByLabel`; buttons via `getByRole('button', { name })`; nav by visible label text. Button/nav names are imported from the i18n catalog (`packages/i18n/messages/en.json`) through `fixtures/strings.ts`, never hardcoded — robust to copy changes and aligned with the no-hardcoded-strings rule. - **Minimal `testID`s.** RN-Web maps `testID` → `data-testid`. Added only where a role/name is genuinely ambiguous (the note list row). Primary action buttons that lacked an `accessibilityRole` got `accessibilityRole="button"` so they expose a button role to assistive tech and Playwright alike — an accessibility win, not test-only scaffolding. - **CI**: mirrors the existing job scaffold (catthehacker image, the same SHA-pinned `actions/checkout` + `actions/setup-node`, `corepack enable`, `pnpm install --frozen-lockfile --ignore-scripts`, `pnpm rebuild`). SQLite + Chromium only. ## Acceptance criteria - [x] `apps/e2e/` workspace with a Playwright config that boots the assembled app and gates readiness on `/api/health`. - [x] One smoke spec covering register → navigate → create note → log out → log back in, passing headless Chromium. - [x] Selectors use roles/aria + i18n catalog values; new `testID`s are minimal and justified. - [x] An `e2e` CI job on PRs (SQLite + Chromium) that builds the bundle and runs the spec. - [x] No new runtime env var (so no README Configuration-table change). ## Follow-ups (child tickets) - #325 — per-domain critical-path coverage (profile, projects, experience/jobs, network, account/PAT, admin invites/approvals). - #326 — test-infra hardening: per-test DB reset + a logged-in `storageState` fixture. - #327 — cross-browser (Firefox/WebKit) + mobile-viewport / drawer-nav variants. This issue stays the umbrella; the foundation PR closes it.
james changed title from (STUB) Playwrite tests to Playwright e2e: foundation + smoke suite 2026-06-28 19:36:38 +00:00
james closed this issue 2026-06-28 20:28:30 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#150
No description provided.