From 83f0650ed421e6b2e42f767d73360f1406ce3071 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 28 Nov 2023 13:52:09 +0000 Subject: [PATCH] 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> --- cypress/e2e/editing/editing.spec.ts | 257 +-------------- playwright/e2e/editing/editing.spec.ts | 292 ++++++++++++++++++ playwright/element-web-test.ts | 1 + playwright/global.d.ts | 5 + playwright/pages/ElementAppPage.ts | 61 +++- .../message-edit-history-dialog-linux.png | Bin 0 -> 8442 bytes 6 files changed, 361 insertions(+), 255 deletions(-) create mode 100644 playwright/e2e/editing/editing.spec.ts create mode 100644 playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png diff --git a/cypress/e2e/editing/editing.spec.ts b/cypress/e2e/editing/editing.spec.ts index 8695531a60..113da3421a 100644 --- a/cypress/e2e/editing/editing.spec.ts +++ b/cypress/e2e/editing/editing.spec.ts @@ -16,17 +16,8 @@ limitations under the License. /// -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 => { - 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. diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts new file mode 100644 index 0000000000..f05f6f3382 --- /dev/null +++ b/playwright/e2e/editing/editing.spec.ts @@ -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 => { + 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(); + }); +}); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 28956919b0..042af50a56 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -64,6 +64,7 @@ export const test = base.extend< app: ElementAppPage; mailhog?: { api: mailhog.API; instance: Instance }; crypto: Crypto; + room?: { roomId: string }; toasts: Toasts; } >({ diff --git a/playwright/global.d.ts b/playwright/global.d.ts index 784c09cee4..8b4a280153 100644 --- a/playwright/global.d.ts +++ b/playwright/global.d.ts @@ -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; + }; } } diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts index a984ba0d00..359c0a54b8 100644 --- a/playwright/pages/ElementAppPage.ts +++ b/playwright/pages/ElementAppPage.ts @@ -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 { + return this.page.evaluate< + Promise, + { + 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 { + return this.page.evaluate< + Promise, + { + 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 }, + ); + } } diff --git a/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png b/playwright/snapshots/editing/editing.spec.ts/message-edit-history-dialog-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..0135b0a66e8bbe7735a0aca278bf5a780d13b2ea GIT binary patch literal 8442 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�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 Nz!cQv^Pii&{~v}^LKOf2 literal 0 HcmV?d00001