/* Copyright 2025 Element Corp. Copyright 2024 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 { ConnectionState, type LocalParticipant, type Participant, ParticipantEvent, type RemoteParticipant, type Room as LivekitRoom, } from "livekit-client"; import { SyncState } from "matrix-js-sdk/lib/sync"; import { BehaviorSubject, type Observable, map, of } from "rxjs"; import { onTestFinished, vi } from "vitest"; import { ClientEvent, type MatrixClient } from "matrix-js-sdk"; import EventEmitter from "events"; import * as ComponentsCore from "@livekit/components-core"; import type { CallMembership } from "matrix-js-sdk/lib/matrixrtc"; import { E2eeType } from "../../e2ee/e2eeType"; import { type RaisedHandInfo, type ReactionInfo } from "../../reactions"; import { type CallViewModel, createCallViewModel$, type CallViewModelOptions, } from "./CallViewModel"; import { mockConfig, mockLivekitRoom, mockLocalParticipant, mockMatrixRoom, mockMatrixRoomMember, mockMediaDevices, mockMuteStates, MockRTCSession, testScope, } from "../../utils/test"; import { alice, aliceDoppelganger, bob, bobZeroWidthSpace, daveRTL, daveRTLRtcMember, local, localRtcMember, } from "../../utils/test-fixtures"; import { type Behavior, constant } from "../Behavior"; import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { type MediaDevices } from "../MediaDevices"; mockConfig({ livekit: { livekit_service_url: "http://my-default-service-url.com" }, }); const carol = local; const dave = mockMatrixRoomMember(daveRTLRtcMember, { rawDisplayName: "Dave" }); const roomMembers = new Map( [alice, aliceDoppelganger, bob, bobZeroWidthSpace, carol, dave, daveRTL].map( (p) => [p.userId, p], ), ); export interface CallViewModelInputs { remoteParticipants$: Behavior; rtcMembers$: Behavior[]>; livekitConnectionState$: Behavior; speaking: Map>; mediaDevices: MediaDevices; initialSyncState: SyncState; } const localParticipant = mockLocalParticipant({ identity: "" }); export function withCallViewModel( { remoteParticipants$ = constant([]), rtcMembers$ = constant([localRtcMember]), livekitConnectionState$: connectionState$ = constant( ConnectionState.Connected, ), speaking = new Map(), mediaDevices = mockMediaDevices({}), initialSyncState = SyncState.Syncing, }: Partial = {}, continuation: ( vm: CallViewModel, rtcSession: MockRTCSession, subjects: { raisedHands$: BehaviorSubject> }, setSyncState: (value: SyncState) => void, ) => void, options: CallViewModelOptions = { encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, autoLeaveWhenOthersLeft: false, }, ): void { let syncState = initialSyncState; const setSyncState = (value: SyncState): void => { const prev = syncState; syncState = value; room.client.emit(ClientEvent.Sync, value, prev); }; const room = mockMatrixRoom({ client: new (class extends EventEmitter { public getUserId(): string | undefined { return localRtcMember.userId; } public getDeviceId(): string { return localRtcMember.deviceId; } public getDomain(): string { return "example.com"; } public getSyncState(): SyncState { return syncState; } })() as Partial as MatrixClient, getMembers: () => Array.from(roomMembers.values()), getMembersWithMembership: () => Array.from(roomMembers.values()), }); const rtcSession = new MockRTCSession(room, []).withMemberships(rtcMembers$); const participantsSpy = vi .spyOn(ComponentsCore, "connectedParticipantsObserver") .mockReturnValue(remoteParticipants$); const mediaSpy = vi .spyOn(ComponentsCore, "observeParticipantMedia") .mockImplementation((p) => of({ participant: p } as Partial< ComponentsCore.ParticipantMedia > as ComponentsCore.ParticipantMedia), ); const eventsSpy = vi .spyOn(ComponentsCore, "observeParticipantEvents") .mockImplementation((p, ...eventTypes) => { if (eventTypes.includes(ParticipantEvent.IsSpeakingChanged)) { return (speaking.get(p) ?? of(false)).pipe( map((s): Participant => ({ ...p, isSpeaking: s }) as Participant), ); } else { return of(p); } }); const roomEventSelectorSpy = vi .spyOn(ComponentsCore, "roomEventSelector") .mockImplementation((_room, _eventType) => of()); const muteStates = mockMuteStates(); const raisedHands$ = new BehaviorSubject>({}); const reactions$ = new BehaviorSubject>({}); const vm = createCallViewModel$( testScope(), rtcSession.asMockedSession(), room, mediaDevices, muteStates, { ...options, livekitRoomFactory: (): LivekitRoom => mockLivekitRoom({ localParticipant, disconnect: async () => Promise.resolve(), setE2EEEnabled: async () => Promise.resolve(), }), connectionState$, }, raisedHands$, reactions$, new BehaviorSubject({ processor: undefined, supported: undefined, }), ); onTestFinished(() => { participantsSpy.mockRestore(); mediaSpy.mockRestore(); eventsSpy.mockRestore(); roomEventSelectorSpy.mockRestore(); }); continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState); }