mirror of https://github.com/vector-im/riot-web
				
				
				
			
		
			
				
	
	
		
			324 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			324 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
| /*
 | |
| 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/"),
 | |
|                 );
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| });
 |