Merge branch 'develop' into Fix/MSC2530

This commit is contained in:
凌莞~(=^▽^=) 2024-12-06 20:59:14 +08:00 committed by GitHub
commit d28a4ee991
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
316 changed files with 2139 additions and 3787 deletions

View file

@ -42,6 +42,10 @@ module.exports = {
name: "setImmediate", name: "setImmediate",
message: "Use setTimeout instead.", message: "Use setTimeout instead.",
}, },
{
name: "Buffer",
message: "Buffer is not available in the web.",
},
], ],
"import/no-duplicates": ["error"], "import/no-duplicates": ["error"],
@ -255,6 +259,9 @@ module.exports = {
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"], additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
}, },
], ],
// These are fine in tests
"no-restricted-globals": "off",
}, },
}, },
{ {

1
.github/CODEOWNERS vendored
View file

@ -13,6 +13,7 @@
# Ignore translations as those will be updated by GHA for Localazy download # Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings /src/i18n/strings
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
# Ignore the synapse plugin as this is updated by GHA for docker image updating # Ignore the synapse plugin as this is updated by GHA for docker image updating
/playwright/plugins/homeserver/synapse/index.ts /playwright/plugins/homeserver/synapse/index.ts

View file

@ -10,24 +10,29 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- name: Download current version for its old bundles - name: Download release tarball
id: current_download
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1 uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
with: with:
tag: steps.current_version.outputs.version tag: ${{ inputs.tag }}
fileName: element-*.tar.gz* fileName: element-*.tar.gz*
out-file-path: ${{ runner.temp }}/download-verify-element-tarball out-file-path: ${{ runner.temp }}/download-verify-element-tarball
- name: Verify tarball - name: Verify tarball
shell: bash
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
working-directory: ${{ runner.temp }}/download-verify-element-tarball working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Extract tarball - name: Extract tarball
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1 shell: bash
run: |
mkdir webapp
tar xvzf element-*.tar.gz -C webapp --strip-components=1
working-directory: ${{ runner.temp }}/download-verify-element-tarball working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Move webapp to out-file-path - name: Move webapp to out-file-path
shell: bash
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }} run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
- name: Clean up temp directory - name: Clean up temp directory
shell: bash
run: rm -R ${{ runner.temp }}/download-verify-element-tarball run: rm -R ${{ runner.temp }}/download-verify-element-tarball

View file

