/* Copyright 2023, 2024 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ import { map } from "rxjs"; import { RunHelpers, TestScheduler } from "rxjs/testing"; import { expect, vi } from "vitest"; import { RoomMember, Room as MatrixRoom } from "matrix-js-sdk/src/matrix"; import { LocalParticipant, LocalTrackPublication, RemoteParticipant, RemoteTrackPublication, Room as LivekitRoom, } from "livekit-client"; import { LocalUserMediaViewModel, RemoteUserMediaViewModel, } from "../state/MediaViewModel"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); try { continuation(); } finally { 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 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); }, }), ); } export function mockMember(member: Partial): RoomMember { return { on() { return this; }, off() { return this; }, addListener() { return this; }, removeListener() { return this; }, ...member, } as RoomMember; } export function mockMatrixRoom(room: Partial): MatrixRoom { return { on() { return this as MatrixRoom; }, off() { return this as MatrixRoom; }, addEventListener() { return this as MatrixRoom; }, removeEventListener() { return this as MatrixRoom; }, ...room, } as Partial as MatrixRoom; } export function mockLivekitRoom(room: Partial): LivekitRoom { return { on() { return this as LivekitRoom; }, off() { return this as LivekitRoom; }, addEventListener() { return this as LivekitRoom; }, removeEventListener() { return this as LivekitRoom; }, ...room, } as Partial as LivekitRoom; } export function mockLocalParticipant( participant: Partial, ): LocalParticipant { return { isLocal: true, getTrackPublication: () => ({}) as Partial as LocalTrackPublication, on() { return this as LocalParticipant; }, off() { return this as LocalParticipant; }, addListener() { return this as LocalParticipant; }, removeListener() { return this as LocalParticipant; }, ...participant, } as Partial as LocalParticipant; } export async function withLocalMedia( member: Partial, continuation: (vm: LocalUserMediaViewModel) => void | Promise, ): Promise { const vm = new LocalUserMediaViewModel( "local", mockMember(member), mockLocalParticipant({}), true, ); try { await continuation(vm); } finally { vm.destroy(); } } export function mockRemoteParticipant( participant: Partial, ): RemoteParticipant { return { isLocal: false, setVolume() {}, getTrackPublication: () => ({}) as Partial as RemoteTrackPublication, on() { return this; }, off() { return this; }, addListener() { return this; }, removeListener() { return this; }, ...participant, } as RemoteParticipant; } export async function withRemoteMedia( member: Partial, participant: Partial, continuation: (vm: RemoteUserMediaViewModel) => void | Promise, ): Promise { const vm = new RemoteUserMediaViewModel( "remote", mockMember(member), mockRemoteParticipant(participant), true, ); try { await continuation(vm); } finally { vm.destroy(); } }