diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 1181bde7..a3d3a049 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -29,6 +29,7 @@ import userEvent from "@testing-library/user-event"; import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container"; import { useState } from "react"; import { TooltipProvider } from "@vector-im/compound-web"; +import { type ITransport } from "matrix-widget-api"; import { prefetchSounds } from "../soundUtils"; import { useAudioContext } from "../useAudioContext"; @@ -43,7 +44,7 @@ import { MockRTCSession, } from "../utils/test"; import { GroupCallView } from "./GroupCallView"; -import { type WidgetHelpers } from "../widget"; +import { ElementWidgetActions, type WidgetHelpers } from "../widget"; import { LazyEventEmitter } from "../LazyEventEmitter"; import { MatrixRTCTransportMissingError } from "../utils/errors"; import { ProcessorProvider } from "../livekit/TrackProcessorContext"; @@ -112,6 +113,10 @@ beforeEach(() => { return (
+ +
); }, @@ -243,6 +248,112 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn expect(leaveRTCSession).toHaveBeenCalledOnce(); }); +test.skip("Should close widget when all other left and have time to play a sound", async () => { + const user = userEvent.setup(); + const widgetClosedCalled = Promise.withResolvers(); + const widgetSendMock = vi.fn().mockImplementation((action: string) => { + if (action === ElementWidgetActions.Close) { + widgetClosedCalled.resolve(); + } + }); + const widgetStopMock = vi.fn().mockResolvedValue(undefined); + const widget = { + api: { + setAlwaysOnScreen: vi.fn().mockResolvedValue(true), + transport: { + send: widgetSendMock, + reply: vi.fn().mockResolvedValue(undefined), + stop: widgetStopMock, + } as unknown as ITransport, + } as Partial, + lazyActions: new LazyEventEmitter(), + }; + const resolvePlaySound = Promise.withResolvers(); + playSound = vi.fn().mockReturnValue(resolvePlaySound); + (useAudioContext as MockedFunction).mockReturnValue({ + playSound, + playSoundLooping: vitest.fn(), + soundDuration: {}, + }); + + const { getByText } = createGroupCallView(widget as WidgetHelpers); + const leaveButton = getByText("SimulateOtherLeft"); + await user.click(leaveButton); + await flushPromises(); + expect(widgetSendMock).not.toHaveBeenCalled(); + resolvePlaySound.resolve(); + await flushPromises(); + + expect(playSound).toHaveBeenCalledWith("left"); + + await widgetClosedCalled.promise; + await flushPromises(); + expect(widgetStopMock).toHaveBeenCalledOnce(); +}); + +test("Should close widget when all other left", async () => { + const user = userEvent.setup(); + const widgetClosedCalled = Promise.withResolvers(); + const widgetSendMock = vi.fn().mockImplementation((action: string) => { + if (action === ElementWidgetActions.Close) { + widgetClosedCalled.resolve(); + } + }); + const widgetStopMock = vi.fn().mockResolvedValue(undefined); + const widget = { + api: { + setAlwaysOnScreen: vi.fn().mockResolvedValue(true), + transport: { + send: widgetSendMock, + reply: vi.fn().mockResolvedValue(undefined), + stop: widgetStopMock, + } as unknown as ITransport, + } as Partial, + lazyActions: new LazyEventEmitter(), + }; + + const { getByText } = createGroupCallView(widget as WidgetHelpers); + const leaveButton = getByText("SimulateOtherLeft"); + await user.click(leaveButton); + await flushPromises(); + + await widgetClosedCalled.promise; + await flushPromises(); + expect(widgetStopMock).toHaveBeenCalledOnce(); +}); + +test("Should not close widget when auto leave due to error", async () => { + const user = userEvent.setup(); + + const widgetStopMock = vi.fn().mockResolvedValue(undefined); + const widgetSendMock = vi.fn().mockResolvedValue(undefined); + const widget = { + api: { + setAlwaysOnScreen: vi.fn().mockResolvedValue(true), + transport: { + send: widgetSendMock, + reply: vi.fn().mockResolvedValue(undefined), + stop: widgetStopMock, + } as unknown as ITransport, + } as Partial, + lazyActions: new LazyEventEmitter(), + }; + + const alwaysOnScreenSpy = vi.spyOn(widget.api, "setAlwaysOnScreen"); + + const { getByText } = createGroupCallView(widget as WidgetHelpers); + const leaveButton = getByText("SimulateErrorLeft"); + await user.click(leaveButton); + await flushPromises(); + + // When onLeft is called, we first set always on screen to false + await waitFor(() => expect(alwaysOnScreenSpy).toHaveBeenCalledWith(false)); + await flushPromises(); + // But then we do not close the widget automatically + expect(widgetStopMock).not.toHaveBeenCalledOnce(); + expect(widgetSendMock).not.toHaveBeenCalledOnce(); +}); + test.skip("GroupCallView leaves the session when an error occurs", async () => { (ActiveCall as MockedFunction).mockImplementation(() => { const [error, setError] = useState(null); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 0c035970..076667a9 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -313,7 +313,9 @@ export const GroupCallView: FC = ({ const navigate = useNavigate(); const onLeft = useCallback( - (reason: "timeout" | "user" | "allOthersLeft" | "decline"): void => { + ( + reason: "timeout" | "user" | "allOthersLeft" | "decline" | "error", + ): void => { let playSound: CallEventSounds = "left"; if (reason === "timeout" || reason === "decline") playSound = reason; @@ -366,7 +368,7 @@ export const GroupCallView: FC = ({ } // On a normal user hangup we can shut down and close the widget. But if an // error occurs we should keep the widget open until the user reads it. - if (reason === "user" && !getUrlParams().returnToLobby) { + if (reason != "error" && !getUrlParams().returnToLobby) { try { await widget.api.transport.send(ElementWidgetActions.Close, {}); } catch (e) { @@ -518,8 +520,7 @@ export const GroupCallView: FC = ({ }} onError={ (/**error*/) => { - // TODO this should not be "user". It needs a new case - if (rtcSession.isJoined()) onLeft("user"); + if (rtcSession.isJoined()) onLeft("error"); } } > diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 10b86770..28293d29 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -115,7 +115,9 @@ export interface ActiveCallProps extends Omit { e2eeSystem: EncryptionSystem; // TODO refactor those reasons into an enum - onLeft: (reason: "user" | "timeout" | "decline" | "allOthersLeft") => void; + onLeft: ( + reason: "user" | "timeout" | "decline" | "allOthersLeft" | "error", + ) => void; } export const ActiveCall: FC = (props) => {