Files
element-call/src/state/CallViewModel/remoteMembers/MatrixLivekitMembers.test.ts

329 lines
9.8 KiB
TypeScript
Raw Normal View History

/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import {
type CallMembership,
type LivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc";
2025-12-15 18:23:30 +01:00
import { BehaviorSubject, combineLatest, map, type Observable } from "rxjs";
2025-11-06 15:26:17 +01:00
import { type IConnectionManager } from "./ConnectionManager.ts";
import {
type RemoteMatrixLivekitMember,
createMatrixLivekitMembers$,
2025-11-06 15:26:17 +01:00
} from "./MatrixLivekitMembers.ts";
2025-11-10 10:43:53 +01:00
import {
Epoch,
mapEpoch,
ObservableScope,
trackEpoch,
} from "../../ObservableScope.ts";
2025-11-06 15:26:17 +01:00
import { ConnectionManagerData } from "./ConnectionManager.ts";
import {
2025-12-15 18:23:30 +01:00
flushPromises,
2026-01-07 13:26:37 +01:00
mockRtcMembership,
mockRemoteParticipant,
} from "../../../utils/test.ts";
import { type Connection } from "./Connection.ts";
2025-12-15 18:23:30 +01:00
import { constant } from "../../Behavior.ts";
let testScope: ObservableScope;
2025-12-15 18:23:30 +01:00
const fallbackMemberId = (userId: string, deviceId: string): string =>
`${userId}:${deviceId}`;
2025-11-10 10:43:53 +01:00
const transportA: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.example.org",
livekit_alias: "!alias:example.org",
};
const transportB: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.sample.com",
livekit_alias: "!alias:sample.com",
};
2026-01-07 13:26:37 +01:00
const bobMembership = mockRtcMembership(
2025-11-10 10:43:53 +01:00
"@bob:example.org",
"DEV000",
transportA,
);
2026-01-07 13:26:37 +01:00
const carlMembership = mockRtcMembership(
2025-11-10 10:43:53 +01:00
"@carl:sample.com",
"DEV111",
transportB,
);
beforeEach(() => {
testScope = new ObservableScope();
});
afterEach(() => {
testScope.end();
});
2025-11-10 10:43:53 +01:00
function epochMeWith$<T, U>(
source$: Observable<Epoch<U>>,
me$: Observable<T>,
): Observable<Epoch<T>> {
return combineLatest([source$, me$]).pipe(
map(([ep, cd]) => {
return new Epoch(cd, ep.epoch);
}),
);
}
2025-12-15 18:23:30 +01:00
test("should signal participant not yet connected to livekit", async () => {
const mockedMemberships$ = new BehaviorSubject([bobMembership]);
const mockConnectionManagerData$ = new BehaviorSubject(
new ConnectionManagerData(),
);
const { memberships$, membershipsWithTransport$ } =
createEpochedMemberships$(mockedMemberships$);
2025-12-15 18:23:30 +01:00
const connectionManagerData$ = epochMeWith$(
memberships$,
mockConnectionManagerData$,
);
2025-12-15 18:23:30 +01:00
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
});
2025-12-15 18:23:30 +01:00
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
2025-12-15 18:23:30 +01:00
expect(data.length).toEqual(1);
expect(data[0].membership$.value).toBe(bobMembership);
expect(data[0].participant.value$.value).toBe(null);
2025-12-15 18:23:30 +01:00
expect(data[0].connection$.value).toBe(null);
return true;
},
);
});
2025-11-10 10:43:53 +01:00
// Helper to create epoch'ed memberships$ and membershipsWithTransport$ from memberships observable.
2025-12-15 18:23:30 +01:00
function createEpochedMemberships$(m$: Observable<CallMembership[]>): {
2025-11-10 10:43:53 +01:00
memberships$: Observable<Epoch<CallMembership[]>>;
membershipsWithTransport$: Observable<
Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]>
>;
} {
const memberships$ = m$.pipe(trackEpoch());
const membershipsWithTransport$ = memberships$.pipe(
mapEpoch((members) => {
return members.map((m) => {
const tr = m.getTransport(m);
return {
membership: m,
transport:
tr?.type === "livekit" ? (tr as LivekitTransport) : undefined,
};
});
}),
2025-11-10 10:43:53 +01:00
);
return {
memberships$,
membershipsWithTransport$,
};
}
2025-12-15 18:23:30 +01:00
test("should signal participant on a connection that is publishing", async () => {
const bobParticipantId = fallbackMemberId(
bobMembership.userId,
bobMembership.deviceId,
);
2025-11-10 10:43:53 +01:00
2025-12-15 18:23:30 +01:00
const { memberships$, membershipsWithTransport$ } = createEpochedMemberships$(
constant([bobMembership]),
);
2025-11-10 10:43:53 +01:00
2025-12-15 18:23:30 +01:00
const connection = {
transport: bobMembership.getTransport(bobMembership),
} as unknown as Connection;
const dataWithPublisher = new ConnectionManagerData();
dataWithPublisher.add(connection, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
2025-11-10 10:43:53 +01:00
2025-12-15 18:23:30 +01:00
const connectionManagerData$ = epochMeWith$(
memberships$,
constant(dataWithPublisher),
);
2025-11-10 10:43:53 +01:00
2025-12-15 18:23:30 +01:00
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
});
2025-12-15 18:23:30 +01:00
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
2025-12-15 18:23:30 +01:00
expect(data.length).toEqual(1);
expect(data[0].membership$.value).toBe(bobMembership);
expect(data[0].participant.value$.value).toSatisfy((participant) => {
2025-12-15 18:23:30 +01:00
expect(participant).toBeDefined();
expect(participant!.identity).toEqual(bobParticipantId);
return true;
2025-12-15 18:23:30 +01:00
});
expect(data[0].connection$.value).toBe(connection);
return true;
},
);
});
test("should signal participant on a connection that is not publishing", async () => {
const { memberships$, membershipsWithTransport$ } = createEpochedMemberships$(
constant([bobMembership]),
);
const connection = {
transport: bobMembership.getTransport(bobMembership),
} as unknown as Connection;
const dataWithPublisher = new ConnectionManagerData();
dataWithPublisher.add(connection, []);
const connectionManagerData$ = epochMeWith$(
memberships$,
constant(dataWithPublisher),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
});
2025-12-15 18:23:30 +01:00
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
2025-12-15 18:23:30 +01:00
expect(data.length).toEqual(1);
expect(data[0].membership$.value).toBe(bobMembership);
expect(data[0].participant.value$.value).toBe(null);
2025-12-15 18:23:30 +01:00
expect(data[0].connection$.value).toBe(connection);
return true;
},
);
});
2025-12-15 18:23:30 +01:00
describe("Publication edge case", () => {
test("bob is publishing in several connections", async () => {
const { memberships$, membershipsWithTransport$ } =
createEpochedMemberships$(constant([bobMembership, carlMembership]));
2025-12-15 18:23:30 +01:00
const connectionWithPublisher = new ConnectionManagerData();
const bobParticipantId = fallbackMemberId(
bobMembership.userId,
bobMembership.deviceId,
);
const connectionA = {
transport: transportA,
} as unknown as Connection;
const connectionB = {
transport: transportB,
} as unknown as Connection;
2025-12-15 18:23:30 +01:00
connectionWithPublisher.add(connectionA, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
connectionWithPublisher.add(connectionB, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
2025-11-10 10:43:53 +01:00
const connectionManagerData$ = epochMeWith$(
memberships$,
2025-12-15 18:23:30 +01:00
constant(connectionWithPublisher),
2025-11-10 10:43:53 +01:00
);
2025-12-22 13:35:40 +01:00
const matrixLivekitMembers$ = createMatrixLivekitMembers$({
scope: testScope,
2025-11-10 10:43:53 +01:00
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
});
2025-12-15 18:23:30 +01:00
await flushPromises();
expect(matrixLivekitMembers$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
2025-12-15 18:23:30 +01:00
expect(data.length).toEqual(2);
expect(data[0].membership$.value).toBe(bobMembership);
expect(data[0].connection$.value).toBe(connectionA);
expect(data[0].participant.value$.value).toSatisfy((participant) => {
2025-12-15 18:23:30 +01:00
expect(participant).toBeDefined();
expect(participant!.identity).toEqual(bobParticipantId);
return true;
2025-11-10 10:43:53 +01:00
});
2025-12-15 18:23:30 +01:00
return true;
2025-12-15 18:23:30 +01:00
},
);
});
});
2025-12-15 18:23:30 +01:00
test("bob is publishing in the wrong connection", async () => {
const mockedMemberships$ = new BehaviorSubject([
bobMembership,
carlMembership,
]);
2025-12-15 18:23:30 +01:00
const { memberships$, membershipsWithTransport$ } =
createEpochedMemberships$(mockedMemberships$);
2025-12-15 18:23:30 +01:00
const connectionWithPublisher = new ConnectionManagerData();
2025-12-15 18:23:30 +01:00
const bobParticipantId = fallbackMemberId(
bobMembership.userId,
bobMembership.deviceId,
);
const connectionA = { transport: transportA } as unknown as Connection;
const connectionB = { transport: transportB } as unknown as Connection;
// Bob is not publishing on A
connectionWithPublisher.add(connectionA, []);
// Bob is publishing on B but his membership says A
connectionWithPublisher.add(connectionB, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
const connectionsWithPublisher$ = new BehaviorSubject(
connectionWithPublisher,
);
const connectionManagerData$ = epochMeWith$(
memberships$,
connectionsWithPublisher$,
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
});
2025-12-15 18:23:30 +01:00
await flushPromises();
expect(matrixLivekitMember$.value.value).toSatisfy(
(data: RemoteMatrixLivekitMember[]) => {
2025-12-15 18:23:30 +01:00
expect(data.length).toEqual(2);
expect(data[0].membership$.value).toBe(bobMembership);
expect(data[0].connection$.value).toBe(connectionA);
expect(data[0].participant.value$.value).toBe(null);
2025-12-15 18:23:30 +01:00
return true;
},
);
});