From 088710811d43090c72a4b1ca50fd90fdd25fe6b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 Dec 2023 13:49:51 +0000 Subject: [PATCH] Migrate composer.spec.ts from Cypress to Playwright (#12024) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- cypress/e2e/composer/composer.spec.ts | 352 ----------------------- playwright/e2e/composer/composer.spec.ts | 323 +++++++++++++++++++++ 2 files changed, 323 insertions(+), 352 deletions(-) delete mode 100644 cypress/e2e/composer/composer.spec.ts create mode 100644 playwright/e2e/composer/composer.spec.ts diff --git a/cypress/e2e/composer/composer.spec.ts b/cypress/e2e/composer/composer.spec.ts deleted file mode 100644 index 43b81d514d..0000000000 --- a/cypress/e2e/composer/composer.spec.ts +++ /dev/null @@ -1,352 +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 { SettingLevel } from "../../../src/settings/SettingLevel"; -import { MatrixClient } from "../../global"; - -describe("Composer", () => { - let homeserver: HomeserverInstance; - - beforeEach(() => { - cy.startHomeserver("default").then((data) => { - homeserver = data; - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - describe("CIDER", () => { - beforeEach(() => { - cy.initTestUser(homeserver, "Janet"); - cy.createRoom({ name: "Composing Room" }).then((roomId) => cy.viewRoomById(roomId)); - }); - - it("sends a message when you click send or press Enter", () => { - // Type a message - cy.findByRole("textbox", { name: "Send a message…" }).type("my message 0"); - // It has not been sent yet - cy.contains(".mx_EventTile_body", "my message 0").should("not.exist"); - - // Click send - cy.findByRole("button", { name: "Send message" }).click(); - // It has been sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 0").should("exist"); - }); - - // Type another and press Enter afterwards - cy.findByRole("textbox", { name: "Send a message…" }).type("my message 1{enter}"); - // It was sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 1").should("exist"); - }); - }); - - it("can write formatted text", () => { - cy.findByRole("textbox", { name: "Send a message…" }).type("my bold{ctrl+b} message"); - cy.findByRole("button", { name: "Send message" }).click(); - // Note: both "bold" and "message" are bold, which is probably surprising - cy.get(".mx_EventTile_body strong").within(() => { - cy.findByText("bold message").should("exist"); - }); - }); - - it("should allow user to input emoji via graphical picker", () => { - cy.getComposer(false).within(() => { - cy.findByRole("button", { name: "Emoji" }).click(); - }); - - cy.findByTestId("mx_EmojiPicker").within(() => { - cy.contains(".mx_EmojiPicker_item", "😇").click(); - }); - - cy.get(".mx_ContextualMenu_background").click(); // Close emoji picker - cy.findByRole("textbox", { name: "Send a message…" }).type("{enter}"); // Send message - - cy.get(".mx_EventTile_body").within(() => { - cy.findByText("😇"); - }); - }); - - describe("when Ctrl+Enter is required to send", () => { - beforeEach(() => { - cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); - }); - - it("only sends when you press Ctrl+Enter", () => { - // Type a message and press Enter - cy.findByRole("textbox", { name: "Send a message…" }).type("my message 3{enter}"); - // It has not been sent yet - cy.contains(".mx_EventTile_body", "my message 3").should("not.exist"); - - // Press Ctrl+Enter - cy.findByRole("textbox", { name: "Send a message…" }).type("{ctrl+enter}"); - // It was sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 3").should("exist"); - }); - }); - }); - }); - - describe("Rich text editor", () => { - beforeEach(() => { - cy.enableLabsFeature("feature_wysiwyg_composer"); - cy.initTestUser(homeserver, "Janet"); - cy.createRoom({ name: "Composing Room" }).then((roomId) => cy.viewRoomById(roomId)); - }); - - describe("Commands", () => { - // TODO add tests for rich text mode - - describe("Plain text mode", () => { - it("autocomplete behaviour tests", () => { - // Select plain text mode after composer is ready - cy.get("div[contenteditable=true]").should("exist"); - cy.findByRole("button", { name: "Hide formatting" }).click(); - - // Typing a single / displays the autocomplete menu and contents - cy.findByRole("textbox").type("/"); - - // Check that the autocomplete options are visible and there are more than 0 items - cy.findByTestId("autocomplete-wrapper").should("not.be.empty"); - - // Entering `//` or `/ ` hides the autocomplete contents - // Add an extra slash for `//` - cy.findByRole("textbox").type("/"); - cy.findByTestId("autocomplete-wrapper").should("be.empty"); - // Remove the extra slash to go back to `/` - cy.findByRole("textbox").type("{Backspace}"); - cy.findByTestId("autocomplete-wrapper").should("not.be.empty"); - // Add a trailing space for `/ ` - cy.findByRole("textbox").type(" "); - cy.findByTestId("autocomplete-wrapper").should("be.empty"); - - // Typing a command that takes no arguments (/devtools) and selecting by click works - cy.findByRole("textbox").type("{Backspace}dev"); - cy.findByTestId("autocomplete-wrapper").within(() => { - cy.findByText("/devtools").click(); - }); - // Check it has closed the autocomplete and put the text into the composer - cy.findByTestId("autocomplete-wrapper").should("not.be.visible"); - cy.findByRole("textbox").within(() => { - cy.findByText("/devtools").should("exist"); - }); - // Send the message and check the devtools dialog appeared, then close it - cy.findByRole("button", { name: "Send message" }).click(); - cy.findByRole("dialog").within(() => { - cy.findByText("Developer Tools").should("exist"); - }); - cy.findByRole("button", { name: "Close dialog" }).click(); - - // Typing a command that takes arguments (/spoiler) and selecting with enter works - cy.findByRole("textbox").type("/spoil"); - cy.findByTestId("autocomplete-wrapper").within(() => { - cy.findByText("/spoiler").should("exist"); - }); - cy.findByRole("textbox").type("{Enter}"); - // Check it has closed the autocomplete and put the text into the composer - cy.findByTestId("autocomplete-wrapper").should("not.be.visible"); - cy.findByRole("textbox").within(() => { - cy.findByText("/spoiler").should("exist"); - }); - // Enter some more text, then send the message - cy.findByRole("textbox").type("this is the spoiler text "); - cy.findByRole("button", { name: "Send message" }).click(); - // Check that a spoiler item has appeared in the timeline and contains the spoiler command text - cy.get("button.mx_EventTile_spoiler").should("exist"); - cy.findByText("this is the spoiler text").should("exist"); - }); - }); - }); - - describe("Mentions", () => { - // TODO add tests for rich text mode - - describe("Plain text mode", () => { - // https://github.com/vector-im/element-web/issues/26037 - it.skip("autocomplete behaviour tests", () => { - // Set up a private room so we have another user to mention - const otherUserName = "Bob"; - let bobClient: MatrixClient; - cy.getBot(homeserver, { - displayName: otherUserName, - }).then((bob) => { - bobClient = bob; - }); - // create DM with bob - cy.getClient() - .then(async (cli) => { - const bobRoom = await cli.createRoom({ is_direct: true }); - await cli.invite(bobRoom.room_id, bobClient.getUserId()); - await cli.setAccountData("m.direct" as EventType, { - [bobClient.getUserId()]: [bobRoom.room_id], - }); - return bobRoom.room_id; - }) - .then((bobRoomId) => cy.viewRoomById(bobRoomId)); - - // Select plain text mode after composer is ready - cy.get("div[contenteditable=true]").should("exist"); - cy.findByRole("button", { name: "Hide formatting" }).click(); - - // Typing a single @ does not display the autocomplete menu and contents - cy.findByRole("textbox").type("@"); - cy.findByTestId("autocomplete-wrapper").should("be.empty"); - - // Entering the first letter of the other user's name opens the autocomplete... - cy.findByRole("textbox").type(otherUserName.slice(0, 1)); - cy.findByTestId("autocomplete-wrapper") - .should("not.be.empty") - .within(() => { - // ...with the other user name visible, and clicking that username... - cy.findByText(otherUserName).should("exist").click(); - }); - // ...inserts the username into the composer - cy.findByRole("textbox").within(() => { - cy.findByText(otherUserName, { exact: false }) - .should("exist") - .should("have.attr", "contenteditable", "false") - .should("have.attr", "data-mention-type", "user"); - }); - - // Send the message to clear the composer - cy.findByRole("button", { name: "Send message" }).click(); - - // Typing an @, then other user's name, then trailing space closes the autocomplete - cy.findByRole("textbox").type(`@${otherUserName} `); - cy.findByTestId("autocomplete-wrapper").should("be.empty"); - - // Send the message to clear the composer - cy.findByRole("button", { name: "Send message" }).click(); - - // Moving the cursor back to an "incomplete" mention opens the autocomplete - cy.findByRole("textbox").type(`initial text @${otherUserName.slice(0, 1)} abc`); - cy.findByTestId("autocomplete-wrapper").should("be.empty"); - // Move the cursor left by 4 to put it to: `@B| abc`, check autocomplete displays - cy.findByRole("textbox").type(`${"{leftArrow}".repeat(4)}`); - cy.findByTestId("autocomplete-wrapper").should("not.be.empty"); - - // Selecting the autocomplete option using Enter inserts it into the composer - cy.findByRole("textbox").type(`{Enter}`); - cy.findByRole("textbox").within(() => { - cy.findByText(otherUserName, { exact: false }) - .should("exist") - .should("have.attr", "contenteditable", "false") - .should("have.attr", "data-mention-type", "user"); - }); - }); - }); - }); - - it("sends a message when you click send or press Enter", () => { - // Type a message - cy.get("div[contenteditable=true]").type("my message 0"); - // It has not been sent yet - cy.contains(".mx_EventTile_body", "my message 0").should("not.exist"); - - // Click send - cy.findByRole("button", { name: "Send message" }).click(); - // It has been sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 0").should("exist"); - }); - - // Type another - cy.get("div[contenteditable=true]").type("my message 1"); - // Send message - cy.get("div[contenteditable=true]").type("{enter}"); - // It was sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 1").should("exist"); - }); - }); - - it("sends only one message when you press Enter multiple times", () => { - // Type a message - cy.get("div[contenteditable=true]").type("my message 0"); - // It has not been sent yet - cy.contains(".mx_EventTile_body", "my message 0").should("not.exist"); - - // Click send - cy.get("div[contenteditable=true]").type("{enter}"); - cy.get("div[contenteditable=true]").type("{enter}"); - cy.get("div[contenteditable=true]").type("{enter}"); - // It has been sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 0").should("exist"); - }); - cy.get(".mx_EventTile_last .mx_EventTile_body").should("have.length", 1); - }); - - it("can write formatted text", () => { - cy.get("div[contenteditable=true]").type("my {ctrl+b}bold{ctrl+b} message"); - cy.findByRole("button", { name: "Send message" }).click(); - cy.get(".mx_EventTile_body strong").within(() => { - cy.findByText("bold").should("exist"); - }); - }); - - describe("when Ctrl+Enter is required to send", () => { - beforeEach(() => { - cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); - }); - - it("only sends when you press Ctrl+Enter", () => { - // Type a message and press Enter - cy.get("div[contenteditable=true]").type("my message 3"); - cy.get("div[contenteditable=true]").type("{enter}"); - // It has not been sent yet - cy.contains(".mx_EventTile_body", "my message 3").should("not.exist"); - - // Press Ctrl+Enter - cy.get("div[contenteditable=true]").type("{ctrl+enter}"); - // It was sent - cy.get(".mx_EventTile_last .mx_EventTile_body").within(() => { - cy.findByText("my message 3").should("exist"); - }); - }); - }); - - describe("links", () => { - it("create link with a forward selection", () => { - // Type a message - cy.get("div[contenteditable=true]").type("my message 0{selectAll}"); - - // Open link modal - cy.findByRole("button", { name: "Link" }).click(); - // Fill the link field - cy.findByRole("textbox", { name: "Link" }).type("https://matrix.org/"); - // Click on save - cy.findByRole("button", { name: "Save" }).click(); - // Send the message - cy.findByRole("button", { name: "Send message" }).click(); - - // It was sent - cy.get(".mx_EventTile_body a").within(() => { - cy.findByText("my message 0").should("exist"); - }); - cy.get(".mx_EventTile_body a").should("have.attr", "href").and("include", "https://matrix.org/"); - }); - }); - }); -}); diff --git a/playwright/e2e/composer/composer.spec.ts b/playwright/e2e/composer/composer.spec.ts new file mode 100644 index 0000000000..e7be457f83 --- /dev/null +++ b/playwright/e2e/composer/composer.spec.ts @@ -0,0 +1,323 @@ +/* +Copyright 2022 - 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 { test, expect } from "../../element-web-test"; +import { SettingLevel } from "../../../src/settings/SettingLevel"; + +const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control"; + +test.describe("Composer", () => { + test.use({ + displayName: "Janet", + }); + + test.use({ + room: async ({ app, user }, use) => { + const roomId = await app.client.createRoom({ name: "Composing Room" }); + await app.viewRoomByName("Composing Room"); + await use({ roomId }); + }, + }); + + test.beforeEach(async ({ room }) => {}); // trigger room fixture + + test.describe("CIDER", () => { + test("sends a message when you click send or press Enter", async ({ page }) => { + const composer = page.getByRole("textbox", { name: "Send a message…" }); + + // Type a message + await composer.pressSequentially("my message 0"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 0" })).not.toBeVisible(); + + // Click send + await page.getByRole("button", { name: "Send message" }).click(); + // It has been sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 0" }), + ).toBeVisible(); + + // Type another and press Enter afterward + await composer.pressSequentially("my message 1"); + await composer.press("Enter"); + // It was sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 1" }), + ).toBeVisible(); + }); + + test("can write formatted text", async ({ page }) => { + const composer = page.getByRole("textbox", { name: "Send a message…" }); + + await composer.pressSequentially("my bold"); + await composer.press(`${CtrlOrMeta}+KeyB`); + await composer.pressSequentially(" message"); + await page.getByRole("button", { name: "Send message" }).click(); + // Note: both "bold" and "message" are bold, which is probably surprising + await expect(page.locator(".mx_EventTile_body strong", { hasText: "bold message" })).toBeVisible(); + }); + + test("should allow user to input emoji via graphical picker", async ({ page, app }) => { + await app.getComposer(false).getByRole("button", { name: "Emoji" }).click(); + + await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click(); + + await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker + await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message + + await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible(); + }); + + test.describe("when Control+Enter is required to send", () => { + test.beforeEach(async ({ app }) => { + await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); + }); + + test("only sends when you press Control+Enter", async ({ page }) => { + const composer = page.getByRole("textbox", { name: "Send a message…" }); + // Type a message and press Enter + await composer.pressSequentially("my message 3"); + await composer.press("Enter"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 3" })).not.toBeVisible(); + + // Press Control+Enter + await composer.press(`${CtrlOrMeta}+Enter`); + // It was sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body", { hasText: "my message 3" }), + ).toBeVisible(); + }); + }); + }); + + test.describe("Rich text editor", () => { + test.use({ + labsFlags: ["feature_wysiwyg_composer"], + }); + + test.describe("Commands", () => { + // TODO add tests for rich text mode + + test.describe("Plain text mode", () => { + test("autocomplete behaviour tests", async ({ page }) => { + // Select plain text mode after composer is ready + await expect(page.locator("div[contenteditable=true]")).toBeVisible(); + await page.getByRole("button", { name: "Hide formatting" }).click(); + + // Typing a single / displays the autocomplete menu and contents + await page.getByRole("textbox").press("/"); + + // Check that the autocomplete options are visible and there are more than 0 items + await expect(page.getByTestId("autocomplete-wrapper")).not.toBeEmpty(); + + // Entering `//` or `/ ` hides the autocomplete contents + // Add an extra slash for `//` + await page.getByRole("textbox").press("/"); + await expect(page.getByTestId("autocomplete-wrapper")).toBeEmpty(); + // Remove the extra slash to go back to `/` + await page.getByRole("textbox").press("Backspace"); + await expect(page.getByTestId("autocomplete-wrapper")).not.toBeEmpty(); + // Add a trailing space for `/ ` + await page.getByRole("textbox").press(" "); + await expect(page.getByTestId("autocomplete-wrapper")).toBeEmpty(); + + // Typing a command that takes no arguments (/devtools) and selecting by click works + await page.getByRole("textbox").press("Backspace"); + await page.getByRole("textbox").pressSequentially("dev"); + await page.getByTestId("autocomplete-wrapper").getByText("/devtools").click(); + // Check it has closed the autocomplete and put the text into the composer + await expect(page.getByTestId("autocomplete-wrapper")).not.toBeVisible(); + await expect(page.getByRole("textbox").getByText("/devtools")).toBeVisible(); + // Send the message and check the devtools dialog appeared, then close it + await page.getByRole("button", { name: "Send message" }).click(); + await expect(page.getByRole("dialog").getByText("Developer Tools")).toBeVisible(); + await page.getByRole("button", { name: "Close dialog" }).click(); + + // Typing a command that takes arguments (/spoiler) and selecting with enter works + await page.getByRole("textbox").pressSequentially("/spoil"); + await expect(page.getByTestId("autocomplete-wrapper").getByText("/spoiler")).toBeVisible(); + await page.getByRole("textbox").press("Enter"); + // Check it has closed the autocomplete and put the text into the composer + await expect(page.getByTestId("autocomplete-wrapper")).not.toBeVisible(); + await expect(page.getByRole("textbox").getByText("/spoiler")).toBeVisible(); + // Enter some more text, then send the message + await page.getByRole("textbox").pressSequentially("this is the spoiler text "); + await page.getByRole("button", { name: "Send message" }).click(); + // Check that a spoiler item has appeared in the timeline and locator the spoiler command text + await expect(page.locator("button.mx_EventTile_spoiler")).toBeVisible(); + await expect(page.getByText("this is the spoiler text")).toBeVisible(); + }); + }); + }); + + test.describe("Mentions", () => { + // TODO add tests for rich text mode + + test.describe("Plain text mode", () => { + test.use({ + botCreateOpts: { + displayName: "Bob", + }, + }); + + // https://github.com/vector-im/element-web/issues/26037 + test.skip("autocomplete behaviour tests", async ({ page, app, bot: bob }) => { + // Set up a private room so we have another user to mention + await app.client.createRoom({ + is_direct: true, + invite: [bob.credentials.userId], + }); + await app.viewRoomByName("Bob"); + + // Select plain text mode after composer is ready + await expect(page.locator("div[contenteditable=true]")).toBeVisible(); + await page.getByRole("button", { name: "Hide formatting" }).click(); + + // Typing a single @ does not display the autocomplete menu and contents + await page.getByRole("textbox").press("@"); + await expect(page.getByTestId("autocomplete-wrapper")).toBeEmpty(); + + // Entering the first letter of the other user's name opens the autocomplete... + await page.getByRole("textbox").pressSequentially(bob.credentials.displayName.slice(0, 1)); + // ...with the other user name visible, and clicking that username... + await page.getByTestId("autocomplete-wrapper").getByText(bob.credentials.displayName).click(); + // ...inserts the username into the composer + const pill = page.getByRole("textbox").getByText(bob.credentials.displayName, { exact: false }); + await expect(pill).toHaveAttribute("contenteditable", "false"); + await expect(pill).toHaveAttribute("data-mention-type", "user"); + + // Send the message to clear the composer + await page.getByRole("button", { name: "Send message" }).click(); + + // Typing an @, then other user's name, then trailing space closes the autocomplete + await page.getByRole("textbox").pressSequentially(`@${bob.credentials.displayName} `); + await expect(page.getByTestId("autocomplete-wrapper")).toBeEmpty(); + + // Send the message to clear the composer + await page.getByRole("button", { name: "Send message" }).click(); + + // Moving the cursor back to an "incomplete" mention opens the autocomplete + await page + .getByRole("textbox") + .pressSequentially(`initial text @${bob.credentials.displayName.slice(0, 1)} abc`); + await expect(page.getByTestId("autocomplete-wrapper")).toBeEmpty(); + // Move the cursor left by 4 to put it to: `@B| abc`, check autocomplete displays + await page.getByRole("textbox").press("LeftArrow"); + await page.getByRole("textbox").press("LeftArrow"); + await page.getByRole("textbox").press("LeftArrow"); + await page.getByRole("textbox").press("LeftArrow"); + await expect(page.getByTestId("autocomplete-wrapper")).not.toBeEmpty(); + + // Selecting the autocomplete option using Enter inserts it into the composer + await page.getByRole("textbox").press("Enter"); + const pill2 = page.getByRole("textbox").getByText(bob.credentials.displayName, { exact: false }); + await expect(pill2).toHaveAttribute("contenteditable", "false"); + await expect(pill2).toHaveAttribute("data-mention-type", "user"); + }); + }); + }); + + test("sends a message when you click send or press Enter", async ({ page }) => { + // Type a message + await page.locator("div[contenteditable=true]").pressSequentially("my message 0"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 0" })).not.toBeVisible(); + + // Click send + await page.getByRole("button", { name: "Send message" }).click(); + // It has been sent + await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 0")).toBeVisible(); + + // Type another + await page.locator("div[contenteditable=true]").pressSequentially("my message 1"); + // Send message + page.locator("div[contenteditable=true]").press("Enter"); + // It was sent + await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible(); + }); + + test("sends only one message when you press Enter multiple times", async ({ page }) => { + // Type a message + await page.locator("div[contenteditable=true]").pressSequentially("my message 0"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 0" })).not.toBeVisible(); + + // Click send + await page.locator("div[contenteditable=true]").press("Enter"); + await page.locator("div[contenteditable=true]").press("Enter"); + await page.locator("div[contenteditable=true]").press("Enter"); + // It has been sent + await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 0")).toBeVisible(); + await expect(page.locator(".mx_EventTile_last .mx_EventTile_body")).toHaveCount(1); + }); + + test("can write formatted text", async ({ page }) => { + await page.locator("div[contenteditable=true]").pressSequentially("my "); + await page.locator("div[contenteditable=true]").press(`${CtrlOrMeta}+KeyB`); + await page.locator("div[contenteditable=true]").pressSequentially("bold"); + await page.locator("div[contenteditable=true]").press(`${CtrlOrMeta}+KeyB`); + await page.locator("div[contenteditable=true]").pressSequentially(" message"); + await page.getByRole("button", { name: "Send message" }).click(); + await expect(page.locator(".mx_EventTile_body strong").getByText("bold")).toBeVisible(); + }); + + test.describe("when Control+Enter is required to send", () => { + test.beforeEach(async ({ app }) => { + await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); + }); + + test("only sends when you press Control+Enter", async ({ page }) => { + // Type a message and press Enter + await page.locator("div[contenteditable=true]").pressSequentially("my message 3"); + await page.locator("div[contenteditable=true]").press("Enter"); + // It has not been sent yet + await expect(page.locator(".mx_EventTile_body", { hasText: "my message 3" })).not.toBeVisible(); + + // Press Control+Enter + await page.locator("div[contenteditable=true]").press("Control+Enter"); + // It was sent + await expect( + page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 3"), + ).toBeVisible(); + }); + }); + + test.describe("links", () => { + test("create link with a forward selection", async ({ page }) => { + // Type a message + await page.locator("div[contenteditable=true]").pressSequentially("my message 0"); + await page.locator("div[contenteditable=true]").press(`${CtrlOrMeta}+A`); + + // Open link modal + await page.getByRole("button", { name: "Link" }).click(); + // Fill the link field + await page.getByRole("textbox", { name: "Link" }).pressSequentially("https://matrix.org/"); + // Click on save + await page.getByRole("button", { name: "Save" }).click(); + // Send the message + await page.getByRole("button", { name: "Send message" }).click(); + + // It was sent + await expect(page.locator(".mx_EventTile_body a").getByText("my message 0")).toBeVisible(); + await expect(page.locator(".mx_EventTile_body a")).toHaveAttribute( + "href", + new RegExp("https://matrix.org/"), + ); + }); + }); + }); +});