@@ -5,9 +5,12 @@ import { DEEP_LINKING } from '../../../actions/actionsTypes';
55import type { VoipPayload } from '../../../definitions/Voip' ;
66import NativeVoipModule from '../../native/NativeVoip' ;
77import { getInitialMediaCallEvents , setupMediaCallEvents } from './MediaCallEvents' ;
8+ import { useCallStore } from './useCallStore' ;
89
910const mockDispatch = jest . fn ( ) ;
1011const mockSetNativeAcceptedCallId = jest . fn ( ) ;
12+ const mockAddEventListener = jest . fn ( ) ;
13+ const mockRNCallKeepClearInitialEvents = jest . fn ( ) ;
1114
1215jest . mock ( '../../methods/helpers' , ( ) => ( {
1316 ...jest . requireActual ( '../../methods/helpers' ) ,
@@ -23,9 +26,7 @@ jest.mock('../../store', () => ({
2326
2427jest . mock ( './useCallStore' , ( ) => ( {
2528 useCallStore : {
26- getState : ( ) => ( {
27- setNativeAcceptedCallId : mockSetNativeAcceptedCallId
28- } )
29+ getState : jest . fn ( )
2930 }
3031} ) ) ;
3132
@@ -37,12 +38,13 @@ jest.mock('../../native/NativeVoip', () => ({
3738 }
3839} ) ) ;
3940
40- const mockRNCallKeepClearInitialEvents = jest . fn ( ) ;
41-
4241jest . mock ( 'react-native-callkeep' , ( ) => ( {
43- addEventListener : jest . fn ( ) ,
44- clearInitialEvents : ( ...args : unknown [ ] ) => mockRNCallKeepClearInitialEvents ( ...args ) ,
45- getInitialEvents : jest . fn ( ( ) => Promise . resolve ( [ ] ) )
42+ __esModule : true ,
43+ default : {
44+ addEventListener : ( ...args : unknown [ ] ) => mockAddEventListener ( ...args ) ,
45+ clearInitialEvents : ( ...args : unknown [ ] ) => mockRNCallKeepClearInitialEvents ( ...args ) ,
46+ getInitialEvents : jest . fn ( ( ) => Promise . resolve ( [ ] ) )
47+ }
4648} ) ) ;
4749
4850jest . mock ( './MediaSessionInstance' , ( ) => ( {
@@ -51,6 +53,10 @@ jest.mock('./MediaSessionInstance', () => ({
5153 }
5254} ) ) ;
5355
56+ jest . mock ( '../restApi' , ( ) => ( {
57+ registerPushToken : jest . fn ( ( ) => Promise . resolve ( ) )
58+ } ) ) ;
59+
5460function buildIncomingPayload ( overrides : Partial < VoipPayload > = { } ) : VoipPayload {
5561 return {
5662 callId : 'call-b-uuid' ,
@@ -64,13 +70,32 @@ function buildIncomingPayload(overrides: Partial<VoipPayload> = {}): VoipPayload
6470 } ;
6571}
6672
73+ function getToggleHoldHandler ( ) : ( payload : { hold : boolean ; callUUID : string } ) => void {
74+ const call = mockAddEventListener . mock . calls . find ( ( [ name ] ) => name === 'didToggleHoldCallAction' ) ;
75+ if ( ! call ) {
76+ throw new Error ( 'didToggleHoldCallAction listener not registered' ) ;
77+ }
78+ return call [ 1 ] as ( payload : { hold : boolean ; callUUID : string } ) => void ;
79+ }
80+
81+ /** Minimal store slice: handler only runs hold logic when call + matching callId/native id exist. */
82+ const activeCallBase = {
83+ call : { } as object ,
84+ callId : 'uuid-1' ,
85+ nativeAcceptedCallId : null as string | null
86+ } ;
87+
6788describe ( 'MediaCallEvents cross-server accept (slice 3)' , ( ) => {
89+ const getState = useCallStore . getState as jest . Mock ;
90+
6891 describe ( 'VoipAccept via setupMediaCallEvents' , ( ) => {
6992 let teardown : ( ( ) => void ) | undefined ;
7093
7194 beforeEach ( ( ) => {
7295 jest . clearAllMocks ( ) ;
96+ mockAddEventListener . mockImplementation ( ( ) => ( { remove : jest . fn ( ) } ) ) ;
7397 ( NativeVoipModule . getInitialEvents as jest . Mock ) . mockReturnValue ( null ) ;
98+ getState . mockReturnValue ( { setNativeAcceptedCallId : mockSetNativeAcceptedCallId } ) ;
7499 teardown = setupMediaCallEvents ( ) ;
75100 } ) ;
76101
@@ -163,10 +188,12 @@ describe('MediaCallEvents cross-server accept (slice 3)', () => {
163188 describe ( 'getInitialMediaCallEvents' , ( ) => {
164189 beforeEach ( ( ) => {
165190 jest . clearAllMocks ( ) ;
191+ mockAddEventListener . mockImplementation ( ( ) => ( { remove : jest . fn ( ) } ) ) ;
166192 mockRNCallKeepClearInitialEvents . mockClear ( ) ;
167193 ( NativeVoipModule . getInitialEvents as jest . Mock ) . mockReset ( ) ;
168194 ( NativeVoipModule . clearInitialEvents as jest . Mock ) . mockClear ( ) ;
169195 ( RNCallKeep . getInitialEvents as jest . Mock ) . mockResolvedValue ( [ ] ) ;
196+ getState . mockReturnValue ( { setNativeAcceptedCallId : mockSetNativeAcceptedCallId } ) ;
170197 } ) ;
171198
172199 it ( 'returns true and dispatches failure deep link when stash has voipAcceptFailed + host + callId' , async ( ) => {
@@ -220,3 +247,102 @@ describe('MediaCallEvents cross-server accept (slice 3)', () => {
220247 } ) ;
221248 } ) ;
222249} ) ;
250+
251+ describe ( 'setupMediaCallEvents — didToggleHoldCallAction' , ( ) => {
252+ const toggleHold = jest . fn ( ) ;
253+ const getState = useCallStore . getState as jest . Mock ;
254+
255+ beforeEach ( ( ) => {
256+ jest . clearAllMocks ( ) ;
257+ toggleHold . mockClear ( ) ;
258+ mockAddEventListener . mockImplementation ( ( ) => ( { remove : jest . fn ( ) } ) ) ;
259+ getState . mockReturnValue ( { ...activeCallBase , isOnHold : false , toggleHold } ) ;
260+ } ) ;
261+
262+ it ( 'registers didToggleHoldCallAction via RNCallKeep.addEventListener' , ( ) => {
263+ setupMediaCallEvents ( ) ;
264+ expect ( mockAddEventListener ) . toHaveBeenCalledWith ( 'didToggleHoldCallAction' , expect . any ( Function ) ) ;
265+ } ) ;
266+
267+ it ( 'hold: true when isOnHold is false calls toggleHold once' , ( ) => {
268+ setupMediaCallEvents ( ) ;
269+ getToggleHoldHandler ( ) ( { hold : true , callUUID : 'uuid-1' } ) ;
270+ expect ( toggleHold ) . toHaveBeenCalledTimes ( 1 ) ;
271+ } ) ;
272+
273+ it ( 'hold: true when isOnHold is true does not call toggleHold' , ( ) => {
274+ getState . mockReturnValue ( { ...activeCallBase , isOnHold : true , toggleHold } ) ;
275+ setupMediaCallEvents ( ) ;
276+ getToggleHoldHandler ( ) ( { hold : true , callUUID : 'uuid-1' } ) ;
277+ expect ( toggleHold ) . not . toHaveBeenCalled ( ) ;
278+ } ) ;
279+
280+ it ( 'hold: false after OS-initiated hold calls toggleHold once (auto-resume)' , ( ) => {
281+ setupMediaCallEvents ( ) ;
282+ const handler = getToggleHoldHandler ( ) ;
283+ handler ( { hold : true , callUUID : 'uuid-1' } ) ;
284+ getState . mockReturnValue ( { ...activeCallBase , isOnHold : true , toggleHold } ) ;
285+ handler ( { hold : false , callUUID : 'uuid-1' } ) ;
286+ expect ( toggleHold ) . toHaveBeenCalledTimes ( 2 ) ;
287+ } ) ;
288+
289+ it ( 'hold: false without prior OS-initiated hold does not call toggleHold' , ( ) => {
290+ setupMediaCallEvents ( ) ;
291+ getToggleHoldHandler ( ) ( { hold : false , callUUID : 'uuid-1' } ) ;
292+ expect ( toggleHold ) . not . toHaveBeenCalled ( ) ;
293+ } ) ;
294+
295+ it ( 'consecutive hold: true events call toggleHold only once' , ( ) => {
296+ setupMediaCallEvents ( ) ;
297+ const handler = getToggleHoldHandler ( ) ;
298+ handler ( { hold : true , callUUID : 'uuid-1' } ) ;
299+ getState . mockReturnValue ( { ...activeCallBase , isOnHold : true , toggleHold } ) ;
300+ handler ( { hold : true , callUUID : 'uuid-1' } ) ;
301+ expect ( toggleHold ) . toHaveBeenCalledTimes ( 1 ) ;
302+ } ) ;
303+
304+ it ( 'clears stale auto-hold when callUUID does not match current call id (e.g. new workspace / call)' , ( ) => {
305+ setupMediaCallEvents ( ) ;
306+ const handler = getToggleHoldHandler ( ) ;
307+ handler ( { hold : true , callUUID : 'uuid-1' } ) ;
308+ expect ( toggleHold ) . toHaveBeenCalledTimes ( 1 ) ;
309+ getState . mockReturnValue ( {
310+ call : { } ,
311+ callId : 'uuid-2' ,
312+ nativeAcceptedCallId : null ,
313+ isOnHold : true ,
314+ toggleHold
315+ } ) ;
316+ handler ( { hold : false , callUUID : 'uuid-1' } ) ;
317+ expect ( toggleHold ) . toHaveBeenCalledTimes ( 1 ) ;
318+ handler ( { hold : false , callUUID : 'uuid-2' } ) ;
319+ expect ( toggleHold ) . toHaveBeenCalledTimes ( 1 ) ;
320+ } ) ;
321+
322+ it ( 'does not toggle when there is no active call object even if ids match' , ( ) => {
323+ setupMediaCallEvents ( ) ;
324+ const handler = getToggleHoldHandler ( ) ;
325+ getState . mockReturnValue ( {
326+ call : null ,
327+ callId : 'uuid-1' ,
328+ nativeAcceptedCallId : null ,
329+ isOnHold : false ,
330+ toggleHold
331+ } ) ;
332+ handler ( { hold : true , callUUID : 'uuid-1' } ) ;
333+ expect ( toggleHold ) . not . toHaveBeenCalled ( ) ;
334+ } ) ;
335+
336+ it ( 'cleanup removes didToggleHoldCallAction subscription' , ( ) => {
337+ const remove = jest . fn ( ) ;
338+ mockAddEventListener . mockImplementation ( ( event : string ) => {
339+ if ( event === 'didToggleHoldCallAction' ) {
340+ return { remove } ;
341+ }
342+ return { remove : jest . fn ( ) } ;
343+ } ) ;
344+ const cleanup = setupMediaCallEvents ( ) ;
345+ cleanup ( ) ;
346+ expect ( remove ) . toHaveBeenCalled ( ) ;
347+ } ) ;
348+ } ) ;
0 commit comments