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