diff --git a/src/room/CallEventAudioRenderer.test.tsx b/src/room/CallEventAudioRenderer.test.tsx index 281bbafd..ff9fa6d4 100644 --- a/src/room/CallEventAudioRenderer.test.tsx +++ b/src/room/CallEventAudioRenderer.test.tsx @@ -31,6 +31,7 @@ import { aliceRtcMember, bobRtcMember, local, + localRtcMember, } from "../utils/test-fixtures"; vitest.mock("../useAudioContext"); @@ -66,7 +67,7 @@ beforeEach(() => { * a noise every time. */ test("plays one sound when entering a call", () => { - const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([ + const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([ local, alice, ]); @@ -74,47 +75,47 @@ test("plays one sound when entering a call", () => { // Joining a call usually means remote participants are added later. act(() => { - remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); + rtcMemberships$.next([localRtcMember, aliceRtcMember, bobRtcMember]); }); expect(playSound).toHaveBeenCalledOnce(); }); test("plays a sound when a user joins", () => { - const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([ + const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([ local, alice, ]); render(); act(() => { - remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); + rtcMemberships$.next([localRtcMember, aliceRtcMember, bobRtcMember]); }); // Play a sound when joining a call. expect(playSound).toBeCalledWith("join"); }); test("plays a sound when a user leaves", () => { - const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([ + const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([ local, alice, ]); render(); act(() => { - remoteRtcMemberships$.next([]); + rtcMemberships$.next([localRtcMember]); }); expect(playSound).toBeCalledWith("left"); }); test("plays no sound when the participant list is more than the maximum size", () => { - const mockRtcMemberships: CallMembership[] = []; + const mockRtcMemberships: CallMembership[] = [localRtcMember]; for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) { mockRtcMemberships.push( mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`), ); } - const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment( + const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment( [local, alice], mockRtcMemberships, ); @@ -122,8 +123,8 @@ test("plays no sound when the participant list is more than the maximum size", ( render(); expect(playSound).not.toBeCalled(); act(() => { - remoteRtcMemberships$.next( - mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), + rtcMemberships$.next( + mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND), ); }); expect(playSound).toBeCalledWith("left"); diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 4eb32af0..12dfdf61 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -137,11 +137,9 @@ function createGroupCallView( getJoinRule: () => JoinRule.Invite, } as Partial as RoomState, }); - const rtcSession = new MockRTCSession( - room, - localRtcMember, - [], - ).withMemberships(constant([])); + const rtcSession = new MockRTCSession(room, []).withMemberships( + constant([localRtcMember]), + ); rtcSession.joined = joined; const muteState = { audio: { enabled: false }, diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index c46b37c6..6dacb370 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -248,11 +248,7 @@ function withCallViewModel( } as Partial as MatrixClient, getMember: (userId) => roomMembers.get(userId) ?? null, }); - const rtcSession = new MockRTCSession( - room, - localRtcMember, - [], - ).withMemberships(rtcMembers$); + const rtcSession = new MockRTCSession(room, []).withMemberships(rtcMembers$); const participantsSpy = vi .spyOn(ComponentsCore, "connectedParticipantsObserver") .mockReturnValue(remoteParticipants$); @@ -322,7 +318,7 @@ test("participants are retained during a focus switch", () => { a: [aliceParticipant, bobParticipant], b: [], }), - constant([aliceRtcMember, bobRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember]), behavior(connectionInputMarbles, { c: ConnectionState.Connected, s: ECAddonConnectionState.ECSwitchingFocus, @@ -365,7 +361,7 @@ test("screen sharing activates spotlight layout", () => { c: [aliceSharingScreen, bobSharingScreen], d: [aliceParticipant, bobSharingScreen], }), - constant([aliceRtcMember, bobRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), mockMediaDevices({}), @@ -445,7 +441,7 @@ test("participants stay in the same order unless to appear/disappear", () => { withCallViewModel( constant([aliceParticipant, bobParticipant, daveParticipant]), - constant([aliceRtcMember, bobRtcMember, daveRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]), of(ConnectionState.Connected), new Map([ [ @@ -512,7 +508,7 @@ test("participants adjust order when space becomes constrained", () => { withCallViewModel( constant([aliceParticipant, bobParticipant, daveParticipant]), - constant([aliceRtcMember, bobRtcMember, daveRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]), of(ConnectionState.Connected), new Map([ [ @@ -571,7 +567,7 @@ test("spotlight speakers swap places", () => { withCallViewModel( constant([aliceParticipant, bobParticipant, daveParticipant]), - constant([aliceRtcMember, bobRtcMember, daveRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]), of(ConnectionState.Connected), new Map([ [ @@ -630,7 +626,7 @@ test("layout enters picture-in-picture mode when requested", () => { withCallViewModel( constant([aliceParticipant, bobParticipant]), - constant([aliceRtcMember, bobRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), mockMediaDevices({}), @@ -672,7 +668,7 @@ test("spotlight remembers whether it's expanded", () => { withCallViewModel( constant([aliceParticipant, bobParticipant]), - constant([aliceRtcMember, bobRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), mockMediaDevices({}), @@ -736,11 +732,11 @@ test("participants must have a MatrixRTCSession to be visible", () => { e: [aliceParticipant, daveParticipant, bobSharingScreen], }), behavior(scenarioInputMarbles, { - a: [], - b: [], - c: [aliceRtcMember], - d: [aliceRtcMember, daveRtcMember], - e: [aliceRtcMember, daveRtcMember], + a: [localRtcMember], + b: [localRtcMember], + c: [localRtcMember, aliceRtcMember], + d: [localRtcMember, aliceRtcMember, daveRtcMember], + e: [localRtcMember, aliceRtcMember, daveRtcMember], }), of(ConnectionState.Connected), new Map(), @@ -786,7 +782,7 @@ test("shows participants without MatrixRTCSession when enabled in settings", () b: [aliceParticipant], c: [aliceParticipant, bobParticipant], }), - constant([]), // No one joins the MatrixRTC session + constant([localRtcMember]), // No one else joins the MatrixRTC session of(ConnectionState.Connected), new Map(), mockMediaDevices({}), @@ -830,10 +826,10 @@ it("should show at least one tile per MatrixRTCSession", () => { withCallViewModel( constant([]), behavior(scenarioInputMarbles, { - a: [], - b: [aliceRtcMember], - c: [aliceRtcMember, daveRtcMember], - d: [daveRtcMember], + a: [localRtcMember], + b: [localRtcMember, aliceRtcMember], + c: [localRtcMember, aliceRtcMember, daveRtcMember], + d: [localRtcMember, daveRtcMember], }), of(ConnectionState.Connected), new Map(), @@ -878,11 +874,16 @@ test("should disambiguate users with the same displayname", () => { withCallViewModel( constant([]), behavior(scenarioInputMarbles, { - a: [], - b: [aliceRtcMember], - c: [aliceRtcMember, aliceDoppelgangerRtcMember], - d: [aliceRtcMember, aliceDoppelgangerRtcMember, bobRtcMember], - e: [aliceDoppelgangerRtcMember, bobRtcMember], + a: [localRtcMember], + b: [localRtcMember, aliceRtcMember], + c: [localRtcMember, aliceRtcMember, aliceDoppelgangerRtcMember], + d: [ + localRtcMember, + aliceRtcMember, + aliceDoppelgangerRtcMember, + bobRtcMember, + ], + e: [localRtcMember, aliceDoppelgangerRtcMember, bobRtcMember], }), of(ConnectionState.Connected), new Map(), @@ -928,8 +929,8 @@ test("should disambiguate users with invisible characters", () => { withCallViewModel( constant([]), behavior(scenarioInputMarbles, { - a: [], - b: [bobRtcMember, bobZeroWidthSpaceRtcMember], + a: [localRtcMember], + b: [localRtcMember, bobRtcMember, bobZeroWidthSpaceRtcMember], }), of(ConnectionState.Connected), new Map(), @@ -961,8 +962,8 @@ test("should strip RTL characters from displayname", () => { withCallViewModel( constant([]), behavior(scenarioInputMarbles, { - a: [], - b: [daveRtcMember, daveRTLRtcMember], + a: [localRtcMember], + b: [localRtcMember, daveRtcMember, daveRTLRtcMember], }), of(ConnectionState.Connected), new Map(), @@ -992,7 +993,7 @@ it("should rank raised hands above video feeds and below speakers and presenters withCallViewModel( constant([aliceParticipant, bobParticipant]), - constant([aliceRtcMember, bobRtcMember]), + constant([localRtcMember, aliceRtcMember, bobRtcMember]), of(ConnectionState.Connected), new Map(), mockMediaDevices({}), @@ -1077,10 +1078,10 @@ function rtcMemberJoinLeave$( ) => Observable, ): Observable { return hot("a-b-c-d", { - a: [], // Start empty - b: [aliceRtcMember], // Alice joins - c: [aliceRtcMember], // Alice still there - d: [], // Alice leaves + a: [localRtcMember], // Start empty + b: [localRtcMember, aliceRtcMember], // Alice joins + c: [localRtcMember, aliceRtcMember], // Alice still there + d: [localRtcMember], // Alice leaves }); } @@ -1089,7 +1090,7 @@ test("allOthersLeft$ emits only when someone joined and then all others left", ( // Test scenario 1: No one ever joins - should only emit initial false and never emit again withCallViewModel( scope.behavior(nooneEverThere$(hot), []), - scope.behavior(nooneEverThere$(hot), []), + constant([localRtcMember]), of(ConnectionState.Connected), new Map(), mockMediaDevices({}), @@ -1237,7 +1238,7 @@ test("audio output changes when toggling earpiece mode", () => { withCallViewModel( constant([]), - constant([]), + constant([localRtcMember]), of(ConnectionState.Connected), new Map(), devices, diff --git a/src/utils/test-viewmodel.ts b/src/utils/test-viewmodel.ts index 7978bd96..ce7082c1 100644 --- a/src/utils/test-viewmodel.ts +++ b/src/utils/test-viewmodel.ts @@ -34,11 +34,11 @@ import { type RaisedHandInfo, type ReactionInfo } from "../reactions"; export function getBasicRTCSession( members: RoomMember[], - initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], + initialRtcMemberships: CallMembership[] = [localRtcMember, aliceRtcMember], ): { rtcSession: MockRTCSession; matrixRoom: Room; - remoteRtcMemberships$: BehaviorSubject; + rtcMemberships$: BehaviorSubject; } { const matrixRoomId = "!myRoomId:example.com"; const matrixRoomMembers = new Map(members.map((p) => [p.userId, p])); @@ -92,41 +92,40 @@ export function getBasicRTCSession( ), }); - const remoteRtcMemberships$ = new BehaviorSubject( - initialRemoteRtcMemberships, + const rtcMemberships$ = new BehaviorSubject( + initialRtcMemberships, ); - const rtcSession = new MockRTCSession( - matrixRoom, - localRtcMember, - ).withMemberships(remoteRtcMemberships$); + const rtcSession = new MockRTCSession(matrixRoom).withMemberships( + rtcMemberships$, + ); return { rtcSession, matrixRoom, - remoteRtcMemberships$, + rtcMemberships$, }; } /** * Construct a basic CallViewModel to test components that make use of it. * @param members - * @param initialRemoteRtcMemberships + * @param initialRtcMemberships * @returns */ export function getBasicCallViewModelEnvironment( members: RoomMember[], - initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], + initialRtcMemberships: CallMembership[] = [localRtcMember, aliceRtcMember], ): { vm: CallViewModel; - remoteRtcMemberships$: BehaviorSubject; + rtcMemberships$: BehaviorSubject; rtcSession: MockRTCSession; handRaisedSubject$: BehaviorSubject>; reactionsSubject$: BehaviorSubject>; } { - const { rtcSession, matrixRoom, remoteRtcMemberships$ } = getBasicRTCSession( + const { rtcSession, matrixRoom, rtcMemberships$ } = getBasicRTCSession( members, - initialRemoteRtcMemberships, + initialRtcMemberships, ); const handRaisedSubject$ = new BehaviorSubject({}); const reactionsSubject$ = new BehaviorSubject({}); @@ -150,7 +149,7 @@ export function getBasicCallViewModelEnvironment( ); return { vm, - remoteRtcMemberships$, + rtcMemberships$, rtcSession, handRaisedSubject$: handRaisedSubject$, reactionsSubject$: reactionsSubject$, diff --git a/src/utils/test.ts b/src/utils/test.ts index b49b5931..dc4f1922 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -326,7 +326,6 @@ export class MockRTCSession extends TypedEventEmitter< public constructor( public readonly room: Room, - private localMembership: CallMembership, public memberships: CallMembership[] = [], ) { super(); @@ -342,10 +341,12 @@ export class MockRTCSession extends TypedEventEmitter< ): MockRTCSession { rtcMembers$.subscribe((m) => { const old = this.memberships; - // always prepend the local participant - const updated = [this.localMembership, ...(m as CallMembership[])]; - this.memberships = updated; - this.emit(MatrixRTCSessionEvent.MembershipsChanged, old, updated); + this.memberships = m as CallMembership[]; + this.emit( + MatrixRTCSessionEvent.MembershipsChanged, + old, + this.memberships, + ); }); return this;