test(e2e): add per-domain critical-path specs #335

Merged
james merged 2 commits from test/e2e-per-domain-specs into main 2026-06-29 15:19:14 +00:00
Owner

What

Five per-domain Playwright specs on top of the smoke foundation (#150), exercising each major surface end to end against the assembled app (Expo RN-Web SPA → Next.js API → SQLite).

Spec Flow
profile.spec.ts edit name/title/brief + persistence across reload; add a contact. Picture upload/remove is a separate test, parked with test.fixme (see below).
projects.spec.ts create → rename → delete a uniquely-named project.
experience.spec.ts Jobs: job → position → contribution. Contracts: contract → set position.
network.spec.ts add a person + a contact, add an organisation, link them (person as a key person on the org).
account.spec.ts mint a PAT (reveal banner → revoke); switch theme + locale and assert each survives a full reload.

Per-user isolation, no DB reset

Carol scopes every row to the authenticated user, so each spec just registers its own uniqueEmail() user (new fixtures/auth.tsregisterFreshUser) and works entirely in that silo. Specs never see each other's data even on the shared per-boot DB, so no per-test reset is needed#325 does not depend on #326 after all. Single-worker / serial config is unchanged.

Selectors come from the i18n catalog via fixtures/strings.ts (new namespaces follow the existing ns() pattern); the avatar fixture is a real 64×64 PNG sharp can decode.

testIDs added (4 total, mirroring note-row)

Only where a created entity's text is ambiguous/absent:

  • company-input, position-title-input — the experience job/position form inputs have no placeholder or associated label.
  • project-name-input — the projects row-edit name input coexists with the create-card name input.

Everything else asserts on the unique created name. accessibilityRole/accessibilityLabel were preserved.

test.fixme: profile picture upload

The profile-picture upload step is the one parked piece. expo-image-picker's web shim appends a hidden <input type=file> and dispatches a synthetic click; headless Chromium answers with a synchronous cancel event, so the shim removes the input within the same tick (verified: 0 file inputs remain a frame later) — setInputFiles can't race it. It's isolated as its own test so basics + contact coverage is unaffected. Re-enabling it (a picker stub, or driving the upload through the API) rides with #326's infra work.

A subtlety worth flagging: expo-router keeps the previous screen mounted (hidden) on web during a push, so a name can match across stacked screens — detail-screen assertions scope with .filter({ visible: true }).first(). Also note RN-Web 0.21 no longer reflects accessibilityState as aria-checked, so the theme/locale persistence assertions check the active segment's non-transparent background instead.

Verification

  • pnpm -F @carol/client export:web + pnpm -F @carol/api build — clean.
  • pnpm -F @carol/e2e test8 passed, 1 skipped (the fixme'd picture upload); smoke spec still green.
  • pnpm -F @carol/e2e exec tsc --noEmit — clean.
  • pnpm -F @carol/client lint + typecheck — clean (touched experience/projects for testIDs).

No CI workflow change (the existing e2e job runs the whole suite) and no new runtime env var.

Admin coverage (invite-only + admin-approval) is deferred to #326, which needs a second app boot under a non-open REGISTRATION_POLICY.

Closes #325
Refs #150
Refs #326

🤖 Generated with Claude Code

## What Five per-domain Playwright specs on top of the smoke foundation (#150), exercising each major surface end to end against the assembled app (Expo RN-Web SPA → Next.js API → SQLite). | Spec | Flow | | --- | --- | | `profile.spec.ts` | edit name/title/brief + persistence across reload; add a contact. Picture upload/remove is a separate test, parked with `test.fixme` (see below). | | `projects.spec.ts` | create → rename → delete a uniquely-named project. | | `experience.spec.ts` | Jobs: job → position → contribution. Contracts: contract → set position. | | `network.spec.ts` | add a person + a contact, add an organisation, link them (person as a key person on the org). | | `account.spec.ts` | mint a PAT (reveal banner → revoke); switch theme + locale and assert each survives a full reload. | ## Per-user isolation, no DB reset Carol scopes every row to the authenticated user, so each spec just registers its own `uniqueEmail()` user (new `fixtures/auth.ts` → `registerFreshUser`) and works entirely in that silo. Specs never see each other's data even on the shared per-boot DB, so **no per-test reset is needed** — `#325` does not depend on `#326` after all. Single-worker / serial config is unchanged. Selectors come from the i18n catalog via `fixtures/strings.ts` (new namespaces follow the existing `ns()` pattern); the avatar fixture is a real 64×64 PNG sharp can decode. ## testIDs added (4 total, mirroring `note-row`) Only where a created entity's text is ambiguous/absent: - `company-input`, `position-title-input` — the experience job/position form inputs have no placeholder or associated label. - `project-name-input` — the projects row-edit name input coexists with the create-card name input. Everything else asserts on the unique created name. `accessibilityRole`/`accessibilityLabel` were preserved. ## `test.fixme`: profile picture upload The profile-picture upload step is the one parked piece. expo-image-picker's web shim appends a hidden `<input type=file>` and dispatches a synthetic click; headless Chromium answers with a synchronous `cancel` event, so the shim removes the input within the same tick (verified: 0 file inputs remain a frame later) — `setInputFiles` can't race it. It's isolated as its own test so basics + contact coverage is unaffected. Re-enabling it (a picker stub, or driving the upload through the API) rides with #326's infra work. A subtlety worth flagging: expo-router keeps the previous screen mounted (hidden) on web during a push, so a name can match across stacked screens — detail-screen assertions scope with `.filter({ visible: true }).first()`. Also note RN-Web 0.21 no longer reflects `accessibilityState` as `aria-checked`, so the theme/locale persistence assertions check the active segment's non-transparent background instead. ## Verification - `pnpm -F @carol/client export:web` + `pnpm -F @carol/api build` — clean. - `pnpm -F @carol/e2e test` — **8 passed, 1 skipped** (the fixme'd picture upload); smoke spec still green. - `pnpm -F @carol/e2e exec tsc --noEmit` — clean. - `pnpm -F @carol/client lint` + `typecheck` — clean (touched experience/projects for testIDs). No CI workflow change (the existing `e2e` job runs the whole suite) and no new runtime env var. Admin coverage (invite-only + admin-approval) is deferred to #326, which needs a second app boot under a non-open `REGISTRATION_POLICY`. Closes #325 Refs #150 Refs #326 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(e2e): add per-domain critical-path specs
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 12s
PR / Static analysis (pull_request) Failing after 56s
PR / OSV-Scanner (pull_request) Successful in 44s
PR / Trivy (image) (pull_request) Successful in 1m26s
PR / pnpm audit (pull_request) Successful in 3m43s
PR / OpenAPI (pull_request) Successful in 4m33s
PR / Client (web export smoke) (pull_request) Successful in 4m43s
PR / Lint (pull_request) Successful in 4m55s
PR / Typecheck (pull_request) Successful in 4m55s
PR / Build (pull_request) Successful in 5m4s
PR / Test (postgres) (pull_request) Successful in 5m9s
PR / Test (sqlite) (pull_request) Successful in 5m15s
PR / Package age policy (soft) (pull_request) Successful in 1m16s
Secrets / gitleaks (pull_request) Successful in 42s
PR / Coverage (soft) (pull_request) Successful in 3m21s
PR / E2E (Playwright) (pull_request) Successful in 7m54s
43e169d158
Add Playwright specs for profile, projects, experience, network, and
account (#325), building on the smoke foundation (#150). Each spec
registers its own uniqueEmail() user via the new registerFreshUser
fixture and works in that user's isolated silo, so no per-test DB reset
is needed.

Coverage:
- profile: edit basics + persistence across reload, add a contact;
  picture upload/remove parked as test.fixme (expo-image-picker's web
  shim self-cancels its hidden file input in headless Chromium).
- projects: create, rename, delete.
- experience: job -> position -> contribution; contract -> set position.
- network: person + contact, organisation, link as key person.
- account: PAT create + revoke; theme + locale persistence across reload.

Minimal testIDs (company-input, position-title-input, project-name-input)
where a created entity's text is ambiguous or absent.

Refs #150
Refs #326
Closes #325

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 78.4% ≥ 50%
Branches 70.0% ⚠️ ≥ 75%
Functions 78.9% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 78.4% ✅ | ≥ 50% | | Branches | 70.0% ⚠️ | ≥ 75% | | Functions | 78.9% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james force-pushed test/e2e-per-domain-specs from 43e169d158
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 12s
PR / Static analysis (pull_request) Failing after 56s
PR / OSV-Scanner (pull_request) Successful in 44s
PR / Trivy (image) (pull_request) Successful in 1m26s
PR / pnpm audit (pull_request) Successful in 3m43s
PR / OpenAPI (pull_request) Successful in 4m33s
PR / Client (web export smoke) (pull_request) Successful in 4m43s
PR / Lint (pull_request) Successful in 4m55s
PR / Typecheck (pull_request) Successful in 4m55s
PR / Build (pull_request) Successful in 5m4s
PR / Test (postgres) (pull_request) Successful in 5m9s
PR / Test (sqlite) (pull_request) Successful in 5m15s
PR / Package age policy (soft) (pull_request) Successful in 1m16s
Secrets / gitleaks (pull_request) Successful in 42s
PR / Coverage (soft) (pull_request) Successful in 3m21s
PR / E2E (Playwright) (pull_request) Successful in 7m54s
to cc04a7dde4
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 6s
PR / OpenAPI (pull_request) Successful in 2m25s
PR / Static analysis (pull_request) Failing after 2m28s
PR / pnpm audit (pull_request) Successful in 2m45s
PR / Lint (pull_request) Successful in 3m6s
PR / OSV-Scanner (pull_request) Successful in 53s
PR / Client (web export smoke) (pull_request) Successful in 3m40s
PR / Typecheck (pull_request) Successful in 3m50s
PR / Package age policy (soft) (pull_request) Successful in 48s
PR / Test (sqlite) (pull_request) Successful in 3m56s
Secrets / gitleaks (pull_request) Successful in 38s
PR / Build (pull_request) Successful in 4m6s
PR / Test (postgres) (pull_request) Successful in 4m15s
PR / E2E (Playwright) (pull_request) Successful in 4m52s
PR / Trivy (image) (pull_request) Successful in 2m24s
PR / Coverage (soft) (pull_request) Successful in 2m4s
2026-06-28 23:34:01 +00:00
Compare
test(e2e): rename masked-input placeholder to clear njsscan
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 11s
PR / OpenAPI (pull_request) Successful in 2m24s
PR / Static analysis (pull_request) Successful in 2m27s
PR / OSV-Scanner (pull_request) Successful in 51s
PR / Typecheck (pull_request) Successful in 3m29s
PR / Lint (pull_request) Successful in 3m40s
PR / Client (web export smoke) (pull_request) Successful in 3m46s
PR / pnpm audit (pull_request) Successful in 3m45s
PR / Test (sqlite) (pull_request) Successful in 4m4s
PR / Test (postgres) (pull_request) Successful in 4m29s
PR / Build (pull_request) Successful in 4m36s
PR / Package age policy (soft) (pull_request) Successful in 1m5s
Secrets / gitleaks (pull_request) Successful in 57s
PR / Trivy (image) (pull_request) Successful in 2m16s
PR / E2E (Playwright) (pull_request) Successful in 5m5s
PR / Coverage (soft) (pull_request) Successful in 3m29s
0f8fb756fe
njsscan's hardcoded_secrets.node_password rule flagged the password
field's bullet placeholder (a UI string, not a credential) because the
constant was named PASSWORD_PLACEHOLDER, failing static analysis.
Rename to MASKED_INPUT_PLACEHOLDER in both the shared fixture and the
smoke spec so the false positive can't recur on either file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
james merged commit ad313e72ea into main 2026-06-29 15:19:14 +00:00
james deleted branch test/e2e-per-domain-specs 2026-06-29 15:19:14 +00:00
Sign in to join this conversation.
No description provided.