@ -1,7 +1,8 @@
# Manual deploy workflow for deploying to app.element.io & staging.element.io # Manual deploy workflow for deploying to app.element.io & staging.element.io
# Runs automatically for staging.element.io when an RC or Release is published # Runs automatically for staging.element.io when an RC or Release is published
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand # Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }} name: Deploy release
run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }}
on: on:
release: release:
types: [published] types: [published]
@ -28,37 +29,40 @@ jobs:
env: env:
SITE: ${{ inputs.site || 'staging.element.io' }} SITE: ${{ inputs.site || 'staging.element.io' }}
steps: steps:
- uses: actions/checkout@v4
- name: Load GPG key - name: Load GPG key
run: | run: |
curl https://packages.element.io/element-release-key.gpg | gpg --import curl https://packages.element.io/element-release-key.gpg | gpg --import
gpg -k "$GPG_FINGERPRINT" gpg -k "$GPG_FINGERPRINT"
env: env:
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }} GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
- name: Check current version on deployment - name: Check current version on deployment
id: current_version id: current_version
run: | run: |
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT version=$(curl -s https://$SITE/version)
echo "version=${version#v}" >> $GITHUB_OUTPUT
# The current version bundle melding dance is skipped if the version we're deploying is the same # The current version bundle melding dance is skipped if the version we're deploying is the same
# as then we're just doing a re-deploy of the same version with potentially different configs. # as then we're just doing a re-deploy of the same version with potentially different configs.
- name: Download current version for its old bundles - name: Download current version for its old bundles
id: current_download id: current_download
if: steps.current_version.outputs.version != github.ref_name if: steps.current_version.outputs.version != github.ref_name
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} uses: ./.github/actions/download-verify-element-tarball
with: with:
tag: steps.current_version.outputs.version tag: v${{ steps.current_version.outputs.version }}
out-file-path: current_version out-file-path: _current_version
- name: Download target version - name: Download target version
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }} uses: ./.github/actions/download-verify-element-tarball
with: with:
tag: ${{ github.ref_name }} tag: ${{ github.ref_name }}
out-file-path: _deploy out-file-path: _deploy
- name: Merge current bundles into target - name: Merge current bundles into target
if: steps.current_download.outcome == 'success' if: steps.current_download.outcome == 'success'
run: cp -vnpr current_version/bundles/* _deploy/bundles/ run: cp -vnpr _current_version/bundles/* _deploy/bundles/
- name: Copy config - name: Copy config
run: cp element.io/app/config.json _deploy/config.json run: cp element.io/app/config.json _deploy/config.json
@ -73,7 +77,7 @@ jobs:
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with: with:
ref: ${{ github.sha }} ref: ${{ github.sha }}
running-workflow-name: "Build and Deploy ${{ env.SITE }}" running-workflow-name: "Deploy to Cloudflare Pages"
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10 wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$ check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$

View file

@ -83,7 +83,7 @@ jobs:
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}" name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
needs: build needs: build
if: inputs.skip != true if: inputs.skip != true
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
permissions: permissions:
actions: read actions: read
issues: read issues: read
@ -124,14 +124,18 @@ jobs:
with: with:
path: | path: |
~/.cache/ms-playwright ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }} key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}-chromium
- name: Install Playwright browsers - name: Install Playwright browser
if: steps.playwright-cache.outputs.cache-hit != 'true' if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps run: yarn playwright install --with-deps --no-shell chromium
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
- name: Run Playwright tests - name: Run Playwright tests
run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }} run: |
yarn playwright test \
--shard "${{ matrix.runner }}/${{ strategy.job-total }}" \
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts
if: always() if: always()

View file

@ -1,3 +1,33 @@
Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03)
==================================================================================================
## ✨ Features
* Send and respect MSC4230 is\_animated flag ([#28513](https://github.com/element-hq/element-web/pull/28513)). Contributed by @t3chguy.
* Display a warning when an unverified user's identity changes ([#28211](https://github.com/element-hq/element-web/pull/28211)). Contributed by @uhoreg.
* Swap out Twitter link for Mastodon on auth footer ([#28508](https://github.com/element-hq/element-web/pull/28508)). Contributed by @t3chguy.
* Consider `org.matrix.msc3417.call` as video room in create room dialog ([#28497](https://github.com/element-hq/element-web/pull/28497)). Contributed by @t3chguy.
* Standardise icons using Compound Design Tokens ([#28217](https://github.com/element-hq/element-web/pull/28217)). Contributed by @t3chguy.
* Start sending stable `m.marked_unread` events ([#28478](https://github.com/element-hq/element-web/pull/28478)). Contributed by @tulir.
* Upgrade to compound-design-tokens v2 ([#28471](https://github.com/element-hq/element-web/pull/28471)). Contributed by @t3chguy.
* Standardise icons using Compound Design Tokens ([#28286](https://github.com/element-hq/element-web/pull/28286)). Contributed by @t3chguy.
* Remove reply fallbacks as per merged MSC2781 ([#28406](https://github.com/element-hq/element-web/pull/28406)). Contributed by @t3chguy.
* Use React Suspense when rendering async modals ([#28386](https://github.com/element-hq/element-web/pull/28386)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Add spinner when room encryption is loading in room settings ([#28535](https://github.com/element-hq/element-web/pull/28535)). Contributed by @florianduros.
* Fix getOidcCallbackUrl for Element Desktop ([#28521](https://github.com/element-hq/element-web/pull/28521)). Contributed by @t3chguy.
* Filter out redacted poll votes to avoid crashing the Poll widget ([#28498](https://github.com/element-hq/element-web/pull/28498)). Contributed by @t3chguy.
* Fix force tab complete not working since switching to React 18 createRoot API ([#28505](https://github.com/element-hq/element-web/pull/28505)). Contributed by @t3chguy.
* Fix media captions in bubble layout ([#28480](https://github.com/element-hq/element-web/pull/28480)). Contributed by @tulir.
* Reset cross-signing before backup when resetting both ([#28402](https://github.com/element-hq/element-web/pull/28402)). Contributed by @uhoreg.
* Listen to events so that encryption icon updates when status changes ([#28407](https://github.com/element-hq/element-web/pull/28407)). Contributed by @uhoreg.
* Check that the file the user chose has a MIME type of `image/*` ([#28467](https://github.com/element-hq/element-web/pull/28467)). Contributed by @t3chguy.
* Fix download button size in message action bar ([#28472](https://github.com/element-hq/element-web/pull/28472)). Contributed by @t3chguy.
* Allow tab completing users in brackets ([#28460](https://github.com/element-hq/element-web/pull/28460)). Contributed by @t3chguy.
* Fix React 18 strict mode breaking spotlight dialog ([#28452](https://github.com/element-hq/element-web/pull/28452)). Contributed by @MidhunSureshR.
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19) Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View file

@ -217,3 +217,10 @@ instead of the native `toHaveScreenshot`.
If you are running Linux and are unfortunate that the screenshots are not rendering identically, If you are running Linux and are unfortunate that the screenshots are not rendering identically,
you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you. you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you.
## Test Tags
We use test tags to categorise tests for running subsets more efficiently.
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.

View file

@ -1,6 +1,6 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.86", "version": "1.11.87",
"description": "A feature-rich client for Matrix.org", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
@ -64,7 +64,7 @@
"test:playwright:open": "yarn test:playwright --ui", "test:playwright:open": "yarn test:playwright --ui",
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run", "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: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", "coverage": "yarn test --coverage",
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts", "analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp", "analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
@ -79,6 +79,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7", "@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.0", "@matrix-org/analytics-events": "^0.29.0",
"@matrix-org/emojibase-bindings": "^1.3.3", "@matrix-org/emojibase-bindings": "^1.3.3",
@ -114,10 +116,10 @@
"jsrsasign": "^11.0.0", "jsrsasign": "^11.0.0",
"jszip": "^3.7.0", "jszip": "^3.7.0",
"katex": "^0.16.0", "katex": "^0.16.0",
"linkify-element": "4.1.4", "linkify-element": "4.2.0",
"linkify-react": "4.1.4", "linkify-react": "4.2.0",
"linkify-string": "4.1.4", "linkify-string": "4.2.0",
"linkifyjs": "4.1.4", "linkifyjs": "4.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maplibre-gl": "^4.0.0", "maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
@ -214,7 +216,6 @@
"babel-loader": "^9.0.0", "babel-loader": "^9.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0", "babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"blob-polyfill": "^9.0.0", "blob-polyfill": "^9.0.0",
"buffer": "^6.0.3",
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"concurrently": "^9.0.0", "concurrently": "^9.0.0",
"copy-webpack-plugin": "^12.0.0", "copy-webpack-plugin": "^12.0.0",

View file

@ -6,11 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { defineConfig } from "@playwright/test"; import { defineConfig, devices } from "@playwright/test";
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080"; const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
export default defineConfig({ export default defineConfig({
projects: [{ name: "Chrome", use: { ...devices["Desktop Chrome"], channel: "chromium" } }],
use: { use: {
viewport: { width: 1280, height: 720 }, viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.49.0-jammy FROM mcr.microsoft.com/playwright:v1.49.0-noble
WORKDIR /work WORKDIR /work

View file

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test"; 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.addInitScript({ content: `delete window.Intl;` });
await page.goto("/"); 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"); 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.addInitScript({ content: `delete window.WebAssembly;` });
await page.goto("/"); await page.goto("/");

View file

@ -134,18 +134,22 @@ test.describe("Audio player", () => {
).toBeVisible(); ).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 uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)"); await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
}); });
test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => { test(
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg"); "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, // Disable system theme in case ThemeWatcher enables the theme automatically,
// so that the high contrast theme can be enabled // so that the high contrast theme can be enabled
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false); 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)"); 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 // Enable dark theme
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark"); await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
@ -207,93 +211,101 @@ test.describe("Audio player", () => {
expect(download.suggestedFilename()).toBe("1sec.ogg"); expect(download.suggestedFilename()).toBe("1sec.ogg");
}); });
test("should support replying to audio file with another audio file", async ({ page, app }) => { test(
await uploadFile(page, "playwright/sample-files/1sec.ogg"); "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 // Assert the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Find and click "Reply" button on MessageActionBar // Find and click "Reply" button on MessageActionBar
const tile = page.locator(".mx_EventTile_last"); 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");
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that replied audio file is rendered as file button inside ReplyChain
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
// Assert that the file button has file name
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
});
test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => {
// Note: "mx_ReplyChain" element is used not only for replies which
// create a reply chain, but also for a single reply without a replied
// message. This test checks whether a reply chain which consists of
// multiple audio file replies is rendered properly.
const tile = page.locator(".mx_EventTile_last");
// Find and click "Reply" button
const clickButtonReply = async () => {
await tile.scrollIntoViewIfNeeded();
await tile.hover(); await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click(); await tile.getByRole("button", { name: "Reply", exact: true }).click();
};
await uploadFile(page, "playwright/sample-files/upload-first.ogg"); // Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert that the audio player is rendered // Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
await clickButtonReply(); // 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();
// Reply to the player with another audio file await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
await uploadFile(page, "playwright/sample-files/upload-second.ogg"); },
);
// Assert that the audio player is rendered test(
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible(); "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.
await clickButtonReply(); const tile = page.locator(".mx_EventTile_last");
// Reply to the player with yet another audio file to create a reply chain // Find and click "Reply" button
await uploadFile(page, "playwright/sample-files/upload-third.ogg"); const clickButtonReply = async () => {
await tile.scrollIntoViewIfNeeded();
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
};
// Assert that the audio player is rendered await uploadFile(page, "playwright/sample-files/upload-first.ogg");
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that there are two "mx_ReplyChain" elements // Assert that the audio player is rendered
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2); await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Assert that one line contains the user name await clickButtonReply();
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
// Assert that the other line contains the file button // Reply to the player with another audio file
await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible(); await uploadFile(page, "playwright/sample-files/upload-second.ogg");
// Click "In reply to" // Assert that the audio player is rendered
await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click(); await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
const replyChain = tile.locator(".mx_ReplyChain:first-of-type"); await clickButtonReply();
// 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 // Reply to the player with yet another audio file to create a reply chain
await expect( await uploadFile(page, "playwright/sample-files/upload-third.ogg");
replyChain
.locator(".mx_MFileBody_info[role='button']")
.locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
).toBeVisible();
// Take snapshots // Assert that the audio player is rendered
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain"); await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
});
// Assert that there are two "mx_ReplyChain" elements
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
// Assert that one line contains the user name
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
// Assert that the other line contains the file button
await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible();
// Click "In reply to"
await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click();
const replyChain = tile.locator(".mx_ReplyChain:first-of-type");
// Assert that "In reply to" has disappeared
await expect(replyChain.getByText("In reply to")).not.toBeVisible();
// Assert that the file button contains the name of the file sent at first
await expect(
replyChain
.locator(".mx_MFileBody_info[role='button']")
.locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
).toBeVisible();
// Take snapshots
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
},
);
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => { 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"); await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");

View file

@ -89,43 +89,47 @@ test.describe("HTML Export", () => {
}, },
}); });
test("should export html successfully and match screenshot", async ({ page, app, room }) => { test(
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry "should export html successfully and match screenshot",
// about the width changing and we can actually test this line looks correct. { tag: "@screenshot" },
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z")); 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 // Send a bunch of messages to populate the room
for (let i = 1; i < 10; i++) { for (let i = 1; i < 10; i++) {
const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" }); const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" });
if (i == 1) { if (i == 1) {
await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃"); await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃");
}
} }
}
// Wait for all the messages to be displayed // Wait for all the messages to be displayed
await expect( await expect(
page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"), page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"),
).toBeVisible(); ).toBeVisible();
await app.toggleRoomInfoPanel(); await app.toggleRoomInfoPanel();
await page.getByRole("menuitem", { name: "Export Chat" }).click(); await page.getByRole("menuitem", { name: "Export Chat" }).click();
const downloadPromise = page.waitForEvent("download"); const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export", exact: true }).click(); await page.getByRole("button", { name: "Export", exact: true }).click();
const download = await downloadPromise; const download = await downloadPromise;
const dirPath = path.join(os.tmpdir(), "html-export-test"); const dirPath = path.join(os.tmpdir(), "html-export-test");
const zipPath = `${dirPath}.zip`; const zipPath = `${dirPath}.zip`;
await download.saveAs(zipPath); await download.saveAs(zipPath);
const zip = await extractZipFileToPath(zipPath, dirPath); const zip = await extractZipFileToPath(zipPath, dirPath);
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`); await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
await expect(page).toMatchScreenshot("html-export.png", { await expect(page).toMatchScreenshot("html-export.png", {
mask: [ mask: [
// We need to mask the whole thing because the width of the time part changes // We need to mask the whole thing because the width of the time part changes
page.locator(".mx_TimelineSeparator"), page.locator(".mx_TimelineSeparator"),
page.locator(".mx_MessageTimestamp"), page.locator(".mx_MessageTimestamp"),
], ],
}); });
}); },
);
}); });

View file

@ -204,30 +204,29 @@ test.describe("Cryptography", function () {
await expect(page.locator(".mx_Dialog")).toHaveCount(1); await expect(page.locator(".mx_Dialog")).toHaveCount(1);
}); });
test("creating a DM should work, being e2e-encrypted / user verification", async ({ test(
page, "creating a DM should work, being e2e-encrypted / user verification",
app, { tag: "@screenshot" },
bot: bob, async ({ page, app, bot: bob, user: aliceCredentials }) => {
user: aliceCredentials, await app.client.bootstrapCrossSigning(aliceCredentials);
}) => { await startDMWithBob(page, bob);
await app.client.bootstrapCrossSigning(aliceCredentials); // send first message
await startDMWithBob(page, bob); await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
// send first message await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!"); await checkDMRoom(page);
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); const bobRoomId = await bobJoin(page, bob);
await checkDMRoom(page); await testMessages(page, bob, bobRoomId);
const bobRoomId = await bobJoin(page, bob); await verify(app, bob);
await testMessages(page, bob, bobRoomId);
await verify(app, bob);
// Assert that verified icon is rendered // Assert that verified icon is rendered
await page.getByTestId("base-card-back-button").click(); await page.getByTestId("base-card-back-button").click();
await page.getByLabel("Room info").nth(1).click(); await page.getByLabel("Room info").nth(1).click();
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted"); await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted");
// Take a snapshot of RoomSummaryCard with a verified E2EE icon // Take a snapshot of RoomSummaryCard with a verified E2EE icon
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png"); await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
}); },
);
test("should allow verification when there is no existing DM", async ({ test("should allow verification when there is no existing DM", async ({
page, page,

View file

@ -102,7 +102,7 @@ test.describe("Device verification", () => {
// feed the QR code into the verification request. // feed the QR code into the verification request.
const qrData = await readQrCode(infoDialog); const qrData = await readQrCode(infoDialog);
const verifier = await verificationRequest.evaluateHandle( const verifier = await verificationRequest.evaluateHandle(
(request, qrData) => request.scanQRCode(new Uint8Array(qrData)), (request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)),
[...qrData], [...qrData],
); );

View file

@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { Locator } from "@playwright/test";
import { expect, test } from "../../element-web-test"; import { expect, test } from "../../element-web-test";
import { import {
autoJoin, autoJoin,
@ -17,6 +19,7 @@ import {
verify, verify,
} from "./utils"; } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts"; import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.describe("Cryptography", function () { test.describe("Cryptography", function () {
test.use({ test.use({
@ -277,6 +280,15 @@ test.describe("Cryptography", function () {
bot: bob, bot: bob,
homeserver, homeserver,
}) => { }) => {
// Workaround for https://github.com/element-hq/element-web/issues/28640:
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
// his user info.
await app.toggleRoomInfoPanel();
const rightPanel = page.locator(".mx_RightPanel");
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
// Our app is blocked from syncing while Bob sends his messages. // Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline(); await app.client.network.goOffline();
@ -306,7 +318,7 @@ test.describe("Cryptography", function () {
); );
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); await assertNoE2EIcon(penultimate, app);
}); });
test("should show correct shields on events sent by users with changed identity", async ({ test("should show correct shields on events sent by users with changed identity", async ({
@ -335,3 +347,21 @@ test.describe("Cryptography", function () {
}); });
}); });
}); });
/**
* Check that the given message doesn't have an E2E warning icon.
*
* If it does, throw an error.
*/
async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
// Make sure the message itself exists, before we check if it has any icons
await messageLocator.waitFor();
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
if ((await e2eIcon.count()) > 0) {
// uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error.
await e2eIcon.focus();
const tooltip = await app.getTooltipForElement(e2eIcon);
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
}
}

