mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-07 21:45:18 +00:00
Merge branch 'livekit' into toger5/tiles_based_on_rtc_member
This commit is contained in:
@@ -6,19 +6,25 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, vi, onTestFinished } from "vitest";
|
||||
import { map, Observable, of } from "rxjs";
|
||||
import {
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
} from "rxjs";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
ConnectionState,
|
||||
LocalParticipant,
|
||||
Participant,
|
||||
RemoteParticipant,
|
||||
} from "livekit-client";
|
||||
import * as ComponentsCore from "@livekit/components-core";
|
||||
import {
|
||||
CallMembership,
|
||||
MatrixRTCSession,
|
||||
MatrixRTCSessionEvent,
|
||||
} from "matrix-js-sdk/src/matrixrtc";
|
||||
import { isEqual } from "lodash";
|
||||
import { CallMembership, MatrixRTCSession, MatrixRTCSessionEvent } from "matrix-js-sdk/src/matrixrtc";
|
||||
|
||||
import { CallViewModel, Layout } from "./CallViewModel";
|
||||
import {
|
||||
@@ -27,7 +33,6 @@ import {
|
||||
mockMatrixRoom,
|
||||
mockRoomMember,
|
||||
mockRemoteParticipant,
|
||||
OurRunHelpers,
|
||||
withTestScheduler,
|
||||
mockMembership,
|
||||
} from "../utils/test";
|
||||
@@ -40,41 +45,40 @@ import { MockRoom, MockRTCSession } from "../useReactions.test";
|
||||
|
||||
vi.mock("@livekit/components-core");
|
||||
|
||||
const carolId = "@carol:example.org";
|
||||
const carolDev = "CCCC";
|
||||
const aliceId = "@alice:example.org";
|
||||
const alice = mockRoomMember({ userId: "@alice:example.org" });
|
||||
const bob = mockRoomMember({ userId: "@bob:example.org" });
|
||||
const carol = mockRoomMember({ userId: "@carol:example.org" });
|
||||
const dave = mockRoomMember({ userId: "@dave:example.org" });
|
||||
|
||||
const aliceDev = "AAAA";
|
||||
const aliceRTCId = aliceId + ":" + aliceDev;
|
||||
|
||||
const bobId = "@bob:example.org";
|
||||
const bobDev = "BBBB";
|
||||
const bobRTCId = bobId + ":" + bobDev;
|
||||
|
||||
const alice = mockRoomMember({ userId: aliceId });
|
||||
const bob = mockRoomMember({ userId: bobId });
|
||||
const carol = mockRoomMember({ userId: carolId });
|
||||
const carolDev = "CCCC";
|
||||
const daveDev = "DDDD";
|
||||
const aliceId = `${alice.userId}:${aliceDev}`;
|
||||
const bobId = `${bob.userId}:${bobDev}`;
|
||||
const carolId = `${carol.userId}:${carolDev}`;
|
||||
const daveId = `${dave.userId}:${daveDev}`;
|
||||
|
||||
const localParticipant = mockLocalParticipant({ identity: "" });
|
||||
const aliceParticipant = mockRemoteParticipant({ identity: aliceRTCId });
|
||||
|
||||
const aliceParticipant = mockRemoteParticipant({ identity: aliceId });
|
||||
const aliceSharingScreen = mockRemoteParticipant({
|
||||
identity: aliceRTCId,
|
||||
identity: aliceId,
|
||||
isScreenShareEnabled: true,
|
||||
});
|
||||
const bobParticipant = mockRemoteParticipant({ identity: bobRTCId });
|
||||
const bobParticipant = mockRemoteParticipant({ identity: bobId });
|
||||
const bobSharingScreen = mockRemoteParticipant({
|
||||
identity: bobRTCId,
|
||||
identity: bobId,
|
||||
isScreenShareEnabled: true,
|
||||
});
|
||||
const daveParticipant = mockRemoteParticipant({ identity: daveId });
|
||||
|
||||
const members = new Map([
|
||||
[alice.userId, alice],
|
||||
[bob.userId, bob],
|
||||
[carol.userId, carol],
|
||||
]);
|
||||
const members = new Map([alice, bob, carol, dave].map((p) => [p.userId, p]));
|
||||
|
||||
const rtcMemberAlice = mockMembership(aliceId, aliceDev);
|
||||
const rtcMemberBob = mockMembership(bobId, bobDev);
|
||||
const rtcMemberCarol = mockMembership(carolId, carolDev);
|
||||
const aliceRtcMember = mockMembership(aliceId, aliceDev);
|
||||
const bobRtcMember = mockMembership(bobId, bobDev);
|
||||
const carolRtcMember = mockMembership(carolId, carolDev);
|
||||
const daveRtcMember = mockMembership(daveId, daveDev);
|
||||
|
||||
export interface GridLayoutSummary {
|
||||
type: "grid";
|
||||
@@ -119,39 +123,72 @@ export type LayoutSummary =
|
||||
| OneOnOneLayoutSummary
|
||||
| PipLayoutSummary;
|
||||
|
||||
function summarizeLayout(l: Layout): LayoutSummary {
|
||||
switch (l.type) {
|
||||
case "grid":
|
||||
return {
|
||||
type: l.type,
|
||||
spotlight: l.spotlight?.map((vm) => vm.id),
|
||||
grid: l.grid.map((vm) => vm.id),
|
||||
};
|
||||
case "spotlight-landscape":
|
||||
case "spotlight-portrait":
|
||||
return {
|
||||
type: l.type,
|
||||
spotlight: l.spotlight.map((vm) => vm.id),
|
||||
grid: l.grid.map((vm) => vm.id),
|
||||
};
|
||||
case "spotlight-expanded":
|
||||
return {
|
||||
type: l.type,
|
||||
spotlight: l.spotlight.map((vm) => vm.id),
|
||||
pip: l.pip?.id,
|
||||
};
|
||||
case "one-on-one":
|
||||
return { type: l.type, local: l.local.id, remote: l.remote.id };
|
||||
case "pip":
|
||||
return { type: l.type, spotlight: l.spotlight.map((vm) => vm.id) };
|
||||
}
|
||||
function summarizeLayout(l: Observable<Layout>): Observable<LayoutSummary> {
|
||||
return l.pipe(
|
||||
switchMap((l) => {
|
||||
switch (l.type) {
|
||||
case "grid":
|
||||
return combineLatest(
|
||||
[
|
||||
l.spotlight?.media ?? of(undefined),
|
||||
...l.grid.map((vm) => vm.media),
|
||||
],
|
||||
(spotlight, ...grid) => ({
|
||||
type: l.type,
|
||||
spotlight: spotlight?.map((vm) => vm.id),
|
||||
grid: grid.map((vm) => vm.id),
|
||||
}),
|
||||
);
|
||||
case "spotlight-landscape":
|
||||
case "spotlight-portrait":
|
||||
return combineLatest(
|
||||
[l.spotlight.media, ...l.grid.map((vm) => vm.media)],
|
||||
(spotlight, ...grid) => ({
|
||||
type: l.type,
|
||||
spotlight: spotlight.map((vm) => vm.id),
|
||||
grid: grid.map((vm) => vm.id),
|
||||
}),
|
||||
);
|
||||
case "spotlight-expanded":
|
||||
return combineLatest(
|
||||
[l.spotlight.media, l.pip?.media ?? of(undefined)],
|
||||
(spotlight, pip) => ({
|
||||
type: l.type,
|
||||
spotlight: spotlight.map((vm) => vm.id),
|
||||
pip: pip?.id,
|
||||
}),
|
||||
);
|
||||
case "one-on-one":
|
||||
return combineLatest(
|
||||
[l.local.media, l.remote.media],
|
||||
(local, remote) => ({
|
||||
type: l.type,
|
||||
local: local.id,
|
||||
remote: remote.id,
|
||||
}),
|
||||
);
|
||||
case "pip":
|
||||
return l.spotlight.media.pipe(
|
||||
map((spotlight) => ({
|
||||
type: l.type,
|
||||
spotlight: spotlight.map((vm) => vm.id),
|
||||
})),
|
||||
);
|
||||
}
|
||||
}),
|
||||
// Sometimes there can be multiple (synchronous) updates per frame. We only
|
||||
// care about the most recent value for each time step, so discard these
|
||||
// extra values.
|
||||
debounceTime(0),
|
||||
distinctUntilChanged(isEqual),
|
||||
);
|
||||
}
|
||||
|
||||
function withCallViewModel(
|
||||
{ cold }: OurRunHelpers,
|
||||
remoteParticipants: Observable<RemoteParticipant[]>,
|
||||
rtcMembers: Observable<Partial<CallMembership>[]>,
|
||||
connectionState: Observable<ECConnectionState>,
|
||||
speaking: Map<Participant, Observable<boolean>>,
|
||||
continuation: (vm: CallViewModel) => void,
|
||||
): void {
|
||||
const room = mockMatrixRoom({
|
||||
@@ -172,19 +209,30 @@ function withCallViewModel(
|
||||
const mediaSpy = vi
|
||||
.spyOn(ComponentsCore, "observeParticipantMedia")
|
||||
.mockImplementation((p) =>
|
||||
cold("a", {
|
||||
a: { participant: p } as Partial<
|
||||
ComponentsCore.ParticipantMedia<LocalParticipant>
|
||||
> as ComponentsCore.ParticipantMedia<LocalParticipant>,
|
||||
}),
|
||||
of({ participant: p } as Partial<
|
||||
ComponentsCore.ParticipantMedia<LocalParticipant>
|
||||
> as ComponentsCore.ParticipantMedia<LocalParticipant>),
|
||||
);
|
||||
const eventsSpy = vi
|
||||
.spyOn(ComponentsCore, "observeParticipantEvents")
|
||||
.mockImplementation((p) => cold("a", { a: p }));
|
||||
.mockImplementation((p) =>
|
||||
(speaking.get(p) ?? of(false)).pipe(
|
||||
map((s) => ({ ...p, isSpeaking: s }) as Participant),
|
||||
),
|
||||
);
|
||||
|
||||
const roomEventSelectorSpy = vi
|
||||
.spyOn(ComponentsCore, "roomEventSelector")
|
||||
.mockImplementation((room, eventType) => of());
|
||||
|
||||
const liveKitRoom = mockLivekitRoom(
|
||||
{ localParticipant },
|
||||
{ remoteParticipants },
|
||||
);
|
||||
|
||||
const vm = new CallViewModel(
|
||||
rtcSession as unknown as MatrixRTCSession,
|
||||
mockLivekitRoom({ localParticipant }),
|
||||
liveKitRoom,
|
||||
{
|
||||
kind: E2eeType.PER_PARTICIPANT,
|
||||
},
|
||||
@@ -196,115 +244,316 @@ function withCallViewModel(
|
||||
participantsSpy!.mockRestore();
|
||||
mediaSpy!.mockRestore();
|
||||
eventsSpy!.mockRestore();
|
||||
roomEventSelectorSpy!.mockRestore();
|
||||
});
|
||||
|
||||
continuation(vm);
|
||||
}
|
||||
|
||||
test("participants are retained during a focus switch", () => {
|
||||
withTestScheduler((helpers) => {
|
||||
const { hot, expectObservable } = helpers;
|
||||
withTestScheduler(({ cold, expectObservable }) => {
|
||||
// Participants disappear on frame 2 and come back on frame 3
|
||||
const partMarbles = "a-ba";
|
||||
// The RTC members never disappear
|
||||
const rtcMemberMarbles = "a---";
|
||||
const participantMarbles = "a-ba";
|
||||
// Start switching focus on frame 1 and reconnect on frame 3
|
||||
const connMarbles = "ab-a";
|
||||
const connectionMarbles = " cs-c";
|
||||
// The visible participants should remain the same throughout the switch
|
||||
const laytMarbles = "aaaa 2997ms a 56998ms a";
|
||||
const layoutMarbles = " a";
|
||||
|
||||
withCallViewModel(
|
||||
helpers,
|
||||
hot(partMarbles, {
|
||||
cold(participantMarbles, {
|
||||
a: [aliceParticipant, bobParticipant],
|
||||
b: [],
|
||||
}),
|
||||
hot(rtcMemberMarbles, {
|
||||
a: [rtcMemberAlice, rtcMemberBob, rtcMemberCarol],
|
||||
}),
|
||||
hot(connMarbles, {
|
||||
a: ConnectionState.Connected,
|
||||
b: ECAddonConnectionState.ECSwitchingFocus,
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
cold(connectionMarbles, {
|
||||
c: ConnectionState.Connected,
|
||||
s: ECAddonConnectionState.ECSwitchingFocus,
|
||||
}),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
expectObservable(vm.layout.pipe(map(summarizeLayout))).toBe(
|
||||
laytMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceRTCId}:0`, `${bobRTCId}:0`],
|
||||
},
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(layoutMarbles, {
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("screen sharing activates spotlight layout", () => {
|
||||
withTestScheduler((helpers) => {
|
||||
const { hot, schedule, expectObservable } = helpers;
|
||||
withTestScheduler(({ cold, schedule, expectObservable }) => {
|
||||
// Start with no screen shares, then have Alice and Bob share their screens,
|
||||
// then return to no screen shares, then have just Alice share for a bit
|
||||
const partMarbles = "abc---d---a-b---a";
|
||||
const participantMarbles = " abcda-ba";
|
||||
// While there are no screen shares, switch to spotlight manually, and then
|
||||
// switch back to grid at the end
|
||||
const modeMarbles = "-----------a--------b";
|
||||
const modeMarbles = " -----s--g";
|
||||
// We should automatically enter spotlight for the first round of screen
|
||||
// sharing, then return to grid, then manually go into spotlight, and
|
||||
// remain in spotlight until we manually go back to grid
|
||||
const laytMarbles = "ab(cc)(dd)ae(bb)(ee)a 59979ms a";
|
||||
|
||||
const layoutMarbles = " abcdaefeg";
|
||||
const showSpeakingMarbles = "y----nyny";
|
||||
withCallViewModel(
|
||||
helpers,
|
||||
hot(partMarbles, {
|
||||
cold(participantMarbles, {
|
||||
a: [aliceParticipant, bobParticipant],
|
||||
b: [aliceSharingScreen, bobParticipant],
|
||||
c: [aliceSharingScreen, bobSharingScreen],
|
||||
d: [aliceParticipant, bobSharingScreen],
|
||||
}),
|
||||
of([rtcMemberAlice, rtcMemberAlice]),
|
||||
hot("a", { a: ConnectionState.Connected }),
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
schedule(modeMarbles, {
|
||||
a: () => vm.setGridMode("spotlight"),
|
||||
b: () => vm.setGridMode("grid"),
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
g: () => vm.setGridMode("grid"),
|
||||
});
|
||||
|
||||
expectObservable(vm.layout.pipe(map(summarizeLayout))).toBe(
|
||||
laytMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0:screen-share`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
c: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [
|
||||
`${aliceId}:0:screen-share`,
|
||||
`${bobId}:0:screen-share`,
|
||||
],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
d: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${bobId}:0:screen-share`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
e: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(layoutMarbles, {
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
);
|
||||
b: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0:screen-share`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
c: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0:screen-share`, `${bobId}:0:screen-share`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
d: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${bobId}:0:screen-share`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
e: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
grid: ["local:0", `${bobId}:0`],
|
||||
},
|
||||
f: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0:screen-share`],
|
||||
grid: ["local:0", `${bobId}:0`, `${aliceId}:0`],
|
||||
},
|
||||
g: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${bobId}:0`, `${aliceId}:0`],
|
||||
},
|
||||
});
|
||||
expectObservable(vm.showSpeakingIndicators).toBe(showSpeakingMarbles, {
|
||||
y: true,
|
||||
n: false,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("participants stay in the same order unless to appear/disappear", () => {
|
||||
withTestScheduler(({ cold, schedule, expectObservable }) => {
|
||||
const modeMarbles = "a";
|
||||
// First Bob speaks, then Dave, then Alice
|
||||
const aSpeakingMarbles = "n- 1998ms - 1999ms y";
|
||||
const bSpeakingMarbles = "ny 1998ms n 1999ms ";
|
||||
const dSpeakingMarbles = "n- 1998ms y 1999ms n";
|
||||
// Nothing should change when Bob speaks, because Bob is already on screen.
|
||||
// When Dave speaks he should switch with Alice because she's the one who
|
||||
// hasn't spoken at all. Then when Alice speaks, she should return to her
|
||||
// place at the top.
|
||||
const layoutMarbles = " a 1999ms b 1999ms a 57999ms c 1999ms a";
|
||||
|
||||
withCallViewModel(
|
||||
of([aliceParticipant, bobParticipant, daveParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember, daveRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map([
|
||||
[aliceParticipant, cold(aSpeakingMarbles, { y: true, n: false })],
|
||||
[bobParticipant, cold(bSpeakingMarbles, { y: true, n: false })],
|
||||
[daveParticipant, cold(dSpeakingMarbles, { y: true, n: false })],
|
||||
]),
|
||||
(vm) => {
|
||||
schedule(modeMarbles, {
|
||||
a: () => {
|
||||
// We imagine that only three tiles (the first three) will be visible
|
||||
// on screen at a time
|
||||
vm.layout.subscribe((layout) => {
|
||||
if (layout.type === "grid") {
|
||||
for (let i = 0; i < layout.grid.length; i++)
|
||||
layout.grid[i].setVisible(i < 3);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(layoutMarbles, {
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`, `${daveId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${daveId}:0`, `${bobId}:0`, `${aliceId}:0`],
|
||||
},
|
||||
c: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${daveId}:0`, `${bobId}:0`],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("spotlight speakers swap places", () => {
|
||||
withTestScheduler(({ cold, schedule, expectObservable }) => {
|
||||
// Go immediately into spotlight mode for the test
|
||||
const modeMarbles = " s";
|
||||
// First Bob speaks, then Dave, then Alice
|
||||
const aSpeakingMarbles = "n--y";
|
||||
const bSpeakingMarbles = "nyn";
|
||||
const dSpeakingMarbles = "n-yn";
|
||||
// Alice should start in the spotlight, then Bob, then Dave, then Alice
|
||||
// again. However, the positions of Dave and Bob in the grid should be
|
||||
// reversed by the end because they've been swapped in and out of the
|
||||
// spotlight.
|
||||
const layoutMarbles = " abcd";
|
||||
|
||||
withCallViewModel(
|
||||
of([aliceParticipant, bobParticipant, daveParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember, daveRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map([
|
||||
[aliceParticipant, cold(aSpeakingMarbles, { y: true, n: false })],
|
||||
[bobParticipant, cold(bSpeakingMarbles, { y: true, n: false })],
|
||||
[daveParticipant, cold(dSpeakingMarbles, { y: true, n: false })],
|
||||
]),
|
||||
(vm) => {
|
||||
schedule(modeMarbles, { s: () => vm.setGridMode("spotlight") });
|
||||
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(layoutMarbles, {
|
||||
a: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
grid: ["local:0", `${bobId}:0`, `${daveId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${bobId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${daveId}:0`],
|
||||
},
|
||||
c: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${daveId}:0`],
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
d: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
grid: ["local:0", `${daveId}:0`, `${bobId}:0`],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("layout enters picture-in-picture mode when requested", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// Enable then disable picture-in-picture
|
||||
const pipControlMarbles = "-ed";
|
||||
// Should go into picture-in-picture layout then back to grid
|
||||
const layoutMarbles = " aba";
|
||||
|
||||
withCallViewModel(
|
||||
of([aliceParticipant, bobParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
schedule(pipControlMarbles, {
|
||||
e: () => window.controls.enablePip(),
|
||||
d: () => window.controls.disablePip(),
|
||||
});
|
||||
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(layoutMarbles, {
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "pip",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("spotlight remembers whether it's expanded", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// Start in spotlight mode, then switch to grid and back to spotlight a
|
||||
// couple times
|
||||
const modeMarbles = " s-gs-gs";
|
||||
// Expand and collapse the spotlight
|
||||
const expandMarbles = "-a--a";
|
||||
// Spotlight should stay expanded during the first mode switch, and stay
|
||||
// collapsed during the second mode switch
|
||||
const layoutMarbles = "abcbada";
|
||||
|
||||
withCallViewModel(
|
||||
of([aliceParticipant, bobParticipant]),
|
||||
of([aliceRtcMember, bobRtcMember]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
schedule(modeMarbles, {
|
||||
s: () => vm.setGridMode("spotlight"),
|
||||
g: () => vm.setGridMode("grid"),
|
||||
});
|
||||
schedule(expandMarbles, {
|
||||
a: () => {
|
||||
let toggle: () => void;
|
||||
vm.toggleSpotlightExpanded.subscribe((val) => (toggle = val!));
|
||||
toggle!();
|
||||
},
|
||||
});
|
||||
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(layoutMarbles, {
|
||||
a: {
|
||||
type: "spotlight-landscape",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
grid: ["local:0", `${bobId}:0`],
|
||||
},
|
||||
b: {
|
||||
type: "spotlight-expanded",
|
||||
spotlight: [`${aliceId}:0`],
|
||||
pip: "local:0",
|
||||
},
|
||||
c: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
d: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${bobId}:0`, `${aliceId}:0`],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user