# QA Scenarios — Cross-Platform SDK Test Catalog > Reference catalog of test scenarios across the Android, iOS, and web SDKs. > Each cluster lists the recurring failure modes observed in the field > (anonymized), the recommended test layer, and the current automated > coverage status. Use this as the input to test-planning decisions and as > a cross-platform parity reference. ## Test layers | Layer | Where | Cost per test | Catches | |-------|-------|---------------|---------| | L1 — Hermetic unit | JVM / Swift unit test, fakes for backend | seconds | Pure logic: reducers, parsers, state machines, formatters | | L2 — Component UI | Compose UI / SwiftUI / Vitest + RTL | seconds–minutes | Single-component rendering + interaction | | L3 — E2E synthetic | Maestro / Playwright against seeded test app | minutes | Multi-screen flows, real network, cross-platform parity | | L4 — Manual | Tester on a real device | hours | UX feel, IME quirks, real-device-only bugs | General rule: **push every test as far down the pyramid as it will go.** A pure reducer behaviour that can be expressed as L1 should never live as L2 or L3. L3 is reserved for what L1 + L2 genuinely cannot reach — real network, real OS surfaces, cross-platform parity, second device. ## Scenario clusters ### A. Multi-room state machine **Failure modes:** - After opening Room A successfully, switching to Room B leaves the UI on "Connecting" indefinitely. - After re-login, only the most recently opened room loads cleanly; previously opened rooms then stick on "Connecting". - Some rooms permanently spin on history fetch while others in the same session load fine. - Room header shows "0 users" despite the room having members. - Empty-state placeholder ("Start a conversation") renders in place of a previously-working room after a server-side data shape change. **Why high priority:** all symptoms point at the same underlying class — per-room state not isolated from session-level state, and presence / history responses not surviving room switches cleanly. Fix-once, test-everywhere territory. **Recommended tests:** - L1 `RoomStoreTest`: open A → open B → switch back to A. Assert per-room connection state survives the switch, current-room pointer transfers, no cross-contamination of pending presence callbacks. - L1 `XMPPPresenceCacheTest`: presence-response markers persist across room changes; no stale `joining=true` flag leaks after switch. - L1 `RoomMembershipTest`: member count derived from roster matches `members.size`, not a stale field — catches the "0 users when populated" failure mode. - L2 Compose / SwiftUI `ChatRoomViewTest`: prop change to room JID re-keys the tree cleanly. Scroll state, draft, and unread reset without holding refs to the previous room. - L3 Maestro: open A → open B → back to A flow, repeated 3× under one session; assert no "Connecting" loop on the third re-entry. **Coverage status:** None hermetic today. Highest-priority cluster. ### B. Send + media orchestration **Failure modes:** - Text + image sent in the same compose action → image arrives, text silently dropped. - File upload completes but doesn't commit as a chat message until the user performs another action (sends a second message, leaves room, etc.). - Send orchestrator emits inconsistent statuses ("sending" vs immediate error) for messages that ultimately deliver successfully. - Combined-send variant per platform: web drops text, native drops or stalls file — same root cause, different presentation. **Recommended tests:** - L1 `MessageSendOrchestratorTest`: `send(body, mediaUri)`. Assert orchestrator emits exactly 2 outgoing messages (or 1 combined OOB, per the locked contract), in order, neither swallowed. Current SDK contract treats two separate messages as acceptable. - L1 `MessageSendOrchestratorTest`: `send(mediaUri only)`. Assert 1 outgoing OOB stanza, no spurious empty-body stanza alongside. - L1 `MessageSendOrchestratorTest`: `send(body only)`. Assert 1 outgoing body stanza, no orphan media artifact. - L2 Compose / SwiftUI `ChatInputTest` extension: attach preview + typed text → tap send → assert both render as expected bubbles. **Coverage status:** Partial. `PendingMediaSendQueueTest` covers the queue; no orchestrator-contract test exists yet. ### C. Connection lifecycle **Failure modes:** - XMPP socket stays null until eventual reopening — incoming messages backfill only when the user resumes the app or sends a second message. - Pending media uploads commit only when the user triggers a subsequent action, not when the network is actually restored. - Same user logged into two sessions (e.g., real device + simulator) sees their own messages duplicated, with a noticeable backfill delay. **Recommended tests:** - L1 `XMPPConnectionStateMachineTest`: reconnect transitions don't double-flush pending queues; socket-null guard doesn't silently swallow outgoing sends. - L1 `MessageRetryQueueTest`: queue commits on a connection-restored event, not on an unrelated user action. - L3 Maestro: backgrounding flow — send msg → home button → resume after 10s → assert msg delivered without manual second-send nudge. - L4 Manual: dual-session same-user test (XMPP resource conflict territory), since the duplicate-self-message bug only surfaces with a real second client. **Coverage status:** None hermetic. ### D. Send-status correctness & duplication **Failure modes:** - Bulk-send under degraded network → some messages briefly show error → seconds later they deliver successfully → original error bubbles remain visible alongside the delivered duplicate. - Status flips between "sending" and "error" inconsistently for messages that eventually deliver fine. - App crashes during sustained high-frequency send on certain Android API levels. **Recommended tests:** - L1 `MessageStoreTest`: `pending → sendFailed → server-echo` arrives. Assert echo merges into the original bubble; no stale failed bubble remains in the list. - L1 `MessageStoreTest`: explicit `retry()` vs auto-retry — both paths must reconcile against incoming echo, neither leaves a duplicate. - L1 `MessageStoreTest`: bulk add of 10 messages where 3 are marked `sendFailed=true` first, then all 10 echo back from server → final list size is 10, no orphan failed bubbles. - L2 Compose / SwiftUI: drive a 10-message rapid-send with simulated 30% failure rate; assert final visible bubble count is 10, not 10 + 3 stale failures. - L4 Manual: sustained-send + intentional network blip + crash-resistance test across the Android API matrix. **Coverage status:** Partial. Bidirectional `pending ↔ echo` ID matching covered. Failure-then-recovery NOT covered. ### E. Unread counter **Failure modes:** - Own messages appear as unread after re-login (cursor reset bug — own messages from MAM history should never count as unread). - Unread badges don't render on the room-list when no chat room is currently focused. - Edge cases acknowledged as still being investigated: re-entry resets, multi-room concurrent receive, edits, deletes. **Recommended tests:** - L1 `UnreadObserverTest`: MAM replay of own messages → cursor sets to last own message → unread count = 0. - L1: room focus reset clears unread badge on entry; room-switch transfers focus correctly without bleeding unread between rooms. - L1: deleted-while-unread doesn't decrement past zero; deleting an already-read message doesn't change count. - L1: edits-while-unread don't double-count (edit is not a new unread). - L1: badge formatter — 0 → empty, 1-99 → exact, 100+ → "99+". - L2: badge component shows count when prop > 0, hides when 0. **Coverage status:** Partial. `UnreadObserverTest` exists. Edge cases above mostly uncovered. ### F. History + render parity **Failure modes:** - After re-login, first room opened shows no history (silent load failure). - File icon missing on file messages loaded from MAM history; the same file-type message rendered from a live receive shows the icon correctly. - Some rooms permanently loading history while others in the same session load fine. **Recommended tests:** - L1 `MessageCacheTest`: seed 30 messages → re-instantiate store → assert 30 visible without round-trip (verifies fast-load contract). - L1 `MessageCacheTest`: per-room cap at 100 — seed 150, assert eviction retains the most-recent 100. - L1 `MessageRenderTest`: `Message(type=file)` rendered from "live receive" path vs "MAM replay" path → assert visual output identical (icon, filename, click target). Catches the historical-icon-missing class of bug. - L2 Compose / SwiftUI: file-message bubble snapshot — regardless of origin path, icon and metadata must render. **Coverage status:** None for cache contract. Visual parity gap (history vs live receive) not covered. ### G. Media upload edges **Failure modes:** - Camera-captured photos can't be sent on Android (different URI scheme path than gallery picker, likely diverges in the upload pipeline). - Large files (>2 MB) fail with 413 in some setups but succeed in others — reference server limit is 200 MB, so failures suggest client- or proxy-side size limits not aligned with backend. - Displayed file size differs between bubble preview and file picker — two formatters drifting. - Sent vs received file bubbles styled differently (sent inset as card, received inlined) — looks like a regression rather than intent. **Recommended tests:** - L1 `MediaSendTest`: file URI from camera → upload pipeline path matches gallery URI path; no special-case lost. - L1 `MediaSizeFormatterTest`: single source of truth between preview formatter and bubble formatter — same byte count → same human string. - L2 Compose / SwiftUI: sent vs received file bubble snapshot parity — same `Message` data, both branches must produce visually equivalent bubbles, or document the intentional difference. **Coverage status:** None. ### H. Configuration safety **Failure modes:** - SDK has historically shipped with hardcoded fallback URLs that would silently route to default servers if configuration was missing — flagged as a privacy/security risk by enterprise consumers. Subsequent decision: SDK must fail explicitly on missing config, never silently fall back. **Recommended tests:** - L1 `ChatConfigurationTest`: SDK init **throws** on missing API host / XMPP host / app ID — no fallback to any default value, no silent degraded mode. - Build-time check: source regex scan for known-default URL patterns in compiled SDK output. **Coverage status:** Partial. `ChatConfigurationTest` exists; explicit "no-fallback, fail-loud" assertion missing. ### I. UI affordances **Failure modes:** - Back arrow visible even when the rooms feature is disabled by config; tapping is a no-op. - Scroll-to-bottom after send doesn't reach the bottommost message; user has to tap the scroll-down chip after every send. - URL bubbles tinted / dimmed inconsistently with other text content. - Header shows "Chat room name" placeholder and "0 users" even when the room is populated — partial render bug. **Recommended tests:** - L2: each affordance against the config matrix — `roomsDisabled = true` must hide the back arrow; `singleRoomMode = true` must hide the room list. - L2: post-send scroll position — assert lazy column / list view scrolls to last item index after the send action. - L2 / screenshot test: bubble style matrix (text, URL, image, file, deleted, pending, failed) — preferably with Paparazzi / Roborazzi for Android and reference-image snapshots for iOS once a baseline exists. - L4 Manual: IME / keyboard layout, since Compose UI assertions on IME state are flaky. **Coverage status:** Component-level tests exist for several affordances. Config-matrix coverage is thin. ### J. App lifecycle **Failure modes:** - App crashes during message send on specific Android API levels. - Process death + restore behavior unverified for draft text, unread counts, scroll position. **Recommended tests:** - L1 `SavedInstanceStateTest`: draft text and scroll position survive a config change (rotation, font scale). - L2 Maestro: backgrounding → resume after 30s → assert no crash, state intact, unread badge correct. - L4 Manual: device-specific Android API matrix (10 / 11 / 12 / 13 / 14 / 15) for crash patterns under sustained load. **Coverage status:** None. ### K. Auth + token state **Failure modes:** - 401 Unauthorized on `/users/client` after a JWT format change between major SDK versions. - Token re-dispatch missing → chat component fails to re-init when the embedding app refreshes its token prop. **Recommended tests:** - L1 `JwtFormatTest`: parser handles both legacy and current token shapes cleanly during a deprecation window. - L1 `LoginWrapperTest`: token-prop change triggers full re-init (close existing sockets, clear stores, re-auth, re-join). - L2 Vitest (web): `` re-renders on token swap. **Coverage status:** Partial on web. Native unrepresented. ### L. Cross-platform parity **Failure modes:** - Message delivery instant browser → Android, ~500 ms Android → browser — same test, asymmetric latency. - Combined send (text + media) drops different parts on different platforms — same orchestration bug presenting differently. - Same multi-room state bug surfaces with different symptoms on web vs native. **Recommended tests:** - L1: shared test-vectors file (JSON) covering wire-level stanza shapes, consumed by parsers on both Android and iOS — guarantees parse symmetry for every documented stanza type. - L3 Maestro + Playwright shared seeded fixture: send from platform A, assert receipt on platform B within N seconds, repeat for each platform pair. **Coverage status:** None. Highest infrastructure cost (needs the seeded test app instance, below). ## Seeded test app instance — recommended infrastructure Several clusters above (A, C, F, L especially) gain massive testability if runs target a pre-seeded test app instance with known data rather than generating data live every run. Suggested shape: - A dedicated QA app ID (e.g. `ethora_qa_`) - 4 deterministic test users (`qa-a`, `qa-b`, `qa-c`, `qa-d`) - 3 deterministic rooms: - empty room (baseline render) - active room (50 messages: mix of text, media, edited, deleted) - long-history room (500 messages, deterministic content) - A reset / reseed script (drops + reseeds in ~10s via existing admin APIs) - A CI job that wakes the QA instance before each E2E suite run After this exists, every cross-platform parity test becomes cheap and deterministic. Estimated build cost: ~1 day. ## Recommended priority — next 30 days 1. **L1 cluster targeting recurring field bugs** (cheap, hermetic, no infra): A multi-room state, D failure-recovery, E unread MAM, F cache contract, B send-orchestrator. Target: 12–15 new tests. 2. **L2 Compose / SwiftUI extensions on emulator / simulator** (week 2): config-matrix UI affordances (I), combined-send (B). 3. **Seeded test app infrastructure** (week 3): unblocks L3. 4. **L3 cross-platform parity** (after seeded app): Maestro + Playwright on the top five flows. ## How to extend this catalog - Add new clusters when a field bug class doesn't fit cleanly into A–L. - Within a cluster, add concrete failure modes (one bullet each) before adding tests — they are the evidence base for why the test matters. - Anonymize: no customer names, individual engineer names, JIRA ticket IDs, or environment-specific URLs. Generic descriptions only. - Mark coverage status honestly — "Partial" means *some* tests exist; list the specific test names if it helps future readers. ## Per-SDK testing entry points - Android SDK: `sdk-android/README.md` — Compose UI test scaffold + JVM unit-test layer. - iOS SDK: `sdk-swift/README.md` — XCTest scaffold + accessibility identifiers for cross-platform parity. - Web (chat component): `sdk-reactjs/README.md` — Vitest + RTL setup, shared `data-testid` IDs. - Sample apps: `sample-android/README.md`, `sample-swift/README.md` — Maestro E2E flow libraries.