From a57d0b72c32d42c126216df6a529b49341ed123f Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Sun, 5 Jan 2025 20:51:44 +0530 Subject: [PATCH] Add jest tests --- test/test-utils/test-utils.ts | 2 + .../memberlist/MemberListHeaderView-test.tsx | 121 +++++++++ .../rooms/memberlist/MemberListView-test.tsx | 256 ++++++++++++++++++ .../rooms/memberlist/MemberTileView-test.tsx | 116 ++++++++ .../memberlist/PresenceIconView-test.tsx | 42 +++ .../MemberTileView-test.tsx.snap | 231 ++++++++++++++++ .../PresenceIconView-test.tsx.snap | 175 ++++++++++++ .../views/rooms/memberlist/common.tsx | 146 ++++++++++ 8 files changed, 1089 insertions(+) create mode 100644 test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/memberlist/MemberTileView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/memberlist/PresenceIconView-test.tsx create mode 100644 test/unit-tests/components/views/rooms/memberlist/__snapshots__/MemberTileView-test.tsx.snap create mode 100644 test/unit-tests/components/views/rooms/memberlist/__snapshots__/PresenceIconView-test.tsx.snap create mode 100644 test/unit-tests/components/views/rooms/memberlist/common.tsx diff --git a/test/test-utils/test-utils.ts b/test/test-utils/test-utils.ts index 39852f049e..95a27ab322 100644 --- a/test/test-utils/test-utils.ts +++ b/test/test-utils/test-utils.ts @@ -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, diff --git a/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx b/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx new file mode 100644 index 0000000000..f0e726c453 --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/MemberListHeaderView-test.tsx @@ -0,0 +1,121 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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, + }); + }); + }); +}); diff --git a/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx b/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx new file mode 100644 index 0000000000..e49b642b6e --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/MemberListView-test.tsx @@ -0,0 +1,256 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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 : "") + + ", " + + (u ? u.getLastActiveTs() : "") + + ", " + + (u ? u.currentlyActive : "") + + ", " + + (u ? u.presence : "") + + ")" + ); + } + } + + function expectOrderedByPresenceAndPowerLevel( + memberListRoom: Room, + memberTiles: NodeListOf, + 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); + }); + }); + }); +}); diff --git a/test/unit-tests/components/views/rooms/memberlist/MemberTileView-test.tsx b/test/unit-tests/components/views/rooms/memberlist/MemberTileView-test.tsx new file mode 100644 index 0000000000..663f5e8490 --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/MemberTileView-test.tsx @@ -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(); + 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(); + 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>(); + deviceMap.set(member.userId, new Map([["deviceId", {} as Device]])); + // Return a DeviceMap = Map> + 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(); + + 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(); + expect(container1).toHaveTextContent("Moderator"); + + member.powerLevel = 100; + const { container: container2 } = render(); + expect(container2).toHaveTextContent("Admin"); + + member.isInvite = true; + const { container: container3 } = render(); + 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(); + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/test/unit-tests/components/views/rooms/memberlist/PresenceIconView-test.tsx b/test/unit-tests/components/views/rooms/memberlist/PresenceIconView-test.tsx new file mode 100644 index 0000000000..d06fcc560f --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/PresenceIconView-test.tsx @@ -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("", () => { + it("renders correctly for presence=online", () => { + const { container } = render(); + expect(container.querySelector(".mx_PresenceIconView_online")).toBeDefined(); + expect(container).toMatchSnapshot(); + }); + + it("renders correctly for presence=offline", () => { + const { container } = render(); + expect(container.querySelector(".mx_PresenceIconView_offline")).toBeDefined(); + expect(container).toMatchSnapshot(); + }); + + it("renders correctly for presence=unavailable/unreachable", () => { + const { container: container1 } = render(); + expect(container1.querySelector(".mx_PresenceIconView_unavailable")).toBeDefined(); + expect(container1).toMatchSnapshot(); + + const { container: container2 } = render(); + expect(container2.querySelector(".mx_PresenceIconView_unavailable")).toBeDefined(); + expect(container2).toMatchSnapshot(); + }); + + it("renders correctly for presence=busy", () => { + const { container } = render(); + expect(container.querySelector(".mx_PresenceIconView_dnd")).toBeDefined(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/components/views/rooms/memberlist/__snapshots__/MemberTileView-test.tsx.snap b/test/unit-tests/components/views/rooms/memberlist/__snapshots__/MemberTileView-test.tsx.snap new file mode 100644 index 0000000000..6feb72ea62 --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/__snapshots__/MemberTileView-test.tsx.snap @@ -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`] = ` +
+
+
+
+
+ + u + + +
+
+
+ + @userId:matrix.org + +
+
+
+
+
+ + + +
+
+
+
+
+`; + +exports[`MemberTileView RoomMemberTileView should display an warning E2EIcon when the e2E status = Warning 1`] = ` +
+
+
+
+
+ + u + + +
+
+
+ + @userId:matrix.org + +
+
+
+
+
+ + + +
+
+
+
+
+`; + +exports[`MemberTileView RoomMemberTileView should not display an E2EIcon when the e2E status = normal 1`] = ` +
+
+
+
+
+ + u + + +
+
+
+ + @userId:matrix.org + +
+
+
+
+
+
+
+`; + +exports[`MemberTileView ThreePidInviteTileView renders ThreePidInvite correctly 1`] = ` +
+
+
+
+
+ + +
+
+ Foobar +
+
+
+
+
+
+`; diff --git a/test/unit-tests/components/views/rooms/memberlist/__snapshots__/PresenceIconView-test.tsx.snap b/test/unit-tests/components/views/rooms/memberlist/__snapshots__/PresenceIconView-test.tsx.snap new file mode 100644 index 0000000000..edf24f529e --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/__snapshots__/PresenceIconView-test.tsx.snap @@ -0,0 +1,175 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly for presence=busy 1`] = ` +
+
+ + + + + + + + + + +
+
+`; + +exports[` renders correctly for presence=offline 1`] = ` +
+
+ + + + + + + + + + +
+
+`; + +exports[` renders correctly for presence=online 1`] = ` +
+
+ + + + + + + + + + +
+
+`; + +exports[` renders correctly for presence=unavailable/unreachable 1`] = ` +
+
+ + + + + + + + + + +
+
+`; + +exports[` renders correctly for presence=unavailable/unreachable 2`] = ` +
+
+ + + + + + + + + + +
+
+`; diff --git a/test/unit-tests/components/views/rooms/memberlist/common.tsx b/test/unit-tests/components/views/rooms/memberlist/common.tsx new file mode 100644 index 0000000000..77a9ca852d --- /dev/null +++ b/test/unit-tests/components/views/rooms/memberlist/common.tsx @@ -0,0 +1,146 @@ +/* +Copyright 2024 New Vector Ltd. +Copyright 2022 The Matrix.org Foundation C.I.C. +Copyright 2021 Šimon Brandner + +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; +}; + +export async function renderMemberList( + enablePresence: boolean, + roomSetup?: (room: Room) => void, + usersPerLevel: number = 2, +): Promise { + 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( + + + {}} /> + + , + ); + 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 { + await act(async () => { + //@ts-ignore + client.emit(RoomStateEvent.Events, { + //@ts-ignore + getType: () => EventType.RoomThirdPartyInvite, + getRoomId: () => memberListRoom.roomId, + }); + }); + await new Promise((r) => setTimeout(r, 1000)); + }; +}