Playwright tests for RoomStatusBar (#31579)
* Add a timeout for toast checks * Add tests for room status bar * Fix MAU toast never appearing. * Also cover local room create fails. * fix await * docstring * Enwiden * Add a test for the changes
This commit is contained in:
174
playwright/e2e/room/room-status-bar.spec.ts
Normal file
174
playwright/e2e/room/room-status-bar.spec.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 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 { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Room Status Bar", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Jim",
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
// Increase width as these components look horrible at lower
|
||||||
|
// widths.
|
||||||
|
await page.setViewportSize({ width: 1400, height: 768 });
|
||||||
|
await use(page);
|
||||||
|
},
|
||||||
|
room: async ({ app, user }, use) => {
|
||||||
|
const roomId = await app.client.createRoom({
|
||||||
|
name: "A room",
|
||||||
|
});
|
||||||
|
await app.closeNotificationToast();
|
||||||
|
await app.viewRoomById(roomId);
|
||||||
|
await use({ roomId });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show an error when sync stops", { tag: "@screenshot" }, async ({ page, user, app, room, axe }) => {
|
||||||
|
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 500,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: '{"error": "Test fail", "errcode": "M_UNKNOWN"}',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await app.client.sendMessage(room.roomId, "forcing sync to run");
|
||||||
|
const banner = page.getByRole("region", { name: "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 }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 400,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: "Test fail",
|
||||||
|
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||||
|
limit_type: "monthly_active_user",
|
||||||
|
admin_contact: "https://example.org",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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 expect(page.getByRole("region", { name: "Room status bar" })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
test(
|
||||||
|
"should show an error when the user needs to consent",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, user, app, room, axe }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await page.route("**/_matrix/client/**/send**", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 400,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: "Test fail",
|
||||||
|
errcode: "M_CONSENT_NOT_GIVEN",
|
||||||
|
consent_uri: "https://example.org",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const composer = app.getComposerField();
|
||||||
|
await composer.fill("Hello!");
|
||||||
|
await composer.press("Enter");
|
||||||
|
await page
|
||||||
|
.getByRole("dialog", { name: "Terms and Conditions" })
|
||||||
|
.getByRole("button", { name: "Dismiss" })
|
||||||
|
.click();
|
||||||
|
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||||
|
await expect(banner).toBeVisible({ timeout: 15000 });
|
||||||
|
await expect(banner).toMatchScreenshot("consent.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
test.describe("Message fails to send", () => {
|
||||||
|
test.beforeEach(async ({ page, user, app, room, axe }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await page.route("**/_matrix/client/**/send**", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 400,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ error: "Test fail", errcode: "M_UNKNOWN" }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const composer = app.getComposerField();
|
||||||
|
await composer.fill("Hello!");
|
||||||
|
await composer.press("Enter");
|
||||||
|
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||||
|
await expect(banner).toBeVisible();
|
||||||
|
});
|
||||||
|
test(
|
||||||
|
"should show an error when a message fails to send",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, user, app, room, axe }) => {
|
||||||
|
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||||
|
await expect(banner).toMatchScreenshot("message_failed.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
test("should be able to 'Delete all' messages", async ({ page, user, app, room, axe }) => {
|
||||||
|
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||||
|
await banner.getByRole("button", { name: "Delete all" }).click();
|
||||||
|
await expect(banner).not.toBeVisible();
|
||||||
|
});
|
||||||
|
test("should be able to 'Retry all' messages", async ({ page, user, app, room, axe }) => {
|
||||||
|
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||||
|
await page.unroute("**/_matrix/client/**/send**");
|
||||||
|
await banner.getByRole("button", { name: "Retry all" }).click();
|
||||||
|
await expect(banner).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Local rooms", () => {
|
||||||
|
test.use({
|
||||||
|
botCreateOpts: {
|
||||||
|
displayName: "Alice",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
test(
|
||||||
|
"should show an error when creating a local room fails",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user, bot }) => {
|
||||||
|
await page
|
||||||
|
.getByRole("navigation", { name: "Room list" })
|
||||||
|
.getByRole("button", { name: "New conversation" })
|
||||||
|
.click();
|
||||||
|
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||||
|
|
||||||
|
await page.route("**/_matrix/client/*/createRoom*", async (route, req) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 400,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
error: "Test fail",
|
||||||
|
errcode: "M_UNKNOWN",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const other = page.locator(".mx_InviteDialog_other");
|
||||||
|
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
||||||
|
await expect(
|
||||||
|
other.getByRole("option", { name: "Alice" }).getByText(bot.credentials.userId),
|
||||||
|
).toBeVisible();
|
||||||
|
await other.getByRole("option", { name: "Alice" }).click();
|
||||||
|
await other.getByRole("button", { name: "Go" }).click();
|
||||||
|
// Send a message to invite the bots
|
||||||
|
const composer = app.getComposerField();
|
||||||
|
await composer.fill("Hello");
|
||||||
|
await composer.press("Enter");
|
||||||
|
|
||||||
|
const banner = page.getByText("!Some of your messages have");
|
||||||
|
await expect(banner).toBeVisible();
|
||||||
|
await expect(banner).toMatchScreenshot("local_room_create_failed.png");
|
||||||
|
|
||||||
|
await page.unroute("**/_matrix/client/*/createRoom*");
|
||||||
|
await banner.getByRole("button", { name: "Retry" }).click();
|
||||||
|
await expect(banner).not.toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,11 +15,12 @@ export class Toasts {
|
|||||||
* Assert that a toast with the given title exists, and return it
|
* Assert that a toast with the given title exists, and return it
|
||||||
*
|
*
|
||||||
* @param expectedTitle - Expected title of the toast
|
* @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
|
* @returns the Locator for the matching toast
|
||||||
*/
|
*/
|
||||||
public async getToast(expectedTitle: string): Promise<Locator> {
|
public async getToast(expectedTitle: string, timeout?: number): Promise<Locator> {
|
||||||
const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first();
|
const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first();
|
||||||
await expect(toast).toBeVisible();
|
await expect(toast).toBeVisible({ timeout });
|
||||||
return toast;
|
return toast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -359,14 +359,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
const newErrCode = (data?.error as MatrixError)?.errcode;
|
const newErrCode = (data?.error as MatrixError)?.errcode;
|
||||||
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
|
if (syncState === oldSyncState && oldErrCode === newErrCode) return;
|
||||||
|
|
||||||
|
const syncErrorData = syncState === SyncState.Error ? data : undefined;
|
||||||
this.setState({
|
this.setState({
|
||||||
syncErrorData: syncState === SyncState.Error ? data : undefined,
|
syncErrorData,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (oldSyncState === SyncState.Prepared && syncState === SyncState.Syncing) {
|
if (oldSyncState === SyncState.Prepared && syncState === SyncState.Syncing) {
|
||||||
this.updateServerNoticeEvents();
|
this.updateServerNoticeEvents();
|
||||||
} else {
|
} else {
|
||||||
this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
|
this.calculateServerLimitToast(syncErrorData, this.state.usageLimitEventContent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -391,7 +392,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
// usageLimitDismissed is true when the user has explicitly hidden the toast
|
// usageLimitDismissed is true when the user has explicitly hidden the toast
|
||||||
// and it will be reset to false if a *new* usage alert comes in.
|
// and it will be reset to false if a *new* usage alert comes in.
|
||||||
if (usageLimitEventContent && this.state.usageLimitDismissed) {
|
if (usageLimitEventContent && !this.state.usageLimitDismissed) {
|
||||||
showServerLimitToast(
|
showServerLimitToast(
|
||||||
usageLimitEventContent.limit_type,
|
usageLimitEventContent.limit_type,
|
||||||
this.onUsageLimitDismissed,
|
this.onUsageLimitDismissed,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
PushRuleKind,
|
PushRuleKind,
|
||||||
ProfileKeyTimezone,
|
ProfileKeyTimezone,
|
||||||
ProfileKeyMSC4175Timezone,
|
ProfileKeyMSC4175Timezone,
|
||||||
|
SyncState,
|
||||||
|
MatrixError,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
@@ -35,6 +37,7 @@ import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
|||||||
import { Action } from "../../../../src/dispatcher/actions";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
import Modal from "../../../../src/Modal";
|
import Modal from "../../../../src/Modal";
|
||||||
import { SETTINGS } from "../../../../src/settings/Settings";
|
import { SETTINGS } from "../../../../src/settings/Settings";
|
||||||
|
import ToastStore from "../../../../src/stores/ToastStore";
|
||||||
|
|
||||||
// Create a mock resizer instance that can be shared across tests
|
// Create a mock resizer instance that can be shared across tests
|
||||||
const mockResizerInstance = {
|
const mockResizerInstance = {
|
||||||
@@ -505,6 +508,29 @@ describe("<LoggedInView />", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resource limit exceeded errors", () => {
|
||||||
|
it("pops a toast when M_RESOURCE_LIMIT_EXCEEDED is seen down sync", async () => {
|
||||||
|
const addOrReplaceToast = jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast");
|
||||||
|
const dismissToast = jest.spyOn(ToastStore.sharedInstance(), "dismissToast");
|
||||||
|
getComponent();
|
||||||
|
mockClient.emit(ClientEvent.Sync, SyncState.Error, null, {
|
||||||
|
error: new MatrixError({
|
||||||
|
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||||
|
limit_type: "hs_disabled",
|
||||||
|
admin_contact: "admin@example.org",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(addOrReplaceToast).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
key: "serverlimit",
|
||||||
|
title: "Warning",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
mockClient.emit(ClientEvent.Sync, SyncState.Prepared, null, undefined);
|
||||||
|
expect(dismissToast).toHaveBeenCalledWith("serverlimit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("resizer preferences", () => {
|
describe("resizer preferences", () => {
|
||||||
let mockResize: jest.Mock;
|
let mockResize: jest.Mock;
|
||||||
let mockForHandleWithId: jest.Mock;
|
let mockForHandleWithId: jest.Mock;
|
||||||
|
|||||||
Reference in New Issue
Block a user