View file

@ -66,126 +66,130 @@ test.describe("Editing", () => {
botCreateOpts: { displayName: "Bob" }, botCreateOpts: { displayName: "Bob" },
}); });
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => { test(
// Click the "Remove" button on the message edit history dialog "should render and interact with the message edit history dialog",
const clickButtonRemove = async (locator: Locator) => { { tag: "@screenshot" },
const eventTileLine = locator.locator(".mx_EventTile_line"); async ({ page, user, app, room }) => {
await eventTileLine.hover(); // Click the "Remove" button on the message edit history dialog
await eventTileLine.getByRole("button", { name: "Remove" }).click(); 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}`); await page.goto(`#/room/${room.roomId}`);
// Send "Message" // Send "Message"
await sendEvent(app, room.roomId); await sendEvent(app, room.roomId);
// Edit "Message" to "Massage" // Edit "Message" to "Massage"
await editLastMessage(page, "Massage"); await editLastMessage(page, "Massage");
// Assert that the edit label is visible // Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible(); await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
await clickEditedMessage(page, "Massage"); await clickEditedMessage(page, "Massage");
// Assert that the message edit history dialog is rendered // Assert that the message edit history dialog is rendered
const dialog = page.getByRole("dialog"); const dialog = page.getByRole("dialog");
const li = dialog.getByRole("listitem").last(); const li = dialog.getByRole("listitem").last();
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected // Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
await expect(li).toHaveCSS("clear", "both"); await expect(li).toHaveCSS("clear", "both");
const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp"); const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp");
await expect(timestamp).toHaveCSS("position", "absolute"); await expect(timestamp).toHaveCSS("position", "absolute");
await expect(timestamp).toHaveCSS("inset-inline-start", "0px"); await expect(timestamp).toHaveCSS("inset-inline-start", "0px");
await expect(timestamp).toHaveCSS("text-align", "center"); await expect(timestamp).toHaveCSS("text-align", "center");
// Assert that monospace characters can fill the content line as expected // 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 expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px");
// Assert that zero block start padding is applied to mx_EventTile as expected // Assert that zero block start padding is applied to mx_EventTile as expected
// See: .mx_EventTile on _EventTile.pcss // See: .mx_EventTile on _EventTile.pcss
await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px"); await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px");
// Assert that the date separator is rendered at the top // Assert that the date separator is rendered at the top
await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS( await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS(
"text-transform", "text-transform",
"capitalize", "capitalize",
); );
{ {
// Assert that the edited message is rendered under the date separator // Assert that the edited message is rendered under the date separator
const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
// Assert that the edited message body consists of both deleted character and inserted character // Assert that the edited message body consists of both deleted character and inserted character
// Above the first "e" of "Message" was replaced with "a" // Above the first "e" of "Message" was replaced with "a"
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
const body = tile.locator(".mx_EventTile_content .mx_EventTile_body"); 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_deletion").getByText("e")).toBeVisible();
await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible(); await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible();
} }
// Assert that the original message is rendered at the bottom // Assert that the original message is rendered at the bottom
await expect( await expect(
dialog dialog
.locator("li:nth-child(3) .mx_EventTile") .locator("li:nth-child(3) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible(); ).toBeVisible();
// Take a snapshot of the dialog // Take a snapshot of the dialog
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", { await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); });
{ {
const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again // Click the "Remove" button again
await clickButtonRemove(tile); await clickButtonRemove(tile);
} }
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered // Do nothing and close the dialog to confirm that the message edit history dialog is rendered
await app.closeDialog(); await app.closeDialog();
{ {
// Assert that the message edit history dialog is rendered again after it was closed // Assert that the message edit history dialog is rendered again after it was closed
const tile = dialog.locator("li:nth-child(2) .mx_EventTile"); const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage"); await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again // Click the "Remove" button again
await clickButtonRemove(tile); await clickButtonRemove(tile);
} }
// This time remove the message really // This time remove the message really
const textInputDialog = page.locator(".mx_TextInputDialog"); const textInputDialog = page.locator(".mx_TextInputDialog");
await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason
await textInputDialog.getByRole("button", { name: "Remove" }).click(); await textInputDialog.getByRole("button", { name: "Remove" }).click();
// Assert that the message edit history dialog is rendered again // Assert that the message edit history dialog is rendered again
const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog"); const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog");
// Assert that the date is rendered // Assert that the date is rendered
await expect( await expect(
messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }), messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }),
).toHaveCSS("text-transform", "capitalize"); ).toHaveCSS("text-transform", "capitalize");
// Assert that the original message is rendered under the date on the dialog // Assert that the original message is rendered under the date on the dialog
await expect( await expect(
messageEditHistoryDialog messageEditHistoryDialog
.locator("li:nth-child(2) .mx_EventTile") .locator("li:nth-child(2) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }), .locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible(); ).toBeVisible();
// Assert that the edited message is gone // Assert that the edited message is gone
await expect( await expect(
messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }), messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }),
).not.toBeVisible(); ).not.toBeVisible();
await app.closeDialog(); await app.closeDialog();
// Assert that the redaction placeholder is rendered // Assert that the redaction placeholder is rendered
await expect( await expect(
page page
.locator(".mx_RoomView_MessageList") .locator(".mx_RoomView_MessageList")
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }), .locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
).toBeVisible(); ).toBeVisible();
}); },
);
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({ test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
page, page,

View file

@ -25,7 +25,7 @@ test.describe("Image Upload", () => {
).toBeVisible(); ).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 await page
.locator(".mx_MessageComposer_actions input[type='file']") .locator(".mx_MessageComposer_actions input[type='file']")
.setInputFiles("playwright/sample-files/riot.png"); .setInputFiles("playwright/sample-files/riot.png");

View file

@ -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.goto("/");
await page.getByRole("link", { name: "Sign in" }).click(); 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"); 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); const user = await homeserver.registerUser(username, password);
await homeserver.setThreepid(user.userId, "email", email); await homeserver.setThreepid(user.userId, "email", email);

View file

@ -19,7 +19,7 @@ test.describe("Invite dialog", function () {
const botName = "BotAlice"; 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 // Create and view a room
await app.client.createRoom({ name: "Test Room" }); await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("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(); await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
}); });
test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => { test(
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click(); "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"); const other = page.locator(".mx_InviteDialog_other");
// Assert that the header is rendered // Assert that the header is rendered
await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible(); await expect(
other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"),
).toBeVisible();
// Assert that the bar is rendered // Assert that the bar is rendered
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible(); await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
// Take a snapshot of the invite dialog // Take a snapshot of the invite dialog
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png"); 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 expect(
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click(); other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
).toBeVisible();
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
await expect( await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName), other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible(); ).toBeVisible();
// Take a snapshot of the invite dialog with a user pill // 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"); await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
// Open a direct message UI // Open a direct message UI
await other.getByRole("button", { name: "Go" }).click(); await other.getByRole("button", { name: "Go" }).click();
// Assert that the invite dialog disappears // Assert that the invite dialog disappears
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible(); await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
// Assert that the hovered user name on invitation UI does not have background color // Assert that the hovered user name on invitation UI does not have background color
// TODO: implement the test on room-header.spec.ts // TODO: implement the test on room-header.spec.ts
const roomHeader = page.locator(".mx_RoomHeader"); const roomHeader = page.locator(".mx_RoomHeader");
await roomHeader.locator(".mx_RoomHeader_heading").hover(); await roomHeader.locator(".mx_RoomHeader_heading").hover();
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)"); await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
// Send a message to invite the bots // Send a message to invite the bots
const composer = app.getComposer().locator("[contenteditable]"); const composer = app.getComposer().locator("[contenteditable]");
await composer.fill("Hello}"); await composer.fill("Hello}");
await composer.press("Enter"); await composer.press("Enter");
// Assert that they were invited and joined // Assert that they were invited and joined
await expect(page.getByText(`${botName} joined the room`)).toBeVisible(); await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
// Assert that the message is displayed at the bottom // Assert that the message is displayed at the bottom
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible(); await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
}); },
);
}); });

