diff --git a/cypress/e2e/timeline/timeline.spec.ts b/cypress/e2e/timeline/timeline.spec.ts
deleted file mode 100644
index e79f506e25..0000000000
--- a/cypress/e2e/timeline/timeline.spec.ts
+++ /dev/null
@@ -1,1031 +0,0 @@
-/*
-Copyright 2022 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 { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/matrix";
-import { HomeserverInstance } from "../../plugins/utils/homeserver";
-import { SettingLevel } from "../../../src/settings/SettingLevel";
-import { Layout } from "../../../src/settings/enums/Layout";
-import { MatrixClient } from "../../global";
-import Chainable = Cypress.Chainable;
-
-// The avatar size used in the timeline
-const AVATAR_SIZE = 30;
-// The resize method used in the timeline
-const AVATAR_RESIZE_METHOD = "crop";
-
-const ROOM_NAME = "Test room";
-const OLD_AVATAR = "avatar_image1";
-const NEW_AVATAR = "avatar_image2";
-const OLD_NAME = "Alan";
-const NEW_NAME = "Alan (away)";
-
-const getEventTilesWithBodies = (): Chainable => {
- return cy.get(".mx_EventTile").filter((_i, e) => e.getElementsByClassName("mx_EventTile_body").length > 0);
-};
-
-const expectDisplayName = (e: JQuery, displayName: string): void => {
- expect(e.find(".mx_DisambiguatedProfile_displayName").text()).to.equal(displayName);
-};
-
-const expectAvatar = (e: JQuery, avatarUrl: string): void => {
- cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => {
- const size = AVATAR_SIZE * win.devicePixelRatio;
- expect(e.find(".mx_BaseAvatar img").attr("src")).to.equal(
- // eslint-disable-next-line no-restricted-properties
- cli.mxcUrlToHttp(avatarUrl, size, size, AVATAR_RESIZE_METHOD),
- );
- });
-};
-
-const sendEvent = (roomId: string, html = false): Chainable => {
- const content = {
- msgtype: "m.text" as MsgType,
- body: "Message",
- format: undefined,
- formatted_body: undefined,
- };
- if (html) {
- content.format = "org.matrix.custom.html";
- content.formatted_body = "Message";
- }
- return cy.sendEvent(roomId, null, "m.room.message" as EventType, content);
-};
-
-describe("Timeline", () => {
- let homeserver: HomeserverInstance;
-
- let roomId: string;
-
- let oldAvatarUrl: string;
- let newAvatarUrl: string;
-
- beforeEach(() => {
- cy.startHomeserver("default").then((data) => {
- homeserver = data;
- cy.initTestUser(homeserver, OLD_NAME).then(() =>
- cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
- roomId = _room1Id;
- }),
- );
- });
- });
-
- afterEach(() => {
- cy.stopHomeserver(homeserver);
- });
-
- describe("useOnlyCurrentProfiles", () => {
- beforeEach(() => {
- cy.uploadContent(OLD_AVATAR).then(({ content_uri: url }) => {
- oldAvatarUrl = url;
- cy.setAvatarUrl(url);
- });
- cy.uploadContent(NEW_AVATAR).then(({ content_uri: url }) => {
- newAvatarUrl = url;
- });
- });
-
- it("should show historical profiles if disabled", () => {
- cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, false);
- sendEvent(roomId);
- cy.setDisplayName("Alan (away)");
- cy.setAvatarUrl(newAvatarUrl);
- // XXX: If we send the second event too quickly, there won't be
- // enough time for the client to register the profile change
- cy.wait(500);
- sendEvent(roomId);
- cy.viewRoomByName(ROOM_NAME);
-
- const events = getEventTilesWithBodies();
-
- events.should("have.length", 2);
- events.each((e, i) => {
- if (i === 0) {
- expectDisplayName(e, OLD_NAME);
- expectAvatar(e, oldAvatarUrl);
- } else if (i === 1) {
- expectDisplayName(e, NEW_NAME);
- expectAvatar(e, newAvatarUrl);
- }
- });
- });
-
- it("should not show historical profiles if enabled", () => {
- cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, true);
- sendEvent(roomId);
- cy.setDisplayName(NEW_NAME);
- cy.setAvatarUrl(newAvatarUrl);
- // XXX: If we send the second event too quickly, there won't be
- // enough time for the client to register the profile change
- cy.wait(500);
- sendEvent(roomId);
- cy.viewRoomByName(ROOM_NAME);
-
- const events = getEventTilesWithBodies();
-
- events.should("have.length", 2);
- events.each((e) => {
- expectDisplayName(e, NEW_NAME);
- expectAvatar(e, newAvatarUrl);
- });
- });
- });
-
- describe("configure room", () => {
- // Exclude timestamp and read marker from snapshots
- const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
-
- beforeEach(() => {
- cy.injectAxe();
- });
-
- it("should create and configure a room on IRC layout", () => {
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
- cy.get(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc']").within(() => {
- cy.get(".mx_GenericEventListSummary_summary")
- .findByText(OLD_NAME + " created and configured the room.")
- .should("exist");
- });
-
- // wait for the date separator to appear to have a stable percy snapshot
- cy.get(".mx_TimelineSeparator").should("have.text", "today");
-
- cy.get(".mx_MainSplit").percySnapshotElement("Configured room on IRC layout");
- });
-
- it("should have an expanded generic event list summary (GELS) on IRC layout", () => {
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
-
- // Wait until configuration is finished
- cy.get(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc']").within(() => {
- cy.get(".mx_GenericEventListSummary_summary")
- .findByText(OLD_NAME + " created and configured the room.")
- .should("exist");
- });
-
- cy.get(".mx_GenericEventListSummary").within(() => {
- // Click "expand" link button
- cy.findByRole("button", { name: "Expand" }).click();
-
- // Assert that the "expand" link button worked
- cy.findByRole("button", { name: "Collapse" }).should("exist");
- });
-
- cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on IRC layout", { percyCSS });
- });
-
- it("should have an expanded generic event list summary (GELS) on compact modern/group layout", () => {
- cy.visit("/#/room/" + roomId);
-
- // Set compact modern layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group).setSettingValue(
- "useCompactLayout",
- null,
- SettingLevel.DEVICE,
- true,
- );
-
- // Wait until configuration is finished
- cy.get(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']")
- .findByText(OLD_NAME + " created and configured the room.")
- .should("exist");
-
- cy.get(".mx_GenericEventListSummary").within(() => {
- // Click "expand" link button
- cy.findByRole("button", { name: "Expand" }).click();
-
- // Assert that the "expand" link button worked
- cy.findByRole("button", { name: "Collapse" }).should("exist");
- });
-
- cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on modern layout", { percyCSS });
- });
-
- it("should click 'collapse' on the first hovered info event line inside GELS on bubble layout", () => {
- // This test checks clickability of the "Collapse" link button, which had been covered with
- // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864
-
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
- cy.get(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='bubble']").within(() => {
- cy.get(".mx_GenericEventListSummary_summary")
- .findByText(OLD_NAME + " created and configured the room.")
- .should("exist");
- });
-
- cy.get(".mx_GenericEventListSummary").within(() => {
- // Click "expand" link button
- cy.findByRole("button", { name: "Expand" }).click();
-
- // Assert that the "expand" link button worked
- cy.findByRole("button", { name: "Collapse" }).should("exist");
- });
-
- // Make sure spacer is not visible on bubble layout
- cy.get(".mx_GenericEventListSummary[data-layout=bubble] .mx_GenericEventListSummary_spacer").should(
- "not.be.visible", // See: _GenericEventListSummary.pcss
- );
-
- // Exclude timestamp from snapshot
- const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
-
- // Save snapshot of expanded generic event list summary on bubble layout
- cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS on bubble layout", { percyCSS });
-
- cy.get(".mx_GenericEventListSummary").within(() => {
- // Click "collapse" link button on the first hovered info event line
- cy.get(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type")
- .realHover()
- .findByRole("toolbar", { name: "Message Actions" })
- .should("be.visible");
- cy.findByRole("button", { name: "Collapse" }).click();
-
- // Assert that "collapse" link button worked
- cy.findByRole("button", { name: "Expand" }).should("exist");
- });
-
- // Save snapshot of collapsed generic event list summary on bubble layout
- cy.get(".mx_MainSplit").percySnapshotElement("Collapsed GELS on bubble layout", { percyCSS });
- });
-
- it("should add inline start margin to an event line on IRC layout", () => {
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
-
- // Wait until configuration is finished
- cy.get(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc']").within(() => {
- cy.get(".mx_GenericEventListSummary_summary")
- .findByText(OLD_NAME + " created and configured the room.")
- .should("exist");
- });
-
- // Click "expand" link button
- cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "Expand" }).click();
-
- // Check the event line has margin instead of inset property
- // cf. _EventTile.pcss
- // --EventTile_irc_line_info-margin-inline-start
- // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding))
- // = 80 + 14 + 5 = 99px
-
- cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
- .should("have.css", "margin-inline-start", "99px")
- .should("have.css", "inset-inline-start", "0px");
-
- // Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
- cy.get(".mx_MainSplit").percySnapshotElement("Event line with inline start margin on IRC layout", {
- percyCSS,
- });
- cy.checkA11y();
- });
- });
-
- describe("message displaying", () => {
- beforeEach(() => {
- cy.injectAxe();
- });
-
- const messageEdit = () => {
- cy.contains(".mx_EventTile .mx_EventTile_line", "Message")
- .realHover()
- .findByRole("toolbar", { name: "Message Actions" })
- .findByRole("button", { name: "Edit" })
- .click();
- cy.findByRole("textbox", { name: "Edit message" }).type("Edit{enter}");
-
- // Assert that the edited message and the link button are found
- cy.contains(".mx_EventTile .mx_EventTile_line", "MessageEdit").within(() => {
- // Regex patterns due to the edited date
- cy.findByRole("button", { name: /Edited at .*? Click to view edits./ });
- });
- };
-
- it("should align generic event list summary with messages and emote on IRC layout", () => {
- // This test aims to check:
- // 1. Alignment of collapsed GELS (generic event list summary) and messages
- // 2. Alignment of expanded GELS and messages
- // 3. Alignment of expanded GELS and placeholder of deleted message
- // 4. Alignment of expanded GELS, placeholder of deleted message, and emote
-
- // Exclude timestamp from snapshot of mx_MainSplit
- const percyCSS = ".mx_MainSplit .mx_MessageTimestamp { visibility: hidden !important; }";
-
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
-
- // Wait until configuration is finished
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- // Send messages
- cy.get(".mx_RoomView_body").within(() => {
- cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}");
- cy.findByRole("textbox", { name: "Send a message…" }).type("Hello again, Mr. Bot{enter}");
- });
-
- // Make sure the second message was sent
- cy.get(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible");
-
- // 1. Alignment of collapsed GELS (generic event list summary) and messages
- // Check inline start spacing of collapsed GELS
- // See: _EventTile.pcss
- // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line
- // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding)
- // = 80 + 14 + 46 + 2 * 5
- // = 150px
- cy.get(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line").should(
- "have.css",
- "padding-inline-start",
- "150px",
- );
- // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px
- // --right-padding should be applied
- cy.get(".mx_EventTile > *").should("have.css", "margin-right", "5px");
- // --name-width width zero inline end margin should be applied
- cy.get(".mx_EventTile .mx_DisambiguatedProfile")
- .should("have.css", "width", "80px")
- .should("have.css", "margin-inline-end", "0px");
- // --icon-width should be applied
- cy.get(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").should("have.css", "width", "14px");
- // var(--MessageTimestamp-width) should be applied
- cy.get(".mx_EventTile > a").should("have.css", "min-width", "46px");
- // Record alignment of collapsed GELS and messages on messagePanel
- cy.get(".mx_MainSplit").percySnapshotElement("Collapsed GELS and messages on IRC layout", { percyCSS });
-
- // 2. Alignment of expanded GELS and messages
- // Click "expand" link button
- cy.get(".mx_GenericEventListSummary").findByRole("button", { name: "Expand" }).click();
- // Check inline start spacing of info line on expanded GELS
- cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
- // See: _EventTile.pcss
- // --EventTile_irc_line_info-margin-inline-start
- // = 80 + 14 + 1 * 5
- .should("have.css", "margin-inline-start", "99px");
- // Record alignment of expanded GELS and messages on messagePanel
- cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS and messages on IRC layout", { percyCSS });
-
- // 3. Alignment of expanded GELS and placeholder of deleted message
- // Delete the second (last) message
- cy.get(".mx_RoomView_MessageList > .mx_EventTile_last")
- .realHover()
- .findByRole("button", { name: "Options" })
- .should("be.visible")
- .click();
- cy.findByRole("menuitem", { name: "Remove" }).should("be.visible").click();
- // Confirm deletion
- cy.get(".mx_Dialog_buttons").within(() => {
- cy.findByRole("button", { name: "Remove" }).click();
- });
- // Make sure the dialog was closed and the second (last) message was redacted
- cy.get(".mx_Dialog").should("not.exist");
- cy.get(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody").should("be.visible");
- cy.get(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible");
- // Record alignment of expanded GELS and placeholder of deleted message on messagePanel
- cy.get(".mx_MainSplit").percySnapshotElement("Expanded GELS and with placeholder of deleted message", {
- percyCSS,
- });
-
- // 4. Alignment of expanded GELS, placeholder of deleted message, and emote
- // Send a emote
- cy.get(".mx_RoomView_body").within(() => {
- cy.findByRole("textbox", { name: "Send a message…" }).type("/me says hello to Mr. Bot{enter}");
- });
- // Check inline start margin of its avatar
- // Here --right-padding is for the avatar on the message line
- // See: _IRCLayout.pcss
- // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar
- // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding))
- // = 80 + 14 + 1 * 5
- cy.get(".mx_EventTile_emote .mx_EventTile_avatar").should("have.css", "margin-left", "99px");
- // Make sure emote was sent
- cy.get(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent").should("be.visible");
- // Record alignment of expanded GELS, placeholder of deleted message, and emote
- cy.get(".mx_MainSplit").percySnapshotElement(
- "Expanded GELS and with emote and placeholder of deleted message",
- {
- percyCSS,
- },
- );
- });
-
- it("should render EventTiles on IRC, modern (group), and bubble layout", () => {
- const percyCSS =
- // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
- ".mx_TopUnreadMessagesBar, " +
- // Exclude timestamp and read marker from snapshots
- ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
-
- sendEvent(roomId);
- sendEvent(roomId); // check continuation
- sendEvent(roomId); // check the last EventTile
-
- cy.visit("/#/room/" + roomId);
- // Send a plain text message
- cy.getComposer().type(`Hello{enter}`);
- // Send a big emoji
- cy.getComposer().type(`🏀{enter}`);
- // Send an inline emoji
- cy.getComposer().type(`This message has an inline emoji 👒{enter}`);
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // IRC layout
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
-
- // Wait until configuration is finished
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- cy.get(".mx_MainSplit").percySnapshotElement("EventTiles on IRC layout", { percyCSS });
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // Group/modern layout
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
-
- cy.get(".mx_RoomView_body[data-layout=group]").within(() => {
- // Check that the last EventTile is rendered
- cy.get(".mx_EventTile.mx_EventTile_last").should("exist");
- });
-
- cy.get(".mx_MainSplit").percySnapshotElement("EventTiles on modern layout", { percyCSS });
-
- // Check the same thing for compact layout
- cy.setSettingValue("useCompactLayout", null, SettingLevel.DEVICE, true);
-
- cy.get(".mx_MatrixChat_useCompactLayout").within(() => {
- // Check that the last EventTile is rendered
- cy.get(".mx_EventTile.mx_EventTile_last").should("exist");
- });
-
- cy.get(".mx_MainSplit").percySnapshotElement("EventTiles on compact modern layout", { percyCSS });
-
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // Message bubble layout
- ////////////////////////////////////////////////////////////////////////////////////////////////////////////
-
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
-
- cy.get(".mx_MainSplit").percySnapshotElement("EventTiles on bubble layout", { percyCSS });
- });
-
- it("should set inline start padding to a hidden event line", () => {
- sendEvent(roomId);
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- // Edit message
- messageEdit();
-
- // Click timestamp to highlight hidden event line
- cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
-
- // Exclude timestamp and read marker from snapshot
- //const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
-
- // should not add inline start padding to a hidden event line on IRC layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
- cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").should(
- "have.css",
- "padding-inline-start",
- "0px",
- );
-
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- /*cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with zero padding on IRC layout", {
- percyCSS,
- });*/
-
- // should add inline start padding to a hidden event line on modern layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
- cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line")
- // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
- .should("have.css", "padding-inline-start", "84px");
-
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //cy.get(".mx_MainSplit").percySnapshotElement("Hidden event line with padding on modern layout", {
- // percyCSS,
- //});
- });
-
- it("should click view source event toggle", () => {
- // This test checks:
- // 1. clickability of top left of view source event toggle
- // 2. clickability of view source toggle on IRC layout
-
- // Exclude timestamp from snapshot
- const percyCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
-
- sendEvent(roomId);
- cy.visit("/#/room/" + roomId);
- cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- // Edit message
- messageEdit();
-
- // 1. clickability of top left of view source event toggle
-
- // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area
- cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent")
- .should("exist")
- .realHover()
- .within(() => {
- cy.findByRole("button", { name: "toggle event" }).click("topLeft");
- });
-
- // Make sure the expand toggle works
- cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded")
- .should("be.visible")
- .realHover()
- .within(() => {
- cy.findByRole("button", { name: "toggle event" })
- // Check size and position of toggle on expanded view source event
- // See: _ViewSourceEvent.pcss
- .should("have.css", "height", "12px") // --ViewSourceEvent_toggle-size
- .should("have.css", "align-self", "flex-end")
-
- // Click again to collapse the source
- .click("topLeft");
- });
-
- // Make sure the collapse toggle works
- cy.get(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded").should("not.exist");
-
- // 2. clickability of view source toggle on IRC layout
-
- // Enable IRC layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
-
- // Hover the view source toggle on IRC layout
- cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent")
- .should("exist")
- .realHover()
- .percySnapshotElement("Hovered hidden event line on IRC layout", { percyCSS });
-
- // Click view source event toggle
- cy.get(".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent")
- .should("exist")
- .realHover()
- .within(() => {
- cy.findByRole("button", { name: "toggle event" }).click("topLeft");
- });
-
- // Make sure the expand toggle worked
- cy.get(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded").should("be.visible");
- });
-
- it("should render file size in kibibytes on a file tile", () => {
- cy.visit("/#/room/" + roomId);
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- // Upload a file from the message composer
- cy.get(".mx_MessageComposer_actions input[type='file']").selectFile(
- "cypress/fixtures/matrix-org-client-versions.json",
- { force: true },
- );
-
- cy.get(".mx_Dialog").within(() => {
- // Click "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");
-
- // Assert that the file size is displayed in kibibytes (1024 bytes), not kilobytes (1000 bytes)
- // See: https://github.com/vector-im/element-web/issues/24866
- cy.get(".mx_EventTile_last").within(() => {
- // actual file size in kibibytes
- cy.get(".mx_MFileBody_info_filename")
- .findByText(/1.12 KB/)
- .should("exist");
- });
- });
-
- it("should render url previews", () => {
- cy.intercept("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", {
- statusCode: 200,
- fixture: "riot.png",
- headers: {
- "Content-Type": "image/png",
- },
- }).as("mxc");
- cy.intercept("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", {
- statusCode: 200,
- body: {
- "og:title": "Element Call",
- "og:description": null,
- "og:image:width": 48,
- "og:image:height": 48,
- "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
- "og:image:type": "image/png",
- "matrix:image:size": 2121,
- },
- headers: {
- "Content-Type": "application/json",
- },
- }).as("preview_url");
-
- cy.sendEvent(roomId, null, "m.room.message" as EventType, {
- msgtype: "m.text" as MsgType,
- body: "https://call.element.io/",
- });
- cy.visit("/#/room/" + roomId);
-
- cy.get(".mx_LinkPreviewWidget").should("exist").findByText("Element Call");
-
- cy.wait("@preview_url");
- cy.wait("@mxc");
-
- cy.checkA11y();
-
- // Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
- cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
- percyCSS,
- widths: [800, 400],
- });
- });
-
- describe("on search results panel", () => {
- it("should highlight search result words regardless of formatting", () => {
- sendEvent(roomId);
- sendEvent(roomId, true);
- cy.visit("/#/room/" + roomId);
-
- cy.get(".mx_LegacyRoomHeader").findByRole("button", { name: "Search" }).click();
-
- cy.get(".mx_SearchBar").percySnapshotElement("Search bar on the timeline", {
- // Emulate narrow timeline
- widths: [320, 640],
- });
-
- cy.get(".mx_SearchBar_input").findByRole("textbox").type("Message{enter}");
-
- cy.get(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight").should("exist");
- cy.get(".mx_RoomView_searchResultsPanel").percySnapshotElement("Highlighted search results");
- });
-
- it("should render a fully opaque textual event", () => {
- const stringToSearch = "Message"; // Same with string sent with sendEvent()
-
- sendEvent(roomId);
-
- cy.visit("/#/room/" + roomId);
-
- // Open a room setting dialog
- cy.findByRole("button", { name: "Room options" }).click();
- cy.findByRole("menuitem", { name: "Settings" }).click();
-
- // Set a room topic to render a TextualEvent
- cy.findByRole("textbox", { name: "Room Topic" }).type(`This is a room for ${stringToSearch}.`);
- cy.findByRole("button", { name: "Save" }).click();
-
- cy.closeDialog();
-
- // Assert that the TextualEvent is rendered
- cy.findByText(`${OLD_NAME} changed the topic to "This is a room for ${stringToSearch}.".`)
- .should("exist")
- .should("have.class", "mx_TextualEvent");
-
- // Display the room search bar
- cy.get(".mx_LegacyRoomHeader").findByRole("button", { name: "Search" }).click();
-
- // Search the string to display both the message and TextualEvent on search results panel
- cy.get(".mx_SearchBar").within(() => {
- cy.findByRole("textbox").type(`${stringToSearch}{enter}`);
- });
-
- // On search results panel
- cy.get(".mx_RoomView_searchResultsPanel").within(() => {
- // Assert that contextual event tiles are translucent
- cy.get(".mx_EventTile.mx_EventTile_contextual").should("have.css", "opacity", "0.4");
-
- // Assert that the TextualEvent is fully opaque (visually solid).
- cy.get(".mx_EventTile .mx_TextualEvent").should("have.css", "opacity", "1");
- });
-
- cy.get(".mx_RoomView_searchResultsPanel").percySnapshotElement("Search results - with TextualEvent");
- });
- });
- });
-
- describe("message sending", () => {
- const MESSAGE = "Hello world";
- const reply = "Reply";
- const viewRoomSendMessageAndSetupReply = () => {
- // View room
- cy.visit("/#/room/" + roomId);
-
- // Send a message
- cy.getComposer().type(`${MESSAGE}{enter}`);
-
- // Reply to the message
- cy.get(".mx_EventTile_last")
- .within(() => {
- cy.findByText(MESSAGE);
- })
- .realHover()
- .findByRole("button", { name: "Reply" })
- .click();
- };
-
- // For clicking the reply button on the last line
- const clickButtonReply = () => {
- cy.get(".mx_RoomView_MessageList").within(() => {
- cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click();
- });
- };
-
- it("can reply with a text message", () => {
- viewRoomSendMessageAndSetupReply();
-
- cy.getComposer().type(`${reply}{enter}`);
-
- cy.get(".mx_RoomView_body").within(() => {
- cy.get(".mx_EventTile_last .mx_EventTile_line").within(() => {
- cy.get(".mx_ReplyTile .mx_MTextBody").within(() => {
- cy.findByText(MESSAGE).should("exist");
- });
-
- cy.findByText(reply).should("have.length", 1);
- });
- });
- });
-
- it("can reply with a voice message", () => {
- viewRoomSendMessageAndSetupReply();
-
- cy.openMessageComposerOptions().within(() => {
- cy.findByRole("menuitem", { name: "Voice Message" }).click();
- });
-
- // Record an empty message
- cy.wait(3000);
-
- cy.get(".mx_RoomView_body").within(() => {
- cy.get(".mx_MessageComposer").findByRole("button", { name: "Send voice message" }).click();
-
- cy.get(".mx_EventTile_last .mx_EventTile_line").within(() => {
- cy.get(".mx_ReplyTile .mx_MTextBody").within(() => {
- cy.findByText(MESSAGE).should("exist");
- });
-
- cy.get(".mx_MVoiceMessageBody").should("have.length", 1);
- });
- });
- });
-
- it("should not be possible to send flag with regional emojis", () => {
- cy.visit("/#/room/" + roomId);
-
- // Send a message
- cy.getComposer().type(":regional_indicator_a");
- cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_a:").click();
- cy.getComposer().type(":regional_indicator_r");
- cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_r:").click();
- cy.getComposer().type(" :regional_indicator_z");
- cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_z:").click();
- cy.getComposer().type(":regional_indicator_a");
- cy.contains(".mx_Autocomplete_Completion_title", ":regional_indicator_a:").click();
- cy.getComposer().type("{enter}");
-
- cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_MTextBody .mx_EventTile_bigEmoji")
- .children()
- .should("have.length", 4);
- });
-
- it("should display a reply chain", () => {
- let bot: MatrixClient;
- const reply2 = "Reply again";
-
- cy.visit("/#/room/" + roomId);
-
- // Wait until configuration is finished
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- // Create a bot "BotBob" and invite it
- cy.getBot(homeserver, {
- displayName: "BotBob",
- autoAcceptInvites: false,
- }).then((_bot) => {
- bot = _bot;
- cy.inviteUser(roomId, bot.getUserId());
- bot.joinRoom(roomId);
-
- // Make sure the bot joined the room
- cy.get(".mx_GenericEventListSummary .mx_EventTile_info.mx_EventTile_last").within(() => {
- cy.findByText("BotBob joined the room").should("exist");
- });
-
- // Have bot send MESSAGE to roomId
- cy.botSendMessage(bot, roomId, MESSAGE);
- });
-
- // Assert that MESSAGE is found
- cy.findByText(MESSAGE);
-
- // Reply to the message
- clickButtonReply();
- cy.getComposer().type(`${reply}{enter}`);
-
- // Make sure 'reply' was sent
- cy.get(".mx_RoomView_body .mx_EventTile_last").within(() => {
- cy.findByText(reply).should("exist");
- });
-
- // Reply again to create a replyChain
- clickButtonReply();
- cy.getComposer().type(`${reply2}{enter}`);
-
- // Assert that 'reply2' was sent
- cy.get(".mx_RoomView_body .mx_EventTile_last").within(() => {
- cy.findByText(reply2).should("exist");
- });
-
- cy.get(".mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible");
-
- // Exclude timestamp and read marker from snapshot
- const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
-
- // Check the margin value of ReplyChains of EventTile at the bottom on IRC layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
- cy.get(".mx_EventTile_last[data-layout='irc'] .mx_ReplyChain").should("have.css", "margin", "0px");
-
- // Take a snapshot on IRC layout
- // Note that because zero margin is applied to mx_ReplyChain, the left borders of two mx_ReplyChain
- // components may seem to be connected to one.
- cy.get(".mx_EventTile_last").percySnapshotElement("EventTile with reply chains on IRC layout", {
- percyCSS,
- });
-
- // Check the margin value of ReplyChains of EventTile at the bottom on group/modern layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
- cy.get(".mx_EventTile_last[data-layout='group'] .mx_ReplyChain").should("have.css", "margin-bottom", "8px");
-
- // Take a snapshot on modern layout
- cy.get(".mx_EventTile_last").percySnapshotElement("EventTile with reply chains on modern layout", {
- percyCSS,
- });
-
- // Check the margin value of ReplyChains of EventTile at the bottom on group/modern compact layout
- cy.setSettingValue("useCompactLayout", null, SettingLevel.DEVICE, true);
- cy.get(".mx_EventTile_last[data-layout='group'] .mx_ReplyChain").should("have.css", "margin-bottom", "4px");
-
- // Take a snapshot on compact modern layout
- cy.get(".mx_EventTile_last").percySnapshotElement("EventTile with reply chains on compact modern layout", {
- percyCSS,
- });
-
- // Check the margin value of ReplyChains of EventTile at the bottom on bubble layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
- cy.get(".mx_EventTile_last[data-layout='bubble'] .mx_ReplyChain").should(
- "have.css",
- "margin-bottom",
- "8px",
- );
-
- // Take a snapshot on bubble layout
- cy.get(".mx_EventTile_last").percySnapshotElement("EventTile with reply chains on bubble layout", {
- percyCSS,
- });
- });
-
- it("should send, reply, and display long strings without overflowing", () => {
- // Max 256 characters for display name
- const LONG_STRING =
- "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " +
- "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
- "aliquip";
-
- // Create a bot with a long display name
- let bot: MatrixClient;
- cy.getBot(homeserver, {
- displayName: LONG_STRING,
- autoAcceptInvites: false,
- }).then((_bot) => {
- bot = _bot;
- });
-
- // Create another room with a long name, invite the bot, and open the room
- cy.createRoom({ name: LONG_STRING })
- .as("testRoomId")
- .then((_roomId) => {
- roomId = _roomId;
- cy.inviteUser(roomId, bot.getUserId());
- bot.joinRoom(roomId);
- cy.visit("/#/room/" + roomId);
- });
-
- // Wait until configuration is finished
- cy.get(".mx_GenericEventListSummary_summary").within(() => {
- cy.findByText(OLD_NAME + " created and configured the room.").should("exist");
- });
-
- // Set the display name to "LONG_STRING 2" in order to avoid a warning in Percy tests from being triggered
- // due to the generated random mxid being displayed inside the GELS summary.
- cy.setDisplayName(`${LONG_STRING} 2`);
-
- // Have the bot send a long message
- cy.get("@testRoomId").then((roomId) => {
- bot.sendMessage(roomId, {
- body: LONG_STRING,
- msgtype: "m.text",
- });
- });
-
- // Wait until the message is rendered
- cy.get(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").within(() => {
- cy.findByText(LONG_STRING);
- });
-
- // Reply to the message
- clickButtonReply();
- cy.getComposer().type(`${reply}{enter}`);
-
- // Make sure the reply tile is rendered
- cy.get(".mx_EventTile_last .mx_EventTile_line").within(() => {
- cy.get(".mx_ReplyTile .mx_MTextBody").within(() => {
- cy.findByText(LONG_STRING).should("exist");
- });
-
- cy.findByText(reply).should("have.length", 1);
- });
-
- // Change the viewport size
- cy.viewport(1600, 1200);
-
- // Exclude timestamp and read marker from snapshots
- //const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }";
-
- // Make sure the strings do not overflow on IRC layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
- // Scroll to the bottom to have Percy take a snapshot of the whole viewport
- cy.get(".mx_ScrollPanel").scrollTo("bottom", { ensureScrollable: false });
- // Assert that both avatar in the introduction and the last message are visible at the same time
- cy.get(".mx_NewRoomIntro .mx_BaseAvatar").should("be.visible");
- cy.get(".mx_EventTile_last[data-layout='irc']").within(() => {
- cy.get(".mx_MTextBody").should("be.visible");
- cy.get(".mx_EventTile_receiptSent").should("be.visible"); // rendered at the bottom of EventTile
- });
- // Take a snapshot in IRC layout
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //cy.get(".mx_ScrollPanel").percySnapshotElement("Long strings with a reply on IRC layout", { percyCSS });
-
- // Make sure the strings do not overflow on modern layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
- cy.get(".mx_ScrollPanel").scrollTo("bottom", { ensureScrollable: false }); // Scroll again in case
- cy.get(".mx_NewRoomIntro .mx_BaseAvatar").should("be.visible");
- cy.get(".mx_EventTile_last[data-layout='group']").within(() => {
- cy.get(".mx_MTextBody").should("be.visible");
- cy.get(".mx_EventTile_receiptSent").should("be.visible");
- });
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //cy.get(".mx_ScrollPanel").percySnapshotElement("Long strings with a reply on modern layout", { percyCSS });
-
- // Make sure the strings do not overflow on bubble layout
- cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
- cy.get(".mx_ScrollPanel").scrollTo("bottom", { ensureScrollable: false }); // Scroll again in case
- cy.get(".mx_NewRoomIntro .mx_BaseAvatar").should("be.visible");
- cy.get(".mx_EventTile_last[data-layout='bubble']").within(() => {
- cy.get(".mx_MTextBody").should("be.visible");
- cy.get(".mx_EventTile_receiptSent").should("be.visible");
- });
- // Disabled because flaky - see https://github.com/vector-im/element-web/issues/24881
- //cy.get(".mx_ScrollPanel").percySnapshotElement("Long strings with a reply on bubble layout", { percyCSS });
- });
- });
-});
diff --git a/cypress/fixtures/element.png b/cypress/fixtures/element.png
new file mode 100644
index 0000000000..53ca7652b4
Binary files /dev/null and b/cypress/fixtures/element.png differ
diff --git a/playwright.config.ts b/playwright.config.ts
index 7ab3093ba6..40065b92c4 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -26,7 +26,10 @@ export default defineConfig({
ignoreHTTPSErrors: true,
video: "retain-on-failure",
baseURL,
- permissions: ["clipboard-write", "clipboard-read"],
+ permissions: ["clipboard-write", "clipboard-read", "microphone"],
+ launchOptions: {
+ args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
+ },
trace: "on-first-retry",
},
webServer: {
diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts
new file mode 100644
index 0000000000..5e1102c09b
--- /dev/null
+++ b/playwright/e2e/timeline/timeline.spec.ts
@@ -0,0 +1,1130 @@
+/*
+Copyright 2022 - 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 * as fs from "node:fs";
+
+import type { Locator, Page } from "@playwright/test";
+import type { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/matrix";
+import { test, expect } from "../../element-web-test";
+import { SettingLevel } from "../../../src/settings/SettingLevel";
+import { Layout } from "../../../src/settings/enums/Layout";
+import { Client } from "../../pages/client";
+import { ElementAppPage } from "../../pages/ElementAppPage";
+import { Bot } from "../../pages/bot";
+
+// The avatar size used in the timeline
+const AVATAR_SIZE = 30;
+// The resize method used in the timeline
+const AVATAR_RESIZE_METHOD = "crop";
+
+const ROOM_NAME = "Test room";
+const OLD_AVATAR = fs.readFileSync("cypress/fixtures/riot.png");
+const NEW_AVATAR = fs.readFileSync("cypress/fixtures/element.png");
+const OLD_NAME = "Alan";
+const NEW_NAME = "Alan (away)";
+
+const getEventTilesWithBodies = (page: Page): Locator => {
+ return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
+};
+
+const expectDisplayName = async (e: Locator, displayName: string): Promise => {
+ await expect(e.locator(".mx_DisambiguatedProfile_displayName")).toHaveText(displayName);
+};
+
+const expectAvatar = async (cli: Client, e: Locator, avatarUrl: string): Promise => {
+ const size = await e.page().evaluate((size) => size * window.devicePixelRatio, AVATAR_SIZE);
+ const url = await cli.evaluate(
+ (client, { avatarUrl, size, resizeMethod }) => {
+ // eslint-disable-next-line no-restricted-properties
+ return client.mxcUrlToHttp(avatarUrl, size, size, resizeMethod);
+ },
+ { avatarUrl, size, resizeMethod: AVATAR_RESIZE_METHOD },
+ );
+ await expect(e.locator(".mx_BaseAvatar img")).toHaveAttribute("src", url);
+};
+
+const sendEvent = async (client: Client, roomId: string, html = false): Promise => {
+ const content = {
+ msgtype: "m.text" as MsgType,
+ body: "Message",
+ format: undefined,
+ formatted_body: undefined,
+ };
+ if (html) {
+ content.format = "org.matrix.custom.html";
+ content.formatted_body = "Message";
+ }
+ return client.sendEvent(roomId, null, "m.room.message" as EventType, content);
+};
+
+test.describe("Timeline", () => {
+ test.use({
+ displayName: OLD_NAME,
+ room: async ({ app, user }, use) => {
+ const roomId = await app.client.createRoom({ name: ROOM_NAME });
+ await use({ roomId });
+ },
+ });
+
+ let oldAvatarUrl: string;
+ let newAvatarUrl: string;
+
+ test.describe("useOnlyCurrentProfiles", () => {
+ test.beforeEach(async ({ app, user }) => {
+ ({ content_uri: oldAvatarUrl } = await app.client.uploadContent(OLD_AVATAR, { type: "image/png" }));
+ await app.client.setAvatarUrl(oldAvatarUrl);
+ ({ content_uri: newAvatarUrl } = await app.client.uploadContent(NEW_AVATAR, { type: "image/png" }));
+ });
+
+ test("should show historical profiles if disabled", async ({ page, app, room }) => {
+ await app.settings.setValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, false);
+ await sendEvent(app.client, room.roomId);
+ await app.client.setDisplayName("Alan (away)");
+ await app.client.setAvatarUrl(newAvatarUrl);
+ // XXX: If we send the second event too quickly, there won't be
+ // enough time for the client to register the profile change
+ await page.waitForTimeout(500);
+ await sendEvent(app.client, room.roomId);
+ await app.viewRoomByName(ROOM_NAME);
+
+ const events = getEventTilesWithBodies(page);
+ await expect(events).toHaveCount(2);
+ await expectDisplayName(events.nth(0), OLD_NAME);
+ await expectAvatar(app.client, events.nth(0), oldAvatarUrl);
+ await expectDisplayName(events.nth(1), NEW_NAME);
+ await expectAvatar(app.client, events.nth(1), newAvatarUrl);
+ });
+
+ test("should not show historical profiles if enabled", async ({ page, app, room }) => {
+ await app.settings.setValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, true);
+ await sendEvent(app.client, room.roomId);
+ await app.client.setDisplayName(NEW_NAME);
+ await app.client.setAvatarUrl(newAvatarUrl);
+ // XXX: If we send the second event too quickly, there won't be
+ // enough time for the client to register the profile change
+ await page.waitForTimeout(500);
+ await sendEvent(app.client, room.roomId);
+ await app.viewRoomByName(ROOM_NAME);
+
+ const events = getEventTilesWithBodies(page);
+ await expect(events).toHaveCount(2);
+ for (const e of await events.all()) {
+ await expectDisplayName(e, NEW_NAME);
+ await expectAvatar(app.client, e, newAvatarUrl);
+ }
+ });
+ });
+
+ test.describe("configure room", () => {
+ test("should create and configure a room on IRC layout", async ({ page, app, room }) => {
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+ await expect(
+ page.locator(
+ ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary",
+ { hasText: `${OLD_NAME} created and configured the room.` },
+ ),
+ ).toBeVisible();
+
+ // wait for the date separator to appear to have a stable percy snapshot
+ await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today");
+
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png");
+ });
+
+ test("should have an expanded generic event list summary (GELS) on IRC layout", async ({ page, app, room }) => {
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+
+ // Wait until configuration is finished
+ await expect(
+ page.locator(
+ ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary",
+ { hasText: `${OLD_NAME} created and configured the room.` },
+ ),
+ ).toBeVisible();
+
+ const gels = page.locator(".mx_GenericEventListSummary");
+ // Click "expand" link button
+ await gels.getByRole("button", { name: "Expand" }).click();
+ // Assert that the "expand" link button worked
+ await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
+
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ });
+ });
+
+ test("should have an expanded generic event list summary (GELS) on compact modern/group layout", async ({
+ page,
+ app,
+ room,
+ }) => {
+ await page.goto(`/#/room/${room.roomId}`);
+
+ // Set compact modern layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+ await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
+
+ // Wait until configuration is finished
+ await expect(
+ page.locator(".mx_RoomView_body .mx_GenericEventListSummary[data-layout='group']", {
+ hasText: `${OLD_NAME} created and configured the room.`,
+ }),
+ ).toBeVisible();
+
+ const gels = page.locator(".mx_GenericEventListSummary");
+ // Click "expand" link button
+ await gels.getByRole("button", { name: "Expand" }).click();
+ // Assert that the "expand" link button worked
+ await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
+
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ });
+ });
+
+ test("should click 'collapse' on the first hovered info event line inside GELS on bubble layout", async ({
+ page,
+ app,
+ room,
+ }) => {
+ // This test checks clickability of the "Collapse" link button, which had been covered with
+ // MessageActionBar's safe area - https://github.com/vector-im/element-web/issues/22864
+
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
+ await expect(
+ page.locator(
+ ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='bubble'] .mx_GenericEventListSummary_summary",
+ { hasText: `${OLD_NAME} created and configured the room.` },
+ ),
+ ).toBeVisible();
+
+ const gels = page.locator(".mx_GenericEventListSummary");
+ // Click "expand" link button
+ await gels.getByRole("button", { name: "Expand" }).click();
+ // Assert that the "expand" link button worked
+ await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
+
+ // Make sure spacer is not visible on bubble layout
+ await expect(
+ page.locator(".mx_GenericEventListSummary[data-layout=bubble] .mx_GenericEventListSummary_spacer"),
+ ).not.toBeVisible(); // See: _GenericEventListSummary.pcss
+
+ // Save snapshot of expanded generic event list summary on bubble layout
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", {
+ // Exclude timestamp from snapshot
+ mask: [page.locator(".mx_MessageTimestamp")],
+ });
+
+ // Click "collapse" link button on the first hovered info event line
+ const firstTile = gels.locator(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type");
+ await firstTile.hover();
+ await expect(firstTile.getByRole("toolbar", { name: "Message Actions" })).toBeVisible();
+ await gels.getByRole("button", { name: "Collapse" }).click();
+
+ // Assert that "collapse" link button worked
+ await expect(gels.getByRole("button", { name: "Expand" })).toBeVisible();
+
+ // Save snapshot of collapsed generic event list summary on bubble layout
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ });
+ });
+
+ test("should add inline start margin to an event line on IRC layout", async ({
+ page,
+ app,
+ room,
+ axe,
+ checkA11y,
+ }) => {
+ axe.disableRules("color-contrast");
+
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+
+ // Wait until configuration is finished
+ await expect(
+ page.locator(
+ ".mx_RoomView_body .mx_GenericEventListSummary[data-layout='irc'] .mx_GenericEventListSummary_summary",
+ { hasText: `${OLD_NAME} created and configured the room.` },
+ ),
+ ).toBeVisible();
+
+ // Click "expand" link button
+ await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click();
+
+ // Check the event line has margin instead of inset property
+ // cf. _EventTile.pcss
+ // --EventTile_irc_line_info-margin-inline-start
+ // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding))
+ // = 80 + 14 + 5 = 99px
+
+ const firstEventLineIrc = page.locator(
+ ".mx_EventTile_info[data-layout=irc]:first-of-type .mx_EventTile_line",
+ );
+ await expect(firstEventLineIrc).toHaveCSS("margin-inline-start", "99px");
+ await expect(firstEventLineIrc).toHaveCSS("inset-inline-start", "0px");
+
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "event-line-inline-start-margin-irc-layout.png",
+ {
+ // Exclude timestamp and read marker from snapshot
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ },
+ );
+ await checkA11y();
+ });
+ });
+
+ test.describe("message displaying", () => {
+ const messageEdit = async (page: Page) => {
+ const line = page.locator(".mx_EventTile .mx_EventTile_line", { hasText: "Message" });
+ await line.hover();
+ await line.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }).click();
+ await page.getByRole("textbox", { name: "Edit message" }).pressSequentially("Edit");
+ await page.getByRole("textbox", { name: "Edit message" }).press("Enter");
+
+ // Assert that the edited message and the link button are found
+ // Regex patterns due to the edited date
+ await expect(
+ page.locator(".mx_EventTile .mx_EventTile_line", { hasText: "MessageEdit" }).getByRole("button", {
+ name: /Edited at .*? Click to view edits./,
+ }),
+ ).toBeVisible();
+ };
+
+ test("should align generic event list summary with messages and emote on IRC layout", async ({
+ page,
+ app,
+ room,
+ }) => {
+ // This test aims to check:
+ // 1. Alignment of collapsed GELS (generic event list summary) and messages
+ // 2. Alignment of expanded GELS and messages
+ // 3. Alignment of expanded GELS and placeholder of deleted message
+ // 4. Alignment of expanded GELS, placeholder of deleted message, and emote
+
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+
+ // Wait until configuration is finished
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(`${OLD_NAME} created and configured the room.`),
+ ).toBeVisible();
+
+ // Send messages
+ const composer = app.getComposerField();
+ await composer.fill("Hello Mr. Bot");
+ await composer.press("Enter");
+ await composer.fill("Hello again, Mr. Bot");
+ await composer.press("Enter");
+
+ // Make sure the second message was sent
+ await expect(
+ page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"),
+ ).toBeVisible();
+
+ // 1. Alignment of collapsed GELS (generic event list summary) and messages
+ // Check inline start spacing of collapsed GELS
+ // See: _EventTile.pcss
+ // .mx_GenericEventListSummary[data-layout="irc"] > .mx_EventTile_line
+ // = var(--name-width) + var(--icon-width) + var(--MessageTimestamp-width) + 2 * var(--right-padding)
+ // = 80 + 14 + 46 + 2 * 5
+ // = 150px
+ await expect(page.locator(".mx_GenericEventListSummary[data-layout=irc] > .mx_EventTile_line")).toHaveCSS(
+ "padding-inline-start",
+ "150px",
+ );
+ // Check width and spacing values of elements in .mx_EventTile, which should be equal to 150px
+ // --right-padding should be applied
+ for (const locator of await page.locator(".mx_EventTile > a").all()) {
+ if (await locator.isVisible()) {
+ await expect(locator).toHaveCSS("margin-right", "5px");
+ }
+ }
+ // --name-width width zero inline end margin should be applied
+ for (const locator of await page.locator(".mx_EventTile .mx_DisambiguatedProfile").all()) {
+ await expect(locator).toHaveCSS("width", "80px");
+ await expect(locator).toHaveCSS("margin-inline-end", "0px");
+ }
+ // --icon-width should be applied
+ for (const locator of await page.locator(".mx_EventTile .mx_EventTile_avatar > .mx_BaseAvatar").all()) {
+ await expect(locator).toHaveCSS("width", "14px");
+ }
+ // var(--MessageTimestamp-width) should be applied
+ for (const locator of await page.locator(".mx_EventTile > a").all()) {
+ await expect(locator).toHaveCSS("min-width", "46px");
+ }
+ // Record alignment of collapsed GELS and messages on messagePanel
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "collapsed-gels-and-messages-irc-layout.png",
+ {
+ // Exclude timestamp from snapshot of mx_MainSplit
+ mask: [page.locator(".mx_MessageTimestamp")],
+ },
+ );
+
+ // 2. Alignment of expanded GELS and messages
+ // Click "expand" link button
+ await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click();
+ // Check inline start spacing of info line on expanded GELS
+ // See: _EventTile.pcss
+ // --EventTile_irc_line_info-margin-inline-start
+ // = 80 + 14 + 1 * 5
+ await expect(
+ page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line"),
+ ).toHaveCSS("margin-inline-start", "99px");
+ // Record alignment of expanded GELS and messages on messagePanel
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-and-messages-irc-layout.png", {
+ // Exclude timestamp from snapshot of mx_MainSplit
+ mask: [page.locator(".mx_MessageTimestamp")],
+ });
+
+ // 3. Alignment of expanded GELS and placeholder of deleted message
+ // Delete the second (last) message
+ const lastTile = page.locator(".mx_RoomView_MessageList > .mx_EventTile_last");
+ await lastTile.hover();
+ await lastTile.getByRole("button", { name: "Options" }).click();
+ await page.getByRole("menuitem", { name: "Remove" }).click();
+ // Confirm deletion
+ await page.locator(".mx_Dialog_buttons").getByRole("button", { name: "Remove" }).click();
+ // Make sure the dialog was closed and the second (last) message was redacted
+ await expect(page.locator(".mx_Dialog")).not.toBeVisible();
+ await expect(page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_RedactedBody")).toBeVisible();
+ await expect(
+ page.locator(".mx_GenericEventListSummary .mx_EventTile_last .mx_EventTile_receiptSent"),
+ ).toBeVisible();
+ // Record alignment of expanded GELS and placeholder of deleted message on messagePanel
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-redaction-placeholder.png", {
+ // Exclude timestamp from snapshot of mx_MainSplit
+ mask: [page.locator(".mx_MessageTimestamp")],
+ });
+
+ // 4. Alignment of expanded GELS, placeholder of deleted message, and emote
+ // Send a emote
+ await page
+ .locator(".mx_RoomView_body")
+ .getByRole("textbox", { name: "Send a message…" })
+ .fill("/me says hello to Mr. Bot");
+ await page.locator(".mx_RoomView_body").getByRole("textbox", { name: "Send a message…" }).press("Enter");
+ // Check inline start margin of its avatar
+ // Here --right-padding is for the avatar on the message line
+ // See: _IRCLayout.pcss
+ // .mx_IRCLayout .mx_EventTile_emote .mx_EventTile_avatar
+ // = calc(var(--name-width) + var(--icon-width) + 1 * var(--right-padding))
+ // = 80 + 14 + 1 * 5
+ await expect(page.locator(".mx_EventTile_emote .mx_EventTile_avatar")).toHaveCSS("margin-left", "99px");
+ // Make sure emote was sent
+ await expect(page.locator(".mx_EventTile_last.mx_EventTile_emote .mx_EventTile_receiptSent")).toBeVisible();
+ // Record alignment of expanded GELS, placeholder of deleted message, and emote
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", {
+ // Exclude timestamp from snapshot of mx_MainSplit
+ mask: [page.locator(".mx_MessageTimestamp")],
+ });
+ });
+
+ test("should render EventTiles on IRC, modern (group), and bubble layout", async ({ page, app, room }) => {
+ const screenshotOptions = {
+ // Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ };
+
+ await sendEvent(app.client, room.roomId);
+ await sendEvent(app.client, room.roomId); // check continuation
+ await sendEvent(app.client, room.roomId); // check the last EventTile
+
+ await page.goto(`/#/room/${room.roomId}`);
+ const composer = app.getComposerField();
+ // Send a plain text message
+ await composer.fill("Hello");
+ await composer.press("Enter");
+ // Send a big emoji
+ await composer.fill("🏀");
+ await composer.press("Enter");
+ // Send an inline emoji
+ await composer.fill("This message has an inline emoji 👒");
+ await composer.press("Enter");
+
+ await expect(page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒")).toBeVisible();
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // IRC layout
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+
+ // Wait until configuration is finished
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(`${OLD_NAME} created and configured the room.`),
+ ).toBeVisible();
+
+ await app.scrollToBottom(page);
+ await expect(
+ page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"),
+ ).toBeInViewport();
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "event-tiles-irc-layout.png",
+ screenshotOptions,
+ );
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Group/modern layout
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+
+ // Check that the last EventTile is rendered
+ await app.scrollToBottom(page);
+ await expect(
+ page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"),
+ ).toBeInViewport();
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "event-tiles-modern-layout.png",
+ screenshotOptions,
+ );
+
+ // Check the same thing for compact layout
+ await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
+
+ // Check that the last EventTile is rendered
+ await app.scrollToBottom(page);
+ await expect(
+ page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"),
+ ).toBeInViewport();
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "event-tiles-compact-modern-layout.png",
+ screenshotOptions,
+ );
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Message bubble layout
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
+
+ await app.scrollToBottom(page);
+ await expect(
+ page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"),
+ ).toBeInViewport();
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "event-tiles-bubble-layout.png",
+ screenshotOptions,
+ );
+ });
+
+ test("should set inline start padding to a hidden event line", async ({ page, app, room }) => {
+ await sendEvent(app.client, room.roomId);
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(`${OLD_NAME} created and configured the room.`),
+ ).toBeVisible();
+
+ // Edit message
+ await messageEdit(page);
+
+ // Click timestamp to highlight hidden event line
+ await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
+
+ // should not add inline start padding to a hidden event line on IRC layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+ await expect(
+ page.locator(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").first(),
+ ).toHaveCSS("padding-inline-start", "0px");
+
+ // Exclude timestamp and read marker from snapshot
+ const screenshotOptions = {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ };
+
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "hidden-event-line-zero-padding-irc-layout.png",
+ screenshotOptions,
+ );
+
+ // should add inline start padding to a hidden event line on modern layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+ // calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
+ await expect(
+ page.locator(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line").first(),
+ ).toHaveCSS("padding-inline-start", "84px");
+
+ await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
+ "hidden-event-line-padding-modern-layout.png",
+ screenshotOptions,
+ );
+ });
+
+ test("should click view source event toggle", async ({ page, app, room }) => {
+ // This test checks:
+ // 1. clickability of top left of view source event toggle
+ // 2. clickability of view source toggle on IRC layout
+
+ // Exclude timestamp from snapshot
+ const screenshotOptions = {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ };
+
+ await sendEvent(app.client, room.roomId);
+ await page.goto(`/#/room/${room.roomId}`);
+ await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(OLD_NAME + " created and configured the room."),
+ ).toBeVisible();
+
+ // Edit message
+ await messageEdit(page);
+
+ // 1. clickability of top left of view source event toggle
+
+ // Click top left of the event toggle, which should not be covered by MessageActionBar's safe area
+ const viewSourceEventGroup = page.locator(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent");
+ await viewSourceEventGroup.hover();
+ await viewSourceEventGroup
+ .getByRole("button", { name: "toggle event" })
+ .click({ position: { x: 0, y: 0 } });
+
+ // Make sure the expand toggle works
+ const viewSourceEventExpanded = page.locator(
+ ".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded",
+ );
+ await viewSourceEventExpanded.hover();
+ const toggleEventButton = viewSourceEventExpanded.getByRole("button", { name: "toggle event" });
+ // Check size and position of toggle on expanded view source event
+ // See: _ViewSourceEvent.pcss
+ await expect(toggleEventButton).toHaveCSS("height", "12px"); // --ViewSourceEvent_toggle-size
+ await expect(toggleEventButton).toHaveCSS("align-self", "flex-end");
+ // Click again to collapse the source
+ await toggleEventButton.click({ position: { x: 0, y: 0 } });
+
+ // Make sure the collapse toggle works
+ await expect(
+ page.locator(".mx_EventTile_last[data-layout=group] .mx_ViewSourceEvent_expanded"),
+ ).not.toBeVisible();
+
+ // 2. clickability of view source toggle on IRC layout
+
+ // Enable IRC layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+
+ // Hover the view source toggle on IRC layout
+ const viewSourceEventIrc = page.locator(
+ ".mx_GenericEventListSummary[data-layout=irc] .mx_EventTile .mx_ViewSourceEvent",
+ );
+ await viewSourceEventIrc.hover();
+ await expect(viewSourceEventIrc).toMatchScreenshot(
+ "hovered-hidden-event-line-irc-layout.png",
+ screenshotOptions,
+ );
+
+ // Click view source event toggle
+ await viewSourceEventIrc.getByRole("button", { name: "toggle event" }).click({ position: { x: 0, y: 0 } });
+
+ // Make sure the expand toggle worked
+ await expect(page.locator(".mx_EventTile[data-layout=irc] .mx_ViewSourceEvent_expanded")).toBeVisible();
+ });
+
+ test("should render file size in kibibytes on a file tile", async ({ page, room }) => {
+ await page.goto(`/#/room/${room.roomId}`);
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(OLD_NAME + " created and configured the room."),
+ ).toBeVisible();
+
+ // Upload a file from the message composer
+ await page
+ .locator(".mx_MessageComposer_actions input[type='file']")
+ .setInputFiles("cypress/fixtures/matrix-org-client-versions.json");
+
+ // Click "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();
+
+ // Assert that the file size is displayed in kibibytes (1024 bytes), not kilobytes (1000 bytes)
+ // See: https://github.com/vector-im/element-web/issues/24866
+ await expect(
+ page.locator(".mx_EventTile_last .mx_MFileBody_info_filename").getByText(/1.12 KB/),
+ ).toBeVisible();
+ });
+
+ test("should render url previews", async ({ page, app, room, axe, checkA11y }) => {
+ axe.disableRules("color-contrast");
+
+ await page.route(
+ "**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
+ async (route) => {
+ await route.fulfill({
+ path: "cypress/fixtures/riot.png",
+ });
+ },
+ );
+ await page.route(
+ "**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
+ async (route) => {
+ await route.fulfill({
+ json: {
+ "og:title": "Element Call",
+ "og:description": null,
+ "og:image:width": 48,
+ "og:image:height": 48,
+ "og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
+ "og:image:type": "image/png",
+ "matrix:image:size": 2121,
+ },
+ });
+ },
+ );
+
+ const requestPromises: Promise[] = [
+ page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
+ page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
+ ];
+
+ await app.client.sendMessage(room.roomId, "https://call.element.io/");
+ await page.goto(`/#/room/${room.roomId}`);
+
+ await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
+ await Promise.all(requestPromises);
+
+ await checkA11y();
+
+ await app.scrollToBottom(page);
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
+ // Exclude timestamp and read marker from snapshot
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ });
+ });
+
+ test.describe("on search results panel", () => {
+ test("should highlight search result words regardless of formatting", async ({ page, app, room }) => {
+ await sendEvent(app.client, room.roomId);
+ await sendEvent(app.client, room.roomId, true);
+ await page.goto(`/#/room/${room.roomId}`);
+
+ await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
+
+ await expect(page.locator(".mx_SearchBar")).toMatchScreenshot("search-bar-on-timeline.png");
+
+ await page.locator(".mx_SearchBar_input").getByRole("textbox").fill("Message");
+ await page.locator(".mx_SearchBar_input").getByRole("textbox").press("Enter");
+
+ for (const locator of await page
+ .locator(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight")
+ .all()) {
+ await expect(locator).toBeVisible();
+ }
+ await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot(
+ "highlighted-search-results.png",
+ );
+ });
+
+ test("should render a fully opaque textual event", async ({ page, app, room }) => {
+ const stringToSearch = "Message"; // Same with string sent with sendEvent()
+
+ await sendEvent(app.client, room.roomId);
+
+ await page.goto(`/#/room/${room.roomId}`);
+
+ // Open a room setting dialog
+ await page.getByRole("button", { name: "Room options" }).click();
+ await page.getByRole("menuitem", { name: "Settings" }).click();
+
+ // Set a room topic to render a TextualEvent
+ await page.getByRole("textbox", { name: "Room Topic" }).type(`This is a room for ${stringToSearch}.`);
+ await page.getByRole("button", { name: "Save" }).click();
+
+ await app.closeDialog();
+
+ // Assert that the TextualEvent is rendered
+ await expect(
+ page.getByText(`${OLD_NAME} changed the topic to "This is a room for ${stringToSearch}.".`),
+ ).toHaveClass(/mx_TextualEvent/);
+
+ // Display the room search bar
+ await page.locator(".mx_LegacyRoomHeader").getByRole("button", { name: "Search" }).click();
+
+ // Search the string to display both the message and TextualEvent on search results panel
+ await page.locator(".mx_SearchBar").getByRole("textbox").fill(stringToSearch);
+ await page.locator(".mx_SearchBar").getByRole("textbox").press("Enter");
+
+ // On search results panel
+ const resultsPanel = page.locator(".mx_RoomView_searchResultsPanel");
+ // Assert that contextual event tiles are translucent
+ for (const locator of await resultsPanel.locator(".mx_EventTile.mx_EventTile_contextual").all()) {
+ await expect(locator).toHaveCSS("opacity", "0.4");
+ }
+ // Assert that the TextualEvent is fully opaque (visually solid).
+ for (const locator of await resultsPanel.locator(".mx_EventTile .mx_TextualEvent").all()) {
+ await expect(locator).toHaveCSS("opacity", "1");
+ }
+
+ await expect(page.locator(".mx_RoomView_searchResultsPanel")).toMatchScreenshot(
+ "search-results-with-TextualEvent.png",
+ );
+ });
+ });
+ });
+
+ test.describe("message sending", () => {
+ const MESSAGE = "Hello world";
+ const reply = "Reply";
+ const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => {
+ // View room
+ await page.goto(`/#/room/${roomId}`);
+
+ // Send a message
+ const composer = app.getComposerField();
+ await composer.fill(MESSAGE);
+ await composer.press("Enter");
+
+ // Reply to the message
+ const lastTile = page.locator(".mx_EventTile_last");
+ await expect(lastTile.getByText(MESSAGE)).toBeVisible();
+ await lastTile.hover();
+ await lastTile.getByRole("button", { name: "Reply", exact: true }).click();
+ };
+
+ // For clicking the reply button on the last line
+ const clickButtonReply = async (page: Page): Promise => {
+ const lastTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
+ await lastTile.hover();
+ await lastTile.getByRole("button", { name: "Reply", exact: true }).click();
+ };
+
+ test("can reply with a text message", async ({ page, app, room }) => {
+ await viewRoomSendMessageAndSetupReply(page, app, room.roomId);
+
+ await app.getComposerField().fill(reply);
+ await app.getComposerField().press("Enter");
+
+ const eventTileLine = page.locator(".mx_RoomView_body .mx_EventTile_last .mx_EventTile_line");
+ await expect(eventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(MESSAGE)).toBeVisible();
+ await expect(eventTileLine.getByText(reply)).toHaveCount(1);
+ });
+
+ test("can reply with a voice message", async ({ page, app, room, context }) => {
+ await context.grantPermissions(["microphone"]);
+ await viewRoomSendMessageAndSetupReply(page, app, room.roomId);
+
+ const composerOptions = await app.openMessageComposerOptions();
+ await composerOptions.getByRole("menuitem", { name: "Voice Message" }).click();
+
+ // Record an empty message
+ await page.waitForTimeout(3000);
+
+ const roomViewBody = page.locator(".mx_RoomView_body");
+ await roomViewBody
+ .locator(".mx_MessageComposer")
+ .getByRole("button", { name: "Send voice message" })
+ .click();
+
+ const lastEventTileLine = roomViewBody.locator(".mx_EventTile_last .mx_EventTile_line");
+ await expect(lastEventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(MESSAGE)).toBeVisible();
+
+ await expect(lastEventTileLine.locator(".mx_MVoiceMessageBody")).toHaveCount(1);
+ });
+
+ test("should not be possible to send flag with regional emojis", async ({ page, app, room }) => {
+ await page.goto(`/#/room/${room.roomId}`);
+
+ // Send a message
+ await app.getComposerField().pressSequentially(":regional_indicator_a");
+ await page.locator(".mx_Autocomplete_Completion_title", { hasText: ":regional_indicator_a:" }).click();
+ await app.getComposerField().pressSequentially(":regional_indicator_r");
+ await page.locator(".mx_Autocomplete_Completion_title", { hasText: ":regional_indicator_r:" }).click();
+ await app.getComposerField().pressSequentially(" :regional_indicator_z");
+ await page.locator(".mx_Autocomplete_Completion_title", { hasText: ":regional_indicator_z:" }).click();
+ await app.getComposerField().pressSequentially(":regional_indicator_a");
+ await page.locator(".mx_Autocomplete_Completion_title", { hasText: ":regional_indicator_a:" }).click();
+ await app.getComposerField().press("Enter");
+
+ await expect(
+ page.locator(
+ ".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_MTextBody .mx_EventTile_bigEmoji > *",
+ ),
+ ).toHaveCount(4);
+ });
+
+ test("should display a reply chain", async ({ page, app, room, homeserver }) => {
+ const reply2 = "Reply again";
+
+ await page.goto(`/#/room/${room.roomId}`);
+
+ // Wait until configuration is finished
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(OLD_NAME + " created and configured the room."),
+ ).toBeVisible();
+
+ // Create a bot "BotBob" and invite it
+ const bot = new Bot(page, homeserver, {
+ displayName: "BotBob",
+ autoAcceptInvites: false,
+ });
+ await bot.prepareClient();
+ await app.client.inviteUser(room.roomId, bot.credentials.userId);
+ await bot.joinRoom(room.roomId);
+
+ // Make sure the bot joined the room
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary .mx_EventTile_info.mx_EventTile_last")
+ .getByText("BotBob joined the room"),
+ ).toBeVisible();
+
+ // Have bot send MESSAGE to roomId
+ await bot.sendMessage(room.roomId, MESSAGE);
+
+ // Assert that MESSAGE is found
+ await expect(page.getByText(MESSAGE)).toBeVisible();
+
+ // Reply to the message
+ await clickButtonReply(page);
+ await app.getComposerField().fill(reply);
+ await app.getComposerField().press("Enter");
+
+ // Make sure 'reply' was sent
+ await expect(page.locator(".mx_RoomView_body .mx_EventTile_last").getByText(reply)).toBeVisible();
+
+ // Reply again to create a replyChain
+ await clickButtonReply(page);
+ await app.getComposerField().fill(reply2);
+ await app.getComposerField().press("Enter");
+
+ // Assert that 'reply2' was sent
+ await expect(page.locator(".mx_RoomView_body .mx_EventTile_last").getByText(reply2)).toBeVisible();
+
+ await expect(page.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible();
+
+ // Exclude timestamp and read marker from snapshot
+ const screenshotOptions = {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ };
+
+ // Check the margin value of ReplyChains of EventTile at the bottom on IRC layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+ for (const locator of await page.locator(".mx_EventTile_last[data-layout='irc'] .mx_ReplyChain").all()) {
+ await expect(locator).toHaveCSS("margin", "0px");
+ }
+
+ // Take a snapshot on IRC layout
+ // Note that because zero margin is applied to mx_ReplyChain, the left borders of two mx_ReplyChain
+ // components may seem to be connected to one.
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ "event-tile-reply-chains-irc-layout.png",
+ screenshotOptions,
+ );
+
+ // Check the margin value of ReplyChains of EventTile at the bottom on group/modern layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+ for (const locator of await page.locator(".mx_EventTile_last[data-layout='group'] .mx_ReplyChain").all()) {
+ await expect(locator).toHaveCSS("margin-bottom", "8px");
+ }
+
+ // Take a snapshot on modern layout
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ "event-tile-reply-chains-irc-modern.png",
+ screenshotOptions,
+ );
+
+ // Check the margin value of ReplyChains of EventTile at the bottom on group/modern compact layout
+ await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true);
+ for (const locator of await page.locator(".mx_EventTile_last[data-layout='group'] .mx_ReplyChain").all()) {
+ await expect(locator).toHaveCSS("margin-bottom", "4px");
+ }
+
+ // Take a snapshot on compact modern layout
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ "event-tile-reply-chains-compact-modern-layout.png",
+ screenshotOptions,
+ );
+
+ // Check the margin value of ReplyChains of EventTile at the bottom on bubble layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
+ for (const locator of await page.locator(".mx_EventTile_last[data-layout='bubble'] .mx_ReplyChain").all()) {
+ await expect(locator).toHaveCSS("margin-bottom", "8px");
+ }
+
+ // Take a snapshot on bubble layout
+ await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot(
+ "event-tile-reply-chains-bubble-layout.png",
+ screenshotOptions,
+ );
+ });
+
+ test("should send, reply, and display long strings without overflowing", async ({
+ page,
+ app,
+ room,
+ homeserver,
+ }) => {
+ // Max 256 characters for display name
+ const LONG_STRING =
+ "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut " +
+ "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
+ "aliquip";
+
+ // Create a bot with a long display name
+ const bot = new Bot(page, homeserver, {
+ displayName: LONG_STRING,
+ autoAcceptInvites: false,
+ });
+ await bot.prepareClient();
+
+ // Create another room with a long name, invite the bot, and open the room
+ const testRoomId = await app.client.createRoom({ name: LONG_STRING });
+ await app.client.inviteUser(testRoomId, bot.credentials.userId);
+ await bot.joinRoom(testRoomId);
+ await page.goto(`/#/room/${testRoomId}`);
+
+ // Wait until configuration is finished
+ await expect(
+ page
+ .locator(".mx_GenericEventListSummary_summary")
+ .getByText(OLD_NAME + " created and configured the room."),
+ ).toBeVisible();
+
+ // Set the display name to "LONG_STRING 2" in order to avoid a warning in Percy tests from being triggered
+ // due to the generated random mxid being displayed inside the GELS summary.
+ await app.client.setDisplayName(`${LONG_STRING} 2`);
+
+ // Have the bot send a long message
+ await bot.sendMessage(testRoomId, {
+ body: LONG_STRING,
+ msgtype: "m.text",
+ });
+
+ // Wait until the message is rendered
+ await expect(
+ page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText(LONG_STRING),
+ ).toBeVisible();
+
+ // Reply to the message
+ await clickButtonReply(page);
+ await app.getComposerField().fill(reply);
+ await app.getComposerField().press("Enter");
+
+ // Make sure the reply tile is rendered
+ const eventTileLine = page.locator(".mx_EventTile_last .mx_EventTile_line");
+ await expect(eventTileLine.locator(".mx_ReplyTile .mx_MTextBody").getByText(LONG_STRING)).toBeVisible();
+
+ await expect(eventTileLine.getByText(reply)).toHaveCount(1);
+
+ // Change the viewport size
+ await page.setViewportSize({ width: 1600, height: 1200 });
+
+ // Exclude timestamp and read marker from snapshot
+ const screenshotOptions = {
+ mask: [page.locator(".mx_MessageTimestamp")],
+ css: `
+ .mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
+ display: none !important;
+ }
+ `,
+ };
+
+ // Make sure the strings do not overflow on IRC layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
+ // Scroll to the bottom to have Percy take a snapshot of the whole viewport
+ await app.scrollToBottom(page);
+ // Assert that both avatar in the introduction and the last message are visible at the same time
+ await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible();
+ const lastEventTileIrc = page.locator(".mx_EventTile_last[data-layout='irc']");
+ await expect(lastEventTileIrc.locator(".mx_MTextBody").first()).toBeVisible();
+ await expect(lastEventTileIrc.locator(".mx_EventTile_receiptSent")).toBeVisible(); // rendered at the bottom of EventTile
+ // Take a snapshot in IRC layout
+ await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot(
+ "long-strings-with-reply-irc-layout.png",
+ screenshotOptions,
+ );
+
+ // Make sure the strings do not overflow on modern layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
+ await app.scrollToBottom(page); // Scroll again in case
+ await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible();
+ const lastEventTileGroup = page.locator(".mx_EventTile_last[data-layout='group']");
+ await expect(lastEventTileGroup.locator(".mx_MTextBody").first()).toBeVisible();
+ await expect(lastEventTileGroup.locator(".mx_EventTile_receiptSent")).toBeVisible();
+ await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot(
+ "long-strings-with-reply-modern-layout.png",
+ screenshotOptions,
+ );
+
+ // Make sure the strings do not overflow on bubble layout
+ await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
+ await app.scrollToBottom(page); // Scroll again in case
+ await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible();
+ const lastEventTileBubble = page.locator(".mx_EventTile_last[data-layout='bubble']");
+ await expect(lastEventTileBubble.locator(".mx_MTextBody").first()).toBeVisible();
+ await expect(lastEventTileBubble.locator(".mx_EventTile_receiptSent")).toBeVisible();
+ await expect(page.locator(".mx_ScrollPanel")).toMatchScreenshot(
+ "long-strings-with-reply-bubble-layout.png",
+ screenshotOptions,
+ );
+ });
+ });
+});
diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts
index ecea296332..cb8638ebbf 100644
--- a/playwright/element-web-test.ts
+++ b/playwright/element-web-test.ts
@@ -250,6 +250,10 @@ export const expect = baseExpect.extend({
.mx_ReplyChain {
border-left-color: var(--cpd-color-blue-1200) !important;
}
+ /* Use monospace font for timestamp for consistent mask width */
+ .mx_MessageTimestamp {
+ font-family: Inconsolata !important;
+ }
${options?.css ?? ""}
`,
})) as ElementHandle;
diff --git a/playwright/pages/ElementAppPage.ts b/playwright/pages/ElementAppPage.ts
index 4e065a4b17..8bc0f5ae0e 100644
--- a/playwright/pages/ElementAppPage.ts
+++ b/playwright/pages/ElementAppPage.ts
@@ -105,6 +105,14 @@ export class ElementAppPage {
return this.page.locator(`${panelClass} .mx_MessageComposer`);
}
+ /**
+ * Get the composer input field
+ * @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
+ */
+ public getComposerField(isRightPanel?: boolean): Locator {
+ return this.getComposer(isRightPanel).locator("[contenteditable]");
+ }
+
/**
* Open the message composer kebab menu
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
@@ -155,4 +163,10 @@ export class ElementAppPage {
await spotlight.open();
return spotlight;
}
+
+ public async scrollToBottom(page: Page): Promise {
+ await page
+ .locator(".mx_ScrollPanel")
+ .evaluate((scrollPanel) => scrollPanel.scrollTo(0, scrollPanel.scrollHeight));
+ }
}
diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts
index 6c56cb90d3..a197f09333 100644
--- a/playwright/pages/client.ts
+++ b/playwright/pages/client.ts
@@ -28,6 +28,8 @@ import type {
IRoomDirectoryOptions,
KnockRoomOpts,
Visibility,
+ UploadOpts,
+ Upload,
} from "matrix-js-sdk/src/matrix";
import { Credentials } from "../plugins/homeserver";
@@ -293,6 +295,46 @@ export class Client {
}, options);
}
+ /**
+ * @param {string} name
+ * @param {module:client.callback} callback Optional.
+ * @return {Promise} Resolves: {} an empty object.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+ public async setDisplayName(name: string): Promise<{}> {
+ const client = await this.prepareClient();
+ return client.evaluate(async (cli: MatrixClient, name) => cli.setDisplayName(name), name);
+ }
+
+ /**
+ * @param {string} url
+ * @param {module:client.callback} callback Optional.
+ * @return {Promise} Resolves: {} an empty object.
+ * @return {module:http-api.MatrixError} Rejects: with an error response.
+ */
+ public async setAvatarUrl(url: string): Promise<{}> {
+ const client = await this.prepareClient();
+ return client.evaluate(async (cli: MatrixClient, url) => cli.setAvatarUrl(url), url);
+ }
+
+ /**
+ * Upload a file to the media repository on the homeserver.
+ *
+ * @param {object} file The object to upload. On a browser, something that
+ * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
+ * a Buffer, String or ReadStream.
+ */
+ public async uploadContent(file: Buffer, opts?: UploadOpts): Promise> {
+ const client = await this.prepareClient();
+ return client.evaluate(
+ async (cli: MatrixClient, { file, opts }) => cli.uploadContent(new Uint8Array(file), opts),
+ {
+ file: [...file],
+ opts,
+ },
+ );
+ }
+
/**
* Boostraps cross-signing.
*/
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png
new file mode 100644
index 0000000000..db736e2fe5
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-and-messages-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png
new file mode 100644
index 0000000000..d4f2492a1a
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/collapsed-gels-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png
new file mode 100644
index 0000000000..a954d9a007
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/configured-room-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png
new file mode 100644
index 0000000000..dfe188636c
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-line-inline-start-margin-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png
new file mode 100644
index 0000000000..3a0da67d18
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png
new file mode 100644
index 0000000000..cbf9c2927a
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-compact-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png
new file mode 100644
index 0000000000..b7ec691552
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png
new file mode 100644
index 0000000000..ed65bbc63d
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tile-reply-chains-irc-modern-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png
new file mode 100644
index 0000000000..3d684c73cb
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png
new file mode 100644
index 0000000000..dc5484c77c
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-compact-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png
new file mode 100644
index 0000000000..a5e26c3e69
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png
new file mode 100644
index 0000000000..a8587e7604
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/event-tiles-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png
new file mode 100644
index 0000000000..c16ff4480a
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-and-messages-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png
new file mode 100644
index 0000000000..f8b0504ec9
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png
new file mode 100644
index 0000000000..4d4972d9f3
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-emote-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png
new file mode 100644
index 0000000000..dfe188636c
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png
new file mode 100644
index 0000000000..90db42b411
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png
new file mode 100644
index 0000000000..791a1f93a2
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/expanded-gels-redaction-placeholder-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png
new file mode 100644
index 0000000000..b4ab81ebe6
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-padding-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png
new file mode 100644
index 0000000000..8c93664911
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/hidden-event-line-zero-padding-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png
new file mode 100644
index 0000000000..ff75b3473f
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/highlighted-search-results-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png
new file mode 100644
index 0000000000..100dc86c7a
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/hovered-hidden-event-line-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png
new file mode 100644
index 0000000000..8d6e089834
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-bubble-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png
new file mode 100644
index 0000000000..a7c7ecc5b8
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-irc-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png
new file mode 100644
index 0000000000..f51b8eb332
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/long-strings-with-reply-modern-layout-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png
new file mode 100644
index 0000000000..64d44a9778
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/search-bar-on-timeline-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png
new file mode 100644
index 0000000000..4c553cfdaf
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/search-results-with-TextualEvent-linux.png differ
diff --git a/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png
new file mode 100644
index 0000000000..49f2c0bad8
Binary files /dev/null and b/playwright/snapshots/timeline/timeline.spec.ts/url-preview-linux.png differ