From ab97aa21dee5cfe5f74b5b65e9fb0702345d296f Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 1 May 2026 11:03:42 +0100 Subject: [PATCH] Add tests for pasting --- .../web/playwright/e2e/composer/CIDER.spec.ts | 15 +++++ apps/web/playwright/e2e/composer/RTE.spec.ts | 7 ++ apps/web/playwright/pages/ElementAppPage.ts | 64 +++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/apps/web/playwright/e2e/composer/CIDER.spec.ts b/apps/web/playwright/e2e/composer/CIDER.spec.ts index d164a689e1..741c85b759 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,19 @@ 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"); + + const composer = page.locator(".mx_BasicMessageComposer_input"); + await composer.focus(); + await app.composerDragAndPasteFile(composer, 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..4d850a577b 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,12 @@ test.describe("Composer", () => { await expect(page.locator(".mx_EventTile_body strong").getByText("bold")).toBeVisible(); }); + test("can paste a file", async ({ page, bot, app }) => { + const composer = page.locator("div[contenteditable=true]"); + await app.composerDragAndPasteFile(composer, 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/pages/ElementAppPage.ts b/apps/web/playwright/pages/ElementAppPage.ts index ac9212fd10..0b8be3f27e 100644 --- a/apps/web/playwright/pages/ElementAppPage.ts +++ b/apps/web/playwright/pages/ElementAppPage.ts @@ -218,6 +218,70 @@ export class ElementAppPage { await this.page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click(); } + /** + * Drags a "file" into the specified composer 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 composerDragAndDropFiles(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 dataTransfer = await this.page.evaluateHandle( + async ([buffer, name, type]) => { + const dt = new DataTransfer(); + const file = new File([Uint8Array.fromBase64(buffer)], name, { + type, + }); + dt.items.add(file); + return dt; + }, + [buffer.toString("base64"), name, type], + ); + await this.page.dispatchEvent(location === "room" ? ".mx_RoomView_body" : ".mx_ThreadPanel", "drop", { + dataTransfer, + }); + 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(locator: Locator, 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); + + await locator.evaluate( + async (element, [buffer, name, type]) => { + const clipboardData = new DataTransfer(); + const file = new File([Uint8Array.fromBase64(buffer)], name, { + type, + }); + clipboardData.items.add(file); + console.log("Dispatching event..."); + 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