Skip to content

Mirror Android L1 + L2 coverage to chat-component + wire testIds (+37 Vitest) - #72

Open
phwizard wants to merge 3 commits into
mainfrom
tf/qa-coverage-mirror
Open

Mirror Android L1 + L2 coverage to chat-component + wire testIds (+37 Vitest)#72
phwizard wants to merge 3 commits into
mainfrom
tf/qa-coverage-mirror

Conversation

@phwizard

Copy link
Copy Markdown
Member

Summary

Adds `roomsSlice.test.ts` — 17 hermetic reducer tests targeting the same field-bug clusters covered by the Android SDK's L1 suite and the iOS SDK's `RoomStoreTests`, ported to the Redux Toolkit slice shape.

The web's `roomMessages` slice unifies what Android splits between `RoomStore` and `MessageStore` (rooms hold their messages inline). Tests call the reducer directly with `(previousState, action)` — pure, no store/middleware/thunks, ~7 ms runtime total.

Cluster mapping (per QA_SCENARIOS.md in the ethora monorepo)

Cluster A — Multi-room state machine (+7)

  • `addRoom` inserts new room keyed by jid
  • `addRoom` preserves existing unread + lastViewed on upsert (REST refreshes must not wipe the badge)
  • `addRoom` rejects invalid jids without mutating state
  • `setCurrentRoom` transitions A → B → A
  • `deleteRoom` drops only the targeted room
  • `updateRoom` merges partial updates without dropping fields
  • `setLastViewedTimestamp` writes back, documenting the normalize-to-ms contract via `getTimestampFromUnknown`

Cluster D — Send + duplication (+4)

  • `addRoomMessage` appends a new message
  • `addRoomMessage` idempotent on duplicate id (MAM-replay safety)
  • `addRoomMessage` merges pending + echo across optimistic/server ids (bidirectional id/xmppId match)
  • `addRoomMessage` cross-room — sending to A doesn't touch B

Cluster F — History + render parity (+4)

  • `setRoomMessages` round-trips content
  • `replaceRoomMessages` sorts by timestamp ascending
  • `deleteRoomMessage` tombstones the bubble — web preserves the row with `isDeleted=true`. Documents divergence from iOS, which drops the row outright (both are intentional contracts).
  • `editRoomMessage` updates body in place

Per-room state isolation (+2)

  • `setComposing` on room A doesn't bleed into room B
  • `setComposing` transitions cleanly true → false on the same room

Test results

`npx vitest run` → 21 tests (this PR +17, Login.test.tsx 4), 0 failures.

Test plan

  • All 21 Vitest cases pass locally
  • CI runs the same suite
  • Reviewer confirms cluster mappings against `QA_SCENARIOS.md`

phwizard added 2 commits May 11, 2026 14:08
Adds roomsSlice.test.ts — 17 hermetic reducer tests targeting the
same field-bug clusters covered by the Android SDK's L1 suite and the
iOS SDK's RoomStoreTests, ported to the Redux Toolkit slice shape.

The web's `roomMessages` slice unifies what Android splits between
RoomStore and MessageStore (rooms hold their messages inline). Tests
call the reducer directly with (previousState, action) — pure, no
store/middleware/thunks, ~7ms total runtime.

Cluster mapping (see QA_SCENARIOS.md in the ethora monorepo):

  Cluster A — multi-room state machine (+7):
    - addRoom inserts new room keyed by jid
    - addRoom preserves existing unread + lastViewed on upsert
      (REST refreshes must not wipe the badge)
    - addRoom rejects invalid jids without mutating state
    - setCurrentRoom transitions A → B → A
    - deleteRoom drops only the targeted room
    - updateRoom merges partial updates without dropping fields
    - setLastViewedTimestamp writes timestamp back onto the room
      (and documents the normalize-to-ms contract via
      getTimestampFromUnknown)

  Cluster D — send + duplication (+4):
    - addRoomMessage appends a new message
    - addRoomMessage idempotent on duplicate id (MAM-replay safety)
    - addRoomMessage merges pending + echo across optimistic/server
      ids (bidirectional id/xmppId match)
    - addRoomMessage cross-room — sending to A doesn't touch B

  Cluster F — history + render parity (+4):
    - setRoomMessages round-trips content
    - replaceRoomMessages sorts by timestamp ascending
    - deleteRoomMessage tombstones the bubble (web contract —
      preserves row with isDeleted=true; documents divergence from
      iOS which drops the row outright)
    - editRoomMessage updates the body in place

  Per-room state isolation (+2):
    - setComposing on room A doesn't bleed into room B
    - setComposing transitions cleanly true → false on same room

