From 9a8ffbe0bd0b239d4a035100e2f6aa16ddd23cb4 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Tue, 14 Apr 2026 12:49:27 +0100 Subject: [PATCH] playwright-common utilities for handling toasts (#33119) * playwright-common utilities for handling toasts * Set element-web-playwright-common version to 3.1.0 * Add comments to explain the linear hierarchy of fixtures --- .../e2e/crypto/device-verification.spec.ts | 8 +- apps/web/playwright/element-web-test.ts | 5 - apps/web/playwright/pages/toasts.ts | 53 --------- packages/playwright-common/package.json | 2 +- .../playwright-common/src/fixtures/toasts.ts | 109 ++++++++++++++++++ .../playwright-common/src/fixtures/user.ts | 5 +- 6 files changed, 119 insertions(+), 63 deletions(-) delete mode 100644 apps/web/playwright/pages/toasts.ts create mode 100644 packages/playwright-common/src/fixtures/toasts.ts diff --git a/apps/web/playwright/e2e/crypto/device-verification.spec.ts b/apps/web/playwright/e2e/crypto/device-verification.spec.ts index 1a1731e6ae..07fa4ed9d8 100644 --- a/apps/web/playwright/e2e/crypto/device-verification.spec.ts +++ b/apps/web/playwright/e2e/crypto/device-verification.spec.ts @@ -21,7 +21,6 @@ import { waitForVerificationRequest, } from "./utils"; import { type Bot } from "../../pages/bot"; -import { Toasts } from "../../pages/toasts.ts"; import type { ElementAppPage } from "../../pages/ElementAppPage.ts"; test.describe("Device verification", { tag: "@no-webkit" }, () => { @@ -82,7 +81,11 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { ); // Regression test for https://github.com/element-hq/element-web/issues/29110 - test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => { + test("No toast after verification, even if the secrets take a while to arrive", async ({ + page, + credentials, + toasts, + }) => { // Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens // when we are in an encrypted room. await aliceBotClient.createRoom({ @@ -121,7 +124,6 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { await infoDialog.getByRole("button", { name: "Got it" }).click(); // There should be no toast (other than the notifications one) - const toasts = new Toasts(page); await toasts.rejectToast("Notifications"); await toasts.assertNoToasts(); diff --git a/apps/web/playwright/element-web-test.ts b/apps/web/playwright/element-web-test.ts index f7c6f5b8e2..cae16cce8d 100644 --- a/apps/web/playwright/element-web-test.ts +++ b/apps/web/playwright/element-web-test.ts @@ -24,7 +24,6 @@ import type { IConfigOptions } from "../src/IConfigOptions"; import { type Credentials } from "./plugins/homeserver"; import { ElementAppPage } from "./pages/ElementAppPage"; import { Crypto } from "./pages/crypto"; -import { Toasts } from "./pages/toasts"; import { Bot, type CreateBotOpts } from "./pages/bot"; import { Webserver } from "./plugins/webserver"; import { type WorkerOptions, type Services, test as base } from "./services"; @@ -52,7 +51,6 @@ export interface TestFixtures extends BaseTestFixtures { crypto: Crypto; room?: { roomId: string }; - toasts: Toasts; uut?: Locator; // Unit Under Test, useful place to refer a prepared locator botCreateOpts: CreateBotOpts; bot: Bot; @@ -92,9 +90,6 @@ export const test = base.extend({ crypto: async ({ page, homeserver, request }, use) => { await use(new Crypto(page, homeserver, request)); }, - toasts: async ({ page }, use) => { - await use(new Toasts(page)); - }, botCreateOpts: {}, bot: async ({ page, homeserver, botCreateOpts, user }, use, testInfo) => { diff --git a/apps/web/playwright/pages/toasts.ts b/apps/web/playwright/pages/toasts.ts deleted file mode 100644 index 80ee3c9f26..0000000000 --- a/apps/web/playwright/pages/toasts.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 The Matrix.org Foundation C.I.C. - -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, expect, type Locator } from "@playwright/test"; - -export class Toasts { - public constructor(private readonly page: Page) {} - - /** - * Assert that a toast with the given title exists, and return it - * - * @param expectedTitle - Expected title of the toast - * @param timeout Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. - * @returns the Locator for the matching toast - */ - public async getToast(expectedTitle: string, timeout?: number): Promise { - const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first(); - await expect(toast).toBeVisible({ timeout }); - return toast; - } - - /** - * Assert that no toasts exist - */ - public async assertNoToasts(): Promise { - await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible(); - } - - /** - * Accept a toast with the given title, only works for the first toast in the stack - * - * @param expectedTitle - Expected title of the toast - */ - public async acceptToast(expectedTitle: string): Promise { - const toast = await this.getToast(expectedTitle); - await toast.locator('.mx_Toast_buttons button[data-kind="primary"]').click(); - } - - /** - * Reject a toast with the given title, only works for the first toast in the stack - * - * @param expectedTitle - Expected title of the toast - */ - public async rejectToast(expectedTitle: string): Promise { - const toast = await this.getToast(expectedTitle); - await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click(); - } -} diff --git a/packages/playwright-common/package.json b/packages/playwright-common/package.json index 9aa51eef68..8436b651cb 100644 --- a/packages/playwright-common/package.json +++ b/packages/playwright-common/package.json @@ -1,7 +1,7 @@ { "name": "@element-hq/element-web-playwright-common", "type": "module", - "version": "3.0.0", + "version": "3.1.0", "license": "SEE LICENSE IN README.md", "repository": { "type": "git", diff --git a/packages/playwright-common/src/fixtures/toasts.ts b/packages/playwright-common/src/fixtures/toasts.ts new file mode 100644 index 0000000000..780884e2b0 --- /dev/null +++ b/packages/playwright-common/src/fixtures/toasts.ts @@ -0,0 +1,109 @@ +/* + * 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 { expect, type Locator, type Page } from "@playwright/test"; + +// We want to avoid using `mergeTests` in index.ts because it drops useful type +// information about the fixtures. Instead, we add `services` into our fixture +// suite by using its `test` as a base, so that there is a linear hierarchy. +import { test as base } from "./services.js"; + +// This fixture provides convenient handling of Element Web's toasts. +export const test = base.extend<{ + /** + * Convenience functions for handling toasts. + */ + toasts: Toasts; +}>({ + toasts: async ({ page }, use) => { + const toasts = new Toasts(page); + await use(toasts); + }, +}); + +class Toasts { + public constructor(public readonly page: Page) {} + + /** + * Assert that no toasts exist + */ + public async assertNoToasts(): Promise { + await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible(); + } + + /** + * Assert that a toast with the given title exists, and return it + * + * @param title - Expected title of the toast + * @param timeout - Time to retry the assertion for in milliseconds. + * Defaults to `timeout` in `TestConfig.expect`. + * @returns the Locator for the matching toast + */ + public async getToast(title: string, timeout?: number): Promise { + const toast = this.getToastIfExists(title); + await expect(toast).toBeVisible({ timeout }); + return toast; + } + + /** + * Find a toast with the given title, if it exists. + * + * @param title - Title of the toast. + * @returns the Locator for the matching toast, or an empty locator if it + * doesn't exist. + */ + public getToastIfExists(title: string): Locator { + return this.page.locator(".mx_Toast_toast", { hasText: title }).first(); + } + + /** + * Accept a toast with the given title. Only works for the first toast in + * the stack. + * + * @param title - Expected title of the toast + */ + public async acceptToast(title: string): Promise { + const toast = await this.getToast(title); + await toast.locator('.mx_Toast_buttons button[data-kind="primary"]').click(); + } + /** + * Accept a toast with the given title, if it exists. Only works for the + * first toast in the stack. + * + * @param title - Title of the toast + */ + public async acceptToastIfExists(title: string): Promise { + const toast = this.getToastIfExists(title).locator('.mx_Toast_buttons button[data-kind="primary"]'); + if ((await toast.count()) > 0) { + await toast.click(); + } + } + + /** + * Reject a toast with the given title. Only works for the first toast in + * the stack. + * + * @param title - Expected title of the toast + */ + public async rejectToast(title: string): Promise { + const toast = await this.getToast(title); + await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click(); + } + + /** + * Reject a toast with the given title, if it exists. Only works for the + * first toast in the stack. + * + * @param title - Title of the toast + */ + public async rejectToastIfExists(title: string): Promise { + const toast = this.getToastIfExists(title).locator('.mx_Toast_buttons button[data-kind="secondary"]'); + if ((await toast.count()) > 0) { + await toast.click(); + } + } +} diff --git a/packages/playwright-common/src/fixtures/user.ts b/packages/playwright-common/src/fixtures/user.ts index 073b6149d8..61f8d70105 100644 --- a/packages/playwright-common/src/fixtures/user.ts +++ b/packages/playwright-common/src/fixtures/user.ts @@ -9,7 +9,10 @@ Please see LICENSE files in the repository root for full details. import { type Page } from "@playwright/test"; import { sample, uniqueId } from "lodash-es"; -import { test as base } from "./services.js"; +// We want to avoid using `mergeTests` in index.ts because it drops useful type +// information about the fixtures. Instead, we add `toasts` into our fixture +// suite by using its `test` as a base, so that there is a linear hierarchy. +import { test as base } from "./toasts.js"; import { type Credentials } from "../utils/api.js"; /** Adds an initScript to the given page which will populate localStorage appropriately so that Element will use the given credentials. */