Compare commits

..

12 Commits

Author SHA1 Message Date
David Baker
b6980e5c6f Merge branch 'develop' into dbkr/message_previews 2025-04-22 15:00:58 +01:00
David Baker
cea8673ed2 Snapshots 2025-04-22 13:42:35 +01:00
David Baker
647134967e Type import 2025-04-22 13:20:42 +01:00
David Baker
16856ed0a2 Merge branch 'develop' into dbkr/message_previews 2025-04-22 13:15:53 +01:00
David Baker
038662fd5e Fix test
Split into two tests that test setting & updating
2025-04-22 13:04:59 +01:00
David Baker
cd2833e0c2 Fix more snapshots 2025-04-17 17:59:10 +01:00
David Baker
21f66e17b5 Fix test & update snapshot 2025-04-17 17:31:37 +01:00
David Baker
7b30f79877 Unused imports 2025-04-17 16:28:42 +01:00
David Baker
0768dc3487 Unused i18n keys 2025-04-17 16:20:28 +01:00
David Baker
e6e5c3e4a7 Use new compund release 2025-04-17 16:17:48 +01:00
David Baker
889af87c99 Merge remote-tracking branch 'origin/develop' into dbkr/message_previews 2025-04-17 16:02:17 +01:00
David Baker
4ddb1ebf2f Add message preview support to the new room list
* Support showing message previews in the room list items
 * Add the secondary filters bar with the '...' menu, containing
   just the option for message previews for now
 * Change message preview toggle hook to update when setting is updated
2025-04-17 15:51:48 +01:00
671 changed files with 13063 additions and 25096 deletions

View File

