Files
element-call-Github/src/state/remoteMembers/MatrixLivekitMembers.test.ts
2025-11-06 15:26:17 +01:00

401 lines
12 KiB
TypeScript

/*
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, vi, expect, beforeEach, afterEach } from "vitest";
import {
type CallMembership,
type LivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc";
import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk";
import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils";
import { type IConnectionManager } from "./ConnectionManager.ts";
import {
type MatrixLivekitMember,
createMatrixLivekitMembers$,
areLivekitTransportsEqual,
} from "./MatrixLivekitMembers.ts";
import { ObservableScope } from "../ObservableScope.ts";
import { ConnectionManagerData } from "./ConnectionManager.ts";
import {
mockCallMembership,
mockRemoteParticipant,
type OurRunHelpers,
withTestScheduler,
} from "../../utils/test.ts";
import { type Connection } from "./Connection.ts";
let testScope: ObservableScope;
let mockMatrixRoom: MatrixRoom;
// The merger beeing tested
beforeEach(() => {
testScope = new ObservableScope();
mockMatrixRoom = vi.mocked<MatrixRoom>({
getMember: vi.fn().mockImplementation((userId: string) => {
return {
userId,
rawDisplayName: userId.replace("@", "").replace(":example.org", ""),
getMxcAvatarUrl: vi.fn().mockReturnValue(null),
} as unknown as RoomMember;
}),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
} as unknown as MatrixRoom);
});
afterEach(() => {
testScope.end();
});
test("should signal participant not yet connected to livekit", () => {
withTestScheduler(({ behavior, expectObservable }) => {
const bobMembership = {
userId: "@bob:example.org",
deviceId: "DEV000",
transports: [
{
type: "livekit",
livekit_service_url: "https://lk.example.org",
livekit_alias: "!alias:example.org",
},
],
} as unknown as CallMembership;
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: behavior("a", {
a: [
{
membership: bobMembership,
},
],
}),
connectionManager: {
connectionManagerData$: behavior("a", {
a: new ConnectionManagerData(),
}),
transports$: behavior("a", { a: [] }),
connections$: behavior("a", { a: [] }),
},
matrixRoom: mockMatrixRoom,
});
expectObservable(matrixLivekitMember$).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
return (
data.length == 1 &&
data[0].membership === bobMembership &&
data[0].participant === undefined &&
data[0].connection === undefined
);
}),
});
});
});
function aConnectionManager(
data: ConnectionManagerData,
behavior: OurRunHelpers["behavior"],
): IConnectionManager {
return {
connectionManagerData$: behavior("a", { a: data }),
transports$: behavior("a", {
a: data.getConnections().map((connection) => connection.transport),
}),
connections$: behavior("a", { a: data.getConnections() }),
};
}
test("should signal participant on a connection that is publishing", () => {
withTestScheduler(({ behavior, expectObservable }) => {
const transport: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.example.org",
livekit_alias: "!alias:example.org",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transport,
);
const connectionWithPublisher = new ConnectionManagerData();
const bobParticipantId = getParticipantId(
bobMembership.userId,
bobMembership.deviceId,
);
const connection = {
transport: transport,
} as unknown as Connection;
connectionWithPublisher.add(connection, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: behavior("a", {
a: [
{
membership: bobMembership,
transport,
},
],
}),
connectionManager: aConnectionManager(connectionWithPublisher, behavior),
matrixRoom: mockMatrixRoom,
});
expectObservable(matrixLivekitMember$).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].participant).toBeDefined();
expect(data[0].connection).toBeDefined();
expect(data[0].membership).toEqual(bobMembership);
expect(
areLivekitTransportsEqual(data[0].connection!.transport, transport),
).toBe(true);
return true;
}),
});
});
});
test("should signal participant on a connection that is not publishing", () => {
withTestScheduler(({ behavior, expectObservable }) => {
const transport: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.example.org",
livekit_alias: "!alias:example.org",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transport,
);
const connectionWithPublisher = new ConnectionManagerData();
// const bobParticipantId = getParticipantId(bobMembership.userId, bobMembership.deviceId);
const connection = {
transport: transport,
} as unknown as Connection;
connectionWithPublisher.add(connection, []);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: behavior("a", {
a: [
{
membership: bobMembership,
transport,
},
],
}),
connectionManager: aConnectionManager(connectionWithPublisher, behavior),
matrixRoom: mockMatrixRoom,
});
expectObservable(matrixLivekitMember$).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].participant).not.toBeDefined();
expect(data[0].connection).toBeDefined();
expect(data[0].membership).toEqual(bobMembership);
expect(
areLivekitTransportsEqual(data[0].connection!.transport, transport),
).toBe(true);
return true;
}),
});
});
});
describe("Publication edge case", () => {
test("bob is publishing in several connections", () => {
withTestScheduler(({ behavior, expectObservable }) => {
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",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transportA,
);
const connectionWithPublisher = new ConnectionManagerData();
const bobParticipantId = getParticipantId(
bobMembership.userId,
bobMembership.deviceId,
);
const connectionA = {
transport: transportA,
} as unknown as Connection;
const connectionB = {
transport: transportB,
} as unknown as Connection;
connectionWithPublisher.add(connectionA, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
connectionWithPublisher.add(connectionB, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: behavior("a", {
a: [
{
membership: bobMembership,
transport: transportA,
},
],
}),
connectionManager: aConnectionManager(
connectionWithPublisher,
behavior,
),
matrixRoom: mockMatrixRoom,
});
expectObservable(matrixLivekitMember$).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].participant).toBeDefined();
expect(data[0].participant!.identity).toEqual(bobParticipantId);
expect(data[0].connection).toBeDefined();
expect(data[0].membership).toEqual(bobMembership);
expect(
areLivekitTransportsEqual(
data[0].connection!.transport,
transportA,
),
).toBe(true);
return true;
}),
});
});
});
test("bob is publishing in the wrong connection", () => {
withTestScheduler(({ behavior, expectObservable }) => {
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",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transportA,
);
const connectionWithPublisher = new ConnectionManagerData();
const bobParticipantId = getParticipantId(
bobMembership.userId,
bobMembership.deviceId,
);
const connectionA = {
transport: transportA,
} as unknown as Connection;
const connectionB = {
transport: transportB,
} as unknown as Connection;
connectionWithPublisher.add(connectionA, []);
connectionWithPublisher.add(connectionB, [
mockRemoteParticipant({ identity: bobParticipantId }),
]);
const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope,
membershipsWithTransport$: behavior("a", {
a: [
{
membership: bobMembership,
transport: transportA,
},
],
}),
connectionManager: aConnectionManager(
connectionWithPublisher,
behavior,
),
matrixRoom: mockMatrixRoom,
});
expectObservable(matrixLivekitMember$).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data.length).toEqual(1);
expect(data[0].participant).not.toBeDefined();
expect(data[0].connection).toBeDefined();
expect(data[0].membership).toEqual(bobMembership);
expect(
areLivekitTransportsEqual(
data[0].connection!.transport,
transportA,
),
).toBe(true);
return true;
}),
});
});
// let lastMatrixLkItems: MatrixLivekitMember[] = [];
// matrixLivekitMerger.matrixLivekitMember$.subscribe((items) => {
// lastMatrixLkItems = items;
// });
// vi.mocked(bobMembership).getTransport = vi
// .fn()
// .mockReturnValue(connectionA.transport);
// fakeMemberships$.next([bobMembership]);
// const lkMap = new ConnectionManagerData();
// lkMap.add(connectionA, []);
// lkMap.add(connectionB, [
// mockRemoteParticipant({ identity: bobParticipantId })
// ]);
// fakeManagerData$.next(lkMap);
// const items = lastMatrixLkItems;
// expect(items).toHaveLength(1);
// const item = items[0];
// // Assert the expected membership
// expect(item.membership.userId).toEqual(bobMembership.userId);
// expect(item.membership.deviceId).toEqual(bobMembership.deviceId);
// expect(item.participant).not.toBeDefined();
// // The transport info should come from the membership transports and not only from the publishing connection
// expect(item.connection?.transport?.livekit_service_url).toEqual(
// bobMembership.transports[0]?.livekit_service_url
// );
// expect(item.connection?.transport?.livekit_alias).toEqual(
// bobMembership.transports[0]?.livekit_alias
// );
});
});