Merge remote-tracking branch 'origin/develop' into hs/enable-profile-updates

This commit is contained in:
David Baker 2026-04-15 17:52:47 +01:00
commit 61cf98b0f4
778 changed files with 22407 additions and 9091 deletions

7
.github/CODEOWNERS vendored
View File

@ -4,12 +4,13 @@
/pnpm-lock.yaml @element-hq/element-web-team
/apps/web/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
/apps/web/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
/apps/web/test/unit-tests/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
/apps/web/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
/apps/web/test/unit-tests/async-components/dialogs/security/ @element-hq/element-crypto-web-reviewers
/apps/web/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
/apps/web/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
/apps/web/test/unit-tests/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
/apps/web/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
/apps/web/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
/apps/web/test/unit-tests/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
/apps/web/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/apps/web/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/apps/web/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers

View File

@ -31,7 +31,9 @@ runs:
- name: Move webapp to out-file-path
shell: bash
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp "$OUT_PATH"
env:
OUT_PATH: ${{ inputs.out-file-path }}
- name: Clean up temp directory
shell: bash

View File

@ -0,0 +1,49 @@
name: Setup playwright
description: Installs playwright browsers and sets up a cache
inputs:
needs-webkit:
description: Whether to install the additional dependencies for webkit
required: false
default: "false"
write-cache:
description: Whether to write the cache back
required: true
runs:
using: composite
steps:
- name: Calculate cache key
id: key
run: |
PW_VERSION=$(pnpm --silent -- playwright --version | awk '{print $2}')
echo "key=${PREFIX}-playwright-${PW_VERSION}" >> $GITHUB_OUTPUT
shell: bash
env:
PREFIX: ${{ runner.os }}-${{ runner.arch }}
- name: Cache playwright binaries
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: inputs.write-cache == 'true'
id: cache
with:
path: ~/.cache/ms-playwright
key: ${{ steps.key.outputs.key }}
# When running in merge queue only restore the cache, never write it
- name: Restore playwright binaries cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
if: inputs.write-cache != 'true'
id: cache-restore
with:
path: ~/.cache/ms-playwright
key: ${{ steps.key.outputs.key }}
- name: Install Playwright browsers
if: (steps.cache.outputs.cache-hit || steps.cache-restore.outputs.cache-hit) != 'true'
shell: bash
run: pnpm playwright install --with-deps
# Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
- name: Install system dependencies for WebKit
if: inputs.needs-webkit == 'true' && (steps.cache.outputs.cache-hit || steps.cache-restore.outputs.cache-hit) == 'true'
shell: bash
run: pnpm playwright install-deps webkit

19
.github/renovate.json vendored
View File

@ -2,6 +2,14 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>matrix-org/renovate-config-element-web"],
"postUpdateOptions": ["pnpmDedupe"],
"packageRules": [
{
"groupName": "testcontainers docker digests",
"groupSlug": "testcontainers-docker",
"matchDepTypes": ["testcontainers-docker"],
"matchPackageNames": ["*"]
}
],
"customManagers": [
{
"customType": "regex",
@ -9,7 +17,16 @@
"versioningTemplate": "loose",
"description": "Update testcontainers docker digests",
"managerFilePatterns": ["**/testcontainers/*.ts"],
"matchStrings": ["\\s+\"(?<depName>[^@]+):(?<currentValue>[^@]+)@(?<currentDigest>sha256:[a-f0-9]+)\""]
"matchStrings": ["\\s+\"(?<depName>[^@]+):(?<currentValue>[^@]+)@(?<currentDigest>sha256:[a-f0-9]+)\""],
"depTypeTemplate": "testcontainers-docker"
},
{
"customType": "jsonata",
"managerFilePatterns": ["/(^|/)package\\.json$/"],
"fileFormat": "json",
"matchStrings": ["hakDependencies.$each(function($v, $k) { { 'packageName': $k, 'currentValue': $v } })"],
"datasourceTemplate": "npm",
"depTypeTemplate": "hak"
}
]
}

View File

@ -5,7 +5,7 @@ on:
# Privilege escalation necessary to publish to Netlify
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["End to End Tests"]
workflows: ["Build & Test"]
types:
- completed

View File

@ -1,7 +1,13 @@
# Produce a build of element-web with this version of react-sdk
# and any matching branches of element-web and js-sdk, output it
# as an artifact and run end-to-end tests.
name: End to End Tests
# builds Element Web
# runs Playwright tests against the built Element Web
# builds Element Desktop using the built Element Web
#
# Tries to use a matching js-sdk branch for the build.
#
# Produces a `webapp` artifact
# Produces multiple Desktop artifacts
# Produces multiple Playwright report artifacts
name: Build & Test
on:
# CRON to run all Projects at 6am UTC
schedule:
@ -10,9 +16,8 @@ on:
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
repository_dispatch:
types: [element-web-notify]
# We do not build on push to develop as the merge_group check handles that
branches: [staging, master]
# support triggering from other workflows
workflow_call:
@ -35,20 +40,22 @@ concurrency:
env:
# fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }}
# Use 6 runners in the default case, but 4 when running on a schedule where we run all 5 projects (20 runners total)
NUM_RUNNERS: ${{ github.event_name == 'schedule' && 4 || 6 }}
# Use 4 runners in the default case, but only 1 when running on a schedule where we run all 5 projects
NUM_RUNNERS: ${{ github.event_name == 'schedule' && 1 || 4 }}
NX_DEFAULT_OUTPUT_STYLE: stream-without-prefixes
permissions: {} # No permissions required
jobs:
build:
name: "Build Element-Web"
build_ew:
name: "Build Element Web"
runs-on: ubuntu-24.04
if: inputs.skip != true
outputs:
num-runners: ${{ env.NUM_RUNNERS }}
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
# Skip pull_request runs on renovate PRs to speed up CI time, delegating to the full run in merge queue
skip: ${{ inputs.skip || (github.event_name == 'pull_request' && startsWith(github.head_ref, 'renovate/')) }}
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@ -79,7 +86,7 @@ jobs:
run: VERSION=$(scripts/get-version-from-git.sh) pnpm run build
- name: Upload Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: webapp
path: apps/web/webapp
@ -87,17 +94,17 @@ jobs:
- name: Calculate runner variables
id: runner-vars
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
const matrix = Array.from({ length: numRunners }, (_, i) => i + 1);
core.setOutput("matrix", JSON.stringify(matrix));
playwright:
name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}"
needs: build
if: inputs.skip != true
playwright_ew:
name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build_ew.outputs.num-runners }}"
needs: build_ew
if: needs.build_ew.outputs.skip == 'false'
runs-on: ubuntu-24.04
permissions:
actions: read
@ -107,7 +114,7 @@ jobs:
fail-fast: false
matrix:
# Run multiple instances in parallel to speed up the tests
runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }}
runner: ${{ fromJSON(needs.build_ew.outputs.runners-matrix) }}
project:
- Chrome
- Firefox
@ -148,105 +155,147 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Get installed Playwright version
id: playwright
run: echo "version=$(pnpm --silent -- playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
id: playwright-cache
- name: Setup playwright
uses: ./.github/actions/setup-playwright
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: apps/web
run: pnpm playwright install --with-deps --no-shell
- name: Install system dependencies for WebKit
# Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
if: matrix.project == 'WebKit' && steps.playwright-cache.outputs.cache-hit == 'true'
working-directory: apps/web
run: pnpm playwright install-deps webkit
needs-webkit: ${{ matrix.project == 'WebKit' }}
write-cache: ${{ github.event_name != 'merge_group' }}
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
- name: Run Playwright tests
working-directory: apps/web
run: |
pnpm playwright test \
pnpm test:playwright \
--shard "$SHARD" \
--project="${{ matrix.project }}" \
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
env:
SHARD: ${{ format('{0}/{1}', matrix.runner, needs.build.outputs.num-runners) }}
SHARD: ${{ format('{0}/{1}', matrix.runner, needs.build_ew.outputs.num-runners) }}
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
name: blob-report-${{ matrix.project }}-${{ matrix.runner }}
path: apps/web/blob-report
retention-days: 1
if-no-files-found: error
downstream-modules:
name: Downstream Playwright tests [element-modules]
needs: build
if: inputs.skip != true && github.event_name == 'merge_group'
needs: build_ew
if: needs.build_ew.outputs.skip == 'false' && github.event_name == 'merge_group'
uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main # zizmor: ignore[unpinned-uses]
with:
webapp-artifact: webapp
reporter: blob
prepare_ed:
name: "Prepare Element Desktop"
uses: ./.github/workflows/build_desktop_prepare.yaml
needs: build_ew
if: needs.build_ew.outputs.skip == 'false'
permissions:
contents: read
with:
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ case((github.event.pull_request.base.ref || github.ref_name) == 'develop' || github.event_name == 'merge_group', 'develop', '') }}
webapp-artifact: webapp
build_ed_windows:
needs: prepare_ed
name: "Desktop Windows"
uses: ./.github/workflows/build_desktop_windows.yaml
strategy:
matrix:
arch: [x64, ia32, arm64]
with:
arch: ${{ matrix.arch }}
blob_report: true
build_ed_linux:
needs: prepare_ed
name: "Desktop Linux"
uses: ./.github/workflows/build_desktop_linux.yaml
strategy:
matrix:
sqlcipher: [system, static]
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:
- runAllTests: false
sqlcipher: system
with:
sqlcipher: ${{ matrix.sqlcipher }}
arch: ${{ matrix.arch }}
blob_report: true
build_ed_macos:
needs: prepare_ed
name: "Desktop macOS"
uses: ./.github/workflows/build_desktop_macos.yaml
with:
blob_report: true
complete:
name: end-to-end-tests
needs:
- playwright
- build_ew
- playwright_ew
- downstream-modules
- prepare_ed
- build_ed_windows
- build_ed_linux
- build_ed_macos
if: always()
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
if: inputs.skip != true
if: needs.build_ew.outputs.skip == 'false'
with:
persist-credentials: false
repository: element-hq/element-web
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
if: inputs.skip != true
if: needs.build_ew.outputs.skip == 'false'
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
if: inputs.skip != true
if: needs.build_ew.outputs.skip == 'false'
with:
cache: "pnpm"
node-version: "lts/*"
- name: Install dependencies
if: inputs.skip != true
if: needs.build_ew.outputs.skip == 'false'
run: pnpm install --frozen-lockfile
- name: Download blob reports from GitHub Actions Artifacts
if: inputs.skip != true
if: needs.build_ew.outputs.skip == 'false'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: all-blob-reports-*
path: apps/web/all-blob-reports
pattern: blob-report-*
path: all-blob-reports
merge-multiple: true
- name: Merge into HTML Report
if: inputs.skip != true
working-directory: apps/web
run: pnpm playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
if: needs.build_ew.outputs.skip == 'false'
run: |
pnpm playwright merge-reports \
--config=playwright-merge.config.ts \
./all-blob-reports
env:
# Only pass creds to the flaky-reporter on main branch runs
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
PLAYWRIGHT_HTML_TITLE: ${{ case(github.event_name == 'pull_request', format('EW Playwright Report PR-{0}', env.PR_NUMBER), 'EW Playwright Report') }}
PLAYWRIGHT_HTML_TITLE: ${{ case(github.event_name == 'pull_request', format('Playwright Report PR-{0}', env.PR_NUMBER), 'Playwright Report') }}
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
- name: Upload HTML report
if: always() && inputs.skip != true
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
if: always() && needs.build_ew.outputs.skip == 'false'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: html-report
path: apps/web/playwright-report
path: playwright-report
retention-days: 14
if-no-files-found: error

