diff --git a/apps/web/playwright/e2e/crypto/device-verification.spec.ts b/apps/web/playwright/e2e/crypto/device-verification.spec.ts index 07fa4ed9d85..34690893f4f 100644 --- a/apps/web/playwright/e2e/crypto/device-verification.spec.ts +++ b/apps/web/playwright/e2e/crypto/device-verification.spec.ts @@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import jsQR from "jsqr"; +import { assertNoToasts, getToast, rejectToast } from "@element-hq/element-web-playwright-common/src/utils/toasts"; import type { JSHandle, Locator, Page } from "@playwright/test"; import type { VerificationRequest } from "matrix-js-sdk/src/crypto-api"; @@ -81,11 +82,7 @@ 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, - toasts, - }) => { + test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => { // 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({ @@ -124,8 +121,8 @@ 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) - await toasts.rejectToast("Notifications"); - await toasts.assertNoToasts(); + await rejectToast(page, "Notifications"); + await assertNoToasts(page); // There may still be a `/sendToDevice/m.secret.request` in flight, which will later throw an error and cause // a *subsequent* test to fail. Tell playwright to ignore any errors resulting from in-flight routes. @@ -272,7 +269,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true); } - test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts, app }) => { + test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, app }) => { /* Log in but don't verify the device */ await logIntoElement(page, credentials); const authPage = page.locator(".mx_AuthPage"); @@ -302,7 +299,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => { ); /* Check the toast for the incoming request */ - const toast = await toasts.getToast("Verification requested"); + const toast = await getToast(page, "Verification requested"); // it should contain the device ID of the requesting device await expect(toast.getByText(`${aliceBotClient.credentials.deviceId} from `)).toBeVisible(); // Accept diff --git a/apps/web/playwright/e2e/crypto/toasts.spec.ts b/apps/web/playwright/e2e/crypto/toasts.spec.ts index 9ce1b8a5ae9..f090f2b52de 100644 --- a/apps/web/playwright/e2e/crypto/toasts.spec.ts +++ b/apps/web/playwright/e2e/crypto/toasts.spec.ts @@ -6,6 +6,7 @@ */ import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; +import { assertNoToasts, getToast, rejectToast } from "@element-hq/element-web-playwright-common/src/utils/toasts"; import { test, expect } from "../../element-web-test"; import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement, logIntoElementAndVerify } from "./utils"; @@ -72,7 +73,7 @@ test.describe("Key storage out of sync toast", () => { test.describe("'Turn on key storage' toast", () => { let botClient: Bot | undefined; - test.beforeEach(async ({ page, homeserver, credentials, toasts }) => { + test.beforeEach(async ({ page, homeserver, credentials }) => { // Set up all crypto stuff. Key storage defaults to on. const res = await createBot(page, homeserver, credentials); @@ -90,13 +91,13 @@ test.describe("'Turn on key storage' toast", () => { await page.getByRole("textbox", { name: "Name" }).fill("Test room"); await page.getByRole("button", { name: "Create room" }).click(); - await toasts.rejectToast("Notifications"); + await rejectToast(page, "Notifications"); }); - test("should not show toast if key storage is on", async ({ page, toasts }) => { + test("should not show toast if key storage is on", async ({ page }) => { // Given the default situation after signing in // Then no toast is shown (because key storage is on) - await toasts.assertNoToasts(); + await assertNoToasts(page); // When we reload await page.reload(); @@ -105,15 +106,15 @@ test.describe("'Turn on key storage' toast", () => { await new Promise((resolve) => setTimeout(resolve, 2000)); // Then still no toast is shown - await toasts.assertNoToasts(); + await assertNoToasts(page); }); - test("should not show toast if key storage is off because we turned it off", async ({ app, page, toasts }) => { + test("should not show toast if key storage is off because we turned it off", async ({ app, page }) => { // Given the backup is disabled because we disabled it await disableKeyBackup(app); // Then no toast is shown - await toasts.assertNoToasts(); + await assertNoToasts(page); // When we reload await page.reload(); @@ -122,10 +123,10 @@ test.describe("'Turn on key storage' toast", () => { await new Promise((resolve) => setTimeout(resolve, 2000)); // Then still no toast is shown - await toasts.assertNoToasts(); + await assertNoToasts(page); }); - test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => { + test("should show toast if key storage is off but account data is missing", async ({ app, page }) => { // Given the backup is disabled but we didn't set account data saying that is expected await disableKeyBackup(app); await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false }); @@ -137,7 +138,7 @@ test.describe("'Turn on key storage' toast", () => { await page.reload(); // Then the toast is displayed - let toast = await toasts.getToast("Turn on key storage"); + let toast = await getToast(page, "Turn on key storage"); // And when we click "Continue" await toast.getByRole("button", { name: "Continue" }).click(); @@ -149,7 +150,7 @@ test.describe("'Turn on key storage' toast", () => { await page.getByRole("button", { name: "Close dialog" }).click(); // Then we see the toast again - toast = await toasts.getToast("Turn on key storage"); + toast = await getToast(page, "Turn on key storage"); // And when we click "Dismiss" await toast.getByRole("button", { name: "Dismiss" }).click(); @@ -163,7 +164,7 @@ test.describe("'Turn on key storage' toast", () => { await page.getByTestId("dialog-background").click({ force: true, position: { x: 10, y: 10 } }); // Then we see the toast again - toast = await toasts.getToast("Turn on key storage"); + toast = await getToast(page, "Turn on key storage"); // And when we click Dismiss and then "Go to Settings" await toast.getByRole("button", { name: "Dismiss" }).click(); @@ -174,12 +175,12 @@ test.describe("'Turn on key storage' toast", () => { // And when we close that, see the toast, click Dismiss, and Yes, Dismiss await page.getByRole("button", { name: "Close dialog" }).click(); - toast = await toasts.getToast("Turn on key storage"); + toast = await getToast(page, "Turn on key storage"); await toast.getByRole("button", { name: "Dismiss" }).click(); await page.getByRole("button", { name: "Yes, dismiss" }).click(); // Then the toast is gone - await toasts.assertNoToasts(); + await assertNoToasts(page); }); }); diff --git a/apps/web/playwright/e2e/room/room-status-bar.spec.ts b/apps/web/playwright/e2e/room/room-status-bar.spec.ts index 78d5c49a300..2ef12113f57 100644 --- a/apps/web/playwright/e2e/room/room-status-bar.spec.ts +++ b/apps/web/playwright/e2e/room/room-status-bar.spec.ts @@ -4,6 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { getToast } from "@element-hq/element-web-playwright-common/src/utils/toasts"; + import { test, expect } from "../../element-web-test"; test.describe("Room Status Bar", () => { @@ -38,7 +40,7 @@ test.describe("Room Status Bar", () => { await expect(banner).toBeVisible({ timeout: 15000 }); await expect(banner).toMatchScreenshot("connectivity_lost.png"); }); - test("should NOT an error when a resource limit is hit", async ({ page, user, app, room, axe, toasts }) => { + test("should NOT an error when a resource limit is hit", async ({ page, user, app, room, axe }) => { await app.viewRoomById(room.roomId); await page.route("**/_matrix/client/*/sync*", async (route, req) => { await route.fulfill({ @@ -54,7 +56,7 @@ test.describe("Room Status Bar", () => { }); await app.client.sendMessage(room.roomId, "forcing sync to run"); // Wait for the MAU warning toast to appear so we know this status bar would have appeared. - await toasts.getToast("Warning", 15000); + await getToast(page, "Warning", 15000); await expect(page.getByRole("region", { name: "Room status bar" })).not.toBeVisible(); }); test( diff --git a/apps/web/playwright/e2e/toasts/analytics-toast.spec.ts b/apps/web/playwright/e2e/toasts/analytics-toast.spec.ts index ff106298725..8bd992e5cf3 100644 --- a/apps/web/playwright/e2e/toasts/analytics-toast.spec.ts +++ b/apps/web/playwright/e2e/toasts/analytics-toast.spec.ts @@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import { acceptToast, assertNoToasts, rejectToast } from "@element-hq/element-web-playwright-common/src/utils/toasts"; + import { test } from "../../element-web-test"; test.describe("Analytics Toast", () => { @@ -13,9 +15,9 @@ test.describe("Analytics Toast", () => { displayName: "Tod", }); - test("should not show an analytics toast if config has nothing about posthog", async ({ user, toasts }) => { - await toasts.rejectToast("Notifications"); - await toasts.assertNoToasts(); + test("should not show an analytics toast if config has nothing about posthog", async ({ user, page }) => { + await rejectToast(page, "Notifications"); + await assertNoToasts(page); }); test.describe("with posthog enabled", () => { @@ -28,18 +30,18 @@ test.describe("Analytics Toast", () => { }, }); - test.beforeEach(async ({ user, toasts }) => { - await toasts.rejectToast("Notifications"); + test.beforeEach(async ({ user, page }) => { + await rejectToast(page, "Notifications"); }); - test("should show an analytics toast which can be accepted", async ({ user, toasts }) => { - await toasts.acceptToast("Help improve Element"); - await toasts.assertNoToasts(); + test("should show an analytics toast which can be accepted", async ({ user, page }) => { + await acceptToast(page, "Help improve Element"); + await assertNoToasts(page); }); - test("should show an analytics toast which can be rejected", async ({ user, toasts }) => { - await toasts.rejectToast("Help improve Element"); - await toasts.assertNoToasts(); + test("should show an analytics toast which can be rejected", async ({ user, page }) => { + await rejectToast(page, "Help improve Element"); + await assertNoToasts(page); }); }); }); diff --git a/apps/web/playwright/e2e/voip/pstn.spec.ts b/apps/web/playwright/e2e/voip/pstn.spec.ts index 4241db65226..5c40b4bf068 100644 --- a/apps/web/playwright/e2e/voip/pstn.spec.ts +++ b/apps/web/playwright/e2e/voip/pstn.spec.ts @@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ +import {} from "@element-hq/element-web-playwright-common/src/utils/toasts"; + import { test, expect } from "../../element-web-test"; test.describe("PSTN", () => { @@ -20,9 +22,9 @@ test.describe("PSTN", () => { }); }); - test("should render dialpad as expected", { tag: "@screenshot" }, async ({ page, user, toasts }) => { - await toasts.rejectToast("Notifications"); - await toasts.assertNoToasts(); + test("should render dialpad as expected", { tag: "@screenshot" }, async ({ page, user }) => { + await rejectToast(page, "Notifications"); + await assertNoToasts(page); await expect(page.getByTestId("room-list-search")).toMatchScreenshot("dialpad-trigger.png"); await page.getByLabel("Open dial pad").click(); diff --git a/packages/playwright-common/src/utils/toasts.ts b/packages/playwright-common/src/utils/toasts.ts new file mode 100644 index 00000000000..87c3b3e1776 --- /dev/null +++ b/packages/playwright-common/src/utils/toasts.ts @@ -0,0 +1,102 @@ +/* + * 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"; + +/** + * Assert that no toasts exist + * + * @public + * @param page - Playwright page we are working with + */ +export async function assertNoToasts(page: Page): Promise { + await expect(page.locator(".mx_Toast_toast")).not.toBeVisible(); +} + +/** + * Assert that a toast with the given title exists, and return it + * + * @public + * @param page - Playwright page we are working with + * @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 + */ +export async function getToast(page: Page, title: string, timeout?: number): Promise { + const toast = getToastIfExists(page, title); + await expect(toast).toBeVisible({ timeout }); + return toast; +} + +/** + * Find a toast with the given title, if it exists. + * + * @public + * @param page - Playwright page we are working with + * @param title - Title of the toast. + * @returns the Locator for the matching toast, or an empty locator if it + * doesn't exist. + */ +export function getToastIfExists(page: Page, title: string): Locator { + return page.locator(".mx_Toast_toast", { hasText: title }).first(); +} + +/** + * Accept a toast with the given title. Only works for the first toast in + * the stack. + * + * @public + * @param page - Playwright page we are working with + * @param title - Expected title of the toast + */ +export async function acceptToast(page: Page, title: string): Promise { + const toast = await getToast(page, 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. + * + * @public + * @param page - Playwright page we are working with + * @param title - Title of the toast + */ +export async function acceptToastIfExists(page: Page, title: string): Promise { + const toast = getToastIfExists(page, 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. + * + * @public + * @param page - Playwright page we are working with + * @param title - Expected title of the toast + */ +export async function rejectToast(page: Page, title: string): Promise { + const toast = await getToast(page, 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. + * + * @public + * @param page - Playwright page we are working with + * @param title - Title of the toast + */ +export async function rejectToastIfExists(page: Page, title: string): Promise { + const toast = getToastIfExists(page, title).locator('.mx_Toast_buttons button[data-kind="secondary"]'); + if ((await toast.count()) > 0) { + await toast.click(); + } +}