The e2e job now runs scripts/dev-homeserver.sh start + dev-seed.py before Playwright (installing python3-venv if the runner lacks it) and stops it afterwards. e2e/local-homeserver.spec.ts registers its own users and rooms over the CS API and drives the built client — no prod secrets — covering the fixes that were reproduced with scratch scripts this week: login/send/receive, own-message scroll (#212), /kick toast (#216), upload 413 wording (#213), forward provenance, thread panel at 1400px (#218), timeline lightbox (#219), clock-skew banner via page.clock (#158), status save under the presence limit (#226), long-press action sheet on a Pixel 7 emulation (#166). Skips itself when no homeserver answers, so > lotus-chat@4.12.7-lotus test:e2e > playwright test Running 16 tests using 1 worker ✓ 1 [chromium] › e2e/boot.spec.ts:8:3 › boot › client boots to the login screen without errors (1.5s) ✓ 2 [chromium] › e2e/boot.spec.ts:27:3 › boot › service worker script is served and registers (1.4s) ✓ 3 [chromium] › e2e/boot.spec.ts:54:3 › boot › bundled Element Call loads in a frame (3.8s) - 4 [chromium] › e2e/e2ee-composer.spec.ts:67:3 › E2EE composer › logs in with a password and reaches the client - 5 [chromium] › e2e/e2ee-composer.spec.ts:84:3 › E2EE composer › creates a private encrypted room and sends a text message - 6 [chromium] › e2e/e2ee-composer.spec.ts:146:3 › E2EE composer › attaches a compressed image and it is sent encrypted ✓ 7 [chromium] › e2e/local-homeserver.spec.ts:31:3 › local homeserver regression › logs in, opens a room, sends and receives (4.5s) ✓ 8 [chromium] › e2e/local-homeserver.spec.ts:58:3 › local homeserver regression › your own message scrolls into view even after scrolling up (#212) (11.4s) ✓ 9 [chromium] › e2e/local-homeserver.spec.ts:76:3 › local homeserver regression › /kick failure is reported, not swallowed (#216) (5.4s) ✓ 10 [chromium] › e2e/local-homeserver.spec.ts:90:3 › local homeserver regression › upload failure shows a plain sentence, never the raw MatrixError (#213) (3.7s) ✓ 11 [chromium] › e2e/local-homeserver.spec.ts:122:3 › local homeserver regression › forwarded message carries its provenance header (6.7s) ✓ 12 [chromium] › e2e/local-homeserver.spec.ts:143:3 › local homeserver regression › thread panel: opens from the chip and yields the member drawer at 1400px (#218) (4.7s) ✓ 13 [chromium] › e2e/local-homeserver.spec.ts:172:3 › local homeserver regression › timeline image opens the gallery lightbox (#219) (3.9s) ✓ 14 [chromium] › e2e/local-homeserver.spec.ts:204:3 › local homeserver regression › warns when the local clock is far off the server (#158) (6.5s) ✓ 15 [chromium] › e2e/local-homeserver.spec.ts:221:3 › local homeserver regression › status save survives the presence rate limit (#226) (12.6s) ✓ 16 [chromium] › e2e/local-homeserver.spec.ts:265:3 › local homeserver regression › touch: long-press opens the message action sheet (#166) (5.0s) 3 skipped 13 passed (1.2m) still works cold. 13 pass locally against dist + the dev homeserver in 1.8 min. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
16 KiB
Lotus Chat — Testing
Manual QA checklists no longer live here. Every shipped-but-unverified behaviour is a Gitea issue labelled
qa— cinny milestone Manual QA backlog, cinny-desktop milestone Desktop QA backlog. Each issue carries the full steps + expected results; tick items as they pass, comment on FAIL (what you saw vs expected, browser/OS, web vs desktop, theme, console errors), close when green. Migrated from this file on 2026-09-17 (full text in git history).
This file keeps what a contributor needs to run and extend the automated coverage, plus the ops tip at the bottom.
Environment notes
- Test the web build (
chat.lotusguild.org, LXC 106) first; re-run call + poll items on the desktop (Tauri) build too, since CSP and the EC iframe behave differently there. - Several call checks need a second participant (marked 👥 2 people in the issues); a couple need a third room/call in parallel (👥👥).
Local dev environment — drive the real UI against a throwaway homeserver
scripts/dev-homeserver.sh start # Synapse in .dev-homeserver/ (venv, SQLite), open registration, no rate limits, :8008
python3 scripts/dev-seed.py 400 # alice + bob, "Busy Room": 400 messages, an image every 10th
npm start # Vite on :5173
Calls too: scripts/dev-homeserver.sh calls adds a LiveKit SFU, a JWT issuer, the real voice-limit-guard from the matrix checkout and an https well-known — real two-party calls in headless Chromium with fake mic/camera (--use-fake-device-for-media-stream, --use-file-for-fake-audio-capture=<tone.wav> to trigger speaking detection, --auto-select-desktop-capture-source="Entire screen" for screenshare; ignoreHTTPSErrors: true). This is how #29, #161, #173/#174 and the guard's live-revoke bug were found and fixed on 2026-09-18.
Log in at http://127.0.0.1:5173/login/http%3A%2F%2Flocalhost%3A8008/ as alice / password123 (bob is the second participant; both can also be driven over the client API with their tokens). Playwright is installed (npm run test:e2e:install), so a scripted reproduction is node + chromium.launch() against :5173 — this is how Gitea #163 was reproduced and its fix verified in both plain and encrypted rooms. scripts/dev-homeserver.sh reset wipes the database; stop shuts it down.
Automated coverage map — what the unit tests already pin (2026-07)
Read this before working a qa issue. Much of the logic the manual checks were written to catch is now locked by deterministic unit tests (npm test, 920+ cases, green in CI). Unit tests do not prove visual rendering, real-call behavior, the desktop build, E2EE, or cross-device sync — those still need a human. But where a decision is pure logic, you can trust the test and spend your manual time on the human-only part. For each row below, the middle column is "don't bother re-deriving this by hand"; the right column is "this is what your manual pass is actually for."
| QA item | Logic pinned by a unit test (trust it) | What still needs you (manual) |
|---|---|---|
| A1 ringtone previews | callSounds.test.ts — each style's synthesized melody (chime/soft/retro), click-free gain ramps, context unlock/reuse, unknown = no-op |
that it's actually audible + the WebAudio first-gesture caveat |
| A2 ringtone persist/fallback | settings.test.ts — unknown ringtoneId → default, malformed JSON → defaults, merge-over-defaults (this is A2 step 3) |
the dropdown shows the persisted value after reload (trivial glance) |
| B2/B3 poll voting | poll.test.ts (18) — vote tally, latest-per-sender, multi-select, cleared/re-vote, winners, results-visible, single-vs-multi validation |
visual only: borders, radio-vs-checkbox, progress-bar fill, on each theme |
| O2/P4-1 thread notifications | threadNotifications.test.ts (32) — the entire notify decision (participating default, All/Mentions/Mute, @mention+highlight override, room-mute trumps), mode-map + muted-badge hygiene |
live 2-person delivery + sound + cross-device account-data sync |
| O3 math / LaTeX | mathParse.test.ts (14) — inline $…$, block $$…$$, currency guard ($5 and $10), escaped/unbalanced stay text, adjacency rules |
KaTeX renders visually + lazy-chunk load; code-block-literal is the markdown pipeline |
| O4/P4-8 encrypted search cache | searchCache.test.ts — the pure helpers mergeSearchResults (merge/dedupe/sort) + computeCoverage (window widening) + resilient-when-IDB-absent. The IDB round-trip test is skipped under npm test (node has no IndexedDB), so it runs only in a browser-like env, not CI |
the actual IndexedDB persist-across-reload, Clear button, logout wipe (integration — and the round-trip itself) |
M1 has:image/file/video |
useMessageSearch.test.ts — filterGroupsByMsgType union filter, drops empty groups, ignores non-string msgtype |
the chips render + compose with room/sender/date filters |
| M4 pinned-only filter | useMessageSearch.test.ts — filterGroupsByPinned keeps pinned, drops empty |
chip renders; needs a room with actual pins |
| M2 recent searches | recentSearches.test.ts (6) — prepend, dedupe+move-to-front, trim, ignore-empty, cap-at-10 |
chips render/click-re-run; persistence across refresh |
| Retention (disappearing msgs) | retention.test.ts — isExpired window math (strict boundary), disabled = never, preset monotonicity |
the timeline hide + self-redact integration; Synapse-side purge |
| O5/N97a session hardening | sessions.test.ts (22) — blob migration, legacy-key coercion, dual-write blob↔legacy sync, corrupt/partial-blob fallback, token-refresh, AND the subscribeSessionChanges storage-event logic (fires on session/null, ignores unrelated keys) |
the real cross-tab logout behavior end-to-end (two live tabs) |
| Q1/Q2 embeds (URL→player) | videoEmbed.test.ts (26) — every provider's URL→{provider, kind, embedUrl, height} parse (incl. Mixcloud/Deezer, TikTok, reserved-path guards) |
the click-to-play facade, no-network-until-Play, the CSP (esp. desktop), visuals |
| Seasonal theme resolution (part of F2) | seasonSchedule.test.ts — resolveSeasonTheme (off→none, auto→active season, pinned→that) + getActiveSeason priority/boundary days. NB: this pins which theme shows for a date, NOT F2's background↔seasonal mutual exclusion — that write-side logic is untested |
all of F2: the picker actually clearing the other setting live, and the overlay suppression when a background is set |
Everything else in the guide (calls, screen readers, desktop/Tauri, chat backgrounds, animated visuals, PWA install, real E2EE) is genuinely manual — no unit test substitutes for it. Items already verified live were dropped when the checklists moved to issues.
Playwright smoke test (Gitea #90) — npm run test:e2e
Browser-level smoke tests under e2e/ (config: playwright.config.ts). They boot the built dist/ through vite preview on port 4173, so run npm run build first (one-time: npm run test:e2e:install downloads the pinned Chromium). Three tiers:
| Tier | File | When it runs | What it proves |
|---|---|---|---|
| Boot (always) | e2e/boot.spec.ts |
every CI run (e2e job in .gitea/workflows/ci.yml) and locally |
login page renders with #root populated and no pageerror / unexpected console.error (allowlist in e2e/helpers.ts: the README's avatar-thumbnail 404, the login page's POST /register 401 probe, offline discovery), sw.js is served and registers, bundled Element Call mounts in a frame with every /public/element-call/ asset returning 200 |
| E2EE composer (gated) | e2e/e2ee-composer.spec.ts |
only when E2E_HOMESERVER, E2E_USER, E2E_PASSWORD are all set |
password login → /home/create/ with the End-to-End Encryption switch on (asserts createRoom carries m.room.encryption) → text message renders → attach a generated JPEG with "Compress image before uploading" ticked, image renders → every PUT …/rooms/*/send/* was m.room.encrypted with ciphertext and no plaintext body / url / file / mxc:// |
| Local homeserver (Gitea #220) | e2e/local-homeserver.spec.ts |
when a Synapse answers at E2E_LOCAL_HS (default http://localhost:8008); the CI e2e job starts one with scripts/dev-homeserver.sh start + dev-seed.py, locally run the same two commands |
registers its own e2e_alice_*/e2e_bob_* users and rooms over the CS API, then drives the built client: login + send/receive, own message scrolls into view (#212), /kick failure toast (#216), upload 413 sentence (#213), forward provenance header, thread panel + drawer at 1400 px (#218), timeline image → gallery lightbox (#219), clock-skew banner via page.clock (#158), status save under the presence rate limit (#226), long-press action sheet on a Pixel 7 emulation (#166). Helpers in e2e/localHs.ts; add a test here whenever a fix was reproduced with a scratch Playwright script |
CI secrets (Gitea → repo → Settings → Actions → Secrets; the e2e job forwards them via env:; until they exist the E2EE tier reports skipped, the boot tier still runs):
E2E_HOMESERVER— server name as typed on the login page (e.g.matrix.example.org). Must offerm.login.password; a next-gen-auth (MAS/OIDC-issuer) server shows only the OIDC button and the tier will fail at the username field.E2E_USER/E2E_PASSWORD— a throwaway account: each run logs in as a new device and creates a newe2e-smoke-<timestamp>room. Prune devices/rooms occasionally.
The e2e job is continue-on-error: true for now because playwright install --with-deps needs apt on the runner image — promote it to a hard gate once it is green on the runner. Locally: npm run test:e2e (boot tier only), or E2E_HOMESERVER=… E2E_USER=… E2E_PASSWORD=… npm run test:e2e for both; on failure look in test-results/ (screenshot + trace) and playwright-report/.
Green CI but the fix isn't live?
curl -s https://chat.lotusguild.org/index.html | grep -o 'assets/index-[^"]*\.js' gives the deployed entry chunk; grep it for a string unique to your change (curl -s https://chat.lotusguild.org/<that path> | grep -c <string>). If it's 0 after ~15 min, the deploy trigger was lost — push again (any commit) to re-fire the lotus-deploy webhook. The deploy log lives at /var/log/lotus-deploy.log on LXC 106.