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,
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(<CallEventAudioRenderer vm={vm} />);
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(<CallEventAudioRenderer vm={vm} />);
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(<CallEventAudioRenderer vm={vm} />);
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");

View File

@@ -137,11 +137,9 @@ function createGroupCallView(
getJoinRule: () => JoinRule.Invite,
} as Partial<RoomState> 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 },

View File

@@ -248,11 +248,7 @@ function withCallViewModel(
} as Partial<MatrixClient> 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<CallMembership[]>,
): Observable<CallMembership[]> {
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,

View File

@@ -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<CallMembership[]>;
rtcMemberships$: BehaviorSubject<CallMembership[]>;
} {
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<CallMembership[]>(
initialRemoteRtcMemberships,
const rtcMemberships$ = new BehaviorSubject<CallMembership[]>(
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<CallMembership[]>;
rtcMemberships$: BehaviorSubject<CallMembership[]>;
rtcSession: MockRTCSession;
handRaisedSubject$: BehaviorSubject<Record<string, RaisedHandInfo>>;
reactionsSubject$: BehaviorSubject<Record<string, ReactionInfo>>;
} {
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$,

View File

@@ -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;