Verified: \`npx vitest run\` → 21 tests, 0 failures (was 4 before
this PR — Login.test.tsx; +17 net).
Login.test.tsx +5 tests (4 → 9) pinning real UX contracts the form
must hold:

  - shows BOTH email and password errors when both fields are blank
    (regression guard: a single submit must surface both unrelated
    validation paths — not let the second wipe the first)
  - accepts a password of exactly 6 characters
    (boundary — guards against the classic off-by-one if the check
    flips from `< 6` to `<= 6`)
  - surfaces a "wrong data" error when the server returns 401
    (the documented 401 recovery path that re-uses the
    passwordError slot)
  - does not render error texts on a successful login
    (regression guard against stale errors persisting through a
    successful flow)
  - re-submitting with corrected input replaces stale error text
    (validates the `errors.email && ...` render-guard — a merge-
    instead-of-replace regression would leave the corrected field
    showing the old error)

Web's L2 component test infrastructure (Vitest + RTL +
renderWithProviders) is the canonical equivalent of Android's
Compose UI tests and iOS's ViewModel tests. Existing AuthTestIds
data-testid wiring on Login.tsx makes these tests robust to copy
changes — they query by ID, not by visible text.

Verified: \`npx vitest run\` → 26 tests across roomsSlice.test.ts
+ Login.test.tsx, 0 failures.
@phwizard phwizard changed the title Mirror Android L1 reducer coverage to chat-component (+17 Vitest) Mirror Android L1 + L2 coverage to chat-component (+22 Vitest) May 11, 2026
…ist + tests

Wires the cross-platform selectors from src/testIds.ts onto the
actual React components, then exercises each via component tests.
Before this commit, testIds.ts shipped the contract but no component
rendered it — the matching Android testTags and iOS
accessibilityIdentifiers had no web counterpart to test against.

Wiring (production-code changes):
  - SendInput.tsx → ChatInputTestIds.{attachButton,inputField,sendButton}
    on the paperclip, both input variants (textarea + single-line),
    and the primary send button. Secondary-send variant intentionally
    not wired (the primary button is the canonical send affordance).
  - MessageImage.tsx → MessageBubbleTestIds.mediaContent on both
    image branches (with fileURL and the placeholder fallback) so a
    cross-platform flow doesn't need to know which branch rendered.
  - RoomList.tsx → RoomListTestIds.{roomsList,searchInput} on the
    list container and the search input.
  - ChatRoomItem.tsx → RoomListTestIds.roomRow on the per-row
    container (`<ChatItem>`).
  - NewChatModal.tsx → RoomListTestIds.createRoomButton on the "+"
    trigger Button.

Tests (+15 across 3 new files):

SendInput.test.tsx (+6):
  - renders inputField + attachButton
  - typed text populates the controlled input
  - sendButton is hidden until the user types (no accidental empty sends)
  - typing reveals sendButton; clicking it dispatches sendMessage
    with the typed text
  - config.disableMedia=true hides the attachButton
  - placeholderText prop overrides the default "Type message" copy

RoomList.test.tsx (+6):
  - renders roomsList + searchInput + createRoomButton
  - renders one roomRow per chat
  - typing in searchInput filters rows by title
  - clicking a roomRow fires onRoomClick with that room object
  - hideSearch config hides searchInput
  - disableCreate config hides createRoomButton

MessageImage.test.tsx (+3):
  - renders the <img> with the mediaContent testid + correct src
  - placeholder branch (no fileURL) still carries the same testid
    so cross-platform flows don't need to know which branch rendered
  - clicking the image dispatches the file-preview modal (the only
    opener for the full-screen preview)

Verified: \`npx vitest run\` → 41 tests across 5 files, 0 failures.
Unhandled Firebase Messaging warnings during test runs are unrelated
noise from a transitive import — they're "Unhandled Errors" not test
failures, every test passes.
@phwizard phwizard changed the title Mirror Android L1 + L2 coverage to chat-component (+22 Vitest) Mirror Android L1 + L2 coverage to chat-component + wire testIds (+37 Vitest) May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant