mirror of
https://github.com/vector-im/element-web.git
synced 2026-05-04 11:51:36 +02:00
Playwright tests for file uploads (#33319)
* Refactor tests to use helper method for composer uploads. * Add drag and drop tests * lint * Add commentary * fixup test * More precise selector
This commit is contained in:
parent
b0ee6f5323
commit
af20018ea2
@ -11,7 +11,7 @@ import type { Locator, Page } from "@playwright/test";
|
||||
import { test, expect, type ExtendedToMatchScreenshotOptions } from "../../element-web-test";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { getSampleFilePath } from "../../sample-files";
|
||||
|
||||
// Find and click "Reply" button
|
||||
@ -29,22 +29,17 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
const uploadFile = async (page: Page, sampleFile: string) => {
|
||||
const uploadFile = async (app: ElementAppPage, sampleFile: string) => {
|
||||
// Upload a file from the message composer
|
||||
await page
|
||||
.locator(".mx_MessageComposer_actions input[type='file']")
|
||||
.setInputFiles(getSampleFilePath(sampleFile));
|
||||
|
||||
// Find and click primary "Upload" button
|
||||
await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();
|
||||
await app.composerUploadFiles("room", getSampleFilePath(sampleFile));
|
||||
|
||||
// Wait until the file is sent
|
||||
await expect(page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible();
|
||||
await expect(page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName(
|
||||
await expect(app.page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible();
|
||||
await expect(app.page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName(
|
||||
"Your message was sent",
|
||||
);
|
||||
// wait for the tile to finish loading
|
||||
await expect(page.getByTestId("audio-player-name").last().filter({ hasText: sampleFile })).toBeVisible();
|
||||
await expect(app.page.getByTestId("audio-player-name").last().filter({ hasText: sampleFile })).toBeVisible();
|
||||
};
|
||||
|
||||
const scrollToBottomOfTimeline = async (page: Page) => {
|
||||
@ -157,7 +152,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
});
|
||||
|
||||
test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await uploadFile(page, "1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(app, "1sec-long-name-audio-file.ogg");
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
|
||||
});
|
||||
|
||||
@ -165,7 +160,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
"should be correctly rendered - light theme with monospace font",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app }) => {
|
||||
await uploadFile(page, "1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(app, "1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
|
||||
},
|
||||
@ -182,7 +177,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
await app.closeDialog();
|
||||
|
||||
await uploadFile(page, "1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(app, "1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
|
||||
});
|
||||
@ -191,13 +186,13 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// Enable dark theme
|
||||
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
|
||||
|
||||
await uploadFile(page, "1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(app, "1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (dark theme)");
|
||||
});
|
||||
|
||||
test("should play an audio file", async ({ page, app }) => {
|
||||
await uploadFile(page, "1sec.ogg");
|
||||
await uploadFile(app, "1sec.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" });
|
||||
@ -219,7 +214,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
});
|
||||
|
||||
test("should support downloading an audio file", async ({ page, app }) => {
|
||||
await uploadFile(page, "1sec.ogg");
|
||||
await uploadFile(app, "1sec.ogg");
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
|
||||
@ -237,7 +232,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
"should support replying to audio file with another audio file",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app }) => {
|
||||
await uploadFile(page, "1sec.ogg");
|
||||
await uploadFile(app, "1sec.ogg");
|
||||
|
||||
// Assert the audio player is rendered
|
||||
await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
@ -247,7 +242,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "1sec.ogg");
|
||||
await uploadFile(app, "1sec.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
@ -272,7 +267,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
const tile = page.locator(".mx_EventTile_last");
|
||||
|
||||
await uploadFile(page, "upload-first.ogg");
|
||||
await uploadFile(app, "upload-first.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(
|
||||
@ -282,7 +277,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "upload-second.ogg");
|
||||
await uploadFile(app, "upload-second.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(
|
||||
@ -292,7 +287,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with yet another audio file to create a reply chain
|
||||
await uploadFile(page, "upload-third.ogg");
|
||||
await uploadFile(app, "upload-third.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
@ -324,7 +319,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
);
|
||||
|
||||
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
|
||||
await uploadFile(page, "1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(app, "1sec-long-name-audio-file.ogg");
|
||||
|
||||
// On the main timeline
|
||||
const messageList = page.locator(".mx_RoomView_MessageList");
|
||||
|
||||
@ -27,12 +27,17 @@ test.describe("Image Upload", () => {
|
||||
});
|
||||
|
||||
test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await page
|
||||
.locator(".mx_MessageComposer_actions input[type='file']")
|
||||
.setInputFiles(getSampleFilePath("riot.png"));
|
||||
await app.setComposerInputFiles("room", getSampleFilePath("riot.png"));
|
||||
|
||||
await expect(page.getByRole("button", { name: "Upload" })).toBeEnabled();
|
||||
await expect(page.getByRole("button", { name: "Close dialog" })).toBeEnabled();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("image-upload-preview.png");
|
||||
});
|
||||
|
||||
test("should allow upload via drag and drop", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.composerDragAndUploadFiles("room", getSampleFilePath("riot.png"), "image/png");
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MImageBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,26 +5,21 @@ Copyright 2023 Suguru Hirahara
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { getSampleFilePath } from "../../sample-files";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
|
||||
async function uploadFile(page: Page, sampleFile: string) {
|
||||
async function uploadFile(app: ElementAppPage, sampleFile: string) {
|
||||
// Upload a file from the message composer
|
||||
await page.locator(".mx_MessageComposer_actions input[type='file']").setInputFiles(getSampleFilePath(sampleFile));
|
||||
|
||||
await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();
|
||||
|
||||
await app.composerUploadFiles("room", getSampleFilePath(sampleFile));
|
||||
// Wait until the file is sent
|
||||
await expect(page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible();
|
||||
await expect(page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName(
|
||||
await expect(app.page.locator(".mx_RoomView_statusArea_expanded")).not.toBeVisible();
|
||||
await expect(app.page.locator(".mx_EventTile.mx_EventTile_last").getByRole("status")).toHaveAccessibleName(
|
||||
"Your message was sent",
|
||||
);
|
||||
}
|
||||
@ -52,11 +47,11 @@ test.describe("FilePanel", () => {
|
||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
||||
});
|
||||
|
||||
test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => {
|
||||
test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
// Upload multiple files
|
||||
await uploadFile(page, "riot.png"); // Image
|
||||
await uploadFile(page, "1sec.ogg"); // Audio
|
||||
await uploadFile(page, "matrix-org-client-versions.json"); // JSON
|
||||
await uploadFile(app, "riot.png"); // Image
|
||||
await uploadFile(app, "1sec.ogg"); // Audio
|
||||
await uploadFile(app, "matrix-org-client-versions.json"); // JSON
|
||||
|
||||
const roomViewBody = page.locator(".mx_RoomView_body");
|
||||
// Assert that all of the file were uploaded and rendered
|
||||
@ -136,9 +131,9 @@ test.describe("FilePanel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should render the audio player and play the audio file on the panel", async ({ page }) => {
|
||||
test("should render the audio player and play the audio file on the panel", async ({ page, app }) => {
|
||||
// Upload an image file
|
||||
await uploadFile(page, "1sec.ogg");
|
||||
await uploadFile(app, "1sec.ogg");
|
||||
|
||||
const audioBody = page.getByTestId("right-panel").getByRole("region", { name: "Audio player" });
|
||||
|
||||
@ -167,11 +162,11 @@ test.describe("FilePanel", () => {
|
||||
await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should render file size in kibibytes on a file tile", async ({ page }) => {
|
||||
test("should render file size in kibibytes on a file tile", async ({ page, app }) => {
|
||||
const size = "1.12 KB"; // actual file size in kibibytes (1024 bytes)
|
||||
|
||||
// Upload a file
|
||||
await uploadFile(page, "matrix-org-client-versions.json");
|
||||
await uploadFile(app, "matrix-org-client-versions.json");
|
||||
|
||||
const tile = page.locator(".mx_FilePanel .mx_EventTile");
|
||||
// Assert that the file size is displayed in kibibytes, not kilobytes (1000 bytes)
|
||||
@ -183,9 +178,9 @@ test.describe("FilePanel", () => {
|
||||
test.describe("download", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite sending Content-Disposition inline");
|
||||
|
||||
test("should download an image via the link on the panel", async ({ page, context }) => {
|
||||
test("should download an image via the link on the panel", async ({ page, app, context }) => {
|
||||
// Upload an image file
|
||||
await uploadFile(page, "riot.png");
|
||||
await uploadFile(app, "riot.png");
|
||||
|
||||
// Detect the image file on the panel
|
||||
const imageBody = page.locator(
|
||||
|
||||
@ -9,6 +9,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { getSampleFilePath } from "../../sample-files";
|
||||
|
||||
test.describe("Threads", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3489");
|
||||
@ -360,6 +361,50 @@ test.describe("Threads", () => {
|
||||
await app.getComposer(true).getByRole("button", { name: "Send voice message" }).click();
|
||||
await expect(page.locator(".mx_ThreadView .mx_MVoiceMessageBody")).toHaveCount(1);
|
||||
});
|
||||
test("can send files", 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.composerUploadFiles("thread", getSampleFilePath("riot.png"));
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile_image")).toHaveCount(1);
|
||||
});
|
||||
test("can send files via drag&drop", 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.composerDragAndUploadFiles("thread", getSampleFilePath("riot.png"), "image/png");
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile_image")).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@ -7,6 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Locator, type Page, expect } from "@playwright/test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import { Settings } from "./settings";
|
||||
import { Client } from "./client";
|
||||
@ -156,6 +158,66 @@ export class ElementAppPage {
|
||||
return this.page.getByRole("menu");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the files on the composers file input, causing it to open the file
|
||||
* upload dialog.
|
||||
* @param location Should the main room input or the thread view input be used.
|
||||
*/
|
||||
public setComposerInputFiles(
|
||||
location: "room" | "thread",
|
||||
...params: Parameters<Locator["setInputFiles"]>
|
||||
): ReturnType<Locator["setInputFiles"]> {
|
||||
const input = this.page
|
||||
.locator(location === "room" ? ".mx_RoomView_body" : ".mx_RightPanel")
|
||||
.getByRole("region", { name: "Message composer" })
|
||||
.locator("input[type='file']");
|
||||
return input.setInputFiles(...params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the files on the composers file input, causing it to open the file
|
||||
* upload dialog, and then automaticlly submits the dialog that pops up which
|
||||
* causes the file to be uploaded.
|
||||
* @param location Should the main room input or the thread view input be used.
|
||||
*/
|
||||
public async composerUploadFiles(
|
||||
location: "room" | "thread",
|
||||
...params: Parameters<Locator["setInputFiles"]>
|
||||
): Promise<void> {
|
||||
await this.setComposerInputFiles(location, ...params);
|
||||
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 composerDragAndUploadFiles(location: "room" | "thread", path: string, type: string): Promise<void> {
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the space panel space button based on a name. The space
|
||||
* must be visible in the space panel
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user