diff --git a/cypress/e2e/audio-player/audio-player.spec.ts b/cypress/e2e/audio-player/audio-player.spec.ts
deleted file mode 100644
index 30470716c9..0000000000
--- a/cypress/e2e/audio-player/audio-player.spec.ts
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
-Copyright 2023 Suguru Hirahara
-
-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 { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { SettingLevel } from "../../../src/settings/SettingLevel";
-import { Layout } from "../../../src/settings/enums/Layout";
-
-describe("Audio player", () => {
- let homeserver: HomeserverInstance;
- const TEST_USER = "Hanako";
-
- const percyCSS =
- // FIXME: hide mx_SeekBar because flaky - see https://github.com/vector-im/element-web/issues/24898
- ".mx_SeekBar, " +
- // Exclude various components from the snapshot, for consistency
- ".mx_JumpToBottomButton, " +
- ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
-
- const uploadFile = (file: string) => {
- // Upload a file from the message composer
- cy.get(".mx_MessageComposer_actions input[type='file']").selectFile(file, { force: true });
-
- cy.get(".mx_Dialog").within(() => {
- // Find and click primary "Upload" button
- cy.findByRole("button", { name: "Upload" }).click();
- });
-
- // Wait until the file is sent
- cy.get(".mx_RoomView_statusArea_expanded").should("not.exist");
- cy.get(".mx_EventTile.mx_EventTile_last .mx_EventTile_receiptSent").should("exist");
- // wait for the tile to finish loading
- cy.get(".mx_AudioPlayer_mediaName").should("exist");
- };
-
- /**
- * Take snapshots of mx_EventTile_last on each layout, outputting log for reference/debugging.
- * @param detail The Percy snapshot name. Used for outputting logs too.
- * @param monospace This changes the font used to render the UI from a default one to a monospace one.
- * Set to false by default. Note that the font applied to Percy snapshots can be different from the test result
- * on your local environment.
- */
- const takeSnapshots = (detail: string, monospace = false) => {
- // Check that the audio player is rendered and its button becomes visible
- const checkPlayerVisibility = () => {
- // Assert that the audio player and media information are visible
- cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo").within(
- () => {
- cy.contains(".mx_AudioPlayer_mediaName", ".ogg").should("be.visible"); // extension
- cy.contains(".mx_AudioPlayer_byline", "00:01").should("be.visible");
- cy.contains(".mx_AudioPlayer_byline", "(3.56 KB)").should("be.visible"); // actual size
- },
- );
-
- // Assert that the play button can be found and is visible
- cy.findByRole("button", { name: "Play" }).should("be.visible");
-
- if (monospace) {
- // Assert that the monospace timer is visible
- cy.get("[role='timer']").should("have.css", "font-family", '"monospace"').should("be.visible");
- }
- };
-
- /**
- * Define snapshot widths of selected EventTile, on which the audio player is rendered
- *
- * 50px (magic number): narrow enough EventTile to be compressed to check a11y
- * 267px: EventTile on IRC and modern/group layout, on which the player is rendered in its full width
- * 285px: EventTile on bubble layout, on which the player is rendered in its full width
- */
- const snapshotWidthsIRC = [50, 267];
- const snapshotWidthsGroup = snapshotWidthsIRC;
- const snapshotWidthsBubble = [50, 285];
-
- if (monospace) {
- // Enable system font and monospace setting
- cy.setSettingValue("useSystemFont", null, SettingLevel.DEVICE, true);
- cy.setSettingValue("systemFont", null, SettingLevel.DEVICE, "monospace");
- }
-
- // Check the status of the seek bar
- // TODO: check if visible - currently checking its visibility on a compressed EventTile returns an error
- cy.get(".mx_AudioPlayer_seek input[type='range']").should("exist");
-
- // Enable IRC layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
-
- cy.get(".mx_EventTile_last[data-layout='irc']").within(() => {
- // Click the event timestamp to highlight EventTile in case it is not visible
- cy.get(".mx_MessageTimestamp").click();
-
- // Assert that rendering of the player settled and the play button is visible before taking a snapshot
- checkPlayerVisibility();
- });
-
- // Take a snapshot of mx_EventTile_last on IRC layout
- cy.get(".mx_EventTile_last").percySnapshotElement(detail + " on IRC layout", {
- percyCSS,
- widths: snapshotWidthsIRC,
- });
-
- // Output a log
- cy.log("Took a snapshot of " + detail + " on IRC layout");
-
- // Take a snapshot on modern/group layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
- cy.get(".mx_EventTile_last[data-layout='group']").within(() => {
- cy.get(".mx_MessageTimestamp").click();
- checkPlayerVisibility();
- });
- cy.get(".mx_EventTile_last").percySnapshotElement(detail + " on modern/group layout", {
- percyCSS,
- widths: snapshotWidthsGroup,
- });
- cy.log("Took a snapshot of " + detail + " on modern/group layout");
-
- // Take a snapshot on bubble layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
- cy.get(".mx_EventTile_last[data-layout='bubble']").within(() => {
- cy.get(".mx_MessageTimestamp").click();
- checkPlayerVisibility();
- });
- cy.get(".mx_EventTile_last").percySnapshotElement(detail + " on bubble layout", {
- percyCSS,
- widths: snapshotWidthsBubble,
- });
- cy.log("Took a snapshot of " + detail + " on bubble layout");
- };
-
- beforeEach(() => {
- cy.startHomeserver("default").then((data) => {
- homeserver = data;
- cy.initTestUser(homeserver, TEST_USER);
- });
-
- cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
-
- // Wait until configuration is finished
- cy.get(".mx_GenericEventListSummary[data-layout='group'] .mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(TEST_USER + " created and configured the room.").should("exist");
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- it("should be correctly rendered - light theme", () => {
- uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg");
-
- takeSnapshots("Selected EventTile of audio player (light theme)");
- });
-
- it("should be correctly rendered - light theme with monospace font", () => {
- uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg");
-
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //takeSnapshots("Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
- });
-
- it("should be correctly rendered - high contrast theme", () => {
- // Disable system theme in case ThemeWatcher enables the theme automatically,
- // so that the high contrast theme can be enabled
- cy.setSettingValue("use_system_theme", null, SettingLevel.DEVICE, false);
-
- // Enable high contrast manually
- cy.openUserSettings("Appearance")
- .findByTestId("mx_ThemeChoicePanel")
- .findByLabelText("Use high contrast")
- .click({ force: true }); // force click because the size of the checkbox is zero
-
- cy.closeDialog();
-
- uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg");
-
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //takeSnapshots("Selected EventTile of audio player (high contrast)");
- });
-
- it("should be correctly rendered - dark theme", () => {
- // Enable dark theme
- cy.setSettingValue("theme", null, SettingLevel.ACCOUNT, "dark");
-
- uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg");
-
- takeSnapshots("Selected EventTile of audio player (dark theme)");
- });
-
- it("should play an audio file", () => {
- uploadFile("cypress/fixtures/1sec.ogg");
-
- // Assert that the audio player is rendered
- cy.get(".mx_EventTile_last .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
- // Assert that the counter is zero before clicking the play button
- cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
-
- // Find and click "Play" button, the wait is to make the test less flaky
- cy.findByRole("button", { name: "Play" }).should("exist");
- cy.wait(500).findByRole("button", { name: "Play" }).click();
-
- // Assert that "Pause" button can be found
- cy.findByRole("button", { name: "Pause" }).should("exist");
-
- // Assert that the timer is reset when the audio file finished playing
- cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
-
- // Assert that "Play" button can be found
- cy.findByRole("button", { name: "Play" }).should("exist");
- });
- });
-
- it("should support downloading an audio file", () => {
- uploadFile("cypress/fixtures/1sec.ogg");
-
- // Find and click "Download" button on MessageActionBar
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Download" }).click();
-
- // Assert that the file was downloaded
- cy.readFile("cypress/downloads/1sec.ogg").should("exist");
- });
-
- it("should support replying to audio file with another audio file", () => {
- uploadFile("cypress/fixtures/1sec.ogg");
-
- // Assert the audio player is rendered
- cy.get(".mx_EventTile_last .mx_AudioPlayer_container").should("exist");
-
- // Find and click "Reply" button on MessageActionBar
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click();
-
- // Reply to the player with another audio file
- uploadFile("cypress/fixtures/1sec.ogg");
-
- cy.get(".mx_EventTile_last").within(() => {
- // Assert that the audio player is rendered
- cy.get(".mx_AudioPlayer_container").should("exist");
-
- // Assert that replied audio file is rendered as file button inside ReplyChain
- cy.get(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']").within(() => {
- // Assert that the file button has file name
- cy.get(".mx_MFileBody_info_filename").should("exist");
- });
- });
-
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //takeSnapshots("Selected EventTile of audio player with a reply");
- });
-
- it("should support creating a reply chain with multiple audio files", () => {
- // Note: "mx_ReplyChain" element is used not only for replies which
- // create a reply chain, but also for a single reply without a replied
- // message. This test checks whether a reply chain which consists of
- // multiple audio file replies is rendered properly.
-
- // Find and click "Reply" button
- const clickButtonReply = () => {
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click();
- };
-
- uploadFile("cypress/fixtures/upload-first.ogg");
-
- // Assert that the audio player is rendered
- cy.get(".mx_EventTile_last .mx_AudioPlayer_container").should("exist");
-
- clickButtonReply();
-
- // Reply to the player with another audio file
- uploadFile("cypress/fixtures/upload-second.ogg");
-
- // Assert that the audio player is rendered
- cy.get(".mx_EventTile_last .mx_AudioPlayer_container").should("exist");
-
- clickButtonReply();
-
- // Reply to the player with yet another audio file to create a reply chain
- uploadFile("cypress/fixtures/upload-third.ogg");
-
- cy.get(".mx_EventTile_last").within(() => {
- // Assert that the audio player is rendered
- cy.get(".mx_AudioPlayer_container").should("exist");
-
- // Assert that there are two "mx_ReplyChain" elements
- cy.get(".mx_ReplyChain").should("have.length", 2);
-
- // Assert that one line contains the user name
- cy.get(".mx_ReplyChain .mx_ReplyTile_sender").within(() => {
- cy.findByText(TEST_USER);
- });
-
- // Assert that the other line contains the file button
- cy.get(".mx_ReplyChain .mx_MFileBody").should("exist");
-
- // Click "In reply to"
- cy.contains(".mx_ReplyChain .mx_ReplyChain_show", "In reply to").click();
-
- cy.get("blockquote.mx_ReplyChain:first-of-type").within(() => {
- // Assert that "In reply to" has disappeared
- cy.findByText("In reply to").should("not.exist");
-
- // Assert that audio file on the first row is rendered as file button
- cy.get(".mx_MFileBody_info[role='button']").within(() => {
- // Assert that the file button contains the name of the file sent at first
- cy.contains(".mx_MFileBody_info_filename", "upload-first.ogg");
- });
- });
- });
-
- // Take snapshots
- takeSnapshots("Selected EventTile of audio player with a reply chain");
- });
-
- it("should be rendered, play, and support replying on a thread", () => {
- uploadFile("cypress/fixtures/1sec-long-name-audio-file.ogg");
-
- // On the main timeline
- cy.get(".mx_RoomView_MessageList").within(() => {
- // Assert the audio player is rendered
- cy.get(".mx_EventTile_last .mx_AudioPlayer_container").should("exist");
-
- // Find and click "Reply in thread" button
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply in thread" }).click();
- });
-
- // On a thread
- cy.get(".mx_ThreadView").within(() => {
- cy.get(".mx_EventTile_last").within(() => {
- // Assert that the player is correctly rendered on a thread
- cy.get(".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container").within(() => {
- // Assert that the counter is zero before clicking the play button
- cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
-
- // Find and click "Play" button, the wait is to make the test less flaky
- cy.findByRole("button", { name: "Play" }).should("exist");
- cy.wait(500).findByRole("button", { name: "Play" }).click();
-
- // Assert that "Pause" button can be found
- cy.findByRole("button", { name: "Pause" }).should("exist");
-
- // Assert that the timer is reset when the audio file finished playing
- cy.contains(".mx_AudioPlayer_seek [role='timer']", "00:00").should("exist");
-
- // Assert that "Play" button can be found
- cy.findByRole("button", { name: "Play" }).should("exist").should("not.have.attr", "disabled");
- });
- });
-
- // Find and click "Reply" button
- //
- // Calling cy.get(".mx_EventTile_last") again here is a workaround for
- // https://github.com/matrix-org/matrix-js-sdk/issues/3394: the event tile may have been re-mounted while
- // the audio was playing.
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click();
-
- cy.get(".mx_MessageComposer--compact").within(() => {
- // Assert that the reply preview is rendered on the message composer
- cy.get(".mx_ReplyPreview").within(() => {
- // Assert that the reply preview contains audio ReplyTile the file info button
- cy.get(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']").should("exist");
- });
-
- // Select :smile: emoji and send it
- cy.findByTestId("basicmessagecomposer").type(":smile:");
- cy.get(".mx_Autocomplete_Completion[aria-selected='true']").click();
- cy.findByTestId("basicmessagecomposer").type("{enter}");
- });
-
- cy.get(".mx_EventTile_last").within(() => {
- // Assert that the file name is rendered on the file button
- cy.get(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']").should("exist");
- });
- });
- });
-});
diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts
new file mode 100644
index 0000000000..f7aaf1fb46
--- /dev/null
+++ b/playwright/e2e/audio-player/audio-player.spec.ts
@@ -0,0 +1,351 @@
+/*
+Copyright 2023 Suguru Hirahara
+Copyright 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 type { Locator, Page } from "@playwright/test";
+import { test, expect } from "../../element-web-test";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+import { Layout } from "../../../src/settings/enums/Layout";
+import { ElementAppPage } from "../../pages/ElementAppPage";
+
+test.describe("Audio player", () => {
+ test.use({
+ displayName: "Hanako",
+ });
+
+ const uploadFile = async (page: Page, file: string) => {
+ // Upload a file from the message composer
+ await page.locator(".mx_MessageComposer_actions input[type='file']").setInputFiles(file);
+
+ // Find and click primary "Upload" button
+ await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();
+
+ // 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 .mx_EventTile_receiptSent")).toBeVisible();
+ // wait for the tile to finish loading
+ await expect(
+ page
+ .locator(".mx_AudioPlayer_mediaName")
+ .last()
+ .filter({ hasText: file.split("/").at(-1) }),
+ ).toBeVisible();
+ };
+
+ /**
+ * Take snapshots of mx_EventTile_last on each layout, outputting log for reference/debugging.
+ * @param detail The snapshot name. Used for outputting logs too.
+ * @param monospace This changes the font used to render the UI from a default one to Inconsolata. Set to false by default.
+ */
+ const takeSnapshots = async (page: Page, app: ElementAppPage, detail: string, monospace = false) => {
+ // Check that the audio player is rendered and its button becomes visible
+ const checkPlayerVisibility = async (locator: Locator) => {
+ // Assert that the audio player and media information are visible
+ const mediaInfo = locator.locator(
+ ".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo",
+ );
+ await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName", { hasText: ".ogg" })).toBeVisible(); // extension
+ await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
+ await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
+
+ // Assert that the play button can be found and is visible
+ await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
+
+ if (monospace) {
+ // Assert that the monospace timer is visible
+ await expect(locator.locator("[role='timer']")).toHaveCSS("font-family", "Inconsolata");
+ }
+ };
+
+ if (monospace) {
+ // Enable system font and monospace setting
+ await app.settings.setValue("useBundledEmojiFont", null, SettingLevel.DEVICE, false);
+ await app.settings.setValue("useSystemFont", null, SettingLevel.DEVICE, true);
+ await app.settings.setValue("systemFont", null, SettingLevel.DEVICE, "Inconsolata");
+ }
+
+ // Check the status of the seek bar
+ expect(await page.locator(".mx_AudioPlayer_seek input[type='range']").count()).toBeGreaterThan(0);
+
+ // Enable IRC layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+
+ const ircTile = page.locator(".mx_EventTile_last[data-layout='irc']");
+ // Click the event timestamp to highlight EventTile in case it is not visible
+ await ircTile.locator(".mx_MessageTimestamp").click();
+ // Assert that rendering of the player settled and the play button is visible before taking a snapshot
+ await checkPlayerVisibility(ircTile);
+
+ const screenshotOptions = {
+ css: `
+ /* The timestamp is of inconsistent width depending on the time the test runs at */
+ .mx_MessageTimestamp {
+ display: none !important;
+ }
+ `,
+ mask: [page.locator(".mx_AudioPlayer_seek")],
+ };
+
+ // Take a snapshot of mx_EventTile_last on IRC layout
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ `${detail.replaceAll(" ", "-")}-irc-layout.png`,
+ screenshotOptions,
+ );
+
+ // Take a snapshot on modern/group layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+ const groupTile = page.locator(".mx_EventTile_last[data-layout='group']");
+ await groupTile.locator(".mx_MessageTimestamp").click();
+ await checkPlayerVisibility(groupTile);
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ `${detail.replaceAll(" ", "-")}-group-layout.png`,
+ screenshotOptions,
+ );
+
+ // Take a snapshot on bubble layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
+ const bubbleTile = page.locator(".mx_EventTile_last[data-layout='bubble']");
+ await bubbleTile.locator(".mx_MessageTimestamp").click();
+ await checkPlayerVisibility(bubbleTile);
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ `${detail.replaceAll(" ", "-")}-bubble-layout.png`,
+ screenshotOptions,
+ );
+ };
+
+ test.beforeEach(async ({ page, app, user }) => {
+ await app.client.createRoom({ name: "Test Room" });
+ await app.viewRoomByName("Test Room");
+
+ // Wait until configuration is finished
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary[data-layout='group'] .mx_GenericEventListSummary_summary")
+ .getByText(`${user.displayName} created and configured the room.`),
+ ).toBeVisible();
+ });
+
+ test("should be correctly rendered - light theme", async ({ page, app }) => {
+ await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
+ await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
+ });
+
+ test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => {
+ await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
+
+ await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
+ });
+
+ test("should be correctly rendered - high contrast theme", async ({ page, app }) => {
+ // Disable system theme in case ThemeWatcher enables the theme automatically,
+ // so that the high contrast theme can be enabled
+ await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
+
+ // Enable high contrast manually
+ const settings = await app.settings.openUserSettings("Appearance");
+ await settings.getByTestId("mx_ThemeChoicePanel").getByText("Use high contrast").click();
+
+ await app.closeDialog();
+
+ await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
+
+ await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
+ });
+
+ test("should be correctly rendered - dark theme", async ({ page, app }) => {
+ // Enable dark theme
+ await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
+
+ await uploadFile(page, "cypress/fixtures/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, "cypress/fixtures/1sec.ogg");
+
+ // Assert that the audio player is rendered
+ const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container");
+ // Assert that the counter is zero before clicking the play button
+ await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
+
+ // Find and click "Play" button, the wait is to make the test less flaky
+ await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
+ await container.getByRole("button", { name: "Play" }).click();
+
+ // Assert that "Pause" button can be found
+ await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
+
+ // Assert that the timer is reset when the audio file finished playing
+ await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
+
+ // Assert that "Play" button can be found
+ await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
+ });
+
+ test("should support downloading an audio file", async ({ page, app }) => {
+ await uploadFile(page, "cypress/fixtures/1sec.ogg");
+
+ const downloadPromise = page.waitForEvent("download");
+
+ // Find and click "Download" button on MessageActionBar
+ const tile = page.locator(".mx_EventTile_last");
+ await tile.hover();
+ await tile.getByRole("button", { name: "Download" }).click();
+
+ // Assert that the file was downloaded
+ const download = await downloadPromise;
+ expect(download.suggestedFilename()).toBe("1sec.ogg");
+ });
+
+ test("should support replying to audio file with another audio file", async ({ page, app }) => {
+ await uploadFile(page, "cypress/fixtures/1sec.ogg");
+
+ // Assert the audio player is rendered
+ await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
+
+ // Find and click "Reply" button on MessageActionBar
+ const tile = page.locator(".mx_EventTile_last");
+ await tile.hover();
+ await tile.getByRole("button", { name: "Reply", exact: true }).click();
+
+ // Reply to the player with another audio file
+ await uploadFile(page, "cypress/fixtures/1sec.ogg");
+
+ // Assert that the audio player is rendered
+ await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
+
+ // Assert that replied audio file is rendered as file button inside ReplyChain
+ const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
+ // Assert that the file button has file name
+ await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
+
+ await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
+ });
+
+ test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => {
+ // Note: "mx_ReplyChain" element is used not only for replies which
+ // create a reply chain, but also for a single reply without a replied
+ // message. This test checks whether a reply chain which consists of
+ // multiple audio file replies is rendered properly.
+
+ const tile = page.locator(".mx_EventTile_last");
+
+ // Find and click "Reply" button
+ const clickButtonReply = async () => {
+ await tile.hover();
+ await tile.getByRole("button", { name: "Reply", exact: true }).click();
+ };
+
+ await uploadFile(page, "cypress/fixtures/upload-first.ogg");
+
+ // Assert that the audio player is rendered
+ await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
+
+ await clickButtonReply();
+
+ // Reply to the player with another audio file
+ await uploadFile(page, "cypress/fixtures/upload-second.ogg");
+
+ // Assert that the audio player is rendered
+ await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
+
+ await clickButtonReply();
+
+ // Reply to the player with yet another audio file to create a reply chain
+ await uploadFile(page, "cypress/fixtures/upload-third.ogg");
+
+ // Assert that the audio player is rendered
+ await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
+
+ // Assert that there are two "mx_ReplyChain" elements
+ await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
+
+ // Assert that one line contains the user name
+ await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
+
+ // Assert that the other line contains the file button
+ await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible();
+
+ // Click "In reply to"
+ await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click();
+
+ const replyChain = tile.locator(".mx_ReplyChain:first-of-type");
+ // Assert that "In reply to" has disappeared
+ await expect(replyChain.getByText("In reply to")).not.toBeVisible();
+
+ // Assert that the file button contains the name of the file sent at first
+ await expect(
+ replyChain
+ .locator(".mx_MFileBody_info[role='button']")
+ .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
+ ).toBeVisible();
+
+ // Take snapshots
+ await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
+ });
+
+ test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
+ await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
+
+ // On the main timeline
+ const messageList = page.locator(".mx_RoomView_MessageList");
+ // Assert the audio player is rendered
+ await expect(messageList.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
+ // Find and click "Reply in thread" button
+ await messageList.locator(".mx_EventTile_last").hover();
+ await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
+
+ // On a thread
+ const thread = page.locator(".mx_ThreadView");
+ const threadTile = thread.locator(".mx_EventTile_last");
+ const audioPlayer = threadTile.locator(".mx_AudioPlayer_container");
+
+ // Assert that the counter is zero before clicking the play button
+ await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
+
+ // Find and click "Play" button, the wait is to make the test less flaky
+ await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
+ await audioPlayer.getByRole("button", { name: "Play" }).click();
+
+ // Assert that "Pause" button can be found
+ await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
+
+ // Assert that the timer is reset when the audio file finished playing
+ await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
+
+ // Assert that "Play" button can be found
+ await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();
+
+ // Find and click "Reply" button
+ await threadTile.hover();
+ await threadTile.getByRole("button", { name: "Reply", exact: true }).click();
+
+ const composer = thread.locator(".mx_MessageComposer--compact");
+ // Assert that the reply preview contains audio ReplyTile the file info button
+ await expect(
+ composer.locator(".mx_ReplyPreview .mx_ReplyTile_audio .mx_MFileBody_info[role='button']"),
+ ).toBeVisible();
+
+ // Select :smile: emoji and send it
+ await composer.getByTestId("basicmessagecomposer").fill(":smile:");
+ await composer.locator(".mx_Autocomplete_Completion[aria-selected='true']").click();
+ await composer.getByTestId("basicmessagecomposer").press("Enter");
+
+ // Assert that the file name is rendered on the file button
+ await expect(threadTile.locator(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']")).toBeVisible();
+ });
+});
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index 52bd31c0ce..ee78be2d06 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -211,7 +211,17 @@ export const test = base.extend<
});
export const expect = baseExpect.extend({
- async toMatchScreenshot(this: ExpectMatcherState, receiver: Page | Locator, ...args) {
+ async toMatchScreenshot(
+ this: ExpectMatcherState,
+ receiver: Page | Locator,
+ name?: `${string}.png`,
+ options?: {
+ mask?: Array;
+ omitBackground?: boolean;
+ timeout?: number;
+ css?: string;
+ },
+ ) {
const page = "page" in receiver ? receiver.page() : receiver;
// We add a custom style tag before taking screenshots
@@ -233,10 +243,11 @@ export const expect = baseExpect.extend({
.mx_ReplyChain {
border-left-color: var(--cpd-color-blue-1200) !important;
}
+ ${options?.css ?? ""}
`,
})) as ElementHandle;
- await baseExpect(receiver).toHaveScreenshot(...args);
+ await baseExpect(receiver).toHaveScreenshot(name, options);
await style.evaluate((tag) => tag.remove());
return { pass: true, message: () => "", name: "toMatchScreenshot" };
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png
new file mode 100644
index 0000000000..1a6148c9c1
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png
new file mode 100644
index 0000000000..6d074f2773
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png
new file mode 100644
index 0000000000..98ce66178e
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--dark-theme--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png
new file mode 100644
index 0000000000..7e22dea5df
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png
new file mode 100644
index 0000000000..655baf15a2
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png
new file mode 100644
index 0000000000..f230fe06da
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--high-contrast--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png
new file mode 100644
index 0000000000..6968d3176d
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png
new file mode 100644
index 0000000000..2e661cac95
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png
new file mode 100644
index 0000000000..0c474b2c5c
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png
new file mode 100644
index 0000000000..1d8b5771d4
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png
new file mode 100644
index 0000000000..a59f8fbf17
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png
new file mode 100644
index 0000000000..4b74eb5745
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player--light-theme--monospace-font--irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png
new file mode 100644
index 0000000000..6a486d591f
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png
new file mode 100644
index 0000000000..22b7b56d27
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png
new file mode 100644
index 0000000000..8592436e75
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png
new file mode 100644
index 0000000000..5ec4e38282
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-chain-irc-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png
new file mode 100644
index 0000000000..00a570e933
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-group-layout-linux.png differ
diff --git a/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png
new file mode 100644
index 0000000000..a3bbd87162
Binary files /dev/null and b/playwright/snapshots/audio-player/audio-player.spec.ts/Selected-EventTile-of-audio-player-with-a-reply-irc-layout-linux.png differ