View File

@ -69,7 +69,7 @@ jobs:
run: VERSION=$(scripts/get-version-from-git.sh) pnpm run build
- name: Upload Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: webapp-${{ matrix.image }}
path: apps/web/webapp

View File

@ -69,7 +69,7 @@ jobs:
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: element-web.deb
path: apps/web/element-web.deb

View File

@ -212,7 +212,7 @@ jobs:
- name: Stash packages.element.io
if: needs.prepare.outputs.deploy == 'false'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: packages.element.io
path: packages.element.io
@ -250,7 +250,7 @@ jobs:
- name: Stash debs
if: needs.prepare.outputs.deploy == 'false' && needs.linux.result == 'success'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: debs
path: |
@ -289,7 +289,7 @@ jobs:
id-token: write # This is required for requesting the JWT
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6
uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6
with:
role-to-assume: arn:aws:iam::264135176173:role/Push-ElementDesktop-MSI
role-session-name: githubaction-run-${{ github.run_id }}

View File

@ -1,89 +0,0 @@
name: Build and Test
on:
pull_request: {}
push:
branches: [develop, staging, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {} # No permissions required
jobs:
fetch:
uses: ./.github/workflows/build_desktop_prepare.yaml
permissions:
contents: read
with:
config: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'element.io/nightly' || 'element.io/release' }}
version: ${{ (github.event.pull_request.base.ref || github.ref_name) == 'develop' && 'develop' || '' }}
branch-matching: true
windows:
needs: fetch
name: Windows
uses: ./.github/workflows/build_desktop_windows.yaml
strategy:
matrix:
arch: [x64, ia32, arm64]
with:
arch: ${{ matrix.arch }}
blob_report: true
linux:
needs: fetch
name: "Linux (${{ matrix.arch }}) (sqlcipher: ${{ matrix.sqlcipher }})"
uses: ./.github/workflows/build_desktop_linux.yaml
strategy:
matrix:
sqlcipher: [system, static]
arch: [amd64, arm64]
with:
sqlcipher: ${{ matrix.sqlcipher }}
arch: ${{ matrix.arch }}
blob_report: true
macos:
needs: fetch
name: macOS
uses: ./.github/workflows/build_desktop_macos.yaml
with:
blob_report: true
tests-done:
needs: [windows, linux, macos]
runs-on: ubuntu-24.04
if: ${{ !cancelled() }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
cache: "pnpm"
node-version: "lts/*"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: blob-report-*
path: apps/desktop/all-blob-reports
merge-multiple: true
- name: Merge into HTML Report
working-directory: apps/desktop
run: pnpm playwright merge-reports -c ./playwright.config.ts --reporter=html ./all-blob-reports
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: html-report
path: apps/desktop/playwright-report
retention-days: 14
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1

View File

@ -28,7 +28,7 @@ on:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
@ -38,7 +38,7 @@ on:
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
default: "desktop-prepare"
test:
type: boolean
required: false
@ -73,20 +73,8 @@ jobs:
# https://github.com/matrix-org/seshat/issues/135
runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }}
env:
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-web/desktop-build-env
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-web/desktop-build-env:${{ case(github.event_name == 'push', inputs.ref || github.ref_name, github.event_name == 'release', 'staging', 'develop') }}
steps:
- name: Resolve docker image tag for push
if: github.event_name == 'push'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:$REF" >> $GITHUB_ENV
env:
REF: ${{ inputs.ref || github.ref_name }}
- name: Resolve docker image tag for release
if: github.event_name == 'release'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:staging" >> $GITHUB_ENV
- name: Resolve docker image tag for other triggers
if: github.event_name != 'push' && github.event_name != 'release'
run: echo "HAK_DOCKER_IMAGE=$HAK_DOCKER_IMAGE:develop" >> $GITHUB_ENV
- uses: nbucic/variable-mapper@0673f6891a0619ba7c002ecfed0f9f4f39017b6f
id: config
with:
@ -95,11 +83,9 @@ jobs:
map: |
{
"amd64": {
"target": "x86_64-unknown-linux-gnu",
"arch": "x86-64"
},
"arm64": {
"target": "aarch64-unknown-linux-gnu",
"arch": "aarch64",
"build-args": "--arm64"
}
@ -118,9 +104,9 @@ jobs:
- name: Cache .hak
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion', 'dockerbuild/*') }}
key: ${{ runner.os }}-${{ github.ref_name }}-${{ inputs.sqlcipher }}-${{ inputs.arch }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion', 'apps/desktop/dockerbuild/*') }}
path: |
apps/desktop/.hak
@ -135,7 +121,7 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: pnpm install --frozen-lockfile
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: "Get modified files"
id: changed_files
@ -147,7 +133,7 @@ jobs:
# This allows contributors to test changes to the dockerbuild image within a pull request
- name: Build docker image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
if: steps.changed_files.outputs.any_modified == 'true'
with:
file: apps/desktop/dockerbuild/Dockerfile
@ -230,7 +216,7 @@ jobs:
# We exclude *-unpacked as it loses permissions and the tarball contains it with correct permissions
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
path: |

View File

@ -37,7 +37,7 @@ on:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
@ -46,7 +46,7 @@ on:
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
default: "desktop-prepare"
test:
type: boolean
required: false
@ -90,9 +90,9 @@ jobs:
- name: Cache .hak
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
key: ${{ runner.os }}-${{ hashFiles('hakHash', 'electronVersion') }}
key: ${{ runner.os }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }}
path: |
apps/desktop/.hak
@ -121,28 +121,29 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Build Natives
if: steps.cache.outputs.cache-hit != 'true'
working-directory: apps/desktop
run: pnpm run build:native:universal
- name: "Build App"
# We split these because electron-builder gets upset if we set CSC_LINK even to an empty string
- name: "[Signed] Build App"
if: inputs.sign != ''
working-directory: apps/desktop
run: pnpm run build:universal --publish never -m ${TARGETS}
run: |
pnpm run build:universal --publish never -m ${TARGETS}
env:
# Code signing parameters
CSC_IDENTITY_AUTO_DISCOVERY: ${{ inputs.sign != '' }}
APPLE_TEAM_ID: ${{ case(inputs.sign != '', vars.APPLE_TEAM_ID, '') }}
APPLE_ID: ${{ case(inputs.sign != '', secrets.APPLE_ID, '') }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ case(inputs.sign != '', secrets.APPLE_ID_PASSWORD, '') }}
CSC_KEY_PASSWORD: ${{ case(inputs.sign != '', secrets.APPLE_CSC_KEY_PASSWORD, '') }}
CSC_LINK: ${{ case(inputs.sign != '', secrets.APPLE_CSC_LINK, '') }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CSC_KEY_PASSWORD }}
CSC_LINK: ${{ secrets.APPLE_CSC_LINK }}
VARIANT_PATH: variant.json
TARGETS: ${{ inputs.targets }}
# Only set for Nightly builds
VERSION: ${{ inputs.version }}
TARGETS: ${{ inputs.targets }}
- name: Check app was signed & notarised successfully
if: inputs.sign != ''
@ -153,6 +154,16 @@ jobs:
spctl -a -vvv -t install /Volumes/Element/*.app
hdiutil detach /Volumes/Element
- name: "[Unsigned] Build App"
if: inputs.sign == ''
working-directory: apps/desktop
run: |
pnpm run build:universal --publish never -m ${TARGETS}
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
VARIANT_PATH: variant.json
TARGETS: ${{ inputs.targets }}
- name: Generate releases.json
if: inputs.base-url
working-directory: apps/desktop
@ -183,7 +194,7 @@ jobs:
# We exclude mac-universal as the unpacked app takes forever to upload and zip and dmg already contains it
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.artifact-prefix }}macos
path: |

View File

@ -20,11 +20,10 @@ on:
required: false
default: false
description: "Whether the build should be deployed to production"
branch-matching:
type: boolean
webapp-artifact:
type: string
required: false
default: false
description: "Whether the branch name should be matched to find the element-web commit"
description: "Name of the webapp artifact that should be used, will fetch a relevant build if omitted"
secrets:
# Required if `nightly` is set
CF_R2_ACCESS_KEY_ID:
@ -57,6 +56,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
repository: element-hq/element-web
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
@ -66,28 +66,26 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Fetch Element Web (matching branch)
id: branch-matching
if: inputs.branch-matching
- name: Fetch Element Web (from artifact)
if: inputs.webapp-artifact != ''
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ inputs.webapp-artifact }}
path: apps/desktop/webapp
- name: Build webapp.asar (from artifact)
if: inputs.webapp-artifact != ''
working-directory: apps/desktop
continue-on-error: true
run: |
scripts/branch-match.sh
cp "$CONFIG_DIR/config.json" element-web/
pnpm --cwd element-web install --frozen-lockfile
pnpm --cwd element-web run build
mv element-web/webapp .
cp -f "$CONFIG_DIR/config.json" webapp/config.json
pnpm run asar-webapp
env:
# These must be set for branch-match.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
CONFIG_DIR: ${{ inputs.config }}
- name: Fetch Element Web (${{ inputs.version }})
if: steps.branch-matching.outcome == 'failure' || steps.branch-matching.outcome == 'skipped'
if: inputs.webapp-artifact == ''
working-directory: apps/desktop
run: pnpm run fetch --noverify -d ${CONFIG} ${VERSION}
env:
@ -187,9 +185,9 @@ jobs:
env:
NIGHTLY_VERSION: ${{ steps.versions.outputs.nightly }}
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: webapp
name: desktop-prepare
retention-days: 1
path: |
apps/desktop/webapp.asar

View File

@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: ${{ github.repository == 'element-hq/element-web-pro' && 'element-hq/element-web' || github.repository }}
repository: element-hq/element-web
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
@ -48,8 +48,7 @@ jobs:
cache: "pnpm"
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
@ -85,25 +84,34 @@ jobs:
EXECUTABLE: ${{ steps.executable.outputs.path }}
- name: Run tests
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
timeout-minutes: 20
with:
run: pnpm -C apps/desktop test --project=${{ inputs.project }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }} ${{ inputs.args }}
shell: bash
working-directory: apps/desktop
run: |
$PREFIX pnpm playwright test \
${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} \
${{ inputs.blob_report == false && '--reporter=html' || '' }} \
$ARGS
env:
PREFIX: ${{ runner.os == 'Linux' && 'xvfb-run' || '' }}
PW_TAG: ${{ inputs.project }}
ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }}
ARGS: ${{ inputs.args }}
- name: Upload blob report
if: always() && inputs.blob_report
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: blob-report-${{ inputs.artifact }}
path: apps/desktop/blob-report
retention-days: 1
if-no-files-found: error
- name: Upload HTML report
if: always() && inputs.blob_report == false
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.artifact }}-test
path: apps/desktop/playwright-report
retention-days: 14
if-no-files-found: error

View File

@ -42,7 +42,7 @@ on:
type: string
required: false
description: |
The name of the prepare artifact to use, defaults to 'webapp'.
The name of the prepare artifact to use, defaults to 'desktop-prepare'.
The artifact must contain the following:
+ webapp.asar - the asar archive of the webapp to embed in the desktop app
+ electronVersion - the version of electron to use for cache keying
@ -52,7 +52,7 @@ on:
The artifact can also contain any additional files which will be applied as overrides to the checkout root before building,
for example icons in the `build/` directory to override the app icons.
default: "webapp"
default: "desktop-prepare"
test:
type: boolean
required: false
@ -121,9 +121,9 @@ jobs:
- name: Cache .hak
id: cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('hakHash', 'electronVersion') }}
key: ${{ runner.os }}-${{ inputs.arch }}-${{ hashFiles('apps/desktop/hakHash', 'apps/desktop/electronVersion') }}
path: |
apps/desktop/.hak
@ -160,7 +160,7 @@ jobs:
- name: Install Deps
working-directory: apps/desktop
run: "pnpm install --frozen-lockfile"
run: "pnpm install --frozen-lockfile --filter element-desktop"
- name: Insert config snippet
if: steps.config.outputs.extra_config != ''
@ -274,7 +274,7 @@ jobs:
| ForEach-Object -Process {. $env:SIGNTOOL_PATH verify /pa $_.FullName; if(!$?) { throw }}
- name: Upload Artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }}
path: |

View File

@ -60,7 +60,7 @@ jobs:
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
working-directory: apps/web
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: webapp
path: apps/web/dist/develop.tar.gz
@ -111,7 +111,7 @@ jobs:
running-workflow-name: "Build & Deploy develop.element.io"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload).)*$
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages|Upload|Netlify).)*$
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
# as the expires after 24h and requires auth to download.

View File

@ -2,6 +2,13 @@ name: CD # Continuous Delivery
on:
push:
branches: [master, staging, develop]
paths:
- "**/Dockerfile"
- "**/dockerbuild"
- "**/docker"
- "**/docker-*"
- "pnpm-lock.yaml"
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
permissions: {}
@ -43,7 +50,7 @@ jobs:
run: "pnpm install --frozen-lockfile"
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@ -39,7 +39,7 @@ jobs:
- name: Build and load
id: test-build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
context: .
file: apps/web/Dockerfile
@ -97,14 +97,14 @@ jobs:
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
- name: Login to Docker Hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@ -140,7 +140,7 @@ jobs:
services/web-repositories/secret/data/oci.element.io password | OCI_PASSWORD ;
- name: Login to oci.element.io Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
if: github.event_name != 'pull_request'
with:
registry: oci-push.vpn.infra.element.io
@ -149,7 +149,7 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
if: github.event_name != 'pull_request'
with:
context: .

View File

@ -36,7 +36,7 @@ jobs:
run: pnpm run docs:build
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: ./docs/.vitepress/dist

View File

@ -10,7 +10,7 @@ jobs:
name: Tidy closed issues
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
id: main
with:
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
@ -142,7 +142,7 @@ jobs:
});
}
}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
name: Close duplicate as Not Planned
if: steps.main.outputs.closeAsNotPlanned
with:

