Files
healthchat-web/apps/web/playwright/e2e/left-panel/room-list-panel/room-list-custom-sections.spec.ts
T
Florian Duros 85aca65a81 Room list: drag and drop rooms into sections (#33366)
* chore: add dnd kit deps

* chore: patch dnd kit to fix ts error

* feat(sc): add drag-and-drop to room list item and wrapper

* feat(sc): make the room list header a droppable element

* feat(sc): add dnd to room list view

* feat(tags): can tag room as CHAT

* feat(vm): implement `changeRoomSection`

* feat(sc): disable dragging in flat list

* fix: disable keyboard navigation when dragging element

* test(sc): update snapshots

* test(sc): add dnd test

* test(e2e): add e2e tests for room drag and drop

* test(vm): add tests for changeRoomSection

* fix: remove focus visible when dropping with the mouse

* test(playwright): update existing screenshots

* chore(sc): move numbers out of main build

The Ew RecorderWorklet imports shared component bundle. However if the
bundle uses some deps using document/window which, the worklet will not
work.

The solution is to put the used functions into a separate bundle.

* doc(sc): add subpath import into README

* doc: typo barrel/bundle

* test: improve test expect

* refactor: add utils to section tag

* fix: incorrect check in tagRoom

* fix: add doc about dndkit tunning
2026-05-13 09:06:22 +00:00

359 lines
16 KiB
TypeScript

/*
* Copyright 2026 Element Creations 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 { type Page } from "@playwright/test";
import { expect, test } from "../../../element-web-test";
import { assertRoomInSection, getRoomList, getRoomListHeader, getSectionHeader } from "./utils";
test.describe("Room list custom sections", () => {
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list", "feature_room_list_sections"],
botCreateOpts: {
displayName: "BotBob",
autoAcceptInvites: true,
},
});
/**
* Create a custom section via the header compose menu and dialog.
* @param page
* @param sectionName The name of the section to create
*/
async function createCustomSection(page: Page, sectionName: string): Promise<void> {
const composeMenu = getRoomListHeader(page).getByRole("button", { name: "New conversation" });
await composeMenu.click();
await page.getByRole("menuitem", { name: "New section" }).click();
// Fill in the section name in the dialog
const dialog = page.getByRole("dialog", { name: "Create a section" });
await expect(dialog).toBeVisible();
await dialog.getByRole("textbox", { name: "Section name" }).fill(sectionName);
await dialog.getByRole("button", { name: "Create section" }).click();
// Wait for the dialog to close
await expect(dialog).not.toBeVisible();
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
await app.closeVerifyToast();
// Focus the user menu to avoid hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
});
test.describe("Section creation", () => {
test("should create a custom section via the header compose menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
// The custom section header should be visible (even though it is empty)
await expect(getSectionHeader(page, "Work")).toBeVisible();
// The Chats section should also be visible
await expect(getSectionHeader(page, "Chats")).toBeVisible();
});
test("should show 'Section created' toast after creating a section", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Personal");
// The "Section created" toast should appear
await expect(page.getByText("Section created")).toBeVisible();
});
test("should create a custom section via the room option menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
const roomList = getRoomList(page);
const roomItem = roomList.getByRole("option", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
// Open the More Options menu
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
// Open the "Move to" submenu
await page.getByRole("menuitem", { name: "Move to" }).hover();
// Click on "New section"
await page.getByRole("menuitem", { name: "New section" }).click();
// Fill in the section name in the dialog
const dialog = page.getByRole("dialog", { name: "Create a section" });
await expect(dialog).toBeVisible();
await dialog.getByRole("textbox", { name: "Section name" }).fill("Projects");
await dialog.getByRole("button", { name: "Create section" }).click();
// Wait for the dialog to close
await expect(dialog).not.toBeVisible();
// The custom section should be created
await expect(getSectionHeader(page, "Projects")).toBeVisible();
// Room should be moved to the new section
await assertRoomInSection(page, "Projects", "my room");
});
test("should cancel section creation when dialog is dismissed", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
const composeMenu = getRoomListHeader(page).getByRole("button", { name: "New conversation" });
await composeMenu.click();
await page.getByRole("menuitem", { name: "New section" }).click();
// The dialog should appear
const dialog = page.getByRole("dialog", { name: "Create a section" });
await expect(dialog).toBeVisible();
// Cancel the dialog
await dialog.getByRole("button", { name: "Cancel" }).click();
// The dialog should close
await expect(dialog).not.toBeVisible();
// No custom section should be created - should remain a flat list
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
});
test("should create multiple custom sections", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
await createCustomSection(page, "Personal");
// Both custom sections should be visible
await expect(getSectionHeader(page, "Work")).toBeVisible();
await expect(getSectionHeader(page, "Personal")).toBeVisible();
await expect(getSectionHeader(page, "Chats")).toBeVisible();
});
});
test.describe("Custom section display", () => {
test("should show empty custom sections", async ({ page, app }) => {
// Create a room so the Chats section has something
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Empty Section");
// The custom section should be visible even with no rooms
await expect(getSectionHeader(page, "Empty Section")).toBeVisible();
// The room should still be in the Chats section
const roomList = getRoomList(page);
await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible();
});
test("should display custom sections between Favourites and Chats", async ({ page, app }) => {
// Create a favourite room
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);
// Create a low priority room
const lowPrioId = await app.client.createRoom({ name: "low prio room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.lowpriority");
}, lowPrioId);
// Create a regular room
await app.client.createRoom({ name: "regular room" });
// Create a custom section
await createCustomSection(page, "Work");
// All section headers should be visible
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
await expect(getSectionHeader(page, "Work")).toBeVisible();
// Should be expanded by default
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true");
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
});
});
test.describe("Section editing", () => {
test("should edit a custom section name via the section header menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
// Open the section header menu
const sectionHeader = getSectionHeader(page, "Work");
await sectionHeader.hover();
await sectionHeader.getByRole("button", { name: "More options" }).click();
// Click "Edit section"
await page.getByRole("menuitem", { name: "Edit section" }).click();
// The edit dialog should appear pre-filled with the current name
const dialog = page.getByRole("dialog", { name: "Edit a section" });
await expect(dialog).toBeVisible();
await expect(dialog.getByRole("textbox", { name: "Section name" })).toHaveValue("Work");
// Change the name and confirm
await dialog.getByRole("textbox", { name: "Section name" }).fill("Personal");
await dialog.getByRole("button", { name: "Edit section" }).click();
// Dialog should close
await expect(dialog).not.toBeVisible();
// Section should have the new name
await expect(getSectionHeader(page, "Personal")).toBeVisible();
await expect(getSectionHeader(page, "Work")).not.toBeVisible();
});
});
test.describe("Section removal", () => {
test("should move rooms back to Chats when their section is removed", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
await createCustomSection(page, "Personal");
const roomList = getRoomList(page);
// Move room to Work section
const roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
await assertRoomInSection(page, "Work", "my room");
// Remove the Work section
const sectionHeader = getSectionHeader(page, "Work");
await sectionHeader.hover();
await sectionHeader.getByRole("button", { name: "More options" }).click();
await page.getByRole("menuitem", { name: "Remove section" }).click();
const dialog = page.getByRole("dialog", { name: "Remove section?" });
await dialog.getByRole("button", { name: "Remove section" }).click();
// Section should be gone
await expect(getSectionHeader(page, "Work")).not.toBeVisible();
// Room should now be in the Chats section
await assertRoomInSection(page, "Chats", "my room");
});
});
test.describe("Collapse and expand all sections", () => {
test("should collapse all sections when 'Collapse all sections' button is clicked", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
const header = getRoomListHeader(page);
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Work")).toBeVisible();
const collapseButton = header.getByRole("button", { name: "Collapse all sections" });
await expect(collapseButton).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible();
await collapseButton.click();
await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "false");
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "false");
});
test("should expand all sections when 'Expand all sections' button is clicked", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
const header = getRoomListHeader(page);
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await header.getByRole("button", { name: "Collapse all sections" }).click();
await expect(roomList.getByRole("row", { name: "Open room my room" })).not.toBeVisible();
await header.getByRole("button", { name: "Expand all sections" }).click();
await expect(getSectionHeader(page, "Chats")).toHaveAttribute("aria-expanded", "true");
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true");
});
});
test.describe("Adding a room to a custom section", () => {
test("should add a room to a custom section via the More Options menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
// Room starts in Chats section (aria-level=2)
const roomItem = roomList.getByRole("row", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
// Open More Options and move to the Work section
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
// Room should now be nested under the Work section header (aria-level=1 → aria-level=2)
await assertRoomInSection(page, "Work", "my room");
});
test(
"should show 'Chat moved' toast when adding a room to a custom section",
{ tag: "@screenshot" },
async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
const roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
// The "Chat moved" toast should appear
await expect(page.getByText("Chat moved")).toBeVisible();
// Remove focus outline from the room item before taking the screenshot
await page.getByRole("button", { name: "User menu" }).focus();
await expect(roomList).toMatchScreenshot("room-list-sections-chat-moved-toast.png");
},
);
test("should remove a room from a custom section when toggling the same section", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
// Move to Work section and verify placement via aria-level
let roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
await assertRoomInSection(page, "Work", "my room");
// Toggle off by selecting the same section again
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
// Room is back in the Chats section
await assertRoomInSection(page, "Chats", "my room");
});
});
});