Fix a couple of CallViewModel tests.

This commit is contained in:
Timo K
2025-11-14 10:44:16 +01:00
parent 0115242a2b
commit fdce3ec1aa
9 changed files with 1426 additions and 1378 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -182,6 +182,21 @@ export class CallViewModel {
this.matrixRTCSession, this.matrixRTCSession,
); );
// Each hbar seperates a block of input variables required for the CallViewModel to function.
// The outputs of this block is written under the hbar.
//
// For mocking purposes it is recommended to only mock the functions creating those outputs.
// All other fields are just temp computations for the mentioned output.
// The class does not need anything except the values underneath the bar.
// The creation of the values under the bar are all tested independently and testing the callViewModel Should
// not test their cretation. Call view model only needs:
// - memberships$ via createMemberships$
// - localMembership via createLocalMembership$
// - callLifecycle via createCallNotificationLifecycle$
// - matrixMemberMetadataStore via createMatrixMemberMetadata$
// ------------------------------------------------------------------------
// memberships$
private memberships$ = createMemberships$(this.scope, this.matrixRTCSession); private memberships$ = createMemberships$(this.scope, this.matrixRTCSession);
private membershipsAndTransports = membershipsAndTransports$( private membershipsAndTransports = membershipsAndTransports$(
@@ -189,6 +204,9 @@ export class CallViewModel {
this.memberships$, this.memberships$,
); );
// ------------------------------------------------------------------------
// matrixLivekitMembers$ AND localMembership
private localTransport$ = createLocalTransport$({ private localTransport$ = createLocalTransport$({
scope: this.scope, scope: this.scope,
memberships$: this.memberships$, memberships$: this.memberships$,
@@ -199,38 +217,32 @@ export class CallViewModel {
), ),
}); });
// ------------------------------------------------------------------------
private connectionFactory = new ECConnectionFactory( private connectionFactory = new ECConnectionFactory(
this.matrixRoom.client, this.matrixRoom.client,
this.mediaDevices, this.mediaDevices,
this.trackProcessorState$, this.trackProcessorState$,
this.livekitKeyProvider, this.livekitKeyProvider,
getUrlParams().controlledAudioDevices, getUrlParams().controlledAudioDevices,
); this.options.livekitRoomFactory,
// Can contain duplicates. The connection manager will take care of this.
private allTransports$ = this.scope.behavior(
combineLatest(
[this.localTransport$, this.membershipsAndTransports.transports$],
(localTransport, transports) => {
const localTransportAsArray = localTransport ? [localTransport] : [];
return transports.mapInner((transports) => [
...localTransportAsArray,
...transports,
]);
},
),
); );
private connectionManager = createConnectionManager$({ private connectionManager = createConnectionManager$({
scope: this.scope, scope: this.scope,
connectionFactory: this.connectionFactory, connectionFactory: this.connectionFactory,
inputTransports$: this.allTransports$, inputTransports$: this.scope.behavior(
combineLatest(
[this.localTransport$, this.membershipsAndTransports.transports$],
(localTransport, transports) => {
const localTransportAsArray = localTransport ? [localTransport] : [];
return transports.mapInner((transports) => [
...localTransportAsArray,
...transports,
]);
},
),
),
}); });
// ------------------------------------------------------------------------
private matrixLivekitMembers$ = createMatrixLivekitMembers$({ private matrixLivekitMembers$ = createMatrixLivekitMembers$({
scope: this.scope, scope: this.scope,
membershipsWithTransport$: membershipsWithTransport$:
@@ -273,6 +285,7 @@ export class CallViewModel {
), ),
), ),
); );
private localMatrixLivekitMemberUninitialized = { private localMatrixLivekitMemberUninitialized = {
membership$: this.localRtcMembership$, membership$: this.localRtcMembership$,
participant$: this.localMembership.participant$, participant$: this.localMembership.participant$,
@@ -294,6 +307,7 @@ export class CallViewModel {
); );
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// callLifecycle
private callLifecycle = createCallNotificationLifecycle$({ private callLifecycle = createCallNotificationLifecycle$({
scope: this.scope, scope: this.scope,
@@ -309,6 +323,13 @@ export class CallViewModel {
public autoLeave$ = this.callLifecycle.autoLeave$; public autoLeave$ = this.callLifecycle.autoLeave$;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// matrixMemberMetadataStore
private matrixMemberMetadataStore = createMatrixMemberMetadata$(
this.scope,
this.scope.behavior(this.memberships$.pipe(map((mems) => mems.value))),
createRoomMembers$(this.scope, this.matrixRoom),
);
/** /**
* If there is a configuration error with the call (e.g. misconfigured E2EE). * If there is a configuration error with the call (e.g. misconfigured E2EE).
@@ -402,12 +423,6 @@ export class CallViewModel {
), ),
); );
private matrixMemberMetadataStore = createMatrixMemberMetadata$(
this.scope,
this.scope.behavior(this.memberships$.pipe(map((mems) => mems.value))),
createRoomMembers$(this.scope, this.matrixRoom),
);
/** /**
* List of user media (camera feeds) that we want tiles for. * List of user media (camera feeds) that we want tiles for.
*/ */
@@ -426,20 +441,23 @@ export class CallViewModel {
{ value: matrixLivekitMembers }, { value: matrixLivekitMembers },
duplicateTiles, duplicateTiles,
]) { ]) {
let localParticipantId = undefined;
// add local member if available // add local member if available
if (localMatrixLivekitMember) { if (localMatrixLivekitMember) {
const { const { userId, participant$, connection$, membership$ } =
userId, localMatrixLivekitMember;
participant$, localParticipantId = `${userId}:${membership$.value.deviceId}`; // should be membership$.value.membershipID which is not optional
connection$,
// membership$,
} = localMatrixLivekitMember;
const participantId = participant$.value?.identity; // should be membership$.value.membershipID which is not optional
// const participantId = membership$.value.membershipID; // const participantId = membership$.value.membershipID;
if (participantId) { if (localParticipantId) {
for (let dup = 0; dup < 1 + duplicateTiles; dup++) { for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
yield { yield {
keys: [dup, participantId, userId, participant$, connection$], keys: [
dup,
localParticipantId,
userId,
participant$,
connection$,
],
data: undefined, data: undefined,
}; };
} }
@@ -450,11 +468,11 @@ export class CallViewModel {
userId, userId,
participant$, participant$,
connection$, connection$,
// membership$ membership$,
} of matrixLivekitMembers) { } of matrixLivekitMembers) {
const participantId = participant$.value?.identity; const participantId = `${userId}:${membership$.value.deviceId}`;
if (participantId === localParticipantId) continue;
// const participantId = membership$.value?.identity; // const participantId = membership$.value?.identity;
if (!participantId) continue;
for (let dup = 0; dup < 1 + duplicateTiles; dup++) { for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
yield { yield {
keys: [dup, participantId, userId, participant$, connection$], keys: [dup, participantId, userId, participant$, connection$],
@@ -550,9 +568,8 @@ export class CallViewModel {
* - There can be multiple participants for one Matrix user if they join from * - There can be multiple participants for one Matrix user if they join from
* multiple devices. * multiple devices.
*/ */
// TODO KEEP THIS!! and adapt it to what our membershipManger returns
public readonly participantCount$ = this.scope.behavior( public readonly participantCount$ = this.scope.behavior(
this.memberships$.pipe(map((ms) => ms.value.length)), this.matrixLivekitMembers$.pipe(map((ms) => ms.value.length)),
); );
// only public to expose to the view. // only public to expose to the view.

View File

@@ -30,7 +30,7 @@ import {
startWith, startWith,
switchMap, switchMap,
} from "rxjs"; } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../../Behavior"; import { type Behavior } from "../../Behavior";
import { type IConnectionManager } from "../remoteMembers/ConnectionManager"; import { type IConnectionManager } from "../remoteMembers/ConnectionManager";
@@ -52,7 +52,7 @@ import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
import { MatrixRTCMode } from "../../../settings/settings.ts"; import { MatrixRTCMode } from "../../../settings/settings.ts";
import { Config } from "../../../config/Config.ts"; import { Config } from "../../../config/Config.ts";
import { type Connection } from "../remoteMembers/Connection.ts"; import { type Connection } from "../remoteMembers/Connection.ts";
const logger = rootLogger.getChild("[LocalMembership]");
export enum LivekitState { export enum LivekitState {
Uninitialized = "uninitialized", Uninitialized = "uninitialized",
Connecting = "connecting", Connecting = "connecting",
@@ -323,10 +323,19 @@ export const createLocalMembership$ = ({
!connectRequested || !connectRequested ||
state.matrix$.value.state !== MatrixState.Disconnected state.matrix$.value.state !== MatrixState.Disconnected
) { ) {
logger.info("Waiting for transport to enter rtc session"); logger.info(
"Not yet connecting because: ",
"transport === null:",
transport === null,
"!connectRequested:",
!connectRequested,
"state.matrix$.value.state !== MatrixState.Disconnected:",
state.matrix$.value.state !== MatrixState.Disconnected,
);
return; return;
} }
state.matrix$.next({ state: MatrixState.Connecting }); state.matrix$.next({ state: MatrixState.Connecting });
logger.info("Matrix State connecting");
enterRTCSession(matrixRTCSession, transport, options.value).catch( enterRTCSession(matrixRTCSession, transport, options.value).catch(
(error) => { (error) => {
logger.error(error); logger.error(error);
@@ -376,7 +385,9 @@ export const createLocalMembership$ = ({
for (const p of publications) { for (const p of publications) {
if (p.track?.isUpstreamPaused === true) { if (p.track?.isUpstreamPaused === true) {
const kind = p.track.kind; const kind = p.track.kind;
logger.log(`Resuming ${kind} track (MatrixRTC connection present)`); logger.info(
`Resuming ${kind} track (MatrixRTC connection present)`,
);
p.track p.track
.resumeUpstream() .resumeUpstream()
.catch((e) => .catch((e) =>
@@ -391,7 +402,7 @@ export const createLocalMembership$ = ({
for (const p of publications) { for (const p of publications) {
if (p.track?.isUpstreamPaused === false) { if (p.track?.isUpstreamPaused === false) {
const kind = p.track.kind; const kind = p.track.kind;
logger.log( logger.info(
`Pausing ${kind} track (uncertain MatrixRTC connection)`, `Pausing ${kind} track (uncertain MatrixRTC connection)`,
); );
p.track p.track

View File

@@ -117,7 +117,7 @@ export function createConnectionManager$({
connectionFactory, connectionFactory,
inputTransports$, inputTransports$,
}: Props): IConnectionManager { }: Props): IConnectionManager {
const logger = rootLogger.getChild("ConnectionManager"); const logger = rootLogger.getChild("[ConnectionManager]");
const running$ = new BehaviorSubject(true); const running$ = new BehaviorSubject(true);
scope.onEnd(() => running$.next(false)); scope.onEnd(() => running$.next(false));

View File

@@ -22,7 +22,7 @@ import { Epoch, type ObservableScope } from "../../ObservableScope";
import { type Connection } from "./Connection"; import { type Connection } from "./Connection";
import { generateItemsWithEpoch } from "../../../utils/observable"; import { generateItemsWithEpoch } from "../../../utils/observable";
const logger = rootLogger.getChild("MatrixLivekitMembers"); const logger = rootLogger.getChild("[MatrixLivekitMembers]");
/** /**
* Represents a Matrix call member and their associated LiveKit participation. * Represents a Matrix call member and their associated LiveKit participation.

View File

@@ -74,22 +74,14 @@ export class ObservableScope {
// they will no longer re-emit their current value upon subscription. We want // they will no longer re-emit their current value upon subscription. We want
// to support Observables that complete (for example `of({})`), so we have to // to support Observables that complete (for example `of({})`), so we have to
// take care to not propagate the completion event. // take care to not propagate the completion event.
setValue$ setValue$.pipe(this.bind(), distinctUntilChanged()).subscribe({
.pipe( next(value) {
this.bind(), subject$.next(value);
distinctUntilChanged((a, b) => { },
logger.log("distinctUntilChanged", a, b); error(err: unknown) {
return a === b; subject$.error(err);
}), },
) });
.subscribe({
next(value) {
subject$.next(value);
},
error(err: unknown) {
subject$.error(err);
},
});
if (subject$.value === nothing) if (subject$.value === nothing)
throw new Error("Behavior failed to synchronously emit an initial value"); throw new Error("Behavior failed to synchronously emit an initial value");
return subject$ as Behavior<T>; return subject$ as Behavior<T>;

View File

@@ -76,6 +76,6 @@ export const createMemberships$ = (
MatrixRTCSessionEvent.MembershipsChanged, MatrixRTCSessionEvent.MembershipsChanged,
(_, memberships: CallMembership[]) => memberships, (_, memberships: CallMembership[]) => memberships,
).pipe(trackEpoch()), ).pipe(trackEpoch()),
new Epoch([]), new Epoch(matrixRTCSession.memberships),
); );
}; };

View File

@@ -11,9 +11,9 @@ import {
mockRemoteParticipant, mockRemoteParticipant,
} from "./test"; } from "./test";
export const localRtcMember = mockRtcMembership("@carol:example.org", "1111"); export const localRtcMember = mockRtcMembership("@local:example.org", "1111");
export const localRtcMemberDevice2 = mockRtcMembership( export const localRtcMemberDevice2 = mockRtcMembership(
"@carol:example.org", "@local:example.org",
"2222", "2222",
); );
export const local = mockMatrixRoomMember(localRtcMember); export const local = mockMatrixRoomMember(localRtcMember);

View File

@@ -6,7 +6,14 @@ Please see LICENSE in the repository root for full details.
*/ */
import { map, type Observable, of, type SchedulerLike } from "rxjs"; import { map, type Observable, of, type SchedulerLike } from "rxjs";
import { type RunHelpers, TestScheduler } from "rxjs/testing"; import { type RunHelpers, TestScheduler } from "rxjs/testing";
import { expect, type MockedObject, onTestFinished, vi, vitest } from "vitest"; import {
expect,
type MockedObject,
type MockInstance,
onTestFinished,
vi,
vitest,
} from "vitest";
import { import {
MatrixEvent, MatrixEvent,
type Room as MatrixRoom, type Room as MatrixRoom,
@@ -269,6 +276,7 @@ export function mockLivekitRoom(
}: { remoteParticipants$?: Observable<RemoteParticipant[]> } = {}, }: { remoteParticipants$?: Observable<RemoteParticipant[]> } = {},
): LivekitRoom { ): LivekitRoom {
const livekitRoom = { const livekitRoom = {
options: {},
...mockEmitter(), ...mockEmitter(),
...room, ...room,
} as Partial<LivekitRoom> as LivekitRoom; } as Partial<LivekitRoom> as LivekitRoom;
@@ -291,6 +299,7 @@ export function mockLocalParticipant(
return { return {
isLocal: true, isLocal: true,
trackPublications: new Map(), trackPublications: new Map(),
unpublishTracks: async () => Promise.resolve(),
getTrackPublication: () => getTrackPublication: () =>
({}) as Partial<LocalTrackPublication> as LocalTrackPublication, ({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
...mockEmitter(), ...mockEmitter(),
@@ -331,6 +340,8 @@ export function mockRemoteParticipant(
setVolume() {}, setVolume() {},
getTrackPublication: () => getTrackPublication: () =>
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication, ({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
// this will only get used for `getTrackPublications().length`
getTrackPublications: () => [0],
...mockEmitter(), ...mockEmitter(),
...participant, ...participant,
} as RemoteParticipant; } as RemoteParticipant;
@@ -363,13 +374,16 @@ export function createRemoteMedia(
); );
} }
export function mockConfig(config: Partial<ResolvedConfigOptions> = {}): void { export function mockConfig(
vi.spyOn(Config, "get").mockReturnValue({ config: Partial<ResolvedConfigOptions> = {},
): MockInstance<() => ResolvedConfigOptions> {
const spy = vi.spyOn(Config, "get").mockReturnValue({
...DEFAULT_CONFIG, ...DEFAULT_CONFIG,
...config, ...config,
}); });
// simulate loading the config // simulate loading the config
vi.spyOn(Config, "init").mockResolvedValue(void 0); vi.spyOn(Config, "init").mockResolvedValue(void 0);
return spy;
} }
export class MockRTCSession extends TypedEventEmitter< export class MockRTCSession extends TypedEventEmitter<