/* * Copyright 2024 New Vector Ltd. * Copyright 2024 The Matrix.org Foundation C.I.C. * * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only * Please see LICENSE files in the repository root for full details. */ import { Page } from "@playwright/test"; import { test as base, expect } from "../../element-web-test"; import { Client } from "../../pages/client"; import { ElementAppPage } from "../../pages/ElementAppPage"; import { Bot } from "../../pages/bot"; /** * Set up for pinned message tests. */ export const test = base.extend<{ room1Name?: string; room1: { name: string; roomId: string }; util: Helpers; }>({ displayName: "Alice", botCreateOpts: { displayName: "Other User" }, room1Name: "Room 1", room1: async ({ room1Name: name, app, user, bot }, use) => { const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] }); await use({ name, roomId }); }, util: async ({ page, app, bot }, use) => { await use(new Helpers(page, app, bot)); }, }); export class Helpers { constructor( private page: Page, private app: ElementAppPage, private bot: Bot, ) {} /** * Sends messages into given room as a bot * @param room - the name of the room to send messages into * @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf` */ async receiveMessages(room: string | { name: string }, messages: string[]) { await this.sendMessageAsClient(this.bot, room, messages); } /** * Use the supplied client to send messages or perform actions as specified by * the supplied {@link Message} items. */ private async sendMessageAsClient(cli: Client, roomName: string | { name: string }, messages: string[]) { const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name); const roomId = await room.evaluate((room) => room.roomId); for (const message of messages) { await cli.sendMessage(roomId, { body: message, msgtype: "m.text" }); // TODO: without this wait, some tests that send lots of messages flake // from time to time. I (andyb) have done some investigation, but it // needs more work to figure out. The messages do arrive over sync, but // they never appear in the timeline, and they never fire a // Room.timeline event. I think this only happens with events that refer // to other events (e.g. replies), so it might be caused by the // referring event arriving before the referred-to event. await this.page.waitForTimeout(100); } } /** * Find a room by its name * @param roomName * @private */ private async findRoomByName(roomName: string) { return this.app.client.evaluateHandle((cli, roomName) => { return cli.getRooms().find((r) => r.name === roomName); }, roomName); } /** * Open the room with the supplied name. */ async goTo(room: string | { name: string }) { await this.app.viewRoomByName(typeof room === "string" ? room : room.name); } /** * Get the timeline tile for the given message * @param message */ getEventTile(message: string) { return this.page.locator(".mx_EventTile", { hasText: message }); } /** * Pin the given message from the quick actions * @param message * @param unpin */ async pinMessageFromQuickActions(message: string, unpin = false) { const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); await timelineMessage.hover(); await this.page.getByRole("button", { name: unpin ? "Unpin" : "Pin", exact: true }).click(); } /** * Pin the given messages from the quick actions * @param messages * @param unpin */ async pinMessagesFromQuickActions(messages: string[], unpin = false) { for (const message of messages) { await this.pinMessageFromQuickActions(message, unpin); } } /** * Pin the given message from the contextual menu * @param message */ async pinMessage(message: string) { const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); await timelineMessage.click({ button: "right" }); await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click(); } /** * Pin the given messages * @param messages */ async pinMessages(messages: string[]) { for (const message of messages) { await this.pinMessage(message); } } /** * Open the room info panel */ async openRoomInfo() { await this.page.getByRole("button", { name: "Room info" }).nth(1).click(); } /** * Assert that the pinned count in the room info is correct * Open the room info and check the pinned count * @param count */ async assertPinnedCountInRoomInfo(count: number) { await expect(this.page.getByRole("menuitem", { name: "Pinned messages" })).toHaveText( `Pinned messages${count}`, ); } /** * Open the pinned messages list */ async openPinnedMessagesList() { await this.page.getByRole("menuitem", { name: "Pinned messages" }).click(); } /** * Return the right panel */ public getRightPanel() { return this.page.locator("#mx_RightPanel"); } /** * Assert that the pinned message list contains the given messages * @param messages */ async assertPinnedMessagesList(messages: string[]) { const rightPanel = this.getRightPanel(); await expect(rightPanel.getByRole("heading", { name: "Pinned messages" })).toHaveText( `${messages.length} Pinned messages`, ); const list = rightPanel.getByRole("list"); await expect(list.getByRole("listitem")).toHaveCount(messages.length); for (const message of messages) { await expect(list.getByText(message)).toBeVisible(); } } /** * Assert that the pinned message list is empty */ async assertEmptyPinnedMessagesList() { const rightPanel = this.getRightPanel(); await expect(rightPanel).toMatchScreenshot(`pinned-messages-list-empty.png`, { // hide the tooltip "Room information" to avoid flakiness css: ` [data-floating-ui-portal] { display: none !important; } `, }); } /** * Open the unpin all dialog */ async openUnpinAllDialog() { await this.openRoomInfo(); await this.openPinnedMessagesList(); await this.page.getByRole("button", { name: "Unpin all" }).click(); } /** * Return the unpin all dialog */ getUnpinAllDialog() { return this.page.locator(".mx_Dialog", { hasText: "Unpin all messages?" }); } /** * Click on the Continue button of the unoin all dialog */ async confirmUnpinAllDialog() { await this.getUnpinAllDialog().getByRole("button", { name: "Continue" }).click(); } /** * Go back from the pinned messages list */ async backPinnedMessagesList() { await this.page.locator("#mx_RightPanel").getByTestId("base-card-back-button").click(); } /** * Open the contextual menu of a message in the pin message list and click on unpin * @param message */ async unpinMessageFromMessageList(message: string) { const item = this.getRightPanel().getByRole("list").getByRole("listitem").filter({ hasText: message, }); await item.getByRole("button").click(); await this.page.getByRole("menu", { name: "Open menu" }).getByRole("menuitem", { name: "Unpin" }).click(); } /** * Return the banner * @private */ public getBanner() { return this.page.getByTestId("pinned-message-banner"); } /** * Assert that the banner contains the given message * @param msg */ async assertMessageInBanner(msg: string) { await expect(this.getBanner().getByText(msg)).toBeVisible(); } /** * Return the view all button */ public getViewAllButton() { return this.page.getByRole("button", { name: "View all" }); } /** * Return the close list button */ public getCloseListButton() { return this.page.getByRole("button", { name: "Close list" }); } } export { expect };