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 zcmd5=^OS{js2X%wVkkxuE7?v^eQ5SH#-q`ON}x*MsbVQCh=m-qcA zzUPN~=QDF>?!=k%Jm(2lR+N2-L5cwafnI))lTrnNko17EI2tPOWDXsr0bb9XRAnVV z&{6U|5QymJgOs?sd)nc$*ZUDSsuw3$#d#3pUgl!;LP(h1?|g`qjZ?}`TP>46?wnsT zME+!$3!fh}axIcgGDjABcU^a{W4NdmSP)ei7Iaa)Iqp!QQvT=FE3%E+ zkn!CsPmWGwT3{^CmBb{y?vTh{2PenC36$Z5)G~%xa*rg+O=E_Z2-OA~` z|4#hVSH&%p*%dl&KOS;EX3`_*=qqvHsW@tD{6&kicnXg?Q(p{k5)vAf74_VNokpgw!fh5 zZUqJSAUTsp<{*C@_kzvLN&z3JPE@Ivcx`^6=e3FM&x&i?B`ZQ)ZAVJeYu9J**TLth zQgOZ0*dBKxwfHvu1g-76(~#3XuupyVf}74&yHQ~?((emll?!9;yqA*tQ+OUCZDC;% z$JFd}#lAsBdV@c)p7}2Bds4n;SIFIf@!7Zn`GbJuuel85f#MWqMv@Uad~|HOLh4N^ z=uCe_aciy%S(AwHo%r1rG

K@8aJI!_WM>p`!nrjmOF^z@WMvf* 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