@@ -1,11 +1,6 @@
module.exports = {
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: [
"plugin:matrix-org/babel",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:storybook/recommended",
],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
},
@@ -35,10 +30,6 @@ 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.",
@@ -64,11 +55,6 @@ module.exports = {
"error",
{
paths: [
{
name: "react",
importNames: ["forwardRef"],
message: "Use ref props instead.",
},
{
name: "@testing-library/react",
message: "Please use jest-matrix-react instead",

View File

@@ -43,9 +43,9 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@v4
with:
name: webapp-${{ matrix.image }}
path: webapp

View File

@@ -14,7 +14,7 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }}
VERSION: ${{ github.ref_name }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@v4
with:
name: element-web.deb
path: element-web.deb

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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
- uses: actions/upload-artifact@v4
with:
name: webapp
path: dist/develop.tar.gz

View File

@@ -34,7 +34,7 @@ jobs:
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- name: Load GPG key
run: |

View File

@@ -20,19 +20,19 @@ jobs:
env:
TEST_TAG: vectorim/element-web:test
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
with:
install: true
@@ -53,7 +53,7 @@ jobs:
- name: Build and load
id: test-build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
with:
context: .
load: true
@@ -110,7 +110,7 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
if: github.event_name != 'pull_request'
with:
context: .
@@ -139,16 +139,3 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
if: github.event_name != 'pull_request'
with:
repository: element-hq/element-web-pro
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
event-type: image-built
# Stable way to determine the :version
client-payload: |-
{
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
}

View File

@@ -17,23 +17,23 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Fetch element-desktop
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
repository: element-hq/element-desktop
path: element-desktop
- name: Fetch element-web
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
path: element-web
- name: Fetch matrix-js-sdk
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@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@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
uses: peaceiris/actions-mdbook@v2
with:
mdbook-version: "0.4.10"
@@ -88,7 +88,7 @@ jobs:
run: mdbook build
- name: Upload artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
uses: actions/upload-pages-artifact@v3
with:
path: ./book
@@ -104,4 +104,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
uses: actions/deploy-pages@v4

View File

@@ -25,7 +25,7 @@ jobs:
actions: read
steps:
- name: Download HTML report
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@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@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
uses: matrix-org/netlify-pr-preview@v3
with:
path: playwright-report
owner: ${{ github.event.workflow_run.head_repository.owner.login }}

View File

@@ -50,11 +50,11 @@ jobs:
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
repository: element-hq/element-web
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -81,7 +81,7 @@ jobs:
yarn build
- name: Upload Artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@v4
with:
name: webapp
path: webapp
@@ -89,7 +89,7 @@ jobs:
- name: Calculate runner variables
id: runner-vars
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
uses: actions/github-script@v7
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
@@ -129,18 +129,18 @@ jobs:
- runAllTests: false
project: Pinecone
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
with:
persist-credentials: false
repository: element-hq/element-web
- name: 📥 Download artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@v4
with:
name: webapp
path: webapp
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
cache-dependency-path: yarn.lock
@@ -154,11 +154,12 @@ 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@5a3ec84eff668545956fd18022155c47e93e2684 # v4
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
@@ -179,35 +180,25 @@ jobs:
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@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
- downstream-modules
needs: playwright
if: always()
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
if: inputs.skip != true
with:
persist-credentials: false
repository: element-hq/element-web
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
if: inputs.skip != true
with:
cache: "yarn"
@@ -219,7 +210,7 @@ jobs:
- name: Download blob reports from GitHub Actions Artifacts
if: inputs.skip != true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@v4
with:
pattern: all-blob-reports-*
path: all-blob-reports
@@ -227,7 +218,7 @@ jobs:
- name: Merge into HTML Report
if: inputs.skip != true
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports
env:
# Only pass creds to the flaky-reporter on main branch runs
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
@@ -235,11 +226,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@v4
with:
name: html-report
path: playwright-report
retention-days: 14
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
- if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success'
run: exit 1

View File

@@ -10,7 +10,7 @@ jobs:
name: Tidy closed issues
runs-on: ubuntu-24.04
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
name: Close duplicate as Not Planned
if: steps.main.outputs.closeAsNotPlanned
with:

View File

@@ -28,7 +28,7 @@ jobs:
Exercise caution. Use test accounts.
- name: 📥 Download artifact
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
uses: actions/download-artifact@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@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
uses: matrix-org/netlify-pr-preview@v3
with:
path: webapp
owner: ${{ github.event.workflow_run.head_repository.owner.login }}

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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
ROOM_ID: ${{ secrets.ROOM_ID }}

View File

@@ -10,7 +10,7 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- name: Update synapse image
run: |

View File

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

View File

@@ -41,7 +41,7 @@ jobs:
REPOS: matrix-js-sdk element-web element-desktop
steps:
- name: Checkout Element Desktop
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
if: inputs.matrix-js-sdk
with:
repository: matrix-org/matrix-js-sdk

View File

@@ -1,51 +0,0 @@
# Triggers after the shared component tests have finished,
# It uploads the received images and diffs to netlify, printing the URLs to the console
name: Upload Shared Component Visual Test Diffs
on:
workflow_run:
workflows: ["Shared Component Visual Tests"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
permissions: {}
jobs:
report:
if: github.event.workflow_run.conclusion == 'failure'
name: Upload Diffs
runs-on: ubuntu-24.04
environment: Netlify
permissions:
actions: read
deployments: write
steps:
- name: Install tree
run: "sudo apt-get install -y tree"
- name: Download Diffs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: received-images
path: received-images
- name: Generate Index
run: "cd received-images && tree -L 1 --noreport -H '' -o index.html ."
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
with:
path: received-images
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
site_id: ${{ vars.NETLIFY_SITE_ID }}
desc: Shared Component Visual Diffs
deployment_env: SharedComponentDiffs
prefix: "diffs-"

View File

@@ -1,70 +0,0 @@
name: Shared Component Visual Tests
on:
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
permissions: {} # No permissions required
jobs:
testStorybook:
name: "Run Visual Tests"
runs-on: ubuntu-24.04
permissions:
actions: read
issues: read
pull-requests: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
repository: element-hq/element-web
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Get installed Playwright version
id: playwright
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@5a3ec84eff668545956fd18022155c47e93e2684 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "yarn playwright install --with-deps --only-shell"
- name: Build Element Web resources
# Needed to prepare language files
run: "yarn build:res"
- name: Build storybook dependencies
# When the first test is ran, it will fail because the dependencies are not yet built.
# This step is to ensure that the dependencies are built before running the tests.
run: "yarn test:storybook:ci"
continue-on-error: true
- name: Run Visual tests
run: "yarn test:storybook:ci"
- name: Upload received images & diffs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: received-images
path: playwright/shared-component-received

View File

@@ -23,9 +23,9 @@ jobs:
name: "Typescript Syntax Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -57,7 +57,7 @@ jobs:
name: "Rethemendex Check"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- run: ./res/css/rethemendex.sh
@@ -67,9 +67,9 @@ jobs:
name: "ESLint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -85,9 +85,9 @@ jobs:
name: "Style Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"
@@ -103,9 +103,9 @@ jobs:
name: "Workflow Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"

View File

@@ -39,12 +39,12 @@ jobs:
runner: [1, 2]
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
uses: actions/checkout@v4
with:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
- name: Yarn cache
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@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@5a3ec84eff668545956fd18022155c47e93e2684 # v4
uses: actions/cache@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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
uses: actions/upload-artifact@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@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@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@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
uses: actions/add-to-project@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@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
uses: actions/add-to-project@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 }}
crypto:
name: Add labelled issues to Crypto project
verticals_feature:
name: Add labelled issues to Verticals Feature project
runs-on: ubuntu-24.04
if: >
contains(github.event.issue.labels.*.name, 'Team: Crypto')
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/element-hq/projects/76
project-url: https://github.com/orgs/element-hq/projects/57
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
tech_debt:

View File

@@ -12,17 +12,15 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
- uses: actions/stale@v9
with:
operations-per-run: 100
# Flaky test issue closing
any-of-issue-labels: "Z-Flaky-Test-Chrome,Z-Flaky-Test-Firefox,Z-Flaky-Test-Webkit"
only-issue-labels: "Z-Flaky-Test"
days-before-issue-stale: 14
days-before-issue-close: 0
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
exempt-issue-labels: "Z-Flaky-Test-Disabled"
# Stale PR closing
days-before-pr-stale: 180
days-before-pr-close: 0

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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.removeLabel({

View File

@@ -9,9 +9,9 @@ jobs:
update:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: actions/checkout@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: "lts/*"

View File

@@ -22,11 +22,11 @@ jobs:
runs-on: ubuntu-24.04
environment: Matrix
steps:
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
- uses: actions/github-script@v7
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
PUBLIC_ROOM_ID: "!YTvKGNlinIzlkMTVRl:matrix.org"
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
@@ -81,11 +81,6 @@ jobs:
d.body = d.body.replace(regex, releaseTopic);
});
}
if (data["m.topic"]) {
data["m.topic"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
res = await fetch(apiUrl, {
method: "PUT",

4
.gitignore vendored
View File

@@ -25,12 +25,10 @@ electron/pub
.env
/coverage
# Auto-generated file
/src/modules.ts
/src/modules.js
/build_config.yaml
/book
/index.html
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
*storybook.log
storybook-static

View File

@@ -1,28 +0,0 @@
/*
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 { create } from "storybook/theming";
export default create({
base: "light",
// Colors
textColor: "#1b1d22",
colorSecondary: "#111111",
// UI
appBg: "#ffffff",
appContentBg: "#ffffff",
// Toolbar
barBg: "#ffffff",
brandTitle: "Element Web",
brandUrl: "https://github.com/element-hq/element-web",
brandImage: "https://element.io/images/logo-ele-secondary.svg",
brandTarget: "_self",
});

View File

@@ -1,61 +0,0 @@
/*
* 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 { Addon, types, useGlobals } from "storybook/manager-api";
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
import React from "react";
import { GlobeIcon } from "@storybook/icons";
// We can't import `shared/i18n.tsx` directly here.
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
import json from "../webapp/i18n/languages.json";
const languages = Object.keys(json).filter((lang) => lang !== "default");
/**
* Returns the title of a language in the user's locale.
*/
function languageTitle(language: string): string {
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
}
export const languageAddon: Addon = {
title: "Language Selector",
type: types.TOOL,
render: ({ active }) => {
const [globals, updateGlobals] = useGlobals();
const selectedLanguage = globals.language || "en";
return (
<WithTooltip
placement="top"
trigger="click"
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList
links={languages.map((language) => ({
id: language,
title: languageTitle(language),
active: selectedLanguage === language,
onClick: async () => {
// Update the global state with the selected language
updateGlobals({ language });
onHide();
},
}))}
/>
);
}}
>
<IconButton title="Language">
<GlobeIcon />
{languageTitle(selectedLanguage)}
</IconButton>
</WithTooltip>
);
},
};

View File

@@ -1,40 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import type { StorybookConfig } from "@storybook/react-vite";
import path from "node:path";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import { mergeConfig } from "vite";
const config: StorybookConfig = {
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../webapp"],
addons: ["@storybook/addon-docs", "@storybook/addon-designs"],
framework: "@storybook/react-vite",
core: {
disableTelemetry: true,
},
typescript: {
reactDocgen: "react-docgen-typescript",
},
async viteFinal(config) {
return mergeConfig(config, {
resolve: {
alias: {
// Alias used by i18n.tsx
$webapp: path.resolve("webapp"),
},
},
// Needed for counterpart to work
plugins: [nodePolyfills({ include: ["process", "util"] })],
server: {
allowedHosts: ["localhost", ".docker.internal"],
},
});
},
};
export default config;

View File

@@ -1,18 +0,0 @@
/*
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 React from "react";
import { addons } from "storybook/manager-api";
import ElementTheme from "./ElementTheme";
import { languageAddon } from "./languageAddon";
addons.setConfig({
theme: ElementTheme,
});
addons.register("elementhq/language", () => addons.add("language", languageAddon));

View File

@@ -1,10 +0,0 @@
/*
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.
*/
.docs-story {
background: var(--cpd-color-bg-canvas-default);
}

View File

@@ -1,106 +0,0 @@
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
import { addons } from "storybook/preview-api";
import "../res/css/shared.pcss";
import "./preview.css";
import React, { useLayoutEffect } from "react";
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
export const globalTypes = {
theme: {
name: "Theme",
description: "Global theme for components",
toolbar: {
icon: "circlehollow",
title: "Theme",
items: [
{ title: "System", value: "system", icon: "browser" },
{ title: "Light", value: "light", icon: "sun" },
{ title: "Light (high contrast)", value: "light-hc", icon: "sun" },
{ title: "Dark", value: "dark", icon: "moon" },
{ title: "Dark (high contrast)", value: "dark-hc", icon: "moon" },
],
},
},
language: {
name: "Language",
description: "Global language for components",
},
initialGlobals: {
theme: "system",
language: "en",
},
} satisfies ArgTypes;
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
const ThemeSwitcher: React.FC<{
theme: string;
}> = ({ theme }) => {
useLayoutEffect(() => {
document.documentElement.classList.remove(...allThemesClasses);
if (theme !== "system") {
document.documentElement.classList.add(`cpd-theme-${theme}`);
}
return () => document.documentElement.classList.remove(...allThemesClasses);
}, [theme]);
return null;
};
const withThemeProvider: Decorator = (Story, context) => {
return (
<>
<ThemeSwitcher theme={context.globals.theme} />
<Story />
</>
);
};
const LanguageSwitcher: React.FC<{
language: string;
}> = ({ language }) => {
useLayoutEffect(() => {
const changeLanguage = async (language: string) => {
await setLanguage(language);
// Force the component to re-render to apply the new language
addons.getChannel().emit(FORCE_RE_RENDER);
};
changeLanguage(language);
}, [language]);
return null;
};
export const withLanguageProvider: Decorator = (Story, context) => {
return (
<>
<LanguageSwitcher language={context.globals.language} />
<Story />
</>
);
};
const withTooltipProvider: Decorator = (Story) => {
return (
<TooltipProvider>
<Story />
</TooltipProvider>
);
};
const preview: Preview = {
tags: ["autodocs"],
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
},
};
export default preview;

View File

@@ -1,37 +0,0 @@
/*
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 { waitForPageReady } from "@storybook/test-runner";
import { toMatchImageSnapshot } from "jest-image-snapshot";
const customSnapshotsDir = `${process.cwd()}/playwright/shared-component-snapshots/`;
const customReceivedDir = `${process.cwd()}/playwright/shared-component-received/`;
/**
* @type {import('@storybook/test-runner').TestRunnerConfig}
*/
const config = {
setup(page) {
expect.extend({ toMatchImageSnapshot });
},
async postVisit(page, context) {
await waitForPageReady(page);
// If you want to take screenshot of multiple browsers, use
// page.context().browser().browserType().name() to get the browser name to prefix the file name
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotsDir,
customSnapshotIdentifier: `${context.id}-${process.platform}`,
storeReceivedOnFailure: true,
customReceivedDir,
customDiffDir: customReceivedDir,
});
},
};
export default config;

