Migrate most of editing.spec.ts from Cypress to Playwright (#11947)

* Migrate location.spec.ts from Cypress to Playwright

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

* Migrate location.spec.ts from Cypress to Playwright

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

* Add screenshot

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

* Deflake

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2023-11-28 13:52:09 +00:00 committed by GitHub
parent 372737d075
commit 83f0650ed4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 361 additions and 255 deletions

View file

@ -16,17 +16,8 @@ limitations under the License.
/// <reference types="cypress" />
import type { EventType, MsgType, ISendEventResponse, IContent } from "matrix-js-sdk/src/matrix";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import type { MsgType, IContent } from "matrix-js-sdk/src/matrix";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import Chainable = Cypress.Chainable;
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
return cy.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.text" as MsgType,
body: "Message",
});
};
/** generate a message event which will take up some room on the page. */
function mkPadding(n: number): IContent {
@ -40,37 +31,13 @@ function mkPadding(n: number): IContent {
describe("Editing", () => {
let homeserver: HomeserverInstance;
let roomId: string;
// Edit "Message"
const editLastMessage = (edit: string) => {
cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Edit" }).click();
cy.findByRole("textbox", { name: "Edit message" }).type(`{selectAll}{del}${edit}{enter}`);
};
const clickEditedMessage = (edited: string) => {
// Assert that the message was edited
cy.contains(".mx_EventTile", edited)
.should("exist")
.within(() => {
// Click to display the message edit history dialog
cy.contains(".mx_EventTile_edited", "(edited)").click();
});
};
const clickButtonViewSource = () => {
// Assert that "View Source" button is rendered and click it
cy.get(".mx_EventTile .mx_EventTile_line").realHover().findByRole("button", { name: "View Source" }).click();
};
beforeEach(() => {
cy.startHomeserver("default").then((data) => {
homeserver = data;
cy.initTestUser(homeserver, "Edith").then(() => {
cy.createRoom({ name: "Test room" }).then((_room1Id) => {
roomId = _room1Id;
}),
cy.injectAxe();
cy.createRoom({ name: "Test room" });
cy.injectAxe();
});
});
});
@ -79,224 +46,6 @@ describe("Editing", () => {
cy.stopHomeserver(homeserver);
});
it("should render and interact with the message edit history dialog", () => {
// Click the "Remove" button on the message edit history dialog
const clickButtonRemove = () => {
cy.get(".mx_EventTile_line").realHover().findByRole("button", { name: "Remove" }).click();
};
cy.visit("/#/room/" + roomId);
// Send "Message"
sendEvent(roomId);
cy.get(".mx_RoomView_MessageList").within(() => {
// Edit "Message" to "Massage"
editLastMessage("Massage");
// Assert that the edit label is visible
cy.get(".mx_EventTile_edited").should("be.visible");
clickEditedMessage("Massage");
});
cy.get(".mx_Dialog").within(() => {
// Assert that the message edit history dialog is rendered
cy.get(".mx_MessageEditHistoryDialog").within(() => {
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
cy.get("li").should("have.css", "clear", "both");
cy.get(".mx_EventTile .mx_MessageTimestamp")
.should("have.css", "position", "absolute")
.should("have.css", "inset-inline-start", "0px")
.should("have.css", "text-align", "center");
// Assert that monospace characters can fill the content line as expected
cy.get(".mx_EventTile .mx_EventTile_content").should("have.css", "margin-inline-end", "0px");
// Assert that zero block start padding is applied to mx_EventTile as expected
// See: .mx_EventTile on _EventTile.pcss
cy.get(".mx_EventTile").should("have.css", "padding-block-start", "0px");
// Assert that the date separator is rendered at the top
cy.get("li:nth-child(1) .mx_TimelineSeparator").within(() => {
cy.get("h2").within(() => {
cy.findByText("today").should("have.css", "text-transform", "capitalize");
});
});
// Assert that the edited message is rendered under the date separator
cy.get("li:nth-child(2) .mx_EventTile").within(() => {
// Assert that the edited message body consists of both deleted character and inserted character
// Above the first "e" of "Message" was replaced with "a"
cy.get(".mx_EventTile_content .mx_EventTile_body").should("have.text", "Meassage");
cy.get(".mx_EventTile_content .mx_EventTile_body").within(() => {
cy.get(".mx_EditHistoryMessage_deletion").within(() => {
cy.findByText("e");
});
cy.get(".mx_EditHistoryMessage_insertion").within(() => {
cy.findByText("a");
});
});
});
// Assert that the original message is rendered at the bottom
cy.get("li:nth-child(3) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").within(() => {
cy.findByText("Message");
});
});
});
});
// Exclude timestamps from a snapshot
const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
// Take a snapshot of the dialog
cy.get(".mx_Dialog_wrapper").percySnapshotElement("Message edit history dialog", { percyCSS });
cy.get(".mx_Dialog").within(() => {
cy.get(".mx_MessageEditHistoryDialog li:nth-child(2) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").should("have.text", "Meassage");
// Click the "Remove" button again
clickButtonRemove();
});
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
cy.get(".mx_TextInputDialog").closeDialog();
// Assert that the message edit history dialog is rendered again after it was closed
cy.get(".mx_MessageEditHistoryDialog li:nth-child(2) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").should("have.text", "Meassage");
// Click the "Remove" button again
clickButtonRemove();
});
// This time remove the message really
cy.get(".mx_TextInputDialog").within(() => {
cy.findByRole("textbox", { name: "Reason (optional)" }).type("This is a test."); // Reason
cy.findByRole("button", { name: "Remove" }).click();
});
// Assert that the message edit history dialog is rendered again
cy.get(".mx_MessageEditHistoryDialog").within(() => {
// Assert that the date is rendered
cy.get("li:nth-child(1) .mx_TimelineSeparator").within(() => {
cy.get("h2").within(() => {
cy.findByText("today").should("have.css", "text-transform", "capitalize");
});
});
// Assert that the original message is rendered under the date on the dialog
cy.get("li:nth-child(2) .mx_EventTile").within(() => {
cy.get(".mx_EventTile_content .mx_EventTile_body").within(() => {
cy.findByText("Message");
});
});
// Assert that the edited message is gone
cy.contains(".mx_EventTile_content .mx_EventTile_body", "Meassage").should("not.exist");
cy.closeDialog();
});
});
// Assert that the main timeline is rendered
cy.get(".mx_RoomView_MessageList").within(() => {
cy.get(".mx_EventTile_last .mx_RedactedBody").within(() => {
// Assert that the placeholder is rendered
cy.findByText("Message deleted");
});
});
});
it("should render 'View Source' button in developer mode on the message edit history dialog", () => {
cy.visit("/#/room/" + roomId);
// Send "Message"
sendEvent(roomId);
cy.get(".mx_RoomView_MessageList").within(() => {
// Edit "Message" to "Massage"
editLastMessage("Massage");
// Assert that the edit label is visible
cy.get(".mx_EventTile_edited").should("be.visible");
clickEditedMessage("Massage");
});
cy.get(".mx_Dialog").within(() => {
// Assert that the original message is rendered
cy.get(".mx_MessageEditHistoryDialog li:nth-child(3)").within(() => {
// Assert that "View Source" is not rendered
cy.get(".mx_EventTile .mx_EventTile_line")
.realHover()
.findByRole("button", { name: "View Source" })
.should("not.exist");
});
cy.closeDialog();
});
// Enable developer mode
cy.setSettingValue("developerMode", null, SettingLevel.ACCOUNT, true);
cy.get(".mx_RoomView_MessageList").within(() => {
clickEditedMessage("Massage");
});
cy.get(".mx_Dialog").within(() => {
// Assert that the edited message is rendered
cy.get(".mx_MessageEditHistoryDialog li:nth-child(2)").within(() => {
// Assert that "Remove" button for the original message is rendered
cy.get(".mx_EventTile .mx_EventTile_line").realHover().findByRole("button", { name: "Remove" });
clickButtonViewSource();
});
// Assert that view source dialog is rendered and close the dialog
cy.get(".mx_ViewSource").closeDialog();
// Assert that the original message is rendered
cy.get(".mx_MessageEditHistoryDialog li:nth-child(3)").within(() => {
// Assert that "Remove" button for the original message does not exist
cy.get(".mx_EventTile .mx_EventTile_line")
.realHover()
.findByRole("button", { name: "Remove" })
.should("not.exist");
clickButtonViewSource();
});
// Assert that view source dialog is rendered and close the dialog
cy.get(".mx_ViewSource").closeDialog();
});
});
it("should close the composer when clicking save after making a change and undoing it", () => {
cy.visit("/#/room/" + roomId);
sendEvent(roomId);
// Edit message
cy.get(".mx_RoomView_body .mx_EventTile").within(() => {
cy.findByText("Message");
cy.get(".mx_EventTile_line").realHover().findByRole("button", { name: "Edit" }).click().checkA11y();
cy.get(".mx_EventTile_line")
.findByRole("textbox", { name: "Edit message" })
.type("Foo{backspace}{backspace}{backspace}{enter}")
.checkA11y();
});
cy.get(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]").within(() => {
cy.findByText("Message");
});
// Assert that the edit composer has gone away
cy.findByRole("textbox", { name: "Edit message" }).should("not.exist");
});
it("should correctly display events which are edited, where we lack the edit event", () => {
// This tests the behaviour when a message has been edited some time after it has been sent, and we
// jump back in room history to view the event, but do not have the actual edit event.

View file

@ -0,0 +1,292 @@
/*
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, Page } from "@playwright/test";
import type { EventType, MsgType, ISendEventResponse } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { SettingLevel } from "../../../src/settings/SettingLevel";
const sendEvent = async (app: ElementAppPage, roomId: string): Promise<ISendEventResponse> => {
return app.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.text" as MsgType,
body: "Message",
});
};
test.describe("Editing", () => {
// Edit "Message"
const editLastMessage = async (page: Page, edit: string) => {
const eventTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
await eventTile.hover();
await eventTile.getByRole("button", { name: "Edit" }).click();
const textbox = page.getByRole("textbox", { name: "Edit message" });
await textbox.fill(edit);
await textbox.press("Enter");
};
const clickEditedMessage = async (page: Page, edited: string) => {
// Assert that the message was edited
const eventTile = page.locator(".mx_EventTile", { hasText: edited });
await expect(eventTile).toBeVisible();
// Click to display the message edit history dialog
await eventTile.getByText("(edited)").click();
};
const clickButtonViewSource = async (locator: Locator) => {
const eventTile = locator.locator(".mx_EventTile_line");
await eventTile.hover();
// Assert that "View Source" button is rendered and click it
await eventTile.getByRole("button", { name: "View Source" }).click();
};
test.use({
displayName: "Edith",
room: async ({ user, app }, use) => {
const roomId = await app.createRoom({ name: "Test room" });
await use({ roomId });
},
});
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => {
// Click the "Remove" button on the message edit history dialog
const clickButtonRemove = async (locator: Locator) => {
const eventTileLine = locator.locator(".mx_EventTile_line");
await eventTileLine.hover();
await eventTileLine.getByRole("button", { name: "Remove" }).click();
};
await page.goto(`#/room/${room.roomId}`);
// Send "Message"
await sendEvent(app, room.roomId);
// Edit "Message" to "Massage"
await editLastMessage(page, "Massage");
// Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
await clickEditedMessage(page, "Massage");
// Assert that the message edit history dialog is rendered
const dialog = page.getByRole("dialog");
const li = dialog.getByRole("listitem").last();
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
await expect(li).toHaveCSS("clear", "both");
const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp");
await expect(timestamp).toHaveCSS("position", "absolute");
await expect(timestamp).toHaveCSS("inset-inline-start", "0px");
await expect(timestamp).toHaveCSS("text-align", "center");
// Assert that monospace characters can fill the content line as expected
await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px");
// Assert that zero block start padding is applied to mx_EventTile as expected
// See: .mx_EventTile on _EventTile.pcss
await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px");
// Assert that the date separator is rendered at the top
await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS(
"text-transform",
"capitalize",
);
{
// Assert that the edited message is rendered under the date separator
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
// Assert that the edited message body consists of both deleted character and inserted character
// Above the first "e" of "Message" was replaced with "a"
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
const body = tile.locator(".mx_EventTile_content .mx_EventTile_body");
await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible();
await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible();
}
// Assert that the original message is rendered at the bottom
await expect(
dialog
.locator("li:nth-child(3) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible();
// Take a snapshot of the dialog
await expect(dialog).toHaveScreenshot("message-edit-history-dialog.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
{
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again
await clickButtonRemove(tile);
}
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
await app.closeDialog();
{
// Assert that the message edit history dialog is rendered again after it was closed
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again
await clickButtonRemove(tile);
}
// This time remove the message really
const textInputDialog = page.locator(".mx_TextInputDialog");
await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason
await textInputDialog.getByRole("button", { name: "Remove" }).click();
// Assert that the message edit history dialog is rendered again
const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog");
// Assert that the date is rendered
await expect(
messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }),
).toHaveCSS("text-transform", "capitalize");
// Assert that the original message is rendered under the date on the dialog
await expect(
messageEditHistoryDialog
.locator("li:nth-child(2) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible();
// Assert that the edited message is gone
await expect(
messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }),
).not.toBeVisible();
await app.closeDialog();
// Assert that the redaction placeholder is rendered
await expect(
page
.locator(".mx_RoomView_MessageList")
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
).toBeVisible();
});
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
page,
user,
app,
room,
}) => {
await page.goto(`#/room/${room.roomId}`);
// Send "Message"
await sendEvent(app, room.roomId);
// Edit "Message" to "Massage"
await editLastMessage(page, "Massage");
// Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
await clickEditedMessage(page, "Massage");
{
const dialog = page.getByRole("dialog");
// Assert that the original message is rendered
const li = dialog.locator("li:nth-child(3)");
// Assert that "View Source" is not rendered
const eventLine = li.locator(".mx_EventTile_line");
await eventLine.hover();
await expect(eventLine.getByRole("button", { name: "View Source" })).not.toBeVisible();
}
await app.closeDialog();
// Enable developer mode
await app.setSettingValue("developerMode", null, SettingLevel.ACCOUNT, true);
await clickEditedMessage(page, "Massage");
{
const dialog = page.getByRole("dialog");
{
// Assert that the edited message is rendered
const li = dialog.locator("li:nth-child(2)");
// Assert that "Remove" button for the original message is rendered
const line = li.locator(".mx_EventTile_line");
await line.hover();
await expect(line.getByRole("button", { name: "Remove" })).toBeVisible();
await clickButtonViewSource(li);
}
// Assert that view source dialog is rendered and close the dialog
await app.closeDialog();
{
// Assert that the original message is rendered
const li = dialog.locator("li:nth-child(3)");
// Assert that "Remove" button for the original message does not exist
const line = li.locator(".mx_EventTile_line");
await line.hover();
await expect(line.getByRole("button", { name: "Remove" })).not.toBeVisible();
await clickButtonViewSource(li);
}
// Assert that view source dialog is rendered and close the dialog
await app.closeDialog();
}
});
test("should close the composer when clicking save after making a change and undoing it", async ({
page,
user,
app,
room,
axe,
checkA11y,
}) => {
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
axe.exclude(".mx_Tooltip_visible"); // XXX: this is fine but would be good to fix
await page.goto(`#/room/${room.roomId}`);
await sendEvent(app, room.roomId);
{
// Edit message
const tile = page.locator(".mx_RoomView_body .mx_EventTile").last();
await expect(tile.getByText("Message", { exact: true })).toBeVisible();
const line = tile.locator(".mx_EventTile_line");
await line.hover();
await line.getByRole("button", { name: "Edit" }).click();
await checkA11y();
const editComposer = page.getByRole("textbox", { name: "Edit message" });
await editComposer.pressSequentially("Foo");
await editComposer.press("Backspace");
await editComposer.press("Backspace");
await editComposer.press("Backspace");
await editComposer.press("Enter");
await checkA11y();
}
await expect(
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
).toBeVisible();
// Assert that the edit composer has gone away
await expect(page.getByRole("textbox", { name: "Edit message" })).not.toBeVisible();
});
});

View file

@ -64,6 +64,7 @@ export const test = base.extend<
app: ElementAppPage;
mailhog?: { api: mailhog.API; instance: Instance };
crypto: Crypto;
room?: { roomId: string };
toasts: Toasts;
}
>({

View file

@ -16,10 +16,15 @@ limitations under the License.
import { type MatrixClient } from "matrix-js-sdk/src/matrix";
import { type SettingLevel } from "../src/settings/SettingLevel";
declare global {
interface Window {
mxMatrixClientPeg: {
get(): MatrixClient;
};
mxSettingsStore: {
setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void>;
};
}
}

View file

@ -15,11 +15,42 @@ limitations under the License.
*/
import { type Locator, type Page } from "@playwright/test";
import { type ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
import type { IContent, ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/matrix";
import type { SettingLevel } from "../../src/settings/SettingLevel";
export class ElementAppPage {
public constructor(private readonly page: Page) {}
/**
* Sets the value for a setting. The room ID is optional if the
* setting is not being set for a particular room, otherwise it
* should be supplied. The value may be null to indicate that the
* level should no longer have an override.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be
* null.
* @param {SettingLevel} level The level to change the value at.
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
public async setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
return this.page.evaluate<
Promise<void>,
{
settingName: string;
roomId: string | null;
level: SettingLevel;
value: any;
}
>(
({ settingName, roomId, level, value }) => {
return window.mxSettingsStore.setValue(settingName, roomId, level, value);
},
{ settingName, roomId, level, value },
);
}
/**
* Open the top left user menu, returning a Locator to the resulting context menu.
*/
@ -100,4 +131,32 @@ export class ElementAppPage {
await composer.getByRole("button", { name: "More options", exact: true }).click();
return this.page.getByRole("menu");
}
/**
* @param {string} roomId
* @param {string} threadId
* @param {string} eventType
* @param {Object} content
*/
public async sendEvent(
roomId: string,
threadId: string | null,
eventType: string,
content: IContent,
): Promise<ISendEventResponse> {
return this.page.evaluate<
Promise<ISendEventResponse>,
{
roomId: string;
threadId: string | null;
eventType: string;
content: IContent;
}
>(
async ({ roomId, threadId, eventType, content }) => {
return window.mxMatrixClientPeg.get().sendEvent(roomId, threadId, eventType, content);
},
{ roomId, threadId, eventType, content },
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB