diff --git a/package.json b/package.json index d0a073ec2e6..68ee57efcd4 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "test:playwright:open": "yarn test:playwright --ui", "test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "test:playwright:screenshots:build": "docker build playwright -t element-web-playwright", - "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright", + "test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot", "coverage": "yarn test --coverage", "analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", diff --git a/playwright/e2e/app-loading/feature-detection.spec.ts b/playwright/e2e/app-loading/feature-detection.spec.ts index 16e17a80549..ee61fb56628 100644 --- a/playwright/e2e/app-loading/feature-detection.spec.ts +++ b/playwright/e2e/app-loading/feature-detection.spec.ts @@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details. import { test, expect } from "../../element-web-test"; -test(`shows error page if browser lacks Intl support`, async ({ page }) => { +test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => { await page.addInitScript({ content: `delete window.Intl;` }); await page.goto("/"); @@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => { await expect(page).toMatchScreenshot("unsupported-browser.png"); }); -test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => { +test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => { await page.addInitScript({ content: `delete window.WebAssembly;` }); await page.goto("/"); diff --git a/playwright/e2e/audio-player/audio-player.spec.ts b/playwright/e2e/audio-player/audio-player.spec.ts index c2081dfcd80..2bb9ab0be45 100644 --- a/playwright/e2e/audio-player/audio-player.spec.ts +++ b/playwright/e2e/audio-player/audio-player.spec.ts @@ -134,18 +134,22 @@ test.describe("Audio player", () => { ).toBeVisible(); }); - test("should be correctly rendered - light theme", async ({ page, app }) => { + test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => { await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)"); }); - test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => { - await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); + test( + "should be correctly rendered - light theme with monospace font", + { tag: "@screenshot" }, + async ({ page, app }) => { + await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); - await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace - }); + await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace + }, + ); - test("should be correctly rendered - high contrast theme", async ({ page, app }) => { + test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => { // Disable system theme in case ThemeWatcher enables the theme automatically, // so that the high contrast theme can be enabled await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); @@ -161,7 +165,7 @@ test.describe("Audio player", () => { await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)"); }); - test("should be correctly rendered - dark theme", async ({ page, app }) => { + test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => { // Enable dark theme await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark"); @@ -207,93 +211,101 @@ test.describe("Audio player", () => { expect(download.suggestedFilename()).toBe("1sec.ogg"); }); - test("should support replying to audio file with another audio file", async ({ page, app }) => { - await uploadFile(page, "playwright/sample-files/1sec.ogg"); + test( + "should support replying to audio file with another audio file", + { tag: "@screenshot" }, + async ({ page, app }) => { + await uploadFile(page, "playwright/sample-files/1sec.ogg"); - // Assert the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - // Find and click "Reply" button on MessageActionBar - const tile = page.locator(".mx_EventTile_last"); - await tile.hover(); - await tile.getByRole("button", { name: "Reply", exact: true }).click(); + // Find and click "Reply" button on MessageActionBar + const tile = page.locator(".mx_EventTile_last"); + await tile.hover(); + await tile.getByRole("button", { name: "Reply", exact: true }).click(); - // Reply to the player with another audio file - await uploadFile(page, "playwright/sample-files/1sec.ogg"); + // Reply to the player with another audio file + await uploadFile(page, "playwright/sample-files/1sec.ogg"); - // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); - // Assert that replied audio file is rendered as file button inside ReplyChain - const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); - // Assert that the file button has file name - await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); + // Assert that replied audio file is rendered as file button inside ReplyChain + const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']"); + // Assert that the file button has file name + await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible(); - await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); - }); + await takeSnapshots(page, app, "Selected EventTile of audio player with a reply"); + }, + ); - test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => { - // Note: "mx_ReplyChain" element is used not only for replies which - // create a reply chain, but also for a single reply without a replied - // message. This test checks whether a reply chain which consists of - // multiple audio file replies is rendered properly. + test( + "should support creating a reply chain with multiple audio files", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + // Note: "mx_ReplyChain" element is used not only for replies which + // create a reply chain, but also for a single reply without a replied + // message. This test checks whether a reply chain which consists of + // multiple audio file replies is rendered properly. - const tile = page.locator(".mx_EventTile_last"); + const tile = page.locator(".mx_EventTile_last"); - // Find and click "Reply" button - const clickButtonReply = async () => { - await tile.scrollIntoViewIfNeeded(); - await tile.hover(); - await tile.getByRole("button", { name: "Reply", exact: true }).click(); - }; + // Find and click "Reply" button + const clickButtonReply = async () => { + await tile.scrollIntoViewIfNeeded(); + await tile.hover(); + await tile.getByRole("button", { name: "Reply", exact: true }).click(); + }; - await uploadFile(page, "playwright/sample-files/upload-first.ogg"); + await uploadFile(page, "playwright/sample-files/upload-first.ogg"); - // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - await clickButtonReply(); + await clickButtonReply(); - // Reply to the player with another audio file - await uploadFile(page, "playwright/sample-files/upload-second.ogg"); + // Reply to the player with another audio file + await uploadFile(page, "playwright/sample-files/upload-second.ogg"); - // Assert that the audio player is rendered - await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); - await clickButtonReply(); + await clickButtonReply(); - // Reply to the player with yet another audio file to create a reply chain - await uploadFile(page, "playwright/sample-files/upload-third.ogg"); + // Reply to the player with yet another audio file to create a reply chain + await uploadFile(page, "playwright/sample-files/upload-third.ogg"); - // Assert that the audio player is rendered - await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); + // Assert that the audio player is rendered + await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible(); - // Assert that there are two "mx_ReplyChain" elements - await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); + // Assert that there are two "mx_ReplyChain" elements + await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); - // Assert that one line contains the user name - await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); + // Assert that one line contains the user name + await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible(); - // Assert that the other line contains the file button - await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); + // Assert that the other line contains the file button + await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); - // Click "In reply to" - await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); + // Click "In reply to" + await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); - const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); - // Assert that "In reply to" has disappeared - await expect(replyChain.getByText("In reply to")).not.toBeVisible(); + const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); + // Assert that "In reply to" has disappeared + await expect(replyChain.getByText("In reply to")).not.toBeVisible(); - // Assert that the file button contains the name of the file sent at first - await expect( - replyChain - .locator(".mx_MFileBody_info[role='button']") - .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }), - ).toBeVisible(); + // Assert that the file button contains the name of the file sent at first + await expect( + replyChain + .locator(".mx_MFileBody_info[role='button']") + .locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }), + ).toBeVisible(); - // Take snapshots - await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); - }); + // Take snapshots + await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); + }, + ); test("should be rendered, play, and support replying on a thread", async ({ page, app }) => { await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); diff --git a/playwright/e2e/chat-export/html-export.spec.ts b/playwright/e2e/chat-export/html-export.spec.ts index 9a66a4907a3..f914cccd96b 100644 --- a/playwright/e2e/chat-export/html-export.spec.ts +++ b/playwright/e2e/chat-export/html-export.spec.ts @@ -89,43 +89,47 @@ test.describe("HTML Export", () => { }, }); - test("should export html successfully and match screenshot", async ({ page, app, room }) => { - // Set a fixed time rather than masking off the line with the time in it: we don't need to worry - // about the width changing and we can actually test this line looks correct. - page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); - - // Send a bunch of messages to populate the room - for (let i = 1; i < 10; i++) { - const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); - if (i == 1) { - await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + test( + "should export html successfully and match screenshot", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + // Set a fixed time rather than masking off the line with the time in it: we don't need to worry + // about the width changing and we can actually test this line looks correct. + page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); + + // Send a bunch of messages to populate the room + for (let i = 1; i < 10; i++) { + const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); + if (i == 1) { + await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); + } } - } - - // Wait for all the messages to be displayed - await expect( - page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), - ).toBeVisible(); - - await app.toggleRoomInfoPanel(); - await page.getByRole("menuitem", { name: "Export Chat" }).click(); - - const downloadPromise = page.waitForEvent("download"); - await page.getByRole("button", { name: "Export", exact: true }).click(); - const download = await downloadPromise; - - const dirPath = path.join(os.tmpdir(), "html-export-test"); - const zipPath = `${dirPath}.zip`; - await download.saveAs(zipPath); - - const zip = await extractZipFileToPath(zipPath, dirPath); - await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); - await expect(page).toMatchScreenshot("html-export.png", { - mask: [ - // We need to mask the whole thing because the width of the time part changes - page.locator(".mx_TimelineSeparator"), - page.locator(".mx_MessageTimestamp"), - ], - }); - }); + + // Wait for all the messages to be displayed + await expect( + page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), + ).toBeVisible(); + + await app.toggleRoomInfoPanel(); + await page.getByRole("menuitem", { name: "Export Chat" }).click(); + + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Export", exact: true }).click(); + const download = await downloadPromise; + + const dirPath = path.join(os.tmpdir(), "html-export-test"); + const zipPath = `${dirPath}.zip`; + await download.saveAs(zipPath); + + const zip = await extractZipFileToPath(zipPath, dirPath); + await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); + await expect(page).toMatchScreenshot("html-export.png", { + mask: [ + // We need to mask the whole thing because the width of the time part changes + page.locator(".mx_TimelineSeparator"), + page.locator(".mx_MessageTimestamp"), + ], + }); + }, + ); }); diff --git a/playwright/e2e/crypto/crypto.spec.ts b/playwright/e2e/crypto/crypto.spec.ts index 2ab49e72ec9..668c17d931d 100644 --- a/playwright/e2e/crypto/crypto.spec.ts +++ b/playwright/e2e/crypto/crypto.spec.ts @@ -204,30 +204,29 @@ test.describe("Cryptography", function () { await expect(page.locator(".mx_Dialog")).toHaveCount(1); }); - test("creating a DM should work, being e2e-encrypted / user verification", async ({ - page, - app, - bot: bob, - user: aliceCredentials, - }) => { - await app.client.bootstrapCrossSigning(aliceCredentials); - await startDMWithBob(page, bob); - // send first message - await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); - await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); - await checkDMRoom(page); - const bobRoomId = await bobJoin(page, bob); - await testMessages(page, bob, bobRoomId); - await verify(app, bob); - - // Assert that verified icon is rendered - await page.getByTestId("base-card-back-button").click(); - await page.getByLabel("Room info").nth(1).click(); - await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); - - // Take a snapshot of RoomSummaryCard with a verified E2EE icon - await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); - }); + test( + "creating a DM should work, being e2e-encrypted / user verification", + { tag: "@screenshot" }, + async ({ page, app, bot: bob, user: aliceCredentials }) => { + await app.client.bootstrapCrossSigning(aliceCredentials); + await startDMWithBob(page, bob); + // send first message + await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); + await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); + await checkDMRoom(page); + const bobRoomId = await bobJoin(page, bob); + await testMessages(page, bob, bobRoomId); + await verify(app, bob); + + // Assert that verified icon is rendered + await page.getByTestId("base-card-back-button").click(); + await page.getByLabel("Room info").nth(1).click(); + await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); + + // Take a snapshot of RoomSummaryCard with a verified E2EE icon + await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); + }, + ); test("should allow verification when there is no existing DM", async ({ page, diff --git a/playwright/e2e/editing/editing.spec.ts b/playwright/e2e/editing/editing.spec.ts index 206d91982e0..e22ec250b98 100644 --- a/playwright/e2e/editing/editing.spec.ts +++ b/playwright/e2e/editing/editing.spec.ts @@ -66,126 +66,130 @@ test.describe("Editing", () => { botCreateOpts: { displayName: "Bob" }, }); - test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => { - // Click the "Remove" button on the message edit history dialog - const clickButtonRemove = async (locator: Locator) => { - const eventTileLine = locator.locator(".mx_EventTile_line"); - await eventTileLine.hover(); - await eventTileLine.getByRole("button", { name: "Remove" }).click(); - }; + test( + "should render and interact with the message edit history dialog", + { tag: "@screenshot" }, + async ({ page, user, app, room }) => { + // Click the "Remove" button on the message edit history dialog + const clickButtonRemove = async (locator: Locator) => { + const eventTileLine = locator.locator(".mx_EventTile_line"); + await eventTileLine.hover(); + await eventTileLine.getByRole("button", { name: "Remove" }).click(); + }; - await page.goto(`#/room/${room.roomId}`); - - // Send "Message" - await sendEvent(app, room.roomId); - - // Edit "Message" to "Massage" - await editLastMessage(page, "Massage"); - - // Assert that the edit label is visible - await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); + await page.goto(`#/room/${room.roomId}`); - await clickEditedMessage(page, "Massage"); + // Send "Message" + await sendEvent(app, room.roomId); - // Assert that the message edit history dialog is rendered - const dialog = page.getByRole("dialog"); - const li = dialog.getByRole("listitem").last(); - // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected - await expect(li).toHaveCSS("clear", "both"); + // Edit "Message" to "Massage" + await editLastMessage(page, "Massage"); - const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); - await expect(timestamp).toHaveCSS("position", "absolute"); - await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); - await expect(timestamp).toHaveCSS("text-align", "center"); + // Assert that the edit label is visible + await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); - // Assert that monospace characters can fill the content line as expected - await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px"); + await clickEditedMessage(page, "Massage"); - // Assert that zero block start padding is applied to mx_EventTile as expected - // See: .mx_EventTile on _EventTile.pcss - await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); + // Assert that the message edit history dialog is rendered + const dialog = page.getByRole("dialog"); + const li = dialog.getByRole("listitem").last(); + // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected + await expect(li).toHaveCSS("clear", "both"); - // Assert that the date separator is rendered at the top - await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( - "text-transform", - "capitalize", - ); + const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); + await expect(timestamp).toHaveCSS("position", "absolute"); + await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); + await expect(timestamp).toHaveCSS("text-align", "center"); - { - // Assert that the edited message is rendered under the date separator - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - // Assert that the edited message body consists of both deleted character and inserted character - // Above the first "e" of "Message" was replaced with "a" - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - - const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); - await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible(); - await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); - } + // Assert that monospace characters can fill the content line as expected + await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px"); - // Assert that the original message is rendered at the bottom - await expect( - dialog - .locator("li:nth-child(3) .mx_EventTile") - .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), - ).toBeVisible(); + // Assert that zero block start padding is applied to mx_EventTile as expected + // See: .mx_EventTile on _EventTile.pcss + await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); - // Take a snapshot of the dialog - await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); + // Assert that the date separator is rendered at the top + await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( + "text-transform", + "capitalize", + ); - { - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - // Click the "Remove" button again - await clickButtonRemove(tile); - } + { + // Assert that the edited message is rendered under the date separator + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + // Assert that the edited message body consists of both deleted character and inserted character + // Above the first "e" of "Message" was replaced with "a" + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + + const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); + await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible(); + await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); + } - // Do nothing and close the dialog to confirm that the message edit history dialog is rendered - await app.closeDialog(); + // Assert that the original message is rendered at the bottom + await expect( + dialog + .locator("li:nth-child(3) .mx_EventTile") + .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), + ).toBeVisible(); - { - // Assert that the message edit history dialog is rendered again after it was closed - const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); - await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); - // Click the "Remove" button again - await clickButtonRemove(tile); - } + // Take a snapshot of the dialog + await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { + mask: [page.locator(".mx_MessageTimestamp")], + }); - // This time remove the message really - const textInputDialog = page.locator(".mx_TextInputDialog"); - await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason - await textInputDialog.getByRole("button", { name: "Remove" }).click(); + { + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + // Click the "Remove" button again + await clickButtonRemove(tile); + } - // Assert that the message edit history dialog is rendered again - const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); - // Assert that the date is rendered - await expect( - messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), - ).toHaveCSS("text-transform", "capitalize"); + // Do nothing and close the dialog to confirm that the message edit history dialog is rendered + await app.closeDialog(); - // Assert that the original message is rendered under the date on the dialog - await expect( - messageEditHistoryDialog - .locator("li:nth-child(2) .mx_EventTile") - .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), - ).toBeVisible(); + { + // Assert that the message edit history dialog is rendered again after it was closed + const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); + await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); + // Click the "Remove" button again + await clickButtonRemove(tile); + } - // Assert that the edited message is gone - await expect( - messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), - ).not.toBeVisible(); + // This time remove the message really + const textInputDialog = page.locator(".mx_TextInputDialog"); + await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason + await textInputDialog.getByRole("button", { name: "Remove" }).click(); + + // Assert that the message edit history dialog is rendered again + const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); + // Assert that the date is rendered + await expect( + messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), + ).toHaveCSS("text-transform", "capitalize"); + + // Assert that the original message is rendered under the date on the dialog + await expect( + messageEditHistoryDialog + .locator("li:nth-child(2) .mx_EventTile") + .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), + ).toBeVisible(); + + // Assert that the edited message is gone + await expect( + messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), + ).not.toBeVisible(); - await app.closeDialog(); + await app.closeDialog(); - // Assert that the redaction placeholder is rendered - await expect( - page - .locator(".mx_RoomView_MessageList") - .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), - ).toBeVisible(); - }); + // Assert that the redaction placeholder is rendered + await expect( + page + .locator(".mx_RoomView_MessageList") + .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), + ).toBeVisible(); + }, + ); test("should render 'View Source' button in developer mode on the message edit history dialog", async ({ page, diff --git a/playwright/e2e/file-upload/image-upload.spec.ts b/playwright/e2e/file-upload/image-upload.spec.ts index eb473d83b2a..76782e90e8b 100644 --- a/playwright/e2e/file-upload/image-upload.spec.ts +++ b/playwright/e2e/file-upload/image-upload.spec.ts @@ -25,7 +25,7 @@ test.describe("Image Upload", () => { ).toBeVisible(); }); - test("should show image preview when uploading an image", async ({ page, app }) => { + test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => { await page .locator(".mx_MessageComposer_actions input[type='file']") .setInputFiles("playwright/sample-files/riot.png"); diff --git a/playwright/e2e/forgot-password/forgot-password.spec.ts b/playwright/e2e/forgot-password/forgot-password.spec.ts index c148900afd4..0a12514d9ec 100644 --- a/playwright/e2e/forgot-password/forgot-password.spec.ts +++ b/playwright/e2e/forgot-password/forgot-password.spec.ts @@ -26,7 +26,7 @@ test.describe("Forgot Password", () => { }), }); - test("renders properly", async ({ page, homeserver }) => { + test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { await page.goto("/"); await page.getByRole("link", { name: "Sign in" }).click(); @@ -39,7 +39,7 @@ test.describe("Forgot Password", () => { await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png"); }); - test("renders email verification dialog properly", async ({ page, homeserver }) => { + test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => { const user = await homeserver.registerUser(username, password); await homeserver.setThreepid(user.userId, "email", email); diff --git a/playwright/e2e/invite/invite-dialog.spec.ts b/playwright/e2e/invite/invite-dialog.spec.ts index c8bd8eb404b..eb434eb5b56 100644 --- a/playwright/e2e/invite/invite-dialog.spec.ts +++ b/playwright/e2e/invite/invite-dialog.spec.ts @@ -19,7 +19,7 @@ test.describe("Invite dialog", function () { const botName = "BotAlice"; - test("should support inviting a user to a room", async ({ page, app, user, bot }) => { + test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => { // Create and view a room await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); @@ -73,52 +73,63 @@ test.describe("Invite dialog", function () { await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); }); - test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => { - await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); + test( + "should support inviting a user to Direct Messages", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); - const other = page.locator(".mx_InviteDialog_other"); - // Assert that the header is rendered - await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible(); + const other = page.locator(".mx_InviteDialog_other"); + // Assert that the header is rendered + await expect( + other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"), + ).toBeVisible(); - // Assert that the bar is rendered - await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); + // Assert that the bar is rendered + await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); - // Take a snapshot of the invite dialog - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); + // Take a snapshot of the invite dialog + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); - await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); + await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId); - await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible(); - await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); + await expect( + other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId), + ).toBeVisible(); + await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); - await expect( - other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), - ).toBeVisible(); + await expect( + other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), + ).toBeVisible(); - // Take a snapshot of the invite dialog with a user pill - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png"); + // Take a snapshot of the invite dialog with a user pill + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png"); - // Open a direct message UI - await other.getByRole("button", { name: "Go" }).click(); + // Open a direct message UI + await other.getByRole("button", { name: "Go" }).click(); - // Assert that the invite dialog disappears - await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); + // Assert that the invite dialog disappears + await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); - // Assert that the hovered user name on invitation UI does not have background color - // TODO: implement the test on room-header.spec.ts - const roomHeader = page.locator(".mx_RoomHeader"); - await roomHeader.locator(".mx_RoomHeader_heading").hover(); - await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); + // Assert that the hovered user name on invitation UI does not have background color + // TODO: implement the test on room-header.spec.ts + const roomHeader = page.locator(".mx_RoomHeader"); + await roomHeader.locator(".mx_RoomHeader_heading").hover(); + await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS( + "background-color", + "rgba(0, 0, 0, 0)", + ); - // Send a message to invite the bots - const composer = app.getComposer().locator("[contenteditable]"); - await composer.fill("Hello}"); - await composer.press("Enter"); + // Send a message to invite the bots + const composer = app.getComposer().locator("[contenteditable]"); + await composer.fill("Hello}"); + await composer.press("Enter"); - // Assert that they were invited and joined - await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); + // Assert that they were invited and joined + await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); - // Assert that the message is displayed at the bottom - await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); - }); + // Assert that the message is displayed at the bottom + await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); + }, + ); }); diff --git a/playwright/e2e/messages/messages.spec.ts b/playwright/e2e/messages/messages.spec.ts index 0d5a5da4728..1c518199a00 100644 --- a/playwright/e2e/messages/messages.spec.ts +++ b/playwright/e2e/messages/messages.spec.ts @@ -63,7 +63,7 @@ test.describe("Message rendering", () => { { direction: "ltr", displayName: "Quentin" }, { direction: "rtl", displayName: "كوينتين" }, ].forEach(({ direction, displayName }) => { - test.describe(`with ${direction} display name`, () => { + test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => { test.use({ displayName, room: async ({ user, app }, use) => { @@ -72,14 +72,18 @@ test.describe("Message rendering", () => { }, }); - test("should render a basic LTR text message", async ({ page, user, app, room }) => { - await page.goto(`#/room/${room.roomId}`); + test( + "should render a basic LTR text message", + { tag: "@screenshot" }, + async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); - const msgTile = await sendMessage(page, "Hello, world!"); - await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { - mask: [page.locator(".mx_MessageTimestamp")], - }); - }); + const msgTile = await sendMessage(page, "Hello, world!"); + await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { + mask: [page.locator(".mx_MessageTimestamp")], + }); + }, + ); test("should render an LTR emote", async ({ page, user, app, room }) => { await page.goto(`#/room/${room.roomId}`); diff --git a/playwright/e2e/permalinks/permalinks.spec.ts b/playwright/e2e/permalinks/permalinks.spec.ts index bd7884ea78d..746e15d2880 100644 --- a/playwright/e2e/permalinks/permalinks.spec.ts +++ b/playwright/e2e/permalinks/permalinks.spec.ts @@ -24,7 +24,7 @@ test.describe("permalinks", () => { displayName: "Alice", }); - test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => { + test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => { const bob = new Bot(page, homeserver, { displayName: "Bob" }); const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" }); await bob.prepareClient(); diff --git a/playwright/e2e/pinned-messages/pinned-messages.spec.ts b/playwright/e2e/pinned-messages/pinned-messages.spec.ts index ef2c1b27d4c..06d6db80580 100644 --- a/playwright/e2e/pinned-messages/pinned-messages.spec.ts +++ b/playwright/e2e/pinned-messages/pinned-messages.spec.ts @@ -10,35 +10,38 @@ import { test } from "./index"; import { expect } from "../../element-web-test"; test.describe("Pinned messages", () => { - test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => { - await util.goTo(room1); - await util.openRoomInfo(); - await util.assertPinnedCountInRoomInfo(0); - await util.openPinnedMessagesList(); - await util.assertEmptyPinnedMessagesList(); - }); - - test("should pin one message and to have the pinned message badge in the timeline", async ({ - page, - app, - room1, - util, - }) => { - await util.goTo(room1); - await util.receiveMessages(room1, ["Msg1"]); - await util.pinMessages(["Msg1"]); - - const tile = util.getEventTile("Msg1"); - await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { - mask: [tile.locator(".mx_MessageTimestamp")], - // Hide the jump to bottom button in the timeline to avoid flakiness - css: ` + test( + "should show the empty state when there are no pinned messages", + { tag: "@screenshot" }, + async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.openRoomInfo(); + await util.assertPinnedCountInRoomInfo(0); + await util.openPinnedMessagesList(); + await util.assertEmptyPinnedMessagesList(); + }, + ); + + test( + "should pin one message and to have the pinned message badge in the timeline", + { tag: "@screenshot" }, + async ({ page, app, room1, util }) => { + await util.goTo(room1); + await util.receiveMessages(room1, ["Msg1"]); + await util.pinMessages(["Msg1"]); + + const tile = util.getEventTile("Msg1"); + await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { + mask: [tile.locator(".mx_MessageTimestamp")], + // Hide the jump to bottom button in the timeline to avoid flakiness + css: ` .mx_JumpToBottomButton { display: none !important; } `, - }); - }); + }); + }, + ); test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { await util.goTo(room1); @@ -73,7 +76,7 @@ test.describe("Pinned messages", () => { await util.assertPinnedCountInRoomInfo(2); }); - test("should unpin all messages", async ({ page, app, room1, util }) => { + test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg4"]); @@ -98,7 +101,7 @@ test.describe("Pinned messages", () => { await util.assertPinnedCountInRoomInfo(0); }); - test("should display one message in the banner", async ({ page, app, room1, util }) => { + test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1"]); await util.pinMessages(["Msg1"]); @@ -106,7 +109,7 @@ test.describe("Pinned messages", () => { await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png"); }); - test("should display 2 messages in the banner", async ({ page, app, room1, util }) => { + test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2"]); await util.pinMessages(["Msg1", "Msg2"]); @@ -123,7 +126,7 @@ test.describe("Pinned messages", () => { await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png"); }); - test("should display 4 messages in the banner", async ({ page, app, room1, util }) => { + test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => { await util.goTo(room1); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]); diff --git a/playwright/e2e/polls/polls.spec.ts b/playwright/e2e/polls/polls.spec.ts index 4fd81955810..e1d3ebe7e36 100644 --- a/playwright/e2e/polls/polls.spec.ts +++ b/playwright/e2e/polls/polls.spec.ts @@ -93,7 +93,7 @@ test.describe("Polls", () => { }); }); - test("should be creatable and votable", async ({ page, app, bot, user }) => { + test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => { const roomId: string = await app.client.createRoom({}); await app.client.inviteUser(roomId, bot.credentials.userId); await page.goto("/#/room/" + roomId); @@ -219,107 +219,121 @@ test.describe("Polls", () => { await expect(page.locator(".mx_ErrorDialog")).toBeAttached(); }); - test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => { - const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); - await botCharlie.prepareClient(); - - const roomId: string = await app.client.createRoom({}); - await app.client.inviteUser(roomId, bot.credentials.userId); - await app.client.inviteUser(roomId, botCharlie.credentials.userId); - await page.goto("/#/room/" + roomId); - - // wait until the bots joined - await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 }); - - const locator = await app.openMessageComposerOptions(); - await locator.getByRole("menuitem", { name: "Poll" }).click(); - - const pollParams = { - title: "Does the polls feature work?", - options: ["Yes", "No", "Maybe"], - }; - await createPoll(page, pollParams); - - // Wait for message to send, get its ID and save as @pollId - const pollId = await page - .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") - .filter({ hasText: pollParams.title }) - .getAttribute("data-scroll-tokens"); - - // Bob starts thread on the poll - await bot.sendMessage( - roomId, - { - body: "Hello there", - msgtype: "m.text", - }, - pollId, - ); - - // open the thread summary - await page.getByRole("button", { name: "Open thread" }).click(); - - // Bob votes 'Maybe' in the poll - await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); - - // Charlie votes 'No' - await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); - - // no votes shown until I vote, check votes have arrived in main tl - await expect( - page - .locator(".mx_RoomView_body .mx_MPollBody_totalVotes") - .getByText("2 votes cast. Vote to see the results"), - ).toBeAttached(); - - // and thread view - await expect( - page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"), - ).toBeAttached(); - - // Take snapshots of poll on ThreadView - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); - - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); - - await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - }); - - const roomViewLocator = page.locator(".mx_RoomView_body"); - // vote 'Maybe' in the main timeline poll - await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); - // both me and bob have voted Maybe - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); - - const threadViewLocator = page.locator(".mx_ThreadView"); - // votes updated in thread view too - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); - // change my vote to 'Yes' - await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); - - // Bob updates vote to 'No' - await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); - - // me: yes, bob: no, charlie: no - const expectVoteCounts = async (optLocator: Locator) => { - // I voted yes - await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); - // Bob and Charlie voted no - await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); - // 0 for maybe - await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); - }; - - // check counts are correct in main timeline tile - await expectVoteCounts(page.locator(".mx_RoomView_body")); - - // and in thread view tile - await expectVoteCounts(page.locator(".mx_ThreadView")); - }); + test( + "should be displayed correctly in thread panel", + { tag: "@screenshot" }, + async ({ page, app, user, bot, homeserver }) => { + const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); + await botCharlie.prepareClient(); + + const roomId: string = await app.client.createRoom({}); + await app.client.inviteUser(roomId, bot.credentials.userId); + await app.client.inviteUser(roomId, botCharlie.credentials.userId); + await page.goto("/#/room/" + roomId); + + // wait until the bots joined + await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ + timeout: 10000, + }); + + const locator = await app.openMessageComposerOptions(); + await locator.getByRole("menuitem", { name: "Poll" }).click(); + + const pollParams = { + title: "Does the polls feature work?", + options: ["Yes", "No", "Maybe"], + }; + await createPoll(page, pollParams); + + // Wait for message to send, get its ID and save as @pollId + const pollId = await page + .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") + .filter({ hasText: pollParams.title }) + .getAttribute("data-scroll-tokens"); + + // Bob starts thread on the poll + await bot.sendMessage( + roomId, + { + body: "Hello there", + msgtype: "m.text", + }, + pollId, + ); + + // open the thread summary + await page.getByRole("button", { name: "Open thread" }).click(); + + // Bob votes 'Maybe' in the poll + await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); + + // Charlie votes 'No' + await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); + + // no votes shown until I vote, check votes have arrived in main tl + await expect( + page + .locator(".mx_RoomView_body .mx_MPollBody_totalVotes") + .getByText("2 votes cast. Vote to see the results"), + ).toBeAttached(); + + // and thread view + await expect( + page + .locator(".mx_ThreadView .mx_MPollBody_totalVotes") + .getByText("2 votes cast. Vote to see the results"), + ).toBeAttached(); + + // Take snapshots of poll on ThreadView + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible(); + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_a_poll_on_bubble_layout.png", + { + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible(); + + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + "ThreadView_with_a_poll_on_group_layout.png", + { + mask: [page.locator(".mx_MessageTimestamp")], + }, + ); + + const roomViewLocator = page.locator(".mx_RoomView_body"); + // vote 'Maybe' in the main timeline poll + await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); + // both me and bob have voted Maybe + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); + + const threadViewLocator = page.locator(".mx_ThreadView"); + // votes updated in thread view too + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); + // change my vote to 'Yes' + await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); + + // Bob updates vote to 'No' + await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); + + // me: yes, bob: no, charlie: no + const expectVoteCounts = async (optLocator: Locator) => { + // I voted yes + await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); + // Bob and Charlie voted no + await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); + // 0 for maybe + await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); + }; + + // check counts are correct in main timeline tile + await expectVoteCounts(page.locator(".mx_RoomView_body")); + + // and in thread view tile + await expectVoteCounts(page.locator(".mx_ThreadView")); + }, + ); }); diff --git a/playwright/e2e/register/email.spec.ts b/playwright/e2e/register/email.spec.ts index 7a80f0bbf7a..665e20ef01f 100644 --- a/playwright/e2e/register/email.spec.ts +++ b/playwright/e2e/register/email.spec.ts @@ -38,34 +38,33 @@ test.describe("Email Registration", async () => { await page.goto("/#/register"); }); - test("registers an account and lands on the use case selection screen", async ({ - page, - mailhog, - request, - checkA11y, - }) => { - await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); - // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; + test( + "registers an account and lands on the use case selection screen", + { tag: "@screenshot" }, + async ({ page, mailhog, request, checkA11y }) => { + await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); + // Hide the server text as it contains the randomly allocated Homeserver port + const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; - await page.getByRole("textbox", { name: "Username" }).fill("alice"); - await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); - await page.getByPlaceholder("Confirm password").fill("totally a great password"); - await page.getByPlaceholder("Email").fill("alice@email.com"); - await page.getByRole("button", { name: "Register" }).click(); + await page.getByRole("textbox", { name: "Username" }).fill("alice"); + await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); + await page.getByPlaceholder("Confirm password").fill("totally a great password"); + await page.getByPlaceholder("Email").fill("alice@email.com"); + await page.getByRole("button", { name: "Register" }).click(); - await expect(page.getByText("Check your email to continue")).toBeVisible(); - await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); - await checkA11y(); + await expect(page.getByText("Check your email to continue")).toBeVisible(); + await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); + await checkA11y(); - await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); + await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible(); - const messages = await mailhog.api.messages(); - expect(messages.items).toHaveLength(1); - expect(messages.items[0].to).toEqual("alice@email.com"); - const [emailLink] = messages.items[0].text.match(/http.+/); - await request.get(emailLink); // "Click" the link in the email + const messages = await mailhog.api.messages(); + expect(messages.items).toHaveLength(1); + expect(messages.items[0].to).toEqual("alice@email.com"); + const [emailLink] = messages.items[0].text.match(/http.+/); + await request.get(emailLink); // "Click" the link in the email - await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); - }); + await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); + }, + ); }); diff --git a/playwright/e2e/register/register.spec.ts b/playwright/e2e/register/register.spec.ts index 2dd3779573d..19608ee174d 100644 --- a/playwright/e2e/register/register.spec.ts +++ b/playwright/e2e/register/register.spec.ts @@ -15,66 +15,73 @@ test.describe("Registration", () => { await page.goto("/#/register"); }); - test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => { - await page.getByRole("button", { name: "Edit", exact: true }).click(); - await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); - - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); - await checkA11y(); - - await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); - await page.getByRole("button", { name: "Continue", exact: true }).click(); - // wait for the dialog to go away - await expect(page.getByRole("dialog")).not.toBeVisible(); - - await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); - // Hide the server text as it contains the randomly allocated Homeserver port - const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true }; - await expect(page).toMatchScreenshot("registration.png", screenshotOptions); - await checkA11y(); - - await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); - await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); - await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password"); - await page.getByRole("button", { name: "Register", exact: true }).click(); - - const dialog = page.getByRole("dialog"); - await expect(dialog).toBeVisible(); - await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); - await checkA11y(); - await dialog.getByRole("button", { name: "Continue", exact: true }).click(); - - await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); - await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); - await checkA11y(); - - const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); - await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link - await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); - - await page.getByRole("button", { name: "Accept", exact: true }).click(); - - await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); - await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); - await checkA11y(); - await page.getByRole("button", { name: "Skip", exact: true }).click(); - - await expect(page).toHaveURL(/\/#\/home$/); - - /* - * Cross-signing checks - */ - // check that the device considers itself verified - await page.getByRole("button", { name: "User menu", exact: true }).click(); - await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); - await page.getByRole("tab", { name: "Sessions", exact: true }).click(); - await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText( - "Verified", - ); - - // check that cross-signing keys have been uploaded. - await crypto.assertDeviceIsCrossSigned(); - }); + test( + "registers an account and lands on the home screen", + { tag: "@screenshot" }, + async ({ homeserver, page, checkA11y, crypto }) => { + await page.getByRole("button", { name: "Edit", exact: true }).click(); + await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png"); + await checkA11y(); + + await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); + await page.getByRole("button", { name: "Continue", exact: true }).click(); + // wait for the dialog to go away + await expect(page.getByRole("dialog")).not.toBeVisible(); + + await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); + // Hide the server text as it contains the randomly allocated Homeserver port + const screenshotOptions = { + mask: [page.locator(".mx_ServerPicker_server")], + includeDialogBackground: true, + }; + await expect(page).toMatchScreenshot("registration.png", screenshotOptions); + await checkA11y(); + + await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); + await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); + await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password"); + await page.getByRole("button", { name: "Register", exact: true }).click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); + await checkA11y(); + await dialog.getByRole("button", { name: "Continue", exact: true }).click(); + + await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); + await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); + await checkA11y(); + + const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); + await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link + await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); + + await page.getByRole("button", { name: "Accept", exact: true }).click(); + + await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); + await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); + await checkA11y(); + await page.getByRole("button", { name: "Skip", exact: true }).click(); + + await expect(page).toHaveURL(/\/#\/home$/); + + /* + * Cross-signing checks + */ + // check that the device considers itself verified + await page.getByRole("button", { name: "User menu", exact: true }).click(); + await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); + await page.getByRole("tab", { name: "Sessions", exact: true }).click(); + await expect( + page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"), + ).toHaveText("Verified"); + + // check that cross-signing keys have been uploaded. + await crypto.assertDeviceIsCrossSigned(); + }, + ); test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { await page.getByRole("button", { name: "Edit", exact: true }).click(); diff --git a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts index 57c27caf1a5..e18d72ddba9 100644 --- a/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts +++ b/playwright/e2e/release-announcement/releaseAnnouncement.spec.ts @@ -18,7 +18,7 @@ test.describe("Release announcement", () => { labsFlags: ["threadsActivityCentre"], }); - test("should display the release announcement process", async ({ page, app, util }) => { + test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => { // The TAC release announcement should be displayed await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); // Hide the release announcement diff --git a/playwright/e2e/right-panel/file-panel.spec.ts b/playwright/e2e/right-panel/file-panel.spec.ts index 1cb39aad256..c535bcdfbb6 100644 --- a/playwright/e2e/right-panel/file-panel.spec.ts +++ b/playwright/e2e/right-panel/file-panel.spec.ts @@ -40,7 +40,7 @@ test.describe("FilePanel", () => { }); test.describe("render", () => { - test("should render empty state", async ({ page }) => { + test("should render empty state", { tag: "@screenshot" }, async ({ page }) => { // Wait until the information about the empty state is rendered await expect(page.locator(".mx_EmptyState")).toBeVisible(); @@ -48,7 +48,7 @@ test.describe("FilePanel", () => { await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); }); - test("should list tiles on the panel", async ({ page }) => { + test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => { // Upload multiple files await uploadFile(page, "playwright/sample-files/riot.png"); // Image await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio diff --git a/playwright/e2e/right-panel/notification-panel.spec.ts b/playwright/e2e/right-panel/notification-panel.spec.ts index 1d10af97988..55a6be04505 100644 --- a/playwright/e2e/right-panel/notification-panel.spec.ts +++ b/playwright/e2e/right-panel/notification-panel.spec.ts @@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => { await app.client.createRoom({ name: ROOM_NAME }); }); - test("should render empty state", async ({ page, app }) => { + test("should render empty state", { tag: "@screenshot" }, async ({ page, app }) => { await app.viewRoomByName(ROOM_NAME); await page.getByRole("button", { name: "Notifications" }).click(); diff --git a/playwright/e2e/right-panel/right-panel.spec.ts b/playwright/e2e/right-panel/right-panel.spec.ts index 110f24a5019..1e9b8ebe1d4 100644 --- a/playwright/e2e/right-panel/right-panel.spec.ts +++ b/playwright/e2e/right-panel/right-panel.spec.ts @@ -38,7 +38,7 @@ test.describe("RightPanel", () => { }); test.describe("in rooms", () => { - test("should handle long room address and long room name", async ({ page, app }) => { + test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => { await app.client.createRoom({ name: ROOM_NAME_LONG }); await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); diff --git a/playwright/e2e/room-directory/room-directory.spec.ts b/playwright/e2e/room-directory/room-directory.spec.ts index f078a858a2a..f299a929bb7 100644 --- a/playwright/e2e/room-directory/room-directory.spec.ts +++ b/playwright/e2e/room-directory/room-directory.spec.ts @@ -47,34 +47,40 @@ test.describe("Room Directory", () => { expect(resp.chunk[0].room_id).toEqual(roomId); }); - test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => { - const name = "This is a public room"; - await bot.createRoom({ - visibility: "public" as Visibility, - name, - room_alias_name: "test1234", - }); - - await page.getByRole("button", { name: "Explore rooms" }).click(); - - const dialog = page.locator(".mx_SpotlightDialog"); - await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); - await expect( - dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."), - ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); - - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); - - await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); - await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); - - await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); - - await page - .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") - .getByRole("button", { name: "Join" }) - .click(); - - await expect(page).toHaveURL("/#/room/#test1234:localhost"); - }); + test( + "should allow finding published rooms in directory", + { tag: "@screenshot" }, + async ({ page, app, user, bot }) => { + const name = "This is a public room"; + await bot.createRoom({ + visibility: "public" as Visibility, + name, + room_alias_name: "test1234", + }); + + await page.getByRole("button", { name: "Explore rooms" }).click(); + + const dialog = page.locator(".mx_SpotlightDialog"); + await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); + await expect( + dialog.getByText( + "If you can't find the room you're looking for, ask for an invite or create a new room.", + ), + ).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png"); + + await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); + await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); + + await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); + + await page + .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") + .getByRole("button", { name: "Join" }) + .click(); + + await expect(page).toHaveURL("/#/room/#test1234:localhost"); + }, + ); }); diff --git a/playwright/e2e/room/room-header.spec.ts b/playwright/e2e/room/room-header.spec.ts index 6ecf4b3b33b..971508b25bb 100644 --- a/playwright/e2e/room/room-header.spec.ts +++ b/playwright/e2e/room/room-header.spec.ts @@ -20,7 +20,7 @@ test.describe("Room Header", () => { test.use({ labsFlags: ["feature_notifications"], }); - test("should render default buttons properly", async ({ page, app, user }) => { + test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => { await app.client.createRoom({ name: "Test Room" }); await app.viewRoomByName("Test Room"); @@ -51,34 +51,38 @@ test.describe("Room Header", () => { await expect(header).toMatchScreenshot("room-header.png"); }); - test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => { - const LONG_ROOM_NAME = - "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 ut " + - "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + - "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + - "officia deserunt mollit anim id est laborum."; + test( + "should render a very long room name without collapsing the buttons", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + const LONG_ROOM_NAME = + "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 ut " + + "aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + + "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + + "officia deserunt mollit anim id est laborum."; - await app.client.createRoom({ name: LONG_ROOM_NAME }); - await app.viewRoomByName(LONG_ROOM_NAME); + await app.client.createRoom({ name: LONG_ROOM_NAME }); + await app.viewRoomByName(LONG_ROOM_NAME); - const header = page.locator(".mx_RoomHeader"); - // Wait until the room name is set - await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); - - // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed - // Note these assertions do not check the size of mx_LegacyRoomHeader_name button - const buttons = header.locator(".mx_Flex").getByRole("button"); - await expect(buttons).toHaveCount(5); - - for (const button of await buttons.all()) { - await expect(button).toBeVisible(); - await expect(button).toHaveCSS("height", "32px"); - await expect(button).toHaveCSS("width", "32px"); - } - - await expect(header).toMatchScreenshot("room-header-long-name.png"); - }); + const header = page.locator(".mx_RoomHeader"); + // Wait until the room name is set + await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); + + // Assert the size of buttons on RoomHeader are specified and the buttons are not compressed + // Note these assertions do not check the size of mx_LegacyRoomHeader_name button + const buttons = header.locator(".mx_Flex").getByRole("button"); + await expect(buttons).toHaveCount(5); + + for (const button of await buttons.all()) { + await expect(button).toBeVisible(); + await expect(button).toHaveCSS("height", "32px"); + await expect(button).toHaveCSS("width", "32px"); + } + + await expect(header).toMatchScreenshot("room-header-long-name.png"); + }, + ); }); test.describe("with a video room", () => { @@ -99,30 +103,34 @@ test.describe("Room Header", () => { test.describe("and with feature_notifications enabled", () => { test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] }); - test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => { - await createVideoRoom(page, app); + test( + "should render buttons for chat, room info, threads and facepile", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await createVideoRoom(page, app); - const header = page.locator(".mx_RoomHeader"); + const header = page.locator(".mx_RoomHeader"); - // There's two room info button - the header itself and the i button - const infoButtons = header.getByRole("button", { name: "Room info" }); - await expect(infoButtons).toHaveCount(2); - await expect(infoButtons.first()).toBeVisible(); - await expect(infoButtons.last()).toBeVisible(); + // There's two room info button - the header itself and the i button + const infoButtons = header.getByRole("button", { name: "Room info" }); + await expect(infoButtons).toHaveCount(2); + await expect(infoButtons.first()).toBeVisible(); + await expect(infoButtons.last()).toBeVisible(); - // Facepile - await expect(header.locator(".mx_FacePile")).toBeVisible(); + // Facepile + await expect(header.locator(".mx_FacePile")).toBeVisible(); - // Chat, Threads and Notification buttons - await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); - await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); - await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); + // Chat, Threads and Notification buttons + await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); + await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); - // Assert that there is not a button except those buttons - await expect(header.getByRole("button")).toHaveCount(7); + // Assert that there is not a button except those buttons + await expect(header.getByRole("button")).toHaveCount(7); - await expect(header).toMatchScreenshot("room-header-video-room.png"); - }); + await expect(header).toMatchScreenshot("room-header-video-room.png"); + }, + ); }); test("should render a working chat button which opens the timeline on a right panel", async ({ diff --git a/playwright/e2e/settings/account-user-settings-tab.spec.ts b/playwright/e2e/settings/account-user-settings-tab.spec.ts index 5492094f937..7390ccfd8d1 100644 --- a/playwright/e2e/settings/account-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/account-user-settings-tab.spec.ts @@ -23,7 +23,7 @@ test.describe("Account user settings tab", () => { }, }); - test("should be rendered properly", async ({ uut, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => { await expect(uut).toMatchScreenshot("account.png"); // Assert that the top heading is rendered @@ -71,7 +71,7 @@ test.describe("Account user settings tab", () => { ); }); - test("should respond to small screen sizes", async ({ page, uut }) => { + test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => { await page.setViewportSize({ width: 700, height: 600 }); await expect(uut).toMatchScreenshot("account-smallscreen.png"); }); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts index de6c9c527a6..c60ecb99d25 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/appearance-user-settings-tab.spec.ts @@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => { displayName: "Hanako", }); - test("should be rendered properly", async ({ page, user, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => { const tab = await app.settings.openUserSettings("Appearance"); // Click "Show advanced" link button @@ -25,19 +25,23 @@ test.describe("Appearance user settings tab", () => { await expect(tab).toMatchScreenshot("appearance-tab.png"); }); - test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { - await app.settings.openUserSettings("Appearance"); + test( + "should support changing font size by using the font size dropdown", + { tag: "@screenshot" }, + async ({ page, app, user }) => { + await app.settings.openUserSettings("Appearance"); - const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); - const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); - await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); + const tab = page.getByTestId("mx_AppearanceUserSettingsTab"); + const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); + await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); - // Default browser font size is 16px and the select value is 0 - // -4 value is 12px - await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); + // Default browser font size is 16px and the select value is 0 + // -4 value is 12px + await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); - await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); - }); + await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true }); + }, + ); test("should support enabling system font", async ({ page, app, user }) => { await app.settings.openUserSettings("Appearance"); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts index a0288baf1dd..157942a5853 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/message-layout-panel.ts @@ -20,20 +20,24 @@ test.describe("Appearance user settings tab", () => { await util.openAppearanceTab(); }); - test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { - await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); - - await util.getBubbleLayout().click(); - - // Assert that modern are irc layout are not selected - await expect(util.getBubbleLayout()).toBeChecked(); - await expect(util.getModernLayout()).not.toBeChecked(); - await expect(util.getIRCLayout()).not.toBeChecked(); - - // Assert that the room layout is set to bubble layout - await util.assertBubbleLayout(); - await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); - }); + test( + "should change the message layout from modern to bubble", + { tag: "@screenshot" }, + async ({ page, app, user, util }) => { + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); + + await util.getBubbleLayout().click(); + + // Assert that modern are irc layout are not selected + await expect(util.getBubbleLayout()).toBeChecked(); + await expect(util.getModernLayout()).not.toBeChecked(); + await expect(util.getIRCLayout()).not.toBeChecked(); + + // Assert that the room layout is set to bubble layout + await util.assertBubbleLayout(); + await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); + }, + ); test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => { await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); diff --git a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts index 4f3b75b5baa..63b53caa23e 100644 --- a/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts +++ b/playwright/e2e/settings/appearance-user-settings-tab/theme-choice-panel.spec.ts @@ -20,31 +20,39 @@ test.describe("Appearance user settings tab", () => { await util.openAppearanceTab(); }); - test("should be rendered with the light theme selected", async ({ page, app, util }) => { - // Assert that 'Match system theme' is not checked - await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); - - // Assert that the light theme is selected - await expect(util.getLightTheme()).toBeChecked(); - // Assert that the dark and high contrast themes are not selected - await expect(util.getDarkTheme()).not.toBeChecked(); - await expect(util.getHighContrastTheme()).not.toBeChecked(); - - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); - }); - - test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { - await util.getMatchSystemThemeCheckbox().click(); - - // Assert that the themes are disabled - await expect(util.getLightTheme()).toBeDisabled(); - await expect(util.getDarkTheme()).toBeDisabled(); - await expect(util.getHighContrastTheme()).toBeDisabled(); - - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); - }); - - test("should change the theme to dark", async ({ page, app, util }) => { + test( + "should be rendered with the light theme selected", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + // Assert that 'Match system theme' is not checked + await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); + + // Assert that the light theme is selected + await expect(util.getLightTheme()).toBeChecked(); + // Assert that the dark and high contrast themes are not selected + await expect(util.getDarkTheme()).not.toBeChecked(); + await expect(util.getHighContrastTheme()).not.toBeChecked(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); + }, + ); + + test( + "should disable the themes when the system theme is clicked", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await util.getMatchSystemThemeCheckbox().click(); + + // Assert that the themes are disabled + await expect(util.getLightTheme()).toBeDisabled(); + await expect(util.getDarkTheme()).toBeDisabled(); + await expect(util.getHighContrastTheme()).toBeDisabled(); + + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); + }, + ); + + test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => { // Assert that the light theme is selected await expect(util.getLightTheme()).toBeChecked(); @@ -63,19 +71,23 @@ test.describe("Appearance user settings tab", () => { labsFlags: ["feature_custom_themes"], }); - test("should render the custom theme section", async ({ page, app, util }) => { + test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => { await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png"); }); - test("should be able to add and remove a custom theme", async ({ page, app, util }) => { - await util.addCustomTheme(); + test( + "should be able to add and remove a custom theme", + { tag: "@screenshot" }, + async ({ page, app, util }) => { + await util.addCustomTheme(); - await expect(util.getCustomTheme()).not.toBeChecked(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); + await expect(util.getCustomTheme()).not.toBeChecked(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); - await util.removeCustomTheme(); - await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); - }); + await util.removeCustomTheme(); + await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); + }, + ); }); }); }); diff --git a/playwright/e2e/settings/general-room-settings-tab.spec.ts b/playwright/e2e/settings/general-room-settings-tab.spec.ts index 47582bf0c0e..828ba5285bb 100644 --- a/playwright/e2e/settings/general-room-settings-tab.spec.ts +++ b/playwright/e2e/settings/general-room-settings-tab.spec.ts @@ -20,7 +20,7 @@ test.describe("General room settings tab", () => { await app.viewRoomByName(roomName); }); - test("should be rendered properly", async ({ page, app }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => { const settings = await app.settings.openRoomSettings("General"); // Assert that "Show less" details element is rendered diff --git a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts index 0880853ee81..8dc2570b426 100644 --- a/playwright/e2e/settings/preferences-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/preferences-user-settings-tab.spec.ts @@ -23,7 +23,7 @@ test.describe("Preferences user settings tab", () => { }, }); - test("should be rendered properly", async ({ app, page, user }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => { page.setViewportSize({ width: 1024, height: 3300 }); const tab = await app.settings.openUserSettings("Preferences"); // Assert that the top heading is rendered diff --git a/playwright/e2e/settings/security-user-settings-tab.spec.ts b/playwright/e2e/settings/security-user-settings-tab.spec.ts index 6eab8306232..e7562698c33 100644 --- a/playwright/e2e/settings/security-user-settings-tab.spec.ts +++ b/playwright/e2e/settings/security-user-settings-tab.spec.ts @@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => { }); test.describe("AnalyticsLearnMoreDialog", () => { - test("should be rendered properly", async ({ app, page }) => { + test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => { const tab = await app.settings.openUserSettings("Security"); await tab.getByRole("button", { name: "Learn more" }).click(); await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( diff --git a/playwright/e2e/share-dialog/share-dialog.spec.ts b/playwright/e2e/share-dialog/share-dialog.spec.ts index 2999b74ca03..e0993dd1bc4 100644 --- a/playwright/e2e/share-dialog/share-dialog.spec.ts +++ b/playwright/e2e/share-dialog/share-dialog.spec.ts @@ -16,7 +16,7 @@ test.describe("Share dialog", () => { }, }); - test("should share a room", async ({ page, app, room }) => { + test("should share a room", { tag: "@screenshot" }, async ({ page, app, room }) => { await app.viewRoomById(room.roomId); await app.toggleRoomInfoPanel(); await page.getByRole("menuitem", { name: "Copy link" }).click(); @@ -29,7 +29,7 @@ test.describe("Share dialog", () => { }); }); - test("should share a room member", async ({ page, app, room, user }) => { + test("should share a room member", { tag: "@screenshot" }, async ({ page, app, room, user }) => { await app.viewRoomById(room.roomId); await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); @@ -46,7 +46,7 @@ test.describe("Share dialog", () => { }); }); - test("should share an event", async ({ page, app, room }) => { + test("should share an event", { tag: "@screenshot" }, async ({ page, app, room }) => { await app.viewRoomById(room.roomId); await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); diff --git a/playwright/e2e/spaces/spaces.spec.ts b/playwright/e2e/spaces/spaces.spec.ts index 575450c6414..233cdee3b4b 100644 --- a/playwright/e2e/spaces/spaces.spec.ts +++ b/playwright/e2e/spaces/spaces.spec.ts @@ -55,7 +55,7 @@ test.describe("Spaces", () => { botCreateOpts: { displayName: "BotBob" }, }); - test("should allow user to create public space", async ({ page, app, user }) => { + test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => { const contextMenu = await openSpaceCreateMenu(page); await expect(contextMenu).toMatchScreenshot("space-create-menu.png"); @@ -88,7 +88,7 @@ test.describe("Spaces", () => { await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible(); }); - test("should allow user to create private space", async ({ page, app, user }) => { + test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => { const menu = await openSpaceCreateMenu(page); await menu.getByRole("button", { name: "Private" }).click(); @@ -216,49 +216,47 @@ test.describe("Spaces", () => { await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible(); }); - test("should render subspaces in the space panel only when expanded", async ({ - page, - app, - user, - axe, - checkA11y, - }) => { - axe.disableRules([ - // Disable this check as it triggers on nested roving tab index elements which are in practice fine - "nested-interactive", - // XXX: We have some known contrast issues here - "color-contrast", - ]); - - const childSpaceId = await app.client.createSpace({ - name: "Child Space", - initial_state: [], - }); - await app.client.createSpace({ - name: "Root Space", - initial_state: [spaceChildInitialState(childSpaceId)], - }); - - // Find collapsed Space panel - const spaceTree = page.getByRole("tree", { name: "Spaces" }); - await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); - await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); - - await checkA11y(); - await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); - - // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another - // button with the same name with different class name "mx_SpacePanel_toggleCollapse". - await spaceTree.getByRole("button", { name: "Expand" }).click(); - await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector - - const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); - await expect(item).toBeVisible(); - await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); - - await checkA11y(); - await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); - }); + test( + "should render subspaces in the space panel only when expanded", + { tag: "@screenshot" }, + async ({ page, app, user, axe, checkA11y }) => { + axe.disableRules([ + // Disable this check as it triggers on nested roving tab index elements which are in practice fine + "nested-interactive", + // XXX: We have some known contrast issues here + "color-contrast", + ]); + + const childSpaceId = await app.client.createSpace({ + name: "Child Space", + initial_state: [], + }); + await app.client.createSpace({ + name: "Root Space", + initial_state: [spaceChildInitialState(childSpaceId)], + }); + + // Find collapsed Space panel + const spaceTree = page.getByRole("tree", { name: "Spaces" }); + await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); + await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); + + await checkA11y(); + await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); + + // This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another + // button with the same name with different class name "mx_SpacePanel_toggleCollapse". + await spaceTree.getByRole("button", { name: "Expand" }).click(); + await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector + + const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); + await expect(item).toBeVisible(); + await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); + + await checkA11y(); + await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); + }, + ); test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({ page, diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts index 452c9a520ac..ecf458c0600 100644 --- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts +++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts @@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => { labsFlags: ["threadsActivityCentre"], }); - test("should have the button correctly aligned and displayed in the space panel when expanded", async ({ - util, - }) => { - // Open the space panel - await util.expandSpacePanel(); - // The buttons in the space panel should be aligned when expanded - await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); - }); - - test("should not show indicator when there is no thread", async ({ room1, util }) => { + test( + "should have the button correctly aligned and displayed in the space panel when expanded", + { tag: "@screenshot" }, + async ({ util }) => { + // Open the space panel + await util.expandSpacePanel(); + // The buttons in the space panel should be aligned when expanded + await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); + }, + ); + + test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => { // No indicator should be shown await util.assertNoTacIndicator(); @@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => { await util.assertHighlightIndicator(); }); - test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => { + test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); // The indicator should be shown @@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); }); - test("should update with a thread is read", async ({ room1, room2, util, msg }) => { + test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => { await util.goTo(room2); await util.populateThreads(room1, room2, msg); @@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => { await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible(); }); - test("should have the correct hover state", async ({ util, page }) => { + test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => { await util.hoverTacButton(); await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png"); @@ -138,7 +140,7 @@ test.describe("Threads Activity Centre", () => { await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png"); }); - test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => { + test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => { await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]); await util.assertNotificationTac(); diff --git a/playwright/e2e/threads/threads.spec.ts b/playwright/e2e/threads/threads.spec.ts index a2642a49d16..06ec57653c7 100644 --- a/playwright/e2e/threads/threads.spec.ts +++ b/playwright/e2e/threads/threads.spec.ts @@ -25,7 +25,7 @@ test.describe("Threads", () => { }); // Flaky: https://github.com/vector-im/element-web/issues/26452 - test.skip("should be usable for a conversation", async ({ page, app, bot }) => { + test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => { const roomId = await app.client.createRoom({}); await app.client.inviteUser(roomId, bot.credentials.userId); await bot.joinRoom(roomId); @@ -150,7 +150,7 @@ test.describe("Threads", () => { ).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart); // Take snapshot of group layout (IRC layout is not available on ThreadView) - expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( "ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png", { mask: mask, @@ -174,7 +174,7 @@ test.describe("Threads", () => { .toHaveCSS("margin-inline-start", "0px"); // Take snapshot of bubble layout - expect(page.locator(".mx_ThreadView")).toMatchScreenshot( + await expect(page.locator(".mx_ThreadView")).toMatchScreenshot( "ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png", { mask: mask, @@ -351,57 +351,61 @@ test.describe("Threads", () => { }); }); - 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 maplibregl-map from snapshots - const css = - ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; + test( + "should send location and reply to the location on ThreadView", + { tag: "@screenshot" }, + 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); - 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"); + // Exclude timestamp, read marker, and maplibregl-map from snapshots + const css = + ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; - // 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, - }); + 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(); + // 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"); - }); + // 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 diff --git a/playwright/e2e/timeline/timeline.spec.ts b/playwright/e2e/timeline/timeline.spec.ts index e8ef0e577c2..7aaabb9759d 100644 --- a/playwright/e2e/timeline/timeline.spec.ts +++ b/playwright/e2e/timeline/timeline.spec.ts @@ -137,182 +137,190 @@ test.describe("Timeline", () => { }); 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 screenshot - 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(); + test( + "should create and configure a room on IRC layout", + { tag: "@screenshot" }, + 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 screenshot + 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", + { tag: "@screenshot" }, + 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_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // 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(); + test( + "should have an expanded generic event list summary (GELS) on compact modern/group layout", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await page.goto(`/#/room/${room.roomId}`); - 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(); + // Set compact modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); - await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", { - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + // 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(); + test( + "should click 'collapse' on the first hovered info event line inside GELS on bubble layout", + { tag: "@screenshot" }, + 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 - // 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")], - }); - }); + 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 - test("should add inline start margin to an event line on IRC layout", async ({ - page, - app, - room, - axe, - checkA11y, - }) => { - axe.disableRules("color-contrast"); + // 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")], + }); - await page.goto(`/#/room/${room.roomId}`); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + // 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(); - // 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(); + // Assert that "collapse" link button worked + await expect(gels.getByRole("button", { name: "Expand" })).toBeVisible(); - // Click "expand" link button - await page.locator(".mx_GenericEventListSummary").getByRole("button", { name: "Expand" }).click(); + // 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")], + }); + }, + ); - // 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 + test( + "should add inline start margin to an event line on IRC layout", + { tag: "@screenshot" }, + async ({ page, app, room, axe, checkA11y }) => { + axe.disableRules("color-contrast"); - 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 page.goto(`/#/room/${room.roomId}`); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - 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: ` + // 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(); - }); + }, + ); + await checkA11y(); + }, + ); }); test.describe("message displaying", () => { @@ -332,289 +340,311 @@ test.describe("Timeline", () => { ).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 + test( + "should align generic event list summary with messages and emote on IRC layout", + { tag: "@screenshot" }, + 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); + 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(); + // 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")], + }, + ); - // 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"); + // 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")], + }, + ); - // Make sure the second message was sent - await expect( - page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"), - ).toBeVisible(); + // 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")], + }, + ); - // 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", - { + // 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")], - }, - ); - - // 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: ` + }); + }, + ); + + test( + "should render EventTiles on IRC, modern (group), and bubble layout", + { tag: "@screenshot" }, + 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 sendEvent(app.client, room.roomId); + await sendEvent(app.client, room.roomId); // check continuation + await sendEvent(app.client, room.roomId); // check the last EventTile - await expect(page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒")).toBeVisible(); + 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"); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // IRC layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + await expect( + page.locator(".mx_RoomView").getByText("This message has an inline emoji 👒"), + ).toBeVisible(); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // IRC layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Wait until configuration is finished - await expect( - page - .locator(".mx_GenericEventListSummary_summary") - .getByText(`${OLD_NAME} created and configured the room.`), - ).toBeVisible(); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); - await app.timeline.scrollToBottom(); - 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, - ); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(`${OLD_NAME} created and configured the room.`), + ).toBeVisible(); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Group/modern layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + await app.timeline.scrollToBottom(); + 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, + ); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Group/modern layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Check that the last EventTile is rendered - await app.timeline.scrollToBottom(); - 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, - ); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); - // 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.timeline.scrollToBottom(); + 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 that the last EventTile is rendered - await app.timeline.scrollToBottom(); - 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, - ); + // Check the same thing for compact layout + await app.settings.setValue("useCompactLayout", null, SettingLevel.DEVICE, true); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Message bubble layout - //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Check that the last EventTile is rendered + await app.timeline.scrollToBottom(); + 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, + ); - await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Message bubble layout + //////////////////////////////////////////////////////////////////////////////////////////////////////////// - await app.timeline.scrollToBottom(); - 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, - ); - }); + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); - test("should set inline start padding to a hidden event line", async ({ page, app, room }) => { - test.skip( - true, - "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", - ); - 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(); + await app.timeline.scrollToBottom(); + 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", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + test.skip( + true, + "Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890", + ); + 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); + // Edit message + await messageEdit(page); - // Click timestamp to highlight hidden event line - await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click(); + // 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"); + // 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: ` + // 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, - ); + 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"); + // 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, - ); - }); + 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 }) => { + test("should click view source event toggle", { tag: "@screenshot" }, 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 @@ -712,89 +742,97 @@ test.describe("Timeline", () => { ).toBeVisible(); }); - test("should render url previews", async ({ page, app, room, axe, checkA11y, context }) => { - axe.disableRules("color-contrast"); - - // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but - // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it - // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because - // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until - // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. - await context.route( - "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", - async (route) => { - await route.fulfill({ - path: "playwright/sample-files/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, - }, - }); - }, - ); + test( + "should render url previews", + { tag: "@screenshot" }, + async ({ page, app, room, axe, checkA11y, context }) => { + axe.disableRules("color-contrast"); + + // Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but + // the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it + // post-worker, but we can't waitForResponse on that, so the page context is still used there. Because + // the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until + // the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully. + await context.route( + "**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", + async (route) => { + await route.fulfill({ + path: "playwright/sample-files/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=*"), - // see context.route above for why we listen for the unauthenticated endpoint - page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"), - ]; + const requestPromises: Promise[] = [ + page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"), + // see context.route above for why we listen for the unauthenticated endpoint + 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 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 expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible(); + await Promise.all(requestPromises); - await checkA11y(); + await checkA11y(); - await app.timeline.scrollToBottom(); - await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", { - // Exclude timestamp and read marker from snapshot - mask: [page.locator(".mx_MessageTimestamp")], - css: ` + await app.timeline.scrollToBottom(); + 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}`); + test( + "should highlight search result words regardless of formatting", + { tag: "@screenshot" }, + async ({ page, app, room }) => { + await sendEvent(app.client, room.roomId); + await sendEvent(app.client, room.roomId, true); + await page.goto(`/#/room/${room.roomId}`); - await app.toggleRoomInfoPanel(); + await app.toggleRoomInfoPanel(); - await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); - await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").fill("Message"); + await page.locator(".mx_RoomSummaryCard_search").getByRole("searchbox").press("Enter"); - await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); + await expect(page.locator(".mx_RoomSearchAuxPanel")).toMatchScreenshot("search-aux-panel.png"); - 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", - ); - }); + 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 }) => { + test("should render a fully opaque textual event", { tag: "@screenshot" }, async ({ page, app, room }) => { const stringToSearch = "Message"; // Same with string sent with sendEvent() await sendEvent(app.client, room.roomId); @@ -918,7 +956,7 @@ test.describe("Timeline", () => { ).toHaveCount(4); }); - test("should display a reply chain", async ({ page, app, room, homeserver }) => { + test("should display a reply chain", { tag: "@screenshot" }, async ({ page, app, room, homeserver }) => { const reply2 = "Reply again"; await page.goto(`/#/room/${room.roomId}`); @@ -1031,122 +1069,121 @@ test.describe("Timeline", () => { ); }); - 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"; - - const newDisplayName = `${LONG_STRING} 2`; - - // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing - // due to the generated random mxid being displayed inside the GELS summary. - // Note that we set it here as the test was failing on CI (but not locally!) if the name - // was changed afterwards. This is quite concerning, but maybe better than just disabling the - // whole test? - // https://github.com/element-hq/element-web/issues/27109 - await app.client.setDisplayName(newDisplayName); - - // Create a bot with a long display name - const bot = new Bot(page, homeserver, { - displayName: LONG_STRING, - autoAcceptInvites: false, - }); - await bot.prepareClient(); + test( + "should send, reply, and display long strings without overflowing", + { tag: "@screenshot" }, + 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"; + + const newDisplayName = `${LONG_STRING} 2`; + + // Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing + // due to the generated random mxid being displayed inside the GELS summary. + // Note that we set it here as the test was failing on CI (but not locally!) if the name + // was changed afterwards. This is quite concerning, but maybe better than just disabling the + // whole test? + // https://github.com/element-hq/element-web/issues/27109 + await app.client.setDisplayName(newDisplayName); + + // 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}`); + // 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(newDisplayName + " created and configured the room."), - ).toBeVisible(); - - // Have the bot send a long message - await bot.sendMessage(testRoomId, { - body: LONG_STRING, - msgtype: "m.text", - }); + // Wait until configuration is finished + await expect( + page + .locator(".mx_GenericEventListSummary_summary") + .getByText(newDisplayName + " created and configured the room."), + ).toBeVisible(); + + // 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(); + // 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"); + // 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(); + // 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); + await expect(eventTileLine.getByText(reply)).toHaveCount(1); - // Change the viewport size - await page.setViewportSize({ width: 1600, height: 1200 }); + // 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: ` + // 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 take a snapshot of the whole viewport - await app.timeline.scrollToBottom(); - // 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 IRC layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC); + // Scroll to the bottom to take a snapshot of the whole viewport + await app.timeline.scrollToBottom(); + // 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.timeline.scrollToBottom(); // 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 modern layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); + await app.timeline.scrollToBottom(); // 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.timeline.scrollToBottom(); // 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, - ); - }); + // Make sure the strings do not overflow on bubble layout + await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); + await app.timeline.scrollToBottom(); // 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, + ); + }, + ); async function testImageRendering(page: Page, app: ElementAppPage, room: { roomId: string }) { await app.viewRoomById(room.roomId); @@ -1176,7 +1213,7 @@ test.describe("Timeline", () => { ); } - test("should render images in the timeline", async ({ page, app, room, context }) => { + test("should render images in the timeline", { tag: "@screenshot" }, async ({ page, app, room, context }) => { await testImageRendering(page, app, room); }); @@ -1188,50 +1225,54 @@ test.describe("Timeline", () => { // In practice, this means this test will *always* succeed because it ends up relying on fallback behaviour tested // above (unless of course the above tests are also broken). test.describe("MSC3916 - Authenticated Media", () => { - test("should render authenticated images in the timeline", async ({ page, app, room, context }) => { - // Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events. - // See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing - - // Install our mocks and preventative measures - await context.route("**/_matrix/client/versions", async (route) => { - // Force enable MSC3916/Matrix 1.11, which may require the service worker's internal cache to be cleared later. - const json = await (await route.fetch()).json(); - if (!json["versions"]) json["versions"] = []; - json["versions"].push("v1.11"); - await route.fulfill({ json }); - }); - await context.route("**/_matrix/media/*/download/**", async (route) => { - // should not be called. We don't use `abort` so that it's clearer in the logs what happened. - await route.fulfill({ - status: 500, - json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + test( + "should render authenticated images in the timeline", + { tag: "@screenshot" }, + async ({ page, app, room, context }) => { + // Note: we have to use `context` instead of `page` for routing, otherwise we'll miss Service Worker events. + // See https://playwright.dev/docs/service-workers-experimental#network-events-and-routing + + // Install our mocks and preventative measures + await context.route("**/_matrix/client/versions", async (route) => { + // Force enable MSC3916/Matrix 1.11, which may require the service worker's internal cache to be cleared later. + const json = await (await route.fetch()).json(); + if (!json["versions"]) json["versions"] = []; + json["versions"].push("v1.11"); + await route.fulfill({ json }); }); - }); - await context.route("**/_matrix/media/*/thumbnail/**", async (route) => { - // should not be called. We don't use `abort` so that it's clearer in the logs what happened. - await route.fulfill({ - status: 500, - json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + await context.route("**/_matrix/media/*/download/**", async (route) => { + // should not be called. We don't use `abort` so that it's clearer in the logs what happened. + await route.fulfill({ + status: 500, + json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + }); }); - }); - await context.route("**/_matrix/client/v1/download/**", async (route) => { - expect(route.request().headers()["Authorization"]).toBeDefined(); - // we can't use route.continue() because no configured homeserver supports MSC3916 yet - await route.fulfill({ - body: NEW_AVATAR, + await context.route("**/_matrix/media/*/thumbnail/**", async (route) => { + // should not be called. We don't use `abort` so that it's clearer in the logs what happened. + await route.fulfill({ + status: 500, + json: { errcode: "M_UNKNOWN", error: "Unexpected route called." }, + }); }); - }); - await context.route("**/_matrix/client/v1/thumbnail/**", async (route) => { - expect(route.request().headers()["Authorization"]).toBeDefined(); - // we can't use route.continue() because no configured homeserver supports MSC3916 yet - await route.fulfill({ - body: NEW_AVATAR, + await context.route("**/_matrix/client/v1/download/**", async (route) => { + expect(route.request().headers()["Authorization"]).toBeDefined(); + // we can't use route.continue() because no configured homeserver supports MSC3916 yet + await route.fulfill({ + body: NEW_AVATAR, + }); + }); + await context.route("**/_matrix/client/v1/thumbnail/**", async (route) => { + expect(route.request().headers()["Authorization"]).toBeDefined(); + // we can't use route.continue() because no configured homeserver supports MSC3916 yet + await route.fulfill({ + body: NEW_AVATAR, + }); }); - }); - // We check the same screenshot because there should be no user-visible impact to using authentication. - await testImageRendering(page, app, room); - }); + // We check the same screenshot because there should be no user-visible impact to using authentication. + await testImageRendering(page, app, room); + }, + ); }); }); }); diff --git a/playwright/e2e/user-menu/user-menu.spec.ts b/playwright/e2e/user-menu/user-menu.spec.ts index 0ad21dbded9..268da00f30f 100644 --- a/playwright/e2e/user-menu/user-menu.spec.ts +++ b/playwright/e2e/user-menu/user-menu.spec.ts @@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test"; test.describe("User Menu", () => { test.use({ displayName: "Jeff" }); - test("should contain our name & userId", async ({ page, user }) => { + test("should contain our name & userId", { tag: "@screenshot" }, async ({ page, user }) => { await page.getByRole("button", { name: "User menu", exact: true }).click(); const menu = page.getByRole("menu"); diff --git a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts index f561eb9615e..b89fa3ac70a 100644 --- a/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts +++ b/playwright/e2e/user-onboarding/user-onboarding-new.spec.ts @@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => { await expect(page.locator(".mx_UserOnboardingList")).toBeVisible(); }); - test("page is shown and preference exists", async ({ page, app }) => { + test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => { await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot( "User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png", ); @@ -34,7 +34,7 @@ test.describe("User Onboarding (new user)", () => { await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible(); }); - test("app download dialog", async ({ page }) => { + test("app download dialog", { tag: "@screenshot" }, async ({ page }) => { await page.getByRole("button", { name: "Download apps" }).click(); await expect( page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }), diff --git a/playwright/e2e/user-view/user-view.spec.ts b/playwright/e2e/user-view/user-view.spec.ts index 218c63fe743..ff8e9684e93 100644 --- a/playwright/e2e/user-view/user-view.spec.ts +++ b/playwright/e2e/user-view/user-view.spec.ts @@ -14,7 +14,7 @@ test.describe("UserView", () => { botCreateOpts: { displayName: "Usman" }, }); - test("should render the user view as expected", async ({ page, homeserver, user, bot }) => { + test("should render the user view as expected", { tag: "@screenshot" }, async ({ page, homeserver, user, bot }) => { await page.goto(`/#/user/${bot.credentials.userId}`); const rightPanel = page.locator("#mx_RightPanel"); diff --git a/playwright/e2e/widgets/layout.spec.ts b/playwright/e2e/widgets/layout.spec.ts index 41cfece6e80..c80ea44078e 100644 --- a/playwright/e2e/widgets/layout.spec.ts +++ b/playwright/e2e/widgets/layout.spec.ts @@ -70,7 +70,7 @@ test.describe("Widget Layout", () => { await app.viewRoomByName(ROOM_NAME); }); - test("should be set properly", async ({ page }) => { + test("should be set properly", { tag: "@screenshot" }, async ({ page }) => { await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png"); }); diff --git a/playwright/element-web-test.ts b/playwright/element-web-test.ts index 8d5229a5100..76e57e33f70 100644 --- a/playwright/element-web-test.ts +++ b/playwright/element-web-test.ts @@ -314,6 +314,10 @@ export const expect = baseExpect.extend({ const testInfo = test.info(); if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); + if (!testInfo.tags.includes("@screenshot")) { + throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot"); + } + const page = "page" in receiver ? receiver.page() : receiver; let css = `