diff --git a/apps/web/playwright/e2e/composer/CIDER.spec.ts b/apps/web/playwright/e2e/composer/CIDER.spec.ts index d164a689e1..89f4cad276 100644 --- a/apps/web/playwright/e2e/composer/CIDER.spec.ts +++ b/apps/web/playwright/e2e/composer/CIDER.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { getSampleFilePath } from "../../sample-files"; const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control"; @@ -198,5 +199,16 @@ test.describe("Composer", () => { // Take a screenshot of the autocomplete await expect(autocomplete).toMatchScreenshot("emoji-autocomplete.png"); }); + + test("can paste a file", async ({ page, bot, app }) => { + // Set up a private room so we have another user to mention + await app.client.createRoom({ + is_direct: true, + invite: [bot.credentials.userId], + }); + await app.viewRoomByName("Bob"); + await app.composerDragAndPasteFile("room", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_MImageBody")).toBeVisible(); + }); }); }); diff --git a/apps/web/playwright/e2e/composer/RTE.spec.ts b/apps/web/playwright/e2e/composer/RTE.spec.ts index e88dd827fc..2c5f8071ec 100644 --- a/apps/web/playwright/e2e/composer/RTE.spec.ts +++ b/apps/web/playwright/e2e/composer/RTE.spec.ts @@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { getSampleFilePath } from "../../sample-files"; const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control"; @@ -195,6 +196,26 @@ test.describe("Composer", () => { await expect(page.locator(".mx_EventTile_body strong").getByText("bold")).toBeVisible(); }); + test("can paste a file", async ({ page, bot, app }) => { + await app.composerDragAndPasteFile("room", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_MImageBody")).toBeVisible(); + }); + + test("can paste a file in a thread", async ({ page, app }) => { + // Send a message + const composer = page.locator("div[contenteditable=true]"); + await composer.pressSequentially("my first message"); + await page.getByRole("button", { name: "Send message" }).click(); + + // Click reply + const tile = page.locator(".mx_EventTile_last"); + await tile.hover(); + await tile.getByRole("button", { name: "Reply in thread" }).click(); + + await app.composerDragAndPasteFile("thread", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_MImageBody")).toBeVisible(); + }); + test.describe("when Control+Enter is required to send", () => { test.beforeEach(async ({ app }) => { await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true); diff --git a/apps/web/playwright/e2e/threads/threads.spec.ts b/apps/web/playwright/e2e/threads/threads.spec.ts index a454031933..c74ca570ea 100644 --- a/apps/web/playwright/e2e/threads/threads.spec.ts +++ b/apps/web/playwright/e2e/threads/threads.spec.ts @@ -405,6 +405,28 @@ test.describe("Threads", () => { await app.composerDragAndUploadFiles("thread", getSampleFilePath("riot.png"), "image/png"); await expect(page.locator(".mx_ThreadView .mx_EventTile_image")).toHaveCount(1); }); + test("can send files via paste", async ({ page, app, user }) => { + // Increase right-panel size, so that files fit + await page.evaluate(() => { + window.localStorage.setItem("mx_rhs_size", "600"); + }); + + const roomId = await app.client.createRoom({}); + await page.goto("/#/room/" + roomId); + + // Send message + const locator = page.locator(".mx_RoomView_body"); + await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hello Mr. Bot"); + await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); + // Create thread + const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" }); + await locator2.hover(); + await locator2.getByRole("button", { name: "Reply in thread" }).click(); + + await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1); + await app.composerDragAndPasteFile("thread", getSampleFilePath("riot.png"), "image/png"); + await expect(page.locator(".mx_ThreadView .mx_EventTile_image")).toHaveCount(1); + }); }); test( diff --git a/apps/web/playwright/pages/ElementAppPage.ts b/apps/web/playwright/pages/ElementAppPage.ts index ac9212fd10..41abc8f980 100644 --- a/apps/web/playwright/pages/ElementAppPage.ts +++ b/apps/web/playwright/pages/ElementAppPage.ts @@ -218,6 +218,40 @@ export class ElementAppPage { await this.page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click(); } + /** + * Paste a "file" into the specified locator and automatically uploads it. + * @param location Should the drop target the main room or the thread. + * @param path The path to the sample file so it can be read. + * @param type The mimetype of the file. + */ + public async composerDragAndPasteFile(location: "room" | "thread", path: string, type: string): Promise { + // Based on https://github.com/microsoft/playwright/issues/10667#issuecomment-2742123424 + // This read a file, encodes it into base64 and then sends it along to the page to be treated + // as a DataTransfer (the mechanism for drag and dropped files). + const buffer = await readFile(path); + const name = basename(path); + const composer = this.getComposerField(location === "thread"); + + await composer.evaluate( + async (element, [buffer, name, type]) => { + const clipboardData = new DataTransfer(); + const file = new File([Uint8Array.fromBase64(buffer)], name, { + type, + }); + clipboardData.items.add(file); + element.dispatchEvent( + new ClipboardEvent("paste", { + clipboardData, + bubbles: true, + cancelable: true, + }), + ); + }, + [buffer.toString("base64"), name, type], + ); + await this.page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click(); + } + /** * Returns the space panel space button based on a name. The space * must be visible in the space panel