Reference files in Playwright more reliably (#32935)

Without assuming the cwd
This commit is contained in:
Michael Telatynski 2026-03-26 12:30:54 +01:00 committed by GitHub
parent 5a074e637a
commit cd429874db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 93 additions and 57 deletions

View File

@ -12,6 +12,7 @@ import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { getSampleFilePath } from "../../sample-files";
// Find and click "Reply" button
const clickButtonReply = async (tile: Locator) => {
@ -28,9 +29,11 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
displayName: "Hanako",
});
const uploadFile = async (page: Page, file: string) => {
const uploadFile = async (page: Page, sampleFile: string) => {
// Upload a file from the message composer
await page.locator(".mx_MessageComposer_actions input[type='file']").setInputFiles(file);
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();
@ -41,12 +44,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
"Your message was sent",
);
// wait for the tile to finish loading
await expect(
page
.getByTestId("audio-player-name")
.last()
.filter({ hasText: file.split("/").at(-1) }),
).toBeVisible();
await expect(page.getByTestId("audio-player-name").last().filter({ hasText: sampleFile })).toBeVisible();
};
const scrollToBottomOfTimeline = async (page: Page) => {
@ -158,7 +156,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, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await uploadFile(page, "1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
});
@ -166,7 +164,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, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await uploadFile(page, "1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
},
@ -183,7 +181,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await app.closeDialog();
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await uploadFile(page, "1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
});
@ -192,13 +190,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, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await uploadFile(page, "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, "playwright/sample-files/1sec.ogg");
await uploadFile(page, "1sec.ogg");
// Assert that the audio player is rendered
const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" });
@ -220,7 +218,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
});
test("should support downloading an audio file", async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec.ogg");
await uploadFile(page, "1sec.ogg");
const downloadPromise = page.waitForEvent("download");
@ -238,7 +236,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, "playwright/sample-files/1sec.ogg");
await uploadFile(page, "1sec.ogg");
// Assert the audio player is rendered
await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible();
@ -248,7 +246,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await clickButtonReply(tile);
// Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/1sec.ogg");
await uploadFile(page, "1sec.ogg");
// Assert that the audio player is rendered
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
@ -273,7 +271,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const tile = page.locator(".mx_EventTile_last");
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
await uploadFile(page, "upload-first.ogg");
// Assert that the audio player is rendered
await expect(
@ -283,7 +281,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await clickButtonReply(tile);
// Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
await uploadFile(page, "upload-second.ogg");
// Assert that the audio player is rendered
await expect(
@ -293,7 +291,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, "playwright/sample-files/upload-third.ogg");
await uploadFile(page, "upload-third.ogg");
// Assert that the audio player is rendered
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
@ -325,7 +323,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, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await uploadFile(page, "1sec-long-name-audio-file.ogg");
// On the main timeline
const messageList = page.locator(".mx_RoomView_MessageList");

View File

@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { getSampleFilePath } from "../../sample-files";
test.describe("Image Upload", () => {
test.use({
@ -28,7 +29,7 @@ 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("playwright/sample-files/riot.png");
.setInputFiles(getSampleFilePath("riot.png"));
await expect(page.getByRole("button", { name: "Upload" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Close dialog" })).toBeEnabled();

View File

@ -9,11 +9,11 @@ Please see LICENSE files in the repository root for full details.
/* See readme.md for tips on writing these tests. */
import { type Locator, type Page } from "@playwright/test";
import { readFileSync } from "node:fs";
import { test, expect } from "../../element-web-test";
import { readSampleFileSync } from "../../sample-files";
const MEDIA_FILE = readFileSync("playwright/sample-files/riot.png");
const MEDIA_FILE = readSampleFileSync("riot.png", null);
async function waitForMessageSentStatus(msgTile: Locator): Promise<void> {
await expect(msgTile.getByRole("status")).toHaveAccessibleName("Your message was sent");

View File

@ -6,9 +6,9 @@ Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import fs from "node:fs";
import { test, expect } from "../../element-web-test";
import { getSampleFilePath, readSampleFileSync } from "../../sample-files";
const screenshotOptions = (page: Page) => ({
// Hide the jump to bottom button in the timeline to avoid flakiness
@ -26,7 +26,7 @@ const screenshotOptions = (page: Page) => ({
`,
});
const IMAGE_FILE = fs.readFileSync("playwright/sample-files/element.png");
const IMAGE_FILE = readSampleFileSync("element.png", null);
test.describe("Custom Component API", () => {
test.use({
@ -36,7 +36,7 @@ test.describe("Custom Component API", () => {
},
page: async ({ page }, use) => {
await page.route("/modules/custom-component-module.js", async (route) => {
await route.fulfill({ path: "playwright/sample-files/custom-component-module.js" });
await route.fulfill({ path: getSampleFilePath("custom-component-module.js") });
});
await use(page);
},

View File

@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { getSampleFilePath } from "../../sample-files";
test.describe("Module loading", () => {
test.use({
@ -20,7 +21,7 @@ test.describe("Module loading", () => {
},
page: async ({ page }, use) => {
await page.route("/modules/example-module.js", async (route) => {
await route.fulfill({ path: "playwright/sample-files/example-module.js" });
await route.fulfill({ path: getSampleFilePath("example-module.js") });
});
await use(page);
},

View File

@ -11,13 +11,14 @@ 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";
const ROOM_NAME = "Test room";
const NAME = "Alice";
async function uploadFile(page: Page, file: string) {
async function uploadFile(page: Page, sampleFile: string) {
// Upload a file from the message composer
await page.locator(".mx_MessageComposer_actions input[type='file']").setInputFiles(file);
await page.locator(".mx_MessageComposer_actions input[type='file']").setInputFiles(getSampleFilePath(sampleFile));
await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();
@ -53,9 +54,9 @@ test.describe("FilePanel", () => {
test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => {
// Upload multiple files
await uploadFile(page, "playwright/sample-files/riot.png"); // Image
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio
await uploadFile(page, "playwright/sample-files/matrix-org-client-versions.json"); // JSON
await uploadFile(page, "riot.png"); // Image
await uploadFile(page, "1sec.ogg"); // Audio
await uploadFile(page, "matrix-org-client-versions.json"); // JSON
const roomViewBody = page.locator(".mx_RoomView_body");
// Assert that all of the file were uploaded and rendered
@ -137,7 +138,7 @@ test.describe("FilePanel", () => {
test("should render the audio player and play the audio file on the panel", async ({ page }) => {
// Upload an image file
await uploadFile(page, "playwright/sample-files/1sec.ogg");
await uploadFile(page, "1sec.ogg");
const audioBody = page.getByTestId("right-panel").getByRole("region", { name: "Audio player" });
@ -170,7 +171,7 @@ test.describe("FilePanel", () => {
const size = "1.12 KB"; // actual file size in kibibytes (1024 bytes)
// Upload a file
await uploadFile(page, "playwright/sample-files/matrix-org-client-versions.json");
await uploadFile(page, "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)
@ -184,7 +185,7 @@ test.describe("FilePanel", () => {
test("should download an image via the link on the panel", async ({ page, context }) => {
// Upload an image file
await uploadFile(page, "playwright/sample-files/riot.png");
await uploadFile(page, "riot.png");
// Detect the image file on the panel
const imageBody = page.locator(

View File

@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { getSampleFilePath } from "../../sample-files";
const USER_NAME = "Bob";
const USER_NAME_NEW = "Alice";
@ -85,7 +86,7 @@ test.describe("Account user settings tab", () => {
test("should support adding and removing a profile picture", async ({ uut, page }) => {
const profileSettings = uut.locator(".mx_UserProfileSettings");
// Upload a picture
await profileSettings.getByAltText("Upload").setInputFiles("playwright/sample-files/riot.png");
await profileSettings.getByAltText("Upload").setInputFiles(getSampleFilePath("riot.png"));
// Image should be visible
await expect(profileSettings.locator(".mx_AvatarSetting_avatar img")).toBeVisible();

View File

@ -12,6 +12,7 @@ import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { UIFeature } from "../../../src/settings/UIFeature";
import { getSampleFilePath } from "../../sample-files";
async function openSpaceCreateMenu(page: Page): Promise<Locator> {
await page.getByRole("button", { name: "Create a space" }).click();
@ -74,7 +75,7 @@ test.describe("Spaces", () => {
await contextMenu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png");
.setInputFiles(getSampleFilePath("riot.png"));
await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot");
await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot");
await contextMenu
@ -108,7 +109,7 @@ test.describe("Spaces", () => {
await menu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png");
.setInputFiles(getSampleFilePath("riot.png"));
await menu.getByRole("textbox", { name: "Name" }).fill("This is not a Riot");
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
await menu.getByRole("textbox", { name: "Description" }).fill("This is a private space of mourning Riot.im...");
@ -154,7 +155,7 @@ test.describe("Spaces", () => {
await menu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png");
.setInputFiles(getSampleFilePath("riot.png"));
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
await menu.getByRole("textbox", { name: "Description" }).fill("This is a personal space to mourn Riot.im...");
await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot");
@ -188,7 +189,7 @@ test.describe("Spaces", () => {
await menu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png");
.setInputFiles(getSampleFilePath("riot.png"));
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
await menu
.getByRole("textbox", { name: "Description" })
@ -406,7 +407,7 @@ test.describe("Spaces", () => {
const menu = await openSpaceCreateMenu(page);
await menu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.setInputFiles("playwright/sample-files/riot.png");
.setInputFiles(getSampleFilePath("riot.png"));
await menu.getByRole("textbox", { name: "Name" }).fill("This is a private space");
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
await menu

View File

@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as fs from "node:fs";
import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types";
import { test, expect } from "../../element-web-test";
import { readSampleFileSync } from "../../sample-files";
const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png");
const MEDIA_FILE = readSampleFileSync("riot.png", null);
test.describe("Media preview settings", () => {
test.use({

View File

@ -6,8 +6,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as fs from "node:fs";
import type { Locator, Page } from "@playwright/test";
import type { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
@ -16,6 +14,7 @@ import { Layout } from "../../../src/settings/enums/Layout";
import { type Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
import { getSampleFilePath, readSampleFileSync } from "../../sample-files";
// The avatar size used in the timeline
const AVATAR_SIZE = 30;
@ -23,12 +22,12 @@ const AVATAR_SIZE = 30;
const AVATAR_RESIZE_METHOD = "crop";
const ROOM_NAME = "Test room";
const OLD_AVATAR = fs.readFileSync("playwright/sample-files/riot.png");
const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
const OLD_AVATAR = readSampleFileSync("riot.png", null);
const NEW_AVATAR = readSampleFileSync("element.png", null);
const OLD_NAME = "Alan";
const NEW_NAME = "Alan (away)";
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
const VIDEO_FILE = readSampleFileSync("5secvid.webm", null);
const getEventTilesWithBodies = (page: Page): Locator => {
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
@ -765,7 +764,7 @@ test.describe("Timeline", () => {
// Upload a file from the message composer
await page
.locator(".mx_MessageComposer_actions input[type='file']")
.setInputFiles("playwright/sample-files/matrix-org-client-versions.json");
.setInputFiles(getSampleFilePath("matrix-org-client-versions.json"));
// Click "Upload" button
await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();

View File

@ -15,13 +15,14 @@ import { test, expect } from "../../element-web-test";
import type { Credentials } from "../../plugins/homeserver";
import { Bot } from "../../pages/bot";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { readSampleFile } from "../../sample-files";
// Load a copy of our fake Element Call app, and the latest widget API.
// The fake call app does *just* enough to convince Element Web that a call is ongoing
// and functions like PiP work. It does not actually do anything though, to limit the
// surface we test.
const widgetApi = readFile(fileURLToPath(import.meta.resolve("matrix-widget-api/dist/api.min.js")), "utf-8");
const fakeCallClient = readFile("playwright/sample-files/fake-element-call.html", "utf-8");
const fakeCallClient = readSampleFile("fake-element-call.html");
function assertCommonCallParameters(
url: URLSearchParams,
@ -615,7 +616,7 @@ test.describe("Element Call", () => {
},
});
const fakeCallClientSend = readFile("playwright/sample-files/fake-element-call-with-send.html", "utf-8");
const fakeCallClientSend = readSampleFile("fake-element-call-with-send.html");
let charlie: Bot;
test.use({

View File

@ -6,20 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import * as fs from "node:fs";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Credentials } from "../../plugins/homeserver";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
import { readSampleFileSync } from "../../sample-files";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
const STICKER_NAME = "Test Sticker";
const ROOM_NAME_1 = "Sticker Test";
const ROOM_NAME_2 = "Sticker Test Two";
const STICKER_IMAGE = fs.readFileSync("playwright/sample-files/riot.png");
const STICKER_IMAGE = readSampleFileSync("riot.png", null);
function getStickerMessage(contentUri: string, mimetype: string): string {
return JSON.stringify({

View File

@ -7,16 +7,18 @@ Please see LICENSE files in the repository root for full details.
*/
import { type SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { type Fixtures } from "../../../element-web-test.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
export const consentHomeserver: Fixtures = {
_homeserver: [
async ({ _homeserver: container, mailpit }, use) => {
(container as SynapseContainer)
.withCopyDirectoriesToContainer([
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
])
.withCopyDirectoriesToContainer([{ source: join(__dirname, "res"), target: "/data/res" }])
.withSmtpServer(mailpit)
.withConfig({
user_consent: {

View File

@ -0,0 +1,32 @@
/*
Copyright 2026 Element Creations Ltd.
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 { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { readFile } from "node:fs/promises";
import { readFileSync } from "node:fs";
const __dirname = dirname(fileURLToPath(import.meta.url));
export function getSampleFilePath(file: string): string {
return join(__dirname, file);
}
export function readSampleFile(file: string, encoding: null): Promise<NonSharedBuffer>;
export function readSampleFile(file: string, encoding?: BufferEncoding): Promise<string>;
export function readSampleFile(
file: string,
encoding: BufferEncoding | null = "utf-8",
): Promise<NonSharedBuffer | string> {
return readFile(getSampleFilePath(file), encoding);
}
export function readSampleFileSync(file: string, encoding: null): NonSharedBuffer;
export function readSampleFileSync(file: string, encoding?: BufferEncoding): string;
export function readSampleFileSync(file: string, encoding: BufferEncoding | null = "utf-8"): NonSharedBuffer | string {
return readFileSync(getSampleFilePath(file), encoding);
}