Migrate right-panel/* from Cypress to Playwright (#11954)

* Migrate file-panel.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate right-panel.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Migrate notification-panel.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test flakes

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* delint

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshot

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Try stabilise test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* sleep

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Handle both cases

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix assertion

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Flip

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28788/head^2
Michael Telatynski 2023-12-01 12:24:49 +00:00 committed by GitHub
parent 1f06d97ffe
commit 446400b6b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 530 additions and 508 deletions

View File

@ -1,265 +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.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import Chainable = Cypress.Chainable;
const ROOM_NAME = "Test room";
const NAME = "Alice";
const viewRoomSummaryByName = (name: string): Chainable<JQuery<HTMLElement>> => {
cy.viewRoomByName(name);
cy.findByRole("button", { name: "Room info" }).click();
return checkRoomSummaryCard(name);
};
const checkRoomSummaryCard = (name: string): Chainable<JQuery<HTMLElement>> => {
cy.get(".mx_RoomSummaryCard").should("have.length", 1);
return cy.get(".mx_RoomSummaryCard").should("contain", name);
};
const uploadFile = (file: string) => {
// Upload a file from the message composer
cy.get(".mx_MessageComposer_actions input[type='file']").selectFile(file, { force: true });
cy.get(".mx_Dialog").within(() => {
cy.findByRole("button", { name: "Upload" }).click();
});
// Wait until the file is sent
cy.get(".mx_RoomView_statusArea_expanded").should("not.exist");
cy.get(".mx_EventTile.mx_EventTile_last .mx_EventTile_receiptSent").should("exist");
};
describe("FilePanel", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, NAME).then(() =>
cy.window({ log: false }).then(() => {
cy.createRoom({ name: ROOM_NAME });
}),
);
});
// Open the file panel
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("menuitem", { name: "Files" }).click();
cy.get(".mx_FilePanel").should("have.length", 1);
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
describe("render", () => {
it("should render empty state", () => {
// Wait until the information about the empty state is rendered
cy.get(".mx_FilePanel_empty").should("exist");
// Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332
cy.get(".mx_RightPanel").percySnapshotElement("File Panel - empty", {
widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
});
});
it("should list tiles on the panel", () => {
// Upload multiple files
uploadFile("cypress/fixtures/riot.png"); // Image
uploadFile("cypress/fixtures/1sec.ogg"); // Audio
uploadFile("cypress/fixtures/matrix-org-client-versions.json"); // JSON
cy.get(".mx_RoomView_body").within(() => {
// Assert that all of the file were uploaded and rendered
cy.get(".mx_EventTile[data-layout='group']").should("have.length", 3);
// Assert that the image exists and has the alt string
cy.get(".mx_EventTile[data-layout='group'] img[alt='riot.png']").should("exist");
// Assert that the audio player is rendered
cy.get(".mx_EventTile[data-layout='group'] .mx_AudioPlayer_container").should("exist");
// Assert that the file button exists
cy.contains(".mx_EventTile_last[data-layout='group'] .mx_MFileBody", ".json").should("exist");
});
// Assert that the file panel is opened inside mx_RightPanel and visible
cy.get(".mx_RightPanel .mx_FilePanel").should("be.visible");
cy.get(".mx_FilePanel").within(() => {
cy.get(".mx_RoomView_MessageList").within(() => {
// Assert that data-layout attribute is not applied to file tiles on the panel
cy.get(".mx_EventTile[data-layout]").should("not.exist");
// Assert that all of the file tiles are rendered
cy.get(".mx_EventTile").should("have.length", 3);
// Assert that the download links are rendered
cy.get(".mx_MFileBody_download").should("have.length", 3);
// Assert that the sender of the files is rendered on all of the tiles
cy.findAllByText(NAME).should("have.length", 3);
// Detect the image file
cy.get(".mx_EventTile_mediaLine.mx_EventTile_image").within(() => {
// Assert that the image is specified as thumbnail and has the alt string
cy.get(".mx_MImageBody").within(() => {
cy.get("img[class='mx_MImageBody_thumbnail']").should("exist");
cy.get("img[alt='riot.png']").should("exist");
});
});
// Detect the audio file
cy.get(".mx_EventTile_mediaLine .mx_MAudioBody").within(() => {
// Assert that the audio player is rendered
cy.get(".mx_AudioPlayer_container").within(() => {
// Assert that the play button is rendered
cy.findByRole("button", { name: "Play" }).should("exist");
});
});
// Detect the JSON file
// Assert that the tile is rendered as a button
cy.get(".mx_EventTile_mediaLine .mx_MFileBody .mx_MFileBody_info[role='button']").within(() => {
// Assert that the file name is rendered inside the button with ellipsis
cy.get(".mx_MFileBody_info_filename").within(() => {
cy.findByText(/matrix.*?\.json/);
});
});
});
});
// Make the viewport tall enough to display all of the file tiles on FilePanel
cy.viewport(660, 1000);
cy.get(".mx_FilePanel").within(() => {
// In case the panel is scrollable on the resized viewport
cy.get(".mx_ScrollPanel").scrollTo("bottom", { ensureScrollable: false });
// Assert that the value for flexbox is applied
cy.get(".mx_ScrollPanel .mx_RoomView_MessageList").should("have.css", "justify-content", "flex-end");
// Assert that all of the file tiles are visible before taking a snapshot
cy.get(".mx_RoomView_MessageList").within(() => {
cy.get(".mx_MImageBody").should("be.visible"); // top
cy.get(".mx_MAudioBody").should("be.visible"); // middle
cy.get(".mx_EventTile_last").within(() => {
// bottom
cy.get(".mx_EventTile_senderDetails").within(() => {
cy.get(".mx_DisambiguatedProfile").should("be.visible");
cy.get(".mx_MessageTimestamp").should("be.visible");
});
});
});
});
// Exclude timestamps and read markers from snapshot
// FIXME: hide mx_SeekBar because flaky - see https://github.com/vector-im/element-web/issues/24897
// Remove this once https://github.com/vector-im/element-web/issues/24898 is fixed.
const percyCSS =
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .mx_SeekBar { visibility: hidden !important; }";
// Take a snapshot of file tiles list on FilePanel
cy.get(".mx_FilePanel .mx_RoomView_MessageList").percySnapshotElement("File tiles list on FilePanel", {
percyCSS,
widths: [250], // magic number, should be around the default width
});
});
// https://github.com/vector-im/element-web/issues/26045
it.skip("should render the audio player and play the audio file on the panel", () => {
// Upload an image file
uploadFile("cypress/fixtures/1sec.ogg");
cy.get(".mx_FilePanel").within(() => {
cy.get(".mx_RoomView_MessageList").within(() => {
// Detect the audio file
cy.get(".mx_EventTile_mediaLine .mx_MAudioBody").within(() => {
// Assert that the audio player is rendered
cy.get(".mx_AudioPlayer_container").within(() => {
// Assert that the audio file information is rendered
cy.get(".mx_AudioPlayer_mediaInfo").within(() => {
cy.get(".mx_AudioPlayer_mediaName").within(() => {
cy.findByText("1sec.ogg");
});
cy.contains(".mx_AudioPlayer_byline", "00:01").should("exist");
cy.contains(".mx_AudioPlayer_byline", "(3.56 KB)").should("exist"); // actual size
});
// Assert that the duration counter is 00:01 before clicking the play button
cy.contains(".mx_AudioPlayer_mediaInfo time", "00:01").should("exist");
// Assert that the counter is zero before clicking the play button
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
// Click the play button
cy.wait(500);
cy.findByRole("button", { name: "Play" }).click();
// Assert that the pause button is rendered
cy.findByRole("button", { name: "Pause" }).should("exist");
// Assert that the timer is reset when the audio file finished playing
cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
// Assert that the play button is rendered
cy.findByRole("button", { name: "Play" }).should("exist");
});
});
});
});
});
it("should render file size in kibibytes on a file tile", () => {
const size = "1.12 KB"; // actual file size in kibibytes (1024 bytes)
// Upload a file
uploadFile("cypress/fixtures/matrix-org-client-versions.json");
cy.get(".mx_FilePanel .mx_EventTile").within(() => {
// Assert that the file size is displayed in kibibytes, not kilobytes (1000 bytes)
// See: https://github.com/vector-im/element-web/issues/24866
cy.contains(".mx_MFileBody_info_filename", size).should("exist");
cy.get(".mx_MFileBody_download").within(() => {
cy.contains("a", size).should("exist");
cy.contains(".mx_MImageBody_size", size).should("exist");
});
});
});
});
describe("download", () => {
it("should download an image via the link on the panel", () => {
// Upload an image file
uploadFile("cypress/fixtures/riot.png");
cy.get(".mx_FilePanel").within(() => {
cy.get(".mx_RoomView_MessageList").within(() => {
// Detect the image file on the panel
cy.get(".mx_EventTile_mediaLine.mx_EventTile_image .mx_MImageBody").within(() => {
// Click the anchor link (not the image itself)
cy.get(".mx_MFileBody_download a").click();
cy.readFile("cypress/downloads/riot.png").should("exist");
});
});
});
});
});
});

View File

@ -1,53 +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.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
const ROOM_NAME = "Test room";
const NAME = "Alice";
describe("NotificationPanel", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, NAME).then(() => {
cy.createRoom({ name: ROOM_NAME });
});
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
it("should render empty state", () => {
cy.enableLabsFeature("feature_notifications");
cy.viewRoomByName(ROOM_NAME);
cy.findByRole("button", { name: "Notifications" }).click();
// Wait until the information about the empty state is rendered
cy.get(".mx_NotificationPanel_empty").should("exist");
// Take a snapshot of RightPanel
cy.get(".mx_RightPanel").percySnapshotElement("Notification Panel - empty", {
widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
});
});
});

View File

@ -1,184 +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.
*/
/// <reference types="cypress" />
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import Chainable = Cypress.Chainable;
const ROOM_NAME = "Test room";
const ROOM_NAME_LONG =
"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.";
const SPACE_NAME = "Test space";
const NAME = "Alice";
const ROOM_ADDRESS_LONG =
"loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua";
const getMemberTileByName = (name: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(`.mx_EntityTile, [title="${name}"]`);
};
const viewRoomSummaryByName = (name: string): Chainable<JQuery<HTMLElement>> => {
cy.viewRoomByName(name);
cy.findByRole("button", { name: "Room info" }).click();
return checkRoomSummaryCard(name);
};
const checkRoomSummaryCard = (name: string): Chainable<JQuery<HTMLElement>> => {
cy.get(".mx_RoomSummaryCard").should("have.length", 1);
return cy.get(".mx_RoomSummaryCard").should("contain", name);
};
describe("RightPanel", () => {
let homeserver: HomeserverInstance;
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, NAME).then(() =>
cy.window({ log: false }).then(() => {
cy.createRoom({ name: ROOM_NAME });
cy.createSpace({ name: SPACE_NAME });
}),
);
});
});
afterEach(() => {
cy.stopHomeserver(homeserver);
});
describe("in rooms", () => {
it("should handle long room address and long room name", () => {
cy.createRoom({ name: ROOM_NAME_LONG });
viewRoomSummaryByName(ROOM_NAME_LONG);
cy.openRoomSettings();
// Set a local room address
cy.contains(".mx_SettingsFieldset", "Local Addresses").within(() => {
cy.findByRole("textbox").type(ROOM_ADDRESS_LONG);
cy.findByRole("button", { name: "Add" }).click();
cy.findByText(`#${ROOM_ADDRESS_LONG}:localhost`)
.should("have.class", "mx_EditableItem_item")
.should("exist");
});
cy.closeDialog();
// Close and reopen the right panel to render the room address
cy.findByRole("button", { name: "Room info" }).click();
cy.get(".mx_RightPanel").should("not.exist");
cy.findByRole("button", { name: "Room info" }).click();
cy.get(".mx_RightPanel").percySnapshotElement("RoomSummaryCard - with a room name and a local address", {
widths: [264], // Emulate the UI. The value is based on minWidth specified on MainSplit.tsx
});
});
it("should handle clicking add widgets", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("button", { name: "Add widgets, bridges & bots" }).click();
cy.get(".mx_IntegrationManager").should("have.length", 1);
});
it("should handle viewing export chat", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("menuitem", { name: "Export Chat" }).click();
cy.get(".mx_ExportDialog").should("have.length", 1);
});
it("should handle viewing share room", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("menuitem", { name: "Copy link" }).click();
cy.get(".mx_ShareDialog").should("have.length", 1);
});
it("should handle viewing room settings", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("menuitem", { name: "Settings" }).click();
cy.get(".mx_RoomSettingsDialog").should("have.length", 1);
cy.get(".mx_Dialog_title").within(() => {
cy.findByText("Room Settings - " + ROOM_NAME).should("exist");
});
});
it("should handle viewing files", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("menuitem", { name: "Files" }).click();
cy.get(".mx_FilePanel").should("have.length", 1);
cy.get(".mx_FilePanel_empty").should("have.length", 1);
cy.findByRole("button", { name: "Room information" }).click();
checkRoomSummaryCard(ROOM_NAME);
});
it("should handle viewing room member", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("menuitem", { name: "People" }).click();
cy.get(".mx_MemberList").should("have.length", 1);
getMemberTileByName(NAME).click();
cy.get(".mx_UserInfo").should("have.length", 1);
cy.get(".mx_UserInfo_profile").within(() => {
cy.findByText(NAME);
});
cy.findByRole("button", { name: "Room members" }).click();
cy.get(".mx_MemberList").should("have.length", 1);
cy.findByRole("button", { name: "Room information" }).click();
checkRoomSummaryCard(ROOM_NAME);
});
});
describe("in spaces", () => {
it("should handle viewing space member", () => {
cy.viewSpaceHomeByName(SPACE_NAME);
cy.get(".mx_RoomInfoLine_private").within(() => {
// \d represents the number of the space members
cy.findByRole("button", { name: /\d member/ }).click();
});
cy.get(".mx_MemberList").should("have.length", 1);
cy.get(".mx_SpaceScopeHeader").within(() => {
cy.findByText(SPACE_NAME);
});
getMemberTileByName(NAME).click();
cy.get(".mx_UserInfo").should("have.length", 1);
cy.get(".mx_UserInfo_profile").within(() => {
cy.findByText(NAME);
});
cy.get(".mx_SpaceScopeHeader").within(() => {
cy.findByText(SPACE_NAME);
});
cy.findByRole("button", { name: "Back" }).click();
cy.get(".mx_MemberList").should("have.length", 1);
});
});
});