View file

@ -63,7 +63,7 @@ test.describe("Message rendering", () => {
{ direction: "ltr", displayName: "Quentin" }, { direction: "ltr", displayName: "Quentin" },
{ direction: "rtl", displayName: "كوينتين" }, { direction: "rtl", displayName: "كوينتين" },
].forEach(({ direction, displayName }) => { ].forEach(({ direction, displayName }) => {
test.describe(`with ${direction} display name`, () => { test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => {
test.use({ test.use({
displayName, displayName,
room: async ({ user, app }, use) => { 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 }) => { test(
await page.goto(`#/room/${room.roomId}`); "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!"); const msgTile = await sendMessage(page, "Hello, world!");
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, { await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
mask: [page.locator(".mx_MessageTimestamp")], mask: [page.locator(".mx_MessageTimestamp")],
}); });
}); },
);
test("should render an LTR emote", async ({ page, user, app, room }) => { test("should render an LTR emote", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`); await page.goto(`#/room/${room.roomId}`);

View file

@ -24,7 +24,7 @@ test.describe("permalinks", () => {
displayName: "Alice", 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 bob = new Bot(page, homeserver, { displayName: "Bob" });
const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" }); const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" });
await bob.prepareClient(); await bob.prepareClient();

View file

@ -129,6 +129,7 @@ export class Helpers {
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message }); const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
await timelineMessage.click({ button: "right" }); await timelineMessage.click({ button: "right" });
await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click(); await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click();
await this.assertMessageInBanner(message);
} }
/** /**

View file

@ -10,35 +10,38 @@ import { test } from "./index";
import { expect } from "../../element-web-test"; import { expect } from "../../element-web-test";
test.describe("Pinned messages", () => { test.describe("Pinned messages", () => {
test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => { test(
await util.goTo(room1); "should show the empty state when there are no pinned messages",
await util.openRoomInfo(); { tag: "@screenshot" },
await util.assertPinnedCountInRoomInfo(0); async ({ page, app, room1, util }) => {
await util.openPinnedMessagesList(); await util.goTo(room1);
await util.assertEmptyPinnedMessagesList(); 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 ({ test(
page, "should pin one message and to have the pinned message badge in the timeline",
app, { tag: "@screenshot" },
room1, async ({ page, app, room1, util }) => {
util, await util.goTo(room1);
}) => { await util.receiveMessages(room1, ["Msg1"]);
await util.goTo(room1); await util.pinMessages(["Msg1"]);
await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]);
const tile = util.getEventTile("Msg1"); const tile = util.getEventTile("Msg1");
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", { await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
mask: [tile.locator(".mx_MessageTimestamp")], mask: [tile.locator(".mx_MessageTimestamp")],
// Hide the jump to bottom button in the timeline to avoid flakiness // Hide the jump to bottom button in the timeline to avoid flakiness
css: ` css: `
.mx_JumpToBottomButton { .mx_JumpToBottomButton {
display: none !important; display: none !important;
} }
`, `,
}); });
}); },
);
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => { test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
await util.goTo(room1); await util.goTo(room1);
@ -73,7 +76,7 @@ test.describe("Pinned messages", () => {
await util.assertPinnedCountInRoomInfo(2); 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.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
@ -98,7 +101,7 @@ test.describe("Pinned messages", () => {
await util.assertPinnedCountInRoomInfo(0); 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.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]); await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]); await util.pinMessages(["Msg1"]);
@ -106,7 +109,7 @@ test.describe("Pinned messages", () => {
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png"); 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.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2"]); await util.receiveMessages(room1, ["Msg1", "Msg2"]);
await util.pinMessages(["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"); 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.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]); await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]); await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);

View file

@ -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({}); const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);
await page.goto("/#/room/" + roomId); await page.goto("/#/room/" + roomId);
@ -219,107 +219,121 @@ test.describe("Polls", () => {
await expect(page.locator(".mx_ErrorDialog")).toBeAttached(); await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
}); });
test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => { test(
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" }); "should be displayed correctly in thread panel",
await botCharlie.prepareClient(); { 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({}); const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);
await app.client.inviteUser(roomId, botCharlie.credentials.userId); await app.client.inviteUser(roomId, botCharlie.credentials.userId);
await page.goto("/#/room/" + roomId); await page.goto("/#/room/" + roomId);
// wait until the bots joined // wait until the bots joined
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 }); await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({
timeout: 10000,
});
const locator = await app.openMessageComposerOptions(); const locator = await app.openMessageComposerOptions();
await locator.getByRole("menuitem", { name: "Poll" }).click(); await locator.getByRole("menuitem", { name: "Poll" }).click();
const pollParams = { const pollParams = {
title: "Does the polls feature work?", title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"], options: ["Yes", "No", "Maybe"],
}; };
await createPoll(page, pollParams); await createPoll(page, pollParams);
// Wait for message to send, get its ID and save as @pollId // Wait for message to send, get its ID and save as @pollId
const pollId = await page const pollId = await page
.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]") .locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
.filter({ hasText: pollParams.title }) .filter({ hasText: pollParams.title })
.getAttribute("data-scroll-tokens"); .getAttribute("data-scroll-tokens");
// Bob starts thread on the poll // Bob starts thread on the poll
await bot.sendMessage( await bot.sendMessage(
roomId, roomId,
{ {
body: "Hello there", body: "Hello there",
msgtype: "m.text", msgtype: "m.text",
}, },
pollId, pollId,
); );
// open the thread summary // open the thread summary
await page.getByRole("button", { name: "Open thread" }).click(); await page.getByRole("button", { name: "Open thread" }).click();
// Bob votes 'Maybe' in the poll // Bob votes 'Maybe' in the poll
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]); await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
// Charlie votes 'No' // Charlie votes 'No'
await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]); await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]);
// no votes shown until I vote, check votes have arrived in main tl // no votes shown until I vote, check votes have arrived in main tl
await expect( await expect(
page page
.locator(".mx_RoomView_body .mx_MPollBody_totalVotes") .locator(".mx_RoomView_body .mx_MPollBody_totalVotes")
.getByText("2 votes cast. Vote to see the results"), .getByText("2 votes cast. Vote to see the results"),
).toBeAttached(); ).toBeAttached();
// and thread view // and thread view
await expect( await expect(
page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"), page
).toBeAttached(); .locator(".mx_ThreadView .mx_MPollBody_totalVotes")
.getByText("2 votes cast. Vote to see the results"),
).toBeAttached();
// Take snapshots of poll on ThreadView // Take snapshots of poll on ThreadView
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); 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 .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", { await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], "ThreadView_with_a_poll_on_bubble_layout.png",
}); {
mask: [page.locator(".mx_MessageTimestamp")],
},
);
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group); 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 .mx_EventTile[data-layout='group']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", { await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
mask: [page.locator(".mx_MessageTimestamp")], "ThreadView_with_a_poll_on_group_layout.png",
}); {
mask: [page.locator(".mx_MessageTimestamp")],
},
);
const roomViewLocator = page.locator(".mx_RoomView_body"); const roomViewLocator = page.locator(".mx_RoomView_body");
// vote 'Maybe' in the main timeline poll // vote 'Maybe' in the main timeline poll
await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click(); await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click();
// both me and bob have voted Maybe // both me and bob have voted Maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator); await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator);
const threadViewLocator = page.locator(".mx_ThreadView"); const threadViewLocator = page.locator(".mx_ThreadView");
// votes updated in thread view too // votes updated in thread view too
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator); await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator);
// change my vote to 'Yes' // change my vote to 'Yes'
await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click(); await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click();
// Bob updates vote to 'No' // Bob updates vote to 'No'
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]); await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
// me: yes, bob: no, charlie: no // me: yes, bob: no, charlie: no
const expectVoteCounts = async (optLocator: Locator) => { const expectVoteCounts = async (optLocator: Locator) => {
// I voted yes // I voted yes
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator); await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator);
// Bob and Charlie voted no // Bob and Charlie voted no
await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator); await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator);
// 0 for maybe // 0 for maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator); await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator);
}; };
// check counts are correct in main timeline tile // check counts are correct in main timeline tile
await expectVoteCounts(page.locator(".mx_RoomView_body")); await expectVoteCounts(page.locator(".mx_RoomView_body"));
// and in thread view tile // and in thread view tile
await expectVoteCounts(page.locator(".mx_ThreadView")); await expectVoteCounts(page.locator(".mx_ThreadView"));
}); },
);
}); });

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("editing messages", () => { test.describe("editing messages", () => {
test.describe("in threads", () => { test.describe("in threads", () => {
test("An edit of a threaded message makes the room unread", async ({ test("An edit of a threaded message makes the room unread", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("editing messages", () => { test.describe("editing messages", () => {
test.describe("in the main timeline", () => { test.describe("in the main timeline", () => {
test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => { test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("editing messages", () => { test.describe("editing messages", () => {
test.describe("thread roots", () => { test.describe("thread roots", () => {
test("An edit of a thread root leaves the room read", async ({ test("An edit of a thread root leaves the room read", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { customEvent, many, test } from "."; import { customEvent, many, test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Ignored events", () => { test.describe("Ignored events", () => {
test("If all events after receipt are unimportant, the room is read", async ({ test("If all events after receipt are unimportant, the room is read", async ({
roomAlpha: room1, roomAlpha: room1,

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Message ordering", () => { test.describe("Message ordering", () => {
test.describe("in the main timeline", () => { test.describe("in the main timeline", () => {
test.fixme( test.fixme(

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("messages with missing referents", () => { test.describe("messages with missing referents", () => {
test.fixme( test.fixme(
"A message in an unknown thread is not visible and the room is read", "A message in an unknown thread is not visible and the room is read",

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { many, test } from "."; import { many, test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("new messages", () => { test.describe("new messages", () => {
test.describe("in threads", () => { test.describe("in threads", () => {
test("Receiving a message makes a room unread", async ({ test("Receiving a message makes a room unread", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { many, test } from "."; import { many, test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("new messages", () => { test.describe("new messages", () => {
test.describe("in the main timeline", () => { test.describe("in the main timeline", () => {
test("Receiving a message makes a room unread", async ({ test("Receiving a message makes a room unread", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { many, test } from "."; import { many, test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("new messages", () => { test.describe("new messages", () => {
test.describe("thread roots", () => { test.describe("thread roots", () => {
test("Reading a thread root does not mark the thread as read", async ({ test("Reading a thread root does not mark the thread as read", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Notifications", () => { test.describe("Notifications", () => {
test.describe("in the main timeline", () => { test.describe("in the main timeline", () => {
test.fixme("A new message that mentions me shows a notification", () => {}); test.fixme("A new message that mentions me shows a notification", () => {});

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "."; import { test, expect } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("reactions", () => { test.describe("reactions", () => {
test.describe("in threads", () => { test.describe("in threads", () => {
test("A reaction to a threaded message does not make the room unread", async ({ test("A reaction to a threaded message does not make the room unread", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("reactions", () => { test.describe("reactions", () => {
test.describe("in the main timeline", () => { test.describe("in the main timeline", () => {
test("Receiving a reaction to a message does not make a room unread", async ({ test("Receiving a reaction to a message does not make a room unread", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("reactions", () => { test.describe("reactions", () => {
test.describe("thread roots", () => { test.describe("thread roots", () => {
test("A reaction to a thread root does not make the room unread", async ({ test("A reaction to a thread root does not make the room unread", async ({

View file

@ -13,7 +13,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.use({ test.use({
displayName: "Mae", displayName: "Mae",
botCreateOpts: { displayName: "Other User" }, botCreateOpts: { displayName: "Other User" },

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("redactions", () => { test.describe("redactions", () => {
test.describe("in threads", () => { test.describe("in threads", () => {
test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({ test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("redactions", () => { test.describe("redactions", () => {
test.describe("in the main timeline", () => { test.describe("in the main timeline", () => {
test("Redacting the message pointed to by my receipt leaves the room read", async ({ test("Redacting the message pointed to by my receipt leaves the room read", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("redactions", () => { test.describe("redactions", () => {
test.describe("thread roots", () => { test.describe("thread roots", () => {
test("Redacting a thread root after it was read leaves the room read", async ({ test("Redacting a thread root after it was read leaves the room read", async ({

View file

@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from "."; import { test } from ".";
test.describe("Read receipts", () => { test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Room list order", () => { test.describe("Room list order", () => {
test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({ test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({
roomAlpha: room1, roomAlpha: room1,

View file

@ -38,34 +38,33 @@ test.describe("Email Registration", async () => {
await page.goto("/#/register"); await page.goto("/#/register");
}); });
test("registers an account and lands on the use case selection screen", async ({ test(
page, "registers an account and lands on the use case selection screen",
mailhog, { tag: "@screenshot" },
request, async ({ page, mailhog, request, checkA11y }) => {
checkA11y, await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
}) => { // Hide the server text as it contains the randomly allocated Homeserver port
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
// 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.getByRole("textbox", { name: "Username" }).fill("alice");
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
await page.getByPlaceholder("Confirm password").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.getByPlaceholder("Email").fill("alice@email.com");
await page.getByRole("button", { name: "Register" }).click(); await page.getByRole("button", { name: "Register" }).click();
await expect(page.getByText("Check your email to continue")).toBeVisible(); await expect(page.getByText("Check your email to continue")).toBeVisible();
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions); await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions);
await checkA11y(); 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(); const messages = await mailhog.api.messages();
expect(messages.items).toHaveLength(1); expect(messages.items).toHaveLength(1);
expect(messages.items[0].to).toEqual("alice@email.com"); expect(messages.items[0].to).toEqual("alice@email.com");
const [emailLink] = messages.items[0].text.match(/http.+/); const [emailLink] = messages.items[0].text.match(/http.+/);
await request.get(emailLink); // "Click" the link in the email 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();
}); },
);
}); });

View file

@ -15,66 +15,73 @@ test.describe("Registration", () => {
await page.goto("/#/register"); await page.goto("/#/register");
}); });
test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => { test(
await page.getByRole("button", { name: "Edit", exact: true }).click(); "registers an account and lands on the home screen",
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible(); { 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 expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
await checkA11y(); await checkA11y();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl); await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click(); await page.getByRole("button", { name: "Continue", exact: true }).click();
// wait for the dialog to go away // wait for the dialog to go away
await expect(page.getByRole("dialog")).not.toBeVisible(); await expect(page.getByRole("dialog")).not.toBeVisible();
await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
// Hide the server text as it contains the randomly allocated Homeserver port // Hide the server text as it contains the randomly allocated Homeserver port
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true }; const screenshotOptions = {
await expect(page).toMatchScreenshot("registration.png", screenshotOptions); mask: [page.locator(".mx_ServerPicker_server")],
await checkA11y(); includeDialogBackground: true,
};
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
await checkA11y();
await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice"); await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password"); 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.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password");
await page.getByRole("button", { name: "Register", exact: true }).click(); await page.getByRole("button", { name: "Register", exact: true }).click();
const dialog = page.getByRole("dialog"); const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible(); await expect(dialog).toBeVisible();
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions); await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
await checkA11y(); await checkA11y();
await dialog.getByRole("button", { name: "Continue", exact: true }).click(); await dialog.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible(); await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions); await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
await checkA11y(); await checkA11y();
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy"); const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link
await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible(); await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible();
await page.getByRole("button", { name: "Accept", exact: true }).click(); await page.getByRole("button", { name: "Accept", exact: true }).click();
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible(); await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions); await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions);
await checkA11y(); await checkA11y();
await page.getByRole("button", { name: "Skip", exact: true }).click(); await page.getByRole("button", { name: "Skip", exact: true }).click();
await expect(page).toHaveURL(/\/#\/home$/); await expect(page).toHaveURL(/\/#\/home$/);
/* /*
* Cross-signing checks * Cross-signing checks
*/ */
// check that the device considers itself verified // check that the device considers itself verified
await page.getByRole("button", { name: "User menu", exact: true }).click(); await page.getByRole("button", { name: "User menu", exact: true }).click();
await page.getByRole("menuitem", { name: "All settings", exact: true }).click(); await page.getByRole("menuitem", { name: "All settings", exact: true }).click();
await page.getByRole("tab", { name: "Sessions", exact: true }).click(); await page.getByRole("tab", { name: "Sessions", exact: true }).click();
await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText( await expect(
"Verified", page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"),
); ).toHaveText("Verified");
// check that cross-signing keys have been uploaded. // check that cross-signing keys have been uploaded.
await crypto.assertDeviceIsCrossSigned(); await crypto.assertDeviceIsCrossSigned();
}); },
);
test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => { test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
await page.getByRole("button", { name: "Edit", exact: true }).click(); await page.getByRole("button", { name: "Edit", exact: true }).click();

View file

@ -18,7 +18,7 @@ test.describe("Release announcement", () => {
labsFlags: ["threadsActivityCentre"], 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 // The TAC release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre"); await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre");
// Hide the release announcement // Hide the release announcement

View file

@ -40,7 +40,7 @@ test.describe("FilePanel", () => {
}); });
test.describe("render", () => { 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 // Wait until the information about the empty state is rendered
await expect(page.locator(".mx_EmptyState")).toBeVisible(); await expect(page.locator(".mx_EmptyState")).toBeVisible();
@ -48,7 +48,7 @@ test.describe("FilePanel", () => {
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png"); 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 // Upload multiple files
await uploadFile(page, "playwright/sample-files/riot.png"); // Image await uploadFile(page, "playwright/sample-files/riot.png"); // Image
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio

View file

@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => {
await app.client.createRoom({ name: ROOM_NAME }); 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 app.viewRoomByName(ROOM_NAME);
await page.getByRole("button", { name: "Notifications" }).click(); await page.getByRole("button", { name: "Notifications" }).click();

View file

@ -38,7 +38,7 @@ test.describe("RightPanel", () => {
}); });
test.describe("in rooms", () => { 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 app.client.createRoom({ name: ROOM_NAME_LONG });
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG); await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);

View file

@ -47,34 +47,40 @@ test.describe("Room Directory", () => {
expect(resp.chunk[0].room_id).toEqual(roomId); expect(resp.chunk[0].room_id).toEqual(roomId);
}); });
test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => { test(
const name = "This is a public room"; "should allow finding published rooms in directory",
await bot.createRoom({ { tag: "@screenshot" },
visibility: "public" as Visibility, async ({ page, app, user, bot }) => {
name, const name = "This is a public room";
room_alias_name: "test1234", await bot.createRoom({
}); visibility: "public" as Visibility,
name,
room_alias_name: "test1234",
});
await page.getByRole("button", { name: "Explore rooms" }).click(); await page.getByRole("button", { name: "Explore rooms" }).click();
const dialog = page.locator(".mx_SpotlightDialog"); const dialog = page.locator(".mx_SpotlightDialog");
await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room"); await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room");
await expect( await expect(
dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."), dialog.getByText(
).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText"); "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 expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png");
await dialog.getByRole("textbox", { name: "Search" }).fill("test1234"); await dialog.getByRole("textbox", { name: "Search" }).fill("test1234");
await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName"); await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName");
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png"); await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png");
await page await page
.locator(".mx_SpotlightDialog .mx_SpotlightDialog_option") .locator(".mx_SpotlightDialog .mx_SpotlightDialog_option")
.getByRole("button", { name: "Join" }) .getByRole("button", { name: "Join" })
.click(); .click();
await expect(page).toHaveURL("/#/room/#test1234:localhost"); await expect(page).toHaveURL("/#/room/#test1234:localhost");
}); },
);
}); });

