@@ -16,22 +16,49 @@ let lastChannel: FakeRTCDataChannel | null = null;
1616
1717class FakeRTCPeerConnection {
1818 ontrack : ( ( ev : any ) => void ) | null = null ;
19+ onconnectionstatechange : ( ( ) => void ) | null = null ;
20+ connectionState = 'new' ;
21+
1922 createDataChannel ( _name : string ) {
2023 lastChannel = new FakeRTCDataChannel ( ) ;
2124 // simulate async open event
22- setTimeout ( ( ) => lastChannel ?. dispatchEvent ( new Event ( 'open' ) ) ) ;
25+ setTimeout ( ( ) => {
26+ this . _simulateStateChange ( 'connected' ) ;
27+ lastChannel ?. dispatchEvent ( new Event ( 'open' ) ) ;
28+ } , 0 ) ;
2329 return lastChannel as unknown as RTCDataChannel ;
2430 }
2531 addTrack ( ) { }
2632 async createOffer ( ) {
33+ this . _simulateStateChange ( 'connecting' ) ;
2734 return { sdp : 'offer' , type : 'offer' } ;
2835 }
2936 async setLocalDescription ( _desc : any ) { }
3037 async setRemoteDescription ( _desc : any ) { }
31- close ( ) { }
38+ close ( ) {
39+ this . _simulateStateChange ( 'closed' ) ;
40+ }
3241 getSenders ( ) {
3342 return [ ] as any ;
3443 }
44+
45+ _simulateStateChange (
46+ state :
47+ | 'new'
48+ | 'connecting'
49+ | 'connected'
50+ | 'disconnected'
51+ | 'failed'
52+ | 'closed' ,
53+ ) {
54+ if ( this . connectionState === state ) return ;
55+ this . connectionState = state ;
56+ setTimeout ( ( ) => {
57+ if ( this . onconnectionstatechange ) {
58+ this . onconnectionstatechange ( ) ;
59+ }
60+ } , 0 ) ;
61+ }
3562}
3663
3764describe ( 'OpenAIRealtimeWebRTC.interrupt' , ( ) => {
@@ -219,6 +246,113 @@ describe('OpenAIRealtimeWebRTC.interrupt', () => {
219246 } ) ;
220247} ) ;
221248
249+ describe ( 'OpenAIRealtimeWebRTC.connectionState' , ( ) => {
250+ const originals : Record < string , any > = { } ;
251+
252+ beforeEach ( ( ) => {
253+ originals . RTCPeerConnection = ( global as any ) . RTCPeerConnection ;
254+ originals . navigator = ( global as any ) . navigator ;
255+ originals . document = ( global as any ) . document ;
256+ originals . fetch = ( global as any ) . fetch ;
257+
258+ ( global as any ) . RTCPeerConnection = FakeRTCPeerConnection as any ;
259+ Object . defineProperty ( globalThis , 'navigator' , {
260+ value : {
261+ mediaDevices : {
262+ getUserMedia : async ( ) => ( {
263+ getAudioTracks : ( ) => [ { enabled : true } ] ,
264+ } ) ,
265+ } ,
266+ } ,
267+ configurable : true ,
268+ writable : true ,
269+ } ) ;
270+ Object . defineProperty ( globalThis , 'document' , {
271+ value : { createElement : ( ) => ( { autoplay : true } ) } ,
272+ configurable : true ,
273+ writable : true ,
274+ } ) ;
275+ Object . defineProperty ( globalThis , 'fetch' , {
276+ value : async ( ) => ( {
277+ text : async ( ) => 'answer' ,
278+ headers : {
279+ get : ( headerKey : string ) => {
280+ if ( headerKey === 'Location' ) {
281+ return 'https://api.openai.com/v1/calls/rtc_u1_1234567890' ;
282+ }
283+ return null ;
284+ } ,
285+ } ,
286+ } ) ,
287+ configurable : true ,
288+ writable : true ,
289+ } ) ;
290+ } ) ;
291+
292+ afterEach ( ( ) => {
293+ ( global as any ) . RTCPeerConnection = originals . RTCPeerConnection ;
294+ Object . defineProperty ( globalThis , 'navigator' , {
295+ value : originals . navigator ,
296+ configurable : true ,
297+ writable : true ,
298+ } ) ;
299+ Object . defineProperty ( globalThis , 'document' , {
300+ value : originals . document ,
301+ configurable : true ,
302+ writable : true ,
303+ } ) ;
304+ Object . defineProperty ( globalThis , 'fetch' , {
305+ value : originals . fetch ,
306+ configurable : true ,
307+ writable : true ,
308+ } ) ;
309+ lastChannel = null ;
310+ } ) ;
311+
312+ it ( 'fires connection_change and disconnects on peer connection failure' , async ( ) => {
313+ const rtc = new OpenAIRealtimeWebRTC ( ) ;
314+ const events : string [ ] = [ ] ;
315+ rtc . on ( 'connection_change' , ( status ) => events . push ( status ) ) ;
316+ await rtc . connect ( { apiKey : 'ek_test' } ) ;
317+ expect ( rtc . status ) . toBe ( 'connected' ) ;
318+ expect ( events ) . toEqual ( [ 'connecting' , 'connected' ] ) ;
319+ const pc = rtc . connectionState
320+ . peerConnection as unknown as FakeRTCPeerConnection ;
321+ expect ( pc ) . toBeInstanceOf ( FakeRTCPeerConnection ) ;
322+ pc . _simulateStateChange ( 'failed' ) ;
323+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
324+ expect ( rtc . status ) . toBe ( 'disconnected' ) ;
325+ expect ( events ) . toEqual ( [ 'connecting' , 'connected' , 'disconnected' ] ) ;
326+ } ) ;
327+
328+ it ( 'migrates connection state handler when peer connection is replaced' , async ( ) => {
329+ class CustomFakePeerConnection extends FakeRTCPeerConnection { }
330+ const customPC = new CustomFakePeerConnection ( ) ;
331+
332+ const rtc = new OpenAIRealtimeWebRTC ( {
333+ changePeerConnection : async ( ) => customPC as any ,
334+ } ) ;
335+
336+ const closeSpy = vi . spyOn ( rtc , 'close' ) ;
337+ const events : string [ ] = [ ] ;
338+ rtc . on ( 'connection_change' , ( status ) => events . push ( status ) ) ;
339+
340+ await rtc . connect ( { apiKey : 'ek_test' } ) ;
341+
342+ expect ( rtc . status ) . toBe ( 'connected' ) ;
343+ expect ( rtc . connectionState . peerConnection ) . toBe ( customPC as any ) ;
344+ expect ( closeSpy ) . not . toHaveBeenCalled ( ) ;
345+ expect ( events ) . toEqual ( [ 'connecting' , 'connected' ] ) ;
346+
347+ customPC . _simulateStateChange ( 'failed' ) ;
348+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
349+
350+ expect ( closeSpy ) . toHaveBeenCalled ( ) ;
351+ expect ( rtc . status ) . toBe ( 'disconnected' ) ;
352+ expect ( events ) . toEqual ( [ 'connecting' , 'connected' , 'disconnected' ] ) ;
353+ } ) ;
354+ } ) ;
355+
222356describe ( 'OpenAIRealtimeWebRTC.callId' , ( ) => {
223357 const originals : Record < string , any > = { } ;
224358 const callId = 'rtc_u1_1234567890' ;
@@ -288,6 +422,7 @@ describe('OpenAIRealtimeWebRTC.callId', () => {
288422 await rtc . connect ( { apiKey : 'ek_test' } ) ;
289423 expect ( rtc . callId ) . toBe ( callId ) ;
290424 rtc . close ( ) ;
425+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
291426 expect ( rtc . callId ) . toBeUndefined ( ) ;
292427 } ) ;
293428} ) ;
0 commit comments