2024-12-12 07:33:47 +00:00
|
|
|
/*
|
|
|
|
|
Copyright 2024 New Vector Ltd.
|
|
|
|
|
|
2025-02-18 17:59:58 +00:00
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
2024-12-12 07:33:47 +00:00
|
|
|
Please see LICENSE in the repository root for full details.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { beforeEach, expect, type MockedFunction, test, vitest } from "vitest";
|
2025-02-26 17:20:30 +07:00
|
|
|
import { render, waitFor, screen } from "@testing-library/react";
|
2024-12-12 07:33:47 +00:00
|
|
|
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
|
|
|
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
|
|
|
|
import { of } from "rxjs";
|
|
|
|
|
import { JoinRule, type RoomState } from "matrix-js-sdk/src/matrix";
|
2025-01-06 18:00:20 +01:00
|
|
|
import { BrowserRouter } from "react-router-dom";
|
2024-12-12 07:33:47 +00:00
|
|
|
import userEvent from "@testing-library/user-event";
|
2024-12-19 15:54:28 +00:00
|
|
|
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
2025-02-26 17:20:30 +07:00
|
|
|
import { useState } from "react";
|
2024-12-12 07:33:47 +00:00
|
|
|
|
|
|
|
|
import { type MuteStates } from "./MuteStates";
|
|
|
|
|
import { prefetchSounds } from "../soundUtils";
|
|
|
|
|
import { useAudioContext } from "../useAudioContext";
|
|
|
|
|
import { ActiveCall } from "./InCallView";
|
|
|
|
|
import {
|
2025-02-17 19:19:31 +07:00
|
|
|
flushPromises,
|
2024-12-12 07:33:47 +00:00
|
|
|
mockMatrixRoom,
|
|
|
|
|
mockMatrixRoomMember,
|
|
|
|
|
mockRtcMembership,
|
|
|
|
|
MockRTCSession,
|
|
|
|
|
} from "../utils/test";
|
|
|
|
|
import { GroupCallView } from "./GroupCallView";
|
|
|
|
|
import { leaveRTCSession } from "../rtcSessionHelpers";
|
|
|
|
|
import { type WidgetHelpers } from "../widget";
|
|
|
|
|
import { LazyEventEmitter } from "../LazyEventEmitter";
|
|
|
|
|
|
|
|
|
|
vitest.mock("../soundUtils");
|
|
|
|
|
vitest.mock("../useAudioContext");
|
|
|
|
|
vitest.mock("./InCallView");
|
|
|
|
|
|
|
|
|
|
vitest.mock("../rtcSessionHelpers", async (importOriginal) => {
|
|
|
|
|
// TODO: perhaps there is a more elegant way to manage the type import here?
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
|
|
|
const orig = await importOriginal<typeof import("../rtcSessionHelpers")>();
|
|
|
|
|
vitest.spyOn(orig, "leaveRTCSession");
|
|
|
|
|
return orig;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let playSound: MockedFunction<
|
|
|
|
|
NonNullable<ReturnType<typeof useAudioContext>>["playSound"]
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
|
|
|
|
const carol = mockMatrixRoomMember(localRtcMember);
|
|
|
|
|
const roomMembers = new Map([carol].map((p) => [p.userId, p]));
|
|
|
|
|
|
|
|
|
|
const roomId = "!foo:bar";
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2025-02-17 19:19:31 +07:00
|
|
|
vitest.clearAllMocks();
|
2024-12-12 07:33:47 +00:00
|
|
|
(prefetchSounds as MockedFunction<typeof prefetchSounds>).mockResolvedValue({
|
|
|
|
|
sound: new ArrayBuffer(0),
|
|
|
|
|
});
|
2025-02-17 19:19:31 +07:00
|
|
|
playSound = vitest.fn();
|
2024-12-12 07:33:47 +00:00
|
|
|
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
|
|
|
|
playSound,
|
|
|
|
|
});
|
|
|
|
|
// A trivial implementation of Active call to ensure we are testing GroupCallView exclusively here.
|
|
|
|
|
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(
|
|
|
|
|
({ onLeave }) => {
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<button onClick={() => onLeave()}>Leave</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function createGroupCallView(widget: WidgetHelpers | null): {
|
|
|
|
|
rtcSession: MockRTCSession;
|
|
|
|
|
getByText: ReturnType<typeof render>["getByText"];
|
|
|
|
|
} {
|
|
|
|
|
const client = {
|
|
|
|
|
getUser: () => null,
|
|
|
|
|
getUserId: () => localRtcMember.sender,
|
|
|
|
|
getDeviceId: () => localRtcMember.deviceId,
|
|
|
|
|
getRoom: (rId) => (rId === roomId ? room : null),
|
|
|
|
|
} as Partial<MatrixClient> as MatrixClient;
|
|
|
|
|
const room = mockMatrixRoom({
|
2024-12-19 15:54:28 +00:00
|
|
|
relations: {
|
|
|
|
|
getChildEventsForEvent: () =>
|
|
|
|
|
vitest.mocked({
|
|
|
|
|
getRelations: () => [],
|
|
|
|
|
}),
|
|
|
|
|
} as unknown as RelationsContainer,
|
2024-12-12 07:33:47 +00:00
|
|
|
client,
|
|
|
|
|
roomId,
|
|
|
|
|
getMember: (userId) => roomMembers.get(userId) ?? null,
|
|
|
|
|
getMxcAvatarUrl: () => null,
|
|
|
|
|
getCanonicalAlias: () => null,
|
|
|
|
|
currentState: {
|
|
|
|
|
getJoinRule: () => JoinRule.Invite,
|
|
|
|
|
} as Partial<RoomState> as RoomState,
|
|
|
|
|
});
|
|
|
|
|
const rtcSession = new MockRTCSession(
|
|
|
|
|
room,
|
|
|
|
|
localRtcMember,
|
|
|
|
|
[],
|
|
|
|
|
).withMemberships(of([]));
|
|
|
|
|
const muteState = {
|
|
|
|
|
audio: { enabled: false },
|
|
|
|
|
video: { enabled: false },
|
|
|
|
|
} as MuteStates;
|
|
|
|
|
const { getByText } = render(
|
2025-01-06 18:00:20 +01:00
|
|
|
<BrowserRouter>
|
2024-12-12 07:33:47 +00:00
|
|
|
<GroupCallView
|
|
|
|
|
client={client}
|
|
|
|
|
isPasswordlessUser={false}
|
|
|
|
|
confineToRoom={false}
|
|
|
|
|
preload={false}
|
|
|
|
|
skipLobby={false}
|
|
|
|
|
hideHeader={true}
|
|
|
|
|
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
2025-01-17 10:30:28 -05:00
|
|
|
isJoined
|
2024-12-12 07:33:47 +00:00
|
|
|
muteStates={muteState}
|
|
|
|
|
widget={widget}
|
|
|
|
|
/>
|
2025-01-06 18:00:20 +01:00
|
|
|
</BrowserRouter>,
|
2024-12-12 07:33:47 +00:00
|
|
|
);
|
|
|
|
|
return {
|
|
|
|
|
getByText,
|
|
|
|
|
rtcSession,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("will play a leave sound asynchronously in SPA mode", async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const { getByText, rtcSession } = createGroupCallView(null);
|
|
|
|
|
const leaveButton = getByText("Leave");
|
|
|
|
|
await user.click(leaveButton);
|
|
|
|
|
expect(playSound).toHaveBeenCalledWith("left");
|
2025-02-17 19:19:31 +07:00
|
|
|
expect(leaveRTCSession).toHaveBeenCalledWith(
|
|
|
|
|
rtcSession,
|
|
|
|
|
"user",
|
|
|
|
|
expect.any(Promise),
|
|
|
|
|
);
|
2024-12-12 07:33:47 +00:00
|
|
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
2025-02-17 19:19:31 +07:00
|
|
|
// Ensure that the playSound promise resolves within this test to avoid
|
|
|
|
|
// impacting the results of other tests
|
|
|
|
|
await waitFor(() => expect(leaveRTCSession).toHaveResolved());
|
2024-12-12 07:33:47 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("will play a leave sound synchronously in widget mode", async () => {
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const widget = {
|
|
|
|
|
api: {
|
|
|
|
|
setAlwaysOnScreen: async () => Promise.resolve(true),
|
|
|
|
|
} as Partial<WidgetHelpers["api"]>,
|
|
|
|
|
lazyActions: new LazyEventEmitter(),
|
|
|
|
|
};
|
2025-02-17 19:19:31 +07:00
|
|
|
let resolvePlaySound: () => void;
|
|
|
|
|
playSound = vitest
|
|
|
|
|
.fn()
|
|
|
|
|
.mockReturnValue(
|
|
|
|
|
new Promise<void>((resolve) => (resolvePlaySound = resolve)),
|
|
|
|
|
);
|
|
|
|
|
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
|
|
|
|
playSound,
|
|
|
|
|
});
|
|
|
|
|
|
2024-12-12 07:33:47 +00:00
|
|
|
const { getByText, rtcSession } = createGroupCallView(
|
|
|
|
|
widget as WidgetHelpers,
|
|
|
|
|
);
|
|
|
|
|
const leaveButton = getByText("Leave");
|
|
|
|
|
await user.click(leaveButton);
|
2025-02-17 19:19:31 +07:00
|
|
|
await flushPromises();
|
|
|
|
|
expect(leaveRTCSession).not.toHaveResolved();
|
|
|
|
|
resolvePlaySound!();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
2024-12-12 07:33:47 +00:00
|
|
|
expect(playSound).toHaveBeenCalledWith("left");
|
2025-02-17 19:19:31 +07:00
|
|
|
expect(leaveRTCSession).toHaveBeenCalledWith(
|
|
|
|
|
rtcSession,
|
|
|
|
|
"user",
|
|
|
|
|
expect.any(Promise),
|
|
|
|
|
);
|
2024-12-12 07:33:47 +00:00
|
|
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
|
|
|
|
});
|
2025-02-26 17:20:30 +07:00
|
|
|
|
|
|
|
|
test("GroupCallView leaves the session when an error occurs", async () => {
|
|
|
|
|
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(() => {
|
|
|
|
|
const [error, setError] = useState<Error | null>(null);
|
|
|
|
|
if (error !== null) throw error;
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<button onClick={() => setError(new Error())}>Panic!</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
const user = userEvent.setup();
|
|
|
|
|
const { rtcSession } = createGroupCallView(null);
|
|
|
|
|
await user.click(screen.getByRole("button", { name: "Panic!" }));
|
2025-03-03 14:37:29 +01:00
|
|
|
screen.getByText("Something went wrong");
|
2025-02-26 17:20:30 +07:00
|
|
|
expect(leaveRTCSession).toHaveBeenCalledWith(
|
|
|
|
|
rtcSession,
|
|
|
|
|
"error",
|
|
|
|
|
expect.any(Promise),
|
|
|
|
|
);
|
|
|
|
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
|
|
|
|
// Ensure that the playSound promise resolves within this test to avoid
|
|
|
|
|
// impacting the results of other tests
|
|
|
|
|
await waitFor(() => expect(leaveRTCSession).toHaveResolved());
|
|
|
|
|
});
|