29
.github/workflows/merge-queue.yaml vendored Normal file
View File

@ -0,0 +1,29 @@
# Tweaks the behaviour of Merge Queue to skip certain checks
name: Merge Queue tweaks
on:
merge_group:
types: [checks_requested]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
permissions: {}
jobs:
run:
runs-on: ubuntu-24.04
permissions:
statuses: write
steps:
# This is only needed as license/cla at time of writing seems to be extraordinarily flaky
# and Github doesn't support conditional checks between PR & merge queue.
# This is fine to do as a PR won't make it to merge queue until it has license/cla passing.
- name: Skip license/cla on merge queues
uses: guibranco/github-status-action-v2@9bfa8773cdbdc6c185747fd43cd7faa9d7c32f09
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
context: license/cla
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}

View File

@ -1,6 +1,16 @@
name: Publish shared component npm package
name: Publish npm package
run-name: Publish ${{ inputs.package }}
on:
workflow_dispatch: {}
workflow_dispatch:
inputs:
package:
description: Which package to release
required: true
type: choice
options:
- playwright-common
- shared-components
- module-api
concurrency: release
jobs:
@ -29,10 +39,9 @@ jobs:
- name: Update npm
run: npm install -g npm@latest
# Need to setup element web too as it needs the translations
- name: 🛠️ Setup EW
- name: 🛠️ Install dependencies
run: pnpm install --frozen-lockfile
- name: 🚀 Publish to npm
working-directory: packages/shared-components
working-directory: packages/${{ inputs.package }}
run: npm publish --access public --provenance

