Merge branch 'element-hq:master' into master

This commit is contained in:
2025-06-22 08:37:03 +02:00
committed by GitHub
691 changed files with 22611 additions and 15850 deletions
+9
View File
@@ -30,6 +30,10 @@ module.exports = {
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
"Use UIStore to access window dimensions instead.",
),
...buildRestrictedPropertiesOptions(
["React.forwardRef", "*.forwardRef", "forwardRef"],
"Use ref props instead.",
),
...buildRestrictedPropertiesOptions(
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
"Use Media helper instead to centralise access for customisation.",
@@ -55,6 +59,11 @@ module.exports = {
"error",
{
paths: [
{
name: "react",
importNames: ["forwardRef"],
message: "Use ref props instead.",
},
{
name: "@testing-library/react",
message: "Please use jest-matrix-react instead",
+3 -3
View File
@@ -43,9 +43,9 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
# Disable cache on Windows as it is slower than not caching
# https://github.com/actions/setup-node/issues/975
@@ -77,7 +77,7 @@ jobs:
yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp-${{ matrix.image }}
path: webapp
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
VERSION: ${{ github.ref_name }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Download package
run: |
@@ -62,7 +62,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@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: element-web.deb
path: element-web.deb
+3 -3
View File
@@ -26,9 +26,9 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
R2_PUBLIC_URL: "https://element-web-develop.element.io"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -53,7 +53,7 @@ jobs:
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp
path: dist/develop.tar.gz
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Load GPG key
run: |
+3 -3
View File
@@ -20,12 +20,12 @@ jobs:
env:
TEST_TAG: vectorim/element-web:test
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU
@@ -132,7 +132,7 @@ jobs:
cosign sign --yes ${images}
- name: Update repo description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
if: github.event_name != 'pull_request'
continue-on-error: true
with:
+7 -7
View File
@@ -17,23 +17,23 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch element-desktop
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
repository: element-hq/element-desktop
path: element-desktop
- name: Fetch element-web
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
path: element-web
- name: Fetch matrix-js-sdk
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
cache-dependency-path: element-web/yarn.lock
@@ -47,7 +47,7 @@ jobs:
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v2
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
with:
mdbook-version: "0.4.10"
@@ -88,7 +88,7 @@ jobs:
run: mdbook build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: ./book
@@ -104,4 +104,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
@@ -25,7 +25,7 @@ jobs:
actions: read
steps:
- name: Download HTML report
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -33,7 +33,7 @@ jobs:
path: playwright-report
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v3
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
with:
path: playwright-report
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
+27 -18
View File
@@ -50,11 +50,11 @@ jobs:
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
repository: element-hq/element-web
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -81,7 +81,7 @@ jobs:
yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp
path: webapp
@@ -89,7 +89,7 @@ jobs:
- name: Calculate runner variables
id: runner-vars
uses: actions/github-script@v7
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
@@ -129,18 +129,18 @@ jobs:
- runAllTests: false
project: Pinecone
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
repository: element-hq/element-web
- name: 📥 Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: webapp
path: webapp
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
cache-dependency-path: yarn.lock
@@ -154,12 +154,11 @@ jobs:
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
uses: actions/cache@v4
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
id: playwright-cache
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
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'
@@ -180,25 +179,35 @@ jobs:
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
path: blob-report
retention-days: 1
downstream-modules:
name: Downstream Playwright tests [element-modules]
needs: build
if: inputs.skip != true && github.event_name == 'merge_group'
uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main
with:
webapp-artifact: webapp
complete:
name: end-to-end-tests
needs: playwright
needs:
- playwright
- downstream-modules
if: always()
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
if: inputs.skip != true
with:
persist-credentials: false
repository: element-hq/element-web
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
if: inputs.skip != true
with:
cache: "yarn"
@@ -210,7 +219,7 @@ jobs:
- name: Download blob reports from GitHub Actions Artifacts
if: inputs.skip != true
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: all-blob-reports-*
path: all-blob-reports
@@ -226,11 +235,11 @@ jobs:
# 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@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: html-report
path: playwright-report
retention-days: 14
- if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success'
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
name: Tidy closed issues
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
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@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
name: Close duplicate as Not Planned
if: steps.main.outputs.closeAsNotPlanned
with:
+2 -2
View File
@@ -28,7 +28,7 @@ jobs:
Exercise caution. Use test accounts.
- name: 📥 Download artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -36,7 +36,7 @@ jobs:
path: webapp
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@v3
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
with:
path: webapp
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
ROOM_ID: ${{ secrets.ROOM_ID }}
@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Update synapse image
run: |
@@ -8,7 +8,7 @@ jobs:
name: Check PR base branch
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
const baseBranch = context.payload.pull_request.base.ref;
+6 -6
View File
@@ -41,7 +41,7 @@ jobs:
REPOS: matrix-js-sdk element-web element-desktop
steps:
- name: Checkout Element Desktop
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
if: inputs.element-desktop
with:
repository: element-hq/element-desktop
@@ -51,7 +51,7 @@ jobs:
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Checkout Element Web
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
if: inputs.element-web
with:
repository: element-hq/element-web
@@ -61,7 +61,7 @@ jobs:
fetch-tags: true
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
- name: Checkout Matrix JS SDK
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
if: inputs.matrix-js-sdk
with:
repository: matrix-org/matrix-js-sdk
@@ -100,7 +100,7 @@ jobs:
repo: matrix-org/matrix-js-sdk
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
wait-interval: 10
check-name: draft
check-name: "draft / draft"
allowed-conclusions: success
- name: Wait for element-web draft
@@ -111,7 +111,7 @@ jobs:
repo: element-hq/element-web
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
wait-interval: 10
check-name: draft
check-name: "draft / draft"
allowed-conclusions: success
- name: Wait for element-desktop draft
@@ -122,5 +122,5 @@ jobs:
repo: element-hq/element-desktop
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
wait-interval: 10
check-name: draft
check-name: "draft / draft"
allowed-conclusions: success
+11 -11
View File
@@ -23,9 +23,9 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -57,7 +57,7 @@ jobs:
name: "Rethemendex Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- run: ./res/css/rethemendex.sh
@@ -67,9 +67,9 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -85,9 +85,9 @@ jobs:
name: "Style Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -103,9 +103,9 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -121,9 +121,9 @@ jobs:
name: "Analyse Dead Code"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
+5 -5
View File
@@ -39,12 +39,12 @@ jobs:
runner: [1, 2]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
- name: Yarn cache
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "lts/*"
cache: "yarn"
@@ -55,7 +55,7 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- name: Jest Cache
uses: actions/cache@v4
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/yarn.lock') }}
@@ -84,7 +84,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-${{ matrix.runner }}
path: |
@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@fe98467f9071758c7fc214af9dbac7f301bd23d4
uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success
+2 -1
View File
@@ -11,7 +11,8 @@ jobs:
runs-on: ubuntu-24.04
if: |
contains(github.event.issue.assignees.*.login, 't3chguy') ||
contains(github.event.issue.assignees.*.login, 'andybalaam') ||
contains(github.event.issue.assignees.*.login, 'florianduros') ||
contains(github.event.issue.assignees.*.login, 'dbkr') ||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
steps:
- uses: actions/add-to-project@main
+8 -8
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@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
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@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
github.rest.issues.addLabels({
@@ -61,7 +61,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
steps:
- id: add_to_project
uses: actions/add-to-project@v1.0.2
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: ${{ env.PROJECT_URL }}
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -84,7 +84,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
steps:
- id: add_to_project
uses: actions/add-to-project@v1.0.2
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: ${{ env.PROJECT_URL }}
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -150,15 +150,15 @@ jobs:
project-url: https://github.com/orgs/element-hq/projects/41
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
verticals_feature:
name: Add labelled issues to Verticals Feature project
crypto:
name: Add labelled issues to Crypto project
runs-on: ubuntu-24.04
if: >
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
contains(github.event.issue.labels.*.name, 'Team: Crypto')
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/element-hq/projects/57
project-url: https://github.com/orgs/element-hq/projects/76
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
tech_debt:
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
operations-per-run: 100
# Flaky test issue closing
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
contains(github.event.issue.labels.*.name, 'Z-Labs')
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
with:
script: |
github.rest.issues.removeLabel({
+2 -2
View File
@@ -9,9 +9,9 @@ jobs:
update:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-24.04
environment: Matrix
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
-1
View File
@@ -25,7 +25,6 @@ electron/pub
.env
/coverage
# Auto-generated file
/src/modules.ts
/src/modules.js
/build_config.yaml
/book
+145
View File
@@ -1,3 +1,148 @@
Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
====================================================================================================
## ✨ Features
* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave.
* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam.
* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot.
* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR.
* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy.
* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros.
* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros.
* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR.
* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR.
* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros.
* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n.
## 🐛 Bug Fixes
* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto.
* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram.
* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh.
* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh.
* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy.
* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR.
* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR.
* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis.
* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh.
Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10)
====================================================================================================
## 🐛 Bug Fixes
+ Check the sender of an event matches owner of session, preventing sender spoofing by homeserver owners.
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
Changes in [1.11.102](https://github.com/element-hq/element-web/releases/tag/v1.11.102) (2025-06-03)
====================================================================================================
## ✨ Features
* EW: Modernize the recovery key input modal ([#29819](https://github.com/element-hq/element-web/pull/29819)). Contributed by @uhoreg.
* New room list: move secondary filters into primary filters ([#29972](https://github.com/element-hq/element-web/pull/29972)). Contributed by @florianduros.
* Prompt the user when key storage is unexpectedly off ([#29912](https://github.com/element-hq/element-web/pull/29912)). Contributed by @andybalaam.
* New room list: move sort menu in room list header ([#29983](https://github.com/element-hq/element-web/pull/29983)). Contributed by @florianduros.
* New room list: rework spacing of room list item ([#29965](https://github.com/element-hq/element-web/pull/29965)). Contributed by @florianduros.
* RLS: Remove forgotten room from skiplist ([#29933](https://github.com/element-hq/element-web/pull/29933)). Contributed by @MidhunSureshR.
* Add room list sorting ([#29951](https://github.com/element-hq/element-web/pull/29951)). Contributed by @dbkr.
* Don't use the minimised width(68px) on the new room list ([#29778](https://github.com/element-hq/element-web/pull/29778)). Contributed by @langleyd.
## 🐛 Bug Fixes
* [Backport staging] Close call options popup menu when option has been selected ([#30054](https://github.com/element-hq/element-web/pull/30054)). Contributed by @RiotRobot.
* RoomListStoreV3: Only add new rooms that pass `VisibilityProvider` check ([#29974](https://github.com/element-hq/element-web/pull/29974)). Contributed by @MidhunSureshR.
* Re-order primary filters ([#29957](https://github.com/element-hq/element-web/pull/29957)). Contributed by @dbkr.
* Fix leaky CSS adding `!` to all H1 elements ([#29964](https://github.com/element-hq/element-web/pull/29964)). Contributed by @t3chguy.
* Fix extensions panel style ([#29273](https://github.com/element-hq/element-web/pull/29273)). Contributed by @langleyd.
* Fix state events being hidden from widgets in read\_events actions ([#29954](https://github.com/element-hq/element-web/pull/29954)). Contributed by @robintown.
* Remove old filter test ([#29963](https://github.com/element-hq/element-web/pull/29963)). Contributed by @dbkr.
Changes in [1.11.101](https://github.com/element-hq/element-web/releases/tag/v1.11.101) (2025-05-20)
====================================================================================================
## ✨ Features
* New room list: add keyboard navigation support ([#29805](https://github.com/element-hq/element-web/pull/29805)). Contributed by @florianduros.
* Use the JoinRuleSettings component for the guest link access prompt. ([#28614](https://github.com/element-hq/element-web/pull/28614)). Contributed by @toger5.
* Add loading state to the new room list view ([#29725](https://github.com/element-hq/element-web/pull/29725)). Contributed by @langleyd.
* Make OIDC identity reset consistent with EX ([#29854](https://github.com/element-hq/element-web/pull/29854)). Contributed by @andybalaam.
* Support error code for email / phone adding unsupported (MSC4178) ([#29855](https://github.com/element-hq/element-web/pull/29855)). Contributed by @dbkr.
* Update identity reset UI (Make consistent with EX) ([#29701](https://github.com/element-hq/element-web/pull/29701)). Contributed by @andybalaam.
* Add secondary filters to the new room list ([#29818](https://github.com/element-hq/element-web/pull/29818)). Contributed by @dbkr.
* Fix battery drain from Web Audio ([#29203](https://github.com/element-hq/element-web/pull/29203)). Contributed by @mbachry.
## 🐛 Bug Fixes
* Fix go home shortcut on macos and change toggle action events shortcut ([#29929](https://github.com/element-hq/element-web/pull/29929)). Contributed by @florianduros.
* New room list: fix outdated message preview when space or filter change ([#29925](https://github.com/element-hq/element-web/pull/29925)). Contributed by @florianduros.
* Stop migrating to MSC4278 if the config exists. ([#29924](https://github.com/element-hq/element-web/pull/29924)). Contributed by @Half-Shot.
* Ensure consistent download file name on download from ImageView ([#29913](https://github.com/element-hq/element-web/pull/29913)). Contributed by @t3chguy.
* Add error toast when service worker registration fails ([#29895](https://github.com/element-hq/element-web/pull/29895)). Contributed by @t3chguy.
* New Room List: Prevent old tombstoned rooms from appearing in the list ([#29881](https://github.com/element-hq/element-web/pull/29881)). Contributed by @MidhunSureshR.
* Remove lag in search field ([#29885](https://github.com/element-hq/element-web/pull/29885)). Contributed by @florianduros.
* Respect UIFeature.Voip ([#29873](https://github.com/element-hq/element-web/pull/29873)). Contributed by @langleyd.
* Allow jumping to message search from spotlight ([#29850](https://github.com/element-hq/element-web/pull/29850)). Contributed by @t3chguy.
Changes in [1.11.100](https://github.com/element-hq/element-web/releases/tag/v1.11.100) (2025-05-06)
====================================================================================================
## ✨ Features
* Move rich topics out of labs / stabilise MSC3765 ([#29817](https://github.com/element-hq/element-web/pull/29817)). Contributed by @Johennes.
* Spell out that Element Web does \*not\* work on mobile. ([#29211](https://github.com/element-hq/element-web/pull/29211)). Contributed by @ara4n.
* Add message preview support to the new room list ([#29784](https://github.com/element-hq/element-web/pull/29784)). Contributed by @dbkr.
* Global configuration flag for media previews ([#29582](https://github.com/element-hq/element-web/pull/29582)). Contributed by @Half-Shot.
* New room list: add partial keyboard shortcuts support ([#29783](https://github.com/element-hq/element-web/pull/29783)). Contributed by @florianduros.
* MVVM RoomSummaryCard Topic ([#29710](https://github.com/element-hq/element-web/pull/29710)). Contributed by @MarcWadai.
* Warn on self change from settings > roles ([#28926](https://github.com/element-hq/element-web/pull/28926)). Contributed by @MarcWadai.
* New room list: new visual for invitation ([#29773](https://github.com/element-hq/element-web/pull/29773)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix incorrect display of the user info display name ([#29826](https://github.com/element-hq/element-web/pull/29826)). Contributed by @langleyd.
* RoomListStore: Remove invite rooms on decline ([#29804](https://github.com/element-hq/element-web/pull/29804)). Contributed by @MidhunSureshR.
* Fix the buttons not being displayed with long preview text ([#29811](https://github.com/element-hq/element-web/pull/29811)). Contributed by @dbkr.
* New room list: fix missing/incorrect notification decoration ([#29796](https://github.com/element-hq/element-web/pull/29796)). Contributed by @florianduros.
* New Room List: Prevent potential scroll jump/flicker when switching spaces ([#29781](https://github.com/element-hq/element-web/pull/29781)). Contributed by @MidhunSureshR.
* New room list: fix incorrect decoration ([#29770](https://github.com/element-hq/element-web/pull/29770)). Contributed by @florianduros.
Changes in [1.11.99](https://github.com/element-hq/element-web/releases/tag/v1.11.99) (2025-04-23)
==================================================================================================
No changes, just bumping the version to accommodate a new Element Desktop release
Changes in [1.11.98](https://github.com/element-hq/element-web/releases/tag/v1.11.98) (2025-04-22)
==================================================================================================
## ✨ Features
* print better errors in the search view instead of a blocking modal ([#29724](https://github.com/element-hq/element-web/pull/29724)). Contributed by @Jujure.
* New room list: video room and video call decoration ([#29693](https://github.com/element-hq/element-web/pull/29693)). Contributed by @florianduros.
* Remove Secure Backup, Cross-signing and Cryptography sections in `Security & Privacy` user settings ([#29088](https://github.com/element-hq/element-web/pull/29088)). Contributed by @florianduros.
* Allow reporting a room when rejecting an invite. ([#29570](https://github.com/element-hq/element-web/pull/29570)). Contributed by @Half-Shot.
* RoomListViewModel: Reset primary and secondary filters on space change ([#29672](https://github.com/element-hq/element-web/pull/29672)). Contributed by @MidhunSureshR.
* RoomListStore: Support specific sorting requirements for muted rooms ([#29665](https://github.com/element-hq/element-web/pull/29665)). Contributed by @MidhunSureshR.
* New room list: add notification options menu ([#29639](https://github.com/element-hq/element-web/pull/29639)). Contributed by @florianduros.
* Room List: Scroll to top of the list when active room is not in the list ([#29650](https://github.com/element-hq/element-web/pull/29650)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* Fix unwanted form submit behaviour in memberlist ([#29747](https://github.com/element-hq/element-web/pull/29747)). Contributed by @MidhunSureshR.
* New room list: fix public room icon visibility when filter change ([#29737](https://github.com/element-hq/element-web/pull/29737)). Contributed by @florianduros.
* Fix custom theme support for short hex \& rgba hex strings ([#29726](https://github.com/element-hq/element-web/pull/29726)). Contributed by @t3chguy.
* New room list: minor visual fixes ([#29723](https://github.com/element-hq/element-web/pull/29723)). Contributed by @florianduros.
* Fix getOidcCallbackUrl for Element Desktop ([#29711](https://github.com/element-hq/element-web/pull/29711)). Contributed by @t3chguy.
* Fix some webp images improperly marked as animated ([#29713](https://github.com/element-hq/element-web/pull/29713)). Contributed by @Petersmit27.
* Revert deletion of hydrateSession ([#29703](https://github.com/element-hq/element-web/pull/29703)). Contributed by @Jujure.
* Fix converttoroom \& converttodm not working ([#29705](https://github.com/element-hq/element-web/pull/29705)). Contributed by @t3chguy.
* Ensure forceCloseAllModals also closes priority/static modals ([#29706](https://github.com/element-hq/element-web/pull/29706)). Contributed by @t3chguy.
* Continue button is disabled when uploading a recovery key file ([#29695](https://github.com/element-hq/element-web/pull/29695)). Contributed by @Giwayume.
* Catch errors after syncing recovery ([#29691](https://github.com/element-hq/element-web/pull/29691)). Contributed by @andybalaam.
* New room list: fix multiple visual issues ([#29673](https://github.com/element-hq/element-web/pull/29673)). Contributed by @florianduros.
* New Room List: Fix mentions filter matching rooms with any highlight ([#29668](https://github.com/element-hq/element-web/pull/29668)). Contributed by @MidhunSureshR.
* Fix truncated emoji label during emoji SAS ([#29643](https://github.com/element-hq/element-web/pull/29643)). Contributed by @florianduros.
* Remove duplicate jitsi link ([#29642](https://github.com/element-hq/element-web/pull/29642)). Contributed by @dbkr.
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
==================================================================================================
## ✨ Features
+3 -3
View File
@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.14-labs
# syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:2acffd86b1bdefb8fa6b48b6e9aadf75430e8ab9c43c54c515ea7df77897f987
# Need root user to install packages & manipulate the usr directory
USER root
+1 -1
View File
@@ -126,7 +126,7 @@ guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it alread
1. Install the prerequisites: `yarn install`.
- If you're using the `develop` branch, then it is recommended to set up a
proper development environment (see [Setting up a dev
environment](#setting-up-a-dev-environment) below). Alternatively, you
environment](./developer_guide.md#setting-up-a-dev-environment) below). Alternatively, you
can use <https://develop.element.io> - the continuous integration release of
the develop branch.
1. Configure the app by copying `config.sample.json` to `config.json` and
-13
View File
@@ -1,13 +0,0 @@
{
"name": "Element",
"description": "A glossy Matrix collaboration client for the web.",
"repository": {
"url": "https://github.com/element-hq/element-web",
"license": "AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial"
},
"bugs": {
"list": "https://github.com/element-hq/element-web/issues",
"report": "https://github.com/element-hq/element-web/issues/new/choose"
},
"keywords": ["chat", "riot", "matrix"]
}
-1
View File
@@ -46,7 +46,6 @@
- [Skinning](skinning.md)
- [Cider editor](ciderEditor.md)
- [Iconography](icons.md)
- [Jitsi](jitsi.md)
- [Local echo](local-echo-dev.md)
- [Media](media-handling.md)
- [Room List Store](room-list-store.md)
-4
View File
@@ -101,10 +101,6 @@ Under the hood this stops Element Web from adding the `perParticipantE2EE` flag
This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet.
## Rich text in room topics (`feature_html_topic`) [In Development]
Enables rendering of MD / HTML in room topics.
## Enable the notifications panel in the room header (`feature_notifications`)
Unreliable in encrypted rooms.
+34 -32
View File
@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.97",
"version": "1.11.104",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -22,8 +22,7 @@
"LICENSE",
"README.md",
"AUTHORS.rst",
"package.json",
"contribute.json"
"package.json"
],
"style": "bundle.css",
"matrix_i18n_extra_translation_funcs": [
@@ -69,19 +68,20 @@
"postinstall": "patch-package"
},
"resolutions": {
"@playwright/test": "1.51.1",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"oidc-client-ts": "3.2.0",
"**/pretty-format/react-is": "19.1.0",
"@playwright/test": "1.52.0",
"@types/react": "19.1.6",
"@types/react-dom": "19.1.5",
"oidc-client-ts": "3.2.1",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001704",
"testcontainers": "10.21.0",
"caniuse-lite": "1.0.30001720",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "^0.1.1",
"@element-hq/element-web-module-api": "1.0.0",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
@@ -93,8 +93,8 @@
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.9.0",
"@vector-im/matrix-wysiwyg": "2.38.2",
"@vector-im/compound-web": "^7.11.0",
"@vector-im/matrix-wysiwyg": "2.38.3",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -123,14 +123,14 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-react": "4.2.0",
"linkify-string": "4.2.0",
"linkifyjs": "4.2.0",
"linkify-react": "4.3.1",
"linkify-string": "4.3.1",
"linkifyjs": "4.3.1",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "37.3.0",
"matrix-js-sdk": "37.9.0",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
@@ -138,22 +138,22 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.157.2",
"posthog-js": "1.248.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^18.3.1",
"react": "^19.0.0",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.3.0",
"react-dom": "^18.3.1",
"react-dom": "^19.0.0",
"react-focus-lock": "^2.5.1",
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.22.5",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.14.0",
"sanitize-html": "2.17.0",
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.2.5",
"temporal-polyfill": "^0.3.0",
"ua-parser-js": "^1.0.2",
"uuid": "^11.0.0",
"what-input": "^5.2.10"
@@ -180,13 +180,14 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.9.0",
"@element-hq/element-call-embedded": "0.12.0",
"@element-hq/element-web-playwright-common": "^1.1.5",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@rrweb/types": "^2.0.0-alpha.18",
"@sentry/webpack-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^4.0.0",
"@svgr/webpack": "^8.0.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
@@ -211,11 +212,11 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "18.3.18",
"@types/react": "19.1.6",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "18.3.5",
"@types/react-dom": "19.1.5",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.13.0",
"@types/sanitize-html": "2.16.0",
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
"@types/ua-parser-js": "^0.7.36",
@@ -246,7 +247,7 @@
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-unicorn": "^56.0.0",
"express": "^4.18.2",
"express": "^5.0.0",
"fake-indexeddb": "^6.0.0",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
@@ -261,7 +262,7 @@
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"knip": "^5.36.2",
"lint-staged": "^15.0.2",
"lint-staged": "^16.0.0",
"matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2",
"minimist": "^1.2.6",
@@ -286,13 +287,13 @@
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"stylelint": "^16.13.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-config-standard": "^38.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^10.20.0",
"testcontainers": "^11.0.0",
"ts-node": "^10.9.1",
"typescript": "5.8.2",
"typescript": "5.8.3",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
@@ -310,5 +311,6 @@
},
"engines": {
"node": ">=20.0.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
-76
View File
@@ -1,76 +0,0 @@
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
index 6ea73ef..cb51757 100644
--- a/node_modules/@types/react/index.d.ts
+++ b/node_modules/@types/react/index.d.ts
@@ -151,7 +151,7 @@ declare namespace React {
/**
* The current value of the ref.
*/
- readonly current: T | null;
+ current: T;
}
interface DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES {
@@ -186,7 +186,7 @@ declare namespace React {
* @see {@link RefObject}
*/
- type Ref<T> = RefCallback<T> | RefObject<T> | null;
+ type Ref<T> = RefCallback<T> | RefObject<T | null> | null;
/**
* A legacy implementation of refs where you can pass a string to a ref prop.
*
@@ -300,7 +300,7 @@ declare namespace React {
*
* @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
*/
- ref?: LegacyRef<T> | undefined;
+ ref?: LegacyRef<T | null> | undefined;
}
/**
@@ -1234,7 +1234,7 @@ declare namespace React {
*
* @see {@link ForwardRefRenderFunction}
*/
- type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
+ type ForwardedRef<T> = ((instance: T | null) => void) | RefObject<T | null> | null;
/**
* The type of the function passed to {@link forwardRef}. This is considered different
@@ -1565,7 +1565,7 @@ declare namespace React {
[propertyName: string]: any;
}
- function createRef<T>(): RefObject<T>;
+ function createRef<T>(): RefObject<T | null>;
/**
* The type of the component returned from {@link forwardRef}.
@@ -1989,7 +1989,7 @@ declare namespace React {
* @version 16.8.0
* @see {@link https://react.dev/reference/react/useRef}
*/
- function useRef<T>(initialValue: T): MutableRefObject<T>;
+ function useRef<T>(initialValue: T): RefObject<T>;
// convenience overload for refs given as a ref prop as they typically start with a null value
/**
* `useRef` returns a mutable ref object whose `.current` property is initialized to the passed argument
@@ -2004,7 +2004,7 @@ declare namespace React {
* @version 16.8.0
* @see {@link https://react.dev/reference/react/useRef}
*/
- function useRef<T>(initialValue: T | null): RefObject<T>;
+ function useRef<T>(initialValue: T | null): RefObject<T | null>;
// convenience overload for potentially undefined initialValue / call with 0 arguments
// has a default to stop it from defaulting to {} instead
/**
@@ -2017,7 +2017,7 @@ declare namespace React {
* @version 16.8.0
* @see {@link https://react.dev/reference/react/useRef}
*/
- function useRef<T = undefined>(initialValue?: undefined): MutableRefObject<T | undefined>;
+ function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
/**
* The signature is identical to `useEffect`, but it fires synchronously after all DOM mutations.
* Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside
+31
View File
@@ -0,0 +1,31 @@
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
index d3318dc..c2b2c77 100644
--- a/node_modules/@types/react/index.d.ts
+++ b/node_modules/@types/react/index.d.ts
@@ -134,7 +134,7 @@ declare namespace React {
props: P,
) => ReactNode | Promise<ReactNode>)
// constructor signature must match React.Component
- | (new(props: P) => Component<any, any>);
+ | (new(props: P, context?: any) => Component<any, any>);
/**
* Created by {@link createRef}, or {@link useRef} when passed `null`.
@@ -945,7 +945,7 @@ declare namespace React {
context: unknown;
// Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
- constructor(props: P);
+ constructor(props: P, context?: unknown);
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
@@ -1117,7 +1117,7 @@ declare namespace React {
*/
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
// constructor signature must match React.Component
- new(props: P): Component<P, S>;
+ new(props: P, context?: any): Component<P, S>;
/**
* Ignored by React.
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
+22
View File
@@ -0,0 +1,22 @@
diff --git a/node_modules/react-blurhash/dist/index.d.ts b/node_modules/react-blurhash/dist/index.d.ts
index 3adbd0a..32e8c13 100644
--- a/node_modules/react-blurhash/dist/index.d.ts
+++ b/node_modules/react-blurhash/dist/index.d.ts
@@ -19,7 +19,7 @@ declare class Blurhash extends React.PureComponent<Props$1> {
resolutionY: number;
};
componentDidUpdate(): void;
- render(): JSX.Element;
+ render(): React.JSX.Element;
}
declare type Props = React.CanvasHTMLAttributes<HTMLCanvasElement> & {
@@ -37,7 +37,7 @@ declare class BlurhashCanvas extends React.PureComponent<Props> {
componentDidUpdate(): void;
handleRef: (canvas: HTMLCanvasElement) => void;
draw: () => void;
- render(): JSX.Element;
+ render(): React.JSX.Element;
}
export { Blurhash, BlurhashCanvas };
+15 -3
View File
@@ -23,7 +23,13 @@ test.describe("Encryption state after registration", () => {
test("Key backup is enabled by default", async ({ page, mailpitClient, app }, testInfo) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(
page,
mailpitClient,
`alice_${testInfo.testId}`,
`alice_${testInfo.testId}@email.com`,
"Pa$sW0rD!",
);
// Wait for the ui to load
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
@@ -35,7 +41,13 @@ test.describe("Encryption state after registration", () => {
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(
page,
mailpitClient,
`alice_${testInfo.testId}`,
`alice_${testInfo.testId}@email.com`,
"Pa$sW0rD!",
);
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
@@ -64,7 +76,7 @@ test.describe("Key backup reset from elsewhere", () => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
-108
View File
@@ -1,108 +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 } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog } from "./utils.ts";
async function expectBackupVersionToBe(page: Page, version: string) {
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
);
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
}
test.describe("Backups", () => {
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
test.use({
displayName: "Hanako",
});
test(
"Create, delete and recreate a keys backup",
{ tag: "@no-webkit" },
async ({ page, user, app }, workerInfo) => {
// Create a backup
const securityTab = await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
const securityKey = await completeCreateSecretStorageDialog(page);
// Open the settings again
await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
// expand the advanced section to see the active version in the reports
await page
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
.locator("..")
.click();
await expectBackupVersionToBe(page, "1");
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
const currentDialogLocator = page.locator(".mx_Dialog");
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Delete it
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
// Create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// Should be successful
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
// Open the settings again
await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
// expand the advanced section to see the active version in the reports
await page
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
.locator("..")
.click();
await expectBackupVersionToBe(page, "2");
// ==
// Ensure that if you don't have the secret storage passphrase the backup won't be created
// ==
// First delete version 2
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Click "Delete Backup"
await currentDialogLocator.getByTestId("dialog-primary-button").click();
// Try to create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
// check that it failed
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
// cancel
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
// go back to the settings to check that no backup was created (the setup button should still be there)
await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
},
);
});
+38 -94
View File
@@ -8,14 +8,7 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test";
import { expect, test } from "../../element-web-test";
import {
autoJoin,
completeCreateSecretStorageDialog,
copyAndContinue,
createSharedRoomWithUser,
enableKeyBackup,
verify,
} from "./utils";
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
import { type Bot } from "../../pages/bot";
import { type ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite";
@@ -84,86 +77,43 @@ test.describe("Cryptography", function () {
},
});
for (const isDeviceVerified of [true, false]) {
test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => {
/**
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
* @param keyType
*/
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
keyType,
);
expect(accountData.encrypted).toBeDefined();
const keys = Object.keys(accountData.encrypted);
const key = accountData.encrypted[keys[0]];
expect(key.ciphertext).toBeDefined();
expect(key.iv).toBeDefined();
expect(key.mac).toBeDefined();
}
/**
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
* @param keyType
*/
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
keyType,
);
test("by recovery code", async ({ page, app, user: aliceCredentials }) => {
// Verified the device
if (isDeviceVerified) {
await app.client.bootstrapCrossSigning(aliceCredentials);
}
await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => {
// We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness
await new Promise((resolve) => setTimeout(resolve, 500));
await route.continue();
});
await app.settings.openUserSettings("Security & Privacy");
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
await completeCreateSecretStorageDialog(page);
// Verify that the SSSS keys are in the account data stored in the server
await verifyKey(app, "master");
await verifyKey(app, "self_signing");
await verifyKey(app, "user_signing");
});
test("by passphrase", async ({ page, app, user: aliceCredentials }) => {
// Verified the device
if (isDeviceVerified) {
await app.client.bootstrapCrossSigning(aliceCredentials);
}
await app.settings.openUserSettings("Security & Privacy");
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
const dialog = page.locator(".mx_Dialog");
// Select passphrase option
await dialog.getByText("Enter a Security Phrase").click();
await dialog.getByRole("button", { name: "Continue" }).click();
// Fill passphrase input
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
// Confirm passphrase
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
await copyAndContinue(page);
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
await dialog.getByRole("button", { name: "Done" }).click();
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
// Verify that the SSSS keys are in the account data stored in the server
await verifyKey(app, "master");
await verifyKey(app, "self_signing");
await verifyKey(app, "user_signing");
});
});
expect(accountData.encrypted).toBeDefined();
const keys = Object.keys(accountData.encrypted);
const key = accountData.encrypted[keys[0]];
expect(key.ciphertext).toBeDefined();
expect(key.iv).toBeDefined();
expect(key.mac).toBeDefined();
}
test("Setting up key backup by recovery key", async ({ page, app, user: aliceCredentials }) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
await enableKeyBackup(app);
// Wait for the cross signing keys to be uploaded
// Waiting for "Change the recovery key" button ensure that all the secrets are uploaded and cached locally
const encryptionTab = await app.settings.openUserSettings("Encryption");
await expect(encryptionTab.getByRole("button", { name: "Change recovery key" })).toBeVisible();
// Verify that the SSSS keys are in the account data stored in the server
await verifyKey(app, "master");
await verifyKey(app, "self_signing");
await verifyKey(app, "user_signing");
});
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
const secretStorageKey = await enableKeyBackup(app);
await enableKeyBackup(app);
// Fetch the current cross-signing keys
async function fetchMasterKey() {
@@ -177,18 +127,15 @@ test.describe("Cryptography", function () {
return k;
});
}
const masterKey1 = await fetchMasterKey();
// Find the "reset cross signing" button, and click it
await app.settings.openUserSettings("Security & Privacy");
await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click();
// Find "the Reset cryptographic identity" button
const encryptionTab = await app.settings.openUserSettings("Encryption");
await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click();
// Confirm
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
// Enter the 4S key
await page.getByPlaceholder("Recovery Key").fill(secretStorageKey);
await page.getByRole("button", { name: "Continue" }).click();
await encryptionTab.getByRole("button", { name: "Continue" }).click();
// Enter the password
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
@@ -198,9 +145,6 @@ test.describe("Cryptography", function () {
const masterKey2 = await fetchMasterKey();
expect(masterKey1).not.toEqual(masterKey2);
}).toPass();
// The dialog should have gone away
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
});
test(
+28 -12
View File
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
import { createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -28,21 +28,27 @@ test.describe("Dehydration", () => {
test.skip(isDendrite, "does not yet support dehydration v2");
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
// Verify the device by resetting the key (which will create SSSS, and dehydrated device)
// Verify the device by resetting the identity key, and then set up recovery (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
await app.closeDialog();
// Verify the device by resetting the key
// Reset the identity key
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Copy" }).click();
// Set up recovery
await page.getByRole("button", { name: "Set up recovery" }).click();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).click();
const recoveryKey = await page.getByTestId("recoveryKey").innerText();
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("textbox").fill(recoveryKey);
await page.getByRole("button", { name: "Finish set up" }).click();
await page.getByRole("button", { name: "Close" }).click();
await expectDehydratedDeviceEnabled(app);
@@ -80,7 +86,7 @@ test.describe("Dehydration", () => {
await expectDehydratedDeviceEnabled(app);
});
test("Reset recovery key during login re-creates dehydrated device", async ({
test("Reset identity during login and set up recovery re-creates dehydrated device", async ({
page,
homeserver,
app,
@@ -99,16 +105,26 @@ test.describe("Dehydration", () => {
// Log in our client
await logIntoElement(page, credentials);
// Oh no, we forgot our recovery key
// Oh no, we forgot our recovery key - reset our identity
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
).toBeVisible();
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Continue" }).click();
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
// And set up recovery
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Set up recovery" }).click();
await settings.getByRole("button", { name: "Continue" }).click();
const recoveryKey = await settings.getByTestId("recoveryKey").innerText();
await settings.getByRole("button", { name: "Continue" }).click();
await settings.getByRole("textbox").fill(recoveryKey);
await settings.getByRole("button", { name: "Finish set up" }).click();
// There should be a brand new dehydrated device
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
expect(dehydratedDeviceIds.length).toBe(1);
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
await expectDehydratedDeviceEnabled(app);
});
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {
@@ -22,6 +22,7 @@ import {
} 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" }, () => {
let aliceBotClient: Bot;
@@ -163,39 +164,44 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
// Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
// Fill the passphrase
const dialog = page.locator(".mx_Dialog");
await dialog.locator("input").fill("new passphrase");
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
await enterRecoveryKeyAndCheckVerified(page, app, "new passphrase");
});
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;
await logIntoElement(page, credentials);
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
});
test("Verify device with Recovery Key from settings", async ({ page, app, credentials }) => {
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;
await logIntoElement(page, credentials);
// Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
/* Dismiss "Verify this device" */
const authPage = page.locator(".mx_AuthPage");
await authPage.getByRole("button", { name: "Skip verification for now" }).click();
await authPage.getByRole("button", { name: "I'll verify later" }).click();
await page.waitForSelector(".mx_MatrixChat");
// Fill the recovery key
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
});
/** Helper for the three tests above which verify by recovery key */
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
await page.getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
// Enter the recovery key
const dialog = page.locator(".mx_Dialog");
await dialog.getByRole("button", { name: "use your Recovery Key" }).click();
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
// (cf https://github.com/element-hq/element-web/issues/30089)
await dialog.locator("textarea").pressSequentially(recoveryKey);
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
await page.getByRole("button", { name: "Done" }).click();
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
@@ -203,7 +209,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
});
}
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
await logIntoElement(page, credentials);
+3 -2
View File
@@ -67,8 +67,9 @@ test.describe("Cryptography", function () {
// Bob has a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list
await page.getByRole("button", { name: "Not now" }).click();
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
await page.getByRole("button", { name: "Dismiss" }).click();
await page.getByRole("button", { name: "Yes, dismiss" }).click();
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
algorithm: "m.megolm.v1.aes-sha2",
+113 -1
View File
@@ -8,7 +8,8 @@
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "../../element-web-test";
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
import { type Bot } from "../../pages/bot";
test.describe("Key storage out of sync toast", () => {
let recoveryKey: GeneratedSecretStorageKey;
@@ -53,3 +54,114 @@ test.describe("Key storage out of sync toast", () => {
).toBeVisible();
});
});
test.describe("'Turn on key storage' toast", () => {
let botClient: Bot | undefined;
test.beforeEach(async ({ page, homeserver, credentials, toasts }) => {
// Set up all crypto stuff. Key storage defaults to on.
const res = await createBot(page, homeserver, credentials);
const recoveryKey = res.recoveryKey;
botClient = res.botClient;
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
// We won't be prompted for crypto setup unless we have an e2e room, so make one
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
await page.getByRole("button", { name: "Create room" }).click();
await toasts.rejectToast("Notifications");
});
test("should not show toast if key storage is on", async ({ page, toasts }) => {
// Given the default situation after signing in
// Then no toast is shown (because key storage is on)
await toasts.assertNoToasts();
// When we reload
await page.reload();
// Give the toasts time to appear
await new Promise((resolve) => setTimeout(resolve, 2000));
// Then still no toast is shown
await toasts.assertNoToasts();
});
test("should not show toast if key storage is off because we turned it off", async ({ app, page, toasts }) => {
// Given the backup is disabled because we disabled it
await disableKeyBackup(app);
// Then no toast is shown
await toasts.assertNoToasts();
// When we reload
await page.reload();
// Give the toasts time to appear
await new Promise((resolve) => setTimeout(resolve, 2000));
// Then still no toast is shown
await toasts.assertNoToasts();
});
test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => {
// Given the backup is disabled but we didn't set account data saying that is expected
await disableKeyBackup(app);
await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false });
// Wait for the account data setting to stick
await new Promise((resolve) => setTimeout(resolve, 2000));
// When we enter the app
await page.reload();
// Then the toast is displayed
let toast = await toasts.getToast("Turn on key storage");
// And when we click "Continue"
await toast.getByRole("button", { name: "Continue" }).click();
// Then we see the Encryption settings dialog with an option to turn on key storage
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
// And when we close that
await page.getByRole("button", { name: "Close dialog" }).click();
// Then we see the toast again
toast = await toasts.getToast("Turn on key storage");
// And when we click "Dismiss"
await toast.getByRole("button", { name: "Dismiss" }).click();
// Then we see the "are you sure?" dialog
await expect(
page.getByRole("heading", { name: "Are you sure you want to keep key storage turned off?" }),
).toBeVisible();
// And when we close it by clicking away
await page.getByTestId("dialog-background").click({ force: true, position: { x: 10, y: 10 } });
// Then we see the toast again
toast = await toasts.getToast("Turn on key storage");
// And when we click Dismiss and then "Go to Settings"
await toast.getByRole("button", { name: "Dismiss" }).click();
await page.getByRole("button", { name: "Go to Settings" }).click();
// Then we see Encryption settings again
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
await page.getByRole("button", { name: "Close dialog" }).click();
toast = await toasts.getToast("Turn on key storage");
await toast.getByRole("button", { name: "Dismiss" }).click();
await page.getByRole("button", { name: "Yes, dismiss" }).click();
// Then the toast is gone
await toasts.assertNoToasts();
});
});
+22 -3
View File
@@ -228,8 +228,8 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
await useSecurityKey.click();
}
// Fill in the recovery key
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
await page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click();
}
}
@@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click();
await app.settings.closeDialog();
@@ -316,6 +316,25 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
return recoveryKey;
}
/**
* Open the encryption settings and disable key storage (and recovery)
* Assumes that the current device has been verified
*/
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
const encryptionTab = await app.settings.openUserSettings("Encryption");
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
if (await keyStorageToggle.isChecked()) {
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
// Wait for the update to account data to stick
await new Promise((resolve) => setTimeout(resolve, 2000));
}
await app.settings.closeDialog();
}
/**
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
*
@@ -5,8 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
import { type Visibility } from "matrix-js-sdk/src/matrix";
import { type Locator, type Page } from "@playwright/test";
import { expect, test } from "../../../element-web-test";
import type { Page } from "@playwright/test";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
test.describe("Room list filters and sort", () => {
test.use({
@@ -18,24 +21,134 @@ test.describe("Room list filters and sort", () => {
labsFlags: ["feature_new_room_list"],
});
function getPrimaryFilters(page: Page) {
function getPrimaryFilters(page: Page): Locator {
return page.getByRole("listbox", { name: "Room list filters" });
}
function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
test.beforeEach(async ({ page, app, bot, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test.describe("Room list", () => {
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
test("Tombstoned rooms are not shown even when they receive updates", async ({ page, app, bot }) => {
// This bug shows up with this setting turned on
await app.settings.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, true);
/*
We will first create a room named 'Old Room' and will invite the bot user to this room.
We will also send a simple message in this room.
*/
const oldRoomId = await app.client.createRoom({ name: "Old Room" });
await app.client.inviteUser(oldRoomId, bot.credentials.userId);
await bot.joinRoom(oldRoomId);
const response = await app.client.sendMessage(oldRoomId, "Hello!");
/*
At this point, we haven't done anything interesting.
So we expect 'Old Room' to show up in the room list.
*/
const roomListView = getRoomList(page);
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
await expect(oldRoomTile).toBeVisible();
/*
Now let's tombstone 'Old Room'.
First we create a new room ('New Room') with the predecessor set to the old room..
*/
const newRoomId = await bot.createRoom({
name: "New Room",
creation_content: {
predecessor: {
event_id: response.event_id,
room_id: oldRoomId,
},
},
visibility: "public" as Visibility,
});
/*
Now we can send the tombstone event itself to the 'Old Room'.
*/
await app.client.sendStateEvent(oldRoomId, "m.room.tombstone", {
body: "This room has been replaced",
replacement_room: newRoomId,
});
// Let's join the replaced room.
await app.client.joinRoom(newRoomId);
// We expect 'Old Room' to be hidden from the room list.
await expect(oldRoomTile).not.toBeVisible();
/*
Let's say some user in the 'Old Room' changes their display name.
This will send events to the all the rooms including 'Old Room'.
Nevertheless, the replaced room should not be shown in the room list.
*/
await bot.setDisplayName("MyNewName");
await expect(oldRoomTile).not.toBeVisible();
});
test.describe("Scroll behaviour", () => {
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
page,
app,
}) => {
const createFavouriteRoom = async (name: string) => {
const id = await app.client.createRoom({
name,
});
await app.client.evaluate(async (client, favouriteId) => {
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
}, id);
};
// Create 5 favourite rooms
let i = 0;
for (; i < 5; i++) {
await createFavouriteRoom(`room${i}-fav`);
}
// Create a non-favourite room
await app.client.createRoom({ name: `room-non-fav` });
// Create rest of the favourite rooms
for (; i < 20; i++) {
await createFavouriteRoom(`room${i}-fav`);
}
// Open the non-favourite room
const roomListView = getRoomList(page);
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
await tile.scrollIntoViewIfNeeded();
await tile.click();
// Enable Favourite filter
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(tile).not.toBeVisible();
// Ensure the room list is not scrolled
const isScrolledDown = await page
.getByRole("grid", { name: "Room list" })
.evaluate((e) => e.scrollTop !== 0);
expect(isScrolledDown).toStrictEqual(false);
});
});
test.describe("Room list", () => {
let unReadDmId: string | undefined;
let unReadRoomId: string | undefined;
@@ -59,6 +172,38 @@ test.describe("Room list filters and sort", () => {
await app.client.evaluate(async (client, favouriteId) => {
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
}, favouriteId);
const lowPrioId = await app.client.createRoom({ name: "Low prio room" });
await app.client.evaluate(async (client, id) => {
await client.setRoomTag(id, "m.lowpriority", { order: 0.5 });
}, lowPrioId);
await bot.createRoom({
name: "invited room",
invite: [user.userId],
is_direct: true,
});
const mentionRoomId = await app.client.createRoom({ name: "room with mention" });
await app.client.inviteUser(mentionRoomId, bot.credentials.userId);
await bot.joinRoom(mentionRoomId);
const clientBot = await bot.prepareClient();
await clientBot.evaluate(
async (client, { mentionRoomId, userId }) => {
await client.sendMessage(mentionRoomId, {
// @ts-ignore ignore usage of MsgType.text
"msgtype": "m.text",
"body": "User",
"format": "org.matrix.custom.html",
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
"m.mentions": {
user_ids: [userId],
},
});
},
{ mentionRoomId, userId: user.userId },
);
});
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
@@ -75,7 +220,7 @@ test.describe("Room list filters and sort", () => {
// only one room should be visible
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2);
expect(await roomList.locator("role=gridcell").count()).toBe(4);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
@@ -84,37 +229,71 @@ test.describe("Room list filters and sort", () => {
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2);
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(3);
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(5);
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Invites" }).click();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
});
test("unread filter should only match unread rooms that have a count", async ({ page, app, bot }) => {
test(
"unread filter should only match unread rooms that have a count",
{ tag: "@screenshot" },
async ({ page, app, bot }) => {
const roomListView = getRoomList(page);
// Let's configure unread dm room so that we only get notification for mentions and keywords
await app.viewRoomById(unReadDmId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("@mentions & keywords").click();
await app.settings.closeDialog();
// Let's open a room other than unread room or unread dm
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
// Let's make the bot send a new message in both rooms
await bot.sendMessage(unReadDmId, "Hello!");
await bot.sendMessage(unReadRoomId, "Hello!");
// Let's activate the unread filter now
await page.getByRole("option", { name: "Unread" }).click();
// Unread filter should only show unread room and not unread dm!
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
await expect(unreadDm).toBeVisible();
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
},
);
test("should sort the room list alphabetically", async ({ page }) => {
const roomListView = getRoomList(page);
// Let's configure unread dm room so that we only get notification for mentions and keywords
await app.viewRoomById(unReadDmId);
await app.settings.openRoomSettings("Notifications");
await page.getByText("@mentions & keywords").click();
await app.settings.closeDialog();
await getRoomOptionsMenu(page).click();
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
// Let's open a room other than unread room or unread dm
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
});
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
const roomListView = getRoomList(page);
// Let's make the bot send a new message in both rooms
await bot.sendMessage(unReadDmId, "Hello!");
await bot.sendMessage(unReadRoomId, "Hello!");
// Let's activate the unread filter now
await page.getByRole("option", { name: "Unread" }).click();
// Unread filter should only show unread room and not unread dm!
await expect(roomListView.getByRole("gridcell", { name: "Open room unread room" })).toBeVisible();
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
});
});
@@ -137,15 +316,25 @@ test.describe("Room list filters and sort", () => {
},
);
test("should render the placeholder for unread filter", { tag: "@screenshot" }, async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: "Unread" }).click();
[
{ filter: "Unreads", action: "Show all chats" },
{ filter: "Mentions", action: "See all activity" },
{ filter: "Invites", action: "See all activity" },
].forEach(({ filter, action }) => {
test(
`should render the placeholder for ${filter} filter`,
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png");
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
await emptyRoomList.getByRole("button", { name: action }).click();
await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
},
);
});
["People", "Rooms", "Favourite"].forEach((filter) => {
@@ -30,6 +30,9 @@ test.describe("Room list panel", () => {
for (let i = 0; i < 20; i++) {
await app.client.createRoom({ name: `room${i}` });
}
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
});
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
@@ -38,4 +41,10 @@ test.describe("Room list panel", () => {
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
});
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
await page.setViewportSize({ width: 575, height: 600 });
const roomListPanel = page.getByTestId("room-list-panel");
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
});
});
@@ -7,7 +7,7 @@
import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
import { expect, test } from "../../../element-web-test";
test.describe("Room list", () => {
test.use({
@@ -29,6 +29,9 @@ test.describe("Room list", () => {
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("Room list", () => {
@@ -43,7 +46,8 @@ test.describe("Room list", () => {
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
await roomListView.hover();
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
@@ -85,9 +89,49 @@ test.describe("Room list", () => {
await expect(roomItem).not.toBeVisible();
});
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
await roomItem.hover();
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
await roomItemMenu.click();
// Default settings should be selected
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
"aria-selected",
"true",
);
await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");
// It should make the room muted
await page.getByRole("menuitem", { name: "Mute room" }).click();
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
// The room decoration should have the muted icon
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
await roomItem.hover();
// On hover, the room should show the muted icon
await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");
roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
await roomItemMenu.click();
// The Mute room option should be selected
await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
});
test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.hover();
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
@@ -100,6 +144,154 @@ test.describe("Room list", () => {
await filters.getByRole("option", { name: "People" }).click();
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
});
test.describe("Shortcuts", () => {
test("should select the next room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await page.keyboard.press("Alt+ArrowDown");
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
});
test("should select the previous room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
await page.keyboard.press("Alt+ArrowUp");
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
test("should select the last room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await page.keyboard.press("Alt+ArrowUp");
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
});
test("should select the next unread room", async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "1 notification" });
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
await page.keyboard.press("Alt+Shift+ArrowDown");
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
});
});
test.describe("Keyboard navigation", () => {
test("should navigate to the room list", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
// open the room
await room29.click();
// put focus back on the room list item
await room29.click();
await expect(room29).toBeFocused();
await page.keyboard.press("ArrowDown");
await expect(room28).toBeFocused();
await expect(room29).not.toBeFocused();
await page.keyboard.press("ArrowUp");
await expect(room29).toBeFocused();
await expect(room28).not.toBeFocused();
});
test("should navigate to the notification menu", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
const moreButton = room29.getByRole("button", { name: "More options" });
const notificationButton = room29.getByRole("button", { name: "Notification options" });
await room29.click();
// put focus back on the room list item
await room29.click();
await page.keyboard.press("Tab");
await expect(moreButton).toBeFocused();
await page.keyboard.press("Tab");
await expect(notificationButton).toBeFocused();
// Open the menu
await notificationButton.click();
// Wait for the menu to be open
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
"aria-selected",
"true",
);
// Close the menu
await page.keyboard.press("Escape");
// Focus should be back on the room list item
await expect(room29).toBeFocused();
});
});
});
test.describe("Avatar decoration", () => {
test.use({ labsFlags: ["feature_video_rooms", "feature_new_room_list"] });
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
// @ts-ignore Visibility enum is not accessible
await app.client.createRoom({ name: "public room", visibility: "public" });
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
const roomListView = getRoomList(page);
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
await expect(publicRoom).toBeVisible();
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
});
test("should be a low priority room", { tag: "@screenshot" }, async ({ page, app, user }) => {
// @ts-ignore Visibility enum is not accessible
await app.client.createRoom({ name: "low priority room", visibility: "public" });
const roomListView = getRoomList(page);
const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
// Make room low priority
await publicRoom.hover();
const roomItemMenu = publicRoom.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();
// Should have low priority decoration
await expect(publicRoom.locator(".mx_RoomAvatarView_icon")).toHaveAccessibleName(
"This is a low priority room",
);
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await expect(publicRoom).toMatchScreenshot("room-list-item-low-priority.png");
});
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("video room");
await page.getByRole("button", { name: "Create video room" }).click();
const roomListView = getRoomList(page);
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
// 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");
});
});
test.describe("Notification decoration", () => {
@@ -162,6 +354,27 @@ test.describe("Room list", () => {
await expect(room).toMatchScreenshot("room-list-item-mention.png");
});
test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
await app.settings.openUserSettings("Preferences");
await page.getByRole("switch", { name: "Show message previews" }).click();
await app.closeDialog();
const roomListView = getRoomList(page);
const roomId = await app.client.createRoom({ name: "activity" });
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await app.client.inviteUser(roomId, bot.credentials.userId);
await bot.joinRoom(roomId);
await bot.sendMessage(roomId, "I am a robot. Beep.");
const room = roomListView.getByRole("gridcell", { name: "activity" });
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
});
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
const roomListView = getRoomList(page);
@@ -201,8 +414,8 @@ test.describe("Room list", () => {
await room.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "mark as unread" }).click();
// Remove hover on the room list item
await roomListView.hover();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
});
@@ -288,6 +288,43 @@ test.describe("Login", () => {
await expect(h1).toBeVisible();
});
});
test("Can reset identity to become verified", async ({ page, homeserver, request, credentials }) => {
// Log in
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
});
if (!res.ok()) {
console.log(`Uploading dummy keys failed with HTTP status ${res.status}`, await res.json());
throw new Error("Uploading dummy keys failed");
}
await page.goto("/");
await login(page, homeserver, credentials);
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
// Start the reset process
await page.getByRole("button", { name: "Proceed with reset" }).click();
// First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
// Then click outside the dialog and restart
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
await page.getByRole("button", { name: "Proceed with reset" }).click();
// Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Continue" }).click();
// We end up at the Home screen
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome Dave", exact: true })).toBeVisible();
});
});
});
+27 -6
View File
@@ -15,6 +15,7 @@ test.describe("Module loading", () => {
test.describe("Example Module", () => {
test.use({
config: {
brand: "TestBrand",
modules: ["/modules/example-module.js"],
},
page: async ({ page }, use) => {
@@ -25,11 +26,31 @@ test.describe("Module loading", () => {
},
});
test("should show alert", async ({ page }) => {
const dialogPromise = page.waitForEvent("dialog");
await page.goto("/");
const dialog = await dialogPromise;
expect(dialog.message()).toBe("Testing module loading successful!");
});
const testCases = [
["en", "TestBrand module loading successful!"],
["de", "TestBrand-Module erfolgreich geladen!"],
];
for (const [lang, message] of testCases) {
test.describe(`language-${lang}`, () => {
test.use({
config: async ({ config }, use) => {
await use({
...config,
setting_defaults: {
language: lang,
},
});
},
});
test("should show alert", async ({ page }) => {
const dialogPromise = page.waitForEvent("dialog");
await page.goto("/");
const dialog = await dialogPromise;
expect(dialog.message()).toBe(message);
});
});
}
});
});
+2
View File
@@ -37,6 +37,8 @@ export async function registerAccountMas(
await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("textbox", { name: "Display Name" }).fill(username);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
}
+3 -4
View File
@@ -33,7 +33,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
@@ -55,7 +55,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const newPage = await newPagePromise;
await newPage.getByText("Devices").click();
await newPage.getByText(deviceId).click();
await expect(newPage.getByText("Element")).toBeVisible();
await expect(newPage.getByText("Element", { exact: true })).toBeVisible();
await expect(newPage.getByText("http://localhost:8080/")).toBeVisible();
await expect(newPage).toHaveURL(/\/oauth2_session/);
await newPage.close();
@@ -83,12 +83,11 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
await page.goto("about:blank");
// @ts-expect-error
const result = await mas.manage("kill-sessions", userId);
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
@@ -43,6 +43,7 @@ test.describe("Pills", () => {
// go back to the message room and try to click on the pill text, as a user would
await app.viewRoomByName(messageRoom);
await expect(page).toHaveURL(new RegExp(`/#/room/${messageRoomId}`));
const pillText = page.locator(".mx_EventTile_body .mx_Pill .mx_Pill_text");
await expect(pillText).toHaveCSS("pointer-events", "none");
await pillText.click({ force: true }); // force is to ensure we bypass pointer-events
@@ -15,20 +15,34 @@ test.describe("Release announcement", () => {
feature_release_announcement: true,
},
},
labsFlags: ["threadsActivityCentre"],
room: async ({ app, user }, use) => {
const roomId = await app.client.createRoom({
name: "Test room",
});
await app.viewRoomById(roomId);
await use({ roomId });
},
});
test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => {
// The TAC release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre");
// Hide the release announcement
await util.markReleaseAnnouncementAsRead("Threads Activity Centre");
await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre");
test(
"should display the pinned messages release announcement",
{ tag: "@screenshot" },
async ({ page, app, room, util }) => {
await app.toggleRoomInfoPanel();
await page.reload();
// Wait for EW to load
await expect(page.getByRole("navigation", { name: "Spaces" })).toBeVisible();
// Check that once the release announcement has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible("Threads Activity Centre");
});
const name = "All new pinned messages";
// The release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible(name);
// Hide the release announcement
await util.markReleaseAnnouncementAsRead(name);
await util.assertReleaseAnnouncementIsNotVisible(name);
await page.reload();
await app.toggleRoomInfoPanel();
await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
// Check that once the release announcement has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible(name);
},
);
});
+59 -5
View File
@@ -11,6 +11,7 @@ import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { Bot } from "../../pages/bot";
const ROOM_NAME = "Test room";
const ROOM_NAME_LONG =
@@ -21,20 +22,23 @@ const ROOM_NAME_LONG =
"officia deserunt mollit anim id est laborum.";
const SPACE_NAME = "Test space";
const NAME = "Alice";
const LONG_NAME = "Bob long long long long long long long long long long long long long long long name";
const ROOM_ADDRESS_LONG =
"loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
}
test.describe("RightPanel", () => {
let testRoomId: string;
test.use({
displayName: NAME,
});
test.beforeEach(async ({ app, user }) => {
await app.client.createRoom({ name: ROOM_NAME });
testRoomId = await app.client.createRoom({ name: ROOM_NAME });
await app.client.createSpace({ name: SPACE_NAME });
});
@@ -77,10 +81,12 @@ test.describe("RightPanel", () => {
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png");
});
test("should handle clicking add widgets", async ({ page, app }) => {
test("should handle clicking add widgets", { tag: "@screenshot" }, async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Extensions" }).click();
await expect(page.getByTestId("right-panel")).toMatchScreenshot("with-extensions.png");
await page.getByRole("button", { name: "Add extensions" }).click();
await expect(page.locator(".mx_IntegrationManager")).toBeVisible();
});
@@ -134,15 +140,63 @@ test.describe("RightPanel", () => {
await page.getByLabel("Room info").nth(1).click();
await checkRoomSummaryCard(page, ROOM_NAME);
});
test(
"should handle viewing long room member name",
{ tag: "@screenshot" },
async ({ page, homeserver, app }) => {
const bobLongName = new Bot(page, homeserver, { displayName: LONG_NAME });
await bobLongName.prepareClient();
await app.client.inviteUser(testRoomId, bobLongName.credentials.userId);
await bobLongName.joinRoom(testRoomId);
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
await expect(page.locator(".mx_MemberListView")).toBeVisible();
await getMemberTileByName(page, LONG_NAME).click();
await expect(page.locator(".mx_UserInfo")).toBeVisible();
await expect(page.locator(".mx_UserInfo_profile").getByText(LONG_NAME)).toBeVisible();
await expect(page.locator(".mx_UserInfo")).toMatchScreenshot("with-long-name.png", {
mask: [page.locator(".mx_UserInfo_profile_mxid")],
css: `
/* Use monospace font for consistent mask width */
.mx_UserInfo_profile_mxid {
font-family: Inconsolata !important;
}
`,
});
},
);
test.describe("room reporting", () => {
test.skip(isDendrite, "Dendrite does not implement room reporting");
test("should handle reporting a room", async ({ page, app }) => {
test("should handle reporting a room", { tag: "@screenshot" }, async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Report room" }).click();
const dialog = await page.getByRole("dialog", { name: "Report Room" });
await dialog.getByLabel("reason").fill("This room should be reported");
await expect(dialog).toMatchScreenshot("room-report-dialog.png");
await dialog.getByRole("button", { name: "Send report" }).click();
await expect(page.getByText("Your report was sent.")).toBeVisible();
// Dialog should have gone
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
});
test("should handle reporting a room and leaving the room", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.getByRole("menuitem", { name: "Report room" }).click();
const dialog = await page.getByRole("dialog", { name: "Report room" });
await dialog.getByRole("switch", { name: "Leave room" }).click();
await dialog.getByLabel("reason").fill("This room should be reported");
await dialog.getByRole("button", { name: "Send report" }).click();
await page.getByRole("dialog", { name: "Leave room" }).getByRole("button", { name: "Leave" }).click();
// Dialog should have gone
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
});
});
});
+74
View File
@@ -0,0 +1,74 @@
/*
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 { test, expect } from "../../element-web-test";
test.describe("Invites", () => {
test.use({
displayName: "Alice",
botCreateOpts: {
displayName: "Bob",
},
});
test("should render an invite view", { tag: "@screenshot" }, async ({ page, homeserver, user, bot, app }) => {
const roomId = await bot.createRoom({ is_direct: true });
await bot.inviteUser(roomId, user.userId);
await app.viewRoomByName("Bob");
await expect(page.locator(".mx_RoomView")).toMatchScreenshot("Invites_room_view.png", {
// Hide the mxid, which is not stable.
css: `
.mx_RoomPreviewBar_inviter_mxid {
display: none !important;
}
`,
});
});
test("should be able to decline an invite", async ({ page, homeserver, user, bot, app }) => {
const roomId = await bot.createRoom({ is_direct: true });
await bot.inviteUser(roomId, user.userId);
await app.viewRoomByName("Bob");
await page.getByRole("button", { name: "Decline", exact: true }).click();
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Bob", exact: true }),
).not.toBeVisible();
});
test(
"should be able to decline an invite, report the room and ignore the user",
{ tag: "@screenshot" },
async ({ page, homeserver, user, bot, app }) => {
const roomId = await bot.createRoom({ is_direct: true });
await bot.inviteUser(roomId, user.userId);
await app.viewRoomByName("Bob");
await page.getByRole("button", { name: "Decline and block" }).click();
await page.getByLabel("Ignore user").click();
await page.getByLabel("Report room").click();
await page.getByLabel("Reason").fill("Do not want the room");
const roomReported = page.waitForRequest(
(req) =>
req.url().endsWith(`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/report`) &&
req.method() === "POST",
);
await expect(page.getByRole("dialog", { name: "Decline invitation" })).toMatchScreenshot(
"Invites_reject_dialog.png",
);
await page.getByRole("button", { name: "Decline invite" }).click();
// Check room was reported.
await roomReported;
// Check user is ignored.
await app.settings.openUserSettings("Security & Privacy");
const ignoredUsersList = page.getByRole("list", { name: "Ignored users" });
await ignoredUsersList.scrollIntoViewIfNeeded();
await expect(ignoredUsersList.getByRole("listitem", { name: bot.credentials.userId })).toBeVisible();
},
);
});
@@ -19,126 +19,162 @@ import {
test.describe("Encryption tab", () => {
test.use({ displayName: "Alice" });
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.describe("when encryption is set up", () => {
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => {
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion;
});
test.beforeEach(async ({ page, homeserver, credentials }) => {
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion;
});
test(
"should show a 'Verify this device' button if the device is unverified",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
const content = util.getEncryptionTabContent();
test(
"should show a 'Verify this device' button if the device is unverified",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
const content = util.getEncryptionTabContent();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();
await util.verifyDevice(recoveryKey);
await util.verifyDevice(recoveryKey);
await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
});
await expect(content).toMatchScreenshot("default-tab.png", {
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
});
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
//
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
// We simulate this case by deleting the cached secrets in the indexedDB.
test(
"should prompt to enter the recovery key when the secrets are not cached locally",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
//
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
// We simulate this case by deleting the cached secrets in the indexedDB.
test(
"should prompt to enter the recovery key when the secrets are not cached locally",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets
await deleteCachedSecrets(page);
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-tab.png", {
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
});
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
page,
app,
util,
}) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets
await deleteCachedSecrets(page);
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-tab.png", {
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
});
// The user is prompted to reset their identity
await expect(
dialog.getByText("Forgot your recovery key? You’ll need to reset your identity."),
).toBeVisible();
});
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
},
);
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
page,
app,
util,
}) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
// We need to delete the cached secrets
await deleteCachedSecrets(page);
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
await util.openEncryptionTab();
const dialog = util.getEncryptionTabContent();
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
// The user is prompted to reset their identity
await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible();
const deleteRequestPromises = [
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
];
await page.getByRole("button", { name: "Delete key storage" }).click();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;
expect(request.method()).toBe("PUT");
expect(request.postData()).toBe(JSON.stringify({}));
}
});
});
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
test.describe("when encryption is not set up", () => {
test("'Verify this device' allows us to become verified", async ({
page,
user,
credentials,
app,
}, workerInfo) => {
const settings = await app.settings.openUserSettings("Encryption");
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
// Initially, our device is not verified
await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible();
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();
// We will reset our identity
await settings.getByRole("button", { name: "Verify this device" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
// First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
const deleteRequestPromises = [
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
];
// Then click outside the dialog and restart
await page.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
await page.getByRole("button", { name: "Proceed with reset" }).click();
await page.getByRole("button", { name: "Delete key storage" }).click();
// Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;
expect(request.method()).toBe("PUT");
expect(request.postData()).toBe(JSON.stringify({}));
}
// Now we are verified, so we see the Key storage toggle
await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible();
});
});
});
@@ -21,10 +21,12 @@ test.describe("Preferences user settings tab", () => {
const locator = await app.settings.openUserSettings("Preferences");
await use(locator);
},
// display message preview settings
labsFlags: ["feature_new_room_list"],
});
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
await page.setViewportSize({ width: 1024, height: 3300 });
await page.setViewportSize({ width: 1024, height: 4000 });
const tab = await app.settings.openUserSettings("Preferences");
// Assert that the top heading is rendered
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
@@ -37,6 +37,15 @@ test.describe("Roles & Permissions room settings tab", () => {
// Change the role of Alice to Moderator (50)
await combobox.selectOption("Moderator");
await expect(combobox).toHaveValue("50");
// Should display a modal to warn that we are demoting the only admin user
const modal = await page.locator(".mx_Dialog", {
hasText: "Warning",
});
await expect(modal).toBeVisible();
// Click on the continue button in the modal
await modal.getByRole("button", { name: "Continue" }).click();
const respPromise = page.waitForRequest("**/state/**");
await applyButton.click();
await respPromise;
@@ -0,0 +1,34 @@
/*
* 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 { test, expect } from "../../element-web-test";
test.describe("share from URL", () => {
test.use({
displayName: "Alice",
room: async ({ app }, use) => {
const roomId = await app.client.createRoom({ name: "A test room" });
await use({ roomId });
},
});
test("should share message when users navigates to share URL", async ({ page, user, room, app }) => {
await page.goto("/#/share?msg=Hello+world");
// The forward message dialog doesn't update as new infomation arrives via sync, which means sometimes
// this is just says, "Empty room". For the same reason, we can't reliably write a test for loading the
// app straight away with a /#/share url as the room doesn't appear until the client syncs.]
// Ideally we should fix the forward dialog to update and eliminate races, until then, there is only one
// room so we click the first button.
await page.getByRole("listitem" /*, { name: "A test room" }*/).getByRole("button", { name: "Send" }).click();
await page.keyboard.press("Escape");
await app.viewRoomByName("A test room");
const lastMessage = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
await expect(lastMessage).toBeVisible();
const lastMessageText = await lastMessage.locator(".mx_EventTile_body").innerText();
await expect(lastMessageText).toBe("Hello world");
});
});
@@ -255,8 +255,8 @@ test.describe("Sliding Sync", () => {
// Select the room to reject
await page.getByRole("treeitem", { name: "Room to Reject" }).click();
// Reject the invite
await page.locator(".mx_RoomView").getByRole("button", { name: "Reject", exact: true }).click();
// Decline the invite
await page.locator(".mx_RoomView").getByRole("button", { name: "Decline", exact: true }).click();
await expect(
page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
@@ -19,7 +19,6 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test.use({
displayName: "Alice",
botCreateOpts: { displayName: "Other User" },
labsFlags: ["threadsActivityCentre"],
});
test(
@@ -0,0 +1,139 @@
/*
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 * as fs from "node:fs";
import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types";
import { test, expect } from "../../element-web-test";
const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png");
test.describe("Media preview settings", () => {
test.use({
displayName: "Alan",
botCreateOpts: {
displayName: "Bob",
},
room: async ({ app, page, homeserver, bot, user }, use) => {
const mxc = (await bot.uploadContent(MEDIA_FILE, { name: "image.png", type: "image/png" })).content_uri;
const roomId = await bot.createRoom({
name: "Test room",
invite: [user.userId],
initial_state: [{ type: "m.room.avatar", content: { url: mxc }, state_key: "" }],
});
await bot.sendEvent(roomId, null, "m.room.message" as EventType, {
msgtype: "m.image" as MsgType,
body: "image.png",
url: mxc,
});
await use({ roomId });
},
});
test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
let settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Hide avatars of room and inviter").click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await expect(
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
).toMatchScreenshot("invite-no-avatar.png", {
// Hide the mxid, which is not stable.
css: `
.mx_RoomPreviewBar_inviter_mxid {
display: none !important;
}
`,
});
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
).toMatchScreenshot("invite-room-tree-no-avatar.png");
// And then go back to being visible
settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Hide avatars of room and inviter").click();
await app.closeDialog();
await page.goto("#/home");
await app.viewRoomById(room.roomId);
await expect(
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
).toMatchScreenshot("invite-with-avatar.png", {
// Hide the mxid, which is not stable.
css: `
.mx_RoomPreviewBar_inviter_mxid {
display: none !important;
}
`,
});
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
).toMatchScreenshot("invite-room-tree-with-avatar.png");
});
test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
await expect(page.getByText("Show image")).toBeVisible();
});
test("should be able to hide media in non-private rooms globally", async ({ page, app, room, user, bot }) => {
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
join_rule: "public",
});
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByLabel("In private rooms").click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
await expect(page.getByText("Show image")).toBeVisible();
for (const joinRule of ["invite", "knock", "restricted"] as RoomJoinRulesEventContent["join_rule"][]) {
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
join_rule: joinRule,
} satisfies RoomJoinRulesEventContent);
await expect(page.getByText("Show image")).not.toBeVisible();
}
});
test("should be able to show media in rooms globally", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
await expect(page.getByText("Show image")).not.toBeVisible();
});
test("should be able to hide media in an individual room", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
const roomSettings = await app.settings.openRoomSettings("General");
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
await app.closeDialog();
await expect(page.getByText("Show image")).toBeVisible();
});
test("should be able to show media in an individual room", async ({ page, app, room, user }) => {
const settings = await app.settings.openUserSettings("Preferences");
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
await app.closeDialog();
await app.viewRoomById(room.roomId);
await page.getByRole("button", { name: "Accept" }).click();
const roomSettings = await app.settings.openRoomSettings("General");
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
await app.closeDialog();
await expect(page.getByText("Show image")).not.toBeVisible();
});
});
+10 -2
View File
@@ -6,11 +6,19 @@ Please see LICENSE files in the repository root for full details.
*/
export default class ExampleModule {
static moduleApiVersion = "^0.1.0";
static moduleApiVersion = "^1.0.0";
constructor(api) {
this.api = api;
this.api.i18n.register({
key: {
en: "%(brand)s module loading successful!",
de: "%(brand)s-Module erfolgreich geladen!",
},
});
}
async load() {
alert("Testing module loading successful!");
const brand = this.api.config.get("brand");
alert(this.api.i18n.translate("key", { brand }));
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

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