1# Testing a lofi app23The generated template separates fast deterministic tests from opt-in browser scenarios that need a4running app, Chromium, and—for convergence—managed sync. Framework release checks also exercise5recoverable accounts and access policies; generated applications reuse those package-owned seams.67```mermaid8flowchart TB9 Change["Application change"] --> Fast["deno task test<br/>deterministic suite"]10 Fast --> Build["build + preview<br/>production shell"]11 Build --> Manual["reload and offline checks"]12 Build --> Browser["two-client convergence<br/>opt-in Playwright"]13 Manual --> Device["physical iOS and Android"]14 Browser --> Device15 Device --> Ship["Ready to ship"]16```1718## Run the default suite1920```sh21deno task test22```2324The default suite covers application tests without launching a browser. Framework runtime contracts25run once in the `@nzip/lofi` package suite instead of being copied into every application. Keep26domain logic and permission-shape checks in this fast path when possible.2728## Test the production build manually2930```sh31deno task build32deno task preview33```3435Then verify:36371. Add data and reload.382. Confirm the status reports local durability.393. Disable the network and continue reading and writing.404. Reload while offline to exercise the production service worker.415. Restore the network and confirm the local data remains.4243The service worker is intentionally disabled during `deno task dev`; use a production build when44testing offline shell startup.4546## Run the two-client convergence example4748First configure managed sync and start the app:4950```sh51deno task jazz:provision52deno task dev53```5455In another terminal, point the included browser example at the printed URL:5657```sh58LOFI_E2E_BASE_URL=http://127.0.0.1:4321/ \59 deno test -A tests/convergence_e2e_test.ts60```6162```mermaid63sequenceDiagram64 participant A as Client A65 participant B as Client B66 participant S as Managed sync6768 A->>A: Go offline and edit69 B->>B: Go offline and edit70 A->>S: Reconnect and upload71 B->>S: Reconnect and upload72 S-->>A: Both edits73 S-->>B: Both edits74 A->>A: Assert convergence75 B->>B: Assert convergence76```7778Both browser contexts use one test identity. Without `LOFI_E2E_BASE_URL`, the example skips so the79default suite remains fast.8081Two more opt-in browser gates ship with the starter:8283- `tests/backup_migration_e2e_test.ts` — the account journey in one real browser: rows written84 local-only must survive electing backup and sync (the runtime copies them into the managed85 namespace during the reload, settling at local durability, so the sync server does not need to be86 reachable), and the phrase-reveal guard must accept only its enrolled passkey — after the87 authenticator loses its credentials, the reveal fails closed. Serve on `localhost` (a passkey88 RP-ID cannot bind to a bare IP).89- `tests/auth_e2e_test.ts` — the device-credential enroll → authenticate round-trip on a stable90 origin listed in `credentialOrigins`.9192If Chromium is missing:9394```sh95deno run -A npm:playwright@1.61.1 install chromium96```9798## Adapt the example to your UI99100Update these four pieces in `tests/convergence_e2e_test.ts`:101102- `ready` — a DOM condition proving the local store has hydrated;103- `apply` — the user action that makes one local mutation;104- `locallyApplied` — proof that the offline client rendered its own mutation;105- `converged` — proof that both clients eventually render all expected results.106107Use stable accessible roles, labels, and application-owned `data-*` attributes. Avoid arbitrary108sleeps; readiness helpers retry observable conditions until their timeout.109110## Failure artifacts111112Browser fixtures can save artifacts under `test-results/`. Snapshot callbacks should contain counts,113booleans, state names, or sanitized identifiers—not task text, environment values, recovery phrases,114or other user data.115116## Framework recovery and permission evidence117118The framework repository's `deno task check` includes the access-security suite. It uses Jazz's119permission-test app for owner, recipient, unrelated-user, revoke, fixed-role, membership, removal,120and self-leave rejection cases, plus a real local Jazz server for offline grant/revoke and121membership reconciliation.122123`deno task test:golden` extends the existing generated-app runner with a two-profile journey. A124Chromium virtual authenticator creates and exports a resident credential, a fresh browser process125imports it, and the real WebAuthn assertion restores the same Jazz principal and synced rows. The126runner pins the exported credential as the assertion's allow-list because headless Chromium cannot127show its resident-credential account chooser. It also proves recovery-phrase fallback when the128credentials API is unavailable and proves that stopped sync retains the managed local replica129through a production offline reload.130131This is automated virtual-authenticator evidence, not physical iOS or Android evidence. It does not132prove passkey-provider portability, mobile installed-PWA lifecycle behavior, or transport133convergence without the local Jazz server used by the journey.134135## Physical-device checks136137Browser automation does not prove installed-PWA storage and lifecycle behavior on iOS or Android.138Before shipping, use a stable HTTPS origin and exercise installation, termination, device restart,139offline cold start, foreground recovery, and account recovery on every supported mobile surface.140141The WebAuthn PRF extension is the one credential path no virtual authenticator models, so PRF is142feature-detected in the runtime and must be validated on real hardware. The manual pass, per143supported platform/passkey-provider pair:1441451. `getAuthCapability()` reports `prf: "available"` (or `"not-reported"` with a working derive).1462. Enroll a device credential on the pinned production RP-ID and derive a PRF secret twice with the147 same salt — both ceremonies must yield the same 32 bytes.1483. Encrypt with the derived at-rest key, restart the browser/app, derive again, and decrypt.1494. Confirm a different salt yields a different secret and cannot decrypt the first blob.1505. On providers that sync passkeys, repeat the derive on a second device of the same ecosystem and151 record whether PRF results roam — do not assume they do.