View File

@ -8,7 +8,7 @@ jobs:
name: Check PR base branch
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const baseBranch = context.payload.pull_request.base.ref;

View File

@ -30,7 +30,8 @@ jobs:
asset-path: dist/*.tar.gz
expected-asset-count: 3
# Desktop has no dist script so we only target web here
dir: apps/web
dist-dir: apps/web
version-dirs: apps/web apps/desktop
check:
name: Post release checks

View File

@ -31,7 +31,7 @@ jobs:
working-directory: packages/shared-components
run: pnpm build:storybook
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: shared-components-storybook
path: packages/shared-components/storybook-static

View File

@ -2,7 +2,9 @@
# It uploads the received images and diffs to netlify, printing the URLs to the console
name: Upload Shared Component Visual Test Diffs
on:
workflow_run:
# Privilege escalation necessary to deploy to Netlify
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Shared Component Visual Tests"]
types:
- completed

View File

@ -36,22 +36,10 @@ jobs:
working-directory: packages/shared-components
run: pnpm install --frozen-lockfile
- name: Get installed Playwright version
working-directory: packages/shared-components
id: playwright
run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
id: playwright-cache
- name: Setup playwright
uses: ./.github/actions/setup-playwright
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
- name: Install Playwright browsers
working-directory: packages/shared-components
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "pnpm playwright install --with-deps --only-shell"
write-cache: ${{ github.event_name != 'merge_group' }}
- name: Run Visual tests
working-directory: packages/shared-components
@ -65,7 +53,7 @@ jobs:
- name: Upload received images & diffs
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: received-images
path: packages/shared-components/__vis__/linux

View File

@ -1,6 +1,8 @@
name: SonarQube
on:
workflow_run:
# Privilege escalation necessary to call upon SonarCloud
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Tests"]
types:
- completed

View File

@ -5,8 +5,6 @@ on:
branches: [develop, master]
merge_group:
types: [checks_requested]
repository_dispatch:
types: [element-web-notify]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
@ -89,7 +87,7 @@ jobs:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
i18n:
strategy:
@ -127,7 +125,9 @@ jobs:
# Dummy job to simplify branch protections
ci:
name: Static Analysis
needs: [lint, i18n]
needs: [lint, i18n, zizmor]
if: always()
runs-on: ubuntu-24.04
steps:
- run: echo "Ok"
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1

View File

@ -5,8 +5,6 @@ on:
types: [checks_requested]
push:
branches: [develop, master]
repository_dispatch:
types: [element-web-notify]
workflow_call:
inputs:
disable_coverage:
@ -58,7 +56,7 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- name: Jest Cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/pnpm-lock.yaml') }}
@ -93,7 +91,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-${{ matrix.runner }}
path: |
@ -102,7 +100,7 @@ jobs:
complete:
name: jest-tests
needs: [jest_ew, vitest_sc]
needs: [jest_ew, vitest]
if: always()
runs-on: ubuntu-24.04
permissions:
@ -122,8 +120,13 @@ jobs:
sha: ${{ github.sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
vitest_sc:
name: Vitest (Shared Components)
vitest:
name: Vitest
strategy:
matrix:
package:
- shared-components
- module-api
runs-on: ubuntu-24.04
steps:
- name: Checkout code
@ -139,44 +142,32 @@ jobs:
node-version: "lts/*"
cache: "pnpm"
- name: Install Shared Component Deps
working-directory: "packages/shared-components"
- name: Install Deps
run: "pnpm install"
- name: Cache storybook & vitest
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: |
packages/shared-components/node_modules/.cache
packages/shared-components/node_modules/.vite/vitest
packages/${{ matrix.package }}/node_modules/.cache
packages/${{ matrix.package }}/node_modules/.vite/vitest
key: ${{ hashFiles('pnpm-lock.yaml') }}
- name: Get installed Playwright version
working-directory: packages/shared-components
id: playwright
run: echo "version=$(pnpm list @playwright/test --depth=0 --json | jq -r '.[].devDependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
id: playwright-cache
- name: Setup playwright
uses: ./.github/actions/setup-playwright
if: matrix.package == 'shared-components'
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
- name: Install Playwright browsers
working-directory: packages/shared-components
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "pnpm playwright install --with-deps --only-shell"
write-cache: ${{ github.event_name != 'merge_group' }}
- name: Run tests
working-directory: "packages/shared-components"
working-directory: "packages/${{ matrix.package }}"
run: pnpm test:unit --coverage=$ENABLE_COVERAGE
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-sharedcomponents
name: coverage-${{ matrix.package }}
path: |
packages/shared-components/coverage
!packages/shared-components/coverage/lcov-report
packages/${{ matrix.package }}/coverage
!packages/${{ matrix.package }}/coverage/lcov-report

View File

@ -27,7 +27,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.addLabels({
@ -44,7 +44,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'good first issue') ||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.addLabels({

View File

@ -43,7 +43,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
contains(github.event.issue.labels.*.name, 'Z-Labs')
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.removeLabel({

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-24.04
environment: Matrix
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}

View File

@ -14,7 +14,8 @@ webpack-stats.json
.vscode/
.env
coverage
# Auto-generated file
# Auto-generated files
*.api.md
/apps/web/src/modules.ts
/apps/web/src/modules.js
src/i18n/strings

View File

@ -1,3 +1,32 @@
Changes in [1.12.15](https://github.com/element-hq/element-web/releases/tag/v1.12.15) (2026-04-08)
==================================================================================================
Fixes Desktop release workflow.
This release is identical to v1.12.14 otherwise.
Changes in [1.12.14](https://github.com/element-hq/element-web/releases/tag/v1.12.14) (2026-04-07)
==================================================================================================
## ✨ Features
* Add analytics tracking for URL previews ([#32659](https://github.com/element-hq/element-web/pull/32659)). Contributed by @Half-Shot.
* Collapsible Room List - Clicking on separator should expand to last set width ([#32909](https://github.com/element-hq/element-web/pull/32909)). Contributed by @MidhunSureshR.
* RoomList: improve performance ([#32919](https://github.com/element-hq/element-web/pull/32919)). Contributed by @florianduros.
* Implement collapsible panels for the new room list ([#32742](https://github.com/element-hq/element-web/pull/32742)). Contributed by @MidhunSureshR.
* Hide the names of banned users behind a spoiler tag (attempt 2) ([#32636](https://github.com/element-hq/element-web/pull/32636)). Contributed by @andybalaam.
## 🐛 Bug Fixes
* Use the code signing Subject Name as basis for Tray GUID on Windows ([#32939](https://github.com/element-hq/element-web/pull/32939)). Contributed by @t3chguy.
* Ensure the incoming verification request appears above the please verify prompt ([#32931](https://github.com/element-hq/element-web/pull/32931)). Contributed by @andybalaam.
* Collapsible Room List - Prevent any interaction with the separator when the panel is expanded ([#32910](https://github.com/element-hq/element-web/pull/32910)). Contributed by @MidhunSureshR.
* Fix icon size of badges in right panel ([#32952](https://github.com/element-hq/element-web/pull/32952)). Contributed by @florianduros.
* Fix room list often showing the wrong icons for calls ([#32881](https://github.com/element-hq/element-web/pull/32881)). Contributed by @robintown.
* Fix emoticon slash commands including stale buffers ([#32928](https://github.com/element-hq/element-web/pull/32928)). Contributed by @t3chguy.
* Fix presence indicators not showing without cache ([#32880](https://github.com/element-hq/element-web/pull/32880)). Contributed by @DLCSharp.
* Show space name instead of 'Empty room' after creation ([#32886](https://github.com/element-hq/element-web/pull/32886)). Contributed by @gugaribeiro05.
* Strip ephemeral query params from OIDC redirect URI ([#32875](https://github.com/element-hq/element-web/pull/32875)). Contributed by @azmeuk.
Changes in [1.12.13](https://github.com/element-hq/element-web/releases/tag/v1.12.13) (2026-03-24)
==================================================================================================
## 🦖 Deprecations

View File

@ -1 +1 @@
24.14.0
24.14.1

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:16950191527a4cb9e0762d9d48b705a6315158e4035e64f7a93ce8656a1b053c
FROM rust:bullseye@sha256:bc19574c121fe10c1bc68fc2b1ea9b420d87d047a0c50fb1622b282199700cee
ENV DEBIAN_FRONTEND=noninteractive

View File

@ -14,10 +14,7 @@ import type { Tool } from "../../scripts/hak/hakEnv.ts";
import type { DependencyInfo } from "../../scripts/hak/dep.ts";
export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Promise<void> {
const tools: Tool[] = [
["rustc", "--version"],
["python", "--version"], // node-gyp uses python for reasons beyond comprehension
];
const tools: Tool[] = [["rustc", "--version"]];
if (hakEnv.isWin()) {
tools.push(["perl", "--version"]); // for openssl configure
tools.push(["nasm", "-v"]); // for openssl building
@ -28,6 +25,14 @@ export default async function (hakEnv: HakEnv, moduleInfo: DependencyInfo): Prom
}
await hakEnv.checkTools(tools);
try {
// node-gyp uses python for reasons beyond comprehension
// Try python3 first, to get a more sensible error if python is not found in the fallback
await hakEnv.checkTools([["python3", "--version"]]);
} catch {
await hakEnv.checkTools([["python", "--version"]]);
}
// Ensure Rust target exists (nb. we avoid depending on rustup)
await new Promise((resolve, reject) => {
const rustc = childProcess.execFile(

View File

@ -3,7 +3,7 @@
"productName": "Element",
"main": "lib/electron-main.js",
"exports": "./lib/electron-main.js",
"version": "1.12.13",
"version": "1.12.15",
"description": "Element: the future of secure communication",
"author": {
"name": "Element",
@ -71,7 +71,7 @@
"@babel/core": "^7.18.10",
"@babel/preset-env": "^7.18.10",
"@babel/preset-typescript": "^7.18.6",
"@electron/asar": "4.1.0",
"@electron/asar": "4.1.2",
"@playwright/test": "catalog:",
"@stylistic/eslint-plugin": "^5.0.0",
"@types/auto-launch": "^5.0.1",
@ -84,7 +84,7 @@
"app-builder-lib": "26.8.2",
"chokidar": "^5.0.0",
"detect-libc": "^2.0.0",
"electron": "41.0.3",
"electron": "41.1.0",
"electron-builder": "26.8.2",
"electron-builder-squirrel-windows": "26.8.2",
"electron-devtools-installer": "^4.0.0",
@ -105,7 +105,7 @@
"typescript": "5.9.3"
},
"hakDependencies": {
"matrix-seshat": "^4.0.1"
"matrix-seshat": "4.0.1"
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be"
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}

View File

@ -8,25 +8,9 @@ Please see LICENSE files in the repository root for full details.
import { defineConfig } from "@playwright/test";
const projects = [
"macos",
"win-x64",
"win-ia32",
"win-arm64",
"linux-amd64-sqlcipher-system",
"linux-amd64-sqlcipher-static",
"linux-arm64-sqlcipher-system",
"linux-arm64-sqlcipher-static",
];
export default defineConfig({
// Allows the GitHub action to specify a project name (OS + arch) for the combined report to make sense
// workaround for https://github.com/microsoft/playwright/issues/33521
projects: process.env.CI
? projects.map((name) => ({
name,
}))
: undefined,
projects: [{ name: "Desktop" }],
tag: process.env.PW_TAG ? `@${process.env.PW_TAG}` : undefined,
use: {
viewport: { width: 1280, height: 720 },
video: "retain-on-failure",

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.58.2-jammy@sha256:4698a73749c5848d3f5fcd42a2174d172fcad2b2283e087843b115424303a565
FROM mcr.microsoft.com/playwright:v1.59.1-jammy@sha256:8a0360d39d1973be506dd59002904a774f6d697d4946c94063b3fd006461c8ff
WORKDIR /work/element-desktop

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,6 +1,7 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "app",
"projectType": "application",
"implicitDependencies": ["element-web"],
"root": "apps/desktop",
"targets": {
"docker:build": {

View File

@ -1,48 +0,0 @@
#!/bin/bash
# Script for downloading a branch of element-web matching the branch a PR is contributed from
set -x
deforg="element-hq"
defrepo="element-web"
# The PR_NUMBER variable must be set explicitly.
default_org_repo=${GITHUB_REPOSITORY:-"$deforg/$defrepo"}
PR_ORG=${PR_ORG:-${default_org_repo%%/*}}
PR_REPO=${PR_REPO:-${default_org_repo##*/}}
# A function that clones a branch of a repo based on the org, repo and branch
clone() {
org=$1
repo=$2
branch=$3
if [ -n "$branch" ]
then
echo "Trying to use $org/$repo#$branch"
# Disable auth prompts: https://serverfault.com/a/665959
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch "$branch" --depth 1 && exit 0
fi
}
echo "Getting info about a PR with number $PR_NUMBER"
apiEndpoint="https://api.github.com/repos/$PR_ORG/$PR_REPO/pulls/$PR_NUMBER"
head=$(curl "$apiEndpoint" | jq -r '.head.label')
# for forks, $head will be in the format "fork:branch", so we split it by ":"
# into an array. On non-forks, this has the effect of splitting into a single
# element array given ":" shouldn't appear in the head - it'll just be the
# branch name. Based on the results, we clone.
BRANCH_ARRAY=(${head//:/ })
TRY_ORG=$deforg
TRY_BRANCH=${BRANCH_ARRAY[0]}
if [[ "$head" == *":"* ]]; then
# ... but only match that fork if it's a real fork
if [ "${BRANCH_ARRAY[0]}" != "$PR_ORG" ]; then
TRY_ORG=${BRANCH_ARRAY[0]}
fi
TRY_BRANCH=${BRANCH_ARRAY[1]}
fi
clone "$TRY_ORG" "$defrepo" "$TRY_BRANCH"
exit 1

View File

@ -103,6 +103,9 @@ export default class HakEnv {
shell: this.isWin(),
...options,
});
proc.on("error", (err) => {
reject(err);
});
proc.on("exit", (code) => {
if (code) {
reject(code);

View File

@ -56,6 +56,7 @@ module.exports = {
{ from: "res/css/views/rooms/_EditMessageComposer.pcss", type: "css" },
{ from: "res/css/views/right_panel/_BaseCard.pcss", type: "css" },
{ from: "res/css/views/messages/_MessageActionBar.pcss", type: "css" },
{ from: "res/css/views/messages/_ThreadActionBar.pcss", type: "css" },
{ from: "res/css/views/voip/LegacyCallView/_LegacyCallViewButtons.pcss", type: "css" },
{ from: "res/css/views/elements/_ToggleSwitch.pcss", type: "css" },
{ from: "res/css/views/settings/tabs/_SettingsTab.pcss", type: "css" },

View File

@ -2,7 +2,7 @@
# Context must be the root of the monorepo
# Builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:4bfbd78e049926e4ca595c1798810691ca7bb5aedd829ffd8a78b2ab30689810 AS builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:27e462f5db2402700867dfa8ec35e3a68b127fdf61b505db0dd6ab98c38284bb 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:4011c42f28e9b54c86b52211598dbc6bcaa520311ddd55f211587cdd71f88a9c
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:b5831ee7f7aa827cbae87df4a30a642f62c747d8525f5674365389f3adab278d
# Need root user to install packages & manipulate the usr directory
USER root

View File

@ -10,6 +10,7 @@
**/.pnpm-store
**/tsconfig.node.tsbuildinfo
**/*.md
!**/*.api.md
**/*.rst
.idea/

View File

@ -46,7 +46,7 @@ const config: Config = {
"@vector-im/compound-web": "<rootDir>/../../node_modules/@vector-im/compound-web",
},
transformIgnorePatterns: [
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp|matrix-web-i18n|await-lock|@element-hq/web-shared-components|react-virtuoso|lodash)).+$",
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp|matrix-web-i18n|await-lock|@element-hq/web-shared-components|react-virtuoso|lodash|domutils|domhandler|domelementtype|dom-serializer|entities)).+$",
],
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}",

View File

@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.12.13",
"version": "1.12.15",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@ -30,15 +30,15 @@
"lint:types": "nx lint:types",
"lint:style": "stylelint \"res/css/**/*.pcss\"",
"test": "nx test:unit",
"test:playwright": "playwright test",
"test:playwright:open": "pnpm test:playwright --ui",
"test:playwright:screenshots": "playwright-screenshots-experimental pnpm playwright test --update-snapshots --project=Chrome --grep @screenshot",
"test:playwright": "nx test:playwright --",
"test:playwright:open": "nx test:playwright -- --ui",
"test:playwright:screenshots": "nx test:playwright:screenshots --",
"coverage": "pnpm test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "catalog:",
"@element-hq/element-web-module-api": "workspace:*",
"@element-hq/web-shared-components": "workspace:*",
"@fontsource/fira-code": "^5",
"@fontsource/inter": "catalog:",
@ -63,11 +63,11 @@
"css-tree": "^3.0.0",
"diff-dom": "^5.0.0",
"diff-match-patch": "^1.0.5",
"domutils": "^3.2.2",
"domutils": "^4.0.0",
"emojibase-regex": "^17.0.0",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "11.0.13",
"filesize": "11.0.15",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
@ -78,7 +78,7 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"lodash": "npm:lodash-es@^4.17.21",
"lodash": "npm:lodash-es@4.18.1",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#hs/profile-sync-endpoints",
@ -89,7 +89,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.360.2",
"posthog-js": "1.364.7",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "catalog:",
@ -101,7 +101,7 @@
"react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.1",
"sanitize-html": "2.17.2",
"tar-js": "^0.3.0",
"ua-parser-js": "1.0.40",
"uuid": "^13.0.0",
@ -125,10 +125,9 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@casualbot/jest-sonar-reporter": "2.5.0",
"@casualbot/jest-sonar-reporter": "2.5.1",
"@element-hq/element-call-embedded": "0.18.0",
"@element-hq/element-web-playwright-common": "catalog:",
"@element-hq/element-web-playwright-common-local": "workspace:*",
"@element-hq/element-web-playwright-common": "workspace:*",
"@fetch-mock/jest": "^0.2.20",
"@jest/globals": "^30.2.0",
"@peculiar/webcrypto": "^1.4.3",
@ -203,7 +202,7 @@
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"matrix-web-i18n": "catalog:",
"mini-css-extract-plugin": "2.10.1",
"mini-css-extract-plugin": "2.10.2",
"modernizr": "^3.12.0",
"playwright-core": "catalog:",
"postcss": "8.5.8",
@ -246,6 +245,6 @@
"engines": {
"node": ">=22.18"
},
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"private": true
}

View File

@ -21,7 +21,6 @@ import {
waitForVerificationRequest,
} from "./utils";
import { type Bot } from "../../pages/bot";
import { Toasts } from "../../pages/toasts.ts";
import type { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.describe("Device verification", { tag: "@no-webkit" }, () => {
@ -82,7 +81,11 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
);
// Regression test for https://github.com/element-hq/element-web/issues/29110
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
test("No toast after verification, even if the secrets take a while to arrive", async ({
page,
credentials,
toasts,
}) => {
// Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens
// when we are in an encrypted room.
await aliceBotClient.createRoom({
@ -121,7 +124,6 @@ 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)
const toasts = new Toasts(page);
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();

View File

@ -43,11 +43,7 @@ test.describe("Key storage out of sync toast", () => {
});
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
// We need to wait for there to be two toasts as the wait below won't work in isolation:
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
// it would always be checking the same toast, even if another one is now the first.
await expect(page.getByRole("alert")).toHaveCount(2);
await expect(page.getByRole("alert").first()).toMatchScreenshot(
await expect(page.getByRole("alert").filter({ hasText: "Your key storage is out of sync." })).toMatchScreenshot(
"key-storage-out-of-sync-toast.png",
screenshotOptions,
);

View File

@ -0,0 +1,61 @@
/*
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 { test, expect } from "../../element-web-test";
import { getSampleFilePath } from "../../sample-files";
test.describe("Devtools", () => {
test.use({
displayName: "Alice",
});
test("should allow enabling low bandwidth mode", async ({ page, homeserver, user, app }) => {
// Upload a picture
const userSettings = await app.settings.openUserSettings("Account");
const profileSettings = userSettings.locator(".mx_UserProfileSettings");
await profileSettings.getByAltText("Upload").setInputFiles(getSampleFilePath("riot.png"));
await app.closeDialog();
// Create an initial room.
const createRoomDialog = await app.openCreateRoomDialog();
await createRoomDialog.getByRole("textbox", { name: "Name" }).fill("Test Room");
await createRoomDialog.getByRole("button", { name: "Create room" }).click();
const composer = app.getComposer().locator("[contenteditable]");
await composer.fill("/devtools");
await composer.press("Enter");
const dialog = page.locator(".mx_Dialog");
await dialog.getByLabel("Developer mode").check();
await dialog.getByLabel("Disable bandwidth-heavy features").click();
// Wait for refresh.
await page.waitForEvent("domcontentloaded");
await app.viewRoomByName("Test Room");
// This only appears when encryption has been disabled in the client.
await expect(page.getByText("The encryption used by this room isn't supported.")).toBeVisible();
// None of these should be requested.
let hasSentTyping = false;
let hasRequestedThumbnail = false;
await page.route("**/_matrix/client/v3/rooms/*/typing/*", async (route) => {
hasSentTyping = true;
await route.fulfill({ json: {} });
});
await page.route("**/_matrix/media/v3/thumbnail/**", async (route) => {
hasRequestedThumbnail = true;
await route.fulfill({ json: {} });
});
await page.route("**/_matrix/client/v1/media/thumbnail/**", async (route) => {
hasRequestedThumbnail = true;
await route.fulfill({ json: {} });
});
await composer.pressSequentially("Provoke typing request", { delay: 5 });
expect(hasSentTyping).toEqual(false);
expect(hasRequestedThumbnail).toEqual(false);
});
});

View File

@ -0,0 +1,288 @@
/*
* Copyright 2025 New Vector 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";
import { expect, test } from "../../../element-web-test";
test.describe("Room list sections", () => {
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list", "feature_room_list_sections"],
botCreateOpts: {
displayName: "BotBob",
autoAcceptInvites: true,
},
});
/**
* 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
await app.closeNotificationToast();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
});
test.describe("Section rendering", () => {
test.beforeEach(async ({ app, user }) => {
// Create regular rooms
for (let i = 0; i < 3; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});
test("should render sections with correct rooms in each", { tag: "@screenshot" }, 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);
const roomList = getRoomList(page);
// All three section headers should be visible
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
// Ensure all rooms are visible
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room low prio room" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room room0" })).toBeVisible();
await expect(roomList).toMatchScreenshot("room-list-sections.png");
});
test("should only show non-empty sections", async ({ page, app }) => {
// No low priority rooms created, only regular and favourite rooms
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);
// Chats and Favourites sections should still be visible
await expect(getSectionHeader(page, "Chats")).toBeVisible();
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
// Low Priority sections should not be visible
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
});
test("should render a flat list when there is only rooms in Chats section", async ({ page, app }) => {
// All sections should not be visible
await expect(getSectionHeader(page, "Chats")).not.toBeVisible();
await expect(getSectionHeader(page, "Favourites")).not.toBeVisible();
await expect(getSectionHeader(page, "Low Priority")).not.toBeVisible();
// It should be a flat list (using listbox a11y role)
await expect(page.getByRole("listbox", { name: "Room list", exact: true })).toBeVisible();
await expect(getRoomList(page).getByRole("option", { name: "Open room room0" })).toBeVisible();
});
});
test.describe("Section collapse and expand", () => {
[
{ section: "Favourites", roomName: "favourite room", tag: "m.favourite" },
{ section: "Low Priority", roomName: "low prio room", tag: "m.lowpriority" },
].forEach(({ section, roomName, tag }) => {
test(`should collapse and expand the ${section} section`, async ({ page, app }) => {
const roomId = await app.client.createRoom({ name: roomName });
if (tag) {
await app.client.evaluate(
async (client, { roomId, tag }) => {
await client.setRoomTag(roomId, tag);
},
{ roomId, tag },
);
}
const roomList = getRoomList(page);
const sectionHeader = getSectionHeader(page, section);
// The room should be visible
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
// Collapse the section
await sectionHeader.click();
// The room should no longer be visible
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).not.toBeVisible();
// The section header should still be visible
await expect(sectionHeader).toBeVisible();
// Expand the section again
await sectionHeader.click();
// The room should be visible again
await expect(roomList.getByRole("row", { name: `Open room ${roomName}` })).toBeVisible();
});
});
test("should render collapsed section", { tag: "@screenshot" }, async ({ page, app }) => {
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);
await app.client.createRoom({ name: "regular room" });
const roomList = getRoomList(page);
// Collapse the Favourites section
await getSectionHeader(page, "Favourites").click();
// Verify favourite room is hidden but regular room is still visible
await expect(roomList.getByRole("row", { name: "Open room favourite room" })).not.toBeVisible();
await expect(roomList.getByRole("row", { name: "Open room regular room" })).toBeVisible();
await expect(roomList).toMatchScreenshot("room-list-sections-collapsed.png");
});
});
test.describe("Rooms placement in sections", () => {
test("should move a room between sections when tags change", async ({ page, app }) => {
await app.client.createRoom({ name: "my room" });
const roomList = getRoomList(page);
// Flat list because there is only rooms in the Chats section
let roomItem = roomList.getByRole("option", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
// Favourite the room via context menu
await roomItem.click({ button: "right" });
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
// The Favourites section header should now be visible and the room should be under it
await expect(getSectionHeader(page, "Favourites")).toBeVisible();
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
// Unfavourite the room
await roomItem.hover();
await roomItem.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
// Mark the room as low priority via context menu
roomItem = roomList.getByRole("option", { name: "Open room my room" });
await roomItem.click({ button: "right" });
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
// The Low Priority section header should now be visible and the room should be under it
await expect(getSectionHeader(page, "Low Priority")).toBeVisible();
roomItem = roomList.getByRole("row", { name: "Open room my room" });
await expect(roomItem).toBeVisible();
});
});
test("should show unread indicator on section header", async ({ page, app, bot }) => {
// 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);
const roomList = getRoomList(page);
// Invite the bot and have it send a message to generate an unread
await app.client.inviteUser(favouriteId, bot.credentials.userId);
await bot.joinRoom(favouriteId);
await bot.sendMessage(favouriteId, "Hello from bot!");
let sectionHeader = getSectionHeader(page, "Favourites", true);
await expect(sectionHeader).toBeVisible();
// Open the room to mark it as read
await roomList.getByRole("row", { name: "Open room favourite room" }).click();
// The section should no longer be unread
sectionHeader = getSectionHeader(page, "Favourites", false);
await expect(sectionHeader).toBeVisible();
});
test.describe("Sections and filters interaction", () => {
test("should not show Favourite and Low Priority filters when sections are enabled", async ({ page, app }) => {
const primaryFilters = getPrimaryFilters(page);
// Expand the filter list to see all filters
const expandButton = primaryFilters.getByRole("button", { name: "Expand filter list" });
await expandButton.click();
// Favourite and Low Priority filters should NOT be visible since sections handle them
await expect(primaryFilters.getByRole("option", { name: "Favourite" })).not.toBeVisible();
// Other filters should still be present
await expect(primaryFilters.getByRole("option", { name: "People" })).toBeVisible();
await expect(primaryFilters.getByRole("option", { name: "Rooms" })).toBeVisible();
await expect(primaryFilters.getByRole("option", { name: "Unread" })).toBeVisible();
});
test("should maintain sections when a filter is applied", async ({ page, app, bot }) => {
// Create a favourite room with unread messages
const favouriteId = await app.client.createRoom({ name: "fav with unread" });
await app.client.evaluate(async (client, roomId) => {
await client.setRoomTag(roomId, "m.favourite");
}, favouriteId);
await app.client.inviteUser(favouriteId, bot.credentials.userId);
await bot.joinRoom(favouriteId);
await bot.sendMessage(favouriteId, "Hello from favourite!");
// Create a regular room with unread messages
const regularId = await app.client.createRoom({ name: "regular with unread" });
await app.client.inviteUser(regularId, bot.credentials.userId);
await bot.joinRoom(regularId);
await bot.sendMessage(regularId, "Hello from regular!");
// Create a room without unread
await app.client.createRoom({ name: "no unread room" });
const roomList = getRoomList(page);
const primaryFilters = getPrimaryFilters(page);
// Apply the Unread filter
await primaryFilters.getByRole("option", { name: "Unread" }).click();
// Only rooms with unreads should be visible
await expect(roomList.getByRole("row", { name: "fav with unread" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "regular with unread" })).toBeVisible();
await expect(roomList.getByRole("row", { name: "no unread room" })).not.toBeVisible();
});
});
});

View File

@ -328,11 +328,11 @@ test.describe("Room list", () => {
const roomListView = getRoomList(page);
const videoRoom = roomListView.getByRole("option", { name: "video room" });
await expect(videoRoom).toHaveAttribute("aria-selected", "true"); // wait for room list update
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await expect(videoRoom).toBeVisible();
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
});
});

View File

@ -14,7 +14,7 @@ test.describe("Message links", () => {
await use({ roomId });
},
});
for (const link of ["https://example.org", "example.org", "ftp://example.org"]) {
for (const link of ["https://example.org", "ftp://example.org"]) {
test(`should linkify a regular link '${link}'`, async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`);
// Needs to be unformatted so we test linkifing
@ -24,6 +24,13 @@ test.describe("Message links", () => {
await expect(linkElement).toBeVisible();
});
}
test("should NOT linkify a bare domain", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`);
// Needs to be unformatted so we test linkifing
await app.client.sendMessage(room.roomId, `Check out example.org`);
const linkElement = page.locator(".mx_EventTile_last").getByRole("link", { name: "example.org" });
await expect(linkElement).not.toBeVisible();
});
test("should linkify a User ID", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`);
// Needs to be unformatted so we test linkifing

View File

@ -14,13 +14,7 @@ test.describe("Topic links", () => {
await use({ roomId });
},
});
for (const link of [
"https://example.org",
"example.org",
"ftp://example.org",
"#aroom:example.org",
"@alice:example.org",
]) {
for (const link of ["https://example.org", "ftp://example.org", "#aroom:example.org", "@alice:example.org"]) {
// Playwright treats '@' as a tag, so replace it to be safe
test(`should linkify plaintext '${link.replace("@", "_@")}'`, async ({ page, user, app, room }) => {
await app.client.sendStateEvent(

View File

@ -48,9 +48,9 @@ test.describe("Room Directory", () => {
await app.closeDialog();
const resp = await bot.publicRooms({});
expect(resp.total_room_count_estimate).toEqual(1);
expect(resp.chunk).toHaveLength(1);
expect(resp.chunk[0].room_id).toEqual(roomId);
expect(resp.total_room_count_estimate).toBeGreaterThanOrEqual(1);
expect(resp.chunk).toHaveLength(resp.total_room_count_estimate);
expect(resp.chunk.find((r) => r.room_id === roomId)).toBeTruthy();
},
);

View File

@ -24,7 +24,7 @@ test.describe("Account user settings tab", () => {
},
});
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user, axe }) => {
await expect(uut).toMatchScreenshot("account.png");
// Assert that the top heading is rendered
@ -70,6 +70,8 @@ test.describe("Account user settings tab", () => {
await expect(accountManagementSection.getByRole("button", { name: "Deactivate Account" })).toHaveClass(
/mx_AccessibleButton_kind_danger/,
);
await expect(axe).toHaveNoViolations();
});
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {

View File

@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
displayName: "Hanako",
});
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
const tab = await app.settings.openUserSettings("Appearance");
// Click "Show advanced" link button
@ -23,6 +23,8 @@ test.describe("Appearance user settings tab", () => {
await expect(tab.getByRole("button", { name: "Hide advanced" })).toBeVisible();
await expect(tab).toMatchScreenshot("appearance-tab.png");
await expect(axe).toHaveNoViolations();
});
test(

View File

@ -23,7 +23,7 @@ test.describe("Appearance user settings tab", () => {
test(
"should be rendered with the light theme selected",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
async ({ page, app, util, axe }) => {
// Assert that 'Match system theme' is not checked
await expect(util.getMatchSystemThemeSwitch()).not.toBeChecked();
@ -34,6 +34,8 @@ test.describe("Appearance user settings tab", () => {
await expect(util.getHighContrastTheme()).not.toBeChecked();
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
await expect(axe).toHaveNoViolations();
},
);

View File

@ -23,7 +23,7 @@ test.describe("Device manager", () => {
}
});
test("should display sessions", async ({ page, app }) => {
test("should display sessions", async ({ page, app, axe }) => {
await app.settings.openUserSettings("Sessions");
const tab = page.locator(".mx_SettingsTab");
@ -85,7 +85,7 @@ test.describe("Device manager", () => {
// session name updated in details
await expect(firstSession.locator(".mx_DeviceDetailHeading h4").getByText(sessionName)).toBeVisible();
// and main list item
await expect(firstSession.locator(".mx_DeviceTile h4").getByText(sessionName)).toBeVisible();
await expect(firstSession.locator(".mx_DeviceTile h3").getByText(sessionName)).toBeVisible();
// sign out using the device details sign out
await firstSession.getByRole("button", { name: "Remove this session" }).click();
@ -96,5 +96,7 @@ test.describe("Device manager", () => {
// no other sessions or security recommendations sections when only one session
await expect(tab.getByText("Other sessions")).not.toBeVisible();
await expect(tab.getByTestId("security-recommendations-section")).not.toBeVisible();
await expect(axe).toHaveNoViolations();
});
});

View File

@ -16,7 +16,7 @@ test.describe("Advanced section in Encryption tab", () => {
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
});
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util, axe }) => {
await util.openEncryptionTab();
const section = util.getEncryptionDetailsSection();
@ -26,6 +26,8 @@ test.describe("Advanced section in Encryption tab", () => {
await expect(section).toMatchScreenshot("encryption-details.png", {
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
});
await expect(axe).toHaveNoViolations();
});
test("should show the import room keys dialog", async ({ page, app, util }) => {

View File

@ -47,6 +47,9 @@ test.describe("Encryption tab", () => {
await util.verifyDevice(recoveryKey);
// Prevent flakiness by scrolling to top of the tab
await page.getByRole("heading", { name: "Key storage" }).scrollIntoViewIfNeeded();
await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
});

View File

@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
await app.viewRoomByName(roomName);
});
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app, axe }) => {
const settings = await app.settings.openRoomSettings("General");
// Assert that "Show less" details element is rendered
@ -34,6 +34,9 @@ test.describe("General room settings tab", () => {
// Assert that "Show more" details element is rendered instead of "Show more"
await expect(settings.getByText("Show less")).not.toBeVisible();
await expect(settings.getByText("Show more")).toBeVisible();
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
await expect(axe).toHaveNoViolations();
});
test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => {

View File

@ -25,7 +25,7 @@ test.describe("Preferences user settings tab", () => {
labsFlags: ["feature_new_room_list"],
});
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
await page.setViewportSize({ width: 1024, height: 4000 });
const tab = await app.settings.openUserSettings("Preferences");
// Assert that the top heading is rendered
@ -39,6 +39,8 @@ test.describe("Preferences user settings tab", () => {
}
`,
});
await expect(axe).toHaveNoViolations();
});
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {

View File

@ -8,11 +8,13 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test";
test.describe("Quick settings menu", () => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
await page.getByRole("button", { name: "Quick settings" }).click();
// Assert that the top heading is renderedc
const settings = page.getByTestId("quick-settings-menu");
await expect(settings).toBeVisible();
await expect(settings).toMatchScreenshot("quick-settings.png");
await expect(axe).toHaveNoViolations();
});
});

View File

@ -25,7 +25,7 @@ test.describe("Roles & Permissions room settings tab", () => {
settings = await app.settings.openRoomSettings("Roles & Permissions");
});
test("should be able to change the role of a user", async ({ page, app, user }) => {
test("should be able to change the role of a user", async ({ page, app, user, axe }) => {
const privilegedUserSection = settings.locator(".mx_SettingsFieldset").first();
const applyButton = privilegedUserSection.getByRole("button", { name: "Apply" });
@ -55,5 +55,7 @@ test.describe("Roles & Permissions room settings tab", () => {
settings = await app.settings.openRoomSettings("Roles & Permissions");
combobox = privilegedUserSection.getByRole("combobox", { name: user.userId });
await expect(combobox).toHaveValue("50");
await expect(axe).toHaveNoViolations();
});
});

View File

@ -32,12 +32,14 @@ test.describe("Security user settings tab", () => {
});
test.describe("AnalyticsLearnMoreDialog", () => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user, axe }) => {
const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
"Security-user-settings-tab-with-posthog-enable-b5d89-csLearnMoreDialog-should-be-rendered-properly-1.png",
);
await expect(axe).toHaveNoViolations();
});
});

View File

@ -24,7 +24,6 @@ import type { IConfigOptions } from "../src/IConfigOptions";
import { type Credentials } from "./plugins/homeserver";
import { ElementAppPage } from "./pages/ElementAppPage";
import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts";
import { Bot, type CreateBotOpts } from "./pages/bot";
import { Webserver } from "./plugins/webserver";
import { type WorkerOptions, type Services, test as base } from "./services";
@ -52,7 +51,6 @@ export interface TestFixtures extends BaseTestFixtures {
crypto: Crypto;
room?: { roomId: string };
toasts: Toasts;
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
botCreateOpts: CreateBotOpts;
bot: Bot;
@ -92,9 +90,6 @@ export const test = base.extend<TestFixtures>({
crypto: async ({ page, homeserver, request }, use) => {
await use(new Crypto(page, homeserver, request));
},
toasts: async ({ page }, use) => {
await use(new Toasts(page));
},
botCreateOpts: {},
bot: async ({ page, homeserver, botCreateOpts, user }, use, testInfo) => {

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 { Page, Request, Route } from "@playwright/test";
import type { Page, Request, Route, Disposable } from "@playwright/test";
import type { Client } from "./client";
/**
@ -16,7 +16,7 @@ import type { Client } from "./client";
*/
export class Network {
private isOffline = false;
private setupPromise?: Promise<void>;
private setupPromise?: Promise<Disposable>;
constructor(
private page: Page,

View File

@ -1,53 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
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, expect, type Locator } from "@playwright/test";
export class Toasts {
public constructor(private readonly page: Page) {}
/**
* Assert that a toast with the given title exists, and return it
*
* @param expectedTitle - Expected title of the toast
* @param timeout Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
* @returns the Locator for the matching toast
*/
public async getToast(expectedTitle: string, timeout?: number): Promise<Locator> {
const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first();
await expect(toast).toBeVisible({ timeout });
return toast;
}
/**
* Assert that no toasts exist
*/
public async assertNoToasts(): Promise<void> {
await expect(this.page.locator(".mx_Toast_toast")).not.toBeVisible();
}
/**
* Accept a toast with the given title, only works for the first toast in the stack
*
* @param expectedTitle - Expected title of the toast
*/
public async acceptToast(expectedTitle: string): Promise<void> {
const toast = await this.getToast(expectedTitle);
await toast.locator('.mx_Toast_buttons button[data-kind="primary"]').click();
}
/**
* Reject a toast with the given title, only works for the first toast in the stack
*
* @param expectedTitle - Expected title of the toast
*/
public async rejectToast(expectedTitle: string): Promise<void> {
const toast = await this.getToast(expectedTitle);
await toast.locator('.mx_Toast_buttons button[data-kind="secondary"]').click();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 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: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 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: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 KiB

After

Width:  |  Height:  |  Size: 982 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

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