Skip to content

Commit 9a8ffbe

Browse files
authored
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
1 parent 733c685 commit 9a8ffbe

6 files changed

Lines changed: 119 additions & 63 deletions

File tree

apps/web/playwright/e2e/crypto/device-verification.spec.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
waitForVerificationRequest,
2222
} from "./utils";
2323
import { type Bot } from "../../pages/bot";
24-
import { Toasts } from "../../pages/toasts.ts";
2524
import type { ElementAppPage } from "../../pages/ElementAppPage.ts";
2625

2726
test.describe("Device verification", { tag: "@no-webkit" }, () => {
@@ -82,7 +81,11 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
8281
);
8382

8483
// Regression test for https://github.com/element-hq/element-web/issues/29110
85-
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
84+
test("No toast after verification, even if the secrets take a while to arrive", async ({
85+
page,
86+
credentials,
87+
toasts,
88+
}) => {
8689
// Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens
8790
// when we are in an encrypted room.
8891
await aliceBotClient.createRoom({
@@ -121,7 +124,6 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
121124
await infoDialog.getByRole("button", { name: "Got it" }).click();
122125

123126
// There should be no toast (other than the notifications one)
124-
const toasts = new Toasts(page);
125127
await toasts.rejectToast("Notifications");
126128
await toasts.assertNoToasts();
127129

apps/web/playwright/element-web-test.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import type { IConfigOptions } from "../src/IConfigOptions";
2424
import { type Credentials } from "./plugins/homeserver";
2525
import { ElementAppPage } from "./pages/ElementAppPage";
2626
import { Crypto } from "./pages/crypto";
27-
import { Toasts } from "./pages/toasts";
2827
import { Bot, type CreateBotOpts } from "./pages/bot";
2928
import { Webserver } from "./plugins/webserver";
3029
import { type WorkerOptions, type Services, test as base } from "./services";
@@ -52,7 +51,6 @@ export interface TestFixtures extends BaseTestFixtures {
5251

5352
crypto: Crypto;
5453
room?: { roomId: string };
55-
toasts: Toasts;
5654
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
5755
botCreateOpts: CreateBotOpts;
5856
bot: Bot;
@@ -92,9 +90,6 @@ export const test = base.extend<TestFixtures>({
9290
crypto: async ({ page, homeserver, request }, use) => {
9391
await use(new Crypto(page, homeserver, request));
9492
},
95-
toasts: async ({ page }, use) => {
96-
await use(new Toasts(page));
97-
},
9893

9994
botCreateOpts: {},
10095
bot: async ({ page, homeserver, botCreateOpts, user }, use, testInfo) => {

apps/web/playwright/pages/toasts.ts

Lines changed: 0 additions & 53 deletions
This file was deleted.

packages/playwright-common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@element-hq/element-web-playwright-common",
33
"type": "module",
4-
"version": "3.0.0",
4+
"version": "3.1.0",
55
"license": "SEE LICENSE IN README.md",
66
"repository": {
77
"type": "git",
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2026 Element Creations Ltd.
3+
*
4+
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
* Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import { expect, type Locator, type Page } from "@playwright/test";
9+
10+
// We want to avoid using `mergeTests` in index.ts because it drops useful type
11+
// information about the fixtures. Instead, we add `services` into our fixture
12+
// suite by using its `test` as a base, so that there is a linear hierarchy.
13+
import { test as base } from "./services.js";
14+
15+
// This fixture provides convenient handling of Element Web's toasts.
16+
export const test = base.extend<{
17+
/**
18+
* Convenience functions for handling toasts.
19+
*/
20+
toasts: Toasts;
21+
}>({
22+
toasts: async ({ page }, use) => {
23+
const toasts = new Toasts(page);
24+
await use(toasts);
25+
},
26+
});
27+
28+
class Toasts {
29+
public constructor(public readonly page: Page) {}
30+
31+
/**
32+
* Assert that no toasts exist
33+
*/
34+
public async assertNoToasts(): Promise<void> {
35+
await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible();
36+
}
37+
38+
/**
39+
* Assert that a toast with the given title exists, and return it
40+
*
41+
* @param title - Expected title of the toast
42+
* @param timeout - Time to retry the assertion for in milliseconds.
43+
* Defaults to `timeout` in `TestConfig.expect`.
44+
* @returns the Locator for the matching toast
45+
*/
46+
public async getToast(title: string, timeout?: number): Promise<Locator> {
47+
const toast = this.getToastIfExists(title);
48+
await expect(toast).toBeVisible({ timeout });
49+
return toast;
50+
}
51+
52+
/**
53+
* Find a toast with the given title, if it exists.
54+
*
55+
* @param title - Title of the toast.
56+
* @returns the Locator for the matching toast, or an empty locator if it
57+
* doesn't exist.
58+
*/
59+
public getToastIfExists(title: string): Locator {
60+
return this.page.locator(".mx_Toast_toast", { hasText: title }).first();
61+
}
62+
63+
/**
64+
* Accept a toast with the given title. Only works for the first toast in
65+
* the stack.
66+
*
67+
* @param title - Expected title of the toast
68+
*/
69+
public async acceptToast(title: string): Promise<void> {
70+
const toast = await this.getToast(title);
71+
await toast.locator('.mx_Toast_buttons button[data-kind="primary"]').click();
72+
}
73+
/**
74+
* Accept a toast with the given title, if it exists. Only works for the
75+
* first toast in the stack.
76+
*
77+
* @param title - Title of the toast
78+
*/
79+
public async acceptToastIfExists(title: string): Promise<void> {
80+
const toast = this.getToastIfExists(title).locator('.mx_Toast_buttons button[data-kind="primary"]');
81+
if ((await toast.count()) > 0) {
82+
await toast.click();
83+
}
84+
}
85+
86+
/**
87+
* Reject a toast with the given title. Only works for the first toast in
88+
* the stack.
89+
*
90+
* @param title - Expected title of the toast
91+
*/
92+
public async rejectToast(title: string): Promise<void> {
93+
const toast = await this.getToast(title);
94+
await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click();
95+
}
96+
97+
/**
98+
* Reject a toast with the given title, if it exists. Only works for the
99+
* first toast in the stack.
100+
*
101+
* @param title - Title of the toast
102+
*/
103+
public async rejectToastIfExists(title: string): Promise<void> {
104+
const toast = this.getToastIfExists(title).locator('.mx_Toast_buttons button[data-kind="secondary"]');
105+
if ((await toast.count()) > 0) {
106+
await toast.click();
107+
}
108+
}
109+
}

packages/playwright-common/src/fixtures/user.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ Please see LICENSE files in the repository root for full details.
99
import { type Page } from "@playwright/test";
1010
import { sample, uniqueId } from "lodash-es";
1111

12-
import { test as base } from "./services.js";
12+
// We want to avoid using `mergeTests` in index.ts because it drops useful type
13+
// information about the fixtures. Instead, we add `toasts` into our fixture
14+
// suite by using its `test` as a base, so that there is a linear hierarchy.
15+
import { test as base } from "./toasts.js";
1316
import { type Credentials } from "../utils/api.js";
1417

1518
/** Adds an initScript to the given page which will populate localStorage appropriately so that Element will use the given credentials. */

0 commit comments

Comments
 (0)