View File

@ -14,8 +14,6 @@ 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";
test.describe("Consent", () => {
@ -32,7 +30,7 @@ test.describe("Consent", () => {
}) => {
// Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN`
await app.client.createRoom({}).catch(() => {});
const newPagePromise = new Promise<Page>((resolve) => context.once("page", resolve));
const newPagePromise = context.waitForEvent("page");
const dialog = page.locator(".mx_QuestionDialog");
// Accept terms & conditions

View File

@ -0,0 +1,227 @@
/*
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 { type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "./utils";
const ROOM_NAME = "Test room";
const NAME = "Alice";
async function uploadFile(page: Page, file: string) {
// Upload a file from the message composer
await page.locator(".mx_MessageComposer_actions input[type='file']").setInputFiles(file);
await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();
// Wait until the file is sent
await expect(page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible();
await expect(page.locator(".mx_EventTile.mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible();
}
test.describe("FilePanel", () => {
test.use({
displayName: NAME,
});
test.beforeEach(async ({ page, user, app }) => {
await app.client.createRoom({ name: ROOM_NAME });
// Open the file panel
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Files" }).click();
await expect(page.locator(".mx_FilePanel")).toBeVisible();
});
test.describe("render", () => {
test("should render empty state", async ({ page }) => {
// Wait until the information about the empty state is rendered
await expect(page.locator(".mx_FilePanel_empty")).toBeVisible();
// Take a snapshot of RightPanel - fix https://github.com/vector-im/element-web/issues/25332
await expect(page.locator(".mx_RightPanel")).toHaveScreenshot("empty.png");
});
test("should list tiles on the panel", async ({ page }) => {
// Upload multiple files
await uploadFile(page, "cypress/fixtures/riot.png"); // Image
await uploadFile(page, "cypress/fixtures/1sec.ogg"); // Audio
await uploadFile(page, "cypress/fixtures/matrix-org-client-versions.json"); // JSON
const roomViewBody = page.locator(".mx_RoomView_body");
// Assert that all of the file were uploaded and rendered
await expect(roomViewBody.locator(".mx_EventTile[data-layout='group']")).toHaveCount(3);
// Assert that the image exists and has the alt string
await expect(roomViewBody.locator(".mx_EventTile[data-layout='group'] img[alt='riot.png']")).toBeVisible();
// Assert that the audio player is rendered
await expect(
roomViewBody.locator(".mx_EventTile[data-layout='group'] .mx_AudioPlayer_container"),
).toBeVisible();
// Assert that the file button exists
await expect(
roomViewBody.locator(".mx_EventTile_last[data-layout='group'] .mx_MFileBody", { hasText: ".json" }),
).toBeVisible();
const filePanel = page.locator(".mx_FilePanel");
// Assert that the file panel is opened inside mx_RightPanel and visible
await expect(filePanel).toBeVisible();
const filePanelMessageList = filePanel.locator(".mx_RoomView_MessageList");
// Assert that data-layout attribute is not applied to file tiles on the panel
await expect(filePanelMessageList.locator(".mx_EventTile[data-layout]")).not.toBeVisible();
// Assert that all of the file tiles are rendered
await expect(filePanelMessageList.locator(".mx_EventTile")).toHaveCount(3);
// Assert that the download links are rendered
await expect(filePanelMessageList.locator(".mx_MFileBody_download")).toHaveCount(3);
// Assert that the sender of the files is rendered on all of the tiles
await expect(filePanelMessageList.getByText(NAME)).toHaveCount(3);
// Detect the image file
const image = filePanelMessageList.locator(".mx_EventTile_mediaLine.mx_EventTile_image .mx_MImageBody");
// Assert that the image is specified as thumbnail and has the alt string
await expect(image.locator("img[class='mx_MImageBody_thumbnail']")).toBeVisible();
await expect(image.locator("img[alt='riot.png']")).toBeVisible();
// Detect the audio file
const audio = filePanelMessageList.locator(
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
);
// Assert that the play button is rendered
await expect(audio.getByRole("button", { name: "Play" })).toBeVisible();
// Detect the JSON file
// Assert that the tile is rendered as a button
const file = filePanelMessageList.locator(
".mx_EventTile_mediaLine .mx_MFileBody .mx_MFileBody_info[role='button'] .mx_MFileBody_info_filename",
);
// Assert that the file name is rendered inside the button with ellipsis
await expect(file.getByText(/matrix.*?\.json/)).toBeVisible();
// Make the viewport tall enough to display all of the file tiles on FilePanel
await page.setViewportSize({ width: 800, height: 1000 });
// In case the panel is scrollable on the resized viewport
// Assert that the value for flexbox is applied
await expect(filePanel.locator(".mx_ScrollPanel .mx_RoomView_MessageList")).toHaveCSS(
"justify-content",
"flex-end",
);
// Assert that all of the file tiles are visible before taking a snapshot
await expect(filePanelMessageList.locator(".mx_MImageBody")).toBeVisible(); // top
await expect(filePanelMessageList.locator(".mx_MAudioBody")).toBeVisible(); // middle
const senderDetails = filePanelMessageList.locator(".mx_EventTile_last .mx_EventTile_senderDetails");
await expect(senderDetails.locator(".mx_DisambiguatedProfile")).toBeVisible();
await expect(senderDetails.locator(".mx_MessageTimestamp")).toBeVisible();
// Take a snapshot of file tiles list on FilePanel
// XXX: We remove the RM as masking it in different locations causes a false positive
await page.evaluate(() => {
document.querySelectorAll(".mx_MessagePanel_myReadMarker").forEach((e) => e.remove());
});
await expect(filePanelMessageList).toHaveScreenshot("file-tiles-list.png", {
// Exclude timestamps, profiles, avatars & flaky seek bar from snapshot
mask: [
page.locator(
".mx_MessageTimestamp, .mx_DisambiguatedProfile, .mx_BaseAvatar, .mx_AudioPlayer_seek",
),
],
});
});
test("should render the audio player and play the audio file on the panel", async ({ page }) => {
// Upload an image file
await uploadFile(page, "cypress/fixtures/1sec.ogg");
const audioBody = page.locator(
".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
);
// Assert that the audio player is rendered
// Assert that the audio file information is rendered
const mediaInfo = audioBody.locator(".mx_AudioPlayer_mediaInfo");
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName").getByText("1sec.ogg")).toBeVisible();
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
// Assert that the duration counter is 00:01 before clicking the play button
await expect(audioBody.locator(".mx_AudioPlayer_mediaInfo time", { hasText: "00:01" })).toBeVisible();
// Assert that the counter is zero before clicking the play button
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
// Click the play button
await audioBody.getByRole("button", { name: "Play" }).click();
// Assert that the pause button is rendered
await expect(audioBody.getByRole("button", { name: "Pause" })).toBeVisible();
// Assert that the timer is reset when the audio file finished playing
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
// Assert that the play button is rendered
await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible();
});
test("should render file size in kibibytes on a file tile", async ({ page }) => {
const size = "1.12 KB"; // actual file size in kibibytes (1024 bytes)
// Upload a file
await uploadFile(page, "cypress/fixtures/matrix-org-client-versions.json");
const tile = page.locator(".mx_FilePanel .mx_EventTile");
// Assert that the file size is displayed in kibibytes, not kilobytes (1000 bytes)
// See: https://github.com/vector-im/element-web/issues/24866
await expect(tile.locator(".mx_MFileBody_info_filename", { hasText: size })).toBeVisible();
await expect(tile.locator(".mx_MFileBody_download a", { hasText: size })).toBeVisible();
await expect(tile.locator(".mx_MFileBody_download .mx_MImageBody_size", { hasText: size })).toBeVisible();
});
});
test.describe("download", () => {
test("should download an image via the link on the panel", async ({ page, context }) => {
// Upload an image file
await uploadFile(page, "cypress/fixtures/riot.png");
// Detect the image file on the panel
const imageBody = page.locator(
".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine.mx_EventTile_image .mx_MImageBody",
);
const link = imageBody.locator(".mx_MFileBody_download a");
const newPagePromise = context.waitForEvent("page");
// const downloadPromise = page.waitForEvent("download");
// Click the anchor link (not the image itself)
await link.click();
const newPage = await newPagePromise;
// XXX: Clicking the link opens the image in a new tab on some browsers rather than downloading, so handle that case
await expect(newPage).toHaveURL(/.+\/_matrix\/media\/\w+\/download\/localhost\/\w+/);
// .catch(async () => {
// const download = await downloadPromise;
// expect(download.suggestedFilename()).toBe("riot.png");
// });
});
});
});

View File

@ -0,0 +1,43 @@
/*
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 { test, expect } from "../../element-web-test";
const ROOM_NAME = "Test room";
const NAME = "Alice";
test.describe("NotificationPanel", () => {
test.use({
displayName: NAME,
});
test.beforeEach(async ({ app, user }) => {
await app.client.createRoom({ name: ROOM_NAME });
});
test("should render empty state", async ({ page, app }) => {
await app.labs.enableLabsFeature("feature_notifications");
await app.viewRoomByName(ROOM_NAME);
await page.getByRole("button", { name: "Notifications" }).click();
// Wait until the information about the empty state is rendered
await expect(page.locator(".mx_NotificationPanel_empty")).toBeVisible();
// Take a snapshot of RightPanel
await expect(page.locator(".mx_RightPanel")).toHaveScreenshot("empty.png");
});
});

View File

@ -0,0 +1,154 @@
/*
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 { Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
const ROOM_NAME = "Test room";
const ROOM_NAME_LONG =
"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.";
const SPACE_NAME = "Test space";
const NAME = "Alice";
const ROOM_ADDRESS_LONG =
"loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_EntityTile, [title="${name}"]`);
}
test.describe("RightPanel", () => {
test.use({
displayName: NAME,
});
test.beforeEach(async ({ app, user }) => {
await app.client.createRoom({ name: ROOM_NAME });
await app.client.createSpace({ name: SPACE_NAME });
});
test.describe("in rooms", () => {
test("should handle long room address and long room name", async ({ page, app }) => {
await app.client.createRoom({ name: ROOM_NAME_LONG });
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);
await app.settings.openRoomSettings();
// Set a local room address
const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
await localAddresses.getByRole("textbox").fill(ROOM_ADDRESS_LONG);
await localAddresses.getByRole("button", { name: "Add" }).click();
await expect(localAddresses.getByText(`#${ROOM_ADDRESS_LONG}:localhost`)).toHaveClass(
"mx_EditableItem_item",
);
await app.closeDialog();
// Close and reopen the right panel to render the room address
await page.getByRole("button", { name: "Room info" }).click();
await expect(page.locator(".mx_RightPanel")).not.toBeVisible();
await page.getByRole("button", { name: "Room info" }).click();
await expect(page.locator(".mx_RightPanel")).toHaveScreenshot("with-name-and-address.png", {
mask: [page.locator(".mx_BaseAvatar")],
});
});
test("should handle clicking add widgets", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("button", { name: "Add widgets, bridges & bots" }).click();
await expect(page.locator(".mx_IntegrationManager")).toBeVisible();
});
test("should handle viewing export chat", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Export Chat" }).click();
await expect(page.locator(".mx_ExportDialog")).toBeVisible();
});
test("should handle viewing share room", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Copy link" }).click();
await expect(page.locator(".mx_ShareDialog")).toBeVisible();
});
test("should handle viewing room settings", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Settings" }).click();
await expect(page.locator(".mx_RoomSettingsDialog")).toBeVisible();
await expect(page.locator(".mx_Dialog_title").getByText("Room Settings - " + ROOM_NAME)).toBeVisible();
});
test("should handle viewing files", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Files" }).click();
await expect(page.locator(".mx_FilePanel")).toBeVisible();
await expect(page.locator(".mx_FilePanel_empty")).toBeVisible();
await page.getByRole("button", { name: "Room information" }).click();
await checkRoomSummaryCard(page, ROOM_NAME);
});
test("should handle viewing room member", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "People" }).click();
await expect(page.locator(".mx_MemberList")).toBeVisible();
await getMemberTileByName(page, NAME).click();
await expect(page.locator(".mx_UserInfo")).toBeVisible();
await expect(page.locator(".mx_UserInfo_profile").getByText(NAME)).toBeVisible();
await page.getByRole("button", { name: "Room members" }).click();
await expect(page.locator(".mx_MemberList")).toBeVisible();
await page.getByRole("button", { name: "Room information" }).click();
await checkRoomSummaryCard(page, ROOM_NAME);
});
});
test.describe("in spaces", () => {
test("should handle viewing space member", async ({ page, app }) => {
await app.viewSpaceHomeByName(SPACE_NAME);
// \d represents the number of the space members
await page
.locator(".mx_RoomInfoLine_private")
.getByRole("button", { name: /\d member/ })
.click();
await expect(page.locator(".mx_MemberList")).toBeVisible();
await expect(page.locator(".mx_SpaceScopeHeader").getByText(SPACE_NAME)).toBeVisible();
await getMemberTileByName(page, NAME).click();
await expect(page.locator(".mx_UserInfo")).toBeVisible();
await expect(page.locator(".mx_UserInfo_profile").getByText(NAME)).toBeVisible();
await expect(page.locator(".mx_SpaceScopeHeader").getByText(SPACE_NAME)).toBeVisible();
await page.getByRole("button", { name: "Back" }).click();
await expect(page.locator(".mx_MemberList")).toBeVisible();
});
});
});

View File

@ -0,0 +1,30 @@
/*
Copyright 2023 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 Page, expect } from "@playwright/test";
import { ElementAppPage } from "../../pages/ElementAppPage";
export async function viewRoomSummaryByName(page: Page, app: ElementAppPage, name: string): Promise<void> {
await app.viewRoomByName(name);
await page.getByRole("button", { name: "Room info" }).click();
return checkRoomSummaryCard(page, name);
}
export async function checkRoomSummaryCard(page: Page, name: string): Promise<void> {
await expect(page.locator(".mx_RoomSummaryCard")).toBeVisible();
await expect(page.locator(".mx_RoomSummaryCard")).toContainText(name);
}

View File

@ -33,7 +33,9 @@ test.describe("Appearance user settings tab", () => {
// Assert that "Hide advanced" link button is rendered
await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible();
await expect(tab).toHaveScreenshot();
await expect(tab).toHaveScreenshot("appearance-tab.png", {
mask: [tab.locator(".mx_DisambiguatedProfile_displayName, .mx_BaseAvatar")],
});
});
test("should support switching layouts", async ({ page, user, app }) => {

View File

@ -14,14 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type Locator, type Page } from "@playwright/test";
import { type Locator, type Page, expect } from "@playwright/test";
import { Settings } from "./settings";
import { Client } from "./client";
import { Labs } from "./labs";
export class ElementAppPage {
public constructor(private readonly page: Page) {}
public labs = new Labs(this.page);
public settings = new Settings(this.page);
public client: Client = new Client(this.page);
@ -94,4 +96,25 @@ export class ElementAppPage {
await composer.getByRole("button", { name: "More options", exact: true }).click();
return this.page.getByRole("menu");
}
/**
* Returns the space panel space button based on a name. The space
* must be visible in the space panel
* @param name The space name to find
*/
public async getSpacePanelButton(name: string): Promise<Locator> {
const button = this.page.getByRole("button", { name: name });
await expect(button).toHaveClass(/mx_SpaceButton/);
return button;
}
/**
* Opens the given space home by name. The space must be visible in
* the space list.
* @param name The space name to find and click on/open.
*/
public async viewSpaceHomeByName(name: string): Promise<void> {
const button = await this.getSpacePanelButton(name);
return button.dblclick();
}
}

View File

@ -116,6 +116,21 @@ export class Client {
}, options);
}
/**
* Create a space with given options.
* @param options the options to apply when creating the space
* @return the ID of the newly created space (room)
*/
public async createSpace(options: ICreateRoomOpts): Promise<string> {
return this.createRoom({
...options,
creation_content: {
...options.creation_content,
type: "m.space",
},
});
}
/**
* Joins the given room by alias or ID
* @param roomIdOrAlias the id or alias of the room to join

32
playwright/pages/labs.ts Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright 2023 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 { Page } from "playwright-core";
export class Labs {
constructor(private page: Page) {}
/**
* 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<void> {
await this.page.evaluate((feature) => {
window.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
}, feature);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* Percy screenshot test specific CSS */
/* Screenshot test specific CSS */
@media only percy {
/* Stick the default room avatar colour, so it doesn't cause a false diff on the screenshot */
.mx_BaseAvatar {