Test sync loop status and membership status in reconnection test as well

This commit is contained in:
Robin
2025-08-22 18:12:33 +02:00
parent db65a5308a
commit 7ba4df7781
2 changed files with 79 additions and 30 deletions

View File

@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
*/ */
import { test, vi, onTestFinished, it } from "vitest"; import { test, vi, onTestFinished, it } from "vitest";
import EventEmitter from "events";
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
@@ -17,7 +18,7 @@ import {
of, of,
switchMap, switchMap,
} from "rxjs"; } from "rxjs";
import { SyncState, type MatrixClient } from "matrix-js-sdk"; import { ClientEvent, SyncState, type MatrixClient } from "matrix-js-sdk";
import { import {
ConnectionState, ConnectionState,
type LocalParticipant, type LocalParticipant,
@@ -28,6 +29,7 @@ import {
} from "livekit-client"; } from "livekit-client";
import * as ComponentsCore from "@livekit/components-core"; import * as ComponentsCore from "@livekit/components-core";
import { import {
Status,
type CallMembership, type CallMembership,
type MatrixRTCSession, type MatrixRTCSession,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
@@ -48,7 +50,6 @@ import {
mockRtcMembership, mockRtcMembership,
MockRTCSession, MockRTCSession,
mockMediaDevices, mockMediaDevices,
mockEmitter,
} from "../utils/test"; } from "../utils/test";
import { import {
ECAddonConnectionState, ECAddonConnectionState,
@@ -239,6 +240,7 @@ interface CallViewModelInputs {
connectionState$: Observable<ECConnectionState>; connectionState$: Observable<ECConnectionState>;
speaking: Map<Participant, Observable<boolean>>; speaking: Map<Participant, Observable<boolean>>;
mediaDevices: MediaDevices; mediaDevices: MediaDevices;
initialSyncState: SyncState;
} }
function withCallViewModel( function withCallViewModel(
@@ -248,24 +250,37 @@ function withCallViewModel(
connectionState$ = of(ConnectionState.Connected), connectionState$ = of(ConnectionState.Connected),
speaking = new Map(), speaking = new Map(),
mediaDevices = mockMediaDevices({}), mediaDevices = mockMediaDevices({}),
initialSyncState = SyncState.Syncing,
}: Partial<CallViewModelInputs>, }: Partial<CallViewModelInputs>,
continuation: ( continuation: (
vm: CallViewModel, vm: CallViewModel,
rtcSession: MockRTCSession, rtcSession: MockRTCSession,
subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> }, subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
setSyncState: (value: SyncState) => void,
) => void, ) => void,
options: CallViewModelOptions = { options: CallViewModelOptions = {
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
autoLeaveWhenOthersLeft: false, autoLeaveWhenOthersLeft: false,
}, },
): void { ): void {
let syncState = initialSyncState;
const setSyncState = (value: SyncState): void => {
const prev = syncState;
syncState = value;
room.client.emit(ClientEvent.Sync, value, prev);
};
const room = mockMatrixRoom({ const room = mockMatrixRoom({
client: { client: new (class extends EventEmitter {
...mockEmitter(), public getUserId(): string | undefined {
getUserId: () => localRtcMember.sender, return localRtcMember.sender;
getDeviceId: () => localRtcMember.deviceId, }
getSyncState: () => SyncState.Syncing, public getDeviceId(): string {
} as Partial<MatrixClient> as MatrixClient, return localRtcMember.deviceId;
}
public getSyncState(): SyncState {
return syncState;
}
})() as Partial<MatrixClient> as MatrixClient,
getMember: (userId) => roomMembers.get(userId) ?? null, getMember: (userId) => roomMembers.get(userId) ?? null,
}); });
const rtcSession = new MockRTCSession(room, []).withMemberships(rtcMembers$); const rtcSession = new MockRTCSession(room, []).withMemberships(rtcMembers$);
@@ -321,7 +336,7 @@ function withCallViewModel(
roomEventSelectorSpy!.mockRestore(); roomEventSelectorSpy!.mockRestore();
}); });
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }); continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
} }
test("participants are retained during a focus switch", () => { test("participants are retained during a focus switch", () => {
@@ -1276,25 +1291,49 @@ test("media tracks are paused while reconnecting to MatrixRTC", () => {
localParticipant.trackPublications = originalPublications; localParticipant.trackPublications = originalPublications;
}); });
// TODO: Add marbles for sync state and membership status as well // There are three indicators that the client might be disconnected from
const connectedMarbles = " yny"; // MatrixRTC: whether the sync loop is connected, whether the membership is
const expectedReconnectingMarbles = "nyn"; // present in local room state, and whether the membership manager thinks
const expectedTrackRunningMarbles = "yny"; // we've hit the timeout for the delayed leave event. Let's test all
// combinations of these conditions.
const syncingMarbles = " nyny----n--y";
const membershipStatusMarbles = " y---ny-n-yn-y";
const probablyLeftMarbles = " n-----y-ny---n";
const expectedReconnectingMarbles = "n-ynyny------n";
const expectedTrackRunningMarbles = "nynynyn------y";
withCallViewModel({}, (vm, rtcSession) => { withCallViewModel(
schedule(connectedMarbles, { { initialSyncState: SyncState.Reconnecting },
y: () => { (vm, rtcSession, _subjects, setSyncState) => {
rtcSession.probablyLeft = false; schedule(syncingMarbles, {
}, y: () => setSyncState(SyncState.Syncing),
n: () => { n: () => setSyncState(SyncState.Reconnecting),
rtcSession.probablyLeft = true; });
}, schedule(membershipStatusMarbles, {
}); y: () => {
expectObservable(vm.reconnecting$).toBe( rtcSession.membershipStatus = Status.Connected;
expectedReconnectingMarbles, },
yesNo, n: () => {
); rtcSession.membershipStatus = Status.Reconnecting;
expectObservable(trackRunning$).toBe(expectedTrackRunningMarbles, yesNo); },
}); });
schedule(probablyLeftMarbles, {
y: () => {
rtcSession.probablyLeft = true;
},
n: () => {
rtcSession.probablyLeft = false;
},
});
expectObservable(vm.reconnecting$).toBe(
expectedReconnectingMarbles,
yesNo,
);
expectObservable(trackRunning$).toBe(
expectedTrackRunningMarbles,
yesNo,
);
},
);
}); });
}); });

View File

@@ -360,15 +360,25 @@ export class MockRTCSession extends TypedEventEmitter<
return this; return this;
} }
public readonly membershipStatus = Status.Connected; private _membershipStatus = Status.Connected;
public get membershipStatus(): Status {
return this._membershipStatus;
}
public set membershipStatus(value: Status) {
const prev = this._membershipStatus;
this._membershipStatus = value;
if (value !== prev)
this.emit(MembershipManagerEvent.StatusChanged, prev, value);
}
private _probablyLeft = false; private _probablyLeft = false;
public get probablyLeft(): boolean { public get probablyLeft(): boolean {
return this._probablyLeft; return this._probablyLeft;
} }
public set probablyLeft(value: boolean) { public set probablyLeft(value: boolean) {
const prev = this._probablyLeft;
this._probablyLeft = value; this._probablyLeft = value;
this.emit(MembershipManagerEvent.ProbablyLeft, value); if (value !== prev) this.emit(MembershipManagerEvent.ProbablyLeft, value);
} }
} }