From b02ff16a21bf7f064c228329ec182f36d6a593e5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 4 Dec 2023 12:01:06 +0000
Subject: [PATCH] Migrate room/* from Cypress to Playwright (#11985)
* Remove old percy media query CSS
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Stabilise soft_logout.spec.ts
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Update screenshots using `toMatchScreenshot` assertion with CSS overrides
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Fix accidentally commented test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Migrate room/* from Cypress to Playwright
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* delint
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Add screenshots
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
* Update labs.ts
---------
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
cypress/e2e/room/room-header.spec.ts | 286 ------------------
cypress/e2e/room/room.spec.ts | 81 -----
playwright/e2e/room/room-header.spec.ts | 280 +++++++++++++++++
playwright/e2e/room/room.spec.ts | 63 ++++
playwright/element-web-test.ts | 9 +
playwright/pages/ElementAppPage.ts | 4 +-
playwright/pages/labs.ts | 13 +-
playwright/plugins/webserver/index.ts | 46 +++
.../room-header-highlighted-linux.png | Bin 0 -> 5666 bytes
.../room-header.spec.ts/room-header-linux.png | Bin 0 -> 4952 bytes
.../room-header-long-name-linux.png | Bin 0 -> 6826 bytes
.../room-header-video-room-linux.png | Bin 0 -> 6165 bytes
...der-with-apps-button-highlighted-linux.png | Bin 0 -> 5951 bytes
...with-apps-button-not-highlighted-linux.png | Bin 0 -> 6755 bytes
14 files changed, 409 insertions(+), 373 deletions(-)
delete mode 100644 cypress/e2e/room/room-header.spec.ts
delete mode 100644 cypress/e2e/room/room.spec.ts
create mode 100644 playwright/e2e/room/room-header.spec.ts
create mode 100644 playwright/e2e/room/room.spec.ts
create mode 100644 playwright/plugins/webserver/index.ts
create mode 100644 playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png
create mode 100644 playwright/snapshots/room/room-header.spec.ts/room-header-linux.png
create mode 100644 playwright/snapshots/room/room-header.spec.ts/room-header-long-name-linux.png
create mode 100644 playwright/snapshots/room/room-header.spec.ts/room-header-video-room-linux.png
create mode 100644 playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-highlighted-linux.png
create mode 100644 playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-not-highlighted-linux.png
diff --git a/cypress/e2e/room/room-header.spec.ts b/cypress/e2e/room/room-header.spec.ts
deleted file mode 100644
index d92d505167..0000000000
--- a/cypress/e2e/room/room-header.spec.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
-Copyright 2023 Suguru Hirahara
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-///
-
-import { IWidget } from "matrix-widget-api";
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { SettingLevel } from "../../../src/settings/SettingLevel";
-
-describe("Room Header", () => {
- let homeserver: HomeserverInstance;
-
- beforeEach(() => {
- cy.startHomeserver("default").then((data) => {
- homeserver = data;
- cy.initTestUser(homeserver, "Sakura");
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- it("should render default buttons properly", () => {
- cy.enableLabsFeature("feature_notifications");
- cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- // Names (aria-label) of every button rendered on mx_LegacyRoomHeader by default
- const expectedButtonNames = [
- "Room options", // The room name button next to the room avatar, which renders dropdown menu on click
- "Voice call",
- "Video call",
- "Search",
- "Threads",
- "Notifications",
- "Room info",
- ];
-
- // Assert they are found and visible
- for (const name of expectedButtonNames) {
- cy.findByRole("button", { name }).should("be.visible");
- }
-
- // Assert that just those seven buttons exist on mx_LegacyRoomHeader by default
- cy.findAllByRole("button").should("have.length", 7);
- });
-
- cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header");
- });
-
- it("should render the pin button for pinned messages card", () => {
- cy.enableLabsFeature("feature_pinning");
-
- cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
-
- cy.getComposer().type("Test message{enter}");
-
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Options" }).click();
-
- cy.findByRole("menuitem", { name: "Pin" }).should("be.visible").click();
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- cy.findByRole("button", { name: "Pinned messages" }).should("be.visible");
- });
- });
-
- it("should render a very long room name without collapsing the buttons", () => {
- cy.enableLabsFeature("feature_notifications");
- const LONG_ROOM_NAME =
- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
- "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
- "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
- "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
- "officia deserunt mollit anim id est laborum.";
-
- cy.createRoom({ name: LONG_ROOM_NAME }).viewRoomByName(LONG_ROOM_NAME);
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- // Wait until the room name is set
- cy.get(".mx_LegacyRoomHeader_nametext").within(() => {
- cy.findByText(LONG_ROOM_NAME).should("exist");
- });
-
- // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
- // Note these assertions do not check the size of mx_LegacyRoomHeader_name button
- cy.get(".mx_LegacyRoomHeader_button")
- .should("have.length", 6)
- .should("be.visible")
- .should("have.css", "height", "32px")
- .should("have.css", "width", "32px");
- });
-
- cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with a long room name", {
- widths: [300, 600], // Magic numbers to emulate the narrow RoomHeader on the actual UI
- });
- });
-
- it("should have buttons highlighted by being clicked", () => {
- cy.enableLabsFeature("feature_notifications");
- cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- // Check these buttons
- const buttonsHighlighted = ["Threads", "Notifications", "Room info"];
-
- for (const name of buttonsHighlighted) {
- cy.findByRole("button", { name: name }).click(); // Highlight the button
- }
- });
-
- cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with a highlighted button");
- });
-
- describe("with a video room", () => {
- const createVideoRoom = () => {
- // Enable video rooms. This command reloads the app
- cy.setSettingValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
-
- cy.get(".mx_LeftPanel_roomListContainer", { timeout: 20000 })
- .findByRole("button", { name: "Add room" })
- .click();
-
- cy.findByRole("menuitem", { name: "New video room" }).click();
-
- cy.findByRole("textbox", { name: "Name" }).type("Test video room");
-
- cy.findByRole("button", { name: "Create video room" }).click();
-
- cy.viewRoomByName("Test video room");
- };
-
- it("should render buttons for room options, beta pill, invite, chat, and room info", () => {
- cy.enableLabsFeature("feature_notifications");
- createVideoRoom();
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- // Names (aria-label) of the buttons on the video room header
- const expectedButtonNames = [
- "Room options",
- "Video rooms are a beta feature Click for more info", // Beta pill
- "Invite",
- "Chat",
- "Room info",
- ];
-
- // Assert they are found and visible
- for (const name of expectedButtonNames) {
- cy.findByRole("button", { name }).should("be.visible");
- }
-
- // Assert that there is not a button except those buttons
- cy.findAllByRole("button").should("have.length", 7);
- });
-
- cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with a video room");
- });
-
- it("should render a working chat button which opens the timeline on a right panel", () => {
- createVideoRoom();
-
- cy.get(".mx_LegacyRoomHeader").findByRole("button", { name: "Chat" }).click();
-
- // Assert that the video is rendered
- cy.get(".mx_CallView video").should("exist");
-
- cy.get(".mx_RightPanel .mx_TimelineCard")
- .should("exist")
- .within(() => {
- // Assert that GELS is visible
- cy.findByText("Sakura created and configured the room.").should("exist");
- });
- });
- });
-
- describe("with a widget", () => {
- const ROOM_NAME = "Test Room with a widget";
- const WIDGET_ID = "fake-widget";
- const WIDGET_HTML = `
-
-
- Fake Widget
-
-
- Hello World
-
-
- `;
-
- let widgetUrl: string;
- let roomId: string;
-
- beforeEach(() => {
- cy.serveHtmlFile(WIDGET_HTML).then((url) => {
- widgetUrl = url;
- });
-
- cy.createRoom({ name: ROOM_NAME }).then((id) => {
- roomId = id;
-
- // setup widget via state event
- cy.getClient()
- .then(async (matrixClient) => {
- const content: IWidget = {
- id: WIDGET_ID,
- creatorUserId: "somebody",
- type: "widget",
- name: "widget",
- url: widgetUrl,
- };
- await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
- })
- .as("widgetEventSent");
-
- // set initial layout
- cy.getClient()
- .then(async (matrixClient) => {
- const content = {
- widgets: {
- [WIDGET_ID]: {
- container: "top",
- index: 1,
- width: 100,
- height: 0,
- },
- },
- };
- await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
- })
- .as("layoutEventSent");
- });
-
- cy.all([cy.get("@widgetEventSent"), cy.get("@layoutEventSent")]).then(() => {
- // open the room
- cy.viewRoomByName(ROOM_NAME);
- });
- });
-
- it("should highlight the apps button", () => {
- // Assert that AppsDrawer is rendered
- cy.get(".mx_AppsDrawer").should("exist");
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- // Assert that "Hide Widgets" button is rendered and aria-checked is set to true
- cy.findByRole("button", { name: "Hide Widgets" })
- .should("exist")
- .should("have.attr", "aria-checked", "true");
- });
-
- cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with apps button (highlighted)");
- });
-
- it("should support hiding a widget", () => {
- cy.get(".mx_AppsDrawer").should("exist");
-
- cy.get(".mx_LegacyRoomHeader").within(() => {
- // Click the apps button to hide AppsDrawer
- cy.findByRole("button", { name: "Hide Widgets" }).should("exist").click();
-
- // Assert that "Show widgets" button is rendered and aria-checked is set to false
- cy.findByRole("button", { name: "Show Widgets" })
- .should("exist")
- .should("have.attr", "aria-checked", "false");
- });
-
- // Assert that AppsDrawer is not rendered
- cy.get(".mx_AppsDrawer").should("not.exist");
-
- cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with apps button (not highlighted)");
- });
- });
-});
diff --git a/cypress/e2e/room/room.spec.ts b/cypress/e2e/room/room.spec.ts
deleted file mode 100644
index 8d1108d280..0000000000
--- a/cypress/e2e/room/room.spec.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-///
-
-import { EventType } from "matrix-js-sdk/src/matrix";
-
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { MatrixClient } from "../../global";
-
-describe("Room Directory", () => {
- let homeserver: HomeserverInstance;
-
- beforeEach(() => {
- cy.startHomeserver("default").then((data) => {
- homeserver = data;
-
- cy.initTestUser(homeserver, "Alice");
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- it("should switch between existing dm rooms without a loader", () => {
- let bobClient: MatrixClient;
- let charlieClient: MatrixClient;
- cy.getBot(homeserver, {
- displayName: "Bob",
- }).then((bob) => {
- bobClient = bob;
- });
-
- cy.getBot(homeserver, {
- displayName: "Charlie",
- }).then((charlie) => {
- charlieClient = charlie;
- });
-
- // create dms with bob and charlie
- cy.getClient().then(async (cli) => {
- const bobRoom = await cli.createRoom({ is_direct: true });
- const charlieRoom = await cli.createRoom({ is_direct: true });
- await cli.invite(bobRoom.room_id, bobClient.getUserId());
- await cli.invite(charlieRoom.room_id, charlieClient.getUserId());
- await cli.setAccountData("m.direct" as EventType, {
- [bobClient.getUserId()]: [bobRoom.room_id],
- [charlieClient.getUserId()]: [charlieRoom.room_id],
- });
- });
-
- cy.wait(250); // let the room list settle
-
- cy.viewRoomByName("Bob");
-
- // short timeout because loader is only visible for short period
- // we want to make sure it is never displayed when switching these rooms
- cy.get(".mx_RoomPreviewBar_spinnerTitle", { timeout: 1 }).should("not.exist");
- // confirm the room was loaded
- cy.findByText("Bob joined the room").should("exist");
-
- cy.viewRoomByName("Charlie");
- cy.get(".mx_RoomPreviewBar_spinnerTitle", { timeout: 1 }).should("not.exist");
- // confirm the room was loaded
- cy.findByText("Charlie joined the room").should("exist");
- });
-});
diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts
new file mode 100644
index 0000000000..45bb6a6810
--- /dev/null
+++ b/playwright/e2e/room/room-header.spec.ts
@@ -0,0 +1,280 @@
+/*
+Copyright 2023 Suguru Hirahara
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Page } from "@playwright/test";
+
+import { test, expect } from "../../element-web-test";
+import { ElementAppPage } from "../../pages/ElementAppPage";
+
+test.describe("Room Header", () => {
+ test.use({
+ displayName: "Sakura",
+ });
+
+ test.describe("with feature_notifications enabled", () => {
+ test.beforeEach(async ({ app }) => {
+ await app.labs.enableLabsFeature("feature_notifications");
+ });
+
+ test("should render default buttons properly", async ({ page, app, user }) => {
+ await app.client.createRoom({ name: "Test Room" });
+ await app.viewRoomByName("Test Room");
+
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Names (aria-label) of every button rendered on mx_LegacyRoomHeader by default
+ const expectedButtonNames = [
+ "Room options", // The room name button next to the room avatar, which renders dropdown menu on click
+ "Voice call",
+ "Video call",
+ "Search",
+ "Threads",
+ "Notifications",
+ "Room info",
+ ];
+
+ // Assert they are found and visible
+ for (const name of expectedButtonNames) {
+ await expect(header.getByRole("button", { name })).toBeVisible();
+ }
+
+ // Assert that just those seven buttons exist on mx_LegacyRoomHeader by default
+ await expect(header.getByRole("button")).toHaveCount(7);
+
+ await expect(header).toMatchScreenshot("room-header.png");
+ });
+
+ test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => {
+ const LONG_ROOM_NAME =
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
+ "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
+ "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
+ "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
+ "officia deserunt mollit anim id est laborum.";
+
+ await app.client.createRoom({ name: LONG_ROOM_NAME });
+ await app.viewRoomByName(LONG_ROOM_NAME);
+
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Wait until the room name is set
+ await expect(page.locator(".mx_LegacyRoomHeader_nametext").getByText(LONG_ROOM_NAME)).toBeVisible();
+
+ // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
+ // Note these assertions do not check the size of mx_LegacyRoomHeader_name button
+ const buttons = page.locator(".mx_LegacyRoomHeader_button");
+ await expect(buttons).toHaveCount(6);
+ for (const button of await buttons.all()) {
+ await expect(button).toBeVisible();
+ await expect(button).toHaveCSS("height", "32px");
+ await expect(button).toHaveCSS("width", "32px");
+ }
+
+ await expect(header).toMatchScreenshot("room-header-long-name.png");
+ });
+
+ test("should have buttons highlighted by being clicked", async ({ page, app, user }) => {
+ await app.client.createRoom({ name: "Test Room" });
+ await app.viewRoomByName("Test Room");
+
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Check these buttons
+ const buttonsHighlighted = ["Threads", "Notifications", "Room info"];
+
+ for (const name of buttonsHighlighted) {
+ await header.getByRole("button", { name: name }).click(); // Highlight the button
+ }
+
+ await expect(header).toMatchScreenshot("room-header-highlighted.png");
+ });
+ });
+
+ test.describe("with feature_pinning enabled", () => {
+ test.beforeEach(async ({ app }) => {
+ await app.labs.enableLabsFeature("feature_pinning");
+ });
+
+ test("should render the pin button for pinned messages card", async ({ page, app, user }) => {
+ await app.client.createRoom({ name: "Test Room" });
+ await app.viewRoomByName("Test Room");
+
+ const composer = app.getComposer().locator("[contenteditable]");
+ await composer.fill("Test message");
+ await composer.press("Enter");
+
+ const lastTile = page.locator(".mx_EventTile_last");
+ await lastTile.hover();
+ await lastTile.getByRole("button", { name: "Options" }).click();
+
+ await page.getByRole("menuitem", { name: "Pin" }).click();
+
+ await expect(
+ page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Pinned messages" }),
+ ).toBeVisible();
+ });
+ });
+
+ test.describe("with a video room", () => {
+ test.beforeEach(async ({ app }) => {
+ await app.labs.enableLabsFeature("feature_video_rooms");
+ });
+
+ const createVideoRoom = async (page: Page, app: ElementAppPage) => {
+ await page.locator(".mx_LeftPanel_roomListContainer").getByRole("button", { name: "Add room" }).click();
+
+ await page.getByRole("menuitem", { name: "New video room" }).click();
+
+ await page.getByRole("textbox", { name: "Name" }).type("Test video room");
+
+ await page.getByRole("button", { name: "Create video room" }).click();
+
+ await app.viewRoomByName("Test video room");
+ };
+
+ test("should render buttons for room options, beta pill, invite, chat, and room info", async ({
+ page,
+ app,
+ user,
+ }) => {
+ await app.labs.enableLabsFeature("feature_notifications");
+ await createVideoRoom(page, app);
+
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Names (aria-label) of the buttons on the video room header
+ const expectedButtonNames = [
+ "Room options",
+ "Video rooms are a beta feature Click for more info", // Beta pill
+ "Invite",
+ "Chat",
+ "Room info",
+ ];
+
+ // Assert they are found and visible
+ for (const name of expectedButtonNames) {
+ await expect(header.getByRole("button", { name })).toBeVisible();
+ }
+
+ // Assert that there is not a button except those buttons
+ await expect(header.getByRole("button")).toHaveCount(7);
+
+ await expect(header).toMatchScreenshot("room-header-video-room.png");
+ });
+
+ test("should render a working chat button which opens the timeline on a right panel", async ({
+ page,
+ app,
+ user,
+ }) => {
+ await createVideoRoom(page, app);
+
+ await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Chat" }).click();
+
+ // Assert that the video is rendered
+ await expect(page.locator(".mx_CallView video")).toBeVisible();
+
+ // Assert that GELS is visible
+ await expect(
+ page.locator(".mx_RightPanel .mx_TimelineCard").getByText("Sakura created and configured the room."),
+ ).toBeVisible();
+ });
+ });
+
+ test.describe("with a widget", () => {
+ const ROOM_NAME = "Test Room with a widget";
+ const WIDGET_ID = "fake-widget";
+ const WIDGET_HTML = `
+
+
+ Fake Widget
+
+
+ Hello World
+
+
+ `;
+
+ test.beforeEach(async ({ page, app, user, webserver }) => {
+ const widgetUrl = webserver.start(WIDGET_HTML);
+ const roomId = await app.client.createRoom({ name: ROOM_NAME });
+
+ // setup widget via state event
+ await app.client.evaluate(
+ async (matrixClient, { roomId, widgetUrl, id }) => {
+ await matrixClient.sendStateEvent(
+ roomId,
+ "im.vector.modular.widgets",
+ {
+ id,
+ creatorUserId: "somebody",
+ type: "widget",
+ name: "widget",
+ url: widgetUrl,
+ },
+ id,
+ );
+ await matrixClient.sendStateEvent(
+ roomId,
+ "io.element.widgets.layout",
+ {
+ widgets: {
+ [id]: {
+ container: "top",
+ index: 1,
+ width: 100,
+ height: 0,
+ },
+ },
+ },
+ "",
+ );
+ },
+ {
+ roomId,
+ widgetUrl,
+ id: WIDGET_ID,
+ },
+ );
+
+ // open the room
+ await app.viewRoomByName(ROOM_NAME);
+ });
+
+ test("should highlight the apps button", async ({ page, app, user }) => {
+ // Assert that AppsDrawer is rendered
+ await expect(page.locator(".mx_AppsDrawer")).toBeVisible();
+
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Assert that "Hide Widgets" button is rendered and aria-checked is set to true
+ await expect(header.getByRole("button", { name: "Hide Widgets" })).toHaveAttribute("aria-checked", "true");
+
+ await expect(header).toMatchScreenshot("room-header-with-apps-button-highlighted.png");
+ });
+
+ test("should support hiding a widget", async ({ page, app, user }) => {
+ await expect(page.locator(".mx_AppsDrawer")).toBeVisible();
+
+ const header = page.locator(".mx_LegacyRoomHeader");
+ // Click the apps button to hide AppsDrawer
+ await header.getByRole("button", { name: "Hide Widgets" }).click();
+
+ // Assert that "Show widgets" button is rendered and aria-checked is set to false
+ await expect(header.getByRole("button", { name: "Show Widgets" })).toHaveAttribute("aria-checked", "false");
+
+ // Assert that AppsDrawer is not rendered
+ await expect(page.locator(".mx_AppsDrawer")).not.toBeVisible();
+
+ await expect(header).toMatchScreenshot("room-header-with-apps-button-not-highlighted.png");
+ });
+ });
+});
diff --git a/playwright/e2e/room/room.spec.ts b/playwright/e2e/room/room.spec.ts
new file mode 100644
index 0000000000..2bd9e06c07
--- /dev/null
+++ b/playwright/e2e/room/room.spec.ts
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import type { EventType } from "matrix-js-sdk/src/matrix";
+import { test, expect } from "../../element-web-test";
+import { Bot } from "../../pages/bot";
+
+test.describe("Room Directory", () => {
+ test.use({
+ displayName: "Alice",
+ });
+
+ test("should switch between existing dm rooms without a loader", async ({ page, homeserver, app, user }) => {
+ const bob = new Bot(page, homeserver, { displayName: "Bob" });
+ await bob.prepareClient();
+ const charlie = new Bot(page, homeserver, { displayName: "Charlie" });
+ await charlie.prepareClient();
+
+ // create dms with bob and charlie
+ await app.client.evaluate(
+ async (cli, { bob, charlie }) => {
+ const bobRoom = await cli.createRoom({ is_direct: true });
+ const charlieRoom = await cli.createRoom({ is_direct: true });
+ await cli.invite(bobRoom.room_id, bob);
+ await cli.invite(charlieRoom.room_id, charlie);
+ await cli.setAccountData("m.direct" as EventType, {
+ [bob]: [bobRoom.room_id],
+ [charlie]: [charlieRoom.room_id],
+ });
+ },
+ {
+ bob: bob.credentials.userId,
+ charlie: charlie.credentials.userId,
+ },
+ );
+
+ await app.viewRoomByName("Bob");
+
+ // short timeout because loader is only visible for short period
+ // we want to make sure it is never displayed when switching these rooms
+ await expect(page.locator(".mx_RoomPreviewBar_spinnerTitle")).not.toBeVisible({ timeout: 1 });
+ // confirm the room was loaded
+ await expect(page.getByText("Bob joined the room")).toBeVisible();
+
+ await app.viewRoomByName("Charlie");
+ await expect(page.locator(".mx_RoomPreviewBar_spinnerTitle")).not.toBeVisible({ timeout: 1 });
+ // confirm the room was loaded
+ await expect(page.getByText("Charlie joined the room")).toBeVisible();
+ });
+});
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index f7c405b3f0..a551ec593d 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -29,6 +29,7 @@ import { OAuthServer } from "./plugins/oauth_server";
import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts";
import { Bot, CreateBotOpts } from "./pages/bot";
+import { Webserver } from "./plugins/webserver";
const CONFIG_JSON: Partial = {
// This is deliberately quite a minimal config.json, so that we can test that the default settings
@@ -75,6 +76,7 @@ export const test = base.extend<
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
botCreateOpts: CreateBotOpts;
bot: Bot;
+ webserver: Webserver;
}
>({
cryptoBackend: ["legacy", { option: true }],
@@ -195,6 +197,13 @@ export const test = base.extend<
await bot.prepareClient(); // eagerly register the bot
await use(bot);
},
+
+ // eslint-disable-next-line no-empty-pattern
+ webserver: async ({}, use) => {
+ const webserver = new Webserver();
+ await use(webserver);
+ webserver.stop();
+ },
});
export const expect = baseExpect.extend({
diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts
index 6cb6c27e90..0d8a5213fc 100644
--- a/playwright/pages/ElementAppPage.ts
+++ b/playwright/pages/ElementAppPage.ts
@@ -82,7 +82,7 @@ export class ElementAppPage {
* Get the composer element
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
*/
- public async getComposer(isRightPanel?: boolean): Promise {
+ public getComposer(isRightPanel?: boolean): Locator {
const panelClass = isRightPanel ? ".mx_RightPanel" : ".mx_RoomView_body";
return this.page.locator(`${panelClass} .mx_MessageComposer`);
}
@@ -92,7 +92,7 @@ export class ElementAppPage {
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
*/
public async openMessageComposerOptions(isRightPanel?: boolean): Promise {
- const composer = await this.getComposer(isRightPanel);
+ const composer = this.getComposer(isRightPanel);
await composer.getByRole("button", { name: "More options", exact: true }).click();
return this.page.getByRole("menu");
}
diff --git a/playwright/pages/labs.ts b/playwright/pages/labs.ts
index 397eb08305..55bce18225 100644
--- a/playwright/pages/labs.ts
+++ b/playwright/pages/labs.ts
@@ -21,12 +21,17 @@ export class Labs {
/**
* Enables a labs feature for an element session.
- * Has to be called before the session is initialized
* @param feature labsFeature to enable (e.g. "feature_spotlight")
*/
public async enableLabsFeature(feature: string): Promise {
- await this.page.evaluate((feature) => {
- window.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
- }, feature);
+ if (this.page.url() === "about:blank") {
+ await this.page.addInitScript((feature) => {
+ window.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
+ }, feature);
+ } else {
+ await this.page.evaluate((feature) => {
+ window.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
+ }, feature);
+ }
}
}
diff --git a/playwright/plugins/webserver/index.ts b/playwright/plugins/webserver/index.ts
new file mode 100644
index 0000000000..2fe083f179
--- /dev/null
+++ b/playwright/plugins/webserver/index.ts
@@ -0,0 +1,46 @@
+/*
+Copyright 2022 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import * as http from "http";
+import { AddressInfo } from "net";
+
+export class Webserver {
+ private server?: http.Server;
+
+ public start(html: string): string {
+ if (this.server) this.stop();
+
+ this.server = http.createServer((req, res) => {
+ res.writeHead(200, {
+ "Content-Type": "text/html",
+ });
+ res.end(html);
+ });
+ this.server.listen();
+
+ const address = this.server.address() as AddressInfo;
+ console.log(`Started webserver at ${address.address}:${address.port}`);
+ return `http://localhost:${address.port}/`;
+ }
+
+ public stop(): void {
+ if (!this.server) return;
+ console.log("Stopping webserver");
+ const address = this.server.address() as AddressInfo;
+ this.server.close();
+ console.log(`Stopped webserver at ${address.address}:${address.port}`);
+ }
+}
diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-highlighted-linux.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d6ea59ae83072018a391010f594652ad9de0692
GIT binary patch
literal 5666
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
literal 0
HcmV?d00001
diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-linux.png
new file mode 100644
index 0000000000000000000000000000000000000000..c5fd90be5781e55f6cd6d84876f5009d48d0dc02
GIT binary patch
literal 4952
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
MRZ&x+T+RaWA6hWNF8}}l
literal 0
HcmV?d00001
diff --git a/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-highlighted-linux.png b/playwright/snapshots/room/room-header.spec.ts/room-header-with-apps-button-highlighted-linux.png
new file mode 100644
index 0000000000000000000000000000000000000000..6016bb7e7a183a273eddbbe8145d2fdc89f6f8cb
GIT binary patch
literal 5951
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