Merge pull request #3464 from robintown/reconnecting-test
Test sync loop status and membership status in reconnection test as well
This commit is contained in:
@@ -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,
|
||||||
@@ -233,29 +234,53 @@ function summarizeLayout$(l$: Observable<Layout>): Observable<LayoutSummary> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CallViewModelInputs {
|
||||||
|
remoteParticipants$: Behavior<RemoteParticipant[]>;
|
||||||
|
rtcMembers$: Behavior<Partial<CallMembership>[]>;
|
||||||
|
connectionState$: Observable<ECConnectionState>;
|
||||||
|
speaking: Map<Participant, Observable<boolean>>;
|
||||||
|
mediaDevices: MediaDevices;
|
||||||
|
initialSyncState: SyncState;
|
||||||
|
}
|
||||||
|
|
||||||
function withCallViewModel(
|
function withCallViewModel(
|
||||||
remoteParticipants$: Behavior<RemoteParticipant[]>,
|
{
|
||||||
rtcMembers$: Behavior<Partial<CallMembership>[]>,
|
remoteParticipants$ = constant([]),
|
||||||
connectionState$: Observable<ECConnectionState>,
|
rtcMembers$ = constant([localRtcMember]),
|
||||||
speaking: Map<Participant, Observable<boolean>>,
|
connectionState$ = of(ConnectionState.Connected),
|
||||||
mediaDevices: MediaDevices,
|
speaking = new Map(),
|
||||||
|
mediaDevices = mockMediaDevices({}),
|
||||||
|
initialSyncState = SyncState.Syncing,
|
||||||
|
}: 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$);
|
||||||
@@ -311,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", () => {
|
||||||
@@ -324,17 +349,17 @@ test("participants are retained during a focus switch", () => {
|
|||||||
const expectedLayoutMarbles = " a";
|
const expectedLayoutMarbles = " a";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
behavior(participantInputMarbles, {
|
{
|
||||||
a: [aliceParticipant, bobParticipant],
|
remoteParticipants$: behavior(participantInputMarbles, {
|
||||||
b: [],
|
a: [aliceParticipant, bobParticipant],
|
||||||
}),
|
b: [],
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
}),
|
||||||
behavior(connectionInputMarbles, {
|
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||||
c: ConnectionState.Connected,
|
connectionState$: behavior(connectionInputMarbles, {
|
||||||
s: ECAddonConnectionState.ECSwitchingFocus,
|
c: ConnectionState.Connected,
|
||||||
}),
|
s: ECAddonConnectionState.ECSwitchingFocus,
|
||||||
new Map(),
|
}),
|
||||||
mockMediaDevices({}),
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
expectedLayoutMarbles,
|
expectedLayoutMarbles,
|
||||||
@@ -365,16 +390,15 @@ test("screen sharing activates spotlight layout", () => {
|
|||||||
const expectedLayoutMarbles = " abcdaefeg";
|
const expectedLayoutMarbles = " abcdaefeg";
|
||||||
const expectedShowSpeakingMarbles = "y----nyny";
|
const expectedShowSpeakingMarbles = "y----nyny";
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
behavior(participantInputMarbles, {
|
{
|
||||||
a: [aliceParticipant, bobParticipant],
|
remoteParticipants$: behavior(participantInputMarbles, {
|
||||||
b: [aliceSharingScreen, bobParticipant],
|
a: [aliceParticipant, bobParticipant],
|
||||||
c: [aliceSharingScreen, bobSharingScreen],
|
b: [aliceSharingScreen, bobParticipant],
|
||||||
d: [aliceParticipant, bobSharingScreen],
|
c: [aliceSharingScreen, bobSharingScreen],
|
||||||
}),
|
d: [aliceParticipant, bobSharingScreen],
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
}),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(modeInputMarbles, {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.setGridMode("spotlight"),
|
||||||
@@ -447,15 +471,24 @@ test("participants stay in the same order unless to appear/disappear", () => {
|
|||||||
const expectedLayoutMarbles = " a 1999ms b 1999ms a 57999ms c 1999ms a";
|
const expectedLayoutMarbles = " a 1999ms b 1999ms a 57999ms c 1999ms a";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([aliceParticipant, bobParticipant, daveParticipant]),
|
{
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]),
|
remoteParticipants$: constant([
|
||||||
of(ConnectionState.Connected),
|
aliceParticipant,
|
||||||
new Map([
|
bobParticipant,
|
||||||
[aliceParticipant, behavior(aSpeakingInputMarbles, yesNo)],
|
daveParticipant,
|
||||||
[bobParticipant, behavior(bSpeakingInputMarbles, yesNo)],
|
]),
|
||||||
[daveParticipant, behavior(dSpeakingInputMarbles, yesNo)],
|
rtcMembers$: constant([
|
||||||
]),
|
localRtcMember,
|
||||||
mockMediaDevices({}),
|
aliceRtcMember,
|
||||||
|
bobRtcMember,
|
||||||
|
daveRtcMember,
|
||||||
|
]),
|
||||||
|
speaking: new Map([
|
||||||
|
[aliceParticipant, behavior(aSpeakingInputMarbles, yesNo)],
|
||||||
|
[bobParticipant, behavior(bSpeakingInputMarbles, yesNo)],
|
||||||
|
[daveParticipant, behavior(dSpeakingInputMarbles, yesNo)],
|
||||||
|
]),
|
||||||
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(visibilityInputMarbles, {
|
schedule(visibilityInputMarbles, {
|
||||||
a: () => {
|
a: () => {
|
||||||
@@ -505,14 +538,23 @@ test("participants adjust order when space becomes constrained", () => {
|
|||||||
const expectedLayoutMarbles = " a-b";
|
const expectedLayoutMarbles = " a-b";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([aliceParticipant, bobParticipant, daveParticipant]),
|
{
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]),
|
remoteParticipants$: constant([
|
||||||
of(ConnectionState.Connected),
|
aliceParticipant,
|
||||||
new Map([
|
bobParticipant,
|
||||||
[bobParticipant, behavior(bSpeakingInputMarbles, yesNo)],
|
daveParticipant,
|
||||||
[daveParticipant, behavior(dSpeakingInputMarbles, yesNo)],
|
]),
|
||||||
]),
|
rtcMembers$: constant([
|
||||||
mockMediaDevices({}),
|
localRtcMember,
|
||||||
|
aliceRtcMember,
|
||||||
|
bobRtcMember,
|
||||||
|
daveRtcMember,
|
||||||
|
]),
|
||||||
|
speaking: new Map([
|
||||||
|
[bobParticipant, behavior(bSpeakingInputMarbles, yesNo)],
|
||||||
|
[daveParticipant, behavior(dSpeakingInputMarbles, yesNo)],
|
||||||
|
]),
|
||||||
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
let setVisibleTiles: ((value: number) => void) | null = null;
|
let setVisibleTiles: ((value: number) => void) | null = null;
|
||||||
vm.layout$.subscribe((layout) => {
|
vm.layout$.subscribe((layout) => {
|
||||||
@@ -558,15 +600,24 @@ test("spotlight speakers swap places", () => {
|
|||||||
const expectedLayoutMarbles = "abcd";
|
const expectedLayoutMarbles = "abcd";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([aliceParticipant, bobParticipant, daveParticipant]),
|
{
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember, daveRtcMember]),
|
remoteParticipants$: constant([
|
||||||
of(ConnectionState.Connected),
|
aliceParticipant,
|
||||||
new Map([
|
bobParticipant,
|
||||||
[aliceParticipant, behavior(aSpeakingInputMarbles, yesNo)],
|
daveParticipant,
|
||||||
[bobParticipant, behavior(bSpeakingInputMarbles, yesNo)],
|
]),
|
||||||
[daveParticipant, behavior(dSpeakingInputMarbles, yesNo)],
|
rtcMembers$: constant([
|
||||||
]),
|
localRtcMember,
|
||||||
mockMediaDevices({}),
|
aliceRtcMember,
|
||||||
|
bobRtcMember,
|
||||||
|
daveRtcMember,
|
||||||
|
]),
|
||||||
|
speaking: new Map([
|
||||||
|
[aliceParticipant, behavior(aSpeakingInputMarbles, yesNo)],
|
||||||
|
[bobParticipant, behavior(bSpeakingInputMarbles, yesNo)],
|
||||||
|
[daveParticipant, behavior(dSpeakingInputMarbles, yesNo)],
|
||||||
|
]),
|
||||||
|
},
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
|
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
|
||||||
|
|
||||||
@@ -608,11 +659,10 @@ test("layout enters picture-in-picture mode when requested", () => {
|
|||||||
const expectedLayoutMarbles = " aba";
|
const expectedLayoutMarbles = " aba";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([aliceParticipant, bobParticipant]),
|
{
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
remoteParticipants$: constant([aliceParticipant, bobParticipant]),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(pipControlInputMarbles, {
|
schedule(pipControlInputMarbles, {
|
||||||
e: () => window.controls.enablePip(),
|
e: () => window.controls.enablePip(),
|
||||||
@@ -650,11 +700,10 @@ test("spotlight remembers whether it's expanded", () => {
|
|||||||
const expectedLayoutMarbles = "abcbada";
|
const expectedLayoutMarbles = "abcbada";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([aliceParticipant, bobParticipant]),
|
{
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
remoteParticipants$: constant([aliceParticipant, bobParticipant]),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
schedule(modeInputMarbles, {
|
schedule(modeInputMarbles, {
|
||||||
s: () => vm.setGridMode("spotlight"),
|
s: () => vm.setGridMode("spotlight"),
|
||||||
@@ -707,23 +756,22 @@ test("participants must have a MatrixRTCSession to be visible", () => {
|
|||||||
const expectedLayoutMarbles = "a-bc-b";
|
const expectedLayoutMarbles = "a-bc-b";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
behavior(scenarioInputMarbles, {
|
{
|
||||||
a: [],
|
remoteParticipants$: behavior(scenarioInputMarbles, {
|
||||||
b: [bobParticipant],
|
a: [],
|
||||||
c: [aliceParticipant, bobParticipant],
|
b: [bobParticipant],
|
||||||
d: [aliceParticipant, daveParticipant, bobParticipant],
|
c: [aliceParticipant, bobParticipant],
|
||||||
e: [aliceParticipant, daveParticipant, bobSharingScreen],
|
d: [aliceParticipant, daveParticipant, bobParticipant],
|
||||||
}),
|
e: [aliceParticipant, daveParticipant, bobSharingScreen],
|
||||||
behavior(scenarioInputMarbles, {
|
}),
|
||||||
a: [localRtcMember],
|
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||||
b: [localRtcMember],
|
a: [localRtcMember],
|
||||||
c: [localRtcMember, aliceRtcMember],
|
b: [localRtcMember],
|
||||||
d: [localRtcMember, aliceRtcMember, daveRtcMember],
|
c: [localRtcMember, aliceRtcMember],
|
||||||
e: [localRtcMember, aliceRtcMember, daveRtcMember],
|
d: [localRtcMember, aliceRtcMember, daveRtcMember],
|
||||||
}),
|
e: [localRtcMember, aliceRtcMember, daveRtcMember],
|
||||||
of(ConnectionState.Connected),
|
}),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.setGridMode("grid");
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
@@ -760,15 +808,14 @@ test("shows participants without MatrixRTCSession when enabled in settings", ()
|
|||||||
const expectedLayoutMarbles = "abc";
|
const expectedLayoutMarbles = "abc";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
behavior(scenarioInputMarbles, {
|
{
|
||||||
a: [],
|
remoteParticipants$: behavior(scenarioInputMarbles, {
|
||||||
b: [aliceParticipant],
|
a: [],
|
||||||
c: [aliceParticipant, bobParticipant],
|
b: [aliceParticipant],
|
||||||
}),
|
c: [aliceParticipant, bobParticipant],
|
||||||
constant([localRtcMember]), // No one else joins the MatrixRTC session
|
}),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: constant([localRtcMember]), // No one else joins the MatrixRTC session
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.setGridMode("grid");
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
@@ -807,16 +854,14 @@ it("should show at least one tile per MatrixRTCSession", () => {
|
|||||||
const expectedLayoutMarbles = "abcd";
|
const expectedLayoutMarbles = "abcd";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([]),
|
{
|
||||||
behavior(scenarioInputMarbles, {
|
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||||
a: [localRtcMember],
|
a: [localRtcMember],
|
||||||
b: [localRtcMember, aliceRtcMember],
|
b: [localRtcMember, aliceRtcMember],
|
||||||
c: [localRtcMember, aliceRtcMember, daveRtcMember],
|
c: [localRtcMember, aliceRtcMember, daveRtcMember],
|
||||||
d: [localRtcMember, daveRtcMember],
|
d: [localRtcMember, daveRtcMember],
|
||||||
}),
|
}),
|
||||||
of(ConnectionState.Connected),
|
},
|
||||||
new Map(),
|
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
vm.setGridMode("grid");
|
vm.setGridMode("grid");
|
||||||
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
expectObservable(summarizeLayout$(vm.layout$)).toBe(
|
||||||
@@ -855,22 +900,20 @@ test("should disambiguate users with the same displayname", () => {
|
|||||||
const expectedLayoutMarbles = "abcde";
|
const expectedLayoutMarbles = "abcde";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([]),
|
{
|
||||||
behavior(scenarioInputMarbles, {
|
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||||
a: [localRtcMember],
|
a: [localRtcMember],
|
||||||
b: [localRtcMember, aliceRtcMember],
|
b: [localRtcMember, aliceRtcMember],
|
||||||
c: [localRtcMember, aliceRtcMember, aliceDoppelgangerRtcMember],
|
c: [localRtcMember, aliceRtcMember, aliceDoppelgangerRtcMember],
|
||||||
d: [
|
d: [
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
aliceRtcMember,
|
aliceRtcMember,
|
||||||
aliceDoppelgangerRtcMember,
|
aliceDoppelgangerRtcMember,
|
||||||
bobRtcMember,
|
bobRtcMember,
|
||||||
],
|
],
|
||||||
e: [localRtcMember, aliceDoppelgangerRtcMember, bobRtcMember],
|
e: [localRtcMember, aliceDoppelgangerRtcMember, bobRtcMember],
|
||||||
}),
|
}),
|
||||||
of(ConnectionState.Connected),
|
},
|
||||||
new Map(),
|
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
||||||
// Carol has no displayname - So userId is used.
|
// Carol has no displayname - So userId is used.
|
||||||
@@ -910,14 +953,12 @@ test("should disambiguate users with invisible characters", () => {
|
|||||||
const expectedLayoutMarbles = "ab";
|
const expectedLayoutMarbles = "ab";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([]),
|
{
|
||||||
behavior(scenarioInputMarbles, {
|
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||||
a: [localRtcMember],
|
a: [localRtcMember],
|
||||||
b: [localRtcMember, bobRtcMember, bobZeroWidthSpaceRtcMember],
|
b: [localRtcMember, bobRtcMember, bobZeroWidthSpaceRtcMember],
|
||||||
}),
|
}),
|
||||||
of(ConnectionState.Connected),
|
},
|
||||||
new Map(),
|
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
||||||
// Carol has no displayname - So userId is used.
|
// Carol has no displayname - So userId is used.
|
||||||
@@ -943,14 +984,12 @@ test("should strip RTL characters from displayname", () => {
|
|||||||
const expectedLayoutMarbles = "ab";
|
const expectedLayoutMarbles = "ab";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([]),
|
{
|
||||||
behavior(scenarioInputMarbles, {
|
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||||
a: [localRtcMember],
|
a: [localRtcMember],
|
||||||
b: [localRtcMember, daveRtcMember, daveRTLRtcMember],
|
b: [localRtcMember, daveRtcMember, daveRTLRtcMember],
|
||||||
}),
|
}),
|
||||||
of(ConnectionState.Connected),
|
},
|
||||||
new Map(),
|
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
||||||
// Carol has no displayname - So userId is used.
|
// Carol has no displayname - So userId is used.
|
||||||
@@ -970,16 +1009,15 @@ test("should strip RTL characters from displayname", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||||
withTestScheduler(({ schedule, expectObservable, behavior }) => {
|
withTestScheduler(({ schedule, expectObservable }) => {
|
||||||
// There should always be one tile for each MatrixRTCSession
|
// There should always be one tile for each MatrixRTCSession
|
||||||
const expectedLayoutMarbles = "ab";
|
const expectedLayoutMarbles = "ab";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
constant([aliceParticipant, bobParticipant]),
|
{
|
||||||
constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
remoteParticipants$: constant([aliceParticipant, bobParticipant]),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: constant([localRtcMember, aliceRtcMember, bobRtcMember]),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm, _rtcSession, { raisedHands$ }) => {
|
(vm, _rtcSession, { raisedHands$ }) => {
|
||||||
schedule("ab", {
|
schedule("ab", {
|
||||||
a: () => {
|
a: () => {
|
||||||
@@ -1072,11 +1110,7 @@ test("allOthersLeft$ emits only when someone joined and then all others left", (
|
|||||||
withTestScheduler(({ hot, expectObservable, scope }) => {
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
// 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), []),
|
{ remoteParticipants$: scope.behavior(nooneEverThere$(hot), []) },
|
||||||
constant([localRtcMember]),
|
|
||||||
of(ConnectionState.Connected),
|
|
||||||
new Map(),
|
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.allOthersLeft$).toBe("n------", { n: false });
|
expectObservable(vm.allOthersLeft$).toBe("n------", { n: false });
|
||||||
},
|
},
|
||||||
@@ -1087,11 +1121,10 @@ test("allOthersLeft$ emits only when someone joined and then all others left", (
|
|||||||
test("allOthersLeft$ emits true when someone joined and then all others left", () => {
|
test("allOthersLeft$ emits true when someone joined and then all others left", () => {
|
||||||
withTestScheduler(({ hot, expectObservable, scope }) => {
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
scope.behavior(participantJoinLeave$(hot), []),
|
{
|
||||||
scope.behavior(rtcMemberJoinLeave$(hot), []),
|
remoteParticipants$: scope.behavior(participantJoinLeave$(hot), []),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: scope.behavior(rtcMemberJoinLeave$(hot), []),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.allOthersLeft$).toBe(
|
expectObservable(vm.allOthersLeft$).toBe(
|
||||||
"n-----u", // false initially, then at frame 6: true then false emissions in same frame
|
"n-----u", // false initially, then at frame 6: true then false emissions in same frame
|
||||||
@@ -1105,11 +1138,10 @@ test("allOthersLeft$ emits true when someone joined and then all others left", (
|
|||||||
test("autoLeaveWhenOthersLeft$ emits only when autoLeaveWhenOthersLeft option is enabled", () => {
|
test("autoLeaveWhenOthersLeft$ emits only when autoLeaveWhenOthersLeft option is enabled", () => {
|
||||||
withTestScheduler(({ hot, expectObservable, scope }) => {
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
scope.behavior(participantJoinLeave$(hot), []),
|
{
|
||||||
scope.behavior(rtcMemberJoinLeave$(hot), []),
|
remoteParticipants$: scope.behavior(participantJoinLeave$(hot), []),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: scope.behavior(rtcMemberJoinLeave$(hot), []),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe(
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe(
|
||||||
"------e", // false initially, then at frame 6: true then false emissions in same frame
|
"------e", // false initially, then at frame 6: true then false emissions in same frame
|
||||||
@@ -1127,11 +1159,10 @@ test("autoLeaveWhenOthersLeft$ emits only when autoLeaveWhenOthersLeft option is
|
|||||||
test("autoLeaveWhenOthersLeft$ never emits autoLeaveWhenOthersLeft option is enabled but no-one is there", () => {
|
test("autoLeaveWhenOthersLeft$ never emits autoLeaveWhenOthersLeft option is enabled but no-one is there", () => {
|
||||||
withTestScheduler(({ hot, expectObservable, scope }) => {
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
scope.behavior(nooneEverThere$(hot), []),
|
{
|
||||||
scope.behavior(nooneEverThere$(hot), []),
|
remoteParticipants$: scope.behavior(nooneEverThere$(hot), []),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: scope.behavior(nooneEverThere$(hot), []),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("-------");
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("-------");
|
||||||
},
|
},
|
||||||
@@ -1146,11 +1177,10 @@ test("autoLeaveWhenOthersLeft$ never emits autoLeaveWhenOthersLeft option is ena
|
|||||||
test("autoLeaveWhenOthersLeft$ doesn't emit when autoLeaveWhenOthersLeft option is disabled and all others left", () => {
|
test("autoLeaveWhenOthersLeft$ doesn't emit when autoLeaveWhenOthersLeft option is disabled and all others left", () => {
|
||||||
withTestScheduler(({ hot, expectObservable, scope }) => {
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
scope.behavior(participantJoinLeave$(hot), []),
|
{
|
||||||
scope.behavior(rtcMemberJoinLeave$(hot), []),
|
remoteParticipants$: scope.behavior(participantJoinLeave$(hot), []),
|
||||||
of(ConnectionState.Connected),
|
rtcMembers$: scope.behavior(rtcMemberJoinLeave$(hot), []),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("-------");
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("-------");
|
||||||
},
|
},
|
||||||
@@ -1165,27 +1195,26 @@ test("autoLeaveWhenOthersLeft$ doesn't emit when autoLeaveWhenOthersLeft option
|
|||||||
test("autoLeaveWhenOthersLeft$ doesn't emits when autoLeaveWhenOthersLeft option is enabled and all others left", () => {
|
test("autoLeaveWhenOthersLeft$ doesn't emits when autoLeaveWhenOthersLeft option is enabled and all others left", () => {
|
||||||
withTestScheduler(({ hot, expectObservable, scope }) => {
|
withTestScheduler(({ hot, expectObservable, scope }) => {
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
scope.behavior(
|
{
|
||||||
hot("a-b-c-d", {
|
remoteParticipants$: scope.behavior(
|
||||||
a: [], // Alone
|
hot("a-b-c-d", {
|
||||||
b: [aliceParticipant], // Alice joins
|
a: [], // Alone
|
||||||
c: [aliceParticipant],
|
b: [aliceParticipant], // Alice joins
|
||||||
d: [], // Local joins with a second device
|
c: [aliceParticipant],
|
||||||
}),
|
d: [], // Local joins with a second device
|
||||||
[], //Alice leaves
|
}),
|
||||||
),
|
[], //Alice leaves
|
||||||
scope.behavior(
|
),
|
||||||
hot("a-b-c-d", {
|
rtcMembers$: scope.behavior(
|
||||||
a: [localRtcMember], // Start empty
|
hot("a-b-c-d", {
|
||||||
b: [localRtcMember, aliceRtcMember], // Alice joins
|
a: [localRtcMember], // Start empty
|
||||||
c: [localRtcMember, aliceRtcMember, localRtcMemberDevice2], // Alice still there
|
b: [localRtcMember, aliceRtcMember], // Alice joins
|
||||||
d: [localRtcMember, localRtcMemberDevice2], // The second Alice leaves
|
c: [localRtcMember, aliceRtcMember, localRtcMemberDevice2], // Alice still there
|
||||||
}),
|
d: [localRtcMember, localRtcMemberDevice2], // The second Alice leaves
|
||||||
[],
|
}),
|
||||||
),
|
[],
|
||||||
of(ConnectionState.Connected),
|
),
|
||||||
new Map(),
|
},
|
||||||
mockMediaDevices({}),
|
|
||||||
(vm) => {
|
(vm) => {
|
||||||
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("------e", {
|
expectObservable(vm.autoLeaveWhenOthersLeft$).toBe("------e", {
|
||||||
e: undefined,
|
e: undefined,
|
||||||
@@ -1219,27 +1248,18 @@ test("audio output changes when toggling earpiece mode", () => {
|
|||||||
const expectedEarpieceModeMarbles = "n-yn";
|
const expectedEarpieceModeMarbles = "n-yn";
|
||||||
const expectedTargetStateMarbles = " sese";
|
const expectedTargetStateMarbles = " sese";
|
||||||
|
|
||||||
withCallViewModel(
|
withCallViewModel({ mediaDevices: devices }, (vm) => {
|
||||||
constant([]),
|
schedule(toggleInputMarbles, {
|
||||||
constant([localRtcMember]),
|
a: () => getValue(vm.audioOutputSwitcher$)?.switch(),
|
||||||
of(ConnectionState.Connected),
|
});
|
||||||
new Map(),
|
expectObservable(vm.earpieceMode$).toBe(
|
||||||
devices,
|
expectedEarpieceModeMarbles,
|
||||||
(vm) => {
|
yesNo,
|
||||||
schedule(toggleInputMarbles, {
|
);
|
||||||
a: () => getValue(vm.audioOutputSwitcher$)?.switch(),
|
expectObservable(
|
||||||
});
|
vm.audioOutputSwitcher$.pipe(map((switcher) => switcher?.targetOutput)),
|
||||||
expectObservable(vm.earpieceMode$).toBe(
|
).toBe(expectedTargetStateMarbles, { s: "speaker", e: "earpiece" });
|
||||||
expectedEarpieceModeMarbles,
|
});
|
||||||
yesNo,
|
|
||||||
);
|
|
||||||
expectObservable(
|
|
||||||
vm.audioOutputSwitcher$.pipe(
|
|
||||||
map((switcher) => switcher?.targetOutput),
|
|
||||||
),
|
|
||||||
).toBe(expectedTargetStateMarbles, { s: "speaker", e: "earpiece" });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1271,25 +1291,39 @@ 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(
|
withCallViewModel(
|
||||||
constant([]),
|
{ initialSyncState: SyncState.Reconnecting },
|
||||||
constant([localRtcMember]),
|
(vm, rtcSession, _subjects, setSyncState) => {
|
||||||
of(ConnectionState.Connected),
|
schedule(syncingMarbles, {
|
||||||
new Map(),
|
y: () => setSyncState(SyncState.Syncing),
|
||||||
mockMediaDevices({}),
|
n: () => setSyncState(SyncState.Reconnecting),
|
||||||
(vm, rtcSession) => {
|
});
|
||||||
schedule(connectedMarbles, {
|
schedule(membershipStatusMarbles, {
|
||||||
y: () => {
|
y: () => {
|
||||||
rtcSession.probablyLeft = false;
|
rtcSession.membershipStatus = Status.Connected;
|
||||||
},
|
},
|
||||||
n: () => {
|
n: () => {
|
||||||
|
rtcSession.membershipStatus = Status.Reconnecting;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
schedule(probablyLeftMarbles, {
|
||||||
|
y: () => {
|
||||||
rtcSession.probablyLeft = true;
|
rtcSession.probablyLeft = true;
|
||||||
},
|
},
|
||||||
|
n: () => {
|
||||||
|
rtcSession.probablyLeft = false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expectObservable(vm.reconnecting$).toBe(
|
expectObservable(vm.reconnecting$).toBe(
|
||||||
expectedReconnectingMarbles,
|
expectedReconnectingMarbles,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user