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
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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" };
|
||||
|
|
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 12 KiB |