Migrate audio-player.spec.ts from Cypress to Playwright (#12008)

* Migrate audio-player.spec.ts from Cypress to Playwright

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Stabilise screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Stabilise bot MXID length

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Stabilise test user MXID

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Stabilise timestamps

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update screenshots

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update element-web-test.ts

* Use deterministic monospace font

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28217/head
Michael Telatynski 2023-12-11 14:36:30 +00:00 committed by GitHub
parent f9b50938de
commit 6199287fe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 364 additions and 389 deletions

View File

@ -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.
*/
/// <reference types="cypress" />
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");
});
});
});
});

View File

@ -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();
});
});

View File

@ -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<Locator>;
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<Element>;
await baseExpect(receiver).toHaveScreenshot(...args);
await baseExpect(receiver).toHaveScreenshot(name, options);
await style.evaluate((tag) => tag.remove());
return { pass: true, message: () => "", name: "toMatchScreenshot" };