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:
Timo K
2025-11-11 15:52:35 +01:00
parent 85f659bcc9
commit 8671d3fd67
13 changed files with 806 additions and 630 deletions

View File

@@ -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();

View File

@@ -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

View File

@@ -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(() => ({})));

View File

@@ -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
// // })
// // };
// // });
// });
// });
// });

View File

@@ -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(

View File

@@ -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"],
]),
});
});
});
});
});

View File

@@ -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)"],
]),
});
});
});

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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,
};

View File

@@ -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++) {

View File

@@ -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);
},
);

View File

@@ -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}`;