View File

@@ -19,6 +19,3 @@ include:
* Thom Cleary (https://github.com/thomcatdotrocks)
Small update for tarball deployment
* Alexander (https://github.com/ioalexander)
Save image on CTRL + S shortcut

View File

@@ -1,155 +1,3 @@
Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
====================================================================================================
## ✨ Features
* [Backport staging] Fix e2e icon colour ([#30304](https://github.com/element-hq/element-web/pull/30304)). Contributed by @RiotRobot.
* Add support for module message hint `allowDownloadingMedia` ([#30252](https://github.com/element-hq/element-web/pull/30252)). Contributed by @Half-Shot.
* Update the mobile\_guide page to the new design and link out to Element X by default. ([#30172](https://github.com/element-hq/element-web/pull/30172)). Contributed by @pixlwave.
* Filter settings exported when rageshaking ([#30236](https://github.com/element-hq/element-web/pull/30236)). Contributed by @Half-Shot.
* Allow Element Call to learn the room name ([#30213](https://github.com/element-hq/element-web/pull/30213)). Contributed by @robintown.
## 🐛 Bug Fixes
* [Backport staging] Fix missing image download button ([#30322](https://github.com/element-hq/element-web/pull/30322)). Contributed by @RiotRobot.
* Fix transparent verification checkmark in dark mode ([#30235](https://github.com/element-hq/element-web/pull/30235)). Contributed by @Banbuii.
* Fix logic in DeviceListener ([#30230](https://github.com/element-hq/element-web/pull/30230)). Contributed by @uhoreg.
* Disable file drag-and-drop if insufficient permissions ([#30186](https://github.com/element-hq/element-web/pull/30186)). Contributed by @t3chguy.
Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
====================================================================================================
## ✨ Features
* New room list: add context menu to room list item ([#29952](https://github.com/element-hq/element-web/pull/29952)). Contributed by @florianduros.
* Support for custom message components via Module API ([#30074](https://github.com/element-hq/element-web/pull/30074)). Contributed by @Half-Shot.
* Prompt users to set up recovery ([#30075](https://github.com/element-hq/element-web/pull/30075)). Contributed by @uhoreg.
* Update `IconButton` colors ([#30124](https://github.com/element-hq/element-web/pull/30124)). Contributed by @florianduros.
* New room list: filter list can be collapsed ([#29992](https://github.com/element-hq/element-web/pull/29992)). Contributed by @florianduros.
* Show `EmptyRoomListView` when low priority filter matches zero rooms ([#30122](https://github.com/element-hq/element-web/pull/30122)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* Fix untranslatable string "People" in notifications beta ([#30165](https://github.com/element-hq/element-web/pull/30165)). Contributed by @t3chguy.
* Force verification even after logging in via delegate ([#30141](https://github.com/element-hq/element-web/pull/30141)). Contributed by @andybalaam.
* Hide add integrations button based on UIComponent.AddIntegrations ([#30140](https://github.com/element-hq/element-web/pull/30140)). Contributed by @t3chguy.
* Use nav for new room list and label sections ([#30134](https://github.com/element-hq/element-web/pull/30134)). Contributed by @dbkr.
* Spacestore should emit event after rebuilding home space ([#30132](https://github.com/element-hq/element-web/pull/30132)). Contributed by @MidhunSureshR.
* Handle m.room.pinned\_events being invalid ([#30129](https://github.com/element-hq/element-web/pull/30129)). Contributed by @t3chguy.
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

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
# syntax=docker.io/docker/dockerfile:1.15-labs
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:a80324457a2c8d09c83ff9edf2bdf71f378d3288de920e68a358bd3c484b8c4a AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye 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@sha256:86df552d36eb24c45d3f5becf6423bd056a3fd235d7085fe3d5ea28ba89a8232
FROM nginxinc/nginx-unprivileged:alpine-slim
# Need root user to install packages & manipulate the usr directory
USER root

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](./developer_guide.md#setting-up-a-dev-environment) below). Alternatively, you
environment](#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

View File

@@ -127,6 +127,7 @@ Unless otherwise specified, the following applies to all code:
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
1. Note that an explicit type is optional if not expected to be used outside of the function call,
unlike in this example:
@@ -160,6 +161,7 @@ Unless otherwise specified, the following applies to all code:
28. Export only what can be reused.
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should
@@ -258,6 +260,7 @@ Inheriting all the rules of TypeScript, the following additionally apply:
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
if at all possible.
13. A component should only use CSS class names in line with the component name.
1. When knowingly using a class name from another component, document it with a [comment](#comments).
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
@@ -385,6 +388,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
properties should be clearly documented.
4. Inside a function, there is no need to comment every line, but consider:
- before a particular multiline section of code within the function, give an overview of what it does,
to make it easier for a reader to follow the flow through the function as a whole.
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis

View File

@@ -20,7 +20,8 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"default_widget_container_height": 280,
"default_country_code": "GB",

8
declaration.d.ts vendored
View File

@@ -1,8 +0,0 @@
/*
* 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.
*/
declare module "*.module.css";

View File

@@ -109,7 +109,7 @@ yarn test
### End-to-End tests
See [`docs/playwright.md`](./docs/playwright.md) for how to run the end-to-end tests.
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
## General github guidelines

View File

@@ -130,37 +130,32 @@ complete re-branding/private labeling, a more personalised experience can be ach
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
download the app instead.
8. `mobile_guide_app_variant`: Optional. The mobile app that the user is prompted to download from the `/mobile_guide` page. When omitted
the mobile guide will be configured for the new Element X apps. Allowed values are as follows:
1. `element`: Element X Android/iOS.
2. `element-classic`: Element Classic Android/iOS.
3. `element-pro`: Element Pro Android/iOS.
9. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
8. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
in production.
10. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
configuration found in the well-known location is used instead.
11. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
12. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
9. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
configuration found in the well-known location is used instead.
10. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
11. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
13. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
14. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
15. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
12. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
13. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
14. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
`true` to hide these options.
16. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
15. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
to hide this dropdown.
17. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
16. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
users. Set to `true` to disable this functionality.
18. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
17. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
Takes a configuration object as below:
1. `title`: Required. Title to show at the top of the notice.
2. `description`: Required. The description to use for the notice.
3. `show_once`: Optional. If true then the notice will only be shown once per device.
19. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
20. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
21. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
### `desktop_builds` and `mobile_builds`
@@ -450,7 +445,8 @@ If you would like to use Scalar, the integration manager maintained by Element,
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
]
}
```

View File

@@ -55,7 +55,8 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"defaultCountryCode": "GB",

View File

@@ -101,6 +101,10 @@ 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.

View File

@@ -15,7 +15,8 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",

View File

@@ -15,7 +15,8 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api"
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",

View File

@@ -17,13 +17,11 @@ const config: Config = {
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
customExportConditions: ["browser", "node"],
},
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/src/shared-components/**/*.test.[t]s?(x)"],
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
globalSetup: "<rootDir>/test/globalSetup.ts",
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
moduleNameMapper: {
// Support CSS module
"\\.(module.css)$": "identity-obj-proxy",
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.106",
"version": "1.11.98",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -65,28 +65,23 @@
"coverage": "yarn test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
"postinstall": "patch-package",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
"postinstall": "patch-package"
},
"resolutions": {
"**/pretty-format/react-is": "19.1.0",
"@playwright/test": "1.54.1",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"oidc-client-ts": "3.3.0",
"@playwright/test": "1.51.1",
"@types/react": "19.1.1",
"@types/react-dom": "19.1.2",
"oidc-client-ts": "3.2.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001724",
"testcontainers": "^11.0.0",
"caniuse-lite": "1.0.30001714",
"testcontainers": "10.24.2",
"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": "1.3.0",
"@element-hq/element-web-module-api": "^0.1.1",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
@@ -97,9 +92,9 @@
"@sentry/browser": "^9.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^5.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/matrix-wysiwyg": "2.38.4",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.10.2",
"@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",
@@ -128,9 +123,9 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-react": "4.3.1",
"linkify-string": "4.3.1",
"linkifyjs": "4.3.1",
"linkify-react": "4.2.0",
"linkify-string": "4.2.0",
"linkifyjs": "4.2.0",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
@@ -143,7 +138,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.257.0",
"posthog-js": "1.236.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -156,7 +151,7 @@
"react-virtualized": "^9.22.5",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.0",
"sanitize-html": "2.15.0",
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.3.0",
"ua-parser-js": "^1.0.2",
@@ -185,19 +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.13.1",
"@element-hq/element-web-playwright-common": "^1.4.3",
"@element-hq/element-call-embedded": "0.9.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": "^4.0.0",
"@storybook/addon-designs": "^10.0.1",
"@storybook/addon-docs": "^9.0.12",
"@storybook/icons": "^1.4.0",
"@storybook/react-vite": "^9.0.15",
"@storybook/test-runner": "^0.23.0",
"@stylistic/eslint-plugin": "^5.0.0",
"@sentry/webpack-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",
@@ -222,11 +212,11 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "19.1.8",
"@types/react": "19.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.1.6",
"@types/react-dom": "19.1.2",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/sanitize-html": "2.15.0",
"@types/semver": "^7.5.8",
"@types/tar-js": "^0.3.5",
"@types/ua-parser-js": "^0.7.36",
@@ -241,10 +231,10 @@
"concurrently": "^9.0.0",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.38.1",
"cronstrue": "^3.0.0",
"cronstrue": "^2.41.0",
"css-loader": "^7.0.0",
"css-minimizer-webpack-plugin": "^7.0.0",
"dotenv": "^17.0.0",
"dotenv": "^16.0.2",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
@@ -256,25 +246,23 @@
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-storybook": "^9.0.12",
"eslint-plugin-unicorn": "^56.0.0",
"express": "^5.0.0",
"fake-indexeddb": "^6.0.0",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^6.0.0",
"glob": "^11.0.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.6.2",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"jest-image-snapshot": "^6.5.1",
"jest-mock": "^29.6.2",
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"knip": "^5.36.2",
"lint-staged": "^16.0.0",
"lint-staged": "^15.0.2",
"matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2",
"minimist": "^1.2.6",
@@ -287,29 +275,26 @@
"postcss-hexrgba": "2.1.0",
"postcss-import": "16.1.0",
"postcss-loader": "8.1.1",
"postcss-mixins": "^12.0.0",
"postcss-mixins": "^11.0.0",
"postcss-nested": "^7.0.0",
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.6.2",
"prettier": "3.5.3",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"storybook": "^9.0.12",
"stylelint": "^16.13.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": "^11.0.0",
"testcontainers": "^10.20.0",
"ts-node": "^10.9.1",
"typescript": "5.8.3",
"util": "^0.12.5",
"vite": "^7.0.1",
"vite-plugin-node-polyfills": "^0.24.0",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.8.0",
@@ -326,6 +311,5 @@
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}

View File

@@ -1,46 +0,0 @@
diff --git a/node_modules/@types/mdx/types.d.ts b/node_modules/@types/mdx/types.d.ts
index 498bb69..4e89216 100644
--- a/node_modules/@types/mdx/types.d.ts
+++ b/node_modules/@types/mdx/types.d.ts
@@ -5,7 +5,7 @@
*/
// @ts-ignore JSX runtimes may optionally define JSX.ElementType. The MDX types need to work regardless whether this is
// defined or not.
-type ElementType = any extends JSX.ElementType ? never : JSX.ElementType;
+type ElementType = any extends JSX.ElementType ? never : React.JSX.ElementType;
/**
* This matches any function component types that ar part of `ElementType`.
@@ -20,12 +20,12 @@ type ClassElementType = Extract<ElementType, new(props: Record<string, any>) =>
/**
* A valid JSX string component.
*/
-type StringComponent = Extract<keyof JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
+type StringComponent = Extract<keyof React.JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
/**
* A JSX element returned by MDX content.
*/
-export type Element = JSX.Element;
+export type Element = React.JSX.Element;
/**
* A valid JSX function component.
@@ -44,7 +44,7 @@ type FunctionComponent<Props> = ElementType extends never
*/
type ClassComponent<Props> = ElementType extends never
// If JSX.ElementType isnt defined, the valid return type is a constructor that returns JSX.ElementClass
- ? new(props: Props) => JSX.ElementClass
+ ? new(props: Props) => React.JSX.ElementClass
: ClassElementType extends never
// If JSX.ElementType is defined, but doesnt allow constructors, function components are disallowed.
? never
@@ -70,7 +70,7 @@ interface NestedMDXComponents {
export type MDXComponents =
& NestedMDXComponents
& {
- [Key in StringComponent]?: Component<JSX.IntrinsicElements[Key]>;
+ [Key in StringComponent]?: Component<React.JSX.IntrinsicElements[Key]>;
}
& {
/**

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 2272032..18bd20a 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`.
@@ -941,7 +941,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
@@ -1113,7 +1113,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.

View File

@@ -23,13 +23,7 @@ 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_${testInfo.testId}@email.com`,
"Pa$sW0rD!",
);
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
// Wait for the ui to load
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
@@ -41,13 +35,7 @@ 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_${testInfo.testId}@email.com`,
"Pa$sW0rD!",
);
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
@@ -76,7 +64,7 @@ test.describe("Key backup reset from elsewhere", () => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();

View File

@@ -158,8 +158,6 @@ test.describe("Cryptography", function () {
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png");
await testMessages(page, bob, bobRoomId);
await verify(app, bob);
@@ -170,7 +168,6 @@ test.describe("Cryptography", function () {
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon.png");
},
);

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 { createBot, logIntoElement } from "./utils.ts";
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
import { type Client } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
@@ -28,27 +28,21 @@ 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 identity key, and then set up recovery (which will create SSSS, and dehydrated device)
// Verify the device by resetting the key (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();
// Reset the identity key
// Verify the device by resetting the 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();
// Set up recovery
await page.getByRole("button", { name: "Set up recovery" }).click();
await page.getByRole("button", { name: "Copy" }).click();
await page.getByRole("button", { name: "Continue" }).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 page.getByRole("button", { name: "Done" }).click();
await expectDehydratedDeviceEnabled(app);
@@ -86,7 +80,7 @@ test.describe("Dehydration", () => {
await expectDehydratedDeviceEnabled(app);
});
test("Reset identity during login and set up recovery re-creates dehydrated device", async ({
test("Reset recovery key during login re-creates dehydrated device", async ({
page,
homeserver,
app,
@@ -105,26 +99,16 @@ test.describe("Dehydration", () => {
// Log in our client
await logIntoElement(page, credentials);
// Oh no, we forgot our recovery key - reset our identity
// Oh no, we forgot our recovery key
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).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 page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
// 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();
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
// There should be a brand new dehydrated device
await expectDehydratedDeviceEnabled(app);
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
expect(dehydratedDeviceIds.length).toBe(1);
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
});
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {

View File

@@ -22,7 +22,6 @@ 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;
@@ -48,38 +47,31 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
return promiseVerificationRequest;
}
test(
"Verify device with SAS during login",
{ tag: "@screenshot" },
async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
// Handle emoji SAS verification
const infoDialog = page.locator(".mx_InfoDialog");
// the bot chooses to do an emoji verification
const verifier = await verificationRequest.evaluateHandle((request) =>
request.startVerification("m.sas.v1"),
);
// Handle emoji SAS verification
const infoDialog = page.locator(".mx_InfoDialog");
// the bot chooses to do an emoji verification
const verifier = await verificationRequest.evaluateHandle((request) => request.startVerification("m.sas.v1"));
// Handle emoji request and check that emojis are matching
await doTwoWaySasVerification(page, verifier);
// Handle emoji request and check that emojis are matching
await doTwoWaySasVerification(page, verifier);
await infoDialog.getByRole("button", { name: "They match" }).click();
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
await infoDialog.getByRole("button", { name: "Got it" }).click();
await infoDialog.getByRole("button", { name: "They match" }).click();
await infoDialog.getByRole("button", { name: "Got it" }).click();
// 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
// For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen.
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
},
);
// Check that the current device is connected to key backup
// For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen.
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
});
// Regression test for https://github.com/element-hq/element-web/issues/29110
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
@@ -171,44 +163,16 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
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;
// Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
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);
/* 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");
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
// Fill the passphrase
const dialog = page.locator(".mx_Dialog");
// 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 dialog.locator("input").fill("new passphrase");
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
@@ -216,7 +180,30 @@ 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("Verify device with Recovery Key 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 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();
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);
});
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
await logIntoElement(page, credentials);

View File

@@ -67,9 +67,8 @@ test.describe("Cryptography", function () {
// Bob has a second, not cross-signed, device
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
// 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();
// 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();
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
algorithm: "m.megolm.v1.aes-sha2",

View File

@@ -8,8 +8,7 @@
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "../../element-web-test";
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
import { type Bot } from "../../pages/bot";
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
test.describe("Key storage out of sync toast", () => {
let recoveryKey: GeneratedSecretStorageKey;
@@ -54,114 +53,3 @@ 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();
});
});

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("textarea").fill(securityKey);
await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).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("textarea").fill(securityKey);
await app.page.locator(".mx_Dialog").locator('input[type="password"]').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,25 +316,6 @@ 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`).
*

View File

@@ -5,11 +5,8 @@
* 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 { SettingLevel } from "../../../../src/settings/SettingLevel";
import type { Page } from "@playwright/test";
test.describe("Room list filters and sort", () => {
test.use({
@@ -21,20 +18,8 @@ test.describe("Room list filters and sort", () => {
labsFlags: ["feature_new_room_list"],
});
function getPrimaryFilters(page: Page): Locator {
return page.getByTestId("primary-filters");
}
function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
function getFilterExpandButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
}
function getFilterCollapseButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
function getPrimaryFilters(page: Page) {
return page.getByRole("listbox", { name: "Room list filters" });
}
/**
@@ -50,65 +35,6 @@ test.describe("Room list filters and sort", () => {
await app.closeNotificationToast();
});
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,
@@ -144,7 +70,6 @@ test.describe("Room list filters and sort", () => {
await tile.click();
// Enable Favourite filter
await getFilterExpandButton(page).click();
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(tile).not.toBeVisible();
@@ -181,38 +106,6 @@ 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 }) => {
@@ -229,38 +122,22 @@ 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();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4);
expect(await roomList.locator("role=gridcell").count()).toBe(2);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2);
expect(await roomList.locator("role=gridcell").count()).toBe(1);
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();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Invites" }).click();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await getFilterCollapseButton(page).click();
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
expect(await roomList.locator("role=gridcell").count()).toBe(3);
});
test(
@@ -292,23 +169,6 @@ test.describe("Room list filters and sort", () => {
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);
await getRoomOptionsMenu(page).click();
await page.getByRole("menuitemradio", { name: "A-Z" }).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);
await bot.sendMessage(unReadDmId, "Hello!");
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
});
});
test.describe("Empty room list", () => {
@@ -326,33 +186,19 @@ test.describe("Room list filters and sort", () => {
async ({ page, app, user }) => {
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
await expect(page.getByRole("navigation", { name: "Room list" })).toMatchScreenshot(
"room-panel-empty-room-list.png",
);
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
},
);
[
{ 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 getFilterExpandButton(page).click();
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();
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: action }).click();
await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
},
);
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
});
["People", "Rooms", "Favourite"].forEach((filter) => {
@@ -361,8 +207,6 @@ test.describe("Room list filters and sort", () => {
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);

View File

@@ -19,7 +19,7 @@ test.describe("Room list panel", () => {
* @param page
*/
function getRoomListView(page: Page) {
return page.getByRole("navigation", { name: "Room list" });
return page.getByTestId("room-list-panel");
}
test.beforeEach(async ({ page, app, user }) => {
@@ -30,9 +30,6 @@ 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 }) => {
@@ -41,10 +38,4 @@ 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 = getRoomListView(page);
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
});
});

View File

@@ -29,9 +29,6 @@ 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", () => {
@@ -46,10 +43,10 @@ test.describe("Room list", () => {
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await roomListView.hover();
// Scroll to the end of the room list
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
@@ -59,12 +56,6 @@ test.describe("Room list", () => {
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" });
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
});
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
@@ -114,13 +105,13 @@ test.describe("Room list", () => {
// It should make the room muted
await page.getByRole("menuitem", { name: "Mute room" }).click();
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
// Remove hover on the room list item
await roomListView.hover();
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
// Scroll to the end of the room list
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
// Scroll to the bottom of the list
await page.getByRole("grid", { name: "Room list" }).evaluate((e) => {
e.scrollTop = e.scrollHeight;
});
// The room decoration should have the muted icon
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
@@ -138,12 +129,10 @@ test.describe("Room list", () => {
test("should scroll to the current room", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await roomListView.hover();
// Scroll to the end of the room list
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
const filters = page.getByRole("listbox", { name: "Room list filters" });
@@ -194,57 +183,6 @@ test.describe("Room list", () => {
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 page.keyboard.press("Enter");
// Wait for the menu to be open
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
"aria-selected",
"true",
);
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Escape");
// Focus should be back on the notification button
await expect(notificationButton).toBeFocused();
});
});
});
test.describe("Avatar decoration", () => {
@@ -253,10 +191,6 @@ test.describe("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" });
@@ -264,40 +198,14 @@ test.describe("Room list", () => {
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.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
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");
});
@@ -363,27 +271,6 @@ 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);
@@ -423,8 +310,8 @@ test.describe("Room list", () => {
await room.getByRole("button", { name: "More Options" }).click();
await page.getByRole("menuitem", { name: "mark as unread" }).click();
// focus the user menu to avoid to have hover decoration
await page.getByRole("button", { name: "User menu" }).focus();
// Remove hover on the room list item
await roomListView.hover();
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
});

View File

@@ -288,43 +288,6 @@ 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();
});
});
});

View File

@@ -1,36 +0,0 @@
/*
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";
import { MobileAppVariant } from "../../../src/vector/mobile_guide/mobile-apps";
const variants = [MobileAppVariant.Classic, MobileAppVariant.X, MobileAppVariant.Pro];
test.describe("Mobile Guide Screenshots", { tag: "@screenshot" }, () => {
for (const variant of variants) {
test.describe(`for variant ${variant}`, () => {
test.use({
config: {
default_server_config: {
"m.homeserver": {
base_url: "https://matrix.server.invalid",
server_name: "server.invalid",
},
},
mobile_guide_app_variant: variant,
},
viewport: { width: 390, height: 844 }, // iPhone 16e
});
test("should match the mobile_guide screenshot", async ({ page, axe }) => {
await page.goto("/mobile_guide/");
await expect(page).toMatchScreenshot(`mobile-guide-${variant}.png`);
await expect(axe).toHaveNoViolations();
});
});
}
});

View File

@@ -1,160 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import fs from "node:fs";
import { test, expect } from "../../element-web-test";
const screenshotOptions = (page: Page) => ({
mask: [page.locator(".mx_MessageTimestamp")],
// Hide the jump to bottom button in the timeline to avoid flakiness
// Exclude timestamp and read marker from snapshot
css: `
.mx_JumpToBottomButton {
display: none !important;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
});
const IMAGE_FILE = fs.readFileSync("playwright/sample-files/element.png");
test.describe("Custom Component API", () => {
test.use({
displayName: "Manny",
config: {
modules: ["/modules/custom-component-module.js"],
},
page: async ({ page }, use) => {
await page.route("/modules/custom-component-module.js", async (route) => {
await route.fulfill({ path: "playwright/sample-files/custom-component-module.js" });
});
await use(page);
},
room: async ({ page, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "TestRoom" });
await use({ roomId });
},
});
test.describe("basic functionality", () => {
test(
"should replace the render method of a textual event",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Simple message");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile.png",
screenshotOptions(page),
);
},
);
test(
"should fall through if one module does not render a component",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Fall through here");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile-fall-through.png",
screenshotOptions(page),
);
},
);
test(
"should render the original content of a textual event conditionally",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not replace me");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile-original.png",
screenshotOptions(page),
);
},
);
test("should disallow editing when the allowEditingEvent hint is set to false", async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not show edits");
await page.getByText("Do not show edits").hover();
await expect(
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }),
).not.toBeVisible();
});
test("should disallow downloading media when the allowDownloading hint is set to false", async ({
page,
room,
app,
}) => {
await app.viewRoomById(room.roomId);
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(IMAGE_FILE, { name: "bad.png", type: "image/png" });
await app.client.sendEvent(room.roomId, null, "m.room.message", {
msgtype: "m.image",
body: "bad.png",
url: upload.content_uri,
});
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await expect(page.getByRole("button", { name: "Download" })).not.toBeVisible();
await imgTile.click();
await expect(page.getByLabel("Image view").getByLabel("Download")).not.toBeVisible();
});
test("should allow downloading media when the allowDownloading hint is set to true", async ({
page,
room,
app,
}) => {
await app.viewRoomById(room.roomId);
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(IMAGE_FILE, { name: "good.png", type: "image/png" });
await app.client.sendEvent(room.roomId, null, "m.room.message", {
msgtype: "m.image",
body: "good.png",
url: upload.content_uri,
});
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await expect(page.getByRole("button", { name: "Download" })).toBeVisible();
await imgTile.click();
await expect(page.getByLabel("Image view").getByLabel("Download")).toBeVisible();
});
test(
"should render the next registered component if the filter function throws",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Crash the filter!");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-crash-handle-filter.png",
screenshotOptions(page),
);
},
);
test(
"should render original component if the render function throws",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Crash the renderer!");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-crash-handle-renderer.png",
screenshotOptions(page),
);
},
);
});
});

View File

@@ -15,7 +15,6 @@ test.describe("Module loading", () => {
test.describe("Example Module", () => {
test.use({
config: {
brand: "TestBrand",
modules: ["/modules/example-module.js"],
},
page: async ({ page }, use) => {
@@ -26,31 +25,11 @@ test.describe("Module loading", () => {
},
});
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);
});
});
}
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!");
});
});
});

View File

@@ -11,9 +11,6 @@ import { type Page } from "@playwright/test";
import { expect } from "../../element-web-test";
/**
* Click through registering a new user in the MAS UI.
*/
export async function registerAccountMas(
page: Page,
mailpit: MailpitClient,
@@ -40,22 +37,6 @@ 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();
}
/**
* Click through entering username and password into the MAS login prompt.
*/
export async function logInAccountMas(page: Page, username: string, password: string): Promise<void> {
await expect(page.getByText("Please sign in to continue:")).toBeVisible();
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByRole("textbox", { name: "Password", exact: true }).fill(password);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
}

View File

@@ -6,12 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type Config, CONFIG_JSON } from "@element-hq/element-web-playwright-common";
import { type Browser, type Page } from "@playwright/test";
import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/HomeserverContainer";
import { test, expect } from "../../element-web-test.ts";
import { logInAccountMas, registerAccountMas } from ".";
import { registerAccountMas } from ".";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
@@ -37,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, `${userId}@email.com`, "Pa$sW0rD!");
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
@@ -59,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", { exact: true })).toBeVisible();
await expect(newPage.getByText("Element")).toBeVisible();
await expect(newPage.getByText("http://localhost:8080/")).toBeVisible();
await expect(newPage).toHaveURL(/\/oauth2_session/);
await newPage.close();
@@ -81,182 +77,29 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test(
"it should log out the user & wipe data when logging out via MAS",
{ tag: "@screenshot" },
async ({ mas, page, mailpitClient, homeserver }, testInfo) => {
async ({ mas, page, mailpitClient }, testInfo) => {
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await registerAccountMas(page, mailpitClient, userId, "alice@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");
// Workaround for Synapse's 2 minute cache on MAS token validity
// (https://github.com/element-hq/synapse/pull/18231)
await homeserver.restart();
await page.goto("http://localhost:8080");
await expect(
page.getByText("For security, this session has been signed out. Please sign in again."),
).toBeVisible();
//await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
expect(localStorageKeys).toHaveLength(0);
},
);
test("can log in to an existing MAS account", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => {
// Register an account with MAS
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
// Log out
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
// Allow the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
// Log in again
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// We should be in (we see an error because we have no recovery key).
await expect(page.getByText("Unable to verify this device")).toBeVisible();
});
test.describe("with force_verification on", () => {
test.use({
config: {
force_verification: true,
},
});
test("verify dialog cannot be dismissed", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => {
// Register an account with MAS
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
// Log out
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
// Log in again
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// We should be being warned that we need to verify (but we can't)
await expect(page.getByText("Unable to verify this device")).toBeVisible();
// And there should be no way to close this prompt
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
});
test(
"continues to show verification prompt after cancelling device verification",
{ tag: "@screenshot" },
async ({ browser, config, homeserver, page, mailpitClient }, testInfo) => {
// Register an account with MAS
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
const password = "Pa$sW0rD!";
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, password);
await expect(page.getByText("Welcome")).toBeVisible();
// Log in an additional account, and verify it.
//
// This means that when we log out and in again, we are offered
// to verify using another device.
const otherContext = await newContext(browser, config, homeserver);
const otherDevicePage = await otherContext.newPage();
await otherDevicePage.goto("/#/login");
await otherDevicePage.getByRole("button", { name: "Continue" }).click();
await logInAccountMas(otherDevicePage, userId, password);
await verifyUsingOtherDevice(otherDevicePage, page);
await otherDevicePage.close();
// Log out
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
// Log in again
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// We should be in, and not able to dismiss the verify dialog
await expect(page.getByText("Verify this device")).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
// When we start verifying with another device
await page.getByRole("button", { name: "Verify with another device" }).click();
// And then cancel it
await page.getByRole("button", { name: "Close dialog" }).click();
// Then we should still be at the unskippable verify prompt
await expect(page.getByText("Verify this device")).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
},
);
});
});
/**
* Perform interactive emoji verification for a new device.
*/
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Got it" }).click();
await deviceToVerifyPage.getByRole("button", { name: "Got it" }).click();
}
/**
* Create a new browser context which serves up the default config plus what you supplied, and sets m.homeserver to the
* supplied homeserver's URL.
*/
async function newContext(browser: Browser, config: Partial<Partial<Config>>, homeserver: StartedHomeserverContainer) {
const otherContext = await browser.newContext();
await otherContext.route(`http://localhost:8080/config.json*`, async (route) => {
const json = {
...CONFIG_JSON,
...config,
default_server_config: {
"m.homeserver": {
base_url: homeserver.baseUrl,
},
},
};
await route.fulfill({ json });
});
return otherContext;
}

View File

@@ -42,10 +42,7 @@ export class Helpers {
*/
async assertReleaseAnnouncementIsVisible(name: string) {
await expect(this.getReleaseAnnouncement(name)).toBeVisible();
await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, {
showTooltips: true,
hideJumpToBottomButton: true,
});
await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, { showTooltips: true });
}
/**

View File

@@ -15,34 +15,20 @@ test.describe("Release announcement", () => {
feature_release_announcement: true,
},
},
room: async ({ app, user }, use) => {
const roomId = await app.client.createRoom({
name: "Test room",
});
await app.viewRoomById(roomId);
await use({ roomId });
},
labsFlags: ["threadsActivityCentre"],
});
test(
"should display the pinned messages release announcement",
{ tag: "@screenshot" },
async ({ page, app, room, util }) => {
await app.toggleRoomInfoPanel();
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");
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);
},
);
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");
});
});

View File

@@ -11,7 +11,6 @@ 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 =
@@ -22,23 +21,20 @@ 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_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
}
test.describe("RightPanel", () => {
let testRoomId: string;
test.use({
displayName: NAME,
});
test.beforeEach(async ({ app, user }) => {
testRoomId = await app.client.createRoom({ name: ROOM_NAME });
await app.client.createRoom({ name: ROOM_NAME });
await app.client.createSpace({ name: SPACE_NAME });
});
@@ -81,12 +77,10 @@ test.describe("RightPanel", () => {
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png");
});
test("should handle clicking add widgets", { tag: "@screenshot" }, async ({ page, app }) => {
test("should handle clicking add widgets", 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();
});
@@ -140,37 +134,6 @@ 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", { tag: "@screenshot" }, async ({ page, app }) => {

View File

@@ -19,162 +19,126 @@ import {
test.describe("Encryption tab", () => {
test.use({ displayName: "Alice" });
test.describe("when encryption is set up", () => {
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
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 }) => {
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,
}) => {
// 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);
// 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();
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
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();
// The user is prompted to reset their identity
await expect(
dialog.getByText("Forgot your recovery key? Youll need to reset your identity."),
).toBeVisible();
});
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-tab.png", {
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
});
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
// 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 expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();
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(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
// 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();
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({}));
}
});
// The user is prompted to reset their identity
await expect(dialog.getByText("Forgot your recovery key? Youll need to reset your identity.")).toBeVisible();
});
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");
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
// Initially, our device is not verified
await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible();
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
// 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(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();
// First try cancelling and restarting
await page.getByRole("button", { name: "Cancel" }).click();
await page.getByRole("button", { name: "Proceed with reset" }).click();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
// 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();
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.")),
];
// Finally we actually continue
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Delete key storage" }).click();
// Now we are verified, so we see the Key storage toggle
await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible();
});
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({}));
}
});
});

View File

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

View File

@@ -1,34 +0,0 @@
/*
* 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");
});
});

View File

@@ -19,6 +19,7 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
test.use({
displayName: "Alice",
botCreateOpts: { displayName: "Other User" },
labsFlags: ["threadsActivityCentre"],
});
test(

View File

@@ -15,9 +15,6 @@ 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({
@@ -42,14 +39,7 @@ test.describe("Media preview settings", () => {
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;
}
`,
});
).toMatchScreenshot("invite-no-avatar.png");
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
).toMatchScreenshot("invite-room-tree-no-avatar.png");
@@ -62,14 +52,7 @@ test.describe("Media preview settings", () => {
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;
}
`,
});
).toMatchScreenshot("invite-with-avatar.png");
await expect(
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
).toMatchScreenshot("invite-room-tree-with-avatar.png");

View File

@@ -107,7 +107,6 @@ interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
includeDialogBackground?: boolean;
showTooltips?: boolean;
timeout?: number;
hideJumpToBottomButton?: boolean;
}
type Expectations = {
@@ -166,14 +165,6 @@ export const expect = baseExpect.extend<Expectations>({
`;
}
if (options?.hideJumpToBottomButton) {
css += `
.mx_JumpToBottomButton {
display: none !important;
}
`;
}
if (options?.css) {
css += options.css;
}

View File

@@ -213,26 +213,4 @@ export class ElementAppPage {
.getByRole("button", { name: "Dismiss" })
.click();
}
/**
* Scroll an infinite list to the bottom.
* @param list The element to scroll
*/
public async scrollListToBottom(list: Locator): Promise<void> {
// First hover the mouse over the element that we want to scroll
await list.hover();
const needsScroll = async () => {
// From https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
const fullyScrolled = await list.evaluate(
(e) => Math.abs(e.scrollHeight - e.clientHeight - e.scrollTop) <= 1,
);
return !fullyScrolled;
};
// Scroll the element until we detect that it is fully scrolled
do {
await this.page.mouse.wheel(0, 1000);
} while (await needsScroll());
}
}

View File

@@ -1,74 +0,0 @@
/*
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.
*/
// Note: eslint-plugin-jsdoc doesn't like import types as parameters, so we
// get around it with @typedef
/**
* @typedef {import("@element-hq/element-web-module-api").Api} Api
*/
export default class CustomComponentModule {
static moduleApiVersion = "^1.2.0";
/**
* Basic module for testing.
* @param {Api} api API object
*/
constructor(api) {
this.api = api;
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Do not show edits",
(_props, originalComponent) => {
return originalComponent();
},
{ allowEditingEvent: false },
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Fall through here",
(props) => {
const body = props.mxEvent.content.body;
return `Fallthrough text for ${body}`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => {
if (evt.content.body === "Crash the filter!") {
throw new Error("Fail test!");
}
return false;
},
() => {
return `Should not render!`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Crash the renderer!",
() => {
throw new Error("Fail test!");
},
);
this.api.customComponents.registerMessageRenderer(
(mxEvent) => mxEvent.type === "m.room.message" && mxEvent.content.msgtype === "m.image",
(_props, originalComponent) => {
return originalComponent();
},
{ allowDownloadingMedia: async (mxEvent) => mxEvent.content.body !== "bad.png" },
);
// Order is specific here to avoid this overriding the other renderers
this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => {
const body = props.mxEvent.content.body;
if (body === "Do not replace me") {
return originalComponent();
} else if (body === "Fall through here") {
return null;
}
return `Custom text for ${body}`;
});
}
async load() {}
}

View File

@@ -6,19 +6,11 @@ Please see LICENSE files in the repository root for full details.
*/
export default class ExampleModule {
static moduleApiVersion = "^1.0.0";
static moduleApiVersion = "^0.1.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() {
const brand = this.api.config.get("brand");
alert(this.api.i18n.translate("key", { brand }));
alert("Testing module loading successful!");
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 31 KiB

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