/* Copyright 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ import { render } from "@testing-library/react"; import { afterAll, beforeEach, expect, type MockedFunction, test, vitest, afterEach, } from "vitest"; import { type MatrixClient } from "matrix-js-sdk/src/client"; import { ConnectionState } from "livekit-client"; import { BehaviorSubject, of } from "rxjs"; import { act, type ReactNode } from "react"; import { type CallMembership, type MatrixRTCSession, } from "matrix-js-sdk/src/matrixrtc"; import { type RoomMember } from "matrix-js-sdk/src/matrix"; import { mockLivekitRoom, mockLocalParticipant, mockMatrixRoom, mockMatrixRoomMember, mockRemoteParticipant, mockRtcMembership, MockRTCSession, } from "../utils/test"; import { E2eeType } from "../e2ee/e2eeType"; import { CallViewModel } from "../state/CallViewModel"; import { CallEventAudioRenderer, MAX_PARTICIPANT_COUNT_FOR_SOUND, } from "./CallEventAudioRenderer"; import { useAudioContext } from "../useAudioContext"; import { TestReactionsWrapper } from "../utils/testReactions"; import { prefetchSounds } from "../soundUtils"; const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); const local = mockMatrixRoomMember(localRtcMember); const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA"); const alice = mockMatrixRoomMember(aliceRtcMember); const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB"); const localParticipant = mockLocalParticipant({ identity: "" }); const aliceId = `${alice.userId}:${aliceRtcMember.deviceId}`; const aliceParticipant = mockRemoteParticipant({ identity: aliceId }); vitest.mock("../useAudioContext"); vitest.mock("../soundUtils"); afterEach(() => { vitest.resetAllMocks(); }); afterAll(() => { vitest.restoreAllMocks(); }); let playSound: MockedFunction< NonNullable>["playSound"] >; beforeEach(() => { (prefetchSounds as MockedFunction).mockResolvedValue({ sound: new ArrayBuffer(0), }); playSound = vitest.fn(); (useAudioContext as MockedFunction).mockReturnValue({ playSound, }); }); function TestComponent({ rtcSession, vm, }: { rtcSession: MockRTCSession; vm: CallViewModel; }): ReactNode { return ( ); } function getMockEnv( members: RoomMember[], initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], ): { vm: CallViewModel; session: MockRTCSession; remoteRtcMemberships$: BehaviorSubject; } { const matrixRoomMembers = new Map(members.map((p) => [p.userId, p])); const remoteParticipants$ = of([aliceParticipant]); const liveKitRoom = mockLivekitRoom( { localParticipant }, { remoteParticipants$ }, ); const matrixRoom = mockMatrixRoom({ client: { getUserId: () => localRtcMember.sender, getDeviceId: () => localRtcMember.deviceId, on: vitest.fn(), off: vitest.fn(), } as Partial as MatrixClient, getMember: (userId) => matrixRoomMembers.get(userId) ?? null, }); const remoteRtcMemberships$ = new BehaviorSubject( initialRemoteRtcMemberships, ); const session = new MockRTCSession( matrixRoom, localRtcMember, ).withMemberships(remoteRtcMemberships$); const vm = new CallViewModel( session as unknown as MatrixRTCSession, liveKitRoom, { kind: E2eeType.PER_PARTICIPANT, }, of(ConnectionState.Connected), ); return { vm, session, remoteRtcMemberships$ }; } /** * We don't want to play a sound when loading the call state * because typically this occurs in two stages. We first join * the call as a local participant and *then* the remote * participants join from our perspective. We don't want to make * a noise every time. */ test("plays one sound when entering a call", () => { const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]); render(); // Joining a call usually means remote participants are added later. act(() => { remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); }); expect(playSound).toHaveBeenCalledOnce(); }); // TODO: Same test? test("plays a sound when a user joins", () => { const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]); render(); act(() => { remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); }); // Play a sound when joining a call. expect(playSound).toBeCalledWith("join"); }); test("plays a sound when a user leaves", () => { const { session, vm, remoteRtcMemberships$ } = getMockEnv([local, alice]); render(); act(() => { remoteRtcMemberships$.next([]); }); expect(playSound).toBeCalledWith("left"); }); test("plays no sound when the participant list is more than the maximum size", () => { const mockRtcMemberships: CallMembership[] = []; for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) { mockRtcMemberships.push( mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`), ); } const { session, vm, remoteRtcMemberships$ } = getMockEnv( [local, alice], mockRtcMemberships, ); render(); expect(playSound).not.toBeCalled(); act(() => { remoteRtcMemberships$.next( mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), ); }); expect(playSound).toBeCalledWith("left"); });