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
This commit is contained in:
@@ -21,7 +21,6 @@ import {
|
|||||||
waitForVerificationRequest,
|
waitForVerificationRequest,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { type Bot } from "../../pages/bot";
|
import { type Bot } from "../../pages/bot";
|
||||||
import { Toasts } from "../../pages/toasts.ts";
|
|
||||||
import type { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
import type { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
|
|
||||||
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
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
|
// 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
|
// 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.
|
// when we are in an encrypted room.
|
||||||
await aliceBotClient.createRoom({
|
await aliceBotClient.createRoom({
|
||||||
@@ -121,7 +124,6 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
|
|
||||||
// There should be no toast (other than the notifications one)
|
// There should be no toast (other than the notifications one)
|
||||||
const toasts = new Toasts(page);
|
|
||||||
await toasts.rejectToast("Notifications");
|
await toasts.rejectToast("Notifications");
|
||||||
await toasts.assertNoToasts();
|
await toasts.assertNoToasts();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import type { IConfigOptions } from "../src/IConfigOptions";
|
|||||||
import { type Credentials } from "./plugins/homeserver";
|
import { type Credentials } from "./plugins/homeserver";
|
||||||
import { ElementAppPage } from "./pages/ElementAppPage";
|
import { ElementAppPage } from "./pages/ElementAppPage";
|
||||||
import { Crypto } from "./pages/crypto";
|
import { Crypto } from "./pages/crypto";
|
||||||
import { Toasts } from "./pages/toasts";
|
|
||||||
import { Bot, type CreateBotOpts } from "./pages/bot";
|
import { Bot, type CreateBotOpts } from "./pages/bot";
|
||||||
import { Webserver } from "./plugins/webserver";
|
import { Webserver } from "./plugins/webserver";
|
||||||
import { type WorkerOptions, type Services, test as base } from "./services";
|
import { type WorkerOptions, type Services, test as base } from "./services";
|
||||||
@@ -52,7 +51,6 @@ export interface TestFixtures extends BaseTestFixtures {
|
|||||||
|
|
||||||
crypto: Crypto;
|
crypto: Crypto;
|
||||||
room?: { roomId: string };
|
room?: { roomId: string };
|
||||||
toasts: Toasts;
|
|
||||||
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
|
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
|
||||||
botCreateOpts: CreateBotOpts;
|
botCreateOpts: CreateBotOpts;
|
||||||
bot: Bot;
|
bot: Bot;
|
||||||
@@ -92,9 +90,6 @@ export const test = base.extend<TestFixtures>({
|
|||||||
crypto: async ({ page, homeserver, request }, use) => {
|
crypto: async ({ page, homeserver, request }, use) => {
|
||||||
await use(new Crypto(page, homeserver, request));
|
await use(new Crypto(page, homeserver, request));
|
||||||
},
|
},
|
||||||
toasts: async ({ page }, use) => {
|
|
||||||
await use(new Toasts(page));
|
|
||||||
},
|
|
||||||
|
|
||||||
botCreateOpts: {},
|
botCreateOpts: {},
|
||||||
bot: async ({ page, homeserver, botCreateOpts, user }, use, testInfo) => {
|
bot: async ({ page, homeserver, botCreateOpts, user }, use, testInfo) => {
|
||||||
|
|||||||
@@ -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<Locator> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
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<void> {
|
|
||||||
const toast = await this.getToast(expectedTitle);
|
|
||||||
await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@element-hq/element-web-playwright-common",
|
"name": "@element-hq/element-web-playwright-common",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.0.0",
|
"version": "3.1.0",
|
||||||
"license": "SEE LICENSE IN README.md",
|
"license": "SEE LICENSE IN README.md",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -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<void> {
|
||||||
|
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<Locator> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
const toast = this.getToastIfExists(title).locator('.mx_Toast_buttons button[data-kind="secondary"]');
|
||||||
|
if ((await toast.count()) > 0) {
|
||||||
|
await toast.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { type Page } from "@playwright/test";
|
import { type Page } from "@playwright/test";
|
||||||
import { sample, uniqueId } from "lodash-es";
|
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";
|
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. */
|
/** Adds an initScript to the given page which will populate localStorage appropriately so that Element will use the given credentials. */
|
||||||
|
|||||||
Reference in New Issue
Block a user