refact: extract withCallViewModel in test utils file
This commit is contained in:
@@ -24,8 +24,8 @@ import {
|
|||||||
createCallNotificationLifecycle$,
|
createCallNotificationLifecycle$,
|
||||||
type Props as CallNotificationLifecycleProps,
|
type Props as CallNotificationLifecycleProps,
|
||||||
} from "./CallNotificationLifecycle";
|
} from "./CallNotificationLifecycle";
|
||||||
// import { withCallViewModel } from "./CallViewModel.test";
|
|
||||||
import { trackEpoch } from "../ObservableScope";
|
import { trackEpoch } from "../ObservableScope";
|
||||||
|
import { withCallViewModel } from "./CallViewModelTestUtils.test";
|
||||||
|
|
||||||
const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent;
|
const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent;
|
||||||
function mockRingEvent(
|
function mockRingEvent(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
switchMap,
|
switchMap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import {
|
import {
|
||||||
ClientEvent,
|
|
||||||
SyncState,
|
SyncState,
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
RoomEvent as MatrixRoomEvent,
|
RoomEvent as MatrixRoomEvent,
|
||||||
@@ -30,10 +29,7 @@ import {
|
|||||||
} from "matrix-js-sdk";
|
} from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
type LocalParticipant,
|
|
||||||
type LocalTrackPublication,
|
type LocalTrackPublication,
|
||||||
type Participant,
|
|
||||||
ParticipantEvent,
|
|
||||||
type RemoteParticipant,
|
type RemoteParticipant,
|
||||||
type Room as LivekitRoom,
|
type Room as LivekitRoom,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
@@ -49,7 +45,7 @@ import {
|
|||||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||||
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
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 { type Layout } from "../layout-types.ts";
|
||||||
import {
|
import {
|
||||||
mockLocalParticipant,
|
mockLocalParticipant,
|
||||||
@@ -70,15 +66,11 @@ import { E2eeType } from "../../e2ee/e2eeType.ts";
|
|||||||
import type { RaisedHandInfo, ReactionInfo } from "../../reactions/index.ts";
|
import type { RaisedHandInfo, ReactionInfo } from "../../reactions/index.ts";
|
||||||
import {
|
import {
|
||||||
alice,
|
alice,
|
||||||
aliceDoppelganger,
|
|
||||||
aliceId,
|
aliceId,
|
||||||
aliceParticipant,
|
aliceParticipant,
|
||||||
aliceRtcMember,
|
aliceRtcMember,
|
||||||
bob,
|
|
||||||
bobId,
|
bobId,
|
||||||
bobRtcMember,
|
bobRtcMember,
|
||||||
bobZeroWidthSpace,
|
|
||||||
daveRTL,
|
|
||||||
local,
|
local,
|
||||||
localId,
|
localId,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
@@ -87,11 +79,11 @@ import {
|
|||||||
import { MediaDevices } from "../MediaDevices.ts";
|
import { MediaDevices } from "../MediaDevices.ts";
|
||||||
import { getValue } from "../../utils/observable.ts";
|
import { getValue } from "../../utils/observable.ts";
|
||||||
import { type Behavior, constant } from "../Behavior.ts";
|
import { type Behavior, constant } from "../Behavior.ts";
|
||||||
import type { ProcessorState } from "../../livekit/TrackProcessorContext.tsx";
|
|
||||||
import {
|
import {
|
||||||
type ElementCallError,
|
type ElementCallError,
|
||||||
MatrixRTCTransportMissingError,
|
MatrixRTCTransportMissingError,
|
||||||
} from "../../utils/errors.ts";
|
} from "../../utils/errors.ts";
|
||||||
|
import { withCallViewModel } from "./CallViewModelTestUtils.test";
|
||||||
|
|
||||||
mockConfig({
|
mockConfig({
|
||||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
@@ -124,7 +116,7 @@ const yesNo = {
|
|||||||
|
|
||||||
const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD");
|
const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD");
|
||||||
|
|
||||||
const carol = local;
|
// const carol = local;
|
||||||
|
|
||||||
const dave = mockMatrixRoomMember(daveRtcMember, { rawDisplayName: "Dave" });
|
const dave = mockMatrixRoomMember(daveRtcMember, { rawDisplayName: "Dave" });
|
||||||
|
|
||||||
@@ -142,12 +134,6 @@ const bobSharingScreen = mockRemoteParticipant({
|
|||||||
});
|
});
|
||||||
const daveParticipant = mockRemoteParticipant({ identity: daveId });
|
const daveParticipant = mockRemoteParticipant({ identity: daveId });
|
||||||
|
|
||||||
const roomMembers = new Map(
|
|
||||||
[alice, aliceDoppelganger, bob, bobZeroWidthSpace, carol, dave, daveRTL].map(
|
|
||||||
(p) => [p.userId, p],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
export interface GridLayoutSummary {
|
export interface GridLayoutSummary {
|
||||||
type: "grid";
|
type: "grid";
|
||||||
spotlight?: string[];
|
spotlight?: string[];
|
||||||
@@ -269,124 +255,6 @@ function mockRingEvent(
|
|||||||
// need a value to fill in for them when emitting notifications
|
// need a value to fill in for them when emitting notifications
|
||||||
const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent;
|
const mockLegacyRingEvent = {} as { event_id: string } & ICallNotifyContent;
|
||||||
|
|
||||||
interface CallViewModelInputs {
|
|
||||||
remoteParticipants$: Behavior<RemoteParticipant[]>;
|
|
||||||
rtcMembers$: Behavior<Partial<CallMembership>[]>;
|
|
||||||
livekitConnectionState$: Behavior<ConnectionState>;
|
|
||||||
speaking: Map<Participant, Observable<boolean>>;
|
|
||||||
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<CallViewModelInputs>,
|
|
||||||
continuation: (
|
|
||||||
vm: CallViewModel,
|
|
||||||
rtcSession: MockRTCSession,
|
|
||||||
subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
|
|
||||||
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<MatrixClient> 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<LocalParticipant>
|
|
||||||
> as ComponentsCore.ParticipantMedia<LocalParticipant>),
|
|
||||||
);
|
|
||||||
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<Record<string, RaisedHandInfo>>({});
|
|
||||||
const reactions$ = new BehaviorSubject<Record<string, ReactionInfo>>({});
|
|
||||||
|
|
||||||
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<ProcessorState>({
|
|
||||||
processor: undefined,
|
|
||||||
supported: undefined,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
onTestFinished(() => {
|
|
||||||
participantsSpy.mockRestore();
|
|
||||||
mediaSpy.mockRestore();
|
|
||||||
eventsSpy.mockRestore();
|
|
||||||
roomEventSelectorSpy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("CallViewModel", () => {
|
describe("CallViewModel", () => {
|
||||||
// TODO: Restore this test. It requires makeTransport to not be mocked, unlike
|
// TODO: Restore this test. It requires makeTransport to not be mocked, unlike
|
||||||
// the rest of the tests in this file… what do we do?
|
// the rest of the tests in this file… what do we do?
|
||||||
|
|||||||
183
src/state/CallViewModel/CallViewModelTestUtils.test.ts
Normal file
183
src/state/CallViewModel/CallViewModelTestUtils.test.ts
Normal file
@@ -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<RemoteParticipant[]>;
|
||||||
|
rtcMembers$: Behavior<Partial<CallMembership>[]>;
|
||||||
|
livekitConnectionState$: Behavior<ConnectionState>;
|
||||||
|
speaking: Map<Participant, Observable<boolean>>;
|
||||||
|
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<CallViewModelInputs> = {},
|
||||||
|
continuation: (
|
||||||
|
vm: CallViewModel,
|
||||||
|
rtcSession: MockRTCSession,
|
||||||
|
subjects: { raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>> },
|
||||||
|
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<MatrixClient> 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<LocalParticipant>
|
||||||
|
> as ComponentsCore.ParticipantMedia<LocalParticipant>),
|
||||||
|
);
|
||||||
|
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<Record<string, RaisedHandInfo>>({});
|
||||||
|
const reactions$ = new BehaviorSubject<Record<string, ReactionInfo>>({});
|
||||||
|
|
||||||
|
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<ProcessorState>({
|
||||||
|
processor: undefined,
|
||||||
|
supported: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
onTestFinished(() => {
|
||||||
|
participantsSpy.mockRestore();
|
||||||
|
mediaSpy.mockRestore();
|
||||||
|
eventsSpy.mockRestore();
|
||||||
|
roomEventSelectorSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
continuation(vm, rtcSession, { raisedHands$: raisedHands$ }, setSyncState);
|
||||||
|
}
|
||||||
@@ -12,8 +12,8 @@ import { constant } from "./Behavior.ts";
|
|||||||
import { aliceParticipant, localRtcMember } from "../utils/test-fixtures.ts";
|
import { aliceParticipant, localRtcMember } from "../utils/test-fixtures.ts";
|
||||||
import { ElementWidgetActions, widget } from "../widget.ts";
|
import { ElementWidgetActions, widget } from "../widget.ts";
|
||||||
import { E2eeType } from "../e2ee/e2eeType.ts";
|
import { E2eeType } from "../e2ee/e2eeType.ts";
|
||||||
import { withCallViewModel } from "./CallViewModel/CallViewModel.test.ts";
|
|
||||||
import { type CallViewModel } from "./CallViewModel/CallViewModel.ts";
|
import { type CallViewModel } from "./CallViewModel/CallViewModel.ts";
|
||||||
|
import { withCallViewModel } from "./CallViewModel/CallViewModelTestUtils.test";
|
||||||
|
|
||||||
vi.mock("../widget", () => ({
|
vi.mock("../widget", () => ({
|
||||||
ElementWidgetActions: {
|
ElementWidgetActions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user