# Lotus Chat — Manual Testing Guide

**Generated:** June 2026
**Scope:** Everything landed on the `lotus` branch since the v4.12.3 merge that I (Claude) could **not** verify statically and that needs a human in a real environment to confirm. Work through it top-to-bottom; the highest-risk / hardest-to-reproduce items are first.

> **How to report back:** For each numbered check, tell me **PASS** / **FAIL** (or **partial**). On any FAIL, include: what you saw vs. expected, the browser/OS (and whether web LXC 106 or the desktop/Tauri build), the theme you were on, and any **browser console** errors (F12 → Console). Screenshots help for anything visual.

## Environment notes

- You push from your own machine; these commits are local on `lotus` until you do.
- Test the **web** build (LXC 106 / `code.lotusguild.org`) first; re-run the **call** + **poll** sections on the **desktop (Tauri)** build too, since CSP and the EC iframe behave differently there.
- Several call features need a **second participant** (second account on another device/browser, or a colleague). Items that need this are marked **👥 2 people**.
- A couple of call items need a **third room/call** in parallel — marked **👥👥**.

---

## Commits covered

| Commit     | Area                                                                         |
| :--------- | :--------------------------------------------------------------------------- |
| `caf6318a` | Poll vote buttons → folds tokens (N4)                                        |
| `c67aed01` | In-call incoming-call banner (#4b)                                           |
| `4a875884` | Selectable ringtone (#4a)                                                    |
| `0394fce9` | EC iframe load watchdog + recovery UI; avatar decorations on call tiles (#3) |
| `d2946c00` | Upload retry/backoff, presence-on-unload, typed m.direct                     |
| `b7e1f89c` | Timeline/composer/emoji perf memoization                                     |
| `c0f98672` | Upstream **Element Call 0.20.1** merge (regression sweep)                    |

---

## A. Calls — new ringtone + notification work (highest priority)

### A1. Ringtone selection — preview in Settings

**Steps**

1. Open **Settings → General**, scroll to the **Calls** section.
2. Find the new **Ringtone** dropdown (just above **Ringtone Volume**).
3. Select each option in turn: **Classic, Chime, Soft, Retro, Silent**.

**Expected**

- Selecting **Classic** plays the existing `call.ogg` clip (cut off after a few seconds).
- **Chime / Soft / Retro** each play a short, distinct synthesized preview.
- **Silent** plays nothing.
- Changing **Ringtone Volume** then re-selecting a ringtone previews at the new volume.
- No console errors.

> ⚠️ **Known browser limitation:** the synthesized tones use WebAudio. If a preview is ever silent, click anywhere on the page once (a "user gesture") and retry — browsers suspend audio until the page has been interacted with. The Settings preview is _after_ a click so it should always sound; this note matters more for A3.

### A2. Ringtone selection persists

1. Set Ringtone to **Retro**, reload the app.
2. **Expected:** the dropdown still shows **Retro** (setting persisted).
3. Bonus: in devtools, set `localStorage.settings` to a bogus `ringtoneId` and reload → it should fall back to **Classic**, not break.

### A3. Incoming call uses the selected ringtone — 👥 2 people

**Setup:** Account A (you) and Account B in a **DM** or a **private (invite-only) group** room.

1. As A, pick a non-silent ringtone (e.g. **Chime**).
2. From B, **start a call** in that DM/room. Do **not** answer on A.

**Expected on A**

- The full-screen **Incoming Call** dialog appears (caller name, room avatar, Answer / Reject).
- The **selected ringtone loops** until you answer/reject/ignore (at the set volume).
- Answer → joins the call. Reject (DM) / Ignore (group) → dialog dismisses and ring stops.
- Set ringtone to **Silent** and repeat → dialog still appears, **no sound**.

### A4. In-call banner for a second incoming call — 👥👥 (the trickiest one)

**Setup:** You (A) already **in a call** in Room 1. Account B can call you in a **different** Room 2 (a DM or private group you share). Ideally a third account C, or B leaves Room 1's call first.

1. While A is **actively in Room 1's call**, trigger an incoming call to A from **Room 2**.

**Expected on A**

- **No** full-screen takeover. Instead a **compact banner appears in the top-right corner** with the caller's avatar, room name, "Incoming voice/video call", and **Answer / Reject (or Ignore)** buttons.
- It plays a **single soft ping**, _not_ a looping ring (so it doesn't talk over your active call).
- The banner does **not** cover your active call's controls/PiP in a way that blocks them.
- **Answer** → switches you into Room 2's call. **Reject/Ignore** → banner disappears.
- The banner auto-dismisses if the caller hangs up / the call times out.

**Also verify the no-op case:** while in Room 1's call, if a notification for **Room 1 itself** arrives, **nothing** should pop up (no banner, no dialog).

### A5. Camera focus during screenshare (#1) — 👥 2 people

**Setup:** You (A) and B in a call; B (or another participant) **sharing their screen**, and at least one person with **camera on**.

1. As A, open the **participant glance** (the stacked avatars / member list for the call) and click a participant who has their **camera on**.
2. In the menu, click **"Focus camera"**.

**Expected**

- The view switches to **spotlight** and **pins that person's camera tile**, overriding the auto-spotlighted screenshare.
- It **stays** on that camera (doesn't immediately snap back to the screenshare).
- If you pick someone with their camera **off**, it should at worst just toggle spotlight (graceful fallback), not error.

### A6. Avatar decorations on call tiles (#3) — 👥 2 people

**Setup:** A participant in the call has an **avatar decoration** set (Settings → Profile decoration).

1. Join a call with that participant.
2. Look at **our** participant roster / prescreen tiles (not the avatars rendered inside the Element Call video grid — those are EC's and out of scope).

**Expected:** the decoration ring/overlay renders around that participant's avatar on the call tile, the same way it does in member lists.

### A7. EC iframe load watchdog + recovery UI (#EC, N96)

This guards against a permanently-stuck "Loading…" call. Also covers the N96 button-label fix (the old "Retry" and "Leave" buttons were identical — now there is a single **"Back"** button).

1. Normal case: **join a call** → it should connect within a few seconds as usual (the watchdog stays invisible).
2. Failure case (best-effort to reproduce): throttle your network hard (devtools → Network → Offline) **right as** you click join, or block the Element Call origin, so the iframe can't finish loading.

**Expected**

- On a genuine failure/timeout (~25s), instead of an endless spinner you get a **visible error overlay with a single "Back" button** (the old "Retry" + "Leave" pair is gone — they did the same thing and "Retry" was misleading).
- Clicking **Back** returns you to the call prescreen, where you can manually click Join to try again.
- Normal joins must **not** trigger the error overlay (no false positives) — this is the important part to confirm.
- **Self-heal:** if the error overlay appears on a slow network but EC then finishes loading anyway, the overlay should **dismiss itself** and drop you into the live call. Worth confirming on a deliberately throttled-but-not-blocked connection.

---

## B. Polls (N4) — render correctly on non-TDS themes

This was the actual bug: poll buttons used undefined CSS variables, so on the **default (non-Lotus-Terminal) themes** they rendered with invisible borders / no selected state.

### B1. Poll renders on a default theme — ✅ PASS

1. Switch to a **default Cinny theme** (Settings → Appearance — **not** Lotus Terminal / TDS). Test both a **dark** and a **light** theme.
2. In any room, create a poll (composer → poll button): a **single-choice** poll with 3 options.

**Expected**

- Each option is a clearly **bordered** button with visible rounded corners.
- A **radio circle** indicator is visible on the left of each option.
- Text, and (after votes) the percentage, are legible.

### B2. Voting + selected/progress state

1. **Vote** on an option.
   **Expected**

- The selected option shows a **filled accent border + filled radio**, and an **accent progress-bar fill** grows behind it proportional to the vote %.
- The percentage and total vote count update.
- Click again / pick another option → selection moves correctly (single-choice replaces; the bar redraws).

### B3. Multiple-choice poll

1. Create a poll allowing **multiple selections**.
   **Expected**

- Indicators are **square checkboxes** (not circles); selected ones show a **✓** that's legible against the filled box.
- You can select **several** options; each shows its own progress fill.

### B4. Lotus Terminal theme regression — ✅ PASS

1. Switch to **Lotus Terminal / TDS** theme and re-open a poll.
   **Expected:** still looks correct (the fix uses theme tokens, so the TDS accent should now drive it) — no worse than before.

---

## C. Robustness / background behavior

### C1. Presence updates on tab close

1. Open the app, then **close the tab** (or quit the browser).
2. From another session/device, check your **presence** shortly after.
   **Expected:** you go **offline/away** reliably (the unload now uses `fetch({keepalive})`). Previously this could be missed.

### C2. Upload retry on flaky network (best-effort)

1. In devtools → Network, set a throttle that drops/slows requests, or toggle Offline briefly **during** a file upload.
   **Expected**

- A transient failure **retries** (up to 3×, with backoff) and the upload can still succeed once the network recovers.
- A genuine, permanent rejection (e.g. file too large / 4xx) still **fails fast** with the usual error — it should **not** spin retrying.

### C3. General timeline/composer perf (no functional regression)

The memoization changes are invisible if correct. Just confirm **nothing broke**:

- Open a busy room; scrolling, jump-to-latest, mark-as-read all still work.
- Composer: send a message, upload a file, share a location, pick an emoji and a sticker — all still work.

---

## D. Element Call 0.20.1 merge — regression sweep (👥 2 people)

The upstream bump changed EC's internals and DOM selectors; our call controls drive that iframe, so sweep them. In a live call with 2 people, confirm **each** of our control-bar buttons works:

- [ ] **Mic** mute/unmute (icon + actual audio)
- [ ] **Camera** on/off
- [ ] **Deafen / Sound** toggle (your deafen key too)
- [ ] **Screenshare** start/stop (and the "Share your screen?" confirm)
- [ ] **Screenshare audio** mute toggle
- [ ] **Fullscreen** toggle
- [ ] **⋮ More** menu → **Spotlight/Grid**, **Reactions**, **Settings** each open the right EC panel
- [ ] **End** call leaves cleanly
- [ ] **PTT** (push-to-talk) if enabled: hold key = transmit, release = mute; releasing on blur works
- [ ] **AFK auto-mute** if enabled: goes muted after the timeout
- [ ] **PiP** (picture-in-picture) mini window: drag, resize, fullscreen button, return-to-call; the "You muted" / "All muted" badges show on the right person
- [ ] **Denoise** (if ML noise suppression enabled): call audio still flows, no silence

If any control does nothing, that usually means an EC DOM selector changed — capture the console and tell me which button.

---

# Backlog of previously-fixed-but-unverified items

> Sections A–D above are **this session's** work. Everything below was fixed in earlier waves and is still flagged **⚠️ UNTESTED** in `LOTUS_BUGS.md` / `LOTUS_TODO.md`. They're grouped by what kind of environment you need (mobile, desktop, screen reader, etc.) so you can knock out a whole category at once. None of these are urgent the way A–D are; do them as you have the right device handy.

## E. Mobile / responsive (needs a real phone, or devtools device emulation)

### E1. Composer toolbar touch targets (#7)

On a phone, open a room and the composer toolbar. Tap each button (attach, format, sticker, emoji, GIF, location, poll, schedule, send).
**Expected:** every button is comfortably tappable (≥44×44px), no mis-taps hitting the wrong icon.

### E2. Room Settings — no horizontal overflow (#8)

On a narrow phone screen, open **Room Settings**.
**Expected:** the settings nav panel fills the full width; **no** horizontal scrollbar / sideways scrolling anywhere in the panel.

### E3. Modals go fullscreen on mobile (#9)

On a phone, open several dialogs: Leave Room, Create Room, Create Space, Invite User, Report (room/user/message), Edit History, Forward Message, Remind Me, Schedule Message, Device Verification, Poll Creator.
**Expected:** each opens **fullscreen** (no floating box, no rounded corners / max-width margins). On desktop the same modals should still be the normal centered boxes.

### E4. Composer not hidden by the keyboard (#10) — iOS Safari especially

On a phone (priority: **iOS Safari**), tap into the composer so the on-screen keyboard appears.
**Expected:** the composer input stays **visible above** the keyboard; the layout shrinks rather than the composer sliding under the keyboard.

### E5. Mobile "Saved Messages" access (Mobile Bookmarks)

On a phone, **inside a room**, open the room header **··· More Options** menu.
**Expected:** a **"Saved Messages"** item is present; tapping it opens the bookmarks panel. (This was the only in-room access point missing on mobile.)

---

## F. Visual / theming

### F1. Animated chat background — no flicker (#2)

Settings → set an **animated** chat background (e.g. anim-rain / anim-aurora / anim-stars). Watch the message text and composer while it animates.
**Expected:** smooth animation, **no flickering / shimmering** on message text or the composer, especially after scrolling. Note your GPU/browser if you see artifacts.

### F2. Background vs. Seasonal theme are mutually exclusive (#6)

In Settings → Appearance:

1. Pick a **chat background** → confirm any **seasonal theme** auto-switches off.
2. Pick a **seasonal theme** → confirm the **chat background** auto-clears to none.
3. (Edge) If you have old data with both set, after reload only one should visibly apply (no double-overlay clutter).

### F3. Background / seasonal picker grid layout (N81)

In Settings → Appearance, look at the **Chat Background** and **Seasonal Theme** swatch grids; resize the window narrow→wide.
**Expected:** swatches reflow to fill each row evenly (responsive grid), with no lopsided/orphaned last row at any width.

---

## G. Calls — additional unverified (👥 2 people)

### G1. PiP mute badges point at the right person (#12)

In a call with at least one other person, pop out the **Picture-in-Picture** mini window.

- **You** mute your own mic → a **"You"/muted badge appears bottom-left** (your status).
- A **remote** participant (or all of them) mutes → an **"All muted"** badge appears **top-right** (clearly about other people).
  **Expected:** the bottom-left badge is **never** triggered by someone else muting — that was the original bug (it looked like your own mic was muted when it wasn't).

### G2. Full-screen camera broadcasts

1. In a **camera-only** call (no screenshare), confirm the **Fullscreen** button is available (previously only showed during screenshare).
2. Use **MemberGlance → Focus camera** to full-screen/spotlight a specific person's camera. (Overlaps **A5**; if you've done A5 you can skip.)

### G3. PTT badge renders on all themes (N53)

Enable **Push-to-talk** (Settings → Calls) and join a call. Hold the PTT key.
**Expected:** the floating PTT badge above the controls shows "PTT — Hold KEY" when idle and "● Live" (green) while held — on **both** a default theme and Lotus Terminal (it's now a single folds Chip; the old terminal-only variant was removed).

---

## H. Media / performance (needs a room with many images)

### H1. Lazy image decryption (P5-5 / MediaGallery)

Open a room / media gallery with **many images** (ideally encrypted). Scroll down through them.
**Expected:** images decrypt/load as they **approach the viewport**, not all at once on open; scrolling stays smooth and memory doesn't balloon. Off-screen images shouldn't all decode up front.

### H2. Thumbnail framing (P5-6)

Look at **tall portrait** images in the timeline and in the media gallery.
**Expected:** thumbnails are framed **center-top** (so faces/subjects at the top aren't cropped out); no awkward stretching. Opening the full-size viewer still shows the **whole** image (contain, not cropped).

---

## I. Accessibility (needs a screen reader: VoiceOver / NVDA / TalkBack)

With a screen reader on, navigate message hover-actions and content and confirm each control **announces a meaningful label** (not "button" / blank):

- [ ] **Reaction** buttons announce the emoji + count (e.g. "thumbsup reaction, 3 people").
- [ ] **Edit history** button announces "View edit history".
- [ ] **Thread indicator** announces "View thread".
- [ ] **Reply** (jump to original) announces "Jump to original message".

---

## J. Desktop / Tauri build only

### J1. Proactive update notifications (P5-40)

In the **desktop (Tauri)** build, with an update available, launch the app (and/or leave it running ~12h).
**Expected:** an in-app toast/badge alerts you that an update is available, without manually checking Settings. (Needs an actual newer release to point at.)

### J2. DTLN noise suppression sanity

In Settings → Calls, enable **ML noise suppression** with the **DTLN** model, then join a call.
**Expected:** your mic audio still flows (no silence/robotic dropouts) and background noise is reduced. Confirmed working earlier but flagged for a final real-call check; verify on **both** web and desktop.

---

## K. Features — end-to-end unverified

### K1. Remind Me Later

On a message, **··· → Remind Me**, pick a short preset (the 20-min one, or wait one out).
**Expected:** when due, a Lotus toast fires linking to that message; the reminder then clears itself. Survives a reload while pending (stored in account data).

### K2. Advanced search filters (P4-9)

In message search: use the **sender picker** (instead of typing `from:@user`), the **date-range** quick presets (Today / Last week / Last month / Last year), and the **Has link** toggle.
**Expected:** each narrows results correctly and reflects in the search.

### K3. Notification content + click target (P5-20 partial)

Trigger a desktop/browser notification for a new message.
**Expected:** it shows the **real message body** (`username: message`, not "New inbox notification from…"); **clicking it** brings the window to front and navigates **directly to that message** (not just the inbox).

---

## L. Fixed — verify

### L1. AFK auto-mute releases the OS microphone indicator on mute (N95) — 👥 live call

**Context (now FIXED):** `useAfkAutoMute.ts` opened its own `getUserMedia` level-monitor capture for the whole call, so the OS recording indicator (green dot on macOS, mic icon on Windows/Linux) stayed lit even when muted. The capture is now gated on the reactive mic-on state — it runs only while unmuted, so muting releases the stream.

**To verify:**

1. Enable **AFK auto-mute** in Settings → Calls and **join a call**.
2. Manually **mute your mic** using the call controls → the **OS recording indicator should clear** within ~a second.
3. **Unmute** → the indicator should re-appear (capture re-acquired).
4. Also confirm AFK still works end-to-end: stay unmuted and silent past the configured timeout → mic auto-mutes with the "muted after inactivity" toast, and the indicator clears.

### L2. Maskable PWA icon (N108) — Android install

1. On **Android Chrome**, install Lotus Chat as a PWA (Add to Home Screen).
2. Look at the **home-screen icon**.

**Expected:** the icon fills the adaptive-icon shape cleanly (the logo centered with safe-zone padding on the dark background), **not** clipped at the corners or floating in an odd box. Also worth a quick check in Chrome DevTools → Application → Manifest that the two `purpose: maskable` icons load without a 404 (this also validates the manifest's icon paths resolve in production — a pre-existing path convention I couldn't verify statically).

---

## M. New features (this round)

### M1. Search: `has:image` / `has:file` / `has:video` filters

1. Open message search (in a room with shared images/files/videos in history).
2. Run a broad search, then toggle the **Images**, **Files**, **Video** chips (in the filter bar, next to "Has link").

**Expected:**

- Each chip narrows the visible results to that message type; multiple active chips = union (any of them).
- Toggling them off restores the full results. The existing room/sender/date/has-link filters still work alongside.
- **Known limitation (by design):** filtering is client-side over already-fetched results, so the visible count can be lower than the server's total for that query — paginating/loading more pulls in more to filter. Confirm this reads acceptably.

### M2. Search: recent searches

1. Run a few different searches, then **clear the search box** and focus it.

**Expected:** your last (up to 10) distinct searches appear as clickable chips; clicking one re-runs it. A **Clear** affordance wipes the list. The list **persists across a page refresh** (localStorage).

### M3. Custom accent color (non-TDS themes) — ⚠️ needs your visual judgment

1. Make sure **Lotus Terminal (TDS)** is **off**. Settings → Appearance → **Custom Accent Color** → pick a color.

**Expected:**

- The app's accent (buttons, selected/active states, links, primary chips) recolors to your choice **live**.
- **Look critically at quality** (this is the part I can't verify): button **text legibility** (OnMain contrast) on the accent buttons; **hover/active** shades; and **selected-row / chip** backgrounds (the translucent "Container" tints). Try a **light** color and a **dark** color and a **saturated** one.
  - If a dark accent makes selected-row text (OnContainer) hard to read, tell me — that's the one spot in the auto-derived palette most likely to need tuning.
- **Reset** clears it back to the theme default.
- Turn **Lotus Terminal ON** → the custom accent should be **ignored** (TDS fixed palette wins) and the picker shows a "non-TDS only" note; turn it back off → custom accent returns.
- Reload → the chosen accent **persists**.

---

### M4. Search: "Pinned only" filter

In message search, toggle the **Pinned** chip.
**Expected:** results narrow to messages currently pinned in their room; composes with the Images/Files/Video chips and room/sender/date filters; toggling off restores results. It also narrows the **encrypted/local-cache** results section (not just server results). Needs a room with actually pinned messages.

### M5. New theme presets (Cyberpunk / Ocean / Blood Red / Classic Matrix / Midnight) — ⚠️ visual judgment

Settings → Appearance → theme picker → try each of the 5 new themes.
**Expected:** each applies a complete, legible dark palette. Code review computed WCAG contrast and all pass AA, but **eyeball these specifically**: **Midnight** (lowest-contrast accent `#6b7ca8` — selected/focus states), **Classic Matrix** (green accents, light-green body text on near-black), **Blood Red** (white-ish text on bright-red buttons). Confirm Success/Warning/Critical (save/leave/delete) still look correctly green/amber/red, not recolored. Switching back to a stock theme should fully revert.

---

## Priority if you're short on time

1. **A4** (in-call banner) + **A3** (ringtone) — newest, most logic, hardest to reproduce.
2. **B1–B3** (polls on a default theme) — the confirmed visual bug.
3. **D** (EC 0.20.1 control sweep) — guards against the upstream merge breaking calls.
4. **A7** false-positive check (normal joins don't show the error overlay).
5. Everything else.
