/* * 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-core"; import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers"; import { test, expect } from "../../element-web-test"; import { Bot } from "../../pages/bot"; import { type ElementAppPage } from "../../pages/ElementAppPage"; import { type Credentials } from "../../plugins/homeserver"; test.describe("Event List Summary", () => { test.use({ displayName: "Finch", }); test( "should display a single join message on its own", { tag: "@screenshot" }, async ({ app, homeserver, page, user }) => { const { bot, roomId } = await setupRoom(app, homeserver, page, user); // When the bot joins the room await bot.joinRoom(roomId); // Then we say that in a generic event list summary await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "MyBot joined the room", }), ).toBeVisible(); await replaceBotIds(page, bot); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "bot_joined_the_room.png", ignoreTimestamps, ); }, ); test( "should display a single ban message on its own, with the user hidden", { tag: "@screenshot" }, async ({ app, homeserver, page, user }) => { const { bot, roomId } = await setupRoom(app, homeserver, page, user); // Given the bot is in the room await bot.joinRoom(roomId); await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "MyBot joined the room", }), ).toBeVisible(); // And we said something to separate out the messages await app.client.sendMessage(roomId, "Saying something"); // When we ban the bot await app.client.ban(roomId, bot.credentials!.userId); // Then we say that, but the name is hidden await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "banned", }), ).toBeVisible(); await replaceBotIds(page, bot); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "bot_was_banned.png", ignoreTimestamps, ); }, ); test( "should display multiple join/leave messages as a group", { tag: "@screenshot" }, async ({ app, homeserver, page, user }) => { const { bot, roomId } = await setupRoom(app, homeserver, page, user); // Given the bot is in the room await bot.joinRoom(roomId); await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "MyBot joined the room", }), ).toBeVisible(); // When we perform multiple actions on it await app.client.kick(roomId, bot.credentials!.userId); await app.client.inviteUser(roomId, bot.credentials!.userId); await bot.joinRoom(roomId); // Then those actions are gathered into a single summary await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "and joined", }), ).toBeVisible(); await replaceBotIds(page, bot); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "multiple_join_leave_messages.png", ignoreTimestamps, ); }, ); test( "should display multiple messages as a group, and hide banned names", { tag: "@screenshot" }, async ({ app, homeserver, page, user }) => { const { bot, roomId } = await setupRoom(app, homeserver, page, user); // Given the bot is in the room await bot.joinRoom(roomId); await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "MyBot joined the room", }), ).toBeVisible(); // When we perform multiple actions on it, including a ban await app.client.ban(roomId, bot.credentials!.userId); await app.client.unban(roomId, bot.credentials!.userId); await app.client.inviteUser(roomId, bot.credentials!.userId); await bot.joinRoom(roomId); // Then those actions are gathered into a single summary, with the name hidden await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "and joined", }), ).toBeVisible(); await replaceBotIds(page, bot); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "multiple_join_ban_messages.png", ignoreTimestamps, ); }, ); test( "should display join/leave messages for multiple people as a group", { tag: "@screenshot" }, async ({ app, homeserver, page, user }) => { const { bot, roomId } = await setupRoom(app, homeserver, page, user); // Given the bot is in the room const bot2 = new Bot(page, homeserver, { displayName: "MyBot2", autoAcceptInvites: false, }); await bot2.prepareClient(); await app.client.inviteUser(roomId, bot2.credentials!.userId); await app.client.sendMessage(roomId, "I invited MyBot2..."); await bot.joinRoom(roomId); await bot2.joinRoom(roomId); await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "MyBot2 joined the room", }), ).toBeVisible(); // When we perform multiple actions on both bots await app.client.kick(roomId, bot.credentials!.userId); await app.client.kick(roomId, bot2.credentials!.userId); await app.client.inviteUser(roomId, bot.credentials!.userId); await app.client.inviteUser(roomId, bot2.credentials!.userId); await bot.joinRoom(roomId); await bot2.joinRoom(roomId); // Then those actions are gathered into a single summary await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "joined, were removed, were invited, and joined", }), ).toBeVisible(); await expect(page.locator('div[aria-label="3 members"]')).toBeVisible(); await replaceBotIds(page, bot, bot2); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "multiple_people_join_leave_messages.png", ignoreTimestampsRightColumnAndHeader, ); // And when we expand the summary // Note: we can't include "expand" in the screenshot because it // moves around, but at least we know it exists because we click it // here. await page.getByRole("button", { name: "expand" }).nth(3).click(); // Make sure the mouse is in a consistent position to avoid flaking await page.getByRole("textbox").hover(); // Then we see all the individual actions await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "removed MyBot2", }), ).toBeVisible(); await replaceBotIds(page, bot, bot2); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "multiple_people_join_leave_messages_expanded.png", ignoreTimestampsRightColumnAndHeader, ); }, ); test( "should display join/ban messages for multiple people as a group, with banned names hidden", { tag: "@screenshot" }, async ({ app, homeserver, page, user }) => { const { bot, roomId } = await setupRoom(app, homeserver, page, user); // Given the bot is in the room const bot2 = new Bot(page, homeserver, { displayName: "MyBot2 with very long display name causing wrapping", autoAcceptInvites: false, }); await bot2.prepareClient(); await app.client.inviteUser(roomId, bot2.credentials!.userId); await app.client.sendMessage(roomId, "I invited MyBot2..."); await bot.joinRoom(roomId); await bot2.joinRoom(roomId); await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "MyBot2 with very long display name causing wrapping joined the room", }), ).toBeVisible(); // When we ban bot1 but not bot2 await app.client.ban(roomId, bot.credentials!.userId); await app.client.unban(roomId, bot.credentials!.userId); await app.client.kick(roomId, bot2.credentials!.userId); await app.client.inviteUser(roomId, bot.credentials!.userId); await app.client.inviteUser(roomId, bot2.credentials!.userId); await bot.joinRoom(roomId); await bot2.joinRoom(roomId); // Then those actions are gathered into a single summary, with banned names hidden await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "was removed, was invited, and joined", }), ).toBeVisible(); await expect(page.locator('div[aria-label="3 members"]')).toBeVisible(); await replaceBotIds(page, bot, bot2); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "multiple_people_ban_messages.png", ignoreTimestampsRightColumnAndHeader, ); // And when we expand the summary // Note: we can't include "expand" in the screenshot because it // moves around, but at least we know it exists because we click it // here. await page.getByRole("button", { name: "expand" }).nth(3).click(); // Make sure the mouse is in a consistent position to avoid flaking await page.getByRole("textbox").hover(); // Then we see all the individual actions, with banned names hidden await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: "removed MyBot2", }), ).toBeVisible(); await replaceBotIds(page, bot, bot2); await expect(page.locator(".mx_RoomView_timeline")).toMatchScreenshot( "multiple_people_ban_messages_expanded.png", ignoreTimestampsRightColumnAndHeader, ); }, ); }); const ignoreTimestamps = { css: ".mx_MessageTimestamp,.mx_TopUnreadMessagesBar { visibility: hidden; },", }; const ignoreTimestampsRightColumnAndHeader = { css: ".mx_MessageTimestamp," + ".mx_GenericEventListSummary_toggle," + ".mx_ReadReceiptGroup," + ".mx_RoomHeader, " + ".mx_TopUnreadMessagesBar " + "{ visibility: hidden; }", }; /** * Create a room, join it, create a bot and invite it to the room. */ async function setupRoom(app: ElementAppPage, homeserver: StartedHomeserverContainer, page: Page, user: Credentials) { const roomId = await app.client.createRoom({ name: "My room" }); await page.goto(`/#/room/${roomId}`); await expect( page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", { hasText: `${user.displayName} created and configured the room.`, }), ).toBeVisible(); const bot = new Bot(page, homeserver, { displayName: "MyBot", autoAcceptInvites: false, }); await bot.prepareClient(); await app.client.inviteUser(roomId, bot.credentials!.userId); await app.client.sendMessage(roomId, "I invited MyBot..."); return { bot, roomId }; } /** * Find the ID of the supplied bot in the page and replace it with a known string. * * This allows us to create consistent screenshots. */ async function replaceBotIds(page: Page, bot: Bot, bot2?: Bot) { await page.evaluate( ([bot1UserId, bot2UserId]) => { for (const el of document.querySelectorAll("div.mx_TextualEvent")) { const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT); while (walker.nextNode()) { const node = walker.currentNode; node.textContent = node.textContent!.replaceAll(bot1UserId, "<>"); node.textContent = node.textContent.replaceAll(bot2UserId, "<>"); } } }, [bot.credentials!.userId, bot2?.credentials?.userId ?? "no_bot_2_to_replace"], ); }