View file

@ -20,7 +20,7 @@ test.describe("Room Header", () => {
test.use({ test.use({
labsFlags: ["feature_notifications"], 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.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room"); await app.viewRoomByName("Test Room");
@ -51,34 +51,38 @@ test.describe("Room Header", () => {
await expect(header).toMatchScreenshot("room-header.png"); await expect(header).toMatchScreenshot("room-header.png");
}); });
test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => { test(
const LONG_ROOM_NAME = "should render a very long room name without collapsing the buttons",
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " + { tag: "@screenshot" },
"et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " + async ({ page, app, user }) => {
"aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + const LONG_ROOM_NAME =
"dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
"officia deserunt mollit anim id est laborum."; "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.client.createRoom({ name: LONG_ROOM_NAME });
await app.viewRoomByName(LONG_ROOM_NAME); await app.viewRoomByName(LONG_ROOM_NAME);
const header = page.locator(".mx_RoomHeader"); const header = page.locator(".mx_RoomHeader");
// Wait until the room name is set // Wait until the room name is set
await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible(); 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 // 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 // Note these assertions do not check the size of mx_LegacyRoomHeader_name button
const buttons = header.locator(".mx_Flex").getByRole("button"); const buttons = header.locator(".mx_Flex").getByRole("button");
await expect(buttons).toHaveCount(5); await expect(buttons).toHaveCount(5);
for (const button of await buttons.all()) { for (const button of await buttons.all()) {
await expect(button).toBeVisible(); await expect(button).toBeVisible();
await expect(button).toHaveCSS("height", "32px"); await expect(button).toHaveCSS("height", "32px");
await expect(button).toHaveCSS("width", "32px"); await expect(button).toHaveCSS("width", "32px");
} }
await expect(header).toMatchScreenshot("room-header-long-name.png"); await expect(header).toMatchScreenshot("room-header-long-name.png");
}); },
);
}); });
test.describe("with a video room", () => { test.describe("with a video room", () => {
@ -99,30 +103,34 @@ test.describe("Room Header", () => {
test.describe("and with feature_notifications enabled", () => { test.describe("and with feature_notifications enabled", () => {
test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] }); test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => { test(
await createVideoRoom(page, app); "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 // There's two room info button - the header itself and the i button
const infoButtons = header.getByRole("button", { name: "Room info" }); const infoButtons = header.getByRole("button", { name: "Room info" });
await expect(infoButtons).toHaveCount(2); await expect(infoButtons).toHaveCount(2);
await expect(infoButtons.first()).toBeVisible(); await expect(infoButtons.first()).toBeVisible();
await expect(infoButtons.last()).toBeVisible(); await expect(infoButtons.last()).toBeVisible();
// Facepile // Facepile
await expect(header.locator(".mx_FacePile")).toBeVisible(); await expect(header.locator(".mx_FacePile")).toBeVisible();
// Chat, Threads and Notification buttons // Chat, Threads and Notification buttons
await expect(header.getByRole("button", { name: "Chat" })).toBeVisible(); await expect(header.getByRole("button", { name: "Chat" })).toBeVisible();
await expect(header.getByRole("button", { name: "Threads" })).toBeVisible(); await expect(header.getByRole("button", { name: "Threads" })).toBeVisible();
await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible(); await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible();
// Assert that there is not a button except those buttons // Assert that there is not a button except those buttons
await expect(header.getByRole("button")).toHaveCount(7); 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 ({ test("should render a working chat button which opens the timeline on a right panel", async ({

View file

@ -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"); await expect(uut).toMatchScreenshot("account.png");
// Assert that the top heading is rendered // 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 page.setViewportSize({ width: 700, height: 600 });
await expect(uut).toMatchScreenshot("account-smallscreen.png"); await expect(uut).toMatchScreenshot("account-smallscreen.png");
}); });

View file

@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
displayName: "Hanako", 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"); const tab = await app.settings.openUserSettings("Appearance");
// Click "Show advanced" link button // Click "Show advanced" link button
@ -25,19 +25,23 @@ test.describe("Appearance user settings tab", () => {
await expect(tab).toMatchScreenshot("appearance-tab.png"); await expect(tab).toMatchScreenshot("appearance-tab.png");
}); });
test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => { test(
await app.settings.openUserSettings("Appearance"); "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 tab = page.getByTestId("mx_AppearanceUserSettingsTab");
const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown"); const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
await expect(fontDropdown.getByLabel("Font size")).toBeVisible(); await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
// Default browser font size is 16px and the select value is 0 // Default browser font size is 16px and the select value is 0
// -4 value is 12px // -4 value is 12px
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" }); 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 }) => { test("should support enabling system font", async ({ page, app, user }) => {
await app.settings.openUserSettings("Appearance"); await app.settings.openUserSettings("Appearance");

View file

@ -20,20 +20,24 @@ test.describe("Appearance user settings tab", () => {
await util.openAppearanceTab(); await util.openAppearanceTab();
}); });
test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => { test(
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png"); "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(); await util.getBubbleLayout().click();
// Assert that modern are irc layout are not selected // Assert that modern are irc layout are not selected
await expect(util.getBubbleLayout()).toBeChecked(); await expect(util.getBubbleLayout()).toBeChecked();
await expect(util.getModernLayout()).not.toBeChecked(); await expect(util.getModernLayout()).not.toBeChecked();
await expect(util.getIRCLayout()).not.toBeChecked(); await expect(util.getIRCLayout()).not.toBeChecked();
// Assert that the room layout is set to bubble layout // Assert that the room layout is set to bubble layout
await util.assertBubbleLayout(); await util.assertBubbleLayout();
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png"); 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 }) => { test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => {
await expect(util.getCompactLayoutCheckbox()).not.toBeChecked(); await expect(util.getCompactLayoutCheckbox()).not.toBeChecked();

View file

@ -20,31 +20,39 @@ test.describe("Appearance user settings tab", () => {
await util.openAppearanceTab(); await util.openAppearanceTab();
}); });
test("should be rendered with the light theme selected", async ({ page, app, util }) => { test(
// Assert that 'Match system theme' is not checked "should be rendered with the light theme selected",
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked(); { 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 // Assert that the light theme is selected
await expect(util.getLightTheme()).toBeChecked(); await expect(util.getLightTheme()).toBeChecked();
// Assert that the dark and high contrast themes are not selected // Assert that the dark and high contrast themes are not selected
await expect(util.getDarkTheme()).not.toBeChecked(); await expect(util.getDarkTheme()).not.toBeChecked();
await expect(util.getHighContrastTheme()).not.toBeChecked(); await expect(util.getHighContrastTheme()).not.toBeChecked();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
}); },
);
test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => { test(
await util.getMatchSystemThemeCheckbox().click(); "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 // Assert that the themes are disabled
await expect(util.getLightTheme()).toBeDisabled(); await expect(util.getLightTheme()).toBeDisabled();
await expect(util.getDarkTheme()).toBeDisabled(); await expect(util.getDarkTheme()).toBeDisabled();
await expect(util.getHighContrastTheme()).toBeDisabled(); await expect(util.getHighContrastTheme()).toBeDisabled();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
}); },
);
test("should change the theme to dark", async ({ page, app, util }) => { test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => {
// Assert that the light theme is selected // Assert that the light theme is selected
await expect(util.getLightTheme()).toBeChecked(); await expect(util.getLightTheme()).toBeChecked();
@ -63,19 +71,23 @@ test.describe("Appearance user settings tab", () => {
labsFlags: ["feature_custom_themes"], 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"); 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 }) => { test(
await util.addCustomTheme(); "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.getCustomTheme()).not.toBeChecked();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png");
await util.removeCustomTheme(); await util.removeCustomTheme();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png"); await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png");
}); },
);
}); });
}); });
}); });

