feat: show call participants in room list (Discord-style)
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
Docker / Docker Buildx (push) Has been cancelled
Build Debian package / Build package (release) Has been cancelled
Build and Deploy / prepare (release) Has been cancelled
Deploy release / Deploy to Cloudflare Pages (release) Has been cancelled
Build and Deploy / Trigger Pro pipeline (release) Has been cancelled
Build and Deploy / Windows arm64 (release) Has been cancelled
Build and Deploy / Windows x64 (release) Has been cancelled
Build and Deploy / macOS (release) Has been cancelled
Build and Deploy / Linux amd64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / Linux arm64 (sqlcipher static) (release) Has been cancelled
Build and Deploy / ${{ needs.prepare.outputs.deploy == 'true' && 'Deploy' || 'Deploy (dry-run)' }} (release) Has been cancelled
Build and Deploy / Deploy builds to ESS (release) Has been cancelled
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
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 { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("LeftPanel", () => {
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test("should render the Rooms list", async ({ page, app, user }) => {
|
||||
// create rooms and check room names are correct
|
||||
for (const name of ["Apple", "Pineapple", "Orange"]) {
|
||||
await app.client.createRoom({ name });
|
||||
await expect(page.getByRole("option", { name: `Open room ${name}` })).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { test, expect } from "../../../element-web-test";
|
||||
import type { Locator, Page } from "playwright-core";
|
||||
|
||||
test.describe("Collapsible Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Resize the panel and return the bounding box
|
||||
* @param pixels The number of pixels by which to resize the panel
|
||||
*/
|
||||
async function resize(page: Page, pixels: number): ReturnType<Locator["boundingBox"]> {
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
const boundingBox = await leftPanelLocator.boundingBox();
|
||||
|
||||
// Move mouse 2px to the right of the left-panel, this should be region that the user drags to resize the panel.
|
||||
const mouseX = boundingBox.x + boundingBox.width + 2;
|
||||
const mouseY = boundingBox.y + boundingBox.height / 2;
|
||||
|
||||
await page.mouse.move(mouseX, mouseY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(mouseX + pixels, mouseY);
|
||||
|
||||
return boundingBox;
|
||||
}
|
||||
|
||||
test("should be possible to expand/contract the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await expect(page).toMatchScreenshot("room-list-collapse-default.png");
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
|
||||
// Contract the panel
|
||||
let previousBoundingBox = await resize(page, -50);
|
||||
let currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeCloseTo(previousBoundingBox.width - 50, 0);
|
||||
|
||||
// Expand the panel
|
||||
previousBoundingBox = await resize(page, 30);
|
||||
currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeCloseTo(previousBoundingBox.width + 30, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
"should be possible to fully collapse and expand the left panel",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const leftPanelLocator = page.getByTestId("left-panel");
|
||||
|
||||
// Collapse the panel
|
||||
await resize(page, -300);
|
||||
let currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toStrictEqual(0);
|
||||
|
||||
// Expect te separator to be shown
|
||||
const separator = page.getByRole("separator", { name: "Click or drag to expand" });
|
||||
await expect(separator).toBeInViewport();
|
||||
await expect(page).toMatchScreenshot("room-list-collapse-fully-collapsed.png");
|
||||
|
||||
// Should be possible to expand by clicking on the separator
|
||||
await separator.click();
|
||||
currentBoundingBox = await leftPanelLocator.boundingBox();
|
||||
expect(currentBoundingBox.width).toBeGreaterThan(365);
|
||||
|
||||
// Collapse the panel again
|
||||
await resize(page, -300);
|
||||
|
||||
// Check that the panel can be expanded by dragging the separator
|
||||
const separatorBoundingBox = await separator.boundingBox();
|
||||
const mouseX = separatorBoundingBox.x + separatorBoundingBox.width / 2;
|
||||
const mouseY = separatorBoundingBox.y + separatorBoundingBox.height / 2;
|
||||
await page.mouse.move(mouseX, mouseY);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(mouseX + 400, mouseY);
|
||||
expect(currentBoundingBox.width).toBeGreaterThan(365);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
* 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 { 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a room is nested under a specific section using the treegrid aria-level hierarchy.
|
||||
* Section header rows sit at aria-level=1; room rows nested within a section sit at aria-level=2.
|
||||
* Verifies that the closest preceding aria-level=1 row is the expected section header.
|
||||
*/
|
||||
async function assertRoomInSection(page: Page, sectionName: string, roomName: string): Promise<void> {
|
||||
const roomList = getRoomList(page);
|
||||
const roomRow = roomList.getByRole("row", { name: `Open room ${roomName}` });
|
||||
// Room row must be at aria-level=2 (i.e. inside a section)
|
||||
await expect(roomRow).toHaveAttribute("aria-level", "2");
|
||||
// The closest preceding aria-level=1 row must be the expected section header.
|
||||
// XPath preceding:: axis returns nodes before the context in document order; [1] picks the nearest one.
|
||||
const closestSectionHeader = roomRow.locator(`xpath=preceding::*[@role="row" and @aria-level="1"][1]`);
|
||||
await expect(closestSectionHeader).toContainText(sectionName);
|
||||
}
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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 { type Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../../element-web-test";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import { getFilterCollapseButton, getFilterExpandButton, getPrimaryFilters, getRoomOptionsMenu } from "./utils";
|
||||
|
||||
test.describe("Room list filters and sort", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: true,
|
||||
},
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The toasts are displayed above the search section
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("Tombstoned rooms are not shown even when they receive updates", async ({ page, app, bot }) => {
|
||||
// This bug shows up with this setting turned on
|
||||
await app.settings.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, true);
|
||||
|
||||
/*
|
||||
We will first create a room named 'Old Room' and will invite the bot user to this room.
|
||||
We will also send a simple message in this room.
|
||||
*/
|
||||
const oldRoomId = await app.client.createRoom({ name: "Old Room" });
|
||||
await app.client.inviteUser(oldRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(oldRoomId);
|
||||
const response = await app.client.sendMessage(oldRoomId, "Hello!");
|
||||
|
||||
/*
|
||||
At this point, we haven't done anything interesting.
|
||||
So we expect 'Old Room' to show up in the room list.
|
||||
*/
|
||||
const roomListView = getRoomList(page);
|
||||
const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" });
|
||||
await expect(oldRoomTile).toBeVisible();
|
||||
|
||||
/*
|
||||
Now let's tombstone 'Old Room'.
|
||||
First we create a new room ('New Room') with the predecessor set to the old room..
|
||||
*/
|
||||
const newRoomId = await bot.createRoom({
|
||||
name: "New Room",
|
||||
creation_content: {
|
||||
predecessor: {
|
||||
event_id: response.event_id,
|
||||
room_id: oldRoomId,
|
||||
},
|
||||
},
|
||||
visibility: "public" as Visibility,
|
||||
});
|
||||
|
||||
/*
|
||||
Now we can send the tombstone event itself to the 'Old Room'.
|
||||
*/
|
||||
await app.client.sendStateEvent(oldRoomId, "m.room.tombstone", {
|
||||
body: "This room has been replaced",
|
||||
replacement_room: newRoomId,
|
||||
});
|
||||
|
||||
// Let's join the replaced room.
|
||||
await app.client.joinRoom(newRoomId);
|
||||
|
||||
// We expect 'Old Room' to be hidden from the room list.
|
||||
await expect(oldRoomTile).not.toBeVisible();
|
||||
|
||||
/*
|
||||
Let's say some user in the 'Old Room' changes their display name.
|
||||
This will send events to the all the rooms including 'Old Room'.
|
||||
Nevertheless, the replaced room should not be shown in the room list.
|
||||
*/
|
||||
await bot.setDisplayName("MyNewName");
|
||||
await expect(oldRoomTile).not.toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
let unReadDmId: string | undefined;
|
||||
let unReadRoomId: string | undefined;
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await app.client.joinRoom(unReadDmId);
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, favouriteId);
|
||||
|
||||
const lowPrioId = await app.client.createRoom({ name: "Low prio room" });
|
||||
await app.client.evaluate(async (client, id) => {
|
||||
await client.setRoomTag(id, "m.lowpriority", { order: 0.5 });
|
||||
}, lowPrioId);
|
||||
|
||||
await bot.createRoom({
|
||||
name: "invited room",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
|
||||
const mentionRoomId = await app.client.createRoom({ name: "room with mention" });
|
||||
await app.client.inviteUser(mentionRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(mentionRoomId);
|
||||
|
||||
const clientBot = await bot.prepareClient();
|
||||
await clientBot.evaluate(
|
||||
async (client, { mentionRoomId, userId }) => {
|
||||
await client.sendMessage(mentionRoomId, {
|
||||
// @ts-ignore ignore usage of MsgType.text
|
||||
"msgtype": "m.text",
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: [userId],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ mentionRoomId, userId: user.userId },
|
||||
);
|
||||
});
|
||||
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
const allFilters = await primaryFilters.locator("option").all();
|
||||
for (const filter of allFilters) {
|
||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(4);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(2);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(5);
|
||||
|
||||
await getFilterExpandButton(page).click();
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
||||
|
||||
await getFilterCollapseButton(page).click();
|
||||
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
||||
});
|
||||
|
||||
test(
|
||||
"unread filter should only match unread rooms that have a count",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(unReadDmId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than unread room or unread dm
|
||||
await roomListView.getByRole("option", { name: "Open room favourite room" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both rooms
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
|
||||
await expect(unreadDm).toBeVisible();
|
||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
test("should sort the room list alphabetically", async ({ page }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
await getRoomOptionsMenu(page).click();
|
||||
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
||||
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
|
||||
});
|
||||
|
||||
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
|
||||
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Empty room list", () => {
|
||||
/**
|
||||
* Get the empty state
|
||||
* @param page
|
||||
*/
|
||||
function getEmptyRoomList(page: Page) {
|
||||
return page.getByTestId("empty-room-list");
|
||||
}
|
||||
|
||||
test("should render the primary filters", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await expect(primaryFilters).toMatchScreenshot("collapsed-primary-filters.png");
|
||||
await getFilterExpandButton(page).click();
|
||||
await expect(primaryFilters).toMatchScreenshot("expanded-primary-filters.png");
|
||||
});
|
||||
|
||||
test(
|
||||
"should render the default placeholder when there is no filter",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
|
||||
await expect(page.getByRole("navigation", { name: "Room list" })).toMatchScreenshot(
|
||||
"room-panel-empty-room-list.png",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
[
|
||||
{ filter: "Unreads", action: "Show all chats" },
|
||||
{ filter: "Mentions", action: "See all activity" },
|
||||
{ filter: "Invites", action: "See all activity" },
|
||||
].forEach(({ filter, action }) => {
|
||||
test(
|
||||
`should render the placeholder for ${filter} filter`,
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await getFilterExpandButton(page).click();
|
||||
|
||||
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||
|
||||
await emptyRoomList.getByRole("button", { name: action }).click();
|
||||
await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
["People", "Rooms", "Favourite"].forEach((filter) => {
|
||||
test(
|
||||
`should render the placeholder for ${filter} filter`,
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await getFilterExpandButton(page).click();
|
||||
|
||||
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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 { test, expect } from "../../../element-web-test";
|
||||
import { getHeaderSection } from "./utils";
|
||||
|
||||
test.describe("Header section of the room list", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The toasts are displayed above the search section
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("should render the header section", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListHeader = getHeaderSection(page);
|
||||
await expect(roomListHeader).toMatchScreenshot("room-list-header.png");
|
||||
|
||||
const composeMenu = roomListHeader.getByRole("button", { name: "New conversation" });
|
||||
await composeMenu.click();
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||
|
||||
// Start chat should open the direct messages dialog
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// New room should open the room creation dialog
|
||||
await composeMenu.click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Create a private room" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
});
|
||||
|
||||
test("should render the header section for a space", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await app.client.createSpace({ name: "MySpace" });
|
||||
await page.getByRole("button", { name: "MySpace" }).click();
|
||||
|
||||
const roomListHeader = getHeaderSection(page);
|
||||
await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png");
|
||||
|
||||
await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
|
||||
await expect(roomListHeader.getByRole("button", { name: "New conversation" })).toBeVisible();
|
||||
|
||||
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
|
||||
await spaceMenu.click();
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-space-menu.png");
|
||||
|
||||
// It should open the space home
|
||||
await page.getByRole("menuitem", { name: "Space home" }).click();
|
||||
await expect(page.getByRole("main").getByRole("heading", { name: "MySpace" })).toBeVisible();
|
||||
|
||||
// It should open the invite dialog
|
||||
await spaceMenu.click();
|
||||
await page.getByRole("menuitem", { name: "Invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to MySpace" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should open the space preferences
|
||||
await spaceMenu.click();
|
||||
await page.getByRole("menuitem", { name: "Preferences" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should open the space settings
|
||||
await spaceMenu.click();
|
||||
await page.getByRole("menuitem", { name: "Space Settings" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 { test, expect } from "../../../element-web-test";
|
||||
import { getRoomListView } from "./utils";
|
||||
|
||||
test.describe("Room list panel", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The toasts are displayed above the search section
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
|
||||
// Populate the room list
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
});
|
||||
|
||||
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomListView(page);
|
||||
// Wait for the last room to be visible
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||
});
|
||||
|
||||
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
|
||||
await page.setViewportSize({ width: 575, height: 600 });
|
||||
const roomListPanel = getRoomListView(page);
|
||||
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 { test, expect } from "../../../element-web-test";
|
||||
import { getSearchSection } from "./utils";
|
||||
|
||||
test.describe("Search section of the room list", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The toasts are displayed above the search section
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const searchSection = getSearchSection(page);
|
||||
// exact=false to ignore the shortcut which is related to the OS
|
||||
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
|
||||
await expect(searchSection).toMatchScreenshot("search-section.png");
|
||||
});
|
||||
|
||||
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
|
||||
const searchSection = getSearchSection(page);
|
||||
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
|
||||
// The spotlight should be displayed
|
||||
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
|
||||
const searchSection = getSearchSection(page);
|
||||
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
|
||||
// The room directory should be displayed
|
||||
await expect(dialog).toBeVisible();
|
||||
// The public room filter should be displayed
|
||||
await expect(dialog.getByText("Public rooms")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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 { expect, test } from "../../../element-web-test";
|
||||
import { getPrimaryFilters, getRoomList, getSectionHeader } from "./utils";
|
||||
|
||||
test.describe("Room list sections", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_new_room_list", "feature_room_list_sections"],
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: true,
|
||||
},
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The toasts are displayed above the search section
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
});
|
||||
|
||||
test.describe("Section rendering", () => {
|
||||
test.beforeEach(async ({ app, user }) => {
|
||||
// Create regular rooms
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render sections with correct rooms in each", { tag: "@screenshot" }, 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);
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
|
||||
// All three section headers should be visible
|
||||
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
|
||||
await expect(getSectionHeader(page, "Chats")).toBeVisible();
|
||||
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
|
||||
|
||||
// Ensure all rooms are visible
|
||||
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("row", { name: "Open room low prio room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("row", { name: "Open room room0" })).toBeVisible();
|
||||
|
||||
await expect(roomList).toMatchScreenshot("room-list-sections.png");
|
||||
});
|
||||
|
||||
test("should only show non-empty sections", async ({ page, app }) => {
|
||||
// No low priority rooms created, only regular and favourite rooms
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
await client.setRoomTag(roomId, "m.favourite");
|
||||
}, favouriteId);
|
||||
|
||||
// Chats and Favourites sections should still be visible
|
||||
await expect(getSectionHeader(page, "Chats")).toBeVisible();
|
||||
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
|
||||
// Low Priority sections should not be visible
|
||||
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should render a flat list when there is only rooms in Chats section", async ({ page, app }) => {
|
||||
// All sections should not be visible
|
||||
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
|
||||
await expect(getSectionHeader(page, "Favourites")).not.toBeVisible();
|
||||
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
|
||||
// It should be a flat list (using listbox a11y role)
|
||||
await expect(page.getByRole("listbox", { name: "Room list", exact: true })).toBeVisible();
|
||||
await expect(getRoomList(page).getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Section collapse and expand", () => {
|
||||
[
|
||||
{ section: "Favourites", roomName: "favourite room", tag: "m.favourite" },
|
||||
{ section: "Low Priority", roomName: "low prio room", tag: "m.lowpriority" },
|
||||
].forEach(({ section, roomName, tag }) => {
|
||||
test(`should collapse and expand the ${section} section`, async ({ page, app }) => {
|
||||
const roomId = await app.client.createRoom({ name: roomName });
|
||||
if (tag) {
|
||||
await app.client.evaluate(
|
||||
async (client, { roomId, tag }) => {
|
||||
await client.setRoomTag(roomId, tag);
|
||||
},
|
||||
{ roomId, tag },
|
||||
);
|
||||
}
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
const sectionHeader = getSectionHeader(page, section);
|
||||
|
||||
// The room should be visible
|
||||
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
|
||||
|
||||
// Collapse the section
|
||||
await sectionHeader.click();
|
||||
|
||||
// The room should no longer be visible
|
||||
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).not.toBeVisible();
|
||||
|
||||
// The section header should still be visible
|
||||
await expect(sectionHeader).toBeVisible();
|
||||
|
||||
// Expand the section again
|
||||
await sectionHeader.click();
|
||||
|
||||
// The room should be visible again
|
||||
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test("should render collapsed section", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
await client.setRoomTag(roomId, "m.favourite");
|
||||
}, favouriteId);
|
||||
|
||||
await app.client.createRoom({ name: "regular room" });
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
|
||||
// Collapse the Favourites section
|
||||
await getSectionHeader(page, "Favourites").click();
|
||||
|
||||
// Verify favourite room is hidden but regular room is still visible
|
||||
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).not.toBeVisible();
|
||||
await expect(roomList.getByRole("row", { name: "Open room regular room" })).toBeVisible();
|
||||
|
||||
await expect(roomList).toMatchScreenshot("room-list-sections-collapsed.png");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Rooms placement in sections", () => {
|
||||
test("should move a room between sections when tags change", async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: "my room" });
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
|
||||
// Flat list because there is only rooms in the Chats section
|
||||
let roomItem = roomList.getByRole("option", { name: "Open room my room" });
|
||||
await expect(roomItem).toBeVisible();
|
||||
|
||||
// Favourite the room via context menu
|
||||
await roomItem.click({ button: "right" });
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// The Favourites section header should now be visible and the room should be under it
|
||||
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
|
||||
roomItem = roomList.getByRole("row", { name: "Open room my room" });
|
||||
await expect(roomItem).toBeVisible();
|
||||
|
||||
// Unfavourite the room
|
||||
await roomItem.hover();
|
||||
await roomItem.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Mark the room as low priority via context menu
|
||||
roomItem = roomList.getByRole("option", { name: "Open room my room" });
|
||||
await roomItem.click({ button: "right" });
|
||||
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
|
||||
|
||||
// The Low Priority section header should now be visible and the room should be under it
|
||||
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
|
||||
roomItem = roomList.getByRole("row", { name: "Open room my room" });
|
||||
await expect(roomItem).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test("should show unread indicator on section header", async ({ page, app, bot }) => {
|
||||
// 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);
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
|
||||
// Invite the bot and have it send a message to generate an unread
|
||||
await app.client.inviteUser(favouriteId, bot.credentials.userId);
|
||||
await bot.joinRoom(favouriteId);
|
||||
await bot.sendMessage(favouriteId, "Hello from bot!");
|
||||
|
||||
let sectionHeader = getSectionHeader(page, "Favourites", true);
|
||||
await expect(sectionHeader).toBeVisible();
|
||||
|
||||
// Open the room to mark it as read
|
||||
await roomList.getByRole("row", { name: "Open room favourite room" }).click();
|
||||
|
||||
// The section should no longer be unread
|
||||
sectionHeader = getSectionHeader(page, "Favourites", false);
|
||||
await expect(sectionHeader).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Sections and filters interaction", () => {
|
||||
test("should not show Favourite and Low Priority filters when sections are enabled", async ({ page, app }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
// Expand the filter list to see all filters
|
||||
const expandButton = primaryFilters.getByRole("button", { name: "Expand filter list" });
|
||||
await expandButton.click();
|
||||
|
||||
// Favourite and Low Priority filters should NOT be visible since sections handle them
|
||||
await expect(primaryFilters.getByRole("option", { name: "Favourite" })).not.toBeVisible();
|
||||
|
||||
// Other filters should still be present
|
||||
await expect(primaryFilters.getByRole("option", { name: "People" })).toBeVisible();
|
||||
await expect(primaryFilters.getByRole("option", { name: "Rooms" })).toBeVisible();
|
||||
await expect(primaryFilters.getByRole("option", { name: "Unread" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should maintain sections when a filter is applied", async ({ page, app, bot }) => {
|
||||
// Create a favourite room with unread messages
|
||||
const favouriteId = await app.client.createRoom({ name: "fav with unread" });
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
await client.setRoomTag(roomId, "m.favourite");
|
||||
}, favouriteId);
|
||||
await app.client.inviteUser(favouriteId, bot.credentials.userId);
|
||||
await bot.joinRoom(favouriteId);
|
||||
await bot.sendMessage(favouriteId, "Hello from favourite!");
|
||||
|
||||
// Create a regular room with unread messages
|
||||
const regularId = await app.client.createRoom({ name: "regular with unread" });
|
||||
await app.client.inviteUser(regularId, bot.credentials.userId);
|
||||
await bot.joinRoom(regularId);
|
||||
await bot.sendMessage(regularId, "Hello from regular!");
|
||||
|
||||
// Create a room without unread
|
||||
await app.client.createRoom({ name: "no unread room" });
|
||||
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
// Apply the Unread filter
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Only rooms with unreads should be visible
|
||||
await expect(roomList.getByRole("row", { name: "fav with unread" })).toBeVisible();
|
||||
await expect(roomList.getByRole("row", { name: "regular with unread" })).toBeVisible();
|
||||
await expect(roomList.getByRole("row", { name: "no unread room" })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* 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 { type Page } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../../element-web-test";
|
||||
import { type Bot } from "../../../pages/bot";
|
||||
import { type ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { getRoomList } from "./utils";
|
||||
|
||||
test.describe("Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
},
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The toasts are displayed above the search section
|
||||
await app.closeVerifyToast();
|
||||
await app.closeNotificationToast();
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
test.slow();
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
|
||||
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
|
||||
await page.getByRole("button", { name: "User menu" }).hover();
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" });
|
||||
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
|
||||
// Default settings should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");
|
||||
|
||||
// It should make the room muted
|
||||
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await roomItem.scrollIntoViewIfNeeded();
|
||||
|
||||
// Not hovered, the room decoration should be the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
|
||||
// During hover the room decoration should still be the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
|
||||
await roomItem.hover();
|
||||
|
||||
// On hover, the room should show the muted icon
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");
|
||||
|
||||
roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
|
||||
// The Mute room option should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await app.scrollListToBottom(roomListView);
|
||||
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
await roomListView.getByRole("option", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Shortcuts", () => {
|
||||
test("should select the next room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should select the previous room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should select the last room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should select the next unread room", async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "1 notification" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
||||
|
||||
// Make sure the room with the unread is visible before we press the keyboard action to select it
|
||||
await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Keyboard navigation", () => {
|
||||
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("option", { name: "Open room room28" });
|
||||
|
||||
// open the room
|
||||
await room29.click();
|
||||
// put focus back on the room list item
|
||||
await room29.click();
|
||||
await expect(room29).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await expect(room28).toBeFocused();
|
||||
await expect(room29).not.toBeFocused();
|
||||
|
||||
await page.keyboard.press("ArrowUp");
|
||||
await expect(room29).toBeFocused();
|
||||
await expect(room28).not.toBeFocused();
|
||||
});
|
||||
|
||||
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||
|
||||
await room29.click();
|
||||
// put focus back on the room list item
|
||||
await room29.click();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(moreButton).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(notificationButton).toBeFocused();
|
||||
|
||||
// Open the menu
|
||||
await page.keyboard.press("Enter");
|
||||
// Wait for the menu to be open
|
||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await page.keyboard.press("Escape");
|
||||
// Focus should be back on the notification button
|
||||
await expect(notificationButton).toBeFocused();
|
||||
});
|
||||
|
||||
test("should navigate to the top and then bottom of the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const topRoom = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
|
||||
// open the room
|
||||
await topRoom.click();
|
||||
// put focus back on the room list item
|
||||
await topRoom.click();
|
||||
await expect(topRoom).toBeFocused();
|
||||
|
||||
await page.keyboard.press("End");
|
||||
const bottomRoom = roomListView.getByRole("option", { name: "Open room room0" });
|
||||
await expect(bottomRoom).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Home");
|
||||
const topRoomAgain = roomListView.getByRole("option", { name: "Open room room29" });
|
||||
await expect(topRoomAgain).toBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Avatar decoration", () => {
|
||||
test.use({ labsFlags: ["feature_video_rooms", "feature_new_room_list"] });
|
||||
|
||||
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
// @ts-ignore Visibility enum is not accessible
|
||||
await app.client.createRoom({ name: "public room", visibility: "public" });
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("option", { name: "public room" });
|
||||
|
||||
await expect(publicRoom).toBeVisible();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||
});
|
||||
|
||||
test("should be a low priority room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
// @ts-ignore Visibility enum is not accessible
|
||||
await app.client.createRoom({ name: "low priority room", visibility: "public" });
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
|
||||
|
||||
// Make room low priority
|
||||
await publicRoom.click({ button: "right" });
|
||||
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
|
||||
|
||||
// Should have low priority decoration
|
||||
await expect(publicRoom.locator(".mx_RoomAvatarView_icon")).toHaveAccessibleName(
|
||||
"This is a low priority room",
|
||||
);
|
||||
|
||||
// focus the header to avoid to have hover decoration
|
||||
await page.getByTestId("room-list-header").click();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-low-priority.png");
|
||||
});
|
||||
|
||||
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "New video room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("video room");
|
||||
// Make it public to avoid any crypto setup toasts
|
||||
await page.getByRole("button", { name: "Room visibility" }).click();
|
||||
await page.getByRole("option", { name: "Public room" }).click();
|
||||
await page.getByRole("textbox", { name: "Room address" }).fill("video-room");
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const videoRoom = roomListView.getByRole("option", { name: "video room" });
|
||||
await expect(videoRoom).toHaveAttribute("aria-selected", "true"); // wait for room list update
|
||||
|
||||
// Ensure we highlight the video
|
||||
await videoRoom.click();
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).hover();
|
||||
|
||||
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Notification decoration", () => {
|
||||
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
await bot.createRoom({
|
||||
name: "invited room",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
});
|
||||
|
||||
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "2 notifications" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
});
|
||||
|
||||
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mention" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const clientBot = await bot.prepareClient();
|
||||
await clientBot.evaluate(
|
||||
async (client, { roomId, userId }) => {
|
||||
await client.sendMessage(roomId, {
|
||||
// @ts-ignore ignore usage of MsgType.text
|
||||
"msgtype": "m.text",
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: [userId],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ roomId, userId: user.userId },
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
|
||||
async function checkMessagePreview(page: Page, app: ElementAppPage, bot: Bot) {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomId = await app.client.createRoom({ name: "activity" });
|
||||
|
||||
// focus the user menu to avoid to have hover decoration
|
||||
await page.getByRole("button", { name: "User menu" }).focus();
|
||||
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
||||
}
|
||||
|
||||
test(
|
||||
"should render a message preview when enable in settings",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await app.settings.openUserSettings("Preferences");
|
||||
await page.getByRole("switch", { name: "Show message previews" }).click();
|
||||
await app.closeDialog();
|
||||
|
||||
await checkMessagePreview(page, app, bot);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"should render a message preview when enabled in header",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page.getByRole("button", { name: "Room Options" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click();
|
||||
|
||||
await checkMessagePreview(page, app, bot);
|
||||
},
|
||||
);
|
||||
|
||||
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const otherRoomId = await app.client.createRoom({ name: "other room" });
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "activity" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
await app.settings.openUserSettings("Notifications");
|
||||
await page
|
||||
.getByRole("switch", { name: "Show all activity in the room list (dots or number of unread messages)" })
|
||||
.check();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Switch to the other room to avoid the notification to be cleared
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
|
||||
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mark as unread" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "mark as unread" });
|
||||
await room.click({ button: "right" });
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
|
||||
// focus the header to avoid to have hover decoration
|
||||
await page.getByTestId("room-list-header").click();
|
||||
|
||||
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
|
||||
});
|
||||
|
||||
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "silent" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("option", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 Locator, type Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
export function getRoomList(page: Page): Locator {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room list header
|
||||
* @param page
|
||||
*/
|
||||
export function getRoomListHeader(page: Page): Locator {
|
||||
return page.getByTestId("room-list-header");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a section header toggle button by section name
|
||||
* @param page
|
||||
* @param sectionName The display name of the section
|
||||
* @param isUnread Whether to look for the unread version of the section header
|
||||
*/
|
||||
export function getSectionHeader(page: Page, sectionName: string, isUnread = false): Locator {
|
||||
return getRoomList(page).getByRole("gridcell", {
|
||||
name: isUnread ? `Toggle ${sectionName} section with unread room(s)` : `Toggle ${sectionName} section`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the primary filters container
|
||||
* @param page
|
||||
*/
|
||||
export function getPrimaryFilters(page: Page): Locator {
|
||||
return page.getByTestId("primary-filters");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room options menu button in the room list header
|
||||
* @param page
|
||||
*/
|
||||
export function getRoomOptionsMenu(page: Page): Locator {
|
||||
return page.getByRole("button", { name: "Room Options" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter list expand button in the room list header
|
||||
* @param page
|
||||
*/
|
||||
export function getFilterExpandButton(page: Page): Locator {
|
||||
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter list collapse button in the room list header
|
||||
* @param page
|
||||
*/
|
||||
export function getFilterCollapseButton(page: Page): Locator {
|
||||
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header section of the room list
|
||||
* @param page
|
||||
*/
|
||||
export function getHeaderSection(page: Page) {
|
||||
return page.getByTestId("room-list-header");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room list view
|
||||
* @param page
|
||||
*/
|
||||
export function getRoomListView(page: Page) {
|
||||
return page.getByRole("navigation", { name: "Room list" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the search section of the room list
|
||||
* @param page
|
||||
*/
|
||||
export function getSearchSection(page: Page) {
|
||||
return page.getByRole("search");
|
||||
}
|
||||
Reference in New Issue
Block a user