Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions apps/web/playwright/e2e/crypto/device-verification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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
Expand Down
29 changes: 15 additions & 14 deletions apps/web/playwright/e2e/crypto/toasts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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 });
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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);
});
});

Expand Down
6 changes: 4 additions & 2 deletions apps/web/playwright/e2e/room/room-status-bar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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({
Expand All @@ -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(
Expand Down
24 changes: 13 additions & 11 deletions apps/web/playwright/e2e/toasts/analytics-toast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ 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", () => {
test.use({
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", () => {
Expand All @@ -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);
});
});
});
8 changes: 5 additions & 3 deletions apps/web/playwright/e2e/voip/pstn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
Please see LICENSE files in the repository root for full details.
*/

import {} from "@element-hq/element-web-playwright-common/src/utils/toasts";

Check warning on line 8 in apps/web/playwright/e2e/voip/pstn.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

import statement without specifiers is not allowed.

See more on https://sonarcloud.io/project/issues?id=element-web&issues=AZ2Q0GgmzS9VYAvVW4WK&open=AZ2Q0GgmzS9VYAvVW4WK&pullRequest=33152

import { test, expect } from "../../element-web-test";

test.describe("PSTN", () => {
Expand All @@ -20,9 +22,9 @@
});
});

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();
Expand Down
102 changes: 102 additions & 0 deletions packages/playwright-common/src/utils/toasts.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<Locator> {
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<void> {
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<void> {
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<void> {
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<void> {
const toast = getToastIfExists(page, title).locator('.mx_Toast_buttons button[data-kind="secondary"]');
if ((await toast.count()) > 0) {
await toast.click();
}
}
Loading