Add jest tests

midhun/member-redesign-accessibility
R Midhun Suresh 2025-01-05 20:51:44 +05:30
parent 41e20e5038
commit a57d0b72c3
No known key found for this signature in database
8 changed files with 1089 additions and 0 deletions

View File

@ -800,6 +800,8 @@ export const mkThirdPartyInviteEvent = (user: string, displayName: string, room:
type: EventType.RoomThirdPartyInvite,
content: {
display_name: displayName,
public_key: "foo",
key_validity_url: "bar",
},
skey: "test" + Math.random(),
user,

View File

@ -0,0 +1,121 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, fireEvent, screen } from "jest-matrix-react";
import { RoomMember, User, RoomEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { mocked } from "jest-mock";
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import { Rendered, renderMemberList } from "./common";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
type Children = (args: { height: number; width: number }) => React.JSX.Element;
jest.mock("react-virtualized", () => {
const ReactVirtualized = jest.requireActual("react-virtualized");
return {
...ReactVirtualized,
AutoSizer: ({ children }: { children: Children }) => children({ height: 1000, width: 1000 }),
};
});
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
describe("Does not render invite button in memberlist header", () => {
it("when user is not a member", async () => {
await renderMemberList(true, (room) => room.updateMyMembership(KnownMembership.Leave));
expect(screen.queryByRole("button", { name: "Invite" })).toBeNull();
});
it("when UI customisation hides invites", async () => {
mocked(shouldShowComponent).mockReturnValue(false);
const { client, memberListRoom } = await renderMemberList(true);
// Needs this specific event...
act(() => {
client.emit(RoomEvent.MyMembership, memberListRoom, KnownMembership.Join, KnownMembership.Invite);
});
await new Promise((r) => setTimeout(r, 1000));
expect(screen.queryByRole("button", { name: "Invite" })).toBeNull();
});
});
describe("MemberListHeaderView", () => {
let rendered: Rendered;
beforeEach(async function () {
mocked(shouldShowComponent).mockReturnValue(true);
rendered = await renderMemberList(true);
});
it("Shows the correct member count", async () => {
expect(await screen.findByText("6 Members")).toBeVisible();
});
it("Does not show search box when there's less than 20 members", async () => {
expect(screen.queryByPlaceholderText("Search members...")).toBeNull();
});
it("Shows search box when there's more than 20 members", async () => {
const { memberListRoom, client, reRender } = rendered;
// Memberlist already has 6 members, add 14 more to make the total 20
for (let i = 0; i < 14; ++i) {
const newMember = new RoomMember(memberListRoom.roomId, `@new${i}:localhost`);
newMember.membership = KnownMembership.Join;
newMember.powerLevel = 0;
newMember.user = User.createUser(newMember.userId, client);
newMember.user.currentlyActive = true;
newMember.user.presence = "online";
newMember.user.lastPresenceTs = 1000;
newMember.user.lastActiveAgo = 10;
memberListRoom.currentState.members[newMember.userId] = newMember;
}
await reRender();
expect(screen.queryByPlaceholderText("Search members...")).toBeVisible();
});
describe("Invite button functionality", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("Renders disabled invite button when current user is a member but does not have rights to invite", async () => {
const { memberListRoom, reRender } = rendered;
jest.spyOn(memberListRoom, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(memberListRoom, "canInvite").mockReturnValue(false);
await reRender();
expect(screen.getByRole("button", { name: "Invite" })).toHaveAttribute("aria-disabled", "true");
});
it("Renders enabled invite button when current user is a member and has rights to invite", async () => {
const { memberListRoom, reRender } = rendered;
jest.spyOn(memberListRoom, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(memberListRoom, "canInvite").mockReturnValue(true);
await reRender();
expect(screen.getByRole("button", { name: "Invite" })).not.toHaveAttribute("aria-disabled", "true");
});
it("Opens room inviter on button click", async () => {
const { memberListRoom, reRender } = rendered;
jest.spyOn(defaultDispatcher, "dispatch");
jest.spyOn(memberListRoom, "canInvite").mockReturnValue(true);
await reRender();
fireEvent.click(screen.getByRole("button", { name: "Invite" }));
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({
action: "view_invite",
roomId: memberListRoom.roomId,
});
});
});
});

View File

@ -0,0 +1,256 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { act } from "react";
import { waitFor } from "jest-matrix-react";
import { Room, RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { filterConsole } from "../../../../../test-utils";
import { Rendered, renderMemberList } from "./common";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
type Children = (args: { height: number; width: number }) => React.JSX.Element;
jest.mock("react-virtualized", () => {
const ReactVirtualized = jest.requireActual("react-virtualized");
return {
...ReactVirtualized,
AutoSizer: ({ children }: { children: Children }) => children({ height: 1000, width: 1000 }),
};
});
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
describe("MemberListView and MemberlistHeaderView", () => {
filterConsole(
"Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.",
);
function memberString(member: RoomMember): string {
if (!member) {
return "(null)";
} else {
const u = member.user;
return (
"(" +
member.name +
", " +
member.powerLevel +
", " +
(u ? u.lastActiveAgo : "<null>") +
", " +
(u ? u.getLastActiveTs() : "<null>") +
", " +
(u ? u.currentlyActive : "<null>") +
", " +
(u ? u.presence : "<null>") +
")"
);
}
}
function expectOrderedByPresenceAndPowerLevel(
memberListRoom: Room,
memberTiles: NodeListOf<Element>,
isPresenceEnabled: boolean,
) {
let prevMember: RoomMember | undefined;
for (const tile of memberTiles) {
const memberA = prevMember;
const memberB = memberListRoom.currentState.members[tile.getAttribute("aria-label")!.split(" ")[0]];
prevMember = memberB; // just in case an expect fails, set this early
if (!memberA) {
continue;
}
console.log("COMPARING A VS B:", memberString(memberA), memberString(memberB));
const userA = memberA.user!;
const userB = memberB.user!;
let groupChange = false;
if (isPresenceEnabled) {
const convertPresence = (p: string) => (p === "unavailable" ? "online" : p);
const presenceIndex = (p: string) => {
const order = ["active", "online", "offline"];
const idx = order.indexOf(convertPresence(p));
return idx === -1 ? order.length : idx; // unknown states at the end
};
const idxA = presenceIndex(userA.currentlyActive ? "active" : userA.presence);
const idxB = presenceIndex(userB.currentlyActive ? "active" : userB.presence);
console.log("Comparing presence groups...");
expect(idxB).toBeGreaterThanOrEqual(idxA);
groupChange = idxA !== idxB;
} else {
console.log("Skipped presence groups");
}
if (!groupChange) {
console.log("Comparing power levels...");
expect(memberA.powerLevel).toBeGreaterThanOrEqual(memberB.powerLevel);
groupChange = memberA.powerLevel !== memberB.powerLevel;
} else {
console.log("Skipping power level check due to group change");
}
if (!groupChange) {
if (isPresenceEnabled) {
console.log("Comparing last active timestamp...");
expect(userB.getLastActiveTs()).toBeLessThanOrEqual(userA.getLastActiveTs());
groupChange = userA.getLastActiveTs() !== userB.getLastActiveTs();
} else {
console.log("Skipping last active timestamp");
}
} else {
console.log("Skipping last active timestamp check due to group change");
}
if (!groupChange) {
const nameA = memberA.name[0] === "@" ? memberA.name.slice(1) : memberA.name;
const nameB = memberB.name[0] === "@" ? memberB.name.slice(1) : memberB.name;
const collator = new Intl.Collator();
const nameCompare = collator.compare(nameB, nameA);
console.log("Comparing name");
expect(nameCompare).toBeGreaterThanOrEqual(0);
} else {
console.log("Skipping name check due to group change");
}
}
}
describe("MemberListView", () => {
let rendered: Rendered;
beforeEach(async function () {
rendered = await renderMemberList(true);
});
it("Memberlist is re-rendered on unreachable presence event", async () => {
const { root, defaultUsers } = rendered;
await act(async () => {
defaultUsers[0].user?.setPresenceEvent(
new MatrixEvent({
type: "m.presence",
sender: defaultUsers[0].userId,
content: {
presence: "io.element.unreachable",
currently_active: false,
},
}),
);
});
await waitFor(() => {
expect(root.container.querySelector(".mx_PresenceIconView_unavailable")).not.toBeNull();
});
});
});
describe.each([true, false])("does order members correctly (presence %s)", (enablePresence) => {
let rendered: Rendered;
beforeEach(async function () {
rendered = await renderMemberList(enablePresence);
});
describe("does order members correctly", () => {
// Note: even if presence is disabled, we still expect that the presence
// tests will pass. All expectOrderedByPresenceAndPowerLevel does is ensure
// the order is perceived correctly, regardless of what we did to the members.
// Each of the 4 tests here is done to prove that the member list can meet
// all 4 criteria independently. Together, they should work.
it("by presence state", async () => {
const { adminUsers, defaultUsers, moderatorUsers, reRender, root, memberListRoom } = rendered;
// Intentionally pick users that will confuse the power level sorting
const activeUsers = [defaultUsers[0]];
const onlineUsers = [adminUsers[0]];
const offlineUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)];
activeUsers.forEach((u) => {
u.user!.currentlyActive = true;
u.user!.presence = "online";
});
onlineUsers.forEach((u) => {
u.user!.currentlyActive = false;
u.user!.presence = "online";
});
offlineUsers.forEach((u) => {
u.user!.currentlyActive = false;
u.user!.presence = "offline";
});
await reRender();
const tiles = root.container.querySelectorAll(".mx_MemberTileView");
expectOrderedByPresenceAndPowerLevel(memberListRoom, tiles, enablePresence);
});
it("by power level", async () => {
const { reRender, root, memberListRoom } = rendered;
// We already have admin, moderator, and default users so leave them alone
await reRender();
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(memberListRoom, tiles, enablePresence);
});
it("by last active timestamp", async () => {
const { adminUsers, defaultUsers, moderatorUsers, reRender, root, memberListRoom } = rendered;
// Intentionally pick users that will confuse the power level sorting
// lastActiveAgoTs == lastPresenceTs - lastActiveAgo
const activeUsers = [defaultUsers[0]];
const semiActiveUsers = [adminUsers[0]];
const inactiveUsers = [...moderatorUsers, ...adminUsers.slice(1), ...defaultUsers.slice(1)];
activeUsers.forEach((u) => {
u.powerLevel = 100; // set everyone to the same PL to avoid running that check
u.user!.lastPresenceTs = 1000;
u.user!.lastActiveAgo = 0;
});
semiActiveUsers.forEach((u) => {
u.powerLevel = 100;
u.user!.lastPresenceTs = 1000;
u.user!.lastActiveAgo = 50;
});
inactiveUsers.forEach((u) => {
u.powerLevel = 100;
u.user!.lastPresenceTs = 1000;
u.user!.lastActiveAgo = 100;
});
await reRender();
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(memberListRoom, tiles, enablePresence);
});
it("by name", async () => {
const { adminUsers, defaultUsers, moderatorUsers, reRender, root, memberListRoom } = rendered;
// Intentionally put everyone on the same level to force a name comparison
const allUsers = [...adminUsers, ...moderatorUsers, ...defaultUsers];
allUsers.forEach((u) => {
u.user!.currentlyActive = true;
u.user!.presence = "online";
u.user!.lastPresenceTs = 1000;
u.user!.lastActiveAgo = 0;
u.powerLevel = 100;
});
await reRender();
const tiles = root.container.querySelectorAll(".mx_EntityTile");
expectOrderedByPresenceAndPowerLevel(memberListRoom, tiles, enablePresence);
});
});
});
});

View File

@ -0,0 +1,116 @@
/*
* Copyright 2024 New Vector Ltd.
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import { MatrixClient, RoomMember as SdkRoomMember, Device, Room } from "matrix-js-sdk/src/matrix";
import { UserVerificationStatus, DeviceVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import * as TestUtils from "../../../../../test-utils";
import { RoomMember } from "../../../../../../src/models/rooms/RoomMember";
import {
getPending3PidInvites,
sdkRoomMemberToRoomMember,
} from "../../../../../../src/components/viewmodels/memberlist/MemberListViewModel";
import { RoomMemberTileView } from "../../../../../../src/components/views/rooms/MemberList/tiles/RoomMemberTileView";
import { ThreePidInviteTileView } from "../../../../../../src/components/views/rooms/MemberList/tiles/ThreePidInviteTileView";
describe("MemberTileView", () => {
describe("RoomMemberTileView", () => {
let matrixClient: MatrixClient;
let member: RoomMember;
beforeEach(() => {
matrixClient = TestUtils.stubClient();
mocked(matrixClient.isRoomEncrypted).mockReturnValue(true);
const sdkMember = new SdkRoomMember("roomId", matrixClient.getUserId()!);
member = sdkRoomMemberToRoomMember(sdkMember)!.member!;
});
it("should not display an E2EIcon when the e2E status = normal", () => {
const { container } = render(<RoomMemberTileView member={member} />);
const e2eIcon = container.querySelector(".mx_E2EIconView");
expect(e2eIcon).toBeNull();
expect(container).toMatchSnapshot();
});
it("should display an warning E2EIcon when the e2E status = Warning", async () => {
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
isCrossSigningVerified: jest.fn().mockReturnValue(false),
wasCrossSigningVerified: jest.fn().mockReturnValue(true),
} as unknown as UserVerificationStatus);
const { container } = render(<RoomMemberTileView member={member} />);
await waitFor(async () => {
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
expect(screen.getByText("This user has not verified all of their sessions.")).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
it("should display an verified E2EIcon when the e2E status = Verified", async () => {
// Mock all the required crypto methods
const deviceMap = new Map<string, Map<string, Device>>();
deviceMap.set(member.userId, new Map([["deviceId", {} as Device]]));
// Return a DeviceMap = Map<string, Map<string, Device>>
mocked(matrixClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(deviceMap);
mocked(matrixClient.getCrypto()!.getUserVerificationStatus).mockResolvedValue({
isCrossSigningVerified: jest.fn().mockReturnValue(true),
} as unknown as UserVerificationStatus);
mocked(matrixClient.getCrypto()!.getDeviceVerificationStatus).mockResolvedValue({
crossSigningVerified: true,
} as DeviceVerificationStatus);
const { container } = render(<RoomMemberTileView member={member} />);
await waitFor(async () => {
await userEvent.hover(container.querySelector(".mx_E2EIcon")!);
expect(
screen.getByText("You have verified this user. This user has verified all of their sessions."),
).toBeInTheDocument();
});
expect(container).toMatchSnapshot();
});
it("renders user labels correctly", async () => {
member.powerLevel = 50;
const { container: container1 } = render(<RoomMemberTileView member={member} />);
expect(container1).toHaveTextContent("Moderator");
member.powerLevel = 100;
const { container: container2 } = render(<RoomMemberTileView member={member} />);
expect(container2).toHaveTextContent("Admin");
member.isInvite = true;
const { container: container3 } = render(<RoomMemberTileView member={member} />);
expect(container3).toHaveTextContent("Invited");
});
});
describe("ThreePidInviteTileView", () => {
let cli: MatrixClient;
let room: Room;
beforeEach(() => {
cli = TestUtils.stubClient();
room = new Room("!mytestroom:foo.org", cli, cli.getSafeUserId());
room.getLiveTimeline().addEvent(
TestUtils.mkThirdPartyInviteEvent(cli.getSafeUserId(), "Foobar", room.roomId),
{ toStartOfTimeline: false, addToState: true },
);
});
it("renders ThreePidInvite correctly", async () => {
const [{ threePidInvite }] = getPending3PidInvites(room);
const { container } = render(<ThreePidInviteTileView threePidInvite={threePidInvite!} />);
expect(container).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,42 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import AvatarPresenceIconView from "../../../../../../src/components/views/rooms/MemberList/tiles/common/PresenceIconView";
describe("<PresenceIconView/>", () => {
it("renders correctly for presence=online", () => {
const { container } = render(<AvatarPresenceIconView presenceState="online" />);
expect(container.querySelector(".mx_PresenceIconView_online")).toBeDefined();
expect(container).toMatchSnapshot();
});
it("renders correctly for presence=offline", () => {
const { container } = render(<AvatarPresenceIconView presenceState="offline" />);
expect(container.querySelector(".mx_PresenceIconView_offline")).toBeDefined();
expect(container).toMatchSnapshot();
});
it("renders correctly for presence=unavailable/unreachable", () => {
const { container: container1 } = render(<AvatarPresenceIconView presenceState="unavailable" />);
expect(container1.querySelector(".mx_PresenceIconView_unavailable")).toBeDefined();
expect(container1).toMatchSnapshot();
const { container: container2 } = render(<AvatarPresenceIconView presenceState="io.element.unreachable" />);
expect(container2.querySelector(".mx_PresenceIconView_unavailable")).toBeDefined();
expect(container2).toMatchSnapshot();
});
it("renders correctly for presence=busy", () => {
const { container } = render(<AvatarPresenceIconView presenceState="busy" />);
expect(container.querySelector(".mx_PresenceIconView_dnd")).toBeDefined();
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1,231 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MemberTileView RoomMemberTileView should display an verified E2EIcon when the e2E status = Verified 1`] = `
<div>
<div>
<div
aria-label="@userId:matrix.org (power 0)"
class="mx_AccessibleButton mx_MemberTileView"
role="button"
tabindex="0"
>
<div
class="mx_MemberTileView_left"
>
<div
class="mx_MemberTileView_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@userId:matrix.org"
>
u
</span>
</div>
<div
class="mx_MemberTileView_name"
>
<div
class="mx_DisambiguatedProfile"
>
<span
class=""
dir="auto"
>
@userId:matrix.org
</span>
</div>
</div>
</div>
<div
class="mx_MemberTileView_right"
>
<div
aria-labelledby=":ri:"
class="mx_E2EIconView"
>
<svg
class="mx_E2EIconView_verified"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.15 21.75 6.7 19.3l-2.75-.6a.943.943 0 0 1-.6-.387.928.928 0 0 1-.175-.688L3.45 14.8l-1.875-2.15a.934.934 0 0 1-.25-.65c0-.25.083-.467.25-.65L3.45 9.2l-.275-2.825a.928.928 0 0 1 .175-.688.943.943 0 0 1 .6-.387l2.75-.6 1.45-2.45a.983.983 0 0 1 .55-.438.97.97 0 0 1 .7.038l2.6 1.1 2.6-1.1a.97.97 0 0 1 .7-.038.983.983 0 0 1 .55.438L17.3 4.7l2.75.6c.25.05.45.18.6.388.15.208.208.437.175.687L20.55 9.2l1.875 2.15c.167.183.25.4.25.65s-.083.467-.25.65L20.55 14.8l.275 2.825a.928.928 0 0 1-.175.688.943.943 0 0 1-.6.387l-2.75.6-1.45 2.45a.983.983 0 0 1-.55.438.97.97 0 0 1-.7-.038l-2.6-1.1-2.6 1.1a.97.97 0 0 1-.7.038.983.983 0 0 1-.55-.438Zm2.8-9.05L9.5 11.275A.933.933 0 0 0 8.813 11c-.275 0-.513.1-.713.3a.948.948 0 0 0-.275.7.95.95 0 0 0 .275.7l2.15 2.15c.2.2.433.3.7.3.267 0 .5-.1.7-.3l4.25-4.25c.2-.2.296-.433.287-.7a1.055 1.055 0 0 0-.287-.7 1.02 1.02 0 0 0-.713-.313.93.93 0 0 0-.712.288L10.95 12.7Z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
`;
exports[`MemberTileView RoomMemberTileView should display an warning E2EIcon when the e2E status = Warning 1`] = `
<div>
<div>
<div
aria-label="@userId:matrix.org (power 0)"
class="mx_AccessibleButton mx_MemberTileView"
role="button"
tabindex="0"
>
<div
class="mx_MemberTileView_left"
>
<div
class="mx_MemberTileView_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@userId:matrix.org"
>
u
</span>
</div>
<div
class="mx_MemberTileView_name"
>
<div
class="mx_DisambiguatedProfile"
>
<span
class=""
dir="auto"
>
@userId:matrix.org
</span>
</div>
</div>
</div>
<div
class="mx_MemberTileView_right"
>
<div
aria-labelledby=":r8:"
class="mx_E2EIconView"
>
<svg
class="mx_E2EIconView_warning"
fill="currentColor"
height="16px"
viewBox="0 0 24 24"
width="16px"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 15a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 16c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-4c.283 0 .52-.096.713-.287A.968.968 0 0 0 13 12V8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8v4c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 9a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</div>
</div>
</div>
</div>
`;
exports[`MemberTileView RoomMemberTileView should not display an E2EIcon when the e2E status = normal 1`] = `
<div>
<div>
<div
aria-label="@userId:matrix.org (power 0)"
class="mx_AccessibleButton mx_MemberTileView"
role="button"
tabindex="0"
>
<div
class="mx_MemberTileView_left"
>
<div
class="mx_MemberTileView_avatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
title="@userId:matrix.org"
>
u
</span>
</div>
<div
class="mx_MemberTileView_name"
>
<div
class="mx_DisambiguatedProfile"
>
<span
class=""
dir="auto"
>
@userId:matrix.org
</span>
</div>
</div>
</div>
<div
class="mx_MemberTileView_right"
/>
</div>
</div>
</div>
`;
exports[`MemberTileView ThreePidInviteTileView renders ThreePidInvite correctly 1`] = `
<div>
<div>
<div
class="mx_AccessibleButton mx_MemberTileView"
role="button"
tabindex="0"
>
<div
class="mx_MemberTileView_left"
>
<div
class="mx_MemberTileView_avatar"
>
<span
aria-hidden="true"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="1"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
F
</span>
</div>
<div
class="mx_MemberTileView_name"
>
Foobar
</div>
</div>
<div
class="mx_MemberTileView_right"
/>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,175 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PresenceIconView/> renders correctly for presence=busy 1`] = `
<div>
<div
class="mx_PresenceIconView"
>
<svg
class="mx_PresenceIconView_dnd"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
clip-rule="evenodd"
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0ZM5.435 6.048A2.5 2.5 0 0 1 1.687 3.05l3.748 2.998Zm.914-1.19L2.648 1.897a2.5 2.5 0 0 1 3.701 2.961Z"
fill-rule="evenodd"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</div>
`;
exports[`<PresenceIconView/> renders correctly for presence=offline 1`] = `
<div>
<div
class="mx_PresenceIconView"
>
<svg
class="mx_PresenceIconView_offline"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
clip-rule="evenodd"
d="M4 6.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM4 8a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
fill-rule="evenodd"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</div>
`;
exports[`<PresenceIconView/> renders correctly for presence=online 1`] = `
<div>
<div
class="mx_PresenceIconView"
>
<svg
class="mx_PresenceIconView_online"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</div>
`;
exports[`<PresenceIconView/> renders correctly for presence=unavailable/unreachable 1`] = `
<div>
<div
class="mx_PresenceIconView"
>
<svg
class="mx_PresenceIconView_unavailable"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</div>
`;
exports[`<PresenceIconView/> renders correctly for presence=unavailable/unreachable 2`] = `
<div>
<div
class="mx_PresenceIconView"
>
<svg
class="mx_PresenceIconView_unavailable"
fill="currentColor"
height="8px"
viewBox="0 0 8 8"
width="8px"
xmlns="http://www.w3.org/2000/svg"
>
<g
clip-path="url(#a)"
>
<path
d="M8 4a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"
/>
</g>
<defs>
<clippath
id="a"
>
<path
d="M0 0h8v8H0z"
/>
</clippath>
</defs>
</svg>
</div>
</div>
`;

View File

@ -0,0 +1,146 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { act } from "react";
import { render, RenderResult, waitFor } from "jest-matrix-react";
import { Room, MatrixClient, RoomState, RoomMember, User, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import * as TestUtils from "../../../../../test-utils";
import { SDKContext } from "../../../../../../src/contexts/SDKContext";
import { TestSdkContext } from "../../../../TestSdkContext";
import MemberListView from "../../../../../../src/components/views/rooms/MemberList/MemberListView";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
export function createRoom(client: MatrixClient, opts = {}) {
const roomId = "!" + Math.random().toString().slice(2, 10) + ":domain";
const room = new Room(roomId, client, client.getUserId()!);
room.updateMyMembership(KnownMembership.Join);
if (opts) {
Object.assign(room, opts);
}
return room;
}
export type Rendered = {
client: MatrixClient;
root: RenderResult;
memberListRoom: Room;
adminUsers: RoomMember[];
moderatorUsers: RoomMember[];
defaultUsers: RoomMember[];
reRender: () => Promise<void>;
};
export async function renderMemberList(
enablePresence: boolean,
roomSetup?: (room: Room) => void,
usersPerLevel: number = 2,
): Promise<Rendered> {
TestUtils.stubClient();
const client = MatrixClientPeg.safeGet();
client.hasLazyLoadMembersEnabled = () => false;
// Make room
const memberListRoom = createRoom(client);
expect(memberListRoom.roomId).toBeTruthy();
// Give the test an opportunity to make changes to room before first render
roomSetup?.(memberListRoom);
// Make users
const adminUsers = [];
const moderatorUsers = [];
const defaultUsers = [];
for (let i = 0; i < usersPerLevel; i++) {
const adminUser = new RoomMember(memberListRoom.roomId, `@admin${i}:localhost`);
adminUser.membership = KnownMembership.Join;
adminUser.powerLevel = 100;
adminUser.user = User.createUser(adminUser.userId, client);
adminUser.user.currentlyActive = true;
adminUser.user.presence = "online";
adminUser.user.lastPresenceTs = 1000;
adminUser.user.lastActiveAgo = 10;
adminUsers.push(adminUser);
const moderatorUser = new RoomMember(memberListRoom.roomId, `@moderator${i}:localhost`);
moderatorUser.membership = KnownMembership.Join;
moderatorUser.powerLevel = 50;
moderatorUser.user = User.createUser(moderatorUser.userId, client);
moderatorUser.user.currentlyActive = true;
moderatorUser.user.presence = "online";
moderatorUser.user.lastPresenceTs = 1000;
moderatorUser.user.lastActiveAgo = 10;
moderatorUsers.push(moderatorUser);
const defaultUser = new RoomMember(memberListRoom.roomId, `@default${i}:localhost`);
defaultUser.membership = KnownMembership.Join;
defaultUser.powerLevel = 0;
defaultUser.user = User.createUser(defaultUser.userId, client);
defaultUser.user.currentlyActive = true;
defaultUser.user.presence = "online";
defaultUser.user.lastPresenceTs = 1000;
defaultUser.user.lastActiveAgo = 10;
defaultUsers.push(defaultUser);
}
client.getRoom = (roomId) => {
if (roomId === memberListRoom.roomId) return memberListRoom;
else return null;
};
memberListRoom.currentState = {
members: {},
getMember: jest.fn(),
getStateEvents: ((eventType, stateKey) => (stateKey === undefined ? [] : null)) as RoomState["getStateEvents"], // ignore 3pid invites
} as unknown as RoomState;
for (const member of [...adminUsers, ...moderatorUsers, ...defaultUsers]) {
memberListRoom.currentState.members[member.userId] = member;
}
const context = new TestSdkContext();
context.client = client;
context.memberListStore.isPresenceEnabled = jest.fn().mockReturnValue(enablePresence);
const root = render(
<MatrixClientContext.Provider value={client}>
<SDKContext.Provider value={context}>
<MemberListView roomId={memberListRoom.roomId} onClose={() => {}} />
</SDKContext.Provider>
</MatrixClientContext.Provider>,
);
await waitFor(async () => {
expect(root.container.querySelectorAll(".mx_MemberTileView")).toHaveLength(usersPerLevel * 3);
});
const reRender = createReRenderFunction(client, memberListRoom);
return {
client,
root,
memberListRoom,
adminUsers,
moderatorUsers,
defaultUsers,
reRender,
};
}
function createReRenderFunction(client: MatrixClient, memberListRoom: Room): Rendered["reRender"] {
return async function (): Promise<void> {
await act(async () => {
//@ts-ignore
client.emit(RoomStateEvent.Events, {
//@ts-ignore
getType: () => EventType.RoomThirdPartyInvite,
getRoomId: () => memberListRoom.roomId,
});
});
await new Promise((r) => setTimeout(r, 1000));
};
}