View file

@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
await app.viewRoomByName(roomName); 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"); const settings = await app.settings.openRoomSettings("General");
// Assert that "Show less" details element is rendered // Assert that "Show less" details element is rendered

View file

@ -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 }); page.setViewportSize({ width: 1024, height: 3300 });
const tab = await app.settings.openUserSettings("Preferences"); const tab = await app.settings.openUserSettings("Preferences");
// Assert that the top heading is rendered // Assert that the top heading is rendered

View file

@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => {
}); });
test.describe("AnalyticsLearnMoreDialog", () => { 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"); const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click(); await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot( await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(

View file

@ -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.viewRoomById(room.roomId);
await app.toggleRoomInfoPanel(); await app.toggleRoomInfoPanel();
await page.getByRole("menuitem", { name: "Copy link" }).click(); 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.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); 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.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" }); await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });

View file

@ -55,7 +55,7 @@ test.describe("Spaces", () => {
botCreateOpts: { displayName: "BotBob" }, 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); const contextMenu = await openSpaceCreateMenu(page);
await expect(contextMenu).toMatchScreenshot("space-create-menu.png"); await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
@ -88,7 +88,7 @@ test.describe("Spaces", () => {
await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible(); 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); const menu = await openSpaceCreateMenu(page);
await menu.getByRole("button", { name: "Private" }).click(); await menu.getByRole("button", { name: "Private" }).click();
@ -216,49 +216,47 @@ test.describe("Spaces", () => {
await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible(); await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible();
}); });
test("should render subspaces in the space panel only when expanded", async ({ test(
page, "should render subspaces in the space panel only when expanded",
app, { tag: "@screenshot" },
user, async ({ page, app, user, axe, checkA11y }) => {
axe, axe.disableRules([
checkA11y, // Disable this check as it triggers on nested roving tab index elements which are in practice fine
}) => { "nested-interactive",
axe.disableRules([ // XXX: We have some known contrast issues here
// Disable this check as it triggers on nested roving tab index elements which are in practice fine "color-contrast",
"nested-interactive", ]);
// XXX: We have some known contrast issues here
"color-contrast",
]);
const childSpaceId = await app.client.createSpace({ const childSpaceId = await app.client.createSpace({
name: "Child Space", name: "Child Space",
initial_state: [], initial_state: [],
}); });
await app.client.createSpace({ await app.client.createSpace({
name: "Root Space", name: "Root Space",
initial_state: [spaceChildInitialState(childSpaceId)], initial_state: [spaceChildInitialState(childSpaceId)],
}); });
// Find collapsed Space panel // Find collapsed Space panel
const spaceTree = page.getByRole("tree", { name: "Spaces" }); const spaceTree = page.getByRole("tree", { name: "Spaces" });
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible(); await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible(); await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
await checkA11y(); await checkA11y();
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png"); 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 // 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". // button with the same name with different class name "mx_SpacePanel_toggleCollapse".
await spaceTree.getByRole("button", { name: "Expand" }).click(); await spaceTree.getByRole("button", { name: "Expand" }).click();
await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector
const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" }); const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" });
await expect(item).toBeVisible(); await expect(item).toBeVisible();
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible(); await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
await checkA11y(); await checkA11y();
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png"); 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 ({ test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({
page, page,

View file

@ -276,7 +276,7 @@ export class Helpers {
* Assert that the threads activity centre button has no indicator * Assert that the threads activity centre button has no indicator
*/ */
async assertNoTacIndicator() { async assertNoTacIndicator() {
// Assert by checkng neither of the known indicators are visible first. This will wait // Assert by checking neither of the known indicators are visible first. This will wait
// if it takes a little time to disappear, but the screenshot comparison won't. // if it takes a little time to disappear, but the screenshot comparison won't.
await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible(); await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible();
await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible(); await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible();
@ -376,7 +376,7 @@ export class Helpers {
* Clicks the button to mark all threads as read in the current room * Clicks the button to mark all threads as read in the current room
*/ */
clickMarkAllThreadsRead() { clickMarkAllThreadsRead() {
return this.page.getByLabel("Mark all as read").click(); return this.page.locator("#thread-panel").getByRole("button", { name: "Mark all as read" }).click();
} }
} }

View file

@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => {
labsFlags: ["threadsActivityCentre"], labsFlags: ["threadsActivityCentre"],
}); });
test("should have the button correctly aligned and displayed in the space panel when expanded", async ({ test(
util, "should have the button correctly aligned and displayed in the space panel when expanded",
}) => { { tag: "@screenshot" },
// Open the space panel async ({ util }) => {
await util.expandSpacePanel(); // Open the space panel
// The buttons in the space panel should be aligned when expanded await util.expandSpacePanel();
await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png"); // 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 not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => {
// No indicator should be shown // No indicator should be shown
await util.assertNoTacIndicator(); await util.assertNoTacIndicator();
@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => {
await util.assertHighlightIndicator(); 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.goTo(room2);
await util.populateThreads(room1, room2, msg); await util.populateThreads(room1, room2, msg);
// The indicator should be shown // The indicator should be shown
@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => {
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png"); 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.goTo(room2);
await util.populateThreads(room1, room2, msg); await util.populateThreads(room1, room2, msg);
@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => {
await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible(); 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 util.hoverTacButton();
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png"); 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"); 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.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertNotificationTac(); await util.assertNotificationTac();
@ -146,7 +148,7 @@ test.describe("Threads Activity Centre", () => {
await util.openTac(); await util.openTac();
await util.clickRoomInTac(room1.name); await util.clickRoomInTac(room1.name);
util.clickMarkAllThreadsRead(); await util.clickMarkAllThreadsRead();
await util.assertNoTacIndicator(); await util.assertNoTacIndicator();
}); });

View file

@ -25,7 +25,7 @@ test.describe("Threads", () => {
}); });
// Flaky: https://github.com/vector-im/element-web/issues/26452 // 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({}); const roomId = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId); await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId); await bot.joinRoom(roomId);
@ -150,7 +150,7 @@ test.describe("Threads", () => {
).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart); ).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart);
// Take snapshot of group layout (IRC layout is not available on ThreadView) // 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", "ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
{ {
mask: mask, mask: mask,
@ -174,7 +174,7 @@ test.describe("Threads", () => {
.toHaveCSS("margin-inline-start", "0px"); .toHaveCSS("margin-inline-start", "0px");
// Take snapshot of bubble layout // 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", "ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
{ {
mask: mask, mask: mask,
@ -351,57 +351,61 @@ test.describe("Threads", () => {
}); });
}); });
test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => { test(
const roomId = await app.client.createRoom({}); "should send location and reply to the location on ThreadView",
await app.client.inviteUser(roomId, bot.credentials.userId); { tag: "@screenshot" },
await bot.joinRoom(roomId); async ({ page, app, bot }) => {
await page.goto("/#/room/" + roomId); 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 // Exclude timestamp, read marker, and maplibregl-map from snapshots
const css = const css =
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }"; ".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }";
let locator = page.locator(".mx_RoomView_body"); let locator = page.locator(".mx_RoomView_body");
// User sends message // User sends message
let textbox = locator.getByRole("textbox", { name: "Send a message…" }); let textbox = locator.getByRole("textbox", { name: "Send a message…" });
await textbox.fill("Hello Mr. Bot"); await textbox.fill("Hello Mr. Bot");
await textbox.press("Enter"); await textbox.press("Enter");
// Wait for message to send, get its ID and save as @threadId // Wait for message to send, get its ID and save as @threadId
const threadId = await locator const threadId = await locator
.locator(".mx_EventTile[data-scroll-tokens]") .locator(".mx_EventTile[data-scroll-tokens]")
.filter({ hasText: "Hello Mr. Bot" }) .filter({ hasText: "Hello Mr. Bot" })
.getAttribute("data-scroll-tokens"); .getAttribute("data-scroll-tokens");
// Bot starts thread // Bot starts thread
await bot.sendMessage(roomId, "Hello there", threadId); await bot.sendMessage(roomId, "Hello there", threadId);
// User clicks thread summary // User clicks thread summary
await page.locator(".mx_RoomView_body .mx_ThreadSummary").click(); await page.locator(".mx_RoomView_body .mx_ThreadSummary").click();
// User sends location on ThreadView // User sends location on ThreadView
await expect(page.locator(".mx_ThreadView")).toBeAttached(); await expect(page.locator(".mx_ThreadView")).toBeAttached();
await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click(); await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click();
await page.getByTestId(`share-location-option-Pin`).click(); await page.getByTestId(`share-location-option-Pin`).click();
await page.locator("#mx_LocationPicker_map").click(); await page.locator("#mx_LocationPicker_map").click();
await page.getByRole("button", { name: "Share location" }).click(); await page.getByRole("button", { name: "Share location" }).click();
await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({ await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({
timeout: 10000, timeout: 10000,
}); });
// User replies to the location // User replies to the location
locator = page.locator(".mx_ThreadView"); locator = page.locator(".mx_ThreadView");
await locator.locator(".mx_EventTile_last").hover(); await locator.locator(".mx_EventTile_last").hover();
await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click(); await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click();
textbox = locator.getByRole("textbox", { name: "Reply to thread…" }); textbox = locator.getByRole("textbox", { name: "Reply to thread…" });
await textbox.fill("Please come here"); await textbox.fill("Please come here");
await textbox.press("Enter"); await textbox.press("Enter");
// Wait until the reply is sent // Wait until the reply is sent
await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible(); await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible();
// Take a snapshot of reply to the shared location // Take a snapshot of reply to the shared location
await page.addStyleTag({ content: css }); await page.addStyleTag({ content: css });
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png"); await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png");
}); },
);
test("right panel behaves correctly", async ({ page, app, user }) => { test("right panel behaves correctly", async ({ page, app, user }) => {
// Create room // Create room

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test";
test.describe("User Menu", () => { test.describe("User Menu", () => {
test.use({ displayName: "Jeff" }); 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(); await page.getByRole("button", { name: "User menu", exact: true }).click();
const menu = page.getByRole("menu"); const menu = page.getByRole("menu");

View file

@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => {
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible(); 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( await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png", "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(); 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 page.getByRole("button", { name: "Download apps" }).click();
await expect( await expect(
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }), page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),

View file

@ -14,7 +14,7 @@ test.describe("UserView", () => {
botCreateOpts: { displayName: "Usman" }, 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}`); await page.goto(`/#/user/${bot.credentials.userId}`);
const rightPanel = page.locator("#mx_RightPanel"); const rightPanel = page.locator("#mx_RightPanel");

View file

@ -70,7 +70,7 @@ test.describe("Widget Layout", () => {
await app.viewRoomByName(ROOM_NAME); 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"); await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png");
}); });

View file

@ -314,6 +314,10 @@ export const expect = baseExpect.extend({
const testInfo = test.info(); const testInfo = test.info();
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`); 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; const page = "page" in receiver ? receiver.page() : receiver;
let css = ` let css = `

View file

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI. // We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:489fe921e03440af87e001106c41c70ffc55a1e8078d1a7f45e16fbaddc5088a"; const DOCKER_TAG = "develop@sha256:6ff2b43b7412eb4155c0147441421b31fc4b31acd56be82cf27daf172ababa4d";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

After

Width:  |  Height:  |  Size: 8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more