Test MediaViewModel

This was the result of me playing around with RxJS marble testing to understand how to get things done with its TestScheduler. I discovered that it lacks a clear way to fire arbitrary actions during the test, so I built a small helper function called schedule which does this for us.
This commit is contained in:
Robin 2024-08-30 19:09:42 -04:00
parent 3e57a7692c
commit 9d5145a7a6
2 changed files with 169 additions and 2 deletions

View File

@ -0,0 +1,132 @@
/*
Copyright 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { RoomMember } from "matrix-js-sdk/src/matrix";
import { expect, test, vi } from "vitest";
import { LocalParticipant, RemoteParticipant } from "livekit-client";
import {
LocalUserMediaViewModel,
RemoteUserMediaViewModel,
} from "./MediaViewModel";
import { withTestScheduler } from "../utils/test";
function withLocal(continuation: (vm: LocalUserMediaViewModel) => void): void {
const member = {} as unknown as RoomMember;
const vm = new LocalUserMediaViewModel(
"a",
member,
{} as unknown as LocalParticipant,
true,
);
try {
continuation(vm);
} finally {
vm.destroy();
}
}
function withRemote(
participant: Partial<RemoteParticipant>,
continuation: (vm: RemoteUserMediaViewModel) => void,
): void {
const member = {} as unknown as RoomMember;
const vm = new RemoteUserMediaViewModel(
"a",
member,
{ setVolume() {}, ...participant } as RemoteParticipant,
true,
);
try {
continuation(vm);
} finally {
vm.destroy();
}
}
test("set a participant's volume", () => {
const setVolumeSpy = vi.fn();
withRemote({ setVolume: setVolumeSpy }, (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", {
a() {
vm.setLocalVolume(0.8);
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
},
});
expectObservable(vm.localVolume).toBe("ab", { a: 1, b: 0.8 });
}),
);
});
test("mute and unmute a participant", () => {
const setVolumeSpy = vi.fn();
withRemote({ setVolume: setVolumeSpy }, (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-abc|", {
a() {
vm.toggleLocallyMuted();
expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
},
b() {
vm.setLocalVolume(0.8);
expect(setVolumeSpy).toHaveBeenLastCalledWith(0);
},
c() {
vm.toggleLocallyMuted();
expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8);
},
});
expectObservable(vm.locallyMuted).toBe("ab-c", {
a: false,
b: true,
c: false,
});
}),
);
});
test("toggle fit/contain for a participant's video", () => {
withRemote({}, (vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-ab|", {
a: () => vm.toggleFitContain(),
b: () => vm.toggleFitContain(),
});
expectObservable(vm.cropVideo).toBe("abc", {
a: true,
b: false,
c: true,
});
}),
);
});
test("local media remembers whether it should always be shown", () => {
withLocal((vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm.setAlwaysShow(false) });
expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false });
}),
);
// Next local media should start out *not* always shown
withLocal((vm) =>
withTestScheduler(({ expectObservable, schedule }) => {
schedule("-a|", { a: () => vm.setAlwaysShow(true) });
expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true });
}),
);
});

View File

@ -1,5 +1,5 @@
/*
Copyright 2023 New Vector Ltd
Copyright 2023-2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -13,7 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { vi } from "vitest";
import { map } from "rxjs";
import { RunHelpers, TestScheduler } from "rxjs/testing";
import { expect, vi } from "vitest";
export function withFakeTimers(continuation: () => void): void {
vi.useFakeTimers();
@ -23,3 +25,36 @@ export function withFakeTimers(continuation: () => void): void {
vi.useRealTimers();
}
}
export interface OurRunHelpers extends RunHelpers {
/**
* Schedules a sequence of actions to happen, as described by a marble
* diagram.
*/
schedule: (marbles: string, actions: Record<string, () => void>) => void;
}
/**
* Run Observables with a scheduler that virtualizes time, for testing purposes.
*/
export function withTestScheduler(
continuation: (helpers: OurRunHelpers) => void,
): void {
new TestScheduler((actual, expected) => {
expect(actual).deep.equals(expected);
}).run((helpers) =>
continuation({
...helpers,
schedule(marbles, actions) {
const actionsObservable = helpers
.cold(marbles)
.pipe(map((value) => actions[value]()));
const results = Object.fromEntries(
Object.keys(actions).map((value) => [value, undefined] as const),
);
// Run the actions and verify that none of them error
helpers.expectObservable(actionsObservable).toBe(marbles, results);
},
}),
);
}