Merge branch 'develop' of ssh://github.com/element-hq/element-web into t3chguy/fix/32858

This commit is contained in:
Michael Telatynski 2026-04-28 11:53:34 +01:00
commit 2ad73c1eb7
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
684 changed files with 35188 additions and 24811 deletions

View File

@ -11,7 +11,7 @@ runs:
using: composite
steps:
- name: Download release tarball
uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1
uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1
with:
tag: ${{ inputs.tag }}
fileName: element-*.tar.gz*

16
.github/renovate.json vendored
View File

@ -2,25 +2,39 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>matrix-org/renovate-config-element-web"],
"postUpdateOptions": ["pnpmDedupe"],
"stopUpdatingLabel": "X-Blocked",
"packageRules": [
{
"description": "Group all testcontainers docker digests",
"groupName": "testcontainers docker digests",
"groupSlug": "testcontainers-docker",
"matchDepTypes": ["testcontainers-docker"],
"matchPackageNames": ["*"]
},
{
"description": "Separate updates to overrides from other groups",
"matchDepTypes": ["pnpm.overrides"],
"groupSlug": null
},
{
"description": "Disable any major updates to overrides as this almost always is wrong",
"matchDepTypes": ["pnpm.overrides"],
"matchUpdateTypes": ["major"],
"enabled": false
}
],
"customManagers": [
{
"description": "Update testcontainers docker digests",
"customType": "regex",
"datasourceTemplate": "docker",
"versioningTemplate": "loose",
"description": "Update testcontainers docker digests",
"managerFilePatterns": ["**/testcontainers/*.ts"],
"matchStrings": ["\\s+\"(?<depName>[^@]+):(?<currentValue>[^@]+)@(?<currentDigest>sha256:[a-f0-9]+)\""],
"depTypeTemplate": "testcontainers-docker"
},
{
"description": "Update element-desktop hakDependencies",
"customType": "jsonata",
"managerFilePatterns": ["/(^|/)package\\.json$/"],
"fileFormat": "json",

View File

@ -64,7 +64,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: "lts/*"
@ -146,7 +146,7 @@ jobs:
path: apps/web/webapp
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
@ -206,6 +206,8 @@ jobs:
needs: prepare_ed
name: "Desktop Windows"
uses: ./.github/workflows/build_desktop_windows.yaml
# Skip Windows builds on PRs, as the Linux amd64 build is enough of a smoke test and includes the screenshot tests
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests')
strategy:
matrix:
arch: [x64, ia32, arm64]
@ -223,10 +225,13 @@ jobs:
arch: [amd64, arm64]
runAllTests:
- ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }}
# We ship static sqlcipher builds, so delegate testing the system builds to the merge queue
exclude:
# We ship static sqlcipher builds, so delegate testing the system builds to the merge queue
- runAllTests: false
sqlcipher: system
# Additionally skip arm64 system builds on PRs, as the amd64 test is enough for a smoke test and includes the screenshot tests
- runAllTests: false
arch: arm64
with:
sqlcipher: ${{ matrix.sqlcipher }}
arch: ${{ matrix.arch }}
@ -236,6 +241,9 @@ jobs:
needs: prepare_ed
name: "Desktop macOS"
uses: ./.github/workflows/build_desktop_macos.yaml
# Skip macOS builds on PRs, as the Linux amd64 build is enough of a smoke test and includes the screenshot tests
# and we have a very low limit of concurrent macos runners (5) across the Github org.
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests')
with:
blob_report: true
@ -260,7 +268,7 @@ jobs:
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
if: needs.build_ew.outputs.skip == 'false'
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
if: needs.build_ew.outputs.skip == 'false'
with:
cache: "pnpm"

View File

@ -48,7 +48,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
# Disable cache on Windows as it is slower than not caching
# https://github.com/actions/setup-node/issues/975

View File

