Test the CallViewModel output switcher directly

This commit is contained in:
Robin
2025-06-25 16:49:14 -04:00
parent 9d216ae6fa
commit 5e147c6941
2 changed files with 66 additions and 59 deletions

View File

@@ -72,6 +72,12 @@ import {
localId,
localRtcMember,
} from "../utils/test-fixtures";
import { ObservableScope } from "./ObservableScope";
import { MediaDevices } from "./MediaDevices";
import { getValue } from "../utils/observable";
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("../UrlParams", () => ({ getUrlParams }));
vi.mock("@livekit/components-core");
@@ -210,6 +216,7 @@ function withCallViewModel(
rtcMembers$: Observable<Partial<CallMembership>[]>,
connectionState$: Observable<ECConnectionState>,
speaking: Map<Participant, Observable<boolean>>,
mediaDevices: MediaDevices,
continuation: (
vm: CallViewModel,
subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
@@ -263,7 +270,7 @@ function withCallViewModel(
const vm = new CallViewModel(
rtcSession as unknown as MatrixRTCSession,
liveKitRoom,
mockMediaDevices({}),
mediaDevices,
{
kind: E2eeType.PER_PARTICIPANT,
},
@@ -303,6 +310,7 @@ test("participants are retained during a focus switch", () => {
s: ECAddonConnectionState.ECSwitchingFocus,
}),
new Map(),
mockMediaDevices({}),
(vm) => {
expectObservable(summarizeLayout$(vm.layout$)).toBe(
expectedLayoutMarbles,
@@ -342,6 +350,7 @@ test("screen sharing activates spotlight layout", () => {
of([aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
schedule(modeInputMarbles, {
s: () => vm.setGridMode("spotlight"),
@@ -425,6 +434,7 @@ test("participants stay in the same order unless to appear/disappear", () => {
[bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })],
[daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })],
]),
mockMediaDevices({}),
(vm) => {
schedule(visibilityInputMarbles, {
a: () => {
@@ -481,6 +491,7 @@ test("participants adjust order when space becomes constrained", () => {
[bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })],
[daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })],
]),
mockMediaDevices({}),
(vm) => {
let setVisibleTiles: ((value: number) => void) | null = null;
vm.layout$.subscribe((layout) => {
@@ -534,6 +545,7 @@ test("spotlight speakers swap places", () => {
[bobParticipant, hot(bSpeakingInputMarbles, { y: true, n: false })],
[daveParticipant, hot(dSpeakingInputMarbles, { y: true, n: false })],
]),
mockMediaDevices({}),
(vm) => {
schedule(modeInputMarbles, { s: () => vm.setGridMode("spotlight") });
@@ -579,6 +591,7 @@ test("layout enters picture-in-picture mode when requested", () => {
of([aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
schedule(pipControlInputMarbles, {
e: () => window.controls.enablePip(),
@@ -620,6 +633,7 @@ test("spotlight remembers whether it's expanded", () => {
of([aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
schedule(modeInputMarbles, {
s: () => vm.setGridMode("spotlight"),
@@ -688,6 +702,7 @@ test("participants must have a MatrixRTCSession to be visible", () => {
}),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
vm.setGridMode("grid");
expectObservable(summarizeLayout$(vm.layout$)).toBe(
@@ -732,6 +747,7 @@ test("shows participants without MatrixRTCSession when enabled in settings", ()
of([]), // No one joins the MatrixRTC session
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
vm.setGridMode("grid");
expectObservable(summarizeLayout$(vm.layout$)).toBe(
@@ -779,6 +795,7 @@ it("should show at least one tile per MatrixRTCSession", () => {
}),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
vm.setGridMode("grid");
expectObservable(summarizeLayout$(vm.layout$)).toBe(
@@ -827,6 +844,7 @@ test("should disambiguate users with the same displayname", () => {
}),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
// Skip the null state.
expectObservable(vm.memberDisplaynames$.pipe(skip(1))).toBe(
@@ -877,6 +895,7 @@ test("should disambiguate users with invisible characters", () => {
}),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
// Skip the null state.
expectObservable(vm.memberDisplaynames$.pipe(skip(1))).toBe(
@@ -913,6 +932,7 @@ test("should strip RTL characters from displayname", () => {
}),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm) => {
// Skip the null state.
expectObservable(vm.memberDisplaynames$.pipe(skip(1))).toBe(
@@ -945,6 +965,7 @@ it("should rank raised hands above video feeds and below speakers and presenters
of([aliceRtcMember, bobRtcMember]),
of(ConnectionState.Connected),
new Map(),
mockMediaDevices({}),
(vm, { raisedHands$ }) => {
schedule("ab", {
a: () => {
@@ -993,3 +1014,47 @@ it("should rank raised hands above video feeds and below speakers and presenters
);
});
});
test("audio output changes when toggling earpiece mode", () => {
withTestScheduler(({ schedule, expectObservable }) => {
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
vi.mocked(ComponentsCore.createMediaDeviceObserver).mockReturnValue(of([]));
const scope = new ObservableScope();
onTestFinished(() => scope.end());
const devices = new MediaDevices(scope);
window.controls.setAvailableAudioDevices([
{ id: "speaker", name: "Speaker", isSpeaker: true },
{ id: "earpiece", name: "Earpiece", isEarpiece: true },
{ id: "headphones", name: "Headphones" },
]);
window.controls.setAudioDevice("headphones");
const toggleInputMarbles = " -aaa";
const expectedEarpieceModeMarbles = "n-yn";
const expectedTargetStateMarbles = " sese";
withCallViewModel(
of([]),
of([]),
of(ConnectionState.Connected),
new Map(),
devices,
(vm) => {
schedule(toggleInputMarbles, {
a: () => getValue(vm.audioOutputSwitcher$)?.switch(),
});
expectObservable(vm.earpieceMode$).toBe(expectedEarpieceModeMarbles, {
n: false,
y: true,
});
expectObservable(
vm.audioOutputSwitcher$.pipe(
map((switcher) => switcher?.targetOutput),
),
).toBe(expectedTargetStateMarbles, { s: "speaker", e: "earpiece" });
},
);
});
});

View File

@@ -1,58 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { vi, test, onTestFinished } from "vitest";
import { createMediaDeviceObserver } from "@livekit/components-core";
import { map, of } from "rxjs";
import { withTestScheduler } from "../utils/test";
import { MediaDevices } from "./MediaDevices";
import { ObservableScope } from "./ObservableScope";
import { getValue } from "../utils/observable";
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("../UrlParams", () => ({ getUrlParams }));
vi.mock("@livekit/components-core");
test("audio output changes when toggling earpiece mode", () => {
withTestScheduler(({ schedule, expectObservable }) => {
getUrlParams.mockReturnValue({ controlledAudioDevices: true });
vi.mocked(createMediaDeviceObserver).mockReturnValue(of([]));
const scope = new ObservableScope();
onTestFinished(() => scope.end());
const devices = new MediaDevices(scope);
window.controls.setAvailableAudioDevices([
{ id: "speaker", name: "Speaker", isSpeaker: true },
{ id: "earpiece", name: "Earpiece", isEarpiece: true },
]);
const toggleInputMarbles = " -aa";
const expectedEarpieceModeMarbles = " nyn";
const expectedSelectedOutputMarbles = "ses";
schedule(toggleInputMarbles, {
a: () =>
devices.audioOutput.select(
getValue(devices.audioOutput.selected$)?.id === "earpiece"
? "speaker"
: "earpiece",
),
});
expectObservable(
devices.audioOutput.selected$.pipe(map((s) => s?.id === "earpiece")),
).toBe(expectedEarpieceModeMarbles, {
n: false,
y: true,
});
expectObservable(
devices.audioOutput.selected$.pipe(map((d) => d?.id)),
).toBe(expectedSelectedOutputMarbles, { s: "speaker", e: "earpiece" });
});
});