Skip to content

Commit 7de54a3

Browse files
authored
New room list: add empty state (#29512)
* refactor: extract room creation and right verification * refactor: update `RoomListHeaderViewModel` to use utils * feat(room list filter): add filter key to `PrimaryFilter` model * feat(room list filter): return active primary filter * feat(room list): add create room action and rights verification * test: update room list tests * feat(empty room list): add empty room list * test(empty room list): add empty room list tests * feat(room list): use empty room list in `RoomListView` * test(room list panel): update tests * test(e2e): add e2e tests for empty room list * test(e2e): update room list header snapshot
1 parent 55b0b11 commit 7de54a3

26 files changed

+988
-152
lines changed

playwright/e2e/left-panel/room-list-panel/room-list-filter-sort.spec.ts

Lines changed: 100 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,71 +18,120 @@ test.describe("Room list filters and sort", () => {
1818
labsFlags: ["feature_new_room_list"],
1919
});
2020

21-
/**
22-
* Get the room list
23-
* @param page
24-
*/
25-
function getRoomList(page: Page) {
26-
return page.getByTestId("room-list");
27-
}
28-
2921
function getPrimaryFilters(page: Page) {
3022
return page.getByRole("listbox", { name: "Room list filters" });
3123
}
3224

3325
test.beforeEach(async ({ page, app, bot, user }) => {
3426
// The notification toast is displayed above the search section
3527
await app.closeNotificationToast();
28+
});
29+
30+
test.describe("Room list", () => {
31+
/**
32+
* Get the room list
33+
* @param page
34+
*/
35+
function getRoomList(page: Page) {
36+
return page.getByTestId("room-list");
37+
}
38+
39+
test.beforeEach(async ({ page, app, bot, user }) => {
40+
await app.client.createRoom({ name: "empty room" });
3641

37-
await app.client.createRoom({ name: "empty room" });
42+
const unReadDmId = await bot.createRoom({
43+
name: "unread dm",
44+
invite: [user.userId],
45+
is_direct: true,
46+
});
47+
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
3848

39-
const unReadDmId = await bot.createRoom({
40-
name: "unread dm",
41-
invite: [user.userId],
42-
is_direct: true,
49+
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
50+
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
51+
await bot.joinRoom(unReadRoomId);
52+
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
53+
54+
const favouriteId = await app.client.createRoom({ name: "favourite room" });
55+
await app.client.evaluate(async (client, favouriteId) => {
56+
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
57+
}, favouriteId);
4358
});
44-
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
4559

46-
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
47-
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
48-
await bot.joinRoom(unReadRoomId);
49-
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
60+
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
61+
const roomList = getRoomList(page);
62+
const primaryFilters = getPrimaryFilters(page);
5063

51-
const favouriteId = await app.client.createRoom({ name: "favourite room" });
52-
await app.client.evaluate(async (client, favouriteId) => {
53-
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
54-
}, favouriteId);
55-
});
64+
const allFilters = await primaryFilters.locator("option").all();
65+
for (const filter of allFilters) {
66+
expect(await filter.getAttribute("aria-selected")).toBe("false");
67+
}
68+
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
69+
70+
await primaryFilters.getByRole("option", { name: "Unread" }).click();
71+
// only one room should be visible
72+
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
73+
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
74+
expect(await roomList.locator("role=gridcell").count()).toBe(2);
75+
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
5676

57-
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
58-
const roomList = getRoomList(page);
59-
const primaryFilters = getPrimaryFilters(page);
77+
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
78+
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
79+
expect(await roomList.locator("role=gridcell").count()).toBe(1);
6080

61-
const allFilters = await primaryFilters.locator("option").all();
62-
for (const filter of allFilters) {
63-
expect(await filter.getAttribute("aria-selected")).toBe("false");
81+
await primaryFilters.getByRole("option", { name: "People" }).click();
82+
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
83+
expect(await roomList.locator("role=gridcell").count()).toBe(1);
84+
85+
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
86+
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
87+
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
88+
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
89+
expect(await roomList.locator("role=gridcell").count()).toBe(3);
90+
});
91+
});
92+
93+
test.describe("Empty room list", () => {
94+
/**
95+
* Get the empty state
96+
* @param page
97+
*/
98+
function getEmptyRoomList(page: Page) {
99+
return page.getByTestId("empty-room-list");
64100
}
65-
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
66-
67-
await primaryFilters.getByRole("option", { name: "Unread" }).click();
68-
// only one room should be visible
69-
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
70-
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
71-
expect(await roomList.locator("role=gridcell").count()).toBe(2);
72-
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
73-
74-
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
75-
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
76-
expect(await roomList.locator("role=gridcell").count()).toBe(1);
77-
78-
await primaryFilters.getByRole("option", { name: "People" }).click();
79-
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
80-
expect(await roomList.locator("role=gridcell").count()).toBe(1);
81-
82-
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
83-
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
84-
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
85-
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
86-
expect(await roomList.locator("role=gridcell").count()).toBe(3);
101+
102+
test(
103+
"should render the default placeholder when there is no filter",
104+
{ tag: "@screenshot" },
105+
async ({ page, app, user }) => {
106+
const emptyRoomList = getEmptyRoomList(page);
107+
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
108+
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
109+
},
110+
);
111+
112+
test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => {
113+
const primaryFilters = getPrimaryFilters(page);
114+
await primaryFilters.getByRole("option", { name: "Unread" }).click();
115+
116+
const emptyRoomList = getEmptyRoomList(page);
117+
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png");
118+
119+
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
120+
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
121+
});
122+
123+
["People", "Rooms", "Favourite"].forEach((filter) => {
124+
test(
125+
`should render the placeholder for ${filter} filter`,
126+
{ tag: "@screenshot" },
127+
async ({ page, app, user }) => {
128+
const primaryFilters = getPrimaryFilters(page);
129+
await primaryFilters.getByRole("option", { name: filter }).click();
130+
131+
const emptyRoomList = getEmptyRoomList(page);
132+
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
133+
},
134+
);
135+
});
87136
});
88137
});
9.24 KB
Loading
10.1 KB
Loading
7.52 KB
Loading
13 KB
Loading
Loading
7.93 KB
Loading
33 Bytes
Loading

res/css/_components.pcss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@
270270
@import "./views/right_panel/_VerificationPanel.pcss";
271271
@import "./views/right_panel/_WidgetCard.pcss";
272272
@import "./views/room_settings/_AliasSettings.pcss";
273+
@import "./views/rooms/RoomListPanel/_EmptyRoomList.pcss";
273274
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
274275
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
275276
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 New Vector Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
.mx_EmptyRoomList_GenericPlaceholder {
9+
align-self: center;
10+
/** It should take 2/3 of the width **/
11+
width: 66%;
12+
/** It should be positioned at 1/3 of the height **/
13+
padding-top: 33%;
14+
15+
.mx_EmptyRoomList_GenericPlaceholder_title {
16+
font: var(--cpd-font-body-lg-semibold);
17+
text-align: center;
18+
}
19+
20+
.mx_EmptyRoomList_GenericPlaceholder_description {
21+
font: var(--cpd-font-body-sm-regular);
22+
color: var(--cpd-color-text-secondary);
23+
text-align: center;
24+
}
25+
26+
.mx_EmptyRoomList_DefaultPlaceholder {
27+
margin-top: var(--cpd-space-4x);
28+
}
29+
30+
button {
31+
width: 100%;
32+
}
33+
}

0 commit comments

Comments
 (0)