mirror of
https://github.com/vector-im/element-call.git
synced 2026-01-18 02:32:27 +00:00
Very bit test overhaul. All displayname tests are now done in the
Metadata file. and not in the CallViewModel anymore.
This commit is contained in:
@@ -77,13 +77,13 @@ const leaveRTCSession = vi.hoisted(() =>
|
||||
),
|
||||
);
|
||||
|
||||
vi.mock("../rtcSessionHelpers", async (importOriginal) => {
|
||||
// TODO: perhaps there is a more elegant way to manage the type import here?
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
const orig = await importOriginal<typeof import("../rtcSessionHelpers")>();
|
||||
// TODO: leaveRTCSession no longer exists! Tests need adapting.
|
||||
return { ...orig, enterRTCSession, leaveRTCSession };
|
||||
});
|
||||
// vi.mock("../rtcSessionHelpers", async (importOriginal) => {
|
||||
// // TODO: perhaps there is a more elegant way to manage the type import here?
|
||||
// // eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
// const orig = await importOriginal<typeof import("../rtcSessionHelpers")>();
|
||||
// // TODO: leaveRTCSession no longer exists! Tests need adapting.
|
||||
// return { ...orig, enterRTCSession, leaveRTCSession };
|
||||
// });
|
||||
|
||||
let playSound: MockedFunction<
|
||||
NonNullable<ReturnType<typeof useAudioContext>>["playSound"]
|
||||
@@ -266,6 +266,7 @@ test.skip("GroupCallView leaves the session when an error occurs", async () => {
|
||||
|
||||
test.skip("GroupCallView shows errors that occur during joining", async () => {
|
||||
const user = userEvent.setup();
|
||||
// This should not mock this error that deep. it should only mock the CallViewModel.
|
||||
enterRTCSession.mockRejectedValue(new MatrixRTCTransportMissingError(""));
|
||||
onTestFinished(() => {
|
||||
enterRTCSession.mockReset();
|
||||
|
||||
@@ -71,8 +71,6 @@ import type { RaisedHandInfo, ReactionInfo } from "../../reactions/index.ts";
|
||||
import {
|
||||
alice,
|
||||
aliceDoppelganger,
|
||||
aliceDoppelgangerId,
|
||||
aliceDoppelgangerRtcMember,
|
||||
aliceId,
|
||||
aliceParticipant,
|
||||
aliceRtcMember,
|
||||
@@ -80,11 +78,7 @@ import {
|
||||
bobId,
|
||||
bobRtcMember,
|
||||
bobZeroWidthSpace,
|
||||
bobZeroWidthSpaceId,
|
||||
bobZeroWidthSpaceRtcMember,
|
||||
daveRTL,
|
||||
daveRTLId,
|
||||
daveRTLRtcMember,
|
||||
local,
|
||||
localId,
|
||||
localRtcMember,
|
||||
@@ -128,7 +122,7 @@ const yesNo = {
|
||||
const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD");
|
||||
|
||||
const carol = local;
|
||||
const carolId = localId;
|
||||
|
||||
const dave = mockMatrixRoomMember(daveRtcMember, { rawDisplayName: "Dave" });
|
||||
|
||||
const daveId = `${dave.userId}:${daveRtcMember.deviceId}`;
|
||||
@@ -437,7 +431,7 @@ test.skip("test missing RTC config error", async () => {
|
||||
},
|
||||
new BehaviorSubject({} as Record<string, RaisedHandInfo>),
|
||||
new BehaviorSubject({} as Record<string, ReactionInfo>),
|
||||
of({ processor: undefined, supported: false }),
|
||||
constant({ processor: undefined, supported: false }),
|
||||
);
|
||||
|
||||
const failPromise = Promise.withResolvers<ElementCallError>();
|
||||
@@ -1073,120 +1067,6 @@ it("should show at least one tile per MatrixRTCSession", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should disambiguate users with the same displayname", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const scenarioInputMarbles = "abcde";
|
||||
const expectedLayoutMarbles = "abcde";
|
||||
|
||||
withCallViewModel(
|
||||
{
|
||||
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||
a: [localRtcMember],
|
||||
b: [localRtcMember, aliceRtcMember],
|
||||
c: [localRtcMember, aliceRtcMember, aliceDoppelgangerRtcMember],
|
||||
d: [
|
||||
localRtcMember,
|
||||
aliceRtcMember,
|
||||
aliceDoppelgangerRtcMember,
|
||||
bobRtcMember,
|
||||
],
|
||||
e: [localRtcMember, aliceDoppelgangerRtcMember, bobRtcMember],
|
||||
}),
|
||||
},
|
||||
(vm) => {
|
||||
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
||||
// Carol has no displayname - So userId is used.
|
||||
a: new Map([[carolId, carol.userId]]),
|
||||
b: new Map([
|
||||
[carolId, carol.userId],
|
||||
[aliceId, alice.rawDisplayName],
|
||||
]),
|
||||
// The second alice joins.
|
||||
c: new Map([
|
||||
[carolId, carol.userId],
|
||||
[aliceId, "Alice (@alice:example.org)"],
|
||||
[aliceDoppelgangerId, "Alice (@alice2:example.org)"],
|
||||
]),
|
||||
// Bob also joins
|
||||
d: new Map([
|
||||
[carolId, carol.userId],
|
||||
[aliceId, "Alice (@alice:example.org)"],
|
||||
[aliceDoppelgangerId, "Alice (@alice2:example.org)"],
|
||||
[bobId, bob.rawDisplayName],
|
||||
]),
|
||||
// Alice leaves, and the displayname should reset.
|
||||
e: new Map([
|
||||
[carolId, carol.userId],
|
||||
[aliceDoppelgangerId, "Alice"],
|
||||
[bobId, bob.rawDisplayName],
|
||||
]),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("should disambiguate users with invisible characters", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const scenarioInputMarbles = "ab";
|
||||
const expectedLayoutMarbles = "ab";
|
||||
|
||||
withCallViewModel(
|
||||
{
|
||||
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||
a: [localRtcMember],
|
||||
b: [localRtcMember, bobRtcMember, bobZeroWidthSpaceRtcMember],
|
||||
}),
|
||||
},
|
||||
(vm) => {
|
||||
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
||||
// Carol has no displayname - So userId is used.
|
||||
a: new Map([[carolId, carol.userId]]),
|
||||
// Both Bobs join, and should handle zero width hacks.
|
||||
b: new Map([
|
||||
[carolId, carol.userId],
|
||||
[bobId, `Bob (${bob.userId})`],
|
||||
[
|
||||
bobZeroWidthSpaceId,
|
||||
`${bobZeroWidthSpace.rawDisplayName} (${bobZeroWidthSpace.userId})`,
|
||||
],
|
||||
]),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("should strip RTL characters from displayname", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const scenarioInputMarbles = "ab";
|
||||
const expectedLayoutMarbles = "ab";
|
||||
|
||||
withCallViewModel(
|
||||
{
|
||||
rtcMembers$: behavior(scenarioInputMarbles, {
|
||||
a: [localRtcMember],
|
||||
b: [localRtcMember, daveRtcMember, daveRTLRtcMember],
|
||||
}),
|
||||
},
|
||||
(vm) => {
|
||||
expectObservable(vm.memberDisplaynames$).toBe(expectedLayoutMarbles, {
|
||||
// Carol has no displayname - So userId is used.
|
||||
a: new Map([[carolId, carol.userId]]),
|
||||
// Both Dave's join. Since after stripping
|
||||
b: new Map([
|
||||
[carolId, carol.userId],
|
||||
// Not disambiguated
|
||||
[daveId, "Dave"],
|
||||
// This one is, since it's using RTL.
|
||||
[daveRTLId, `evaD (${daveRTL.userId})`],
|
||||
]),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should rank raised hands above video feeds and below speakers and presenters", () => {
|
||||
withTestScheduler(({ schedule, expectObservable }) => {
|
||||
// There should always be one tile for each MatrixRTCSession
|
||||
|
||||
@@ -5,14 +5,31 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import {
|
||||
type LivekitTransport,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
||||
import { mockConfig } from "./utils/test";
|
||||
import { MatrixRTCMode } from "./settings/settings";
|
||||
import { MatrixRTCMode } from "../../../settings/settings";
|
||||
import { mockConfig } from "../../../utils/test";
|
||||
import * as LocalMembership from "./LocalMembership";
|
||||
|
||||
// Read private function so we do not have to make it public
|
||||
const enterRTCSession = (
|
||||
LocalMembership as unknown as {
|
||||
enterRTCSession: (
|
||||
rtcSession: MatrixRTCSession,
|
||||
transport: LivekitTransport,
|
||||
{
|
||||
encryptMedia,
|
||||
matrixRTCMode,
|
||||
}: { encryptMedia: boolean; matrixRTCMode: MatrixRTCMode },
|
||||
) => Promise<void>;
|
||||
}
|
||||
).enterRTCSession;
|
||||
|
||||
const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
|
||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||
@@ -10,18 +10,16 @@ import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
type Mock,
|
||||
type MockedObject,
|
||||
onTestFinished,
|
||||
vi,
|
||||
} from "vitest";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import {
|
||||
type LocalParticipant,
|
||||
type RemoteParticipant,
|
||||
type Room as LivekitRoom,
|
||||
RoomEvent,
|
||||
type RoomOptions,
|
||||
ConnectionState as LivekitConnectionState,
|
||||
} from "livekit-client";
|
||||
import fetchMock from "fetch-mock";
|
||||
@@ -41,10 +39,6 @@ import {
|
||||
import { ObservableScope } from "../../ObservableScope.ts";
|
||||
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
|
||||
import { FailToGetOpenIdToken } from "../../../utils/errors.ts";
|
||||
import { mockMediaDevices, mockMuteStates } from "../../../utils/test.ts";
|
||||
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||
import { type MuteStates } from "../../MuteStates.ts";
|
||||
|
||||
let testScope: ObservableScope;
|
||||
|
||||
let client: MockedObject<OpenIDClientParts>;
|
||||
@@ -395,20 +389,12 @@ describe("Publishing participants observations", () => {
|
||||
const bobIsAPublisher = Promise.withResolvers<void>();
|
||||
const danIsAPublisher = Promise.withResolvers<void>();
|
||||
const observedPublishers: PublishingParticipant[][] = [];
|
||||
const s = connection.allLivekitParticipants$.subscribe((publishers) => {
|
||||
const s = connection.participants$.subscribe((publishers) => {
|
||||
observedPublishers.push(publishers);
|
||||
if (
|
||||
publishers.some(
|
||||
(p) => p.participant?.identity === "@bob:example.org:DEV111",
|
||||
)
|
||||
) {
|
||||
if (publishers.some((p) => p.identity === "@bob:example.org:DEV111")) {
|
||||
bobIsAPublisher.resolve();
|
||||
}
|
||||
if (
|
||||
publishers.some(
|
||||
(p) => p.participant?.identity === "@dan:example.org:DEV333",
|
||||
)
|
||||
) {
|
||||
if (publishers.some((p) => p.identity === "@dan:example.org:DEV333")) {
|
||||
danIsAPublisher.resolve();
|
||||
}
|
||||
});
|
||||
@@ -466,9 +452,7 @@ describe("Publishing participants observations", () => {
|
||||
await bobIsAPublisher.promise;
|
||||
const publishers = observedPublishers.pop();
|
||||
expect(publishers?.length).toEqual(1);
|
||||
expect(publishers?.[0].participant?.identity).toEqual(
|
||||
"@bob:example.org:DEV111",
|
||||
);
|
||||
expect(publishers?.[0].identity).toEqual("@bob:example.org:DEV111");
|
||||
|
||||
// Now let's make dan join the rtc memberships
|
||||
rtcMemberships.push({
|
||||
@@ -482,14 +466,10 @@ describe("Publishing participants observations", () => {
|
||||
const twoPublishers = observedPublishers.pop();
|
||||
expect(twoPublishers?.length).toEqual(2);
|
||||
expect(
|
||||
twoPublishers?.some(
|
||||
(p) => p.participant?.identity === "@bob:example.org:DEV111",
|
||||
),
|
||||
twoPublishers?.some((p) => p.identity === "@bob:example.org:DEV111"),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
twoPublishers?.some(
|
||||
(p) => p.participant?.identity === "@dan:example.org:DEV333",
|
||||
),
|
||||
twoPublishers?.some((p) => p.identity === "@dan:example.org:DEV333"),
|
||||
).toBeTruthy();
|
||||
|
||||
// Now let's make bob leave the livekit room
|
||||
@@ -504,26 +484,27 @@ describe("Publishing participants observations", () => {
|
||||
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"),
|
||||
);
|
||||
|
||||
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.membership.userId == "@bob:example.org",
|
||||
);
|
||||
expect(pp).toBeDefined();
|
||||
expect(pp!.participant).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);
|
||||
// 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);
|
||||
});
|
||||
|
||||
it("should be scoped to parent scope", (): void => {
|
||||
@@ -532,7 +513,7 @@ describe("Publishing participants observations", () => {
|
||||
const connection = setupRemoteConnection();
|
||||
|
||||
let observedPublishers: PublishingParticipant[][] = [];
|
||||
const s = connection.allLivekitParticipants$.subscribe((publishers) => {
|
||||
const s = connection.participants$.subscribe((publishers) => {
|
||||
observedPublishers.push(publishers);
|
||||
});
|
||||
onTestFinished(() => s.unsubscribe());
|
||||
@@ -566,9 +547,7 @@ describe("Publishing participants observations", () => {
|
||||
// We should have bob has a publisher now
|
||||
const publishers = observedPublishers.pop();
|
||||
expect(publishers?.length).toEqual(1);
|
||||
expect(publishers?.[0].participant?.identity).toEqual(
|
||||
"@bob:example.org:DEV111",
|
||||
);
|
||||
expect(publishers?.[0]?.identity).toEqual("@bob:example.org:DEV111");
|
||||
|
||||
// end the parent scope
|
||||
testScope.end();
|
||||
@@ -590,108 +569,112 @@ describe("Publishing participants observations", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PublishConnection", () => {
|
||||
// let fakeBlurProcessor: ProcessorWrapper<BackgroundOptions>;
|
||||
let roomFactoryMock: Mock<() => LivekitRoom>;
|
||||
let muteStates: MockedObject<MuteStates>;
|
||||
//
|
||||
// NOT USED ANYMORE ?
|
||||
//
|
||||
// This setup look like sth for the Publisher. Not a connection.
|
||||
|
||||
function setUpPublishConnection(): void {
|
||||
setupTest();
|
||||
// describe("PublishConnection", () => {
|
||||
// // let fakeBlurProcessor: ProcessorWrapper<BackgroundOptions>;
|
||||
// let roomFactoryMock: Mock<() => LivekitRoom>;
|
||||
// let muteStates: MockedObject<MuteStates>;
|
||||
|
||||
roomFactoryMock = vi.fn().mockReturnValue(fakeLivekitRoom);
|
||||
// function setUpPublishConnection(): void {
|
||||
// setupTest();
|
||||
|
||||
muteStates = mockMuteStates();
|
||||
// roomFactoryMock = vi.fn().mockReturnValue(fakeLivekitRoom);
|
||||
|
||||
// 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)
|
||||
// });
|
||||
}
|
||||
// muteStates = mockMuteStates();
|
||||
|
||||
describe("Livekit room creation", () => {
|
||||
function createSetup(): void {
|
||||
setUpPublishConnection();
|
||||
// // 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)
|
||||
// // });
|
||||
// }
|
||||
|
||||
const fakeTrackProcessorSubject$ = new BehaviorSubject<ProcessorState>({
|
||||
supported: true,
|
||||
processor: undefined,
|
||||
});
|
||||
// describe("Livekit room creation", () => {
|
||||
// function createSetup(): void {
|
||||
// setUpPublishConnection();
|
||||
|
||||
const opts: ConnectionOpts = {
|
||||
client: client,
|
||||
transport: livekitFocus,
|
||||
remoteTransports$: fakeMembershipsFocusMap$,
|
||||
scope: testScope,
|
||||
livekitRoomFactory: roomFactoryMock,
|
||||
};
|
||||
// const fakeTrackProcessorSubject$ = new BehaviorSubject<ProcessorState>({
|
||||
// supported: true,
|
||||
// processor: undefined,
|
||||
// });
|
||||
|
||||
const audioInput = {
|
||||
available$: of(new Map([["mic1", { id: "mic1" }]])),
|
||||
selected$: new BehaviorSubject({ id: "mic1" }),
|
||||
select(): void {},
|
||||
};
|
||||
// const opts: ConnectionOpts = {
|
||||
// client: client,
|
||||
// transport: livekitFocus,
|
||||
// scope: testScope,
|
||||
// livekitRoomFactory: roomFactoryMock,
|
||||
// };
|
||||
|
||||
const videoInput = {
|
||||
available$: of(new Map([["cam1", { id: "cam1" }]])),
|
||||
selected$: new BehaviorSubject({ id: "cam1" }),
|
||||
select(): void {},
|
||||
};
|
||||
// const audioInput = {
|
||||
// available$: of(new Map([["mic1", { id: "mic1" }]])),
|
||||
// selected$: new BehaviorSubject({ id: "mic1" }),
|
||||
// select(): void {},
|
||||
// };
|
||||
|
||||
const audioOutput = {
|
||||
available$: of(new Map([["speaker", { id: "speaker" }]])),
|
||||
selected$: new BehaviorSubject({ id: "speaker" }),
|
||||
select(): void {},
|
||||
};
|
||||
// const videoInput = {
|
||||
// available$: of(new Map([["cam1", { id: "cam1" }]])),
|
||||
// selected$: new BehaviorSubject({ id: "cam1" }),
|
||||
// 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,
|
||||
});
|
||||
// const audioOutput = {
|
||||
// available$: of(new Map([["speaker", { id: "speaker" }]])),
|
||||
// selected$: new BehaviorSubject({ id: "speaker" }),
|
||||
// select(): void {},
|
||||
// };
|
||||
|
||||
new PublishConnection(
|
||||
opts,
|
||||
fakeDevices,
|
||||
muteStates,
|
||||
undefined,
|
||||
fakeTrackProcessorSubject$,
|
||||
);
|
||||
}
|
||||
// // 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,
|
||||
// });
|
||||
|
||||
it("should create room with proper initial audio and video settings", () => {
|
||||
createSetup();
|
||||
// new Connection(
|
||||
// opts,
|
||||
// fakeDevices,
|
||||
// muteStates,
|
||||
// undefined,
|
||||
// fakeTrackProcessorSubject$,
|
||||
// );
|
||||
// }
|
||||
|
||||
expect(roomFactoryMock).toHaveBeenCalled();
|
||||
// it("should create room with proper initial audio and video settings", () => {
|
||||
// createSetup();
|
||||
|
||||
const lastCallArgs =
|
||||
roomFactoryMock.mock.calls[roomFactoryMock.mock.calls.length - 1];
|
||||
// expect(roomFactoryMock).toHaveBeenCalled();
|
||||
|
||||
const roomOptions = lastCallArgs.pop() as unknown as RoomOptions;
|
||||
expect(roomOptions).toBeDefined();
|
||||
// const lastCallArgs =
|
||||
// roomFactoryMock.mock.calls[roomFactoryMock.mock.calls.length - 1];
|
||||
|
||||
expect(roomOptions!.videoCaptureDefaults?.deviceId).toEqual("cam1");
|
||||
expect(roomOptions!.audioCaptureDefaults?.deviceId).toEqual("mic1");
|
||||
expect(roomOptions!.audioOutput?.deviceId).toEqual("speaker");
|
||||
});
|
||||
// const roomOptions = lastCallArgs.pop() as unknown as RoomOptions;
|
||||
// expect(roomOptions).toBeDefined();
|
||||
|
||||
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
|
||||
// })
|
||||
// };
|
||||
// });
|
||||
});
|
||||
});
|
||||
});
|
||||
// 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
|
||||
// // })
|
||||
// // };
|
||||
// // });
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -5,12 +5,11 @@ 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 { describe, test, 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 { combineLatest, map, type Observable } from "rxjs";
|
||||
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
import { type Connection } from "./Connection.ts";
|
||||
|
||||
let testScope: ObservableScope;
|
||||
let mockMatrixRoom: MatrixRoom;
|
||||
|
||||
const transportA: LivekitTransport = {
|
||||
type: "livekit",
|
||||
@@ -61,17 +59,6 @@ const carlMembership = mockCallMembership(
|
||||
|
||||
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(() => {
|
||||
@@ -110,7 +97,6 @@ test("should signal participant not yet connected to livekit", () => {
|
||||
connectionManager: {
|
||||
connectionManagerData$: connectionManagerData$,
|
||||
} as unknown as IConnectionManager,
|
||||
matrixRoom: mockMatrixRoom,
|
||||
});
|
||||
|
||||
expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", {
|
||||
@@ -191,7 +177,6 @@ test("should signal participant on a connection that is publishing", () => {
|
||||
connectionManager: {
|
||||
connectionManagerData$: connectionManagerData$,
|
||||
} as unknown as IConnectionManager,
|
||||
matrixRoom: mockMatrixRoom,
|
||||
});
|
||||
|
||||
expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", {
|
||||
@@ -243,7 +228,6 @@ test("should signal participant on a connection that is not publishing", () => {
|
||||
connectionManager: {
|
||||
connectionManagerData$: connectionManagerData$,
|
||||
} as unknown as IConnectionManager,
|
||||
matrixRoom: mockMatrixRoom,
|
||||
});
|
||||
|
||||
expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", {
|
||||
@@ -307,7 +291,6 @@ describe("Publication edge case", () => {
|
||||
connectionManager: {
|
||||
connectionManagerData$: connectionManagerData$,
|
||||
} as unknown as IConnectionManager,
|
||||
matrixRoom: mockMatrixRoom,
|
||||
});
|
||||
|
||||
expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe(
|
||||
@@ -374,7 +357,6 @@ describe("Publication edge case", () => {
|
||||
connectionManager: {
|
||||
connectionManagerData$: connectionManagerData$,
|
||||
} as unknown as IConnectionManager,
|
||||
matrixRoom: mockMatrixRoom,
|
||||
});
|
||||
|
||||
expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe(
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
/*
|
||||
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 { afterEach, beforeEach, describe, vi } from "vitest";
|
||||
import {
|
||||
type MatrixEvent,
|
||||
type RoomMember,
|
||||
type RoomState,
|
||||
RoomStateEvent,
|
||||
} from "matrix-js-sdk";
|
||||
import EventEmitter from "events";
|
||||
import { it } from "vitest";
|
||||
|
||||
import { ObservableScope } from "../../ObservableScope.ts";
|
||||
import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room";
|
||||
import {
|
||||
mockCallMembership,
|
||||
mockMatrixRoomMember,
|
||||
withTestScheduler,
|
||||
} from "../../../utils/test.ts";
|
||||
import {
|
||||
createMatrixMemberMetadata$,
|
||||
createRoomMembers$,
|
||||
} from "./MatrixMemberMetadata.ts";
|
||||
let testScope: ObservableScope;
|
||||
let mockMatrixRoom: MatrixRoom;
|
||||
|
||||
describe("MatrixMemberMetadata", () => {
|
||||
/*
|
||||
* To be populated in the test setup.
|
||||
* Maps userId to a partial/mock RoomMember object.
|
||||
*/
|
||||
let fakeMembersMap: Map<string, Partial<RoomMember>>;
|
||||
|
||||
beforeEach(() => {
|
||||
testScope = new ObservableScope();
|
||||
fakeMembersMap = new Map<string, Partial<RoomMember>>();
|
||||
|
||||
const roomEmitter = new EventEmitter();
|
||||
mockMatrixRoom = {
|
||||
on: roomEmitter.on.bind(roomEmitter),
|
||||
off: roomEmitter.off.bind(roomEmitter),
|
||||
emit: roomEmitter.emit.bind(roomEmitter),
|
||||
// addListener: roomEmitter.addListener.bind(roomEmitter),
|
||||
// removeListener: roomEmitter.removeListener.bind(roomEmitter),
|
||||
getMember: vi.fn().mockImplementation((userId: string) => {
|
||||
const member = fakeMembersMap.get(userId);
|
||||
if (member) {
|
||||
return member as RoomMember;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
getMembers: vi.fn().mockImplementation(() => {
|
||||
const members = Array.from(fakeMembersMap.values());
|
||||
return members;
|
||||
}),
|
||||
} as unknown as MatrixRoom;
|
||||
});
|
||||
|
||||
function fakeMemberWith(data: Partial<RoomMember>): void {
|
||||
const userId = data.userId || "@alice:example.com";
|
||||
const member: Partial<RoomMember> = {
|
||||
userId: userId,
|
||||
rawDisplayName: data.rawDisplayName ?? userId,
|
||||
getMxcAvatarUrl:
|
||||
data.getMxcAvatarUrl ||
|
||||
vi.fn().mockImplementation(() => {
|
||||
return `mxc://example.com/${userId}`;
|
||||
}),
|
||||
...data,
|
||||
} as unknown as RoomMember;
|
||||
fakeMembersMap.set(userId, member);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
fakeMembersMap.clear();
|
||||
});
|
||||
|
||||
describe("displayname", () => {
|
||||
function updateDisplayName(
|
||||
userId: `@${string}:${string}`,
|
||||
newDisplayName: string,
|
||||
): void {
|
||||
const member = fakeMembersMap.get(userId);
|
||||
if (member) {
|
||||
member.rawDisplayName = newDisplayName;
|
||||
// Emit the event to notify listeners
|
||||
mockMatrixRoom.emit(
|
||||
RoomStateEvent.Members,
|
||||
{} as unknown as MatrixEvent,
|
||||
{} as unknown as RoomState,
|
||||
member as RoomMember,
|
||||
);
|
||||
} else {
|
||||
throw new Error(`No member found with userId: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this is a regression, now there the own user is not always in the map. Ask Timo if fine
|
||||
it("should show our own user if present in rtc session and room", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
fakeMemberWith({
|
||||
userId: "@local:example.com",
|
||||
rawDisplayName: "it's a me",
|
||||
});
|
||||
const memberships$ = behavior("a", {
|
||||
a: [mockCallMembership("@local:example.com", "DEVICE1")],
|
||||
});
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
const dn$ =
|
||||
metadataStore.createDisplayNameBehavior$("@local:example.com");
|
||||
|
||||
expectObservable(dn$).toBe("a", {
|
||||
a: "it's a me",
|
||||
});
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("a", {
|
||||
a: new Map<string, string>([["@local:example.com", "it's a me"]]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setUpBasicRoom(): void {
|
||||
fakeMemberWith({
|
||||
userId: "@local:example.com",
|
||||
rawDisplayName: "it's a me",
|
||||
});
|
||||
fakeMemberWith({ userId: "@alice:example.com", rawDisplayName: "Alice" });
|
||||
fakeMemberWith({ userId: "@bob:example.com", rawDisplayName: "Bob" });
|
||||
fakeMemberWith({ userId: "@carl:example.com", rawDisplayName: "Carl" });
|
||||
fakeMemberWith({ userId: "@evil:example.com", rawDisplayName: "Carl" });
|
||||
fakeMemberWith({ userId: "@bob:foo.bar", rawDisplayName: "Bob" });
|
||||
fakeMemberWith({ userId: "@no-name:foo.bar" });
|
||||
}
|
||||
|
||||
it("should get displayName for users", () => {
|
||||
setUpBasicRoom();
|
||||
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const memberships$ = behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@alice:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
],
|
||||
});
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
const aliceDispName$ =
|
||||
metadataStore.createDisplayNameBehavior$("@alice:example.com");
|
||||
|
||||
expectObservable(aliceDispName$).toBe("a", {
|
||||
a: "Alice",
|
||||
});
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
["@alice:example.com", "Alice"],
|
||||
["@bob:example.com", "Bob"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should use userId if no display name", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const memberships$ = behavior("a", {
|
||||
a: [mockCallMembership("@no-name:foo.bar", "D000")],
|
||||
});
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
["@no-name:foo.bar", "@no-name:foo.bar"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should disambiguate users with same display name", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const memberships$ = behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:example.com", "DEVICE2"),
|
||||
mockCallMembership("@bob:foo.bar", "BOB000"),
|
||||
mockCallMembership("@carl:example.com", "C000"),
|
||||
mockCallMembership("@evil:example.com", "E000"),
|
||||
],
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:foo.bar", "Bob (@bob:foo.bar)"],
|
||||
["@carl:example.com", "Carl (@carl:example.com)"],
|
||||
["@evil:example.com", "Carl (@evil:example.com)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should start to disambiguate reactivly when needed", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const memberships$ = behavior("ab", {
|
||||
a: [mockCallMembership("@bob:example.com", "DEVICE1")],
|
||||
b: [
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:foo.bar", "BOB000"),
|
||||
],
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("ab", {
|
||||
a: new Map<string, string>([["@bob:example.com", "Bob"]]),
|
||||
b: new Map<string, string>([
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:foo.bar", "Bob (@bob:foo.bar)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should keep disambiguated name when other leave", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const memberships$ = behavior("ab", {
|
||||
a: [
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:foo.bar", "BOB000"),
|
||||
],
|
||||
b: [mockCallMembership("@bob:example.com", "DEVICE1")],
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("ab", {
|
||||
a: new Map<string, string>([
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:foo.bar", "Bob (@bob:foo.bar)"],
|
||||
]),
|
||||
b: new Map<string, string>([
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should disambiguate on name change", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const memberships$ = behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@bob:example.com", "B000"),
|
||||
mockCallMembership("@carl:example.com", "C000"),
|
||||
],
|
||||
});
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
schedule("-a", {
|
||||
a: () => {
|
||||
updateDisplayName("@carl:example.com", "Bob");
|
||||
},
|
||||
});
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("ab", {
|
||||
a: new Map<string, string>([
|
||||
["@bob:example.com", "Bob"],
|
||||
["@carl:example.com", "Carl"],
|
||||
]),
|
||||
b: new Map<string, string>([
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@carl:example.com", "Bob (@carl:example.com)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should track individual member id with createDisplayNameBehavior", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
const BOB = "@bob:example.com";
|
||||
const CARL = "@carl:example.com";
|
||||
// for this test we build a mock environment that does all possible changes:
|
||||
// - memberships join/leave
|
||||
// - room join/leave
|
||||
// - disambiguate
|
||||
const memberships$ = behavior("ab-d", {
|
||||
a: [mockCallMembership(CARL, "C000")],
|
||||
b: [
|
||||
mockCallMembership(CARL, "C000"),
|
||||
// bob joins
|
||||
mockCallMembership(BOB, "B000"),
|
||||
],
|
||||
// c carl gets renamed to BOB
|
||||
d: [
|
||||
// carl leaves
|
||||
mockCallMembership(BOB, "B000"),
|
||||
],
|
||||
});
|
||||
schedule("--a-", {
|
||||
a: () => {
|
||||
// carl renames
|
||||
updateDisplayName(CARL, "Bob");
|
||||
},
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
const bob$ = metadataStore.createDisplayNameBehavior$(BOB);
|
||||
const carl$ = metadataStore.createDisplayNameBehavior$(CARL);
|
||||
|
||||
expectObservable(bob$).toBe("abc-", {
|
||||
a: undefined,
|
||||
b: "Bob",
|
||||
c: "Bob (@bob:example.com)",
|
||||
// bob stays disambiguate even though carl left
|
||||
// d: "Bob (@bob:example.com)",
|
||||
});
|
||||
|
||||
expectObservable(carl$).toBe("a-cd", {
|
||||
a: "Carl",
|
||||
// b: "Carl",
|
||||
// carl gets renamed and disambiguate
|
||||
c: "Bob (@carl:example.com)",
|
||||
d: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should disambiguate users with invisible characters", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const bobRtcMember = mockCallMembership("@bob:example.org", "BBBB");
|
||||
const bobZeroWidthSpaceRtcMember = mockCallMembership(
|
||||
"@bob2:example.org",
|
||||
"BBBB",
|
||||
);
|
||||
const bob = mockMatrixRoomMember(bobRtcMember, {
|
||||
rawDisplayName: "Bob",
|
||||
});
|
||||
const bobZeroWidthSpace = mockMatrixRoomMember(
|
||||
bobZeroWidthSpaceRtcMember,
|
||||
{
|
||||
rawDisplayName: "Bo\u200bb",
|
||||
},
|
||||
);
|
||||
fakeMemberWith(bob);
|
||||
fakeMemberWith(bobZeroWidthSpace);
|
||||
fakeMemberWith({ userId: "@carol:example.org" });
|
||||
const memberships$ = behavior("ab", {
|
||||
a: [mockCallMembership("@carol:example.org", "1111"), bobRtcMember],
|
||||
b: [
|
||||
mockCallMembership("@carol:example.org", "1111"),
|
||||
bobRtcMember,
|
||||
bobZeroWidthSpaceRtcMember,
|
||||
],
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
const bob$ =
|
||||
metadataStore.createDisplayNameBehavior$("@bob:example.org");
|
||||
const bob2$ =
|
||||
metadataStore.createDisplayNameBehavior$("@bob2:example.org");
|
||||
const carol$ =
|
||||
metadataStore.createDisplayNameBehavior$("@carol:example.org");
|
||||
expectObservable(bob$).toBe("ab", {
|
||||
a: "Bob",
|
||||
b: "Bob (@bob:example.org)",
|
||||
});
|
||||
expectObservable(bob2$).toBe("ab", {
|
||||
a: undefined,
|
||||
b: "Bo\u200bb (@bob2:example.org)",
|
||||
});
|
||||
expectObservable(carol$).toBe("a-", {
|
||||
a: "@carol:example.org",
|
||||
});
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("ab", {
|
||||
// Carol has no displayname - So userId is used.
|
||||
a: new Map([
|
||||
["@carol:example.org", "@carol:example.org"],
|
||||
["@bob:example.org", "Bob"],
|
||||
]),
|
||||
// Other Bob joins, and should handle zero width hacks.
|
||||
b: new Map([
|
||||
["@carol:example.org", "@carol:example.org"],
|
||||
[bobRtcMember.userId, `Bob (@bob:example.org)`],
|
||||
[
|
||||
bobZeroWidthSpace.userId,
|
||||
`${bobZeroWidthSpace.rawDisplayName} (${bobZeroWidthSpace.userId})`,
|
||||
],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should strip RTL characters from displayname", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const daveRtcMember = mockCallMembership("@dave:example.org", "DDDD");
|
||||
const daveRTLRtcMember = mockCallMembership(
|
||||
"@dave2:example.org",
|
||||
"DDDD",
|
||||
);
|
||||
const dave = mockMatrixRoomMember(daveRtcMember, {
|
||||
rawDisplayName: "Dave",
|
||||
});
|
||||
const daveRTL = mockMatrixRoomMember(daveRTLRtcMember, {
|
||||
rawDisplayName: "\u202eevaD",
|
||||
});
|
||||
|
||||
fakeMemberWith({ userId: "@carol:example.org" });
|
||||
fakeMemberWith(daveRTL);
|
||||
fakeMemberWith(dave);
|
||||
const memberships$ = behavior("ab", {
|
||||
a: [mockCallMembership("@carol:example.org", "DDDD")],
|
||||
b: [
|
||||
mockCallMembership("@carol:example.org", "DDDD"),
|
||||
daveRtcMember,
|
||||
daveRTLRtcMember,
|
||||
],
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
expectObservable(metadataStore.displaynameMap$).toBe("ab", {
|
||||
// Carol has no displayname - So userId is used.
|
||||
a: new Map([["@carol:example.org", "@carol:example.org"]]),
|
||||
// Both Dave's join. Since after stripping
|
||||
b: new Map([
|
||||
["@carol:example.org", "@carol:example.org"],
|
||||
// Not disambiguated
|
||||
["@dave:example.org", "Dave"],
|
||||
// This one is, since it's using RTL.
|
||||
["@dave2:example.org", "evaD (@dave2:example.org)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("avatarUrl", () => {
|
||||
function updateAvatarUrl(
|
||||
userId: `@${string}:${string}`,
|
||||
avatarUrl: string,
|
||||
): void {
|
||||
const member = fakeMembersMap.get(userId);
|
||||
if (member) {
|
||||
member.getMxcAvatarUrl = vi.fn().mockReturnValue(avatarUrl);
|
||||
// Emit the event to notify listeners
|
||||
mockMatrixRoom.emit(
|
||||
RoomStateEvent.Members,
|
||||
{} as unknown as MatrixEvent,
|
||||
{} as unknown as RoomState,
|
||||
member as RoomMember,
|
||||
);
|
||||
} else {
|
||||
throw new Error(`No member found with userId: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this is a regression, now there the own user is not always in the map. Ask Timo if fine
|
||||
it("should use avatar url from room members", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
fakeMemberWith({
|
||||
userId: "@local:example.com",
|
||||
});
|
||||
fakeMemberWith({
|
||||
userId: "@alice:example.com",
|
||||
getMxcAvatarUrl: vi.fn().mockReturnValue("mxc://custom.url/avatar"),
|
||||
});
|
||||
const memberships$ = behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@local:example.com", "DEVICE1"),
|
||||
mockCallMembership("@alice:example.com", "DEVICE1"),
|
||||
],
|
||||
});
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
const local$ =
|
||||
metadataStore.createAvatarUrlBehavior$("@local:example.com");
|
||||
|
||||
const alice$ =
|
||||
metadataStore.createAvatarUrlBehavior$("@alice:example.com");
|
||||
|
||||
expectObservable(local$).toBe("a", {
|
||||
a: "mxc://example.com/@local:example.com",
|
||||
});
|
||||
expectObservable(alice$).toBe("a", {
|
||||
a: "mxc://custom.url/avatar",
|
||||
});
|
||||
expectObservable(metadataStore.avatarMap$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
["@local:example.com", "mxc://example.com/@local:example.com"],
|
||||
["@alice:example.com", "mxc://custom.url/avatar"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should update on avatar change and user join/leave", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
fakeMemberWith({ userId: "@carl:example.com" });
|
||||
fakeMemberWith({ userId: "@bob:example.com" });
|
||||
const memberships$ = behavior("ab-d", {
|
||||
a: [mockCallMembership("@bob:example.com", "B000")],
|
||||
b: [
|
||||
mockCallMembership("@bob:example.com", "B000"),
|
||||
mockCallMembership("@carl:example.com", "C000"),
|
||||
],
|
||||
d: [mockCallMembership("@carl:example.com", "C000")],
|
||||
});
|
||||
|
||||
const metadataStore = createMatrixMemberMetadata$(
|
||||
testScope,
|
||||
memberships$,
|
||||
createRoomMembers$(testScope, mockMatrixRoom),
|
||||
);
|
||||
|
||||
schedule("--c-", {
|
||||
c: () => {
|
||||
updateAvatarUrl(
|
||||
"@carl:example.com",
|
||||
"mxc://updated.me/updatedAvatar",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const bob$ = metadataStore.createAvatarUrlBehavior$("@bob:example.com");
|
||||
const carl$ =
|
||||
metadataStore.createAvatarUrlBehavior$("@carl:example.com");
|
||||
expectObservable(bob$).toBe("a---", {
|
||||
a: "mxc://example.com/@bob:example.com",
|
||||
});
|
||||
expectObservable(carl$).toBe("a-c-", {
|
||||
a: "mxc://example.com/@carl:example.com",
|
||||
|
||||
c: "mxc://updated.me/updatedAvatar",
|
||||
});
|
||||
expectObservable(metadataStore.avatarMap$).toBe("a-c-", {
|
||||
a: new Map<string, string>([
|
||||
["@bob:example.com", "mxc://example.com/@bob:example.com"],
|
||||
["@carl:example.com", "mxc://example.com/@carl:example.com"],
|
||||
]),
|
||||
// expect an update once we update the avatar URL
|
||||
c: new Map<string, string>([
|
||||
["@bob:example.com", "mxc://example.com/@bob:example.com"],
|
||||
["@carl:example.com", "mxc://updated.me/updatedAvatar"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,286 +0,0 @@
|
||||
/*
|
||||
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 { afterEach, beforeEach, test, vi } from "vitest";
|
||||
import {
|
||||
type MatrixEvent,
|
||||
type RoomMember,
|
||||
type RoomState,
|
||||
RoomStateEvent,
|
||||
} from "matrix-js-sdk";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { ObservableScope } from "../../ObservableScope.ts";
|
||||
import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room";
|
||||
import { mockCallMembership, withTestScheduler } from "../../../utils/test.ts";
|
||||
import { memberDisplaynames$ } from "./displayname.ts";
|
||||
|
||||
let testScope: ObservableScope;
|
||||
let mockMatrixRoom: MatrixRoom;
|
||||
|
||||
/*
|
||||
* To be populated in the test setup.
|
||||
* Maps userId to a partial/mock RoomMember object.
|
||||
*/
|
||||
let fakeMembersMap: Map<string, Partial<RoomMember>>;
|
||||
|
||||
beforeEach(() => {
|
||||
testScope = new ObservableScope();
|
||||
fakeMembersMap = new Map<string, Partial<RoomMember>>();
|
||||
|
||||
const roomEmitter = new EventEmitter();
|
||||
mockMatrixRoom = {
|
||||
on: roomEmitter.on.bind(roomEmitter),
|
||||
off: roomEmitter.off.bind(roomEmitter),
|
||||
emit: roomEmitter.emit.bind(roomEmitter),
|
||||
// addListener: roomEmitter.addListener.bind(roomEmitter),
|
||||
// removeListener: roomEmitter.removeListener.bind(roomEmitter),
|
||||
getMember: vi.fn().mockImplementation((userId: string) => {
|
||||
const member = fakeMembersMap.get(userId);
|
||||
if (member) {
|
||||
return member as RoomMember;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
} as unknown as MatrixRoom;
|
||||
});
|
||||
|
||||
function fakeMemberWith(data: Partial<RoomMember>): void {
|
||||
const userId = data.userId || "@alice:example.com";
|
||||
const member: Partial<RoomMember> = {
|
||||
userId: userId,
|
||||
rawDisplayName: data.rawDisplayName ?? userId,
|
||||
...data,
|
||||
} as unknown as RoomMember;
|
||||
fakeMembersMap.set(userId, member);
|
||||
// return member as RoomMember;
|
||||
}
|
||||
|
||||
function updateDisplayName(
|
||||
userId: `@${string}:${string}`,
|
||||
newDisplayName: string,
|
||||
): void {
|
||||
const member = fakeMembersMap.get(userId);
|
||||
if (member) {
|
||||
member.rawDisplayName = newDisplayName;
|
||||
// Emit the event to notify listeners
|
||||
mockMatrixRoom.emit(
|
||||
RoomStateEvent.Members,
|
||||
{} as unknown as MatrixEvent,
|
||||
{} as unknown as RoomState,
|
||||
member as RoomMember,
|
||||
);
|
||||
} else {
|
||||
throw new Error(`No member found with userId: ${userId}`);
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
fakeMembersMap.clear();
|
||||
});
|
||||
|
||||
// TODO this is a regression, now there the own user is not always in the map. Ask Timo if fine
|
||||
test.skip("should always have our own user", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("a", {
|
||||
a: [],
|
||||
}),
|
||||
);
|
||||
|
||||
expectObservable(dn$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
["@local:example.com", "@local:example.com"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setUpBasicRoom(): void {
|
||||
fakeMemberWith({ userId: "@local:example.com", rawDisplayName: "it's a me" });
|
||||
fakeMemberWith({ userId: "@alice:example.com", rawDisplayName: "Alice" });
|
||||
fakeMemberWith({ userId: "@bob:example.com", rawDisplayName: "Bob" });
|
||||
fakeMemberWith({ userId: "@carl:example.com", rawDisplayName: "Carl" });
|
||||
fakeMemberWith({ userId: "@evil:example.com", rawDisplayName: "Carl" });
|
||||
fakeMemberWith({ userId: "@bob:foo.bar", rawDisplayName: "Bob" });
|
||||
fakeMemberWith({ userId: "@no-name:foo.bar" });
|
||||
}
|
||||
|
||||
test("should get displayName for users", () => {
|
||||
setUpBasicRoom();
|
||||
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@alice:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expectObservable(dn$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@alice:example.com", "Alice"],
|
||||
["@bob:example.com", "Bob"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should use userId if no display name", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("a", {
|
||||
a: [mockCallMembership("@no-name:foo.bar", "D000")],
|
||||
}),
|
||||
);
|
||||
|
||||
expectObservable(dn$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@no-name:foo.bar", "@no-name:foo.bar"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should disambiguate users with same display name", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:example.com", "DEVICE2"),
|
||||
mockCallMembership("@bob:foo.bar", "BOB000"),
|
||||
mockCallMembership("@carl:example.com", "C000"),
|
||||
mockCallMembership("@evil:example.com", "E000"),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expectObservable(dn$).toBe("a", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:foo.bar", "Bob (@bob:foo.bar)"],
|
||||
["@carl:example.com", "Carl (@carl:example.com)"],
|
||||
["@evil:example.com", "Carl (@evil:example.com)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should disambiguate when needed", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("ab", {
|
||||
a: [mockCallMembership("@bob:example.com", "DEVICE1")],
|
||||
b: [
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:foo.bar", "BOB000"),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
expectObservable(dn$).toBe("ab", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob"],
|
||||
]),
|
||||
b: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:foo.bar", "Bob (@bob:foo.bar)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.skip("should keep disambiguated name when other leave", () => {
|
||||
withTestScheduler(({ behavior, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("ab", {
|
||||
a: [
|
||||
mockCallMembership("@bob:example.com", "DEVICE1"),
|
||||
mockCallMembership("@bob:foo.bar", "BOB000"),
|
||||
],
|
||||
b: [mockCallMembership("@bob:example.com", "DEVICE1")],
|
||||
}),
|
||||
);
|
||||
|
||||
expectObservable(dn$).toBe("ab", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@bob:foo.bar", "Bob (@bob:foo.bar)"],
|
||||
]),
|
||||
b: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("should disambiguate on name change", () => {
|
||||
withTestScheduler(({ behavior, schedule, expectObservable }) => {
|
||||
setUpBasicRoom();
|
||||
|
||||
const dn$ = memberDisplaynames$(
|
||||
testScope,
|
||||
mockMatrixRoom,
|
||||
behavior("a", {
|
||||
a: [
|
||||
mockCallMembership("@bob:example.com", "B000"),
|
||||
mockCallMembership("@carl:example.com", "C000"),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
schedule("-a", {
|
||||
a: () => {
|
||||
updateDisplayName("@carl:example.com", "Bob");
|
||||
},
|
||||
});
|
||||
|
||||
expectObservable(dn$).toBe("ab", {
|
||||
a: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob"],
|
||||
["@carl:example.com", "Carl"],
|
||||
]),
|
||||
b: new Map<string, string>([
|
||||
// ["@local:example.com", "it's a me"],
|
||||
["@bob:example.com", "Bob (@bob:example.com)"],
|
||||
["@carl:example.com", "Bob (@carl:example.com)"],
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,6 @@ import { type Room as LivekitRoom } from "livekit-client";
|
||||
import EventEmitter from "events";
|
||||
import fetchMock from "fetch-mock";
|
||||
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk";
|
||||
|
||||
import {
|
||||
type Epoch,
|
||||
@@ -40,7 +39,6 @@ let testScope: ObservableScope;
|
||||
let ecConnectionFactory: ECConnectionFactory;
|
||||
let mockClient: OpenIDClientParts;
|
||||
let lkRoomFactory: () => LivekitRoom;
|
||||
let mockMatrixRoom: MatrixRoom;
|
||||
|
||||
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
|
||||
|
||||
@@ -90,18 +88,6 @@ beforeEach(() => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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(() => {
|
||||
@@ -141,7 +127,6 @@ test("bob, carl, then bob joining no tracks yet", () => {
|
||||
membershipsWithTransport$:
|
||||
membershipsAndTransports.membershipsWithTransport$,
|
||||
connectionManager,
|
||||
matrixRoom: mockMatrixRoom,
|
||||
});
|
||||
|
||||
expectObservable(matrixLivekitItems$).toBe(vMarble, {
|
||||
@@ -153,14 +138,12 @@ test("bob, carl, then bob joining no tracks yet", () => {
|
||||
a: bobMembership,
|
||||
});
|
||||
expectObservable(item.connection$).toBe("a", {
|
||||
a: expect.toSatisfy((co) => {
|
||||
expect(
|
||||
areLivekitTransportsEqual(
|
||||
co.transport,
|
||||
bobMembership.transports[0]! as LivekitTransport,
|
||||
),
|
||||
);
|
||||
}),
|
||||
a: expect.toSatisfy((co) =>
|
||||
areLivekitTransportsEqual(
|
||||
co.transport,
|
||||
bobMembership.transports[0]! as LivekitTransport,
|
||||
),
|
||||
),
|
||||
});
|
||||
expectObservable(item.participant$).toBe("a", {
|
||||
a: null,
|
||||
|
||||
@@ -9,11 +9,11 @@ import { test, vi, expect } from "vitest";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { constant } from "./Behavior.ts";
|
||||
import { withCallViewModel } from "./CallViewModel.test.ts";
|
||||
import { aliceParticipant, localRtcMember } from "../utils/test-fixtures.ts";
|
||||
import { ElementWidgetActions, widget } from "../widget.ts";
|
||||
import { E2eeType } from "../e2ee/e2eeType.ts";
|
||||
import { type CallViewModel } from "./CallViewModel.ts";
|
||||
import { withCallViewModel } from "./CallViewModel/CallViewModel.test.ts";
|
||||
import { type CallViewModel } from "./CallViewModel/CallViewModel.ts";
|
||||
|
||||
vi.mock("../widget", () => ({
|
||||
ElementWidgetActions: {
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, test, vi } from "vitest";
|
||||
import { describe, expect, it, test } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { axe } from "vitest-axe";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { LocalTrackPublication, Track } from "livekit-client";
|
||||
import { TrackInfo } from "@livekit/protocol";
|
||||
import { type ComponentProps } from "react";
|
||||
import { type RoomMember } from "matrix-js-sdk";
|
||||
|
||||
import { MediaView } from "./MediaView";
|
||||
import { EncryptionStatus } from "../state/MediaViewModel";
|
||||
@@ -46,10 +45,8 @@ describe("MediaView", () => {
|
||||
mirror: false,
|
||||
unencryptedWarning: false,
|
||||
video: trackReference,
|
||||
member: vi.mocked<RoomMember>({
|
||||
userId: "@alice:example.com",
|
||||
getMxcAvatarUrl: vi.fn().mockReturnValue(undefined),
|
||||
} as unknown as RoomMember),
|
||||
userId: "@alice:example.com",
|
||||
mxcAvatarUrl: undefined,
|
||||
localParticipant: false,
|
||||
focusable: true,
|
||||
};
|
||||
|
||||
@@ -6,10 +6,10 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import { type RoomMember } from "matrix-js-sdk";
|
||||
|
||||
import { shouldDisambiguate } from "./displayname";
|
||||
import { alice } from "./test-fixtures";
|
||||
import { mockMatrixRoom } from "./test";
|
||||
|
||||
// Ideally these tests would be in ./displayname.test.ts but I can't figure out how to
|
||||
// just spy on the removeHiddenChars() function without impacting the other tests.
|
||||
@@ -29,7 +29,7 @@ describe("shouldDisambiguate", () => {
|
||||
});
|
||||
|
||||
test("should only call removeHiddenChars once for a single displayname", () => {
|
||||
const room = mockMatrixRoom({});
|
||||
const room: Map<string, Pick<RoomMember, "userId">> = new Map([]);
|
||||
shouldDisambiguate(alice, [], room);
|
||||
expect(jsUtils.removeHiddenChars).toHaveBeenCalledTimes(1);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
||||
@@ -20,62 +20,70 @@ import {
|
||||
daveRTL,
|
||||
} from "./test-fixtures";
|
||||
import { mockMatrixRoom } from "./test";
|
||||
import { roomToMembersMap } from "../state/CallViewModel/remoteMembers/MatrixMemberMetadata";
|
||||
|
||||
describe("shouldDisambiguate", () => {
|
||||
test("should not disambiguate a solo member", () => {
|
||||
const room = mockMatrixRoom({});
|
||||
expect(shouldDisambiguate(alice, [], room)).toEqual(false);
|
||||
const room = mockMatrixRoom({
|
||||
getMembers: () => [],
|
||||
});
|
||||
expect(shouldDisambiguate(alice, [], roomToMembersMap(room))).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
test("should not disambiguate a member with an empty displayname", () => {
|
||||
const room = mockMatrixRoom({
|
||||
getMember: (u) =>
|
||||
[alice, aliceDoppelganger].find((m) => m.userId === u) ?? null,
|
||||
getMembers: () => [alice, aliceDoppelganger],
|
||||
});
|
||||
expect(
|
||||
shouldDisambiguate(
|
||||
{ rawDisplayName: "", userId: alice.userId },
|
||||
[aliceRtcMember, aliceDoppelgangerRtcMember],
|
||||
room,
|
||||
roomToMembersMap(room),
|
||||
),
|
||||
).toEqual(false);
|
||||
});
|
||||
test("should disambiguate a member with RTL characters", () => {
|
||||
const room = mockMatrixRoom({});
|
||||
expect(shouldDisambiguate(daveRTL, [], room)).toEqual(true);
|
||||
const room = mockMatrixRoom({ getMembers: () => [] });
|
||||
expect(shouldDisambiguate(daveRTL, [], roomToMembersMap(room))).toEqual(
|
||||
true,
|
||||
);
|
||||
});
|
||||
test("should disambiguate a member with a matching displayname", () => {
|
||||
const room = mockMatrixRoom({
|
||||
getMember: (u) =>
|
||||
[alice, aliceDoppelganger].find((m) => m.userId === u) ?? null,
|
||||
getMembers: () => [alice, aliceDoppelganger],
|
||||
});
|
||||
expect(
|
||||
shouldDisambiguate(
|
||||
alice,
|
||||
[aliceRtcMember, aliceDoppelgangerRtcMember],
|
||||
room,
|
||||
roomToMembersMap(room),
|
||||
),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
shouldDisambiguate(
|
||||
aliceDoppelganger,
|
||||
[aliceRtcMember, aliceDoppelgangerRtcMember],
|
||||
room,
|
||||
roomToMembersMap(room),
|
||||
),
|
||||
).toEqual(true);
|
||||
});
|
||||
test("should disambiguate a member with a matching displayname with hidden spaces", () => {
|
||||
const room = mockMatrixRoom({
|
||||
getMember: (u) =>
|
||||
[bob, bobZeroWidthSpace].find((m) => m.userId === u) ?? null,
|
||||
getMembers: () => [bob, bobZeroWidthSpace],
|
||||
});
|
||||
expect(
|
||||
shouldDisambiguate(bob, [bobRtcMember, bobZeroWidthSpaceRtcMember], room),
|
||||
shouldDisambiguate(
|
||||
bob,
|
||||
[bobRtcMember, bobZeroWidthSpaceRtcMember],
|
||||
roomToMembersMap(room),
|
||||
),
|
||||
).toEqual(true);
|
||||
expect(
|
||||
shouldDisambiguate(
|
||||
bobZeroWidthSpace,
|
||||
[bobRtcMember, bobZeroWidthSpaceRtcMember],
|
||||
room,
|
||||
roomToMembersMap(room),
|
||||
),
|
||||
).toEqual(true);
|
||||
});
|
||||
@@ -83,11 +91,14 @@ describe("shouldDisambiguate", () => {
|
||||
"should disambiguate a member with a displayname containing a mxid-like string '%s'",
|
||||
(rawDisplayName) => {
|
||||
const room = mockMatrixRoom({
|
||||
getMember: (u) =>
|
||||
[alice, aliceDoppelganger].find((m) => m.userId === u) ?? null,
|
||||
getMembers: () => [alice, aliceDoppelganger],
|
||||
});
|
||||
expect(
|
||||
shouldDisambiguate({ rawDisplayName, userId: alice.userId }, [], room),
|
||||
shouldDisambiguate(
|
||||
{ rawDisplayName, userId: alice.userId },
|
||||
[],
|
||||
roomToMembersMap(room),
|
||||
),
|
||||
).toEqual(true);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -37,7 +37,6 @@ export const aliceDoppelganger = mockMatrixRoomMember(
|
||||
rawDisplayName: "Alice",
|
||||
},
|
||||
);
|
||||
export const aliceDoppelgangerId = `${aliceDoppelganger.userId}:${aliceDoppelgangerRtcMember.deviceId}`;
|
||||
|
||||
export const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB");
|
||||
export const bob = mockMatrixRoomMember(bobRtcMember, {
|
||||
@@ -55,10 +54,8 @@ export const bobZeroWidthSpace = mockMatrixRoomMember(
|
||||
rawDisplayName: "Bo\u200bb",
|
||||
},
|
||||
);
|
||||
export const bobZeroWidthSpaceId = `${bobZeroWidthSpace.userId}:${bobZeroWidthSpaceRtcMember.deviceId}`;
|
||||
|
||||
export const daveRTLRtcMember = mockRtcMembership("@dave2:example.org", "DDDD");
|
||||
export const daveRTL = mockMatrixRoomMember(daveRTLRtcMember, {
|
||||
rawDisplayName: "\u202eevaD",
|
||||
});
|
||||
export const daveRTLId = `${daveRTL.userId}:${daveRTLRtcMember.deviceId}`;
|
||||
|
||||
Reference in New Issue
Block a user