@ -111,7 +111,7 @@ jobs:
apps/desktop/.hak
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
@ -126,7 +126,7 @@ jobs:
- name: "Get modified files"
id: changed_files
if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && github.repository == 'element-hq/element-web'
uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47
with:
files: |
apps/desktop/dockerbuild/**

View File

@ -114,7 +114,7 @@ jobs:
- run: sudo pip3 install pyobjc-framework-Quartz
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"

View File

@ -59,7 +59,7 @@ jobs:
repository: element-hq/element-web
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"

View File

@ -42,7 +42,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"
@ -97,6 +97,7 @@ jobs:
PW_TAG: ${{ inputs.project }}
ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }}
ARGS: ${{ inputs.args }}
DEBUG: pw:browser
- name: Upload blob report
if: always() && inputs.blob_report

View File

@ -153,7 +153,7 @@ jobs:
TARGET: ${{ steps.config.outputs.target }}
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: apps/desktop/.node-version
cache: "pnpm"

View File

@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: "lts/*"

View File

@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: Install Cosign
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
@ -41,7 +41,7 @@ jobs:
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: package.json
cache: "pnpm"

View File

@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: Install Cosign
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
if: github.event_name != 'pull_request'
- name: Set up QEMU

View File

@ -23,7 +23,7 @@ jobs:
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
with:
package_json_file: package.json
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml

View File

@ -29,7 +29,7 @@ jobs:
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 Set up node environment
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: ".node-version"

View File

@ -18,7 +18,7 @@ jobs:
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 Pnpm cache
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json

View File

@ -25,9 +25,6 @@ jobs:
actions: read
deployments: write
steps:
- name: Install tree
run: "sudo apt-get install -y tree"
- name: Download Diffs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:

View File

@ -27,7 +27,7 @@ jobs:
repository: element-hq/element-web
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: "lts/*"
@ -45,11 +45,12 @@ jobs:
working-directory: packages/shared-components
run: "pnpm test:storybook --run"
# Workaround for vis silently adding new baselines if they didn't exist
# Can be removed once https://github.com/repobuddy/visual-testing/issues/516 is released
- run: |
git add -N .
git diff --exit-code
- name: Detect stale screenshots
run: |
if diff -rq __baselines__ __results__ | grep "^Only in __baselines__"; then
exit 1
fi
working-directory: packages/shared-components/__vis__/linux
- name: Upload received images & diffs
if: always()

View File

@ -54,7 +54,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
if: matrix.install != ''
with:
cache: "pnpm"
@ -103,7 +103,7 @@ jobs:
voip|element_call
error|invalid_json
error|misconfigured
welcome_to_element
welcome|title_element
devtools|settings|elementCallUrl
labs|sliding_sync_description
settings|voip|noise_suppression_description

View File

@ -45,7 +45,7 @@ jobs:
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: pnpm cache
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "lts/*"
cache: "pnpm"
@ -137,7 +137,7 @@ jobs:
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: pnpm cache
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "lts/*"
cache: "pnpm"
@ -163,6 +163,11 @@ jobs:
working-directory: "packages/${{ matrix.package }}"
run: pnpm test:unit --coverage=$ENABLE_COVERAGE
# Dump the disk usage on failure, because this job seems to fail with disk fills sometimes
- name: df
run: df
if: ${{ failure() }}
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7

View File

@ -14,7 +14,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: "lts/*"
@ -27,7 +27,7 @@ jobs:
run: "pnpm vendor:jitsi"
- name: Create Pull Request
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/jitsi-update

View File

@ -50,6 +50,7 @@ CHANGELOG.md
/apps/desktop/dist/
/apps/desktop/build/
/apps/desktop/dockerbuild/
/apps/desktop/deploys/
/apps/desktop/lib/
/apps/desktop/webapp
/apps/desktop/playwright/html-report

View File

@ -49,6 +49,21 @@ As for your PR description, it should include these things:
Please **_do not use force push_** in your PRs. Doing so means we can't see what
has changed. We use squash merge to get a "clean" git history.
### Adding a new feature or enhancement
To make a great product with a great user experience, all the small efforts need to go in the same direction and be aligned and consistent with each other.
Before making your contribution, please consider the following:
- One product cant do everything well. Element is focusing on private end-to-end encrypted messaging and voice - this can either be for consumers (e.g. friends and family) or for professional teams and organizations. Public forums and other types of chats without E2EE remain supported but are not the primary use case in case UX compromises need to be made.
- There are 3 platforms - Web/Desktop, [Android](https://github.com/element-hq/element-x-android) and [iOS](https://github.com/element-hq/element-x-ios). These platforms need to have feature parity and design consistency. For some features, supporting all platforms is a must have, in some cases exceptions can be made to have it on one platform only.
- To make sure your idea fits both from a design/solution and use case perspective, please open a new issue (or find an existing issue) in [element-meta](https://github.com/element-hq/element-meta/issues) repository describing the use case and how you plan to tackle it. Do not just describe what feature is missing, explain why the users need it with a couple of real life examples from the field.
- In case of an existing issue, please comment that you're planning to contribute. If you create a new issue, please specify that in the issue. In such a case we will try to review the issue ASAP and provide you with initial feedback so you can be confident if and at which conditions your contributions will be accepted.
Once we know that you want to contribute and have confirmed that the new feature is overall aligned with the product direction, the designers of the core team will help you with the designs and any other type of guidance when it comes to the user experience. We will try to unblock you as quickly as we can, but it may not be instant. Having a clear understanding of the use case and the impact of the feature will help us with the prioritization and faster responses.
Only once all of the above is met should you open a PR with your proposed changes.
### Changelogs
There's no need to manually add Changelog entries: we use information in the

View File

@ -1 +1 @@
24.14.1
24.15.0

View File

@ -1,7 +1,7 @@
# Docker image to facilitate building Element Desktop's native bits using a glibc version (2.31)
# with broader compatibility, down to Debian bullseye & Ubuntu focal.
FROM rust:bullseye@sha256:bc19574c121fe10c1bc68fc2b1ea9b420d87d047a0c50fb1622b282199700cee
FROM rust:bullseye@sha256:949b0903defbfc4e374dc85f947b153859e9ee0104e425cd9a74d94474a9a335
ENV DEBIAN_FRONTEND=noninteractive

View File

@ -1,6 +1,7 @@
{
"compilerOptions": {
"moduleResolution": "node",
"moduleResolution": "node16",
"module": "Node16",
"esModuleInterop": true,
"target": "es2022",
"sourceMap": false,

View File

@ -28,7 +28,7 @@
"mkdirs": "mkdirp packages deploys",
"fetch": "pnpm run mkdirs && node scripts/fetch-package.ts",
"asar-webapp": "asar p webapp webapp.asar",
"start": "pnpm run build:ts && pnpm run build:res && electron .",
"start": "nx start",
"lint": "pnpm lint:types && pnpm lint:js",
"lint:js": "eslint --max-warnings 0 src hak playwright scripts",
"lint:js-fix": "eslint --fix --max-warnings 0 src hak playwright scripts && prettier --log-level=warn --write .",
@ -39,22 +39,19 @@
"lint:types:hak": "tsc --noEmit -p hak/tsconfig.json",
"build:native": "pnpm run hak",
"build:native:universal": "pnpm run hak --target x86_64-apple-darwin fetchandbuild && pnpm run hak --target aarch64-apple-darwin fetchandbuild && pnpm run hak --target x86_64-apple-darwin --target aarch64-apple-darwin copyandlink",
"build:32": "pnpm run build:ts && pnpm run build:res && electron-builder --ia32",
"build:64": "pnpm run build:ts && pnpm run build:res && electron-builder --x64",
"build:universal": "pnpm run build:ts && pnpm run build:res && electron-builder --universal",
"build": "pnpm run build:ts && pnpm run build:res && electron-builder",
"build:ts": "tsc",
"build:res": "node scripts/copy-res.ts",
"build:32": "nx build --ia32",
"build:64": "nx build --x64",
"build:universal": "nx build --universal",
"build": "nx build --",
"docker:setup": "docker build --platform linux/amd64 -t element-desktop-dockerbuild -f dockerbuild/Dockerfile .",
"docker:build:native": "scripts/in-docker.sh pnpm run hak",
"docker:build": "scripts/in-docker.sh pnpm run build",
"docker:install": "scripts/in-docker.sh pnpm install",
"clean": "rimraf webapp.asar dist packages deploys lib",
"hak": "node scripts/hak/index.ts",
"test": "playwright test",
"test:open": "pnpm test --ui",
"test:screenshots:build": "docker build playwright -t element-desktop-playwright --platform linux/amd64",
"test:screenshots:run": "docker run --rm --network host -v $(pwd):/work/element-desktop -v element-desktop-playwright:/work/element-desktop/node_modules -v /var/run/docker.sock:/var/run/docker.sock --platform linux/amd64 -it element-desktop-playwright",
"test:playwright": "nx test:playwright --",
"test:playwright:open": "nx test:playwright -- --ui",
"test:playwright:screenshots": "nx test:playwright:screenshots --",
"sane-postinstall": "electron-builder install-app-deps"
},
"dependencies": {
@ -65,13 +62,14 @@
"electron-window-state": "^5.0.3",
"minimist": "^1.2.6",
"png-to-ico": "^3.0.0",
"uuid": "^13.0.0"
"uuid": "^14.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@electron/asar": "4.1.2",
"@electron/asar": "4.2.0",
"@electron/fuses": "^2.1.1",
"@playwright/test": "catalog:",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/auto-launch": "^5.0.1",
@ -81,12 +79,12 @@
"@types/pacote": "^11.1.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"app-builder-lib": "26.8.2",
"app-builder-lib": "26.9.0",
"chokidar": "^5.0.0",
"detect-libc": "^2.0.0",
"electron": "41.1.0",
"electron-builder": "26.8.2",
"electron-builder-squirrel-windows": "26.8.2",
"electron": "41.2.2",
"electron-builder": "26.9.0",
"electron-builder-squirrel-windows": "26.9.0",
"electron-devtools-installer": "^4.0.0",
"eslint": "^8.26.0",
"eslint-config-google": "^0.14.0",
@ -102,10 +100,13 @@
"prettier": "^3.0.0",
"rimraf": "^6.0.0",
"tar": "^7.5.8",
"typescript": "5.9.3"
"typescript": "6.0.3"
},
"hakDependencies": {
"matrix-seshat": "4.0.1"
"matrix-seshat": "4.2.0"
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"nx": {
"includedScripts": []
}
}

View File

@ -1,13 +1,19 @@
FROM mcr.microsoft.com/playwright:v1.59.1-jammy@sha256:8a0360d39d1973be506dd59002904a774f6d697d4946c94063b3fd006461c8ff
WORKDIR /work/element-desktop
WORKDIR /work
RUN apt-get update && apt-get -y install xvfb dbus-x11 && apt-get purge -y --auto-remove && rm -rf /var/lib/apt/lists/*
RUN apt-get update && \
apt-get -y install xvfb dbus-x11 && \
apt-get purge -y --auto-remove && \
rm -rf /var/lib/apt/lists/* && \
corepack enable
# Create node_modules & dist dirs so that the volumes have the correct permissions
RUN mkdir node_modules dist && chown 1000:1000 node_modules dist
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
ENV GITHUB_ACTIONS=1
ENV DEBUG=pw:browser
# switch to node user
USER 1000:1000
COPY docker-entrypoint.sh /opt/docker-entrypoint.sh
COPY apps/desktop/playwright/docker-entrypoint.sh /opt/docker-entrypoint.sh
ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"]

View File

@ -8,11 +8,5 @@ sleep 2
export DISPLAY=:99
pnpm install --frozen-lockfile
pnpm build -l --dir
PLAYWRIGHT_HTML_OPEN=never ELEMENT_DESKTOP_EXECUTABLE="./dist/linux-unpacked/element-desktop" \
npx playwright test --update-snapshots --reporter line,html "$1"
# Clean up
rm -R core qemu_* || exit 0
exec pnpm -C apps/desktop exec playwright test --update-snapshots --reporter line,html "$1"

View File

@ -17,14 +17,14 @@ import { PassThrough } from "node:stream";
* A PassThrough stream that captures all data written to it.
*/
class CapturedPassThrough extends PassThrough {
private _chunks = [];
private _chunks: any[] = [];
public constructor() {
super();
super.on("data", this.onData);
}
private onData = (chunk): void => {
private onData = (chunk: any): void => {
this._chunks.push(chunk);
};
@ -69,7 +69,13 @@ export const test = base.extend<Fixtures>({
const args = ["--profile-dir", tmpDir, ...extraArgs];
if (process.env.GITHUB_ACTIONS) {
args.push("--disable-gpu");
if (process.platform === "linux") {
if (process.getuid() === 0) {
args.push("--no-sandbox");
}
// GitHub Actions hosted runner lacks dbus and a compatible keyring, so we need to force plaintext storage
args.push("--storage-mode", "force-plaintext");
} else if (process.platform === "darwin") {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 957 KiB

View File

@ -1,11 +1,12 @@
{
"compilerOptions": {
"resolveJsonModule": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"esModuleInterop": true,
"target": "es2022",
"module": "es2022",
"lib": ["es2022", "dom"],
"module": "ESNext",
"lib": ["es2024", "dom", "dom.iterable"],
"strictNullChecks": false,
"types": ["node"]
},
"include": ["**/*.ts"]

View File

@ -19,6 +19,65 @@
"tags": ["type=ref,event=branch"]
}
}
},
"build:ts": {
"cache": true,
"command": "tsc",
"inputs": ["src", "{projectRoot}/tsconfig.json"],
"outputs": ["{projectRoot}/lib/*.js", "{projectRoot}/lib/*.d.ts"],
"options": { "cwd": "apps/desktop" }
},
"build:res": {
"cache": true,
"command": "node scripts/copy-res.ts",
"inputs": ["{projectRoot}/i18n"],
"outputs": ["{projectRoot}/lib/i18n"],
"options": { "cwd": "apps/desktop" }
},
"build": {
"cache": true,
"command": "pnpm exec electron-builder",
"inputs": [
"src",
"{projectRoot}/.hak/hakModules",
"{projectRoot}/electron-builder.json",
"{projectRoot}/webapp.asar"
],
"outputs": ["{projectRoot}/dist"],
"options": { "cwd": "apps/desktop" },
"dependsOn": ["build:*"]
},
"start": {
"command": "electron .",
"options": { "cwd": "apps/desktop" },
"dependsOn": ["build:*"]
},
"test:playwright": {
"command": "playwright test",
"options": { "cwd": "apps/desktop" }
},
"test:playwright:screenshots:build-app": {
"executor": "nx:run-commands",
"options": {
"commands": [
"pnpm run build -l --x64 --dir --publish=never",
"pnpm exec electron-fuses write --app ./dist/linux-unpacked/element-desktop EnableNodeCliInspectArguments=on"
],
"parallel": false,
"cwd": "apps/desktop"
},
"dependsOn": ["build:*"]
},
"test:playwright:screenshots:build-docker": {
"cache": true,
"command": "docker build -f playwright/Dockerfile -t element-desktop-playwright --platform linux/amd64 ../..",
"inputs": ["{projectRoot}/playwright/Dockerfile", "{projectRoot}/playwright/docker-entrypoint.sh"],
"options": { "cwd": "apps/desktop" }
},
"test:playwright:screenshots": {
"command": "docker run --rm --network host -v $(pwd)/../../:/work/ --platform linux/amd64 -it element-desktop-playwright",
"options": { "cwd": "apps/desktop" },
"dependsOn": ["test:playwright:screenshots:*"]
}
}
}

View File

@ -22,7 +22,9 @@
"about": "关于",
"brand_help": "%(brand)s帮助",
"help": "帮助",
"preferences": "偏好"
"no": "否",
"preferences": "偏好",
"yes": "是"
},
"confirm_quit": "你确定要退出吗?",
"edit_menu": {
@ -30,9 +32,20 @@
"speech_start_speaking": "开始讲话",
"speech_stop_speaking": "停止讲话"
},
"eol": {
"no_more_updates": "你正在使用的 macOS 版本不受支持。请升级以获取 %(brand)s 更新。",
"title": "不受支持的系统",
"warning": "你正在使用的 macOS 版本不受支持。请升级系统以确保 %(brand)s 能保持运行。"
},
"file_menu": {
"label": "文件"
},
"icon_overlay": {
"description_error": "错误",
"description_notifications": {
"other": "你有 %(count)s 条未读通知。"
}
},
"menu": {
"hide": "隐藏",
"hide_others": "隐藏其他",
@ -49,6 +62,21 @@
"save_image_as_error_description": "图片保存失败",
"save_image_as_error_title": "图片保存失败"
},
"store": {
"error": {
"backend_changed": "清除数据并重新加载?",
"backend_changed_detail": "无法从系统密钥环访问密钥,该密钥似乎已被更改。",
"backend_changed_title": "数据库加载失败",
"backend_no_encryption": "你的系统支持密钥环,但加密功能不可用。",
"backend_no_encryption_detail": "Electron 检测到你的密钥环 %(backend)s 的加密不可用。请确保已安装该密钥环。若已安装请重启设备后重试。也可选择允许 %(brand)s 使用弱加密方式。",
"backend_no_encryption_title": "不支持加密",
"unsupported_keyring": "你的系统存在不受支持的密钥环,这意味着无法打开数据库。",
"unsupported_keyring_detail": "Electron 的密钥环检测未找到受支持的后端。你可以尝试通过命令行参数启动 %(brand)s 来手动配置后端,此操作仅需执行一次。详情请参阅:%(link)s。",
"unsupported_keyring_title": "不受支持的系统",
"unsupported_keyring_use_basic_text": "使用弱加密",
"unsupported_keyring_use_plaintext": "不使用加密"
}
},
"view_menu": {
"actual_size": "实际大小",
"toggle_developer_tools": "切换开发者工具",

View File

@ -1,8 +1,8 @@
# syntax=docker.io/docker/dockerfile:1.22-labs@sha256:4c116b618ed48404d579b5467127b20986f2a6b29e4b9be2fee841f632db6a86
# syntax=docker.io/docker/dockerfile:1.23-labs@sha256:7eca9451d94f9b8ad22e44988b92d595d3e4d65163794237949a8c3413fbed5d
# Context must be the root of the monorepo
# Builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:27e462f5db2402700867dfa8ec35e3a68b127fdf61b505db0dd6ab98c38284bb AS builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:d2059a9c157c9f70739736979fa3635008bf3ca74560b30930dc181228bc427f AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@ -25,7 +25,7 @@ RUN --mount=type=bind,source=.git,target=/src/.git /src/scripts/docker-package.s
RUN cp /src/apps/web/config.sample.json /src/apps/web/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:b5831ee7f7aa827cbae87df4a30a642f62c747d8525f5674365389f3adab278d
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:360465db60105a4cbf5215cd9e5a2ba40ef956978dd94f99707e9674050e38ea
# Need root user to install packages & manipulate the usr directory
USER root

View File

@ -23,6 +23,8 @@ apps/web/webpack-stats.json
apps/web/playwright/
apps/web/webapp/
apps/web/debian/
apps/desktop
!apps/desktop/package.json
packages/shared-components/__vis__/
packages/shared-components/storybook-static/

View File

@ -72,7 +72,7 @@
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
"html-entities": "^2.0.0",
"html-react-parser": "^5.2.2",
"html-react-parser": "^6.0.0",
"is-ip": "^5.0.0",
"js-xxhash": "^5.0.0",
"jsrsasign": "^11.0.0",
@ -89,7 +89,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.364.7",
"posthog-js": "1.369.3",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "catalog:",
@ -101,10 +101,9 @@
"react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.2",
"sanitize-html": "2.17.3",
"tar-js": "^0.3.0",
"ua-parser-js": "1.0.40",
"uuid": "^13.0.0",
"what-input": "^5.2.10"
},
"devDependencies": {
@ -125,8 +124,8 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.5.1",
"@element-hq/element-call-embedded": "0.18.0",
"@casualbot/jest-sonar-reporter": "2.6.0",
"@element-hq/element-call-embedded": "0.19.2",
"@element-hq/element-web-playwright-common": "workspace:*",
"@fetch-mock/jest": "^0.2.20",
"@jest/globals": "^30.2.0",
@ -205,17 +204,17 @@
"mini-css-extract-plugin": "2.10.2",
"modernizr": "^3.12.0",
"playwright-core": "catalog:",
"postcss": "8.5.8",
"postcss": "8.5.10",
"postcss-easings": "4.0.0",
"postcss-hexrgba": "2.1.0",
"postcss-import": "16.1.1",
"postcss-loader": "8.2.1",
"postcss-mixins": "12.1.2",
"postcss-nested": "7.0.2",
"postcss-preset-env": "11.2.0",
"postcss-preset-env": "11.2.1",
"postcss-scss": "4.0.9",
"postcss-simple-vars": "7.0.1",
"prettier": "3.8.1",
"prettier": "3.8.3",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"semver": "^7.5.2",

View File

@ -91,7 +91,7 @@ export default defineConfig<{}, WorkerOptions>({
trace: "on-first-retry",
},
webServer: {
command: process.env.CI ? "npx serve -p 8080 -L ./webapp" : "pnpm start",
command: process.env.CI ? "npx serve -p 8080 -L ./webapp" : "nx --outputStyle stream start",
url: `${baseURL}/config.json`,
reuseExistingServer: true,
timeout: (process.env.CI ? 30 : 120) * 1000,

View File

@ -15,6 +15,8 @@ test.describe("Landmark navigation tests", () => {
});
test("without any rooms", async ({ page, homeserver, app, user }) => {
await app.closeVerifyToast();
// sometimes the space button doesn't appear right away
await expect(page.locator(".mx_SpaceButton_active")).toBeVisible();
@ -118,6 +120,7 @@ test.describe("Landmark navigation tests", () => {
},
);
await app.closeVerifyToast();
await app.viewRoomByName("Bob");
// confirm the room was loaded
await expect(page.getByText("Bob joined the room")).toBeVisible();

View File

@ -20,7 +20,7 @@ test.use({
test("Shows the welcome page by default", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible();
await expect(page.getByRole("link", { name: "Sign in" })).toBeVisible();
});

View File

@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/
import type { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { test, expect, type ExtendedToMatchScreenshotOptions } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { type ElementAppPage } from "../../pages/ElementAppPage";
@ -94,7 +94,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Assert that rendering of the player settled and the play button is visible before taking a snapshot
await checkPlayerVisibility(ircTile);
const screenshotOptions = {
const screenshotOptions: ExtendedToMatchScreenshotOptions = {
css: `
/* The timestamp is of inconsistent width depending on the time the test runs at */
.mx_MessageTimestamp {
@ -120,7 +120,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
};
// Take a snapshot of mx_EventTile_last on IRC layout
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
screenshotOptions.clip = (await page.locator(".mx_EventTile_last").boundingBox()) ?? undefined;
await scrollToBottomOfTimeline(page);
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-irc-layout.png`, screenshotOptions);
@ -129,7 +129,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const groupTile = page.locator(".mx_EventTile_last[data-layout='group']");
await groupTile.locator(".mx_MessageTimestamp").click();
await checkPlayerVisibility(groupTile);
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
screenshotOptions.clip = (await page.locator(".mx_EventTile_last").boundingBox()) ?? undefined;
await scrollToBottomOfTimeline(page);
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-group-layout.png`, screenshotOptions);
@ -138,12 +138,13 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const bubbleTile = page.locator(".mx_EventTile_last[data-layout='bubble']");
await bubbleTile.locator(".mx_MessageTimestamp").click();
await checkPlayerVisibility(bubbleTile);
screenshotOptions.clip = await page.locator(".mx_EventTile_last").boundingBox();
screenshotOptions.clip = (await page.locator(".mx_EventTile_last").boundingBox()) ?? undefined;
await scrollToBottomOfTimeline(page);
await expect(page).toMatchScreenshot(`${detail.replaceAll(" ", "-")}-bubble-layout.png`, screenshotOptions);
};
test.beforeEach(async ({ page, app, user }) => {
await app.closeVerifyToast();
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");

View File

@ -27,6 +27,9 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
await page.getByRole("option", { name: bob.credentials.displayName }).click();
await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible();
await page.getByRole("button", { name: "Go" }).click();
await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
};
const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => {
@ -44,7 +47,7 @@ const bobJoin = async (page: Page, bob: Bot) => {
const bobRooms = cli.getRooms();
if (!bobRooms.length) {
await new Promise<void>((resolve) => {
const onMembership = (_event) => {
const onMembership = () => {
cli.off(window.matrixcs.RoomMemberEvent.Membership, onMembership);
resolve();
};
@ -169,6 +172,7 @@ test.describe("Cryptography", function () {
"creating a DM should work, being e2e-encrypted / user verification",
{ tag: "@screenshot" },
async ({ page, app, bot: bob, user: aliceCredentials }) => {
await app.closeVerifyToast();
await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob);
// send first message

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import type { EmittedEvents, Preset } from "matrix-js-sdk/src/matrix";
import type { Preset, RoomMemberEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { expect, test } from "../../element-web-test";
import {
createRoom,
@ -122,7 +122,7 @@ test.describe("Cryptography", function () {
const roomId = await bob.evaluate(
async (client, { alice }) => {
const encryptionStatePromise = new Promise<void>((resolve) => {
client.on("RoomState.events" as EmittedEvents, (event, _state, _lastStateEvent) => {
client.on("RoomState.events" as RoomStateEvent.Events, (event, _state, _lastStateEvent) => {
if (event.getType() === "m.room.encryption") {
resolve();
}
@ -253,11 +253,14 @@ test.describe("Cryptography", function () {
// invite Alice
const inviteAlicePromise = new Promise<void>((resolve) => {
client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => {
if (member.userId === alice.userId && member.membership === "invite") {
resolve();
}
});
client.on(
"RoomMember.membership" as RoomMemberEvent.Membership,
(_event, member, _oldMembership?) => {
if (member.userId === alice.userId && member.membership === "invite") {
resolve();
}
},
);
});
await client.invite(roomId, alice.userId);
// wait for the invite to come back so that we encrypt to Alice
@ -271,11 +274,14 @@ test.describe("Cryptography", function () {
// kick Alice
const kickAlicePromise = new Promise<void>((resolve) => {
client.on("RoomMember.membership" as EmittedEvents, (_event, member, _oldMembership?) => {
if (member.userId === alice.userId && member.membership === "leave") {
resolve();
}
});
client.on(
"RoomMember.membership" as RoomMemberEvent.Membership,
(_event, member, _oldMembership?) => {
if (member.userId === alice.userId && member.membership === "leave") {
resolve();
}
},
);
});
await client.kick(roomId, alice.userId);
await kickAlicePromise;

View File

@ -166,13 +166,9 @@ async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
return await client.evaluate(async (client) => {
const userId = client.getUserId();
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
return Array.from(
devices
.get(userId)
.values()
.filter((d) => d.dehydrated)
.map((d) => d.deviceId),
);
return Array.from(devices.get(userId).values())
.filter((d) => d.dehydrated)
.map((d) => d.deviceId);
});
}

View File

@ -124,6 +124,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await infoDialog.getByRole("button", { name: "Got it" }).click();
// There should be no toast (other than the notifications one)
await toasts.rejectToast("Verify this device");
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();

View File

@ -13,7 +13,6 @@ import { createRoom, sendMessageInCurrentRoom } from "./utils";
test.use({
displayName: "Alice",
labsFlags: ["feature_share_history_on_invite"],
});
/** Tests for MSC4268: encrypted history sharing */
@ -29,12 +28,16 @@ test.describe("History sharing", function () {
// we then invite Bob, and ensure Bob can see the content.
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await aliceElementApp.closeKeyStorageToast();
// Register a second user, and open it in a second instance of the app
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "Bob");
const bobPage = await createNewInstance(browser, bobCredentials, {}, labsFlags);
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
await bobElementApp.closeKeyStorageToast();
await aliceElementApp.closeNotificationToast();
// Create the room and send a message
await createRoom(alicePage, "TestRoom", true);
@ -49,7 +52,7 @@ test.describe("History sharing", function () {
await sendMessageInCurrentRoom(alicePage, "A message from Alice");
// Send the invite to Bob
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
// Bob accepts the invite
await bobPage.getByRole("option", { name: "TestRoom" }).click();
@ -85,6 +88,7 @@ test.describe("History sharing", function () {
// 5. Charlie can't see the message.
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await aliceElementApp.closeKeyStorageToast();
await createRoom(alicePage, "TestRoom", true);
// Register a second user, and open it in a second instance of the app
@ -92,6 +96,7 @@ test.describe("History sharing", function () {
const bobPage = await createNewInstance(browser, bobCredentials, {}, labsFlags);
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
await bobElementApp.closeKeyStorageToast();
// ... and a third
const charlieCredentials = await homeserver.registerUser(
@ -102,10 +107,11 @@ test.describe("History sharing", function () {
const charliePage = await createNewInstance(browser, charlieCredentials, {}, labsFlags);
const charlieElementApp = new ElementAppPage(charliePage);
await charlieElementApp.client.bootstrapCrossSigning(charlieCredentials);
await charlieElementApp.closeKeyStorageToast();
// Alice invites Bob, and Bob accepts
const roomId = await aliceElementApp.getCurrentRoomIdFromUrl();
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
@ -143,7 +149,7 @@ test.describe("History sharing", function () {
await sendMessageInCurrentRoom(bobPage, "Message3: 'shared' visibility, but Bob thinks it is still 'joined'");
// Alice now invites Charlie
await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId);
await aliceElementApp.inviteUserToCurrentRoom(charlieCredentials.userId, { confirmUnknownUser: true });
await charliePage.getByRole("option", { name: "TestRoom" }).click();
await charliePage.getByRole("button", { name: "Accept" }).click();

View File

@ -31,15 +31,6 @@ test.describe("Key storage out of sync toast", () => {
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
await deleteCachedSecrets(page);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();
});
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {

View File

@ -579,8 +579,8 @@ export async function deleteCachedSecrets(page: Page) {
await page.evaluate(async () => {
const removeCachedSecrets = new Promise((resolve) => {
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
request.onsuccess = (event: Event & { target: { result: IDBDatabase } }) => {
const db = event.target.result;
request.onsuccess = function (this: IDBRequest) {
const db = this.result as IDBDatabase;
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
request.onsuccess = () => {
db.close();

View File

@ -19,6 +19,7 @@ test.describe("Devtools", () => {
const profileSettings = userSettings.locator(".mx_UserProfileSettings");
await profileSettings.getByAltText("Upload").setInputFiles(getSampleFilePath("riot.png"));
await app.closeDialog();
await app.closeVerifyToast();
// Create an initial room.
const createRoomDialog = await app.openCreateRoomDialog();

View File

@ -50,7 +50,7 @@ test.describe("Editing", () => {
const eventTile = page.locator(".mx_EventTile", { hasText: edited });
await expect(eventTile).toBeVisible();
// Click to display the message edit history dialog
await eventTile.getByText("(edited)").click();
await eventTile.getByRole("button", { name: /Edited at .*? Click to view edits\./ }).click();
};
const clickButtonViewSource = async (locator: Locator) => {
@ -89,7 +89,7 @@ test.describe("Editing", () => {
await editLastMessage(page, "Massage");
// Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
await expect(page.getByRole("button", { name: /Edited at .*? Click to view edits\./ })).toBeVisible();
await clickEditedMessage(page, "Massage");
@ -213,7 +213,7 @@ test.describe("Editing", () => {
await editLastMessage(page, "Massage");
// Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
await expect(page.getByRole("button", { name: /Edited at .*? Click to view edits\./ })).toBeVisible();
await clickEditedMessage(page, "Massage");
@ -369,6 +369,6 @@ test.describe("Editing", () => {
// nevertheless, the event should be updated
await expect(messageTile.locator(".mx_EventTile_body")).toHaveText("Edited body");
await expect(messageTile.locator(".mx_EventTile_edited")).toBeVisible();
await expect(messageTile.getByRole("button", { name: /Edited at .*? Click to view edits\./ })).toBeVisible();
});
});

View File

@ -9,6 +9,15 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test";
/**
* CSS which will hide the mxid in the user list of the "unknown users" confirmation dialog. This is useful because the
* MXID is not stable and the screenshot tests will otherwise fail.
*
* Ideally RichItem would give us a way to do this that doesn't involve gnarly CSS.
*/
const UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS =
'[data-testid="userlist"] li > span:nth-last-child(1) { display: none }';
test.describe("Invite dialog", function () {
test.use({
displayName: "Hanako",
@ -62,6 +71,15 @@ test.describe("Invite dialog", function () {
// Invite the bot
await other.getByRole("button", { name: "Invite" }).click();
// Expect a confirmation dialog, screenshot, and dismiss
await expect(
page.locator(".mx_Dialog").getByRole("heading", { name: "Invite new contacts to this room?" }),
).toBeVisible();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-invite-new-contact.png", {
css: UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS,
});
await page.locator(".mx_Dialog").getByRole("button", { name: "Invite" }).click();
// Assert that the invite dialog disappears
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
@ -73,6 +91,7 @@ test.describe("Invite dialog", function () {
"should support inviting a user to Direct Messages",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await app.closeVerifyToast();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
@ -104,6 +123,15 @@ test.describe("Invite dialog", function () {
// Open a direct message UI
await other.getByRole("button", { name: "Go" }).click();
// Expect a confirmation dialog, screenshot, and dismiss
await expect(
page.locator(".mx_Dialog").getByRole("heading", { name: "Start a chat with this new contact?" }),
).toBeVisible();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-chat-with-new-contact.png", {
css: UNKNOWN_IDENTITY_USERS_DIALOG_HIDE_MXID_CSS,
});
await page.locator(".mx_Dialog").getByRole("button", { name: "Continue" }).click();
// Assert that the invite dialog disappears
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();

View File

@ -19,6 +19,8 @@ test.describe("Create Knock Room", () => {
});
test("should create a knock room", async ({ page, app, user }) => {
await app.closeVerifyToast();
const dialog = await app.openCreateRoomDialog();
await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity");
await dialog.getByRole("button", { name: "Room visibility" }).click();
@ -37,6 +39,8 @@ test.describe("Create Knock Room", () => {
});
test("should create a room and change a join rule to knock", async ({ page, app, user }) => {
await app.closeVerifyToast();
const dialog = await app.openCreateRoomDialog();
await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity");
await dialog.getByRole("button", { name: "Create room" }).click();
@ -59,6 +63,8 @@ test.describe("Create Knock Room", () => {
});
test("should create a public knock room", async ({ page, app, user }) => {
await app.closeVerifyToast();
const dialog = await app.openCreateRoomDialog();
await dialog.getByRole("textbox", { name: "Name" }).fill("Cybersecurity");
await dialog.getByRole("button", { name: "Room visibility" }).click();

View File

@ -14,6 +14,7 @@ test.describe("Collapsible Room list", () => {
});
test.beforeEach(async ({ page, app, user }) => {
await app.closeVerifyToast();
await app.closeNotificationToast();
for (let i = 0; i < 10; i++) {
await app.client.createRoom({ name: `room${i}` });

View File

@ -0,0 +1,333 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { expect, test } from "../../../element-web-test";
import { getRoomList, getRoomListHeader, getSectionHeader } from "./utils";
test.describe("Room list custom sections", () => {
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list", "feature_room_list_sections"],
botCreateOpts: {
displayName: "BotBob",
autoAcceptInvites: true,
},
});
/**
* Create a custom section via the header compose menu and dialog.
* @param page
* @param sectionName The name of the section to create
*/
async function createCustomSection(page: Page, sectionName: string): Promise<void> {
const composeMenu = getRoomListHeader(page).getByRole("button", { name: "New conversation" });
await composeMenu.click();
await page.getByRole("menuitem", { name: "New section" }).click();
// Fill in the section name in the dialog
const dialog = page.getByRole("dialog", { name: "Create a section" });
await expect(dialog).toBeVisible();
await dialog.getByRole("textbox", { name: "Section name" }).fill(sectionName);
await dialog.getByRole("button", { name: "Create section" }).click();
// Wait for the dialog to close
await expect(dialog).not.toBeVisible();
}
/**
* Asserts a room is nested under a specific section using the treegrid aria-level hierarchy.
* Section header rows sit at aria-level=1; room rows nested within a section sit at aria-level=2.
* Verifies that the closest preceding aria-level=1 row is the expected section header.
*/
async function assertRoomInSection(page: Page, sectionName: string, roomName: string): Promise<void> {
const roomList = getRoomList(page);
const roomRow = roomList.getByRole("row", { name: `Open room ${roomName}` });
// Room row must be at aria-level=2 (i.e. inside a section)
await expect(roomRow).toHaveAttribute("aria-level", "2");
// The closest preceding aria-level=1 row must be the expected section header.
// XPath preceding:: axis returns nodes before the context in document order; [1] picks the nearest one.
const closestSectionHeader = roomRow.locator(`xpath=preceding::*[@role="row" and @aria-level="1"][1]`);
await expect(closestSectionHeader).toContainText(sectionName);
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
await app.closeVerifyToast();
// Focus the user menu to avoid hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
});
test.describe("Section creation", () => {
test("should create a custom section via the header compose menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
// The custom section header should be visible (even though it is empty)
await expect(getSectionHeader(page, "Work")).toBeVisible();
// The Chats section should also be visible
await expect(getSectionHeader(page, "Chats")).toBeVisible();
});
test("should show 'Section created' toast after creating a section", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Personal");
// The "Section created" toast should appear
await expect(page.getByText("Section created")).toBeVisible();
});
test("should create a custom section via the room option menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
const roomList = getRoomList(page);
const roomItem = roomList.getByRole("option", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
// Open the More Options menu
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
// Open the "Move to" submenu
await page.getByRole("menuitem", { name: "Move to" }).hover();
// Click on "New section"
await page.getByRole("menuitem", { name: "New section" }).click();
// Fill in the section name in the dialog
const dialog = page.getByRole("dialog", { name: "Create a section" });
await expect(dialog).toBeVisible();
await dialog.getByRole("textbox", { name: "Section name" }).fill("Projects");
await dialog.getByRole("button", { name: "Create section" }).click();
// Wait for the dialog to close
await expect(dialog).not.toBeVisible();
// The custom section should be created
await expect(getSectionHeader(page, "Projects")).toBeVisible();
// Room should be moved to the new section
await assertRoomInSection(page, "Projects", "my room");
});
test("should cancel section creation when dialog is dismissed", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
const composeMenu = getRoomListHeader(page).getByRole("button", { name: "New conversation" });
await composeMenu.click();
await page.getByRole("menuitem", { name: "New section" }).click();
// The dialog should appear
const dialog = page.getByRole("dialog", { name: "Create a section" });
await expect(dialog).toBeVisible();
// Cancel the dialog
await dialog.getByRole("button", { name: "Cancel" }).click();
// The dialog should close
await expect(dialog).not.toBeVisible();
// No custom section should be created - should remain a flat list
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
});
test("should create multiple custom sections", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
await createCustomSection(page, "Personal");
// Both custom sections should be visible
await expect(getSectionHeader(page, "Work")).toBeVisible();
await expect(getSectionHeader(page, "Personal")).toBeVisible();
await expect(getSectionHeader(page, "Chats")).toBeVisible();
});
});
test.describe("Custom section display", () => {
test("should show empty custom sections", async ({ page, app }) => {
// Create a room so the Chats section has something
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Empty Section");
// The custom section should be visible even with no rooms
await expect(getSectionHeader(page, "Empty Section")).toBeVisible();
// The room should still be in the Chats section
const roomList = getRoomList(page);
await expect(roomList.getByRole("row", { name: "Open room my room" })).toBeVisible();
});
test("should display custom sections between Favourites and Chats", async ({ page, app }) => {
// Create a favourite room
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);
// Create a low priority room
const lowPrioId = await app.client.createRoom({ name: "low prio room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.lowpriority");
}, lowPrioId);
// Create a regular room
await app.client.createRoom({ name: "regular room" });
// Create a custom section
await createCustomSection(page, "Work");
// All section headers should be visible
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
await expect(getSectionHeader(page, "Work")).toBeVisible();
// Should be expanded by default
await expect(getSectionHeader(page, "Work")).toHaveAttribute("aria-expanded", "true");
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
});
});
test.describe("Section editing", () => {
test("should edit a custom section name via the section header menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
// Open the section header menu
const sectionHeader = getSectionHeader(page, "Work");
await sectionHeader.hover();
await sectionHeader.getByRole("button", { name: "More options" }).click();
// Click "Edit section"
await page.getByRole("menuitem", { name: "Edit section" }).click();
// The edit dialog should appear pre-filled with the current name
const dialog = page.getByRole("dialog", { name: "Edit a section" });
await expect(dialog).toBeVisible();
await expect(dialog.getByRole("textbox", { name: "Section name" })).toHaveValue("Work");
// Change the name and confirm
await dialog.getByRole("textbox", { name: "Section name" }).fill("Personal");
await dialog.getByRole("button", { name: "Edit section" }).click();
// Dialog should close
await expect(dialog).not.toBeVisible();
// Section should have the new name
await expect(getSectionHeader(page, "Personal")).toBeVisible();
await expect(getSectionHeader(page, "Work")).not.toBeVisible();
});
});
test.describe("Section removal", () => {
test("should move rooms back to Chats when their section is removed", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
await createCustomSection(page, "Personal");
const roomList = getRoomList(page);
// Move room to Work section
const roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
await assertRoomInSection(page, "Work", "my room");
// Remove the Work section
const sectionHeader = getSectionHeader(page, "Work");
await sectionHeader.hover();
await sectionHeader.getByRole("button", { name: "More options" }).click();
await page.getByRole("menuitem", { name: "Remove section" }).click();
const dialog = page.getByRole("dialog", { name: "Remove section?" });
await dialog.getByRole("button", { name: "Remove section" }).click();
// Section should be gone
await expect(getSectionHeader(page, "Work")).not.toBeVisible();
// Room should now be in the Chats section
await assertRoomInSection(page, "Chats", "my room");
});
});
test.describe("Adding a room to a custom section", () => {
test("should add a room to a custom section via the More Options menu", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
// Room starts in Chats section (aria-level=2)
const roomItem = roomList.getByRole("row", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
// Open More Options and move to the Work section
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
// Room should now be nested under the Work section header (aria-level=1 → aria-level=2)
await assertRoomInSection(page, "Work", "my room");
});
test(
"should show 'Chat moved' toast when adding a room to a custom section",
{ tag: "@screenshot" },
async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
const roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
// The "Chat moved" toast should appear
await expect(page.getByText("Chat moved")).toBeVisible();
// Remove focus outline from the room item before taking the screenshot
await page.getByRole("button", { name: "User menu" }).focus();
await expect(roomList).toMatchScreenshot("room-list-sections-chat-moved-toast.png");
},
);
test("should remove a room from a custom section when toggling the same section", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
await createCustomSection(page, "Work");
const roomList = getRoomList(page);
// Move to Work section and verify placement via aria-level
let roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
await assertRoomInSection(page, "Work", "my room");
// Toggle off by selecting the same section again
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "Move to" }).hover();
await page.getByRole("menuitem", { name: "Work" }).click();
// Room is back in the Chats section
await assertRoomInSection(page, "Chats", "my room");
});
});
});

View File

@ -6,10 +6,11 @@
*/
import { type Visibility } from "matrix-js-sdk/src/matrix";
import { type Locator, type Page } from "@playwright/test";
import { type Page } from "@playwright/test";
import { expect, test } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { getFilterCollapseButton, getFilterExpandButton, getPrimaryFilters, getRoomOptionsMenu } from "./utils";
test.describe("Room list filters and sort", () => {
test.use({
@ -21,22 +22,6 @@ test.describe("Room list filters and sort", () => {
labsFlags: ["feature_new_room_list"],
});
function getPrimaryFilters(page: Page): Locator {
return page.getByTestId("primary-filters");
}
function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
function getFilterExpandButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
}
function getFilterCollapseButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
}
/**
* Get the room list
* @param page
@ -46,7 +31,8 @@ test.describe("Room list filters and sort", () => {
}
test.beforeEach(async ({ page, app, bot, user }) => {
// The notification toast is displayed above the search section
// The toasts are displayed above the search section
await app.closeVerifyToast();
await app.closeNotificationToast();
});

View File

@ -6,23 +6,16 @@
*/
import { test, expect } from "../../../element-web-test";
import type { Page } from "@playwright/test";
import { getHeaderSection } from "./utils";
test.describe("Header section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the header section of the room list
* @param page
*/
function getHeaderSection(page: Page) {
return page.getByTestId("room-list-header");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
// The toasts are displayed above the search section
await app.closeVerifyToast();
await app.closeNotificationToast();
});

View File

@ -5,25 +5,17 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
import { getRoomListView } from "./utils";
test.describe("Room list panel", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the room list view
* @param page
*/
function getRoomListView(page: Page) {
return page.getByRole("navigation", { name: "Room list" });
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
// The toasts are displayed above the search section
await app.closeVerifyToast();
await app.closeNotificationToast();
// Populate the room list

View File

@ -5,25 +5,17 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
import { getSearchSection } from "./utils";
test.describe("Search section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the search section of the room list
* @param page
*/
function getSearchSection(page: Page) {
return page.getByRole("search");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
// The toasts are displayed above the search section
await app.closeVerifyToast();
await app.closeNotificationToast();
});

View File

@ -5,9 +5,8 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Locator, type Page } from "@playwright/test";
import { expect, test } from "../../../element-web-test";
import { getPrimaryFilters, getRoomList, getSectionHeader } from "./utils";
test.describe("Room list sections", () => {
test.use({
@ -19,36 +18,9 @@ test.describe("Room list sections", () => {
},
});
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page): Locator {
return page.getByTestId("room-list");
}
/**
* Get the primary filters
* @param page
*/
function getPrimaryFilters(page: Page): Locator {
return page.getByTestId("primary-filters");
}
/**
* Get a section header toggle button by section name
* @param page
* @param sectionName The display name of the section (e.g. "Favourites", "Chats", "Low Priority")
* @param isUnread Whether to look for the unread version of the section header
*/
function getSectionHeader(page: Page, sectionName: string, isUnread = false): Locator {
return getRoomList(page).getByRole("gridcell", {
name: isUnread ? `Toggle ${sectionName} section with unread room(s)` : `Toggle ${sectionName} section`,
});
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
// The toasts are displayed above the search section
await app.closeVerifyToast();
await app.closeNotificationToast();
// focus the user menu to avoid to have hover decoration

View File

@ -10,6 +10,7 @@ import { type Page } from "@playwright/test";
import { expect, test } from "../../../element-web-test";
import { type Bot } from "../../../pages/bot";
import { type ElementAppPage } from "../../../pages/ElementAppPage";
import { getRoomList } from "./utils";
test.describe("Room list", () => {
test.use({
@ -20,16 +21,9 @@ test.describe("Room list", () => {
},
});
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
// The toasts are displayed above the search section
await app.closeVerifyToast();
await app.closeNotificationToast();
// focus the user menu to avoid to have hover decoration
@ -324,6 +318,10 @@ test.describe("Room list", () => {
.click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("video room");
// Make it public to avoid any crypto setup toasts
await page.getByRole("button", { name: "Room visibility" }).click();
await page.getByRole("option", { name: "Public room" }).click();
await page.getByRole("textbox", { name: "Room address" }).fill("video-room");
await page.getByRole("button", { name: "Create video room" }).click();
const roomListView = getRoomList(page);

View File

@ -0,0 +1,92 @@
/*
* Copyright 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type Locator, type Page } from "@playwright/test";
/**
* Get the room list
* @param page
*/
export function getRoomList(page: Page): Locator {
return page.getByTestId("room-list");
}
/**
* Get the room list header
* @param page
*/
export function getRoomListHeader(page: Page): Locator {
return page.getByTestId("room-list-header");
}
/**
* Get a section header toggle button by section name
* @param page
* @param sectionName The display name of the section
* @param isUnread Whether to look for the unread version of the section header
*/
export function getSectionHeader(page: Page, sectionName: string, isUnread = false): Locator {
return getRoomList(page).getByRole("gridcell", {
name: isUnread ? `Toggle ${sectionName} section with unread room(s)` : `Toggle ${sectionName} section`,
});
}
/**
* Get the primary filters container
* @param page
*/
export function getPrimaryFilters(page: Page): Locator {
return page.getByTestId("primary-filters");
}
/**
* Get the room options menu button in the room list header
* @param page
*/
export function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
/**
* Get the filter list expand button in the room list header
* @param page
*/
export function getFilterExpandButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
}
/**
* Get the filter list collapse button in the room list header
* @param page
*/
export function getFilterCollapseButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
}
/**
* Get the header section of the room list
* @param page
*/
export function getHeaderSection(page: Page) {
return page.getByTestId("room-list-header");
}
/**
* Get the room list view
* @param page
*/
export function getRoomListView(page: Page) {
return page.getByRole("navigation", { name: "Room list" });
}
/**
* Get the search section of the room list
* @param page
*/
export function getSearchSection(page: Page) {
return page.getByRole("search");
}

View File

@ -126,7 +126,7 @@ test.describe("Login", () => {
await page.goto("/");
// Should give us the welcome page initially
await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible();
// Start the login process
await expect(axe).toHaveNoViolations();

View File

@ -88,6 +88,7 @@ test.describe("Message rendering", () => {
room: async ({ user, app }, use) => {
const roomId = await app.client.createRoom({ name: "Test room" });
await use({ roomId });
await app.closeVerifyToast();
},
});
@ -218,6 +219,7 @@ test.describe("Message url previews", () => {
room: async ({ user, app }, use) => {
const roomId = await app.client.createRoom({ name: "Test room" });
await use({ roomId });
await app.closeVerifyToast();
},
});
test("should render a basic preview", { tag: "@screenshot" }, async ({ page, user, app, room, axe }) => {
@ -252,6 +254,7 @@ test.describe("Message url previews", () => {
"og:title": "A simple site",
"og:description": "And with a brief description",
"og:image": mxc,
"og:image:alt": "The riot logo",
},
});
});

View File

@ -148,7 +148,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
// richvdh: This takes several seconds to happen on a dev instance
await expect(page.getByText("Welcome")).toBeVisible({ timeout: 10000 });
// Log out
await page.getByRole("button", { name: "User menu" }).click();
@ -162,11 +163,14 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Log in again
await page.goto("/#/login");
await expect(page.getByText("Sign in")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Continue to Element?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
// We should be being warned that we need to verify (but we can't)
await expect(page.getByText("Confirm your digital identity")).toBeVisible();
// richvdh: Again, Element takes several seconds to load on a dev instance
await expect(page.getByText("Confirm your digital identity")).toBeVisible({ timeout: 10000 });
// And there should be no way to close this prompt
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();

View File

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import type { JSHandle } from "@playwright/test";
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
import type { MatrixEvent, ISendEventResponse, ReceiptType, RelationType } from "matrix-js-sdk/src/matrix";
import { expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { type Bot } from "../../pages/bot";
@ -47,7 +47,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
getId: () => eventResponse.event_id,
threadRootId,
getTs: () => 1,
isRelation: (relType) => {
isRelation: (relType: RelationType) => {
return !relType || relType === "m.thread";
},
} as any as MatrixEvent;

View File

@ -65,6 +65,7 @@ test.describe("Room Directory", () => {
room_alias_name: "test1234",
});
await app.closeVerifyToast();
await page.getByRole("button", { name: "Explore rooms" }).click();
const dialog = page.locator(".mx_SpotlightDialog");

View File

@ -22,6 +22,7 @@ test.describe("Create Room", () => {
"should create a public room with name, topic & address set",
{ tag: "@screenshot" },
async ({ page, user, app, axe }) => {
await app.closeVerifyToast();
const dialog = await app.openCreateRoomDialog();
// Fill name & topic
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
@ -50,6 +51,8 @@ test.describe("Create Room", () => {
);
test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => {
await app.closeVerifyToast();
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "Start chat" }).click();
@ -57,6 +60,9 @@ test.describe("Create Room", () => {
await page.getByRole("button", { name: "Go" }).click();
await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Encryption enabled")).toBeVisible();
await expect(page.getByText("Send your first message to")).toBeVisible();
@ -66,6 +72,7 @@ test.describe("Create Room", () => {
test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
await app.closeVerifyToast();
const dialog = await app.openCreateRoomDialog("New video room");
// Fill name & topic
@ -100,6 +107,7 @@ test.describe("Create Room", () => {
});
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
await app.closeVerifyToast();
const dialog = await app.openCreateRoomDialog();
// Fill name & topic
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
@ -125,7 +133,9 @@ test.describe("Create Room", () => {
test.describe("when the encrypted state labs flag is turned off", () => {
test.use({ labsFlags: [] });
test("creates a room without encrypted state", { tag: "@screenshot" }, async ({ page, user: _user }) => {
test("creates a room without encrypted state", { tag: "@screenshot" }, async ({ page, user: _user, app }) => {
await app.closeVerifyToast();
// When we start to create a room
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
@ -154,7 +164,9 @@ test.describe("Create Room", () => {
test(
"creates a room with encrypted state if we check the box",
{ tag: "@screenshot" },
async ({ page, user: _user }) => {
async ({ page, user: _user, app }) => {
await app.closeVerifyToast();
// Given we check the Encrypted State checkbox
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
@ -181,7 +193,9 @@ test.describe("Create Room", () => {
test(
"creates a room without encrypted state if we don't check the box",
{ tag: "@screenshot" },
async ({ page, user: _user }) => {
async ({ page, user: _user, app }) => {
await app.closeVerifyToast();
// Given we did not check the Encrypted State checkbox
await page.getByRole("button", { name: "New conversation", exact: true }).click();
await page.getByRole("menuitem", { name: "New room" }).click();

View File

@ -19,6 +19,7 @@ test.describe("Room Status Bar", () => {
const roomId = await app.client.createRoom({
name: "A room",
});
await app.closeVerifyToast();
await app.closeNotificationToast();
await app.viewRoomById(roomId);
await use({ roomId });
@ -139,6 +140,7 @@ test.describe("Room Status Bar", () => {
"should show an error when creating a local room fails",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await app.closeVerifyToast();
await page
.getByRole("navigation", { name: "Room list" })
.getByRole("button", { name: "New conversation" })
@ -163,6 +165,10 @@ test.describe("Room Status Bar", () => {
).toBeVisible();
await other.getByRole("option", { name: "Alice" }).click();
await other.getByRole("button", { name: "Go" }).click();
await expect(page.getByRole("heading", { name: "Start a chat with this new contact?" })).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
// Send a message to invite the bots
const composer = app.getComposerField();
await composer.fill("Hello");

View File

@ -14,6 +14,7 @@ test.describe("Appearance user settings tab", () => {
});
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
await app.closeVerifyToast();
const tab = await app.settings.openUserSettings("Appearance");
// Click "Show advanced" link button
@ -31,6 +32,7 @@ test.describe("Appearance user settings tab", () => {
"should support changing font size by using the font size dropdown",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
await app.closeVerifyToast();
await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
@ -46,6 +48,7 @@ test.describe("Appearance user settings tab", () => {
);
test("should support enabling system font", async ({ page, app, user }) => {
await app.closeVerifyToast();
await app.settings.openUserSettings("Appearance");
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
@ -63,7 +66,10 @@ test.describe("Appearance user settings tab", () => {
"should keep same font and emoji when switching theme",
{ tag: "@screenshot" },
async ({ page, app, user, util }) => {
await app.closeVerifyToast();
const roomId = await util.createAndDisplayRoom();
await app.client.sendMessage(roomId, { body: "Message with 🦡", msgtype: "m.text" });
await app.settings.openUserSettings("Appearance");

View File

@ -17,6 +17,8 @@ test.describe("Appearance user settings tab", () => {
test.beforeEach(async ({ app, user, util }) => {
// Disable the default theme for consistency in case ThemeWatcher automatically chooses it
await util.disableSystemTheme();
await app.closeVerifyToast();
await util.openAppearanceTab();
});
@ -102,6 +104,7 @@ test.describe("Appearance user settings tab", () => {
await expect(page).toMatchScreenshot("window-custom-theme.png");
await page.reload();
await app.closeVerifyToast();
await util.openAppearanceTab();
// Assert that the custom theme is still selected after reloading the page

View File

@ -85,6 +85,7 @@ test.describe("Encryption tab", () => {
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await dialog.getByRole("heading", { name: "Key storage" }).scrollIntoViewIfNeeded();
await expect(dialog).toMatchScreenshot("default-tab.png", {
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
});

View File

@ -6,10 +6,13 @@
*/
import { createNewInstance } from "@element-hq/element-web-playwright-common";
import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
import { type Page, type Browser, type TestInfo } from "@playwright/test";
import { test, expect } from "./index";
import { ElementAppPage } from "../../../pages/ElementAppPage";
import { createRoom, sendMessageInCurrentRoom, verifyApp } from "../../crypto/utils";
import { type CredentialsOptionalAccessToken } from "../../../pages/bot";
test.describe("Other people's devices section in Encryption tab", () => {
test.use({
@ -23,21 +26,13 @@ test.describe("Other people's devices section in Encryption tab", () => {
browser,
user: aliceCredentials,
}, testInfo) => {
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await prepForEncryption(aliceElementApp, aliceCredentials);
// Create a second browser instance.
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
const bobPage = await createNewInstance(browser, bobCredentials, {});
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
// Create the room and invite bob
await createRoom(alicePage, "TestRoom", true);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
// Alice sends a message, which Bob should be able to decrypt
await sendMessageInCurrentRoom(alicePage, "Decryptable");
@ -52,7 +47,7 @@ test.describe("Other people's devices section in Encryption tab", () => {
user: aliceCredentials,
util,
}, testInfo) => {
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await prepForEncryption(aliceElementApp, aliceCredentials);
// Enable blacklist toggle.
const dialog = await util.openEncryptionTab();
@ -65,18 +60,10 @@ test.describe("Other people's devices section in Encryption tab", () => {
await aliceElementApp.settings.closeDialog();
// Create a second browser instance.
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
const bobPage = await createNewInstance(browser, bobCredentials, {});
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
// Create the room and invite bob
await createRoom(alicePage, "TestRoom", true);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
// Alice sends a message, which Bob should not be able to decrypt
await sendMessageInCurrentRoom(alicePage, "Undecryptable");
@ -95,7 +82,7 @@ test.describe("Other people's devices section in Encryption tab", () => {
user: aliceCredentials,
util,
}, testInfo) => {
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await prepForEncryption(aliceElementApp, aliceCredentials);
// Enable blacklist toggle.
const dialog = await util.openEncryptionTab();
@ -108,21 +95,11 @@ test.describe("Other people's devices section in Encryption tab", () => {
await aliceElementApp.settings.closeDialog();
// Create a second browser instance.
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
const bobPage = await createNewInstance(browser, bobCredentials, {});
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
const { bobCredentials, bobPage, bobElementApp } = await newBrowser(homeserver, testInfo, browser);
// Create the room and invite bob
await createRoom(alicePage, "TestRoom", true);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite and dismisses the warnings.
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
await bobPage.getByRole("button", { name: "Dismiss" }).click(); // enable notifications
await bobPage.getByRole("button", { name: "Dismiss" }).click(); // enable key storage
await bobPage.getByRole("button", { name: "Yes, dismiss" }).click(); // enable key storage x2
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
await bobElementApp.closeNotificationToast();
// Perform verification.
await verifyApp("alice", aliceElementApp, "bob", bobElementApp);
@ -139,21 +116,13 @@ test.describe("Other people's devices section in Encryption tab", () => {
browser,
user: aliceCredentials,
}, testInfo) => {
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await prepForEncryption(aliceElementApp, aliceCredentials);
// Create a second browser instance.
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
const bobPage = await createNewInstance(browser, bobCredentials, {});
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
// Alice creates the room and invite Bob.
await createRoom(alicePage, "TestRoom", true);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite.
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
// Alice creates the room and invites Bob.
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
// Alice configures her client to blacklist unverified users in this room.
const dialog = await aliceElementApp.settings.openRoomSettings("Security & Privacy");
@ -168,10 +137,6 @@ test.describe("Other people's devices section in Encryption tab", () => {
),
).toBeVisible();
// Alice dismisses key storage warnings, as they now hide the "New conversation" button.
await alicePage.getByRole("button", { name: "Dismiss" }).click(); // enable key storage
await alicePage.getByRole("button", { name: "Yes, dismiss" }).click(); // enable key storage x2
// Alice creates a second room and invites Bob.
await createRoom(alicePage, "TestRoom2", true);
await aliceElementApp.toggleRoomInfoPanel(); // should not be necessary, called in body of below
@ -194,7 +159,7 @@ test.describe("Other people's devices section in Encryption tab", () => {
user: aliceCredentials,
util,
}, testInfo) => {
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
await prepForEncryption(aliceElementApp, aliceCredentials);
// Enable blacklist toggle.
let dialog = await util.openEncryptionTab();
@ -207,18 +172,10 @@ test.describe("Other people's devices section in Encryption tab", () => {
await aliceElementApp.settings.closeDialog();
// Create a second browser instance.
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
const bobPage = await createNewInstance(browser, bobCredentials, {});
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
const { bobCredentials, bobPage } = await newBrowser(homeserver, testInfo, browser);
// Alice creates the room and invite Bob.
await createRoom(alicePage, "TestRoom", true);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite.
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
// Alice creates the room and invites Bob.
await inviteBobToNewRoom(alicePage, aliceElementApp, bobCredentials, bobPage);
// Alice configures her client to allow sending to unverified users in this room.
dialog = await aliceElementApp.settings.openRoomSettings("Security & Privacy");
@ -229,10 +186,6 @@ test.describe("Other people's devices section in Encryption tab", () => {
await sendMessageInCurrentRoom(alicePage, "Decryptable");
await expect(bobPage.getByText("Decryptable")).toBeVisible();
// Alice dismisses key storage warnings, as they now hide the "New conversation" button.
await alicePage.getByRole("button", { name: "Dismiss" }).click(); // enable key storage
await alicePage.getByRole("button", { name: "Yes, dismiss" }).click(); // enable key storage x2
// Alice creates a second room and invites Bob.
await createRoom(alicePage, "TestRoom2", true);
await aliceElementApp.toggleRoomInfoPanel(); // should not be necessary, called in body of below
@ -251,3 +204,32 @@ test.describe("Other people's devices section in Encryption tab", () => {
).toBeVisible();
});
});
async function inviteBobToNewRoom(
alicePage: Page,
aliceElementApp: ElementAppPage,
bobCredentials: CredentialsOptionalAccessToken,
bobPage: Page,
) {
await createRoom(alicePage, "TestRoom", true);
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId, { confirmUnknownUser: true });
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
}
async function newBrowser(
homeserver: StartedHomeserverContainer,
testInfo: TestInfo,
browser: Browser,
): Promise<{ bobCredentials: CredentialsOptionalAccessToken; bobPage: Page; bobElementApp: ElementAppPage }> {
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "bob");
const bobPage = await createNewInstance(browser, bobCredentials, {});
const bobElementApp = new ElementAppPage(bobPage);
await prepForEncryption(bobElementApp, bobCredentials);
return { bobCredentials, bobPage, bobElementApp };
}
async function prepForEncryption(app: ElementAppPage, credentials: CredentialsOptionalAccessToken): Promise<void> {
await app.client.bootstrapCrossSigning(credentials);
await app.closeKeyStorageToast();
}

View File

@ -26,7 +26,8 @@ test.describe("Security user settings tab", () => {
});
test.beforeEach(async ({ page, app, user }) => {
// Dismiss "Notification" toast
// Dismiss toasts
await app.closeVerifyToast();
await app.closeNotificationToast();
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
});

View File

@ -72,6 +72,7 @@ test.describe("Sliding Sync", () => {
// Load the user fixture for all tests
test.beforeEach(async ({ app, user }) => {
await app.closeVerifyToast();
await app.closeNotificationToast();
});

View File

@ -68,6 +68,7 @@ test.describe("Spaces", () => {
"should allow user to create public space",
{ tag: ["@screenshot", "@no-webkit"] },
async ({ page, app, user }) => {
await app.closeVerifyToast();
const contextMenu = await openSpaceCreateMenu(page);
await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
@ -104,6 +105,7 @@ test.describe("Spaces", () => {
);
test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.closeVerifyToast();
const menu = await openSpaceCreateMenu(page);
await menu.getByRole("button", { name: "Private" }).click();
@ -150,6 +152,7 @@ test.describe("Spaces", () => {
name: "Sample Room",
});
await app.closeVerifyToast();
const menu = await openSpaceCreateMenu(page);
await menu.getByRole("button", { name: "Private" }).click();
@ -184,6 +187,7 @@ test.describe("Spaces", () => {
name: "A Room that will not be selected",
});
await app.closeVerifyToast();
const menu = await openSpaceCreateMenu(page);
await menu.getByRole("button", { name: "Private" }).click();
@ -283,6 +287,8 @@ test.describe("Spaces", () => {
"should render subspaces in the space panel only when expanded",
{ tag: "@screenshot" },
async ({ page, app, user, axe }) => {
await app.closeVerifyToast();
axe.disableRules([
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
"nested-interactive",
@ -404,6 +410,7 @@ test.describe("Spaces", () => {
});
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app }) => {
await app.closeVerifyToast();
const menu = await openSpaceCreateMenu(page);
await menu
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')

View File

@ -24,7 +24,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test(
"should have the button correctly aligned and displayed in the space panel when expanded",
{ tag: "@screenshot" },
async ({ util }) => {
async ({ util, app }) => {
await app.closeVerifyToast();
// Open the space panel
await util.expandSpacePanel();
// The buttons in the space panel should be aligned when expanded
@ -32,7 +34,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
},
);
test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => {
test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util, app }) => {
await app.closeVerifyToast();
// No indicator should be shown
await util.assertNoTacIndicator();
@ -43,7 +47,14 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
await util.assertNoTacIndicator();
});
test("should show a notification indicator when there is a message in a thread", async ({ room1, util, msg }) => {
test("should show a notification indicator when there is a message in a thread", async ({
room1,
util,
msg,
app,
}) => {
await app.closeVerifyToast();
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
@ -56,7 +67,10 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
util,
msg,
user,
app,
}) => {
await app.closeVerifyToast();
await util.goTo(room1);
await util.receiveMessages(room1, [
"Msg1",
@ -77,7 +91,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test(
"should show the rooms with unread threads",
{ tag: "@screenshot" },
async ({ room1, room2, util, msg, user }) => {
async ({ room1, room2, util, msg, user, app }) => {
await app.closeVerifyToast();
await util.goTo(room2);
await util.populateThreads(room1, room2, msg, user);
// The indicator should be shown
@ -95,30 +111,38 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
},
);
test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, user }) => {
await util.goTo(room2);
await util.populateThreads(room1, room2, msg, user);
test(
"should update with a thread is read",
{ tag: "@screenshot" },
async ({ room1, room2, util, msg, user, app }) => {
await app.closeVerifyToast();
// Click on the first room in TAC
await util.openTac();
await util.clickRoomInTac(room2.name);
await util.goTo(room2);
await util.populateThreads(room1, room2, msg, user);
// Verify that the thread panel is opened after a click on the room in the TAC
await util.assertThreadPanelIsOpened();
// Click on the first room in TAC
await util.openTac();
await util.clickRoomInTac(room2.name);
// Open a thread and mark it as read
// The room 2 doesn't have a mention anymore in its unread, so the highest notification level is notification
await util.openThread("Msg1");
await util.assertNotificationTac();
await util.openTac();
await util.assertRoomsInTac([
{ room: room1.name, notificationLevel: "notification" },
{ room: room2.name, notificationLevel: "notification" },
]);
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-notification-unread.png");
});
// Verify that the thread panel is opened after a click on the room in the TAC
await util.assertThreadPanelIsOpened();
// Open a thread and mark it as read
// The room 2 doesn't have a mention anymore in its unread, so the highest notification level is notification
await util.openThread("Msg1");
await util.assertNotificationTac();
await util.openTac();
await util.assertRoomsInTac([
{ room: room1.name, notificationLevel: "notification" },
{ room: room2.name, notificationLevel: "notification" },
]);
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-notification-unread.png");
},
);
test("should order by recency after notification level", async ({ room1, room2, util, msg, user, app }) => {
await app.closeVerifyToast();
test("should order by recency after notification level", async ({ room1, room2, util, msg, user }) => {
await util.goTo(room2);
await util.populateThreads(room1, room2, msg, user, false);
@ -129,7 +153,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
]);
});
test("should block the Spotlight to open when the TAC is opened", async ({ util, page }) => {
test("should block the Spotlight to open when the TAC is opened", async ({ util, page, app }) => {
await app.closeVerifyToast();
const toggleSpotlight = () => page.keyboard.press(`${CommandOrControl}+k`);
// Sanity check
@ -144,7 +170,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible();
});
test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => {
test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page, app }) => {
await app.closeVerifyToast();
await util.hoverTacButton();
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png");
@ -154,7 +182,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
});
test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => {
test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page, app }) => {
await app.closeVerifyToast();
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.assertNotificationTac();
@ -167,7 +197,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
await util.assertNoTacIndicator();
});
test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg }) => {
test("should focus the thread tab when clicking an item in the TAC", async ({ room1, room2, util, msg, app }) => {
await app.closeVerifyToast();
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
await util.openTac();

View File

@ -36,6 +36,8 @@ test.describe("Media preview settings", () => {
});
test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
await app.closeVerifyToast();
let settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Hide avatars of room and inviter").click();
await app.closeDialog();

View File

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import type { Locator, Page } from "@playwright/test";
import type { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/matrix";
import type { ISendEventResponse, EventType, MsgType, IContent } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
@ -50,11 +50,9 @@ const expectAvatar = async (cli: Client, e: Locator, avatarUrl: string): Promise
};
const sendEvent = async (client: Client, roomId: string, html = false): Promise<ISendEventResponse> => {
const content = {
const content: IContent = {
msgtype: "m.text" as MsgType,
body: "Message",
format: undefined,
formatted_body: undefined,
};
if (html) {
content.format = "org.matrix.custom.html";
@ -790,6 +788,7 @@ test.describe("Timeline", () => {
await sendEvent(app.client, room.roomId);
await sendEvent(app.client, room.roomId, true);
await page.goto(`/#/room/${room.roomId}`);
await app.closeVerifyToast();
await app.toggleRoomInfoPanel();
@ -815,6 +814,7 @@ test.describe("Timeline", () => {
await sendEvent(app.client, room.roomId);
await page.goto(`/#/room/${room.roomId}`);
await app.closeVerifyToast();
// Open a room setting dialog
await app.toggleRoomInfoPanel();

View File

@ -14,6 +14,7 @@ test.describe("Analytics Toast", () => {
});
test("should not show an analytics toast if config has nothing about posthog", async ({ user, toasts }) => {
await toasts.rejectToast("Verify this device");
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
});
@ -29,6 +30,7 @@ test.describe("Analytics Toast", () => {
});
test.beforeEach(async ({ user, toasts }) => {
await toasts.rejectToast("Verify this device");
await toasts.rejectToast("Notifications");
});

View File

@ -42,7 +42,7 @@ export async function waitForRoom(
return new Promise<Room>((resolve) => {
const room = matrixClient.getRoom(roomId);
if (window[predicateId](room)) {
if ((<any>window)[predicateId](room)) {
resolve(room);
return;
}
@ -50,7 +50,7 @@ export async function waitForRoom(
function onEvent(ev: MatrixEvent) {
if (ev.getRoomId() !== roomId) return;
if (window[predicateId](room)) {
if ((<any>window)[predicateId](room)) {
matrixClient.removeListener("event" as ClientEvent, onEvent);
resolve(room);
}

View File

@ -216,9 +216,9 @@ test.describe("Element Call", () => {
});
});
[true, false].forEach((skipLobbyToggle) => {
[true, false].forEach((joinWithVideo) => {
test(
`should be able to join a call via incoming video call toast (skipLobby=${skipLobbyToggle})`,
`should be able to join a call via incoming video call toast (joinWithVideo=${joinWithVideo})`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
@ -230,7 +230,7 @@ test.describe("Element Call", () => {
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Join" });
if (skipLobbyToggle) {
if (joinWithVideo) {
await toast.getByRole("switch").check();
await expect(toast).toMatchScreenshot(`incoming-call-group-video-toast-checked.png`);
} else {
@ -246,8 +246,8 @@ test.describe("Element Call", () => {
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing");
expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
expect(hash.get("intent")).toEqual(joinWithVideo ? "join_existing" : "join_existing_voice");
expect(hash.get("skipLobby")).toEqual("true");
},
);
});
@ -275,7 +275,7 @@ test.describe("Element Call", () => {
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing");
expect(hash.get("intent")).toEqual("join_existing_voice");
expect(hash.get("skipLobby")).toEqual("true");
},
);
@ -349,9 +349,9 @@ test.describe("Element Call", () => {
expect(hash.get("skipLobby")).toEqual(null);
});
[true, false].forEach((skipLobbyToggle) => {
[true, false].forEach((joinWithVideo) => {
test(
`should be able to join a call via incoming call toast (skipLobby=${skipLobbyToggle})`,
`should be able to join a call via incoming call toast (joinWithVideo=${joinWithVideo})`,
{ tag: ["@screenshot"] },
async ({ page, user, bot, room, app }) => {
await app.viewRoomById(room.roomId);
@ -359,14 +359,14 @@ test.describe("Element Call", () => {
// Fake a start of a call
await sendRTCState(bot, room.roomId, "ring", "video");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Accept" });
if (skipLobbyToggle) {
const button = toast.getByRole("button", { name: "Join" });
if (joinWithVideo) {
await toast.getByRole("switch").check();
} else {
await toast.getByRole("switch").uncheck();
}
await expect(toast).toMatchScreenshot(
`incoming-call-dm-video-toast-${skipLobbyToggle ? "checked" : "unchecked"}.png`,
`incoming-call-dm-video-toast-${joinWithVideo ? "checked" : "unchecked"}.png`,
{
// Hide UserId
css: `
@ -385,8 +385,8 @@ test.describe("Element Call", () => {
const hash = new URLSearchParams(url.hash.slice(1));
assertCommonCallParameters(url.searchParams, hash, user, room);
expect(hash.get("intent")).toEqual("join_existing_dm");
expect(hash.get("skipLobby")).toEqual(skipLobbyToggle.toString());
expect(hash.get("intent")).toEqual(joinWithVideo ? "join_existing_dm" : "join_existing_dm_voice");
expect(hash.get("skipLobby")).toEqual("true");
},
);
});
@ -400,7 +400,7 @@ test.describe("Element Call", () => {
// Fake a start of a call
await sendRTCState(bot, room.roomId, "ring", "audio");
const toast = page.locator(".mx_Toast_toast");
const button = toast.getByRole("button", { name: "Accept" });
const button = toast.getByRole("button", { name: "Join" });
await expect(toast).toMatchScreenshot(`incoming-call-dm-voice-toast.png`, {
// Hide UserId

View File

@ -21,6 +21,7 @@ test.describe("PSTN", () => {
});
test("should render dialpad as expected", { tag: "@screenshot" }, async ({ page, user, toasts }) => {
await toasts.rejectToast("Verify this device");
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();

View File

@ -107,7 +107,7 @@ export const test = base.extend<TestFixtures>({
},
});
interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
export interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
includeDialogBackground?: boolean;
showTooltips?: boolean;
timeout?: number;

View File

@ -233,26 +233,56 @@ export class ElementAppPage {
* Open the room info panel, and use it to send an invite to the given user.
*
* @param userId - The user to invite to the room.
* @param options - Options object
*/
public async inviteUserToCurrentRoom(userId: string): Promise<void> {
public async inviteUserToCurrentRoom(
userId: string,
options?: {
/** If true, expect and acknowledge "Confirm inviting new users" page */
confirmUnknownUser?: boolean;
},
): Promise<void> {
const rightPanel = await this.openRoomInfoPanel();
await rightPanel.getByRole("menuitem", { name: "Invite" }).click();
const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input");
const dialogLocator = this.page.getByRole("dialog");
const input = dialogLocator.getByTestId("invite-dialog-input");
await input.fill(userId);
await input.press("Enter");
await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click();
await dialogLocator.getByRole("button", { name: "Invite" }).click();
if (options?.confirmUnknownUser) {
await expect(
dialogLocator.getByRole("heading", { name: "Invite new contacts to this room?" }),
).toBeVisible();
await dialogLocator.getByRole("button", { name: "Invite" }).click();
}
}
async closeToast(title: string, button: string): Promise<void> {
await this.page.locator(".mx_Toast_toast", { hasText: title }).getByRole("button", { name: button }).click();
}
/**
* Close the notification toast
* Dismiss the "Notifications" toast.
*/
public closeNotificationToast(): Promise<void> {
// Dismiss "Notification" toast
return this.page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
public async closeNotificationToast(): Promise<void> {
await this.closeToast("Notifications", "Dismiss");
}
/**
* Dismiss the "Turn on key storage" toast.
*/
public async closeKeyStorageToast() {
await this.closeToast("Turn on key storage", "Dismiss");
await this.page.getByRole("button", { name: "Yes, dismiss" }).click();
}
/**
* Dismiss the "Verify this device" toast by clicking "Later".
*/
public async closeVerifyToast() {
await this.closeToast("Verify this device", "Later");
}
/**

View File

@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { type JSHandle, type Page } from "@playwright/test";
import { type PageFunctionOn } from "playwright-core/types/structs";
import { type ElementHandle } from "playwright-core";
import { Network } from "./network";
import type {
@ -30,6 +30,34 @@ import type {
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
import { type CredentialsOptionalAccessToken } from "./bot";
/** Types cribbed from playwright-core/types/structs as they are not importable */
export type NoHandles<Arg> = Arg extends JSHandle
? never
: Arg extends object
? { [Key in keyof Arg]: NoHandles<Arg[Key]> }
: Arg;
export type Unboxed<Arg> =
Arg extends ElementHandle<infer T>
? T
: Arg extends JSHandle<infer T>
? T
: Arg extends NoHandles<Arg>
? Arg
: Arg extends [infer A0]
? [Unboxed<A0>]
: Arg extends [infer A0, infer A1]
? [Unboxed<A0>, Unboxed<A1>]
: Arg extends [infer A0, infer A1, infer A2]
? [Unboxed<A0>, Unboxed<A1>, Unboxed<A2>]
: Arg extends [infer A0, infer A1, infer A2, infer A3]
? [Unboxed<A0>, Unboxed<A1>, Unboxed<A2>, Unboxed<A3>]
: Arg extends Array<infer T>
? Array<Unboxed<T>>
: Arg extends object
? { [Key in keyof Arg]: Unboxed<Arg[Key]> }
: Arg;
export type PageFunctionOn<On, Arg2, R> = string | ((on: On, arg2: Unboxed<Arg2>) => R | Promise<R>);
export class Client {
public network: Network;
protected client: JSHandle<MatrixClient>;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

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