/* 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 { test, vi, expect, beforeEach, afterEach } from "vitest"; import { BehaviorSubject } from "rxjs"; import { type Room as LivekitRoom } from "livekit-client"; import EventEmitter from "events"; import fetchMock from "fetch-mock"; import { type LivekitTransportConfig } from "matrix-js-sdk/lib/matrixrtc"; import { logger } from "matrix-js-sdk/lib/logger"; import { type Epoch, ObservableScope, trackEpoch, } from "../../ObservableScope.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts"; import { mockMediaDevices, mockRtcMembership, ownMemberMock, withTestScheduler, } from "../../../utils/test.ts"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import { areLivekitTransportsEqual, createMatrixLivekitMembers$, type RemoteMatrixLivekitMember, } from "./MatrixLivekitMembers.ts"; import { createConnectionManager$ } from "./ConnectionManager.ts"; import { membershipsAndTransports$ } from "../../SessionBehaviors.ts"; import { constant } from "../../Behavior.ts"; import { testJWTToken } from "../../../utils/test-fixtures.ts"; // Test the integration of ConnectionManager and MatrixLivekitMerger let testScope: ObservableScope; let ecConnectionFactory: ECConnectionFactory; let mockClient: OpenIDClientParts; let lkRoomFactory: () => LivekitRoom; const createdMockLivekitRooms: Map = new Map(); beforeEach(() => { testScope = new ObservableScope(); mockClient = { getOpenIdToken: vi.fn().mockReturnValue(""), getDeviceId: vi.fn().mockReturnValue("DEV000"), }; lkRoomFactory = vi.fn().mockImplementation(() => { const emitter = new EventEmitter(); const base = { on: emitter.on.bind(emitter), off: emitter.off.bind(emitter), emit: emitter.emit.bind(emitter), disconnect: vi.fn(), remoteParticipants: new Map(), } as unknown as LivekitRoom; vi.mocked(base).connect = vi.fn().mockImplementation(({ url }) => { createdMockLivekitRooms.set(url, base); }); return base; }); ecConnectionFactory = new ECConnectionFactory( mockClient, "!roomid:example.org", mockMediaDevices({}), new BehaviorSubject({ supported: true, processor: undefined, }), undefined, false, lkRoomFactory, ); //TODO a bit annoying to have to do a http mock? fetchMock.post(`path:/sfu/get`, (url) => { const domain = new URL(url).hostname; // Extract the domain from the URL return { status: 200, body: { url: `wss://${domain}/livekit/sfu`, jwt: testJWTToken, }, }; }); }); afterEach(() => { testScope.end(); fetchMock.reset(); }); test("bob, carl, then bob joining no tracks yet", () => { withTestScheduler(({ expectObservable, behavior, scope }) => { const bobMembership = mockRtcMembership("@bob:example.com", "BDEV000"); const carlMembership = mockRtcMembership("@carl:example.com", "CDEV000"); const daveMembership = mockRtcMembership("@dave:foo.bar", "DDEV000"); const eMarble = "abc"; const vMarble = "abc"; const memberships$ = scope.behavior( behavior(eMarble, { a: [bobMembership], b: [bobMembership, carlMembership], c: [bobMembership, carlMembership, daveMembership], }).pipe(trackEpoch()), ); const membershipsAndTransports = membershipsAndTransports$( testScope, memberships$, ); const connectionManager = createConnectionManager$({ scope: testScope, connectionFactory: ecConnectionFactory, localTransport$: constant(null), remoteTransports$: membershipsAndTransports.transports$, logger: logger, ownMembershipIdentity: ownMemberMock, }); const matrixLivekitMembers$ = createMatrixLivekitMembers$({ scope: testScope, membershipsWithTransport$: membershipsAndTransports.membershipsWithTransport$, connectionManager, }); expectObservable(matrixLivekitMembers$).toBe(vMarble, { a: expect.toSatisfy((e: Epoch) => { const items = e.value; expect(items.length).toBe(1); const item = items[0]!; expectObservable(item.membership$).toBe("a", { a: bobMembership, }); expectObservable(item.connection$).toBe("a", { a: expect.toSatisfy((co) => areLivekitTransportsEqual( co.transport, bobMembership.transports[0]! as LivekitTransportConfig, ), ), }); expectObservable(item.participant.value$).toBe("a", { a: null, }); return true; }), b: expect.toSatisfy((e: Epoch) => { const items = e.value; expect(items.length).toBe(2); { const item = items[0]!; expectObservable(item.membership$).toBe("a", { a: bobMembership, }); expectObservable(item.participant.value$).toBe("a", { a: null, }); } { const item = items[1]!; expectObservable(item.membership$).toBe("a", { a: carlMembership, }); expectObservable(item.participant.value$).toBe("a", { a: null, }); expectObservable(item.connection$).toBe("a", { a: expect.toSatisfy((connection) => { expect( areLivekitTransportsEqual( connection.transport, carlMembership.transports[0]! as LivekitTransportConfig, ), ).toBe(true); return true; }), }); } return true; }), c: expect.toSatisfy((e: Epoch) => { const items = e.value; expect(items.length).toBe(3); expectObservable(items[0].membership$).toBe("a", { a: bobMembership, }); expectObservable(items[1].membership$).toBe("b", { a: carlMembership, }); { const item = items[2]!; expectObservable(item.membership$).toBe("a", { a: daveMembership, }); expectObservable(item.connection$).toBe("a", { a: expect.toSatisfy((connection) => { expect( areLivekitTransportsEqual( connection.transport, daveMembership.transports[0]! as LivekitTransportConfig, ), ).toBe(true); return true; }), }); expectObservable(item.participant.value$).toBe("a", { a: null, }); } return true; }), x: expect.anything(), }); }); });