New room list: basic flat list (#29368)

* chore: make the room list panel a flexbox

* feat(new room list): add `RoomListCell` component

* feat(new room list): add virtualized `RoomList` component

* feat(new room list): add `RoomListView` component

* feat(new room list): use `RoomListView` in `RoomListPanel`

* test(new room list): add test for room cell

* test(new room list): update room list panel tests

* test(new room list): add test to virtualized room list

* test(e2e): add room list tests

* test(e2e): update room panel tests
This commit is contained in:
Florian Duros
2025-03-06 11:01:55 +01:00
committed by GitHub
parent b6c872142b
commit 90cc44b340
20 changed files with 948 additions and 63 deletions

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mkRoom, stubClient } from "../../../../../test-utils";
import { type RoomListViewState } from "../../../../../../src/components/viewmodels/roomlist/RoomListViewModel";
import { RoomList } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomList";
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
describe("<RoomList />", () => {
let matrixClient: MatrixClient;
let vm: RoomListViewState;
beforeEach(() => {
// Needed to render the virtualized list in rtl tests
// https://github.com/bvaughn/react-virtualized/issues/493#issuecomment-640084107
jest.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(1500);
jest.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockReturnValue(1500);
matrixClient = stubClient();
const rooms = Array.from({ length: 10 }, (_, i) => mkRoom(matrixClient, `room${i}`));
vm = { rooms, openRoom: jest.fn() };
// Needed to render a room list cell
DMRoomMap.makeShared(matrixClient);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
});
it("should render a room list", () => {
const { asFragment } = render(<RoomList vm={vm} />);
expect(asFragment()).toMatchSnapshot();
});
it("should open the room", async () => {
const user = userEvent.setup();
render(<RoomList vm={vm} />);
await waitFor(async () => {
expect(screen.getByRole("gridcell", { name: "Open room room9" })).toBeVisible();
await user.click(screen.getByRole("gridcell", { name: "Open room room9" }));
});
expect(vm.openRoom).toHaveBeenCalledWith(vm.rooms[9].roomId);
});
});

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { mkRoom, stubClient } from "../../../../../test-utils";
import { RoomListCell } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListCell";
import DMRoomMap from "../../../../../../src/utils/DMRoomMap";
describe("<RoomListCell />", () => {
let matrixClient: MatrixClient;
let room: Room;
beforeEach(() => {
matrixClient = stubClient();
room = mkRoom(matrixClient, "room1");
DMRoomMap.makeShared(matrixClient);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(null);
});
test("should render a room cell", () => {
const onClick = jest.fn();
const { asFragment } = render(<RoomListCell room={room} onClick={onClick} />);
expect(asFragment()).toMatchSnapshot();
});
test("should call onClick when clicked", async () => {
const user = userEvent.setup();
const onClick = jest.fn();
render(<RoomListCell room={room} onClick={onClick} />);
await user.click(screen.getByRole("button", { name: `Open room ${room.name}` }));
expect(onClick).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,504 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomList /> should render a room list 1`] = `
<DocumentFragment>
<div
class="mx_RoomList"
data-testid="room-list"
>
<div
style="overflow: visible; height: 0px; width: 0px;"
>
<div
aria-label="Room list"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 1500px; position: relative; width: 1500px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
>
<div
class="ReactVirtualized__Grid__innerScrollContainer"
role="row"
style="width: auto; height: 480px; max-width: 1500px; max-height: 480px; overflow: hidden; position: relative;"
>
<button
aria-label="Open room room0"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 0px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room0"
>
room0
</span>
</div>
</div>
</button>
<button
aria-label="Open room room1"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 48px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room1"
>
room1
</span>
</div>
</div>
</button>
<button
aria-label="Open room room2"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 96px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room2"
>
room2
</span>
</div>
</div>
</button>
<button
aria-label="Open room room3"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 144px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="5"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room3"
>
room3
</span>
</div>
</div>
</button>
<button
aria-label="Open room room4"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 192px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="6"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room4"
>
room4
</span>
</div>
</div>
</button>
<button
aria-label="Open room room5"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 240px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="1"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room5"
>
room5
</span>
</div>
</div>
</button>
<button
aria-label="Open room room6"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 288px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="2"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room6"
>
room6
</span>
</div>
</div>
</button>
<button
aria-label="Open room room7"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 336px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room7"
>
room7
</span>
</div>
</div>
</button>
<button
aria-label="Open room room8"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 384px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="4"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room8"
>
room8
</span>
</div>
</div>
</button>
<button
aria-label="Open room room9"
class="mx_RoomListCell"
role="gridcell"
style="height: 48px; left: 0px; position: absolute; top: 432px; width: 100%;"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="5"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room9"
>
room9
</span>
</div>
</div>
</button>
</div>
</div>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1501px; height: 1501px;"
/>
</div>
<div
class="contract-trigger"
/>
</div>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RoomListCell /> should render a room cell 1`] = `
<DocumentFragment>
<button
aria-label="Open room room1"
class="mx_RoomListCell"
type="button"
>
<div
class="mx_Flex mx_RoomListCell_container"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<div
class="mx_DecoratedRoomAvatar"
>
<span
aria-label="Avatar"
class="_avatar_1qbcf_8 mx_BaseAvatar"
data-color="3"
data-testid="avatar-img"
data-type="round"
style="--cpd-avatar-size: 32px;"
>
<img
alt=""
class="_image_1qbcf_41"
data-type="round"
height="32px"
loading="lazy"
referrerpolicy="no-referrer"
src="http://this.is.a.url/avatar.url/room.png"
width="32px"
/>
</span>
</div>
<div
class="mx_Flex mx_RoomListCell_content"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<span
title="room1"
>
room1
</span>
</div>
</div>
</button>
</DocumentFragment>
`;

View File

@@ -3,8 +3,9 @@
exports[`<RoomListPanel /> should not render the RoomListSearch component when UIComponent.FilterContainer is at false 1`] = `
<DocumentFragment>
<section
class="mx_RoomListPanel"
class="mx_Flex mx_RoomListPanel"
data-testid="room-list-panel"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<header
aria-label="Room options"
@@ -24,30 +25,35 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
</div>
</header>
<div
style="overflow: visible; height: 0px; width: 0px;"
class="mx_RoomList"
data-testid="room-list"
>
<div
aria-label="grid"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
style="overflow: visible; height: 0px; width: 0px;"
>
<div
style="width: 1px; height: 1px;"
aria-label="Room list"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
/>
</div>
<div
class="contract-trigger"
/>
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
</div>
</div>
</section>
</DocumentFragment>
@@ -56,8 +62,9 @@ exports[`<RoomListPanel /> should not render the RoomListSearch component when U
exports[`<RoomListPanel /> should render the RoomListSearch component when UIComponent.FilterContainer is at true 1`] = `
<DocumentFragment>
<section
class="mx_RoomListPanel"
class="mx_Flex mx_RoomListPanel"
data-testid="room-list-panel"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: stretch; --mx-flex-justify: start; --mx-flex-gap: 0;"
>
<div
class="mx_Flex mx_RoomListSearch"
@@ -168,30 +175,35 @@ exports[`<RoomListPanel /> should render the RoomListSearch component when UICom
</button>
</header>
<div
style="overflow: visible; height: 0px; width: 0px;"
class="mx_RoomList"
data-testid="room-list"
>
<div
aria-label="grid"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
style="overflow: visible; height: 0px; width: 0px;"
>
<div
style="width: 1px; height: 1px;"
aria-label="Room list"
aria-readonly="true"
class="ReactVirtualized__Grid ReactVirtualized__List mx_RoomList_List"
role="grid"
style="box-sizing: border-box; direction: ltr; height: 0px; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="0"
/>
</div>
<div
class="contract-trigger"
/>
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
</div>
</div>
</section>
</DocumentFragment>