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
feat(e2e): add Playwright coverage, onboarding seed bridge, and CI (#68)
Adds Playwright e2e infrastructure over HTTPS with Phase 1 happy-path
coverage for onboarding, payments, activity, and settings. Most specs
skip onboarding via a dev/test-build-only window.__e2eSeedOnboarding
bridge that calls the same production Task actions the UI does, since
Playwright's storageState can't snapshot Evolu's OPFS-backed SQLite.
Adds test:e2e:preview to run the suite against a one-off production
build, and a labeled-PR GitHub Actions workflow to run it in CI.
Claude-Session: https://claude.ai/code/session_01WgU1jAJx3RvxuwWUpbQwmp
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: AGENTS.md
+15Lines changed: 15 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,6 +37,7 @@
37
37
-`src/assets` contains static frontend assets.
38
38
-`src/index.css` contains global Tailwind and theme styles.
39
39
-`src/zod-utils.ts` contains app-level Zod helpers that are not specific to one domain module.
40
+
-`e2e` contains Playwright end-to-end tests (`playwright.config.ts` at the repo root). See "E2E Testing" below for conventions.
40
41
41
42
## Domain Module Structure
42
43
@@ -127,3 +128,17 @@
127
128
- For aggregate extension/detail tables sharing the root id, soft delete only the root row unless the detail has its own lifecycle.
128
129
- For CRDT actions, write tombstones and updates directly without preloading rows, unless current data is required for a domain invariant.
129
130
- Pass Evolu mutation payloads through `removeUndefinedValues` to avoid extra or undefined fields.
131
+
132
+
## E2E Testing
133
+
134
+
- Run tests with `bun run test:e2e` (headless, dev server) or `test:e2e:ui` (interactive). `bun run test:e2e:preview` runs the same suite against a one-off production build instead: `PAYKY_E2E_BUILD=1 vite build` once, then Playwright's `webServer` runs `vite preview` (set via `PAYKY_E2E_SERVER=preview`, read in `playwright.config.ts`) rather than `bun run dev`. Tests live in `e2e/*.spec.ts`; `playwright.config.ts` at the repo root configures a single `chromium` project.
135
+
- The Playwright `webServer` boots the real Vite dev server (or, for `test:e2e:preview`, `vite preview` serving a real build) with basic-SSL left enabled (never set `PAYKY_DISABLE_BASIC_SSL` for e2e) so tests run over HTTPS with a self-signed cert (`ignoreHTTPSErrors` in the config), the same way production TLS behaves — `@vitejs/plugin-basic-ssl` applies to both `server.https` and `preview.https`. Some browser features (for example `navigator.clipboard`) are unavailable under plain HTTP, so testing over HTTP would hide regressions in those code paths.
136
+
- Evolu/SQLite persists through OPFS (Origin Private File System) in Chromium, not IndexedDB — Playwright's `storageState({ indexedDB: true })` snapshot/restore does **not** capture it, so pre-seeding an onboarded account via storageState does not work here. Don't reintroduce that approach.
137
+
- `src/components/e2e-test-bridge.tsx` (mounted in `App.tsx`) exposes `window.__e2eSeedOnboarding`, which calls the same production Task actions the onboarding UI does (`saveCashRegisterAccount`, `saveSparkAccount`, `saveFiatBankAccount`, `completeOnboarding`) directly, skipping the onboarding UI. It's gated on `import.meta.env.DEV || __E2E_TEST_BUILD__`, **not** `import.meta.env.DEV` alone — that define is `false` in every `vite build` output regardless of how it's later served, so DEV alone would make the bridge dead code in the `test:e2e:preview` build too. `__E2E_TEST_BUILD__` is a `vite.config.ts` `define` wired to `PAYKY_E2E_BUILD=1`, set only by `test:e2e:preview`'s build step — a real production build never sets it, so the bridge stays dead code (removed) there. `e2e/fixtures.ts`'s `seedOnboarding`/`seedCurrentAccountOnboarding` call the bridge via `page.evaluate` and wait for the app's own reactive redirect off `/onboarding`. Use this in specs that don't test onboarding itself; `completeOnboarding()` (real UI clicks) remains for specs that do (`e2e/onboarding.spec.ts`, `e2e/smoke.spec.ts`) and for a second device account created mid-test (see below).
138
+
- Switching the active device account recreates the app's Evolu client, and `E2eTestBridge`'s effect doesn't reliably reattach `window.__e2eSeedOnboarding` to the new client in time — use `completeOnboardingDefaults()` (real UI clicks) for a second account instead of the seed bridge, as `e2e/settings-accounts.spec.ts` does.
139
+
-`e2e/fixtures.ts` holds reusable flow helpers (`completeOnboarding`, `completeOnboardingDefaults`, `seedOnboarding`, `seedCurrentAccountOnboarding`, `enterAmount`, `createPayment`, `markCashPaid`, `translate`, `translateValue`, `gotoPage`, `reloadPage`, `pageWidth`/`pageHeight`) plus a `test`/`expect` re-export extended with a `seededPage` fixture. It is imported both by `*.spec.ts` files and by `bin/generate-doc-screenshots.ts`, which drives a manually launched `chromium.launch()` browser outside the Playwright test runner. Because of that second consumer, functions in `e2e/fixtures.ts` must never call `test.step(...)` (or other APIs that require an active test) — they throw `test.step() can only be called from a test` outside a real test run. Extend `e2e/fixtures.ts` instead of duplicating flow steps in a spec file or in the screenshot script, but keep it runner-agnostic.
140
+
- Import `test`/`expect` from `./fixtures.ts`, not `@playwright/test`, in every spec except `e2e/onboarding.spec.ts` and `e2e/smoke.spec.ts` (which test onboarding itself and must not auto-seed). Destructure the `seededPage` fixture instead of `page` — it seeds onboarding before the test body runs, so specs don't repeat a manual "seed onboarding" step. `settings-accounts.spec.ts` still seeds the *first* account this way but falls back to `completeOnboardingDefaults()` for the second (see the account-switch caveat above).
141
+
- Use `gotoPage(page, path, language, headingKey)`/`reloadPage(page, language, headingKey)` instead of hand-rolling `page.goto(path, { waitUntil: "domcontentloaded" }) + getByRole("heading", ...).waitFor()` — nearly every spec starts with this pattern and every reload-persistence check repeats it. Use `translateValue(language, key, value)` instead of hardcoding a rendered `{value}`-templated string (for example `"10%"` or `"Remove 20%"`) — it substitutes into the real translation key so the test tracks copy changes instead of silently drifting from it.
142
+
- Prefer `getByRole` with the translated accessible name (via `translate(language, key)`) as the default locator — it doubles as an accessibility check and tracks markup changes for free. Reserve `data-testid` for elements without a stable accessible name/role, or for elements that stay mounted in the DOM regardless of visibility (for example the payment-paid success overlay, which is toggled via `aria-hidden`/opacity rather than conditionally rendered — matched via `data-testid="payment-paid-panel"`, not a generic `[aria-hidden="false"]` attribute selector). Most local UI primitives (`Button`, `TabsTrigger`, `ToggleGroupItem`, ...) spread `...props` through to the native element, so `data-testid` can be passed directly as a prop without changing the component.
143
+
- Wrap each logical phase of a test — not each individual click — in `test.step(...)` inside the `*.spec.ts` file, typically one step per fixture-helper call (`completeOnboarding`, `createPayment`, `markCashPaid`, final assertion). This keeps the HTML report/trace readable without requiring step support inside the shared fixtures.
144
+
- There is no network mocking yet for Spark/FIO/Yadio/LNURL (planned for a later phase in `e2e.md`) — payment flows that hit those integrations are not yet deterministic in e2e.
0 commit comments