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) => {