Files
cinny/LOTUS_TESTING.md
T
jaredandClaude Opus 5 d929143f7d fix(gallery): paginate media, activity log and export on detached timeline sets — never the live timeline (#163)
RoomTimeline renders a numeric index window into the live timeline's
event arrays; SDK back-pagination prepends, so any side panel calling
paginateEventTimeline(room.getLiveTimeline()) shifted the visible
messages into the past on the next render and broke at-bottom tracking.

New utils/detachedTimeline.ts builds a timeline set that mirrors the
already-loaded history and paginates independently: a room-registered
filtered set (server-side contains_url / types filter) when the filter
is usable, else a private EventTimelineSet seeded from the live timeline.
useRoomMediaTimeline wraps it for the gallery (live events + redactions
handled); RoomActivityLog uses a type filter (safe in encrypted rooms);
ExportRoomHistory pages a private set so a full export no longer parks
thousands of events in the live timeline.

Verified with Playwright against a local Synapse in a 400-message plain
room and a 200-message encrypted room: timeline stays at the bottom
through gallery pages, activity load-more and a full export; live
messages keep auto-scrolling; all media found in both rooms.

Also adds scripts/dev-homeserver.sh + scripts/dev-seed.py (local
throwaway Synapse for driving the real UI) and documents them.

Closes #163

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-18 00:03:54 -04:00

13 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

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.tsfilterGroupsByMsgType union filter, drops empty groups, ignores non-string msgtype the chips render + compose with room/sender/date filters
M4 pinned-only filter useMessageSearch.test.tsfilterGroupsByPinned 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.tsisExpired 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.tsresolveSeasonTheme (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). Two 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://

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 offer m.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 new e2e-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.