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

681 lines
21 KiB
TypeScript
Raw Normal View History

2025-10-01 14:21:37 +02:00
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
2025-10-07 16:24:02 +02:00
import {
afterEach,
describe,
expect,
it,
type MockedObject,
onTestFinished,
2025-10-07 16:24:02 +02:00
vi,
} from "vitest";
import { BehaviorSubject } from "rxjs";
2025-10-06 10:50:10 +02:00
import {
type LocalParticipant,
type RemoteParticipant,
type Room as LivekitRoom,
2025-10-07 16:24:02 +02:00
RoomEvent,
ConnectionState as LivekitConnectionState,
2025-10-06 10:50:10 +02:00
} from "livekit-client";
2025-10-01 14:21:37 +02:00
import fetchMock from "fetch-mock";
import EventEmitter from "events";
2025-10-01 17:24:19 +02:00
import { type IOpenIDToken } from "matrix-js-sdk";
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
import type {
CallMembership,
LivekitTransport,
} from "matrix-js-sdk/lib/matrixrtc";
import {
Connection,
2025-10-07 16:24:02 +02:00
type ConnectionOpts,
2025-10-28 21:18:47 +01:00
type ConnectionState,
type PublishingParticipant,
2025-10-07 16:24:02 +02:00
} from "./Connection.ts";
import { ObservableScope } from "../../ObservableScope.ts";
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
import { FailToGetOpenIdToken } from "../../../utils/errors.ts";
2025-10-01 17:24:19 +02:00
let testScope: ObservableScope;
2025-10-01 14:21:37 +02:00
2025-10-01 17:24:19 +02:00
let client: MockedObject<OpenIDClientParts>;
2025-10-01 14:21:37 +02:00
2025-10-01 17:24:19 +02:00
let fakeLivekitRoom: MockedObject<LivekitRoom>;
2025-10-01 14:21:37 +02:00
2025-10-06 10:50:10 +02:00
let localParticipantEventEmiter: EventEmitter;
let fakeLocalParticipant: MockedObject<LocalParticipant>;
2025-10-01 17:24:19 +02:00
let fakeRoomEventEmiter: EventEmitter;
2025-10-07 16:24:02 +02:00
let fakeMembershipsFocusMap$: BehaviorSubject<
{ membership: CallMembership; transport: LivekitTransport }[]
>;
2025-10-01 14:21:37 +02:00
const livekitFocus: LivekitTransport = {
2025-10-01 17:24:19 +02:00
livekit_alias: "!roomID:example.org",
livekit_service_url: "https://matrix-rtc.example.org/livekit/jwt",
2025-10-07 16:24:02 +02:00
type: "livekit",
2025-10-02 12:53:59 +02:00
};
2025-10-01 14:21:37 +02:00
2025-10-01 17:24:19 +02:00
function setupTest(): void {
testScope = new ObservableScope();
client = vi.mocked<OpenIDClientParts>({
2025-10-07 16:24:02 +02:00
getOpenIdToken: vi.fn().mockResolvedValue({
access_token: "rYsmGUEwNjKgJYyeNUkZseJN",
token_type: "Bearer",
matrix_server_name: "example.org",
expires_in: 3600,
}),
getDeviceId: vi.fn().mockReturnValue("ABCDEF"),
2025-10-01 17:24:19 +02:00
} as unknown as OpenIDClientParts);
2025-10-07 16:24:02 +02:00
fakeMembershipsFocusMap$ = new BehaviorSubject<
{ membership: CallMembership; transport: LivekitTransport }[]
>([]);
2025-10-01 17:24:19 +02:00
2025-10-06 10:50:10 +02:00
localParticipantEventEmiter = new EventEmitter();
fakeLocalParticipant = vi.mocked<LocalParticipant>({
identity: "@me:example.org",
isMicrophoneEnabled: vi.fn().mockReturnValue(true),
getTrackPublication: vi.fn().mockReturnValue(undefined),
on: localParticipantEventEmiter.on.bind(localParticipantEventEmiter),
off: localParticipantEventEmiter.off.bind(localParticipantEventEmiter),
2025-10-07 16:24:02 +02:00
addListener: localParticipantEventEmiter.addListener.bind(
localParticipantEventEmiter,
),
removeListener: localParticipantEventEmiter.removeListener.bind(
localParticipantEventEmiter,
),
removeAllListeners: localParticipantEventEmiter.removeAllListeners.bind(
localParticipantEventEmiter,
),
2025-10-06 10:50:10 +02:00
} as unknown as LocalParticipant);
2025-10-01 17:24:19 +02:00
fakeRoomEventEmiter = new EventEmitter();
fakeLivekitRoom = vi.mocked<LivekitRoom>({
connect: vi.fn(),
disconnect: vi.fn(),
remoteParticipants: new Map(),
2025-10-06 10:50:10 +02:00
localParticipant: fakeLocalParticipant,
state: LivekitConnectionState.Disconnected,
2025-10-01 17:24:19 +02:00
on: fakeRoomEventEmiter.on.bind(fakeRoomEventEmiter),
off: fakeRoomEventEmiter.off.bind(fakeRoomEventEmiter),
addListener: fakeRoomEventEmiter.addListener.bind(fakeRoomEventEmiter),
2025-10-07 16:24:02 +02:00
removeListener:
fakeRoomEventEmiter.removeListener.bind(fakeRoomEventEmiter),
removeAllListeners:
fakeRoomEventEmiter.removeAllListeners.bind(fakeRoomEventEmiter),
setE2EEEnabled: vi.fn().mockResolvedValue(undefined),
2025-10-01 17:24:19 +02:00
} as unknown as LivekitRoom);
}
function setupRemoteConnection(): Connection {
2025-10-01 17:24:19 +02:00
const opts: ConnectionOpts = {
client: client,
transport: livekitFocus,
2025-10-01 17:24:19 +02:00
scope: testScope,
2025-10-07 16:24:02 +02:00
livekitRoomFactory: () => fakeLivekitRoom,
2025-10-02 12:53:59 +02:00
};
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, () => {
return {
status: 200,
body: {
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
jwt: "ATOKEN",
},
};
});
2025-10-01 14:47:45 +02:00
2025-10-07 16:24:02 +02:00
fakeLivekitRoom.connect.mockResolvedValue(undefined);
2025-10-01 14:47:45 +02:00
return new Connection(opts);
2025-10-01 17:24:19 +02:00
}
2025-10-01 14:47:45 +02:00
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
fetchMock.reset();
});
2025-10-01 17:24:19 +02:00
describe("Start connection states", () => {
2025-10-01 14:21:37 +02:00
it("start in initialized state", () => {
setupTest();
const opts: ConnectionOpts = {
client: client,
transport: livekitFocus,
2025-10-01 14:21:37 +02:00
scope: testScope,
2025-10-07 16:24:02 +02:00
livekitRoomFactory: () => fakeLivekitRoom,
2025-10-02 12:53:59 +02:00
};
const connection = new Connection(opts);
2025-10-01 14:21:37 +02:00
2025-10-28 21:18:47 +01:00
expect(connection.state$.getValue().state).toEqual("Initialized");
2025-10-01 14:21:37 +02:00
});
it("fail to getOpenId token then error state", async () => {
setupTest();
vi.useFakeTimers();
const opts: ConnectionOpts = {
client: client,
transport: livekitFocus,
2025-10-01 14:21:37 +02:00
scope: testScope,
2025-10-07 16:24:02 +02:00
livekitRoomFactory: () => fakeLivekitRoom,
2025-10-02 12:53:59 +02:00
};
2025-10-01 14:21:37 +02:00
const connection = new Connection(opts, undefined);
2025-10-01 14:21:37 +02:00
2025-10-28 21:18:47 +01:00
const capturedStates: ConnectionState[] = [];
const s = connection.state$.subscribe((value) => {
2025-10-01 17:24:19 +02:00
capturedStates.push(value);
2025-10-01 14:21:37 +02:00
});
onTestFinished(() => s.unsubscribe());
2025-10-01 14:21:37 +02:00
2025-10-01 17:24:19 +02:00
const deferred = Promise.withResolvers<IOpenIDToken>();
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
client.getOpenIdToken.mockImplementation(
async (): Promise<IOpenIDToken> => {
return await deferred.promise;
},
);
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
connection.start().catch(() => {
// expected to throw
});
2025-10-01 14:21:37 +02:00
2025-10-02 12:53:59 +02:00
let capturedState = capturedStates.pop();
2025-10-01 17:24:19 +02:00
expect(capturedState).toBeDefined();
expect(capturedState!.state).toEqual("FetchingConfig");
2025-10-01 14:21:37 +02:00
deferred.reject(new FailToGetOpenIdToken(new Error("Failed to get token")));
await vi.runAllTimersAsync();
2025-10-02 12:53:59 +02:00
capturedState = capturedStates.pop();
2025-10-01 17:24:19 +02:00
if (capturedState!.state === "FailedToStart") {
expect(capturedState!.error.message).toEqual("Something went wrong");
expect(capturedState!.transport.livekit_alias).toEqual(
2025-10-07 16:24:02 +02:00
livekitFocus.livekit_alias,
);
2025-10-01 14:21:37 +02:00
} else {
2025-10-07 16:24:02 +02:00
expect.fail(
"Expected FailedToStart state but got " + capturedState?.state,
);
2025-10-01 14:21:37 +02:00
}
});
it("fail to get JWT token and error state", async () => {
setupTest();
vi.useFakeTimers();
const opts: ConnectionOpts = {
client: client,
transport: livekitFocus,
2025-10-01 14:21:37 +02:00
scope: testScope,
2025-10-07 16:24:02 +02:00
livekitRoomFactory: () => fakeLivekitRoom,
2025-10-02 12:53:59 +02:00
};
2025-10-01 14:21:37 +02:00
const connection = new Connection(opts, undefined);
2025-10-01 14:21:37 +02:00
2025-10-28 21:18:47 +01:00
const capturedStates: ConnectionState[] = [];
const s = connection.state$.subscribe((value) => {
2025-10-01 17:24:19 +02:00
capturedStates.push(value);
2025-10-01 14:21:37 +02:00
});
onTestFinished(() => s.unsubscribe());
2025-10-01 14:21:37 +02:00
const deferredSFU = Promise.withResolvers<void>();
// mock the /sfu/get call
2025-10-07 16:24:02 +02:00
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, async () => {
await deferredSFU.promise;
return {
status: 500,
body: "Internal Server Error",
};
});
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
connection.start().catch(() => {
// expected to throw
});
2025-10-01 14:21:37 +02:00
2025-10-02 12:53:59 +02:00
let capturedState = capturedStates.pop();
expect(capturedState).toBeDefined();
2025-10-01 17:24:19 +02:00
expect(capturedState?.state).toEqual("FetchingConfig");
2025-10-01 14:21:37 +02:00
deferredSFU.resolve();
await vi.runAllTimersAsync();
2025-10-02 12:53:59 +02:00
capturedState = capturedStates.pop();
2025-10-01 17:24:19 +02:00
if (capturedState?.state === "FailedToStart") {
2025-10-07 16:24:02 +02:00
expect(capturedState?.error.message).toContain(
"SFU Config fetch failed with exception Error",
);
expect(capturedState?.transport.livekit_alias).toEqual(
2025-10-07 16:24:02 +02:00
livekitFocus.livekit_alias,
);
2025-10-01 14:21:37 +02:00
} else {
2025-10-07 16:24:02 +02:00
expect.fail(
"Expected FailedToStart state but got " + capturedState?.state,
);
2025-10-01 14:21:37 +02:00
}
});
it("fail to connect to livekit error state", async () => {
setupTest();
vi.useFakeTimers();
const opts: ConnectionOpts = {
client: client,
transport: livekitFocus,
2025-10-01 14:21:37 +02:00
scope: testScope,
2025-10-07 16:24:02 +02:00
livekitRoomFactory: () => fakeLivekitRoom,
2025-10-02 12:53:59 +02:00
};
2025-10-01 14:21:37 +02:00
const connection = new Connection(opts, undefined);
2025-10-01 14:21:37 +02:00
2025-10-28 21:18:47 +01:00
const capturedStates: ConnectionState[] = [];
const s = connection.state$.subscribe((value) => {
2025-10-02 12:53:59 +02:00
capturedStates.push(value);
2025-10-01 14:21:37 +02:00
});
onTestFinished(() => s.unsubscribe());
2025-10-01 14:21:37 +02:00
const deferredSFU = Promise.withResolvers<void>();
// mock the /sfu/get call
2025-10-07 16:24:02 +02:00
fetchMock.post(`${livekitFocus.livekit_service_url}/sfu/get`, () => {
return {
status: 200,
body: {
url: "wss://matrix-rtc.m.localhost/livekit/sfu",
jwt: "ATOKEN",
},
};
});
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
fakeLivekitRoom.connect.mockImplementation(async () => {
await deferredSFU.promise;
throw new Error("Failed to connect to livekit");
});
2025-10-01 14:21:37 +02:00
2025-10-07 16:24:02 +02:00
connection.start().catch(() => {
// expected to throw
});
2025-10-01 14:21:37 +02:00
2025-10-02 12:53:59 +02:00
let capturedState = capturedStates.pop();
expect(capturedState).toBeDefined();
2025-10-01 17:24:19 +02:00
expect(capturedState?.state).toEqual("FetchingConfig");
2025-10-01 14:21:37 +02:00
deferredSFU.resolve();
await vi.runAllTimersAsync();
2025-10-02 12:53:59 +02:00
capturedState = capturedStates.pop();
2025-10-01 17:24:19 +02:00
if (capturedState && capturedState?.state === "FailedToStart") {
2025-10-07 16:24:02 +02:00
expect(capturedState.error.message).toContain(
"Failed to connect to livekit",
);
expect(capturedState.transport.livekit_alias).toEqual(
2025-10-07 16:24:02 +02:00
livekitFocus.livekit_alias,
);
2025-10-01 14:21:37 +02:00
} else {
2025-10-07 16:24:02 +02:00
expect.fail(
"Expected FailedToStart state but got " + JSON.stringify(capturedState),
);
2025-10-01 14:21:37 +02:00
}
});
it("connection states happy path", async () => {
vi.useFakeTimers();
2025-10-02 12:53:59 +02:00
setupTest();
2025-10-01 14:21:37 +02:00
2025-10-01 14:47:45 +02:00
const connection = setupRemoteConnection();
2025-10-01 14:21:37 +02:00
2025-10-28 21:18:47 +01:00
const capturedStates: ConnectionState[] = [];
const s = connection.state$.subscribe((value) => {
capturedStates.push(value);
2025-10-01 14:21:37 +02:00
});
onTestFinished(() => s.unsubscribe());
2025-10-01 14:21:37 +02:00
await connection.start();
await vi.runAllTimersAsync();
const initialState = capturedStates.shift();
2025-10-01 14:21:37 +02:00
expect(initialState?.state).toEqual("Initialized");
const fetchingState = capturedStates.shift();
2025-10-01 14:21:37 +02:00
expect(fetchingState?.state).toEqual("FetchingConfig");
const connectingState = capturedStates.shift();
2025-10-01 14:21:37 +02:00
expect(connectingState?.state).toEqual("ConnectingToLkRoom");
const connectedState = capturedStates.shift();
2025-10-01 14:21:37 +02:00
expect(connectedState?.state).toEqual("ConnectedToLkRoom");
});
2025-10-01 16:39:21 +02:00
it("shutting down the scope should stop the connection", async () => {
2025-10-02 12:53:59 +02:00
setupTest();
2025-10-01 16:39:21 +02:00
vi.useFakeTimers();
const connection = setupRemoteConnection();
await connection.start();
const stopSpy = vi.spyOn(connection, "stop");
testScope.end();
expect(stopSpy).toHaveBeenCalled();
expect(fakeLivekitRoom.disconnect).toHaveBeenCalled();
});
});
2025-10-02 12:53:59 +02:00
function fakeRemoteLivekitParticipant(id: string): RemoteParticipant {
return {
2025-10-07 16:24:02 +02:00
identity: id,
} as unknown as RemoteParticipant;
2025-10-02 12:53:59 +02:00
}
function fakeRtcMemberShip(userId: string, deviceId: string): CallMembership {
return {
userId,
deviceId,
} as unknown as CallMembership;
2025-10-02 12:53:59 +02:00
}
describe("Publishing participants observations", () => {
it("should emit the list of publishing participants", async () => {
setupTest();
const connection = setupRemoteConnection();
const bobIsAPublisher = Promise.withResolvers<void>();
const danIsAPublisher = Promise.withResolvers<void>();
const observedPublishers: PublishingParticipant[][] = [];
const s = connection.participants$.subscribe((publishers) => {
2025-10-06 10:50:10 +02:00
observedPublishers.push(publishers);
if (publishers.some((p) => p.identity === "@bob:example.org:DEV111")) {
2025-10-06 10:50:10 +02:00
bobIsAPublisher.resolve();
}
if (publishers.some((p) => p.identity === "@dan:example.org:DEV333")) {
2025-10-06 10:50:10 +02:00
danIsAPublisher.resolve();
}
2025-10-02 12:53:59 +02:00
});
onTestFinished(() => s.unsubscribe());
2025-10-02 12:53:59 +02:00
// The publishingParticipants$ observable is derived from the current members of the
// livekitRoom and the rtc membership in order to publish the members that are publishing
// on this connection.
2025-10-06 10:50:10 +02:00
let participants: RemoteParticipant[] = [
2025-10-02 12:53:59 +02:00
fakeRemoteLivekitParticipant("@alice:example.org:DEV000"),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
fakeRemoteLivekitParticipant("@carol:example.org:DEV222"),
2025-10-07 16:24:02 +02:00
fakeRemoteLivekitParticipant("@dan:example.org:DEV333"),
2025-10-02 12:53:59 +02:00
];
// Let's simulate 3 members on the livekitRoom
2025-10-07 16:24:02 +02:00
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
);
2025-10-02 12:53:59 +02:00
for (const participant of participants) {
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
}
// At this point there should be no publishers
expect(observedPublishers.pop()!.length).toEqual(0);
const otherFocus: LivekitTransport = {
2025-10-02 12:53:59 +02:00
livekit_alias: "!roomID:example.org",
livekit_service_url: "https://other-matrix-rtc.example.org/livekit/jwt",
2025-10-07 16:24:02 +02:00
type: "livekit",
2025-10-06 10:50:10 +02:00
};
2025-10-02 12:53:59 +02:00
const rtcMemberships = [
// Say bob is on the same focus
2025-10-07 16:24:02 +02:00
{
membership: fakeRtcMemberShip("@bob:example.org", "DEV111"),
transport: livekitFocus,
},
2025-10-02 12:53:59 +02:00
// Alice and carol is on a different focus
2025-10-07 16:24:02 +02:00
{
membership: fakeRtcMemberShip("@alice:example.org", "DEV000"),
transport: otherFocus,
},
{
membership: fakeRtcMemberShip("@carol:example.org", "DEV222"),
transport: otherFocus,
},
2025-10-02 12:53:59 +02:00
// NO DAVE YET
];
// signal this change in rtc memberships
fakeMembershipsFocusMap$.next(rtcMemberships);
// We should have bob has a publisher now
await bobIsAPublisher.promise;
const publishers = observedPublishers.pop();
expect(publishers?.length).toEqual(1);
expect(publishers?.[0].identity).toEqual("@bob:example.org:DEV111");
2025-10-02 12:53:59 +02:00
// Now let's make dan join the rtc memberships
2025-10-07 16:24:02 +02:00
rtcMemberships.push({
membership: fakeRtcMemberShip("@dan:example.org", "DEV333"),
transport: livekitFocus,
});
2025-10-02 12:53:59 +02:00
fakeMembershipsFocusMap$.next(rtcMemberships);
// We should have bob and dan has publishers now
await danIsAPublisher.promise;
const twoPublishers = observedPublishers.pop();
expect(twoPublishers?.length).toEqual(2);
2025-10-07 16:24:02 +02:00
expect(
twoPublishers?.some((p) => p.identity === "@bob:example.org:DEV111"),
2025-10-07 16:24:02 +02:00
).toBeTruthy();
expect(
twoPublishers?.some((p) => p.identity === "@dan:example.org:DEV333"),
2025-10-07 16:24:02 +02:00
).toBeTruthy();
2025-10-02 12:53:59 +02:00
// Now let's make bob leave the livekit room
2025-10-07 16:24:02 +02:00
participants = participants.filter(
(p) => p.identity !== "@bob:example.org:DEV111",
);
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
);
fakeRoomEventEmiter.emit(
RoomEvent.ParticipantDisconnected,
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
);
2025-10-02 12:53:59 +02:00
// TODO: evaluate this test. It looks like this is not the task of the Connection anymore. Valere?
// const updatedPublishers = observedPublishers.pop();
// // Bob is not connected to the room but he is still in the rtc memberships declaring that
// // he is using that focus to publish, so he should still appear as a publisher
// expect(updatedPublishers?.length).toEqual(2);
// const pp = updatedPublishers?.find((p) =>
// p.identity.startsWith("@bob:example.org"),
// );
// expect(pp).toBeDefined();
// expect(pp!).not.toBeDefined();
// expect(
// updatedPublishers?.some(
// (p) => p.participant?.identity === "@dan:example.org:DEV333",
// ),
// ).toBeTruthy();
// // Now if bob is not in the rtc memberships, he should disappear
// const noBob = rtcMemberships.filter(
// ({ membership }) => membership.userId !== "@bob:example.org",
// );
// fakeMembershipsFocusMap$.next(noBob);
// expect(observedPublishers.pop()?.length).toEqual(1);
2025-10-06 10:50:10 +02:00
});
2025-10-02 12:53:59 +02:00
2025-10-07 16:00:59 +02:00
it("should be scoped to parent scope", (): void => {
setupTest();
const connection = setupRemoteConnection();
let observedPublishers: PublishingParticipant[][] = [];
const s = connection.participants$.subscribe((publishers) => {
observedPublishers.push(publishers);
});
onTestFinished(() => s.unsubscribe());
2025-10-06 10:50:10 +02:00
let participants: RemoteParticipant[] = [
2025-10-07 16:24:02 +02:00
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
];
// Let's simulate 3 members on the livekitRoom
2025-10-07 16:24:02 +02:00
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
);
for (const participant of participants) {
fakeRoomEventEmiter.emit(RoomEvent.ParticipantConnected, participant);
}
// At this point there should be no publishers
expect(observedPublishers.pop()!.length).toEqual(0);
const rtcMemberships = [
// Say bob is on the same focus
2025-10-07 16:24:02 +02:00
{
membership: fakeRtcMemberShip("@bob:example.org", "DEV111"),
transport: livekitFocus,
},
];
// signal this change in rtc memberships
fakeMembershipsFocusMap$.next(rtcMemberships);
// We should have bob has a publisher now
const publishers = observedPublishers.pop();
expect(publishers?.length).toEqual(1);
expect(publishers?.[0]?.identity).toEqual("@bob:example.org:DEV111");
// end the parent scope
testScope.end();
observedPublishers = [];
// SHOULD NOT emit any more publishers as the scope is ended
2025-10-07 16:24:02 +02:00
participants = participants.filter(
(p) => p.identity !== "@bob:example.org:DEV111",
);
vi.spyOn(fakeLivekitRoom, "remoteParticipants", "get").mockReturnValue(
new Map(participants.map((p) => [p.identity, p])),
);
fakeRoomEventEmiter.emit(
RoomEvent.ParticipantDisconnected,
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
);
expect(observedPublishers.length).toEqual(0);
2025-10-06 10:50:10 +02:00
});
});
//
// NOT USED ANYMORE ?
//
// This setup look like sth for the Publisher. Not a connection.
// describe("PublishConnection", () => {
// // let fakeBlurProcessor: ProcessorWrapper<BackgroundOptions>;
// let roomFactoryMock: Mock<() => LivekitRoom>;
// let muteStates: MockedObject<MuteStates>;
// function setUpPublishConnection(): void {
// setupTest();
// roomFactoryMock = vi.fn().mockReturnValue(fakeLivekitRoom);
// muteStates = mockMuteStates();
// // fakeBlurProcessor = vi.mocked<ProcessorWrapper<BackgroundOptions>>({
// // name: "BackgroundBlur",
// // restart: vi.fn().mockResolvedValue(undefined),
// // setOptions: vi.fn().mockResolvedValue(undefined),
// // getOptions: vi.fn().mockReturnValue({ strength: 0.5 }),
// // isRunning: vi.fn().mockReturnValue(false)
// // });
// }
// describe("Livekit room creation", () => {
// function createSetup(): void {
// setUpPublishConnection();
// const fakeTrackProcessorSubject$ = new BehaviorSubject<ProcessorState>({
// supported: true,
// processor: undefined,
// });
// const opts: ConnectionOpts = {
// client: client,
// transport: livekitFocus,
// scope: testScope,
// livekitRoomFactory: roomFactoryMock,
// };
// const audioInput = {
// available$: of(new Map([["mic1", { id: "mic1" }]])),
// selected$: new BehaviorSubject({ id: "mic1" }),
// select(): void {},
// };
// const videoInput = {
// available$: of(new Map([["cam1", { id: "cam1" }]])),
// selected$: new BehaviorSubject({ id: "cam1" }),
// select(): void {},
// };
// const audioOutput = {
// available$: of(new Map([["speaker", { id: "speaker" }]])),
// selected$: new BehaviorSubject({ id: "speaker" }),
// select(): void {},
// };
// // TODO understand what is wrong with our mocking that requires ts-expect-error
// const fakeDevices = mockMediaDevices({
// // @ts-expect-error Mocking only
// audioInput,
// // @ts-expect-error Mocking only
// videoInput,
// // @ts-expect-error Mocking only
// audioOutput,
// });
// new Connection(
// opts,
// fakeDevices,
// muteStates,
// undefined,
// fakeTrackProcessorSubject$,
// );
// }
// it("should create room with proper initial audio and video settings", () => {
// createSetup();
// expect(roomFactoryMock).toHaveBeenCalled();
// const lastCallArgs =
// roomFactoryMock.mock.calls[roomFactoryMock.mock.calls.length - 1];
// const roomOptions = lastCallArgs.pop() as unknown as RoomOptions;
// expect(roomOptions).toBeDefined();
// expect(roomOptions!.videoCaptureDefaults?.deviceId).toEqual("cam1");
// expect(roomOptions!.audioCaptureDefaults?.deviceId).toEqual("mic1");
// expect(roomOptions!.audioOutput?.deviceId).toEqual("speaker");
// });
// it("respect controlledAudioDevices", () => {
// // TODO: Refactor the code to make it testable.
// // The UrlParams module is a singleton has a cache and is very hard to test.
// // This breaks other tests as well if not handled properly.
// // vi.mock(import("./../UrlParams"), () => {
// // return {
// // getUrlParams: vi.fn().mockReturnValue({
// // controlledAudioDevices: true
// // })
// // };
// // });
// });
// });
// });