From 6d8d2d2384b16a4bf2372022eaa12c872dbdb458 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 14 Nov 2025 17:30:02 +0100 Subject: [PATCH] refact: extract withCallViewModel in test utils file --- .../CallNotificationLifecycle.test.ts | 2 +- src/state/CallViewModel/CallViewModel.test.ts | 138 +------------ .../CallViewModelTestUtils.test.ts | 183 ++++++++++++++++++ src/state/CallViewModelWidget.test.ts | 2 +- 4 files changed, 188 insertions(+), 137 deletions(-) create mode 100644 src/state/CallViewModel/CallViewModelTestUtils.test.ts diff --git a/src/state/CallViewModel/CallNotificationLifecycle.test.ts b/src/state/CallViewModel/CallNotificationLifecycle.test.ts index 1fd04d7a..8bdbe059 100644 --- a/src/state/CallViewModel/CallNotificationLifecycle.test.ts +++ b/src/state/CallViewModel/CallNotificationLifecycle.test.ts @@ -24,8 +24,8 @@ import { createCallNotificationLifecycle$, type Props as CallNotificationLifecycleProps, } from "./CallNotificationLifecycle"; -// import { withCallViewModel } from "./CallViewModel.test"; import { trackEpoch } from "../ObservableScope"; +import { withCallViewModel } from "./CallViewModelTestUtils.test"; const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent; function mockRingEvent( diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index 4c5e9cc5..863ba16f 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -19,7 +19,6 @@ import { switchMap, } from "rxjs"; import { - ClientEvent, SyncState, type MatrixClient, RoomEvent as MatrixRoomEvent, @@ -30,10 +29,7 @@ import { } from "matrix-js-sdk"; import { ConnectionState, - type LocalParticipant, type LocalTrackPublication, - type Participant, - ParticipantEvent, type RemoteParticipant, type Room as LivekitRoom, } from "livekit-client"; @@ -49,7 +45,7 @@ import { import { deepCompare } from "matrix-js-sdk/lib/utils"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; -import { CallViewModel, type CallViewModelOptions } from "./CallViewModel"; +import { CallViewModel } from "./CallViewModel"; import { type Layout } from "../layout-types.ts"; import { mockLocalParticipant, @@ -70,15 +66,11 @@ import { E2eeType } from "../../e2ee/e2eeType.ts"; import type { RaisedHandInfo, ReactionInfo } from "../../reactions/index.ts"; import { alice, - aliceDoppelganger, aliceId, aliceParticipant, aliceRtcMember, - bob, bobId, bobRtcMember, - bobZeroWidthSpace, - daveRTL, local, localId, localRtcMember, @@ -87,11 +79,11 @@ import { import { MediaDevices } from "../MediaDevices.ts"; import { getValue } from "../../utils/observable.ts"; import { type Behavior, constant } from "../Behavior.ts"; -import type { ProcessorState } from "../../livekit/TrackProcessorContext.tsx"; import { type ElementCallError, MatrixRTCTransportMissingError, } from "../../utils/errors.ts"; +import { withCallViewModel } from "./CallViewModelTestUtils.test"; mockConfig({ livekit: { livekit_service_url: "http://my-default-service-url.com" }, @@ -124,7 +116,7 @@ const yesNo = { const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD"); -const carol = local; +// const carol = local; const dave = mockMatrixRoomMember(daveRtcMember, { rawDisplayName: "Dave" }); @@ -142,12 +134,6 @@ const bobSharingScreen = mockRemoteParticipant({ }); const daveParticipant = mockRemoteParticipant({ identity: daveId }); -const roomMembers = new Map( - [alice, aliceDoppelganger, bob, bobZeroWidthSpace, carol, dave, daveRTL].map( - (p) => [p.userId, p], - ), -); - export interface GridLayoutSummary { type: "grid"; spotlight?: string[]; @@ -269,124 +255,6 @@ function mockRingEvent( // need a value to fill in for them when emitting notifications const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent; -interface CallViewModelInputs { - remoteParticipants$: Behavior; - rtcMembers$: Behavior[]>; - livekitConnectionState$: Behavior; - speaking: Map>; - mediaDevices: MediaDevices; - initialSyncState: SyncState; -} - -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()), - }); - 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) => ({ ...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 = new CallViewModel( - 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); -} - describe("CallViewModel", () => { // TODO: Restore this test. It requires makeTransport to not be mocked, unlike // the rest of the tests in this file… what do we do? diff --git a/src/state/CallViewModel/CallViewModelTestUtils.test.ts b/src/state/CallViewModel/CallViewModelTestUtils.test.ts new file mode 100644 index 00000000..6cae1bfa --- /dev/null +++ b/src/state/CallViewModel/CallViewModelTestUtils.test.ts @@ -0,0 +1,183 @@ +/* +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 { CallViewModel, type CallViewModelOptions } from "./CallViewModel"; +import { + 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"; + +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()), + }); + 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 = new CallViewModel( + 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); +} diff --git a/src/state/CallViewModelWidget.test.ts b/src/state/CallViewModelWidget.test.ts index 35a451f9..7947cc1a 100644 --- a/src/state/CallViewModelWidget.test.ts +++ b/src/state/CallViewModelWidget.test.ts @@ -12,8 +12,8 @@ import { constant } from "./Behavior.ts"; import { aliceParticipant, localRtcMember } from "../utils/test-fixtures.ts"; import { ElementWidgetActions, widget } from "../widget.ts"; import { E2eeType } from "../e2ee/e2eeType.ts"; -import { withCallViewModel } from "./CallViewModel/CallViewModel.test.ts"; import { type CallViewModel } from "./CallViewModel/CallViewModel.ts"; +import { withCallViewModel } from "./CallViewModel/CallViewModelTestUtils.test"; vi.mock("../widget", () => ({ ElementWidgetActions: {