@@ -52,6 +52,7 @@ class MockRTCPeerConnection {
5252 remoteDescription : RTCSessionDescriptionInit | null = null ;
5353 onicecandidate : ( ( event : { candidate : unknown } ) => void ) | null = null ;
5454 onconnectionstatechange : ( ( ) => void ) | null = null ;
55+ ontrack : ( ( event : { track : MediaStreamTrack } ) => void ) | null = null ;
5556
5657 createOffer = vi . fn ( ) . mockResolvedValue ( { type : 'offer' , sdp : 'test-sdp' } ) ;
5758 createAnswer = vi . fn ( ) . mockResolvedValue ( { type : 'answer' , sdp : 'test-answer-sdp' } ) ;
@@ -113,7 +114,20 @@ describe('useWebRTCHostAPI', () => {
113114 ( globalThis as Record < string , unknown > ) . EventSource = MockEventSource ;
114115 ( globalThis as Record < string , unknown > ) . RTCPeerConnection = MockRTCPeerConnection ;
115116 ( globalThis as Record < string , unknown > ) . RTCIceCandidate = vi . fn ( ( c : unknown ) => c ) ;
117+ ( globalThis as Record < string , unknown > ) . MediaStream = vi . fn ( ( tracks : unknown [ ] ) => ( {
118+ getTracks : ( ) => tracks ,
119+ getAudioTracks : ( ) => tracks . filter ( ( track ) => ( track as { kind ?: string } ) . kind === 'audio' ) ,
120+ getVideoTracks : ( ) => tracks . filter ( ( track ) => ( track as { kind ?: string } ) . kind === 'video' ) ,
121+ } ) ) ;
116122 ( globalThis as Record < string , unknown > ) . fetch = mockFetch ;
123+ ( globalThis as Record < string , unknown > ) . Audio = vi . fn ( ( ) => ( {
124+ srcObject : null ,
125+ autoplay : false ,
126+ volume : 1 ,
127+ muted : false ,
128+ play : vi . fn ( ) . mockResolvedValue ( undefined ) ,
129+ pause : vi . fn ( ) ,
130+ } ) ) ;
117131 } ) ;
118132
119133 afterEach ( ( ) => {
@@ -383,6 +397,47 @@ describe('useWebRTCHostAPI', () => {
383397 const mockPC = viewer ?. peerConnection as unknown as MockRTCPeerConnection ;
384398 expect ( mockPC . createDataChannel ) . toHaveBeenCalledWith ( 'control' , { ordered : true } ) ;
385399 } ) ;
400+
401+ it ( 'should create a local audio element when a viewer audio track is received' , async ( ) => {
402+ const { result } = renderHook ( ( ) => useWebRTCHostAPI ( defaultOptions ) ) ;
403+
404+ await act ( async ( ) => {
405+ await result . current . startHosting ( ) ;
406+ } ) ;
407+
408+ const es = MockEventSource . instances [ 0 ] ;
409+ act ( ( ) => {
410+ es . emit ( 'connected' , JSON . stringify ( { sessionId : 'session-1' } ) ) ;
411+ } ) ;
412+
413+ await act ( async ( ) => {
414+ es . emit (
415+ 'presence-join' ,
416+ JSON . stringify ( {
417+ presences : [ { user_id : 'viewer-1' , role : 'viewer' } ] ,
418+ } )
419+ ) ;
420+ } ) ;
421+
422+ await act ( async ( ) => {
423+ await vi . waitFor ( ( ) => {
424+ expect ( result . current . viewerCount ) . toBe ( 1 ) ;
425+ } ) ;
426+ } ) ;
427+
428+ const viewer = result . current . viewers . get ( 'viewer-1' ) ;
429+ const mockPC = viewer ?. peerConnection as unknown as MockRTCPeerConnection ;
430+ const audioTrack = { kind : 'audio' } as MediaStreamTrack ;
431+
432+ await act ( async ( ) => {
433+ mockPC . ontrack ?.( { track : audioTrack } ) ;
434+ } ) ;
435+
436+ const updatedViewer = result . current . viewers . get ( 'viewer-1' ) ;
437+ expect ( updatedViewer ?. audioTrack ) . toBe ( audioTrack ) ;
438+ expect ( updatedViewer ?. audioElement ) . toBeTruthy ( ) ;
439+ expect ( ( globalThis as Record < string , unknown > ) . Audio ) . toHaveBeenCalled ( ) ;
440+ } ) ;
386441 } ) ;
387442
388443 describe ( 'signal handling' , ( ) => {
0 commit comments