From 3aaaabf84af730fbf673349e9dc8fdc45607bfdb Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 2 Dec 2024 12:08:52 +0000 Subject: [PATCH] Comitting test files I am going to be going to lunch so will tidy up in a little while. --- src/room/CallEventAudioRenderer.test.tsx | 165 +++++++++++++++++++++++ src/room/CallEventAudioRenderer.tsx | 13 +- src/room/ReactionAudioRenderer.test.tsx | 19 +-- src/utils/test.ts | 32 +++++ 4 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 src/room/CallEventAudioRenderer.test.tsx diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx new file mode 100644 index 00000000..8e9209e8 --- /dev/null +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -0,0 +1,165 @@ +/* +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, test } from "vitest"; + + +import { + playReactionsSound, + soundEffectVolumeSetting, +} from "../settings/settings"; +import { MockLivekitRoom, mockLivekitRoom, mockLocalParticipant, mockMatrixRoom, mockMatrixRoomMember, mockMediaPlay, mockRemoteParticipant } from "../utils/test"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { E2eeType } from "../e2ee/e2eeType"; +import { CallViewModel } from "../state/CallViewModel"; +import { ConnectionState, Room } from "livekit-client"; +import { of } from "rxjs"; +import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; + +import enterCallSoundOgg from "../sound/join_call.ogg"; +import leftCallSoundOgg from "../sound/left_call.ogg"; +import { afterEach } from "node:test"; +import { act } from "react"; +import { LazyEventEmitter } from "../LazyEventEmitter"; +import EventEmitter from "events"; + +const alice = mockMatrixRoomMember({ userId: "@alice:example.org" }); +const bob = mockMatrixRoomMember({ userId: "@bob:example.org" }); +const aliceId = `${alice.userId}:AAAA`; +const bobId = `${bob.userId}:BBBB`; +const localParticipant = mockLocalParticipant({ identity: "" }); +const aliceParticipant = mockRemoteParticipant({ identity: aliceId }); +const bobParticipant = mockRemoteParticipant({ identity: bobId }); + +const originalPlayFn = window.HTMLMediaElement.prototype.play; + +beforeEach(() => { + soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue); +}); + +afterEach(() => { + window.HTMLMediaElement.prototype.play = originalPlayFn; +}); + +test("plays a sound when entering a call", () => { + const audioIsPlaying: string[] = mockMediaPlay(); + const members = new Map([alice, bob].map((p) => [p.userId, p])); + const remoteParticipants = of([aliceParticipant, bobParticipant]); + const liveKitRoom = mockLivekitRoom( + { localParticipant }, + { remoteParticipants }, + ); + + const vm = new CallViewModel( + mockMatrixRoom({ + client: { + getUserId: () => "@carol:example.org", + } as Partial as MatrixClient, + getMember: (userId) => members.get(userId) ?? null, + }), + liveKitRoom, + { + kind: E2eeType.PER_PARTICIPANT, + }, + of(ConnectionState.Connected), + ); + + render(); + // Play a sound when joining a call. + expect(audioIsPlaying.includes(enterCallSoundOgg)); +}); + +test("plays no sound when muted", () => { + soundEffectVolumeSetting.setValue(0); + const audioIsPlaying: string[] = mockMediaPlay(); + const members = new Map([alice, bob].map((p) => [p.userId, p])); + const remoteParticipants = of([aliceParticipant, bobParticipant]); + const liveKitRoom = mockLivekitRoom( + { localParticipant }, + { remoteParticipants }, + ); + + const vm = new CallViewModel( + mockMatrixRoom({ + client: { + getUserId: () => "@carol:example.org", + } as Partial as MatrixClient, + getMember: (userId) => members.get(userId) ?? null, + }), + liveKitRoom, + { + kind: E2eeType.PER_PARTICIPANT, + }, + of(ConnectionState.Connected), + ); + + render(); + // Play a sound when joining a call. + expect(audioIsPlaying).toHaveLength(0); +}); + +test("plays a sound when a user joins", () => { + const audioIsPlaying: string[] = mockMediaPlay(); + const members = new Map([alice].map((p) => [p.userId, p])); + const remoteParticipants = new Map([aliceParticipant].map((p) => [p.identity, p])); + const liveKitRoom = new MockLivekitRoom({localParticipant, remoteParticipants}); + + const vm = new CallViewModel( + mockMatrixRoom({ + client: { + getUserId: () => "@carol:example.org", + } as Partial as MatrixClient, + getMember: (userId) => members.get(userId) ?? null, + }), + liveKitRoom as unknown as Room, + { + kind: E2eeType.PER_PARTICIPANT, + }, + of(ConnectionState.Connected), + ); + render(); + + act(() => { + liveKitRoom.addParticipant(bobParticipant); + }); + // Play a sound when joining a call. + expect(audioIsPlaying).toHaveLength(2); +}); + + + +test("plays a sound when a user leaves", () => { + const audioIsPlaying: string[] = mockMediaPlay(); + const members = new Map([alice].map((p) => [p.userId, p])); + const remoteParticipants = new Map([aliceParticipant].map((p) => [p.identity, p])); + const liveKitRoom = new MockLivekitRoom({localParticipant, remoteParticipants}); + + const vm = new CallViewModel( + mockMatrixRoom({ + client: { + getUserId: () => "@carol:example.org", + } as Partial as MatrixClient, + getMember: (userId) => members.get(userId) ?? null, + }), + liveKitRoom as unknown as Room, + { + kind: E2eeType.PER_PARTICIPANT, + }, + of(ConnectionState.Connected), + ); + render(); + + act(() => { + liveKitRoom.removeParticipant(aliceParticipant); + }); + // Play a join sound and a leave sound. + expect(audioIsPlaying).toEqual([ + 'http://localhost:3000/src/sound/join_call.ogg', + 'http://localhost:3000/src/sound/left_call.ogg' + ]); +}); diff --git a/src/room/CallEventAudioRenderer.tsx b/src/room/CallEventAudioRenderer.tsx index 176ca109..f08e63c0 100644 --- a/src/room/CallEventAudioRenderer.tsx +++ b/src/room/CallEventAudioRenderer.tsx @@ -28,16 +28,14 @@ export function CallEventAudioRenderer({ }: { vm: CallViewModel; }): ReactNode { - const [shouldPlay] = useSetting(playReactionsSound); const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); const callEntered = useRef(null); const callLeft = useRef(null); useEffect(() => { - if (!shouldPlay) { + if (effectSoundVolume === 0) { return; - } - + } const joinSub = vm.memberChanges .pipe( filter( @@ -45,7 +43,8 @@ export function CallEventAudioRenderer({ ids.length <= MAX_PARTICIPANT_COUNT_FOR_SOUND && joined.length > 0, ), ) - .subscribe(() => { + .subscribe(({ joined }) => { + console.log("Join", joined); if (callEntered.current?.paused) { void callEntered.current.play(); } @@ -68,7 +67,7 @@ export function CallEventAudioRenderer({ joinSub.unsubscribe(); leftSub.unsubscribe(); }; - }, [shouldPlay, callEntered, callLeft, vm]); + }, [effectSoundVolume, callEntered, callLeft, vm]); // Set volume. useEffect(() => { @@ -83,7 +82,7 @@ export function CallEventAudioRenderer({ // Do not render any audio elements if playback is disabled. Will save // audio file fetches. - if (!shouldPlay) { + if (effectSoundVolume === 0) { return null; } diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index cf9d7fad..80ae2b38 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -21,6 +21,7 @@ import { playReactionsSound, soundEffectVolumeSetting, } from "../settings/settings"; +import { mockMediaPlay } from "../utils/test"; const memberUserIdAlice = "@alice:example.org"; const memberUserIdBob = "@bob:example.org"; @@ -80,11 +81,7 @@ test("loads no audio elements when disabled in settings", () => { }); test("will play an audio sound when there is a reaction", () => { - const audioIsPlaying: string[] = []; - window.HTMLMediaElement.prototype.play = async function (): Promise { - audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); - return Promise.resolve(); - }; + const audioIsPlaying: string[] = mockMediaPlay(); playReactionsSound.setValue(true); const room = new MockRoom(memberUserIdAlice); const rtcSession = new MockRTCSession(room, membership); @@ -105,11 +102,7 @@ test("will play an audio sound when there is a reaction", () => { }); test("will play the generic audio sound when there is soundless reaction", () => { - const audioIsPlaying: string[] = []; - window.HTMLMediaElement.prototype.play = async function (): Promise { - audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); - return Promise.resolve(); - }; + const audioIsPlaying: string[] = mockMediaPlay(); playReactionsSound.setValue(true); const room = new MockRoom(memberUserIdAlice); const rtcSession = new MockRTCSession(room, membership); @@ -152,11 +145,7 @@ test("will play an audio sound with the correct volume", () => { }); test("will play multiple audio sounds when there are multiple different reactions", () => { - const audioIsPlaying: string[] = []; - window.HTMLMediaElement.prototype.play = async function (): Promise { - audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); - return Promise.resolve(); - }; + const audioIsPlaying: string[] = mockMediaPlay(); playReactionsSound.setValue(true); const room = new MockRoom(memberUserIdAlice); diff --git a/src/utils/test.ts b/src/utils/test.ts index f583f797..4ffcc3ba 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -14,6 +14,7 @@ import { RemoteParticipant, RemoteTrackPublication, Room as LivekitRoom, + RoomEvent, } from "livekit-client"; import { @@ -23,6 +24,7 @@ import { import { E2eeType } from "../e2ee/e2eeType"; import { DEFAULT_CONFIG, ResolvedConfigOptions } from "../config/ConfigOptions"; import { Config } from "../config/Config"; +import { EventEmitter } from "stream"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -109,6 +111,27 @@ export function mockMatrixRoom(room: Partial): MatrixRoom { return { ...mockEmitter(), ...room } as Partial as MatrixRoom; } +export class MockLivekitRoom extends EventEmitter { + public localParticipant?: LocalParticipant; + public remoteParticipants: Map; + + constructor(room: {localParticipant?: LocalParticipant, remoteParticipants: Map}) { + super(); + this.localParticipant = room.localParticipant; + this.remoteParticipants = room.remoteParticipants ?? new Map(); + } + + public addParticipant(remoteParticipant: RemoteParticipant) { + this.remoteParticipants.set(remoteParticipant.identity, remoteParticipant); + this.emit(RoomEvent.ParticipantConnected, remoteParticipant); + } + + public removeParticipant(remoteParticipant: RemoteParticipant) { + this.remoteParticipants.delete(remoteParticipant.identity); + this.emit(RoomEvent.ParticipantDisconnected, remoteParticipant); + } +} + export function mockLivekitRoom( room: Partial, { @@ -206,3 +229,12 @@ export function mockConfig(config: Partial = {}): void { ...config, }); } + +export function mockMediaPlay() { + const audioIsPlaying: string[] = []; + window.HTMLMediaElement.prototype.play = async function (): Promise { + audioIsPlaying.push((this.children[0] as HTMLSourceElement).src); + return Promise.resolve(); + }; + return audioIsPlaying; +} \ No newline at end of file