Allow the local participant's RTC membership to be absent in tests

This commit is contained in:
Robin
2025-08-15 20:18:21 +02:00
parent f08ae36f9e
commit db59679ad4
5 changed files with 73 additions and 73 deletions

View File

@@ -31,6 +31,7 @@ import {
aliceRtcMember, aliceRtcMember,
bobRtcMember, bobRtcMember,
local, local,
localRtcMember,
} from "../utils/test-fixtures"; } from "../utils/test-fixtures";
vitest.mock("../useAudioContext"); vitest.mock("../useAudioContext");
@@ -66,7 +67,7 @@ beforeEach(() => {
* a noise every time. * a noise every time.
*/ */
test("plays one sound when entering a call", () => { test("plays one sound when entering a call", () => {
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([ const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([
local, local,
alice, alice,
]); ]);
@@ -74,47 +75,47 @@ test("plays one sound when entering a call", () => {
// Joining a call usually means remote participants are added later. // Joining a call usually means remote participants are added later.
act(() => { act(() => {
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); rtcMemberships$.next([localRtcMember, aliceRtcMember, bobRtcMember]);
}); });
expect(playSound).toHaveBeenCalledOnce(); expect(playSound).toHaveBeenCalledOnce();
}); });
test("plays a sound when a user joins", () => { test("plays a sound when a user joins", () => {
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([ const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([
local, local,
alice, alice,
]); ]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
act(() => { act(() => {
remoteRtcMemberships$.next([aliceRtcMember, bobRtcMember]); rtcMemberships$.next([localRtcMember, aliceRtcMember, bobRtcMember]);
}); });
// Play a sound when joining a call. // Play a sound when joining a call.
expect(playSound).toBeCalledWith("join"); expect(playSound).toBeCalledWith("join");
}); });
test("plays a sound when a user leaves", () => { test("plays a sound when a user leaves", () => {
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment([ const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment([
local, local,
alice, alice,
]); ]);
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
act(() => { act(() => {
remoteRtcMemberships$.next([]); rtcMemberships$.next([localRtcMember]);
}); });
expect(playSound).toBeCalledWith("left"); expect(playSound).toBeCalledWith("left");
}); });
test("plays no sound when the participant list is more than the maximum size", () => { 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++) { for (let i = 0; i < MAX_PARTICIPANT_COUNT_FOR_SOUND; i++) {
mockRtcMemberships.push( mockRtcMemberships.push(
mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`), mockRtcMembership(`@user${i}:example.org`, `DEVICE${i}`),
); );
} }
const { vm, remoteRtcMemberships$ } = getBasicCallViewModelEnvironment( const { vm, rtcMemberships$ } = getBasicCallViewModelEnvironment(
[local, alice], [local, alice],
mockRtcMemberships, mockRtcMemberships,
); );
@@ -122,8 +123,8 @@ test("plays no sound when the participant list is more than the maximum size", (
render(<CallEventAudioRenderer vm={vm} />); render(<CallEventAudioRenderer vm={vm} />);
expect(playSound).not.toBeCalled(); expect(playSound).not.toBeCalled();
act(() => { act(() => {
remoteRtcMemberships$.next( rtcMemberships$.next(
mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND - 1), mockRtcMemberships.slice(0, MAX_PARTICIPANT_COUNT_FOR_SOUND),
); );
}); });
expect(playSound).toBeCalledWith("left"); expect(playSound).toBeCalledWith("left");

View File

@@ -137,11 +137,9 @@ function createGroupCallView(
getJoinRule: () => JoinRule.Invite, getJoinRule: () => JoinRule.Invite,
} as Partial<RoomState> as RoomState, } as Partial<RoomState> as RoomState,
}); });
const rtcSession = new MockRTCSession( const rtcSession = new MockRTCSession(room, []).withMemberships(
room, constant([localRtcMember]),
localRtcMember, );
[],
).withMemberships(constant([]));
rtcSession.joined = joined; rtcSession.joined = joined;
const muteState = { const muteState = {
audio: { enabled: false }, audio: { enabled: false },

View File

@@ -248,11 +248,7 @@ function withCallViewModel(
} as Partial<MatrixClient> as MatrixClient, } as Partial<MatrixClient> as MatrixClient,
getMember: (userId) => roomMembers.get(userId) ?? null, getMember: (userId) => roomMembers.get(userId) ?? null,
}); });
const rtcSession = new MockRTCSession( const rtcSession = new MockRTCSession(room, []).withMemberships(rtcMembers$);
room,
localRtcMember,
[],
).withMemberships(rtcMembers$);
const participantsSpy = vi const participantsSpy = vi
.spyOn(ComponentsCore, "connectedParticipantsObserver") .spyOn(ComponentsCore, "connectedParticipantsObserver")
.mockReturnValue(remoteParticipants$); .mockReturnValue(remoteParticipants$);
@@ -322,7 +318,7 @@ test("participants are retained during a focus switch", () => {
a: [aliceParticipant, bobParticipant], a: [aliceParticipant, bobParticipant],
b: [], b: [],
}), }),
constant([aliceRtcMember, bobRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember]),
behavior(connectionInputMarbles, { behavior(connectionInputMarbles, {
c: ConnectionState.Connected, c: ConnectionState.Connected,
s: ECAddonConnectionState.ECSwitchingFocus, s: ECAddonConnectionState.ECSwitchingFocus,
@@ -365,7 +361,7 @@ test("screen sharing activates spotlight layout", () => {
c: [aliceSharingScreen, bobSharingScreen], c: [aliceSharingScreen, bobSharingScreen],
d: [aliceParticipant, bobSharingScreen], d: [aliceParticipant, bobSharingScreen],
}), }),
constant([aliceRtcMember, bobRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
mockMediaDevices({}), mockMediaDevices({}),
@@ -445,7 +441,7 @@ test("participants stay in the same order unless to appear/disappear", () => {
withCallViewModel( withCallViewModel(
constant([aliceParticipant, bobParticipant, daveParticipant]), constant([aliceParticipant, bobParticipant, daveParticipant]),
constant([aliceRtcMember, bobRtcMember, daveRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map([ new Map([
[ [
@@ -512,7 +508,7 @@ test("participants adjust order when space becomes constrained", () => {
withCallViewModel( withCallViewModel(
constant([aliceParticipant, bobParticipant, daveParticipant]), constant([aliceParticipant, bobParticipant, daveParticipant]),
constant([aliceRtcMember, bobRtcMember, daveRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map([ new Map([
[ [
@@ -571,7 +567,7 @@ test("spotlight speakers swap places", () => {
withCallViewModel( withCallViewModel(
constant([aliceParticipant, bobParticipant, daveParticipant]), constant([aliceParticipant, bobParticipant, daveParticipant]),
constant([aliceRtcMember, bobRtcMember, daveRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map([ new Map([
[ [
@@ -630,7 +626,7 @@ test("layout enters picture-in-picture mode when requested", () => {
withCallViewModel( withCallViewModel(
constant([aliceParticipant, bobParticipant]), constant([aliceParticipant, bobParticipant]),
constant([aliceRtcMember, bobRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
mockMediaDevices({}), mockMediaDevices({}),
@@ -672,7 +668,7 @@ test("spotlight remembers whether it's expanded", () => {
withCallViewModel( withCallViewModel(
constant([aliceParticipant, bobParticipant]), constant([aliceParticipant, bobParticipant]),
constant([aliceRtcMember, bobRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
mockMediaDevices({}), mockMediaDevices({}),
@@ -736,11 +732,11 @@ test("participants must have a MatrixRTCSession to be visible", () => {
e: [aliceParticipant, daveParticipant, bobSharingScreen], e: [aliceParticipant, daveParticipant, bobSharingScreen],
}), }),
behavior(scenarioInputMarbles, { behavior(scenarioInputMarbles, {
a: [], a: [localRtcMember],
b: [], b: [localRtcMember],
c: [aliceRtcMember], c: [localRtcMember, aliceRtcMember],
d: [aliceRtcMember, daveRtcMember], d: [localRtcMember, aliceRtcMember, daveRtcMember],
e: [aliceRtcMember, daveRtcMember], e: [localRtcMember, aliceRtcMember, daveRtcMember],
}), }),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
@@ -786,7 +782,7 @@ test("shows participants without MatrixRTCSession when enabled in settings", ()
b: [aliceParticipant], b: [aliceParticipant],
c: [aliceParticipant, bobParticipant], c: [aliceParticipant, bobParticipant],
}), }),
constant([]), // No one joins the MatrixRTC session constant([localRtcMember]), // No one else joins the MatrixRTC session
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
mockMediaDevices({}), mockMediaDevices({}),
@@ -830,10 +826,10 @@ it("should show at least one tile per MatrixRTCSession", () => {
withCallViewModel( withCallViewModel(
constant([]), constant([]),
behavior(scenarioInputMarbles, { behavior(scenarioInputMarbles, {
a: [], a: [localRtcMember],
b: [aliceRtcMember], b: [localRtcMember, aliceRtcMember],
c: [aliceRtcMember, daveRtcMember], c: [localRtcMember, aliceRtcMember, daveRtcMember],
d: [daveRtcMember], d: [localRtcMember, daveRtcMember],
}), }),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
@@ -878,11 +874,16 @@ test("should disambiguate users with the same displayname", () => {
withCallViewModel( withCallViewModel(
constant([]), constant([]),
behavior(scenarioInputMarbles, { behavior(scenarioInputMarbles, {
a: [], a: [localRtcMember],
b: [aliceRtcMember], b: [localRtcMember, aliceRtcMember],
c: [aliceRtcMember, aliceDoppelgangerRtcMember], c: [localRtcMember, aliceRtcMember, aliceDoppelgangerRtcMember],
d: [aliceRtcMember, aliceDoppelgangerRtcMember, bobRtcMember], d: [
e: [aliceDoppelgangerRtcMember, bobRtcMember], localRtcMember,
aliceRtcMember,
aliceDoppelgangerRtcMember,
bobRtcMember,
],
e: [localRtcMember, aliceDoppelgangerRtcMember, bobRtcMember],
}), }),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
@@ -928,8 +929,8 @@ test("should disambiguate users with invisible characters", () => {
withCallViewModel( withCallViewModel(
constant([]), constant([]),
behavior(scenarioInputMarbles, { behavior(scenarioInputMarbles, {
a: [], a: [localRtcMember],
b: [bobRtcMember, bobZeroWidthSpaceRtcMember], b: [localRtcMember, bobRtcMember, bobZeroWidthSpaceRtcMember],
}), }),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
@@ -961,8 +962,8 @@ test("should strip RTL characters from displayname", () => {
withCallViewModel( withCallViewModel(
constant([]), constant([]),
behavior(scenarioInputMarbles, { behavior(scenarioInputMarbles, {
a: [], a: [localRtcMember],
b: [daveRtcMember, daveRTLRtcMember], b: [localRtcMember, daveRtcMember, daveRTLRtcMember],
}), }),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
@@ -992,7 +993,7 @@ it("should rank raised hands above video feeds and below speakers and presenters
withCallViewModel( withCallViewModel(
constant([aliceParticipant, bobParticipant]), constant([aliceParticipant, bobParticipant]),
constant([aliceRtcMember, bobRtcMember]), constant([localRtcMember, aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
mockMediaDevices({}), mockMediaDevices({}),
@@ -1077,10 +1078,10 @@ function rtcMemberJoinLeave$(
) => Observable<CallMembership[]>, ) => Observable<CallMembership[]>,
): Observable<CallMembership[]> { ): Observable<CallMembership[]> {
return hot("a-b-c-d", { return hot("a-b-c-d", {
a: [], // Start empty a: [localRtcMember], // Start empty
b: [aliceRtcMember], // Alice joins b: [localRtcMember, aliceRtcMember], // Alice joins
c: [aliceRtcMember], // Alice still there c: [localRtcMember, aliceRtcMember], // Alice still there
d: [], // Alice leaves 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 // Test scenario 1: No one ever joins - should only emit initial false and never emit again
withCallViewModel( withCallViewModel(
scope.behavior(nooneEverThere$(hot), []), scope.behavior(nooneEverThere$(hot), []),
scope.behavior(nooneEverThere$(hot), []), constant([localRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
mockMediaDevices({}), mockMediaDevices({}),
@@ -1237,7 +1238,7 @@ test("audio output changes when toggling earpiece mode", () => {
withCallViewModel( withCallViewModel(
constant([]), constant([]),
constant([]), constant([localRtcMember]),
of(ConnectionState.Connected), of(ConnectionState.Connected),
new Map(), new Map(),
devices, devices,

View File

@@ -34,11 +34,11 @@ import { type RaisedHandInfo, type ReactionInfo } from "../reactions";
export function getBasicRTCSession( export function getBasicRTCSession(
members: RoomMember[], members: RoomMember[],
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], initialRtcMemberships: CallMembership[] = [localRtcMember, aliceRtcMember],
): { ): {
rtcSession: MockRTCSession; rtcSession: MockRTCSession;
matrixRoom: Room; matrixRoom: Room;
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>; rtcMemberships$: BehaviorSubject<CallMembership[]>;
} { } {
const matrixRoomId = "!myRoomId:example.com"; const matrixRoomId = "!myRoomId:example.com";
const matrixRoomMembers = new Map(members.map((p) => [p.userId, p])); const matrixRoomMembers = new Map(members.map((p) => [p.userId, p]));
@@ -92,41 +92,40 @@ export function getBasicRTCSession(
), ),
}); });
const remoteRtcMemberships$ = new BehaviorSubject<CallMembership[]>( const rtcMemberships$ = new BehaviorSubject<CallMembership[]>(
initialRemoteRtcMemberships, initialRtcMemberships,
); );
const rtcSession = new MockRTCSession( const rtcSession = new MockRTCSession(matrixRoom).withMemberships(
matrixRoom, rtcMemberships$,
localRtcMember, );
).withMemberships(remoteRtcMemberships$);
return { return {
rtcSession, rtcSession,
matrixRoom, matrixRoom,
remoteRtcMemberships$, rtcMemberships$,
}; };
} }
/** /**
* Construct a basic CallViewModel to test components that make use of it. * Construct a basic CallViewModel to test components that make use of it.
* @param members * @param members
* @param initialRemoteRtcMemberships * @param initialRtcMemberships
* @returns * @returns
*/ */
export function getBasicCallViewModelEnvironment( export function getBasicCallViewModelEnvironment(
members: RoomMember[], members: RoomMember[],
initialRemoteRtcMemberships: CallMembership[] = [aliceRtcMember], initialRtcMemberships: CallMembership[] = [localRtcMember, aliceRtcMember],
): { ): {
vm: CallViewModel; vm: CallViewModel;
remoteRtcMemberships$: BehaviorSubject<CallMembership[]>; rtcMemberships$: BehaviorSubject<CallMembership[]>;
rtcSession: MockRTCSession; rtcSession: MockRTCSession;
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>; handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>; reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>;
} { } {
const { rtcSession, matrixRoom, remoteRtcMemberships$ } = getBasicRTCSession( const { rtcSession, matrixRoom, rtcMemberships$ } = getBasicRTCSession(
members, members,
initialRemoteRtcMemberships, initialRtcMemberships,
); );
const handRaisedSubject$ = new BehaviorSubject({}); const handRaisedSubject$ = new BehaviorSubject({});
const reactionsSubject$ = new BehaviorSubject({}); const reactionsSubject$ = new BehaviorSubject({});
@@ -150,7 +149,7 @@ export function getBasicCallViewModelEnvironment(
); );
return { return {
vm, vm,
remoteRtcMemberships$, rtcMemberships$,
rtcSession, rtcSession,
handRaisedSubject$: handRaisedSubject$, handRaisedSubject$: handRaisedSubject$,
reactionsSubject$: reactionsSubject$, reactionsSubject$: reactionsSubject$,

View File

@@ -326,7 +326,6 @@ export class MockRTCSession extends TypedEventEmitter<
public constructor( public constructor(
public readonly room: Room, public readonly room: Room,
private localMembership: CallMembership,
public memberships: CallMembership[] = [], public memberships: CallMembership[] = [],
) { ) {
super(); super();
@@ -342,10 +341,12 @@ export class MockRTCSession extends TypedEventEmitter<
): MockRTCSession { ): MockRTCSession {
rtcMembers$.subscribe((m) => { rtcMembers$.subscribe((m) => {
const old = this.memberships; const old = this.memberships;
// always prepend the local participant this.memberships = m as CallMembership[];
const updated = [this.localMembership, ...(m as CallMembership[])]; this.emit(
this.memberships = updated; MatrixRTCSessionEvent.MembershipsChanged,
this.emit(MatrixRTCSessionEvent.MembershipsChanged, old, updated); old,
this.memberships,
);
}); });
return this; return this;