diff --git a/cypress/e2e/threads/threads.spec.ts b/cypress/e2e/threads/threads.spec.ts deleted file mode 100644 index 2192447e0b..0000000000 --- a/cypress/e2e/threads/threads.spec.ts +++ /dev/null @@ -1,515 +0,0 @@ -/* -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 { HomeserverInstance } from "../../plugins/utils/homeserver"; -import { MatrixClient } from "../../global"; -import { SettingLevel } from "../../../src/settings/SettingLevel"; -import { Layout } from "../../../src/settings/enums/Layout"; -import Chainable = Cypress.Chainable; - -describe("Threads", () => { - let homeserver: HomeserverInstance; - - beforeEach(() => { - cy.window().then((win) => { - win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests - }); - cy.startHomeserver("default").then((data) => { - homeserver = data; - cy.initTestUser(homeserver, "Tom"); - }); - }); - - afterEach(() => { - cy.stopHomeserver(homeserver); - }); - - // Flaky: https://github.com/vector-im/element-web/issues/26452 - it.skip("should be usable for a conversation", () => { - let bot: MatrixClient; - cy.getBot(homeserver, { - displayName: "BotBob", - autoAcceptInvites: false, - }).then((_bot) => { - bot = _bot; - }); - - let roomId: string; - cy.createRoom({}) - .then((_roomId) => { - roomId = _roomId; - return cy.inviteUser(roomId, bot.getUserId()); - }) - .then(async () => { - await bot.joinRoom(roomId); - cy.visit("/#/room/" + roomId); - }); - - // Around 200 characters - const MessageLong = - "Hello there. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt " + - "ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi"; - - const ThreadViewGroupSpacingStart = "56px"; // --ThreadView_group_spacing-start - // Exclude timestamp and read marker from snapshots - const percyCSS = ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker { visibility: hidden !important; }"; - - cy.get(".mx_RoomView_body").within(() => { - // User sends message - cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - - // Wait for message to send, get its ID and save as @threadId - cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .invoke("attr", "data-scroll-tokens") - .as("threadId"); - }); - - // Bot starts thread - cy.get("@threadId").then((threadId) => { - bot.sendMessage(roomId, threadId, { - // Send a message long enough to be wrapped to check if avatars inside the ReadReceiptGroup are visible - body: MessageLong, - msgtype: "m.text", - }); - }); - - // User asserts timeline thread summary visible & clicks it - cy.get(".mx_RoomView_body .mx_ThreadSummary") - .within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText(MessageLong).should("exist"); - }) - .click(); - - // Wait until the both messages are read - cy.get(".mx_ThreadView .mx_EventTile_last[data-layout=group]").within(() => { - cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); - - // Make sure the CSS style for spacing is applied to mx_EventTile_line on group/modern layout - cy.get(".mx_EventTile_line").should("have.css", "padding-inline-start", ThreadViewGroupSpacingStart); - }); - - // Take Percy snapshots in group layout and bubble layout (IRC layout is not available on ThreadView) - cy.get(".mx_ThreadView").percySnapshotElement("Initial ThreadView on group layout", { percyCSS }); - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble']").should("be.visible"); - cy.get(".mx_ThreadView").percySnapshotElement("Initial ThreadView on bubble layout", { percyCSS }); - - // Set the group layout - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); - - cy.get(".mx_ThreadView .mx_EventTile[data-layout='group'].mx_EventTile_last").within(() => { - // Wait until the messages are rendered - cy.get(".mx_EventTile_line .mx_MTextBody").findByText(MessageLong).should("exist"); - - // Make sure the avatar inside ReadReceiptGroup is visible on the group layout - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); - }); - - // Enable the bubble layout - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - - cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble'].mx_EventTile_last").within(() => { - // TODO: remove this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout - // See: https://github.com/vector-im/element-web/issues/23569 - cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("exist"); - - // Make sure the avatar inside ReadReceiptGroup is visible on bubble layout - // TODO: enable this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout - // See: https://github.com/vector-im/element-web/issues/23569 - // cy.get(".mx_ReadReceiptGroup .mx_BaseAvatar").should("be.visible"); - }); - - // Re-enable the group layout - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); - - cy.get(".mx_ThreadView").within(() => { - // User responds in thread - cy.findByRole("textbox", { name: "Send a message…" }).type("Test{enter}"); - }); - - // User asserts summary was updated correctly - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("Tom").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("Test").should("exist"); - }); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Check reactions and hidden events - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - // Enable hidden events to make the event for reaction displayed - cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); - - // User reacts to message instead - cy.get(".mx_ThreadView").within(() => { - cy.contains(".mx_EventTile .mx_EventTile_line", "Hello there") - .realHover() - .findByRole("toolbar", { name: "Message Actions" }) - .findByRole("button", { name: "React" }) - .click(); - }); - - cy.get(".mx_EmojiPicker").within(() => { - cy.findByRole("textbox").type("wave"); - cy.findByRole("gridcell", { name: "👋" }).click(); - }); - - cy.get(".mx_ThreadView").within(() => { - // Make sure the CSS style for spacing is applied to mx_ReactionsRow on group/modern layout - cy.get(".mx_EventTile[data-layout=group] .mx_ReactionsRow").should( - "have.css", - "margin-inline-start", - ThreadViewGroupSpacingStart, - ); - - // Make sure the CSS style for spacing is applied to the hidden event on group/modern layout - cy.get( - ".mx_GenericEventListSummary[data-layout=group] .mx_EventTile_info.mx_EventTile_last " + - ".mx_EventTile_line", - ).should("have.css", "padding-inline-start", ThreadViewGroupSpacingStart); - }); - - // Take Percy snapshot of group layout (IRC layout is not available on ThreadView) - cy.get(".mx_ThreadView").percySnapshotElement("ThreadView with reaction and a hidden event on group layout", { - percyCSS, - }); - - // Enable bubble layout - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - - // Make sure the CSS style for spacing is applied to the hidden event on bubble layout - cy.get( - ".mx_ThreadView .mx_GenericEventListSummary[data-layout=bubble] .mx_EventTile_info.mx_EventTile_last", - ).within(() => { - cy.get(".mx_EventTile_line .mx_EventTile_content") - // 76px: ThreadViewGroupSpacingStart + 14px + 6px - // 14px: avatar width - // See: _EventTile.pcss - .should("have.css", "margin-inline-start", "76px"); - cy.get(".mx_EventTile_line") - // Make sure the margin is NOT applied to mx_EventTile_line - .should("have.css", "margin-inline-start", "0px"); - }); - - // Take Percy snapshot of bubble layout - cy.get(".mx_ThreadView").percySnapshotElement("ThreadView with reaction and a hidden event on bubble layout", { - percyCSS, - }); - - // Disable hidden events - cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, false); - - // Reset to the group layout - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Check redactions - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - - // User redacts their prior response - cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test") - .realHover() - .findByRole("button", { name: "Options" }) - .click(); - cy.get(".mx_IconizedContextMenu").within(() => { - cy.findByRole("menuitem", { name: "Remove" }).click(); - }); - cy.get(".mx_TextInputDialog").within(() => { - cy.findByRole("button", { name: "Remove" }).should("have.class", "mx_Dialog_primary").click(); - }); - - cy.get(".mx_ThreadView").within(() => { - // Wait until the response is redacted - cy.get(".mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); - }); - - // Take Percy snapshots in group layout and bubble layout (IRC layout is not available on ThreadView) - cy.get(".mx_ThreadView .mx_EventTile[data-layout='group']").should("be.visible"); - cy.get(".mx_ThreadView").percySnapshotElement("ThreadView with redacted messages on group layout", { - percyCSS, - }); - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - cy.get(".mx_ThreadView .mx_EventTile[data-layout='bubble']").should("be.visible"); - cy.get(".mx_ThreadView").percySnapshotElement("ThreadView with redacted messages on bubble layout", { - percyCSS, - }); - - // Set the group layout - cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); - - // User asserts summary was updated correctly - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText(MessageLong).should("exist"); - }); - - // User closes right panel after clicking back to thread list - cy.get(".mx_ThreadPanel").within(() => { - cy.findByRole("button", { name: "Threads" }).click(); - cy.findByRole("button", { name: "Close" }).click(); - }); - - // Bot responds to thread - cy.get("@threadId").then((threadId) => { - bot.sendMessage(roomId, threadId, { - body: "How are things?", - msgtype: "m.text", - }); - }); - - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("How are things?").should("exist"); - }); - - cy.findByRole("button", { name: "Threads" }) - .should("have.class", "mx_LegacyRoomHeader_button--unread") // User asserts thread list unread indicator - .click(); // User opens thread list - - // User asserts thread with correct root & latest events & unread dot - cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => { - cy.get(".mx_EventTile_body").findByText("Hello Mr. Bot").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("How are things?").should("exist"); - - // Check the number of the replies - cy.get(".mx_ThreadPanel_replies_amount").findByText("2").should("exist"); - - // Make sure the notification dot is visible - cy.get(".mx_NotificationBadge_visible").should("be.visible"); - - // User opens thread via threads list - cy.get(".mx_EventTile_line").click(); - }); - - // User responds & asserts - cy.get(".mx_ThreadView").within(() => { - cy.findByRole("textbox", { name: "Send a message…" }).type("Great!{enter}"); - }); - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("Tom").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("Great!").should("exist"); - }); - - // User edits & asserts - cy.get(".mx_ThreadView .mx_EventTile_last").within(() => { - cy.findByText("Great!").should("exist"); - cy.get(".mx_EventTile_line").realHover().findByRole("button", { name: "Edit" }).click(); - cy.findByRole("textbox").type(" How about yourself?{enter}"); - }); - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("Tom").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("Great! How about yourself?").should("exist"); - }); - - // User closes right panel - cy.get(".mx_ThreadPanel").within(() => { - cy.findByRole("button", { name: "Close" }).click(); - }); - - // Bot responds to thread and saves the id of their message to @eventId - cy.get("@threadId").then((threadId) => { - cy.wrap( - bot - .sendMessage(roomId, threadId, { - body: "I'm very good thanks", - msgtype: "m.text", - }) - .then((res) => res.event_id), - ).as("eventId"); - }); - - // User asserts - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("I'm very good thanks").should("exist"); - }); - - // Bot edits their latest event - cy.get("@eventId").then((eventId) => { - bot.sendMessage(roomId, { - "body": "* I'm very good thanks :)", - "msgtype": "m.text", - "m.new_content": { - body: "I'm very good thanks :)", - msgtype: "m.text", - }, - "m.relates_to": { - rel_type: "m.replace", - event_id: eventId, - }, - }); - }); - - // User asserts - cy.get(".mx_RoomView_body .mx_ThreadSummary").within(() => { - cy.get(".mx_ThreadSummary_sender").findByText("BotBob").should("exist"); - cy.get(".mx_ThreadSummary_content").findByText("I'm very good thanks :)").should("exist"); - }); - }); - - it("can send voice messages", () => { - // Increase viewport size and right-panel size, so that voice messages fit - cy.viewport(1280, 720); - cy.window().then((window) => { - window.localStorage.setItem("mx_rhs_size", "600"); - }); - - let roomId: string; - cy.createRoom({}).then((_roomId) => { - roomId = _roomId; - cy.visit("/#/room/" + roomId); - }); - - // Send message - cy.get(".mx_RoomView_body").within(() => { - cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - - // Create thread - cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .realHover() - .findByRole("button", { name: "Reply in thread" }) - .click(); - }); - cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); - - cy.openMessageComposerOptions(true).findByRole("menuitem", { name: "Voice Message" }).click(); - cy.wait(3000); - cy.getComposer(true).findByRole("button", { name: "Send voice message" }).click(); - - cy.get(".mx_ThreadView .mx_MVoiceMessageBody").should("have.length", 1); - }); - - it("should send location and reply to the location on ThreadView", () => { - // See: location.spec.ts - const selectLocationShareTypeOption = (shareType: string): Chainable => { - return cy.findByTestId(`share-location-option-${shareType}`); - }; - const submitShareLocation = (): void => { - cy.findByRole("button", { name: "Share location" }).click(); - }; - - let bot: MatrixClient; - cy.getBot(homeserver, { - displayName: "BotBob", - autoAcceptInvites: false, - }).then((_bot) => { - bot = _bot; - }); - - let roomId: string; - cy.createRoom({}) - .then((_roomId) => { - roomId = _roomId; - return cy.inviteUser(roomId, bot.getUserId()); - }) - .then(async () => { - await bot.joinRoom(roomId); - cy.visit("/#/room/" + roomId); - }); - - // Exclude timestamp, read marker, and mapboxgl-map from snapshots - const percyCSS = - ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .mapboxgl-map { visibility: hidden !important; }"; - - cy.get(".mx_RoomView_body").within(() => { - // User sends message - cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - - // Wait for message to send, get its ID and save as @threadId - cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .invoke("attr", "data-scroll-tokens") - .as("threadId"); - }); - - // Bot starts thread - cy.get("@threadId").then((threadId) => { - bot.sendMessage(roomId, threadId, { - body: "Hello there", - msgtype: "m.text", - }); - }); - - // User clicks thread summary - cy.get(".mx_RoomView_body .mx_ThreadSummary").click(); - - // User sends location on ThreadView - cy.get(".mx_ThreadView").should("exist"); - cy.openMessageComposerOptions(true).findByRole("menuitem", { name: "Location" }).click(); - selectLocationShareTypeOption("Pin").click(); - cy.get("#mx_LocationPicker_map").click("center"); - submitShareLocation(); - cy.get(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody", { timeout: 10000 }).should("exist"); - - // User replies to the location - cy.get(".mx_ThreadView").within(() => { - cy.get(".mx_EventTile_last").realHover().findByRole("button", { name: "Reply" }).click(); - - cy.findByRole("textbox", { name: "Reply to thread…" }).type("Please come here.{enter}"); - - // Wait until the reply is sent - cy.get(".mx_EventTile_last .mx_EventTile_receiptSent").should("be.visible"); - }); - - // Take a snapshot of reply to the shared location - cy.get(".mx_ThreadView").percySnapshotElement("Reply to the location on ThreadView", { percyCSS }); - }); - - it("right panel behaves correctly", () => { - // Create room - let roomId: string; - cy.createRoom({}).then((_roomId) => { - roomId = _roomId; - cy.visit("/#/room/" + roomId); - }); - - // Send message - cy.get(".mx_RoomView_body").within(() => { - cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. Bot{enter}"); - - // Create thread - cy.contains(".mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .realHover() - .findByRole("button", { name: "Reply in thread" }) - .click(); - }); - cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); - - // Send message to thread - cy.get(".mx_ThreadPanel").within(() => { - cy.findByRole("textbox", { name: "Send a message…" }).type("Hello Mr. User{enter}"); - cy.get(".mx_EventTile_last").findByText("Hello Mr. User").should("exist"); - - // Close thread - cy.findByRole("button", { name: "Close" }).click(); - }); - - // Open existing thread - cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") - .realHover() - .findByRole("button", { name: "Reply in thread" }) - .click(); - cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1); - - cy.get(".mx_BaseCard").within(() => { - cy.get(".mx_EventTile").first().findByText("Hello Mr. Bot").should("exist"); - cy.get(".mx_EventTile").last().findByText("Hello Mr. User").should("exist"); - }); - }); -}); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts new file mode 100644 index 0000000000..34fe287d47 --- /dev/null +++ b/playwright/e2e/threads/threads.spec.ts @@ -0,0 +1,450 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import { SettingLevel } from "../../../src/settings/SettingLevel"; +import { Layout } from "../../../src/settings/enums/Layout"; +import { test, expect } from "../../element-web-test"; + +test.describe("Threads", () => { + test.use({ + displayName: "Tom", + botCreateOpts: { + displayName: "BotBob", + autoAcceptInvites: true, + }, + }); + + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests + }); + }); + + // Flaky: https://github.com/vector-im/element-web/issues/26452 + test.skip("should be usable for a conversation", async ({ page, app, bot }) => { + const roomId = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await page.goto("/#/room/" + roomId); + + // Around 200 characters + const MessageLong = + "Hello there. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt " + + "ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi"; + + const ThreadViewGroupSpacingStart = "56px"; // --ThreadView_group_spacing-start + // Exclude timestamp and read marker from snapshots + const mask = [page.locator(".mx_MessageTimestamp"), page.locator(".mx_MessagePanel_myReadMarker")]; + + const roomViewLocator = page.locator(".mx_RoomView_body"); + // User sends message + const textbox = roomViewLocator.getByRole("textbox", { name: "Send a message…" }); + await textbox.fill("Hello Mr. Bot"); + await textbox.press("Enter"); + + // Wait for message to send, get its ID and save as @threadId + const threadId = await roomViewLocator + .locator(".mx_EventTile[data-scroll-tokens]") + .filter({ hasText: "Hello Mr. Bot" }) + .getAttribute("data-scroll-tokens"); + + // Bot starts thread + await bot.sendMessage(roomId, MessageLong, threadId); + + // User asserts timeline thread summary visible & clicks it + let locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("BotBob")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText(MessageLong)).toBeAttached(); + await locator.click(); + + // Wait until the both messages are read + locator = page.locator(".mx_ThreadView .mx_EventTile_last[data-layout=group]"); + await expect(locator.locator(".mx_EventTile_line .mx_MTextBody").getByText(MessageLong)).toBeAttached(); + await expect(locator.locator(".mx_ReadReceiptGroup .mx_BaseAvatar")).toBeVisible(); + // Make sure the CSS style for spacing is applied to mx_EventTile_line on group/modern layout + await expect(locator.locator(".mx_EventTile_line")).toHaveCSS( + "padding-inline-start", + ThreadViewGroupSpacingStart, + ); + + // Take snapshots in group layout and bubble layout (IRC layout is not available on ThreadView) + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_group_layout.png", { + mask: mask, + }); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toBeVisible(); + + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_bubble_layout.png", { + mask: mask, + }); + + // Set the group layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + + locator = page.locator(".mx_ThreadView .mx_EventTile[data-layout='group'].mx_EventTile_last"); + // Wait until the messages are rendered + await expect(locator.locator(".mx_EventTile_line .mx_MTextBody").getByText(MessageLong)).toBeAttached(); + // Make sure the avatar inside ReadReceiptGroup is visible on the group layout + await expect(locator.locator(".mx_ReadReceiptGroup .mx_BaseAvatar")).toBeVisible(); + + // Enable the bubble layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + + locator = page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble'].mx_EventTile_last"); + // TODO: remove this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout + // See: https://github.com/vector-im/element-web/issues/23569 + await expect(locator.locator(".mx_ReadReceiptGroup .mx_BaseAvatar")).toBeAttached(); + // Make sure the avatar inside ReadReceiptGroup is visible on bubble layout + // TODO: enable this after fixing the issue of ReadReceiptGroup being hidden on the bubble layout + // See: https://github.com/vector-im/element-web/issues/23569 + // expect(locator.locator(".mx_ReadReceiptGroup .mx_BaseAvatar")).toBeVisible(); + + // Re-enable the group layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + + // User responds in thread + locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" }); + await locator.fill("Test"); + await locator.press("Enter"); + + // User asserts summary was updated correctly + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("Tom")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText("Test")).toBeAttached(); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Check reactions and hidden events + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // Enable hidden events to make the event for reaction displayed + await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); + + // User reacts to message instead + locator = page + .locator(".mx_ThreadView") + .locator(".mx_EventTile .mx_EventTile_line") + .filter({ hasText: "Hello there" }); + await locator.hover(); + await locator.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "React" }).click(); + + locator = page.locator(".mx_EmojiPicker"); + await locator.getByRole("textbox").fill("wave"); + await page.getByRole("gridcell", { name: "👋" }).click(); + + locator = page.locator(".mx_ThreadView"); + // Make sure the CSS style for spacing is applied to mx_ReactionsRow on group/modern layout + await expect(locator.locator(".mx_EventTile[data-layout=group] .mx_ReactionsRow")).toHaveCSS( + "margin-inline-start", + ThreadViewGroupSpacingStart, + ); + // Make sure the CSS style for spacing is applied to the hidden event on group/modern layout + await expect( + locator.locator( + ".mx_GenericEventListSummary[data-layout=group] .mx_EventTile_info.mx_EventTile_last " + + ".mx_EventTile_line", + ), + ).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart); + + // Take snapshot of group layout (IRC layout is not available on ThreadView) + expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png", + { + mask: mask, + }, + ); + + // Enable bubble layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + + // Make sure the CSS style for spacing is applied to the hidden event on bubble layout + locator = page.locator( + ".mx_ThreadView .mx_GenericEventListSummary[data-layout=bubble] .mx_EventTile_info.mx_EventTile_last", + ); + expect(locator.locator(".mx_EventTile_line .mx_EventTile_content")) + // 76px: ThreadViewGroupSpacingStart + 14px + 6px + // 14px: avatar width + // See: _EventTile.pcss + .toHaveCSS("margin-inline-start", "76px"); + await expect(locator.locator(".mx_EventTile_line")) + // Make sure the margin is NOT applied to mx_EventTile_line + .toHaveCSS("margin-inline-start", "0px"); + + // Take snapshot of bubble layout + expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png", + { + mask: mask, + }, + ); + + // Disable hidden events + await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, false); + + // Reset to the group layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Check redactions + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + // User redacts their prior response + locator = page.locator(".mx_ThreadView .mx_EventTile .mx_EventTile_line").filter({ hasText: "Test" }); + await locator.hover(); + await locator.getByRole("button", { name: "Options" }).click(); + + await page.locator(".mx_IconizedContextMenu").getByRole("menuitem", { name: "Remove" }).click(); + locator = page.locator(".mx_TextInputDialog").getByRole("button", { name: "Remove" }); + await expect(locator).toHaveClass(/mx_Dialog_primary/); + await locator.click(); + + // Wait until the response is redacted + await expect( + page.locator(".mx_ThreadView").locator(".mx_EventTile_last .mx_EventTile_receiptSent"), + ).toBeVisible(); + + // Take snapshots in group layout and bubble layout (IRC layout is not available on ThreadView) + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']")).toBeVisible(); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_redacted_messages_on_group_layout.png", + { + mask: mask, + }, + ); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toBeVisible(); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_redacted_messages_on_bubble_layout.png", + { + mask: mask, + }, + ); + + // Set the group layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + + // User asserts summary was updated correctly + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("BotBob")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText(MessageLong)).toBeAttached(); + + // User closes right panel after clicking back to thread list + locator = page.locator(".mx_ThreadPanel"); + locator.getByRole("button", { name: "Threads" }).click(); + locator.getByRole("button", { name: "Close" }).click(); + + // Bot responds to thread + await bot.sendMessage(roomId, "How are things?", threadId); + + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("BotBob")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText("How are things?")).toBeAttached(); + + locator = page.getByRole("button", { name: "Threads" }); + await expect(locator).toHaveClass(/mx_LegacyRoomHeader_button--unread/); // User asserts thread list unread indicator + await locator.click(); // User opens thread list + + // User asserts thread with correct root & latest events & unread dot + locator = page.locator(".mx_ThreadPanel .mx_EventTile_last"); + await expect(locator.locator(".mx_EventTile_body").getByText("Hello Mr. Bot")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText("How are things?")).toBeAttached(); + // Check the number of the replies + await expect(locator.locator(".mx_ThreadPanel_replies_amount").getByText("2")).toBeAttached(); + // Make sure the notification dot is visible + await expect(locator.locator(".mx_NotificationBadge_visible")).toBeVisible(); + // User opens thread via threads list + await locator.locator(".mx_EventTile_line").click(); + + // User responds & asserts + locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" }); + await locator.fill("Great!"); + await locator.press("Enter"); + + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("Tom")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText("Great!")).toBeAttached(); + + // User edits & asserts + locator = page.locator(".mx_ThreadView .mx_EventTile_last"); + await expect(locator.getByText("Great!")).toBeAttached(); + await locator.locator(".mx_EventTile_line").hover(); + await locator.locator(".mx_EventTile_line").getByRole("button", { name: "Edit" }).click(); + await locator.getByRole("textbox").fill(" How about yourself?{enter}"); + await locator.getByRole("textbox").press("Enter"); + + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("Tom")).toBeAttached(); + await expect( + locator.locator(".mx_ThreadSummary_content").getByText("Great! How about yourself?"), + ).toBeAttached(); + + // User closes right panel + await page.locator(".mx_ThreadPanel").getByRole("button", { name: "Close" }).click(); + + // Bot responds to thread and saves the id of their message to @eventId + const { event_id: eventId } = await bot.sendMessage(roomId, threadId, "I'm very good thanks"); + + // User asserts + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("BotBob")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText("I'm very good thanks")).toBeAttached(); + + // Bot edits their latest event + await bot.sendMessage(roomId, { + "body": "* I'm very good thanks :)", + "msgtype": "m.text", + "m.new_content": { + body: "I'm very good thanks :)", + msgtype: "m.text", + }, + "m.relates_to": { + rel_type: "m.replace", + event_id: eventId, + }, + }); + + // User asserts + locator = page.locator(".mx_RoomView_body .mx_ThreadSummary"); + await expect(locator.locator(".mx_ThreadSummary_sender").getByText("BotBob")).toBeAttached(); + await expect(locator.locator(".mx_ThreadSummary_content").getByText("I'm very good thanks :)")).toBeAttached(); + }); + + test.describe("with larger viewport", async () => { + // Increase viewport size so that voice messages fit + test.use({ viewport: { width: 1280, height: 720 } }); + + test.beforeEach(async ({ page }) => { + // Increase right-panel size, so that voice messages fit + await page.addInitScript(() => { + window.localStorage.setItem("mx_rhs_size", "600"); + }); + }); + + test("can send voice messages", async ({ page, app, user }) => { + // Increase right-panel size, so that voice messages fit + await page.evaluate(() => { + window.localStorage.setItem("mx_rhs_size", "600"); + }); + + const roomId = await app.client.createRoom({}); + await page.goto("/#/room/" + roomId); + + // Send message + const locator = page.locator(".mx_RoomView_body"); + await locator.getByRole("textbox", { name: "Send a message…" }).fill("Hello Mr. Bot"); + await locator.getByRole("textbox", { name: "Send a message…" }).press("Enter"); + // Create thread + const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" }); + await locator2.hover(); + await locator2.getByRole("button", { name: "Reply in thread" }).click(); + + await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1); + + (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Voice Message" }).click(); + await page.waitForTimeout(3000); + await app.getComposer(true).getByRole("button", { name: "Send voice message" }).click(); + await expect(page.locator(".mx_ThreadView .mx_MVoiceMessageBody")).toHaveCount(1); + }); + }); + + test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => { + const roomId = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await bot.joinRoom(roomId); + await page.goto("/#/room/" + roomId); + + // Exclude timestamp, read marker, and mapboxgl-map from snapshots + const css = + ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .mapboxgl-map { visibility: hidden !important; }"; + + let locator = page.locator(".mx_RoomView_body"); + // User sends message + let textbox = locator.getByRole("textbox", { name: "Send a message…" }); + await textbox.fill("Hello Mr. Bot"); + await textbox.press("Enter"); + // Wait for message to send, get its ID and save as @threadId + const threadId = await locator + .locator(".mx_EventTile[data-scroll-tokens]") + .filter({ hasText: "Hello Mr. Bot" }) + .getAttribute("data-scroll-tokens"); + + // Bot starts thread + await bot.sendMessage(roomId, "Hello there", threadId); + + // User clicks thread summary + await page.locator(".mx_RoomView_body .mx_ThreadSummary").click(); + + // User sends location on ThreadView + await expect(page.locator(".mx_ThreadView")).toBeAttached(); + await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click(); + await page.getByTestId(`share-location-option-Pin`).click(); + await page.locator("#mx_LocationPicker_map").click(); + await page.getByRole("button", { name: "Share location" }).click(); + await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({ + timeout: 10000, + }); + + // User replies to the location + locator = page.locator(".mx_ThreadView"); + await locator.locator(".mx_EventTile_last").hover(); + await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); + textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); + await textbox.fill("Please come here"); + await textbox.press("Enter"); + // Wait until the reply is sent + await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); + + // Take a snapshot of reply to the shared location + await page.addStyleTag({ content: css }); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); + }); + + test("right panel behaves correctly", async ({ page, app, user }) => { + // Create room + const roomId = await app.client.createRoom({}); + await page.goto("/#/room/" + roomId); + + // Send message + let locator = page.locator(".mx_RoomView_body"); + let textbox = locator.getByRole("textbox", { name: "Send a message…" }); + await textbox.fill("Hello Mr. Bot"); + await textbox.press("Enter"); + // Create thread + const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" }); + await locator2.hover(); + await locator2.getByRole("button", { name: "Reply in thread" }).click(); + await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1); + + // Send message to thread + locator = page.locator(".mx_ThreadPanel"); + textbox = locator.getByRole("textbox", { name: "Send a message…" }); + await textbox.fill("Hello Mr. User"); + await textbox.press("Enter"); + await expect(locator.locator(".mx_EventTile_last").getByText("Hello Mr. User")).toBeAttached(); + // Close thread + await locator.getByRole("button", { name: "Close" }).click(); + + // Open existing thread + locator = page + .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") + .filter({ hasText: "Hello Mr. Bot" }); + await locator.hover(); + await locator.getByRole("button", { name: "Reply in thread" }).click(); + await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1); + + locator = page.locator(".mx_BaseCard"); + await expect(locator.locator(".mx_EventTile").first().getByText("Hello Mr. Bot")).toBeAttached(); + await expect(locator.locator(".mx_EventTile").last().getByText("Hello Mr. User")).toBeAttached(); + }); +}); diff --git a/playwright/pages/client.ts b/playwright/pages/client.ts index e73e101e3d..054b946845 100644 --- a/playwright/pages/client.ts +++ b/playwright/pages/client.ts @@ -107,8 +107,13 @@ export class Client { * Send a message into a room * @param roomId ID of the room to send the message into * @param content the event content to send + * @param threadId optional thread id */ - public async sendMessage(roomId: string, content: IContent | string): Promise { + public async sendMessage( + roomId: string, + content: IContent | string, + threadId: string | null = null, + ): Promise { if (typeof content === "string") { content = { msgtype: "m.text", @@ -118,12 +123,13 @@ export class Client { const client = await this.prepareClient(); return client.evaluate( - (client, { roomId, content }) => { - return client.sendMessage(roomId, content); + (client, { roomId, content, threadId }) => { + return client.sendMessage(roomId, threadId, content); }, { roomId, content, + threadId, }, ); } diff --git a/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png new file mode 100644 index 0000000000..3b9781553d Binary files /dev/null and b/playwright/snapshots/threads/threads.spec.ts/Reply-to-the-location-on-ThreadView-linux.png differ