Skip to main content

Calls — Testing

Strategy

Tests are split across three layers:

LayerToolingScope
UnitJest + React Native Testing LibraryCallService state machine, CallMediaService audio routing, WebrtcSessionManager adapter wiring
IntegrationJest with mocked adaptersSignalingService mode selection, SDP/ICE relay path, WatermelonDB persistence
E2EMaestro (two simulators)Full call flow: start → accept → active → end

Coverage Targets

AreaTarget
CallService state transitions100%
SignalingService routing logic100%
WebrtcSessionManager SDP wiring90%+
CallMediaService audio routes100%
WatermelonDB record lifecycle90%+
Overall call feature coverage≥ 80%

Mocking Rules

  • WebrtcAdapter — always mocked in unit and integration tests; never connect to a real ICE agent.
  • react-native-incall-manager — mock setSpeakerphoneOn and chooseAudioRoute; assert call args.
  • WsSignalingAdapter / TcpClientAdapter — mock send and onMessage; return controlled SDP/ICE payloads.
  • WatermelonDB — use the in-memory adapter (@nozbe/watermelondb/adapters/memory) for integration tests.

Test Cases

Signalling — Unit / Integration

ScenarioExpected result
Initiator calls startCall(peerId, 'voice')WebrtcAdapter.createOffer() called; offer SDP forwarded to SignalingService.send()
SignalingService in auto mode, LAN adapter availableSends via TcpClientAdapter; does not use WsSignalingAdapter
SignalingService in auto mode, LAN adapter unavailableFalls back to WsSignalingAdapter
SignalingService in server modeAlways sends via WsSignalingAdapter regardless of LAN state
Peer receives offer via relayWebrtcSessionManager calls WebrtcAdapter.setRemoteDescription(offer)
Peer sends answerAnswer SDP relayed back to initiator; WebrtcAdapter.setRemoteDescription(answer) called on initiator
ICE candidate receivedWebrtcAdapter.addIceCandidate(candidate) called on the correct adapter
ICE candidate send fails (transport error)Error logged; candidate queued for retry; call not dropped immediately
Both sides complete ICEonConnectionStateChange fires connected; CallService transitions to active

Call Lifecycle — Unit

ScenarioExpected result
startCall while another call is activeReturns error CALL_IN_PROGRESS; no new session created
Peer declines incoming callCallService transitions to ended with status declined
Peer does not answer within 30 sCallService transitions to ended with status missed; timeout cleared
endCall() called by initiatorWebrtcAdapter.close() called; CallMediaService.stopMedia() called; InCallManager.stop() called
endCall() called by peerSame teardown sequence on peer side; call record updated with ended_at

WatermelonDB Persistence — Integration

ScenarioExpected result
Call initiatedcall row created with status: 'ringing'; callparticipant row created for initiator
Call accepted by peercall row updated to status: 'active'; callparticipant row created for peer with joined_at
Call endedcall row updated to status: 'ended'; callparticipant rows updated with left_at
Declined callcall row has status: 'declined'; no active transition recorded
Missed callcall row has status: 'missed'; ended_at set to timeout timestamp

Audio Routing — Unit

ScenarioExpected result
setAudioRoute('earpiece')InCallManager.setSpeakerphoneOn(false) called once
setAudioRoute('speaker')InCallManager.setSpeakerphoneOn(true) called once
setAudioRoute('bluetooth')InCallManager.chooseAudioRoute('bluetooth') called
Audio route changed mid-callPrevious route deactivated; new route activated; no media interruption
CallMediaService.stopMedia() calledInCallManager.stop() called; all tracks stopped; streams set to null

Call Record Sync — Integration

ScenarioExpected result
Call ends with network availableSyncService.schedulePush() called; call and callparticipant rows included in push payload
Call ends with network unavailableRows remain in WatermelonDB; push deferred to next reconnect
Push succeedsServer returns 200; local updated_at stays in sync
Push returns 409 (conflict)SyncService re-pulls affected rows; local display updated

E2E Scenarios (Maestro)

# calls-voice-flow.yaml
appId: com.ylpsapot.mobile
---
- launchApp
- tapOn: "Conversations"
- tapOn: "Peer Alpha"
- tapOn: "Voice call button"
- assertVisible: "Calling…"
- # On second simulator: accept call
- assertVisible: "Call active"
- tapOn: "End call"
- assertVisible: "Call ended"
- assertNotVisible: "Call active"

Run against two simulators: one as initiator, one as peer.


Test File Locations

mobile-app/sapot-mobile-app/
src/
features/calls/
__tests__/
CallService.test.ts
WebrtcSessionManager.test.ts
CallMediaService.test.ts
SignalingService.test.ts
callPersistence.integration.test.ts
e2e/
calls-voice-flow.yaml
calls-video-flow.yaml