/* Copyright 2023 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ import { type Locator, type Page, expect } from "@playwright/test"; import { Settings } from "./settings"; import { Client } from "./client"; import { Timeline } from "./timeline"; import { Spotlight } from "./Spotlight"; /** * A set of utility methods for interacting with the Element-Web UI. */ export class ElementAppPage { public constructor(public readonly page: Page) {} // We create these lazily on first access to avoid calling setup code which might cause conflicts, // e.g. the network routing code in the client subfixture. private _settings?: Settings; public get settings(): Settings { if (!this._settings) this._settings = new Settings(this.page); return this._settings; } private _client?: Client; public get client(): Client { if (!this._client) this._client = new Client(this.page); return this._client; } private _timeline?: Timeline; public get timeline(): Timeline { if (!this._timeline) this._timeline = new Timeline(this.page); return this._timeline; } /** * Open the top left user menu, returning a Locator to the resulting context menu. */ public async openUserMenu(): Promise { return this.settings.openUserMenu(); } /** * Open room creation dialog. */ public async openCreateRoomDialog(): Promise { await this.page.getByRole("button", { name: "Add room", exact: true }).click(); await this.page.getByRole("menuitem", { name: "New room", exact: true }).click(); return this.page.locator(".mx_CreateRoomDialog"); } /** * Close dialog currently open dialog */ public async closeDialog(): Promise { return this.settings.closeDialog(); } public async getClipboard(): Promise { return await this.page.evaluate(() => navigator.clipboard.readText()); } /** * Opens the given room by name. The room must be visible in the * room list, but the room list may be folded horizontally, and the * room may contain unread messages. * * @param name The exact room name to find and click on/open. */ public async viewRoomByName(name: string): Promise { // We look for the room inside the room list, which is a tree called Rooms. // // There are 3 cases: // - the room list is folded: // then the aria-label on the room tile is the name (with nothing extra) // - the room list is unfolder and the room has messages: // then the aria-label contains the unread count, but the title of the // div inside the titleContainer equals the room name // - the room list is unfolded and the room has no messages: // then the aria-label is the name and so is the title of a div // // So by matching EITHER title=name OR aria-label=name we find this exact // room in all three cases. return this.page .getByRole("tree", { name: "Rooms" }) .locator(`[title="${name}"],[aria-label="${name}"]`) .first() .click(); } public async viewRoomById(roomId: string): Promise { await this.page.goto(`/#/room/${roomId}`); } /** * Get the composer element * @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer */ public getComposer(isRightPanel?: boolean): Locator { const panelClass = isRightPanel ? ".mx_RightPanel" : ".mx_RoomView_body"; return this.page.locator(`${panelClass} .mx_MessageComposer`); } /** * Get the composer input field * @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer */ public getComposerField(isRightPanel?: boolean): Locator { return this.getComposer(isRightPanel).locator("[contenteditable]"); } /** * Open the message composer kebab menu * @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer */ public async openMessageComposerOptions(isRightPanel?: boolean): Promise { const composer = this.getComposer(isRightPanel); await composer.getByRole("button", { name: "More options", exact: true }).click(); return this.page.getByRole("menu"); } /** * Returns the space panel space button based on a name. The space * must be visible in the space panel * @param name The space name to find */ public async getSpacePanelButton(name: string): Promise { const button = this.page.getByRole("button", { name: name }); await expect(button).toHaveClass(/mx_SpaceButton/); return button; } /** * Opens the given space home by name. The space must be visible in * the space list. * @param name The space name to find and click on/open. */ public async viewSpaceHomeByName(name: string): Promise { const button = await this.getSpacePanelButton(name); return button.dblclick(); } /** * Opens the given space by name. The space must be visible in the * space list. * @param name The space name to find and click on/open. */ public async viewSpaceByName(name: string): Promise { const button = await this.getSpacePanelButton(name); return button.click(); } public async getClipboardText(): Promise { return this.page.evaluate("navigator.clipboard.readText()"); } public async openSpotlight(): Promise { const spotlight = new Spotlight(this.page); await spotlight.open(); return spotlight; } }