@@ -29,6 +29,7 @@ import userEvent from "@testing-library/user-event";
2929import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container" ;
3030import { useState } from "react" ;
3131import { TooltipProvider } from "@vector-im/compound-web" ;
32+ import { type ITransport } from "matrix-widget-api" ;
3233
3334import { prefetchSounds } from "../soundUtils" ;
3435import { useAudioContext } from "../useAudioContext" ;
@@ -43,7 +44,7 @@ import {
4344 MockRTCSession ,
4445} from "../utils/test" ;
4546import { GroupCallView } from "./GroupCallView" ;
46- import { type WidgetHelpers } from "../widget" ;
47+ import { ElementWidgetActions , type WidgetHelpers } from "../widget" ;
4748import { LazyEventEmitter } from "../LazyEventEmitter" ;
4849import { MatrixRTCTransportMissingError } from "../utils/errors" ;
4950import { ProcessorProvider } from "../livekit/TrackProcessorContext" ;
@@ -112,6 +113,10 @@ beforeEach(() => {
112113 return (
113114 < div >
114115 < button onClick = { ( ) => onLeave ( "user" ) } > Leave</ button >
116+ < button onClick = { ( ) => onLeave ( "allOthersLeft" ) } >
117+ SimulateOtherLeft
118+ </ button >
119+ < button onClick = { ( ) => onLeave ( "error" ) } > SimulateErrorLeft</ button >
115120 </ div >
116121 ) ;
117122 } ,
@@ -243,6 +248,112 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn
243248 expect ( leaveRTCSession ) . toHaveBeenCalledOnce ( ) ;
244249} ) ;
245250
251+ test . skip ( "Should close widget when all other left and have time to play a sound" , async ( ) => {
252+ const user = userEvent . setup ( ) ;
253+ const widgetClosedCalled = Promise . withResolvers < void > ( ) ;
254+ const widgetSendMock = vi . fn ( ) . mockImplementation ( ( action : string ) => {
255+ if ( action === ElementWidgetActions . Close ) {
256+ widgetClosedCalled . resolve ( ) ;
257+ }
258+ } ) ;
259+ const widgetStopMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
260+ const widget = {
261+ api : {
262+ setAlwaysOnScreen : vi . fn ( ) . mockResolvedValue ( true ) ,
263+ transport : {
264+ send : widgetSendMock ,
265+ reply : vi . fn ( ) . mockResolvedValue ( undefined ) ,
266+ stop : widgetStopMock ,
267+ } as unknown as ITransport ,
268+ } as Partial < WidgetHelpers [ "api" ] > ,
269+ lazyActions : new LazyEventEmitter ( ) ,
270+ } ;
271+ const resolvePlaySound = Promise . withResolvers < void > ( ) ;
272+ playSound = vi . fn ( ) . mockReturnValue ( resolvePlaySound ) ;
273+ ( useAudioContext as MockedFunction < typeof useAudioContext > ) . mockReturnValue ( {
274+ playSound,
275+ playSoundLooping : vitest . fn ( ) ,
276+ soundDuration : { } ,
277+ } ) ;
278+
279+ const { getByText } = createGroupCallView ( widget as WidgetHelpers ) ;
280+ const leaveButton = getByText ( "SimulateOtherLeft" ) ;
281+ await user . click ( leaveButton ) ;
282+ await flushPromises ( ) ;
283+ expect ( widgetSendMock ) . not . toHaveBeenCalled ( ) ;
284+ resolvePlaySound . resolve ( ) ;
285+ await flushPromises ( ) ;
286+
287+ expect ( playSound ) . toHaveBeenCalledWith ( "left" ) ;
288+
289+ await widgetClosedCalled . promise ;
290+ await flushPromises ( ) ;
291+ expect ( widgetStopMock ) . toHaveBeenCalledOnce ( ) ;
292+ } ) ;
293+
294+ test ( "Should close widget when all other left" , async ( ) => {
295+ const user = userEvent . setup ( ) ;
296+ const widgetClosedCalled = Promise . withResolvers < void > ( ) ;
297+ const widgetSendMock = vi . fn ( ) . mockImplementation ( ( action : string ) => {
298+ if ( action === ElementWidgetActions . Close ) {
299+ widgetClosedCalled . resolve ( ) ;
300+ }
301+ } ) ;
302+ const widgetStopMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
303+ const widget = {
304+ api : {
305+ setAlwaysOnScreen : vi . fn ( ) . mockResolvedValue ( true ) ,
306+ transport : {
307+ send : widgetSendMock ,
308+ reply : vi . fn ( ) . mockResolvedValue ( undefined ) ,
309+ stop : widgetStopMock ,
310+ } as unknown as ITransport ,
311+ } as Partial < WidgetHelpers [ "api" ] > ,
312+ lazyActions : new LazyEventEmitter ( ) ,
313+ } ;
314+
315+ const { getByText } = createGroupCallView ( widget as WidgetHelpers ) ;
316+ const leaveButton = getByText ( "SimulateOtherLeft" ) ;
317+ await user . click ( leaveButton ) ;
318+ await flushPromises ( ) ;
319+
320+ await widgetClosedCalled . promise ;
321+ await flushPromises ( ) ;
322+ expect ( widgetStopMock ) . toHaveBeenCalledOnce ( ) ;
323+ } ) ;
324+
325+ test ( "Should not close widget when auto leave due to error" , async ( ) => {
326+ const user = userEvent . setup ( ) ;
327+
328+ const widgetStopMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
329+ const widgetSendMock = vi . fn ( ) . mockResolvedValue ( undefined ) ;
330+ const widget = {
331+ api : {
332+ setAlwaysOnScreen : vi . fn ( ) . mockResolvedValue ( true ) ,
333+ transport : {
334+ send : widgetSendMock ,
335+ reply : vi . fn ( ) . mockResolvedValue ( undefined ) ,
336+ stop : widgetStopMock ,
337+ } as unknown as ITransport ,
338+ } as Partial < WidgetHelpers [ "api" ] > ,
339+ lazyActions : new LazyEventEmitter ( ) ,
340+ } ;
341+
342+ const alwaysOnScreenSpy = vi . spyOn ( widget . api , "setAlwaysOnScreen" ) ;
343+
344+ const { getByText } = createGroupCallView ( widget as WidgetHelpers ) ;
345+ const leaveButton = getByText ( "SimulateErrorLeft" ) ;
346+ await user . click ( leaveButton ) ;
347+ await flushPromises ( ) ;
348+
349+ // When onLeft is called, we first set always on screen to false
350+ await waitFor ( ( ) => expect ( alwaysOnScreenSpy ) . toHaveBeenCalledWith ( false ) ) ;
351+ await flushPromises ( ) ;
352+ // But then we do not close the widget automatically
353+ expect ( widgetStopMock ) . not . toHaveBeenCalledOnce ( ) ;
354+ expect ( widgetSendMock ) . not . toHaveBeenCalledOnce ( ) ;
355+ } ) ;
356+
246357test . skip ( "GroupCallView leaves the session when an error occurs" , async ( ) => {
247358 ( ActiveCall as MockedFunction < typeof ActiveCall > ) . mockImplementation ( ( ) => {
248359 const [ error , setError ] = useState < Error | null > ( null ) ;
0 commit comments