mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-07 01:21:02 +00:00
Compare commits
5 Commits
t3chguy/fi
...
hs/fix-err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9623639b08 | ||
|
|
976a8f44bc | ||
|
|
9cb55970d3 | ||
|
|
d82029d0c1 | ||
|
|
c1df3d1511 |
@@ -8,6 +8,3 @@ src/component-index.js
|
||||
# Auto-generated file
|
||||
src/modules.ts
|
||||
src/modules.js
|
||||
# Test result files
|
||||
/playwright/test-results/
|
||||
/playwright/html-report/
|
||||
|
||||
@@ -30,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.",
|
||||
@@ -59,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",
|
||||
|
||||
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -11,11 +11,10 @@
|
||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
|
||||
@@ -11,7 +11,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download release tarball
|
||||
uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1
|
||||
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
||||
with:
|
||||
tag: ${{ inputs.tag }}
|
||||
fileName: element-*.tar.gz*
|
||||
|
||||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/build_debian.yaml
vendored
4
.github/workflows/build_debian.yaml
vendored
@@ -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
|
||||
|
||||
17
.github/workflows/build_develop.yml
vendored
17
.github/workflows/build_develop.yml
vendored
@@ -26,9 +26,15 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
|
||||
- uses: unfor19/install-aws-cli-action@v1
|
||||
with:
|
||||
version: 2.22.35
|
||||
verbose: false
|
||||
arch: amd64
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version: "lts/*"
|
||||
@@ -53,7 +59,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
|
||||
@@ -109,11 +115,10 @@ jobs:
|
||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||
# as the expires after 24h and requires auth to download.
|
||||
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
||||
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||
- name: Deploy to R2
|
||||
run: |
|
||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
|
||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -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: |
|
||||
|
||||
73
.github/workflows/docker.yaml
vendored
73
.github/workflows/docker.yaml
vendored
@@ -3,7 +3,6 @@ on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
tags: [v*]
|
||||
pull_request: {}
|
||||
schedule:
|
||||
# This job can take a while, and we have usage limits, so just publish develop only twice a day
|
||||
- cron: "0 7/12 * * *"
|
||||
@@ -13,91 +12,42 @@ jobs:
|
||||
buildx:
|
||||
name: Docker Buildx
|
||||
runs-on: ubuntu-24.04
|
||||
environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }}
|
||||
environment: dockerhub
|
||||
permissions:
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
packages: write # needed for publishing packages to GHCR
|
||||
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@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and load
|
||||
id: test-build
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
|
||||
- name: Test the image
|
||||
env:
|
||||
IMAGEID: ${{ steps.test-build.outputs.imageid }}
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
set -x
|
||||
|
||||
# Make a fake module to test the image
|
||||
MODULE_PATH="modules/module_name/index.js"
|
||||
mkdir -p $(dirname $MODULE_PATH)
|
||||
echo 'alert("Testing");' > $MODULE_PATH
|
||||
|
||||
# Spin up a container of the image
|
||||
ELEMENT_WEB_PORT=8181
|
||||
CONTAINER_ID=$(
|
||||
docker run \
|
||||
--rm \
|
||||
-e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \
|
||||
-dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \
|
||||
-v $(pwd)/modules:/modules \
|
||||
"$IMAGEID" \
|
||||
)
|
||||
|
||||
# Run some smoke tests
|
||||
wget --retry-connrefused --tries=5 -q --wait=3 --spider "http://localhost:$ELEMENT_WEB_PORT/modules/module_name/index.js"
|
||||
MODULE_0=$(curl "http://localhost:$ELEMENT_WEB_PORT/config.json" | jq -r .modules[0])
|
||||
test "$MODULE_0" = "/${MODULE_PATH}"
|
||||
|
||||
# Check healthcheck
|
||||
until test "$(docker inspect -f {{.State.Health.Status}} $CONTAINER_ID)" == "healthy"; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Clean up
|
||||
docker stop "$CONTAINER_ID"
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||
with:
|
||||
images: |
|
||||
vectorim/element-web
|
||||
@@ -110,8 +60,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -123,7 +72,6 @@ jobs:
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
images=""
|
||||
for tag in ${TAGS}; do
|
||||
@@ -132,8 +80,7 @@ jobs:
|
||||
cosign sign --yes ${images}
|
||||
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
|
||||
14
.github/workflows/docs.yml
vendored
14
.github/workflows/docs.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
26
.github/workflows/end-to-end-tests.yaml
vendored
26
.github/workflows/end-to-end-tests.yaml
vendored
@@ -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,7 +154,7 @@ 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: |
|
||||
@@ -180,7 +180,7 @@ 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
|
||||
@@ -192,13 +192,13 @@ jobs:
|
||||
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"
|
||||
@@ -210,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
|
||||
@@ -226,7 +226,7 @@ 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
|
||||
|
||||
4
.github/workflows/issue_closed.yml
vendored
4
.github/workflows/issue_closed.yml
vendored
@@ -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:
|
||||
|
||||
4
.github/workflows/netlify.yaml
vendored
4
.github/workflows/netlify.yaml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/pending-reviews.yaml
vendored
2
.github/workflows/pending-reviews.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update synapse image
|
||||
run: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/playwright-image-updates
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -19,7 +19,6 @@ jobs:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
|
||||
12
.github/workflows/release_prepare.yml
vendored
12
.github/workflows/release_prepare.yml
vendored
@@ -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
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
repo: matrix-org/matrix-js-sdk
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: "draft / draft"
|
||||
check-name: draft
|
||||
allowed-conclusions: success
|
||||
|
||||
- name: Wait for element-web draft
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
repo: element-hq/element-web
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: "draft / draft"
|
||||
check-name: draft
|
||||
allowed-conclusions: success
|
||||
|
||||
- name: Wait for element-desktop draft
|
||||
@@ -122,5 +122,5 @@ jobs:
|
||||
repo: element-hq/element-desktop
|
||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
wait-interval: 10
|
||||
check-name: "draft / draft"
|
||||
check-name: draft
|
||||
allowed-conclusions: success
|
||||
|
||||
23
.github/workflows/static_analysis.yaml
vendored
23
.github/workflows/static_analysis.yaml
vendored
@@ -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/*"
|
||||
@@ -51,13 +51,12 @@ jobs:
|
||||
error|invalid_json
|
||||
error|misconfigured
|
||||
welcome_to_element
|
||||
devtools|settings|elementCallUrl
|
||||
|
||||
rethemendex_lint:
|
||||
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 +66,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 +84,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 +102,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 +120,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/*"
|
||||
|
||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@@ -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@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||
uses: guibranco/github-status-action-v2@119b3320db3f04d89e91df840844b92d57ce3468
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
3
.github/workflows/triage-assigned.yml
vendored
3
.github/workflows/triage-assigned.yml
vendored
@@ -11,8 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
contains(github.event.issue.assignees.*.login, 't3chguy') ||
|
||||
contains(github.event.issue.assignees.*.login, 'florianduros') ||
|
||||
contains(github.event.issue.assignees.*.login, 'dbkr') ||
|
||||
contains(github.event.issue.assignees.*.login, 'andybalaam') ||
|
||||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
|
||||
16
.github/workflows/triage-labelled.yml
vendored
16
.github/workflows/triage-labelled.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/triage-stale.yml
vendored
2
.github/workflows/triage-stale.yml
vendored
@@ -12,7 +12,7 @@ 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
|
||||
|
||||
2
.github/workflows/triage-unlabelled.yml
vendored
2
.github/workflows/triage-unlabelled.yml
vendored
@@ -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({
|
||||
|
||||
6
.github/workflows/update-jitsi.yml
vendored
6
.github/workflows/update-jitsi.yml
vendored
@@ -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/*"
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
run: "yarn update:jitsi"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
2
.github/workflows/update-topics.yaml
vendored
2
.github/workflows/update-topics.yaml
vendored
@@ -22,7 +22,7 @@ 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 }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,6 +25,7 @@ electron/pub
|
||||
.env
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/build_config.yaml
|
||||
/book
|
||||
|
||||
199
CHANGELOG.md
199
CHANGELOG.md
@@ -1,202 +1,3 @@
|
||||
Changes in [1.11.100](https://github.com/element-hq/element-web/releases/tag/v1.11.100) (2025-05-06)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Move rich topics out of labs / stabilise MSC3765 ([#29817](https://github.com/element-hq/element-web/pull/29817)). Contributed by @Johennes.
|
||||
* Spell out that Element Web does \*not\* work on mobile. ([#29211](https://github.com/element-hq/element-web/pull/29211)). Contributed by @ara4n.
|
||||
* Add message preview support to the new room list ([#29784](https://github.com/element-hq/element-web/pull/29784)). Contributed by @dbkr.
|
||||
* Global configuration flag for media previews ([#29582](https://github.com/element-hq/element-web/pull/29582)). Contributed by @Half-Shot.
|
||||
* New room list: add partial keyboard shortcuts support ([#29783](https://github.com/element-hq/element-web/pull/29783)). Contributed by @florianduros.
|
||||
* MVVM RoomSummaryCard Topic ([#29710](https://github.com/element-hq/element-web/pull/29710)). Contributed by @MarcWadai.
|
||||
* Warn on self change from settings > roles ([#28926](https://github.com/element-hq/element-web/pull/28926)). Contributed by @MarcWadai.
|
||||
* New room list: new visual for invitation ([#29773](https://github.com/element-hq/element-web/pull/29773)). Contributed by @florianduros.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix incorrect display of the user info display name ([#29826](https://github.com/element-hq/element-web/pull/29826)). Contributed by @langleyd.
|
||||
* RoomListStore: Remove invite rooms on decline ([#29804](https://github.com/element-hq/element-web/pull/29804)). Contributed by @MidhunSureshR.
|
||||
* Fix the buttons not being displayed with long preview text ([#29811](https://github.com/element-hq/element-web/pull/29811)). Contributed by @dbkr.
|
||||
* New room list: fix missing/incorrect notification decoration ([#29796](https://github.com/element-hq/element-web/pull/29796)). Contributed by @florianduros.
|
||||
* New Room List: Prevent potential scroll jump/flicker when switching spaces ([#29781](https://github.com/element-hq/element-web/pull/29781)). Contributed by @MidhunSureshR.
|
||||
* New room list: fix incorrect decoration ([#29770](https://github.com/element-hq/element-web/pull/29770)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [1.11.99](https://github.com/element-hq/element-web/releases/tag/v1.11.99) (2025-04-23)
|
||||
==================================================================================================
|
||||
No changes, just bumping the version to accommodate a new Element Desktop release
|
||||
|
||||
Changes in [1.11.98](https://github.com/element-hq/element-web/releases/tag/v1.11.98) (2025-04-22)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* print better errors in the search view instead of a blocking modal ([#29724](https://github.com/element-hq/element-web/pull/29724)). Contributed by @Jujure.
|
||||
* New room list: video room and video call decoration ([#29693](https://github.com/element-hq/element-web/pull/29693)). Contributed by @florianduros.
|
||||
* Remove Secure Backup, Cross-signing and Cryptography sections in `Security & Privacy` user settings ([#29088](https://github.com/element-hq/element-web/pull/29088)). Contributed by @florianduros.
|
||||
* Allow reporting a room when rejecting an invite. ([#29570](https://github.com/element-hq/element-web/pull/29570)). Contributed by @Half-Shot.
|
||||
* RoomListViewModel: Reset primary and secondary filters on space change ([#29672](https://github.com/element-hq/element-web/pull/29672)). Contributed by @MidhunSureshR.
|
||||
* RoomListStore: Support specific sorting requirements for muted rooms ([#29665](https://github.com/element-hq/element-web/pull/29665)). Contributed by @MidhunSureshR.
|
||||
* New room list: add notification options menu ([#29639](https://github.com/element-hq/element-web/pull/29639)). Contributed by @florianduros.
|
||||
* Room List: Scroll to top of the list when active room is not in the list ([#29650](https://github.com/element-hq/element-web/pull/29650)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix unwanted form submit behaviour in memberlist ([#29747](https://github.com/element-hq/element-web/pull/29747)). Contributed by @MidhunSureshR.
|
||||
* New room list: fix public room icon visibility when filter change ([#29737](https://github.com/element-hq/element-web/pull/29737)). Contributed by @florianduros.
|
||||
* Fix custom theme support for short hex \& rgba hex strings ([#29726](https://github.com/element-hq/element-web/pull/29726)). Contributed by @t3chguy.
|
||||
* New room list: minor visual fixes ([#29723](https://github.com/element-hq/element-web/pull/29723)). Contributed by @florianduros.
|
||||
* Fix getOidcCallbackUrl for Element Desktop ([#29711](https://github.com/element-hq/element-web/pull/29711)). Contributed by @t3chguy.
|
||||
* Fix some webp images improperly marked as animated ([#29713](https://github.com/element-hq/element-web/pull/29713)). Contributed by @Petersmit27.
|
||||
* Revert deletion of hydrateSession ([#29703](https://github.com/element-hq/element-web/pull/29703)). Contributed by @Jujure.
|
||||
* Fix converttoroom \& converttodm not working ([#29705](https://github.com/element-hq/element-web/pull/29705)). Contributed by @t3chguy.
|
||||
* Ensure forceCloseAllModals also closes priority/static modals ([#29706](https://github.com/element-hq/element-web/pull/29706)). Contributed by @t3chguy.
|
||||
* Continue button is disabled when uploading a recovery key file ([#29695](https://github.com/element-hq/element-web/pull/29695)). Contributed by @Giwayume.
|
||||
* Catch errors after syncing recovery ([#29691](https://github.com/element-hq/element-web/pull/29691)). Contributed by @andybalaam.
|
||||
* New room list: fix multiple visual issues ([#29673](https://github.com/element-hq/element-web/pull/29673)). Contributed by @florianduros.
|
||||
* New Room List: Fix mentions filter matching rooms with any highlight ([#29668](https://github.com/element-hq/element-web/pull/29668)). Contributed by @MidhunSureshR.
|
||||
* Fix truncated emoji label during emoji SAS ([#29643](https://github.com/element-hq/element-web/pull/29643)). Contributed by @florianduros.
|
||||
* Remove duplicate jitsi link ([#29642](https://github.com/element-hq/element-web/pull/29642)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros.
|
||||
* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy.
|
||||
* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot.
|
||||
* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr.
|
||||
* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy.
|
||||
* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros.
|
||||
* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR.
|
||||
* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot.
|
||||
* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown.
|
||||
* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure.
|
||||
* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot.
|
||||
* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR.
|
||||
* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot.
|
||||
* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros.
|
||||
* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr.
|
||||
* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR.
|
||||
* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy.
|
||||
* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros.
|
||||
* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy.
|
||||
* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros.
|
||||
* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr.
|
||||
* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy.
|
||||
* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy.
|
||||
* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh.
|
||||
* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR.
|
||||
* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR.
|
||||
* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam.
|
||||
* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123.
|
||||
|
||||
|
||||
Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* RoomListViewModel: Track the index of the active room in the list ([#29519](https://github.com/element-hq/element-web/pull/29519)). Contributed by @MidhunSureshR.
|
||||
* New room list: add empty state ([#29512](https://github.com/element-hq/element-web/pull/29512)). Contributed by @florianduros.
|
||||
* Implement `MessagePreviewViewModel` ([#29514](https://github.com/element-hq/element-web/pull/29514)). Contributed by @MidhunSureshR.
|
||||
* RoomListViewModel: Add functionality to toggle message preview setting ([#29511](https://github.com/element-hq/element-web/pull/29511)). Contributed by @MidhunSureshR.
|
||||
* New room list: add more options menu on room list item ([#29445](https://github.com/element-hq/element-web/pull/29445)). Contributed by @florianduros.
|
||||
* RoomListViewModel: Provide a way to resort the room list and track the active sort method ([#29499](https://github.com/element-hq/element-web/pull/29499)). Contributed by @MidhunSureshR.
|
||||
* Change \*All rooms\* meta space name to \*All Chats\* ([#29498](https://github.com/element-hq/element-web/pull/29498)). Contributed by @florianduros.
|
||||
* Add setting to hide avatars of rooms you have been invited to. ([#29497](https://github.com/element-hq/element-web/pull/29497)). Contributed by @Half-Shot.
|
||||
* Room List Store: Save preferred sorting algorithm and use that on app launch ([#29493](https://github.com/element-hq/element-web/pull/29493)). Contributed by @MidhunSureshR.
|
||||
* Add key storage toggle to Encryption settings ([#29310](https://github.com/element-hq/element-web/pull/29310)). Contributed by @dbkr.
|
||||
* New room list: add primary filters ([#29481](https://github.com/element-hq/element-web/pull/29481)). Contributed by @florianduros.
|
||||
* Implement MSC4142: Remove unintentional intentional mentions in replies ([#28209](https://github.com/element-hq/element-web/pull/28209)). Contributed by @tulir.
|
||||
* White background for 'They do not match' button ([#29470](https://github.com/element-hq/element-web/pull/29470)). Contributed by @andybalaam.
|
||||
* RoomListViewModel: Support secondary filters in the view model ([#29465](https://github.com/element-hq/element-web/pull/29465)). Contributed by @MidhunSureshR.
|
||||
* RoomListViewModel: Support primary filters in the view model ([#29454](https://github.com/element-hq/element-web/pull/29454)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Implement secondary filters ([#29458](https://github.com/element-hq/element-web/pull/29458)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Implement rest of the primary filters ([#29444](https://github.com/element-hq/element-web/pull/29444)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Support filters by implementing just the favourite filter ([#29433](https://github.com/element-hq/element-web/pull/29433)). Contributed by @MidhunSureshR.
|
||||
* Move toggle switch for integration manager for a11y ([#29436](https://github.com/element-hq/element-web/pull/29436)). Contributed by @Half-Shot.
|
||||
* New room list: basic flat list ([#29368](https://github.com/element-hq/element-web/pull/29368)). Contributed by @florianduros.
|
||||
* Improve rageshake upload experience by providing useful error information ([#29378](https://github.com/element-hq/element-web/pull/29378)). Contributed by @Half-Shot.
|
||||
* Add more functionality to the room list vm ([#29402](https://github.com/element-hq/element-web/pull/29402)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* New room list: fix compose menu action in space ([#29500](https://github.com/element-hq/element-web/pull/29500)). Contributed by @florianduros.
|
||||
* Change ToggleHiddenEventVisibility \& GoToHome KeyBindingActions ([#29374](https://github.com/element-hq/element-web/pull/29374)). Contributed by @gy-mate.
|
||||
* Fix Docker Healthcheck ([#29471](https://github.com/element-hq/element-web/pull/29471)). Contributed by @benbz.
|
||||
* Room List Store: Fetch rooms after space store is ready + attach store to window ([#29453](https://github.com/element-hq/element-web/pull/29453)). Contributed by @MidhunSureshR.
|
||||
* Room List Store: Fix bug where left rooms appear in room list ([#29452](https://github.com/element-hq/element-web/pull/29452)). Contributed by @MidhunSureshR.
|
||||
* Add space to the bottom of the room summary actions below leave room ([#29270](https://github.com/element-hq/element-web/pull/29270)). Contributed by @langleyd.
|
||||
* Show error screens in group calls ([#29254](https://github.com/element-hq/element-web/pull/29254)). Contributed by @robintown.
|
||||
* Prevent user from accidentally triggering multiple identity resets ([#29388](https://github.com/element-hq/element-web/pull/29388)). Contributed by @uhoreg.
|
||||
* Remove buggy tooltip on room intro \& homepage ([#29406](https://github.com/element-hq/element-web/pull/29406)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Room List Store: Filter rooms by active space ([#29399](https://github.com/element-hq/element-web/pull/29399)). Contributed by @MidhunSureshR.
|
||||
* Room List - Update the room list store on actions from the dispatcher ([#29397](https://github.com/element-hq/element-web/pull/29397)). Contributed by @MidhunSureshR.
|
||||
* Room List - Implement a minimal view model ([#29357](https://github.com/element-hq/element-web/pull/29357)). Contributed by @MidhunSureshR.
|
||||
* New room list: add space menu in room header ([#29352](https://github.com/element-hq/element-web/pull/29352)). Contributed by @florianduros.
|
||||
* Room List - Store sorted rooms in skip list ([#29345](https://github.com/element-hq/element-web/pull/29345)). Contributed by @MidhunSureshR.
|
||||
* New room list: add dial to search section ([#29359](https://github.com/element-hq/element-web/pull/29359)). Contributed by @florianduros.
|
||||
* New room list: add compose menu for spaces in header ([#29347](https://github.com/element-hq/element-web/pull/29347)). Contributed by @florianduros.
|
||||
* Use EditInPlace control for Identity Server picker to improve a11y ([#29280](https://github.com/element-hq/element-web/pull/29280)). Contributed by @Half-Shot.
|
||||
* First step to add header to new room list ([#29320](https://github.com/element-hq/element-web/pull/29320)). Contributed by @florianduros.
|
||||
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page ([#29312](https://github.com/element-hq/element-web/pull/29312)). Contributed by @t3chguy.
|
||||
* Honour the backup disable flag from Element X ([#29290](https://github.com/element-hq/element-web/pull/29290)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix edited code block width ([#29394](https://github.com/element-hq/element-web/pull/29394)). Contributed by @florianduros.
|
||||
* new room list: keep space name in one line in header ([#29369](https://github.com/element-hq/element-web/pull/29369)). Contributed by @florianduros.
|
||||
* Dismiss "Key storage out of sync" toast when secrets received ([#29348](https://github.com/element-hq/element-web/pull/29348)). Contributed by @richvdh.
|
||||
* Minor CSS fixes for the new room list ([#29334](https://github.com/element-hq/element-web/pull/29334)). Contributed by @florianduros.
|
||||
* Add padding to room header icon ([#29271](https://github.com/element-hq/element-web/pull/29271)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [1.11.94](https://github.com/element-hq/element-web/releases/tag/v1.11.94) (2025-02-27)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] fix: /tmp/element-web-config may already exist preventing the container from booting up ([#29377](https://github.com/element-hq/element-web/pull/29377)). Contributed by @RiotRobot.
|
||||
|
||||
|
||||
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy.
|
||||
* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh.
|
||||
* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh.
|
||||
* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot.
|
||||
* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros.
|
||||
* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros.
|
||||
* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR.
|
||||
* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent.
|
||||
* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros.
|
||||
* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR.
|
||||
* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy.
|
||||
* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros.
|
||||
* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros.
|
||||
* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros.
|
||||
* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd.
|
||||
* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr.
|
||||
* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy.
|
||||
* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown.
|
||||
* Revert `GoToHome` keyboard shortcut to `Ctrl`–`Shift`–`H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate.
|
||||
* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@@ -1,7 +1,5 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.15-labs@sha256:94edd5b349df43675bd6f542e2b9a24e7177432dec45fe3066bfcf2ab14c4355
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:ed0338dd02fd86861a59dc1cbc2e12152f3a93c4ce5933d347d6677232000dc7 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
|
||||
@@ -10,7 +8,7 @@ ARG JS_SDK_BRANCH="master"
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY --exclude=docker . /src
|
||||
COPY . /src
|
||||
RUN /src/scripts/docker-link-repos.sh
|
||||
RUN yarn --network-timeout=200000 install
|
||||
RUN /src/scripts/docker-package.sh
|
||||
@@ -19,20 +17,20 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0a49675e3e35cc2f89ce831f00f767af9c32df04f5a80167739fd32346f1fe99
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
# Install jq and moreutils for sponge, both used by our entrypoints
|
||||
RUN apk add jq moreutils
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
COPY --from=builder /src/webapp /app
|
||||
|
||||
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
||||
# through `envsubst` by the nginx docker image entry point.
|
||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
||||
|
||||
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
||||
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
||||
|
||||
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
||||
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
||||
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
@@ -42,5 +40,3 @@ USER nginx
|
||||
|
||||
# HTTP listen port
|
||||
ENV ELEMENT_WEB_PORT=80
|
||||
|
||||
HEALTHCHECK --start-period=5s CMD wget -q --spider http://localhost:$ELEMENT_WEB_PORT/config.json
|
||||
|
||||
@@ -31,7 +31,5 @@ module.exports = {
|
||||
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
["@babel/plugin-proposal-decorators", { version: "2023-11" }], // only needed by the js-sdk
|
||||
"@babel/plugin-transform-class-static-block", // only needed by the js-sdk for decorators
|
||||
],
|
||||
};
|
||||
|
||||
13
contribute.json
Normal file
13
contribute.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Element",
|
||||
"description": "A glossy Matrix collaboration client for the web.",
|
||||
"repository": {
|
||||
"url": "https://github.com/element-hq/element-web",
|
||||
"license": "AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/element-hq/element-web/issues",
|
||||
"report": "https://github.com/element-hq/element-web/issues/new/choose"
|
||||
},
|
||||
"keywords": ["chat", "riot", "matrix"]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Loads modules from `/modules` into config.json's `modules` field
|
||||
|
||||
set -e
|
||||
|
||||
entrypoint_log() {
|
||||
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
||||
echo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Copy these config files as a base
|
||||
mkdir -p /tmp/element-web-config
|
||||
cp /app/config*.json /tmp/element-web-config/
|
||||
|
||||
# If there are modules to be loaded
|
||||
if [ -d "/modules" ]; then
|
||||
cd /modules
|
||||
|
||||
for MODULE in *
|
||||
do
|
||||
# If the module has a package.json, use its main field as the entrypoint
|
||||
ENTRYPOINT="index.js"
|
||||
if [ -f "/modules/$MODULE/package.json" ]; then
|
||||
ENTRYPOINT=$(jq -r '.main' "/modules/$MODULE/package.json")
|
||||
fi
|
||||
|
||||
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
|
||||
|
||||
# Append the module to the config
|
||||
jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
|
||||
done
|
||||
fi
|
||||
@@ -18,12 +18,8 @@ server {
|
||||
}
|
||||
# covers config.json and config.hostname.json requests as it is prefix.
|
||||
location /config {
|
||||
root /tmp/element-web-config;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
location /modules {
|
||||
alias /modules;
|
||||
}
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
67
docs/MVVM.md
67
docs/MVVM.md
@@ -1,67 +0,0 @@
|
||||
# MVVM
|
||||
|
||||
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
|
||||
|
||||
1. Model: This is where the business logic and data resides.
|
||||
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
|
||||
3. View: This is the UI code itself and depends on the view model.
|
||||
|
||||
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
||||
|
||||
### Practical guidelines for MVVM in element-web
|
||||
|
||||
#### Model
|
||||
|
||||
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
||||
|
||||
#### View Model
|
||||
|
||||
1. View model is always a custom react hook named like `useFooViewModel()`.
|
||||
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
||||
```ts
|
||||
inteface FooViewState {
|
||||
somethingUseful: string;
|
||||
somethingElse: BarType;
|
||||
update: () => Promise<void>
|
||||
...
|
||||
}
|
||||
```
|
||||
3. Any react state that your UI needs must be in the view model.
|
||||
|
||||
#### View
|
||||
|
||||
1. Views are simple react components (eg: `FooView`).
|
||||
2. Views usually start by calling the view model hook, eg:
|
||||
```tsx
|
||||
const FooView: React.FC<IProps> = (props: IProps) => {
|
||||
const vm = useFooViewModel();
|
||||
....
|
||||
return(
|
||||
<div>
|
||||
{vm.somethingUseful}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
3. Views are also allowed to accept the view model as a prop, eg:
|
||||
```tsx
|
||||
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
||||
....
|
||||
return(
|
||||
<div>
|
||||
{vm.somethingUseful}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
4. Multiple views can share the same view model if necessary.
|
||||
|
||||
### Benefits
|
||||
|
||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||
|
||||
### Example
|
||||
|
||||
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
||||
@@ -46,6 +46,7 @@
|
||||
- [Skinning](skinning.md)
|
||||
- [Cider editor](ciderEditor.md)
|
||||
- [Iconography](icons.md)
|
||||
- [Jitsi](jitsi.md)
|
||||
- [Local echo](local-echo-dev.md)
|
||||
- [Media](media-handling.md)
|
||||
- [Room List Store](room-list-store.md)
|
||||
|
||||
@@ -163,14 +163,14 @@ These two options describe the various availability for the application. When th
|
||||
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
|
||||
at to see if the link should be to somewhere else.
|
||||
|
||||
Starting with `desktop_builds`, the following sub-properties are available:
|
||||
Starting with `desktop_builds`, the following subproperties are available:
|
||||
|
||||
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
|
||||
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
|
||||
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
|
||||
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
||||
5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
|
||||
6. `url_win64arm`: Optional. Direct link to download Windows ARM 64-bit desktop app.
|
||||
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
|
||||
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
|
||||
7. `url_linux`: Optional. Direct link to download Linux desktop app.
|
||||
|
||||
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
|
||||
@@ -384,6 +384,8 @@ The VoIP and Jitsi options are:
|
||||
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
||||
at any time without notice.
|
||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
|
||||
and may be removed at any time without notice. Defaults to `https://call.element.io`.
|
||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||
- `participant_limit`: The maximum number of users who can join a call; if
|
||||
|
||||
@@ -66,18 +66,6 @@ on other runtimes may require root privileges. To resolve this, either run the
|
||||
image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
||||
|
||||
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
|
||||
by being made available (e.g. via bind mount) in a directory within `/modules/`.
|
||||
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
|
||||
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
|
||||
|
||||
If you wish to use docker in read-only mode,
|
||||
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
|
||||
but additionally include the following directories:
|
||||
|
||||
- /tmp/
|
||||
- /etc/nginx/conf.d/
|
||||
|
||||
The behaviour of the docker image can be customised via the following
|
||||
environment variables:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
3
knip.ts
3
knip.ts
@@ -19,7 +19,6 @@ export default {
|
||||
ignore: [
|
||||
// Keep for now
|
||||
"src/hooks/useLocalStorageState.ts",
|
||||
"src/hooks/useTimeout.ts",
|
||||
"src/components/views/elements/InfoTooltip.tsx",
|
||||
"src/components/views/elements/StyledCheckbox.tsx",
|
||||
],
|
||||
@@ -40,8 +39,6 @@ export default {
|
||||
// Used by webpack
|
||||
"process",
|
||||
"util",
|
||||
// Embedded into webapp
|
||||
"@element-hq/element-call-embedded",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used in scripts & workflows
|
||||
|
||||
95
package.json
95
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.100",
|
||||
"version": "1.11.91",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -22,7 +22,8 @@
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"AUTHORS.rst",
|
||||
"package.json"
|
||||
"package.json",
|
||||
"contribute.json"
|
||||
],
|
||||
"style": "bundle.css",
|
||||
"matrix_i18n_extra_translation_funcs": [
|
||||
@@ -61,40 +62,38 @@
|
||||
"test": "jest",
|
||||
"test:playwright": "playwright test",
|
||||
"test:playwright:open": "yarn test:playwright --ui",
|
||||
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
||||
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
|
||||
"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"
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.0",
|
||||
"@playwright/test": "1.52.0",
|
||||
"@types/react": "19.1.4",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"oidc-client-ts": "3.2.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001717",
|
||||
"testcontainers": "10.25.0",
|
||||
"caniuse-lite": "1.0.30001697",
|
||||
"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.0.0",
|
||||
"@element-hq/element-web-module-api": "^0.1.1",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@matrix-org/analytics-events": "^0.29.2",
|
||||
"@matrix-org/analytics-events": "^0.29.0",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^9.0.0",
|
||||
"@sentry/browser": "^8.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||
"@vector-im/compound-web": "^7.10.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.3",
|
||||
"@vector-im/compound-design-tokens": "^3.0.0",
|
||||
"@vector-im/compound-web": "^7.6.1",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -108,7 +107,6 @@
|
||||
"css-tree": "^3.0.0",
|
||||
"diff-dom": "^5.0.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"domutils": "^3.2.2",
|
||||
"emojibase-regex": "15.3.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -117,15 +115,15 @@
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"html-react-parser": "^5.2.2",
|
||||
"is-ip": "^3.1.0",
|
||||
"js-xxhash": "^4.0.0",
|
||||
"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-element": "4.2.0",
|
||||
"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",
|
||||
@@ -138,22 +136,21 @@
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.240.6",
|
||||
"posthog-js": "1.157.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
"re-resizable": "6.10.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.16.0",
|
||||
"sanitize-html": "2.14.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.3.0",
|
||||
"temporal-polyfill": "^0.2.5",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uuid": "^11.0.0",
|
||||
"what-input": "^5.2.10"
|
||||
@@ -161,14 +158,13 @@
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
"@action-validator/core": "^0.6.0",
|
||||
"@axe-core/playwright": "^4.8.1",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-decorators": "^7.25.9",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
||||
"@babel/plugin-transform-class-static-block": "^7.26.0",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.20.7",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||
@@ -180,15 +176,13 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.10.0",
|
||||
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@rrweb/types": "^2.0.0-alpha.18",
|
||||
"@sentry/webpack-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^4.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@svgr/webpack": "^8.0.0",
|
||||
"@testcontainers/postgresql": "^10.16.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
@@ -212,11 +206,11 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.4",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.5",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
@@ -224,12 +218,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-loader": "^9.0.0",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"blob-polyfill": "^9.0.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"concurrently": "^9.0.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"core-js": "^3.38.1",
|
||||
"cronstrue": "^2.41.0",
|
||||
"css-loader": "^7.0.0",
|
||||
@@ -247,7 +241,7 @@
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"express": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
"fake-indexeddb": "^6.0.0",
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
@@ -262,14 +256,14 @@
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"jsqr": "^1.4.0",
|
||||
"knip": "^5.36.2",
|
||||
"lint-staged": "^16.0.0",
|
||||
"lint-staged": "^15.0.2",
|
||||
"mailpit-api": "^1.0.5",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimist": "^1.2.6",
|
||||
"modernizr": "^3.12.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"patch-package": "^8.0.0",
|
||||
"playwright-core": "^1.51.0",
|
||||
"playwright-core": "^1.45.1",
|
||||
"postcss": "8.4.46",
|
||||
"postcss-easings": "^4.0.0",
|
||||
"postcss-hexrgba": "2.1.0",
|
||||
@@ -280,20 +274,21 @@
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.5.3",
|
||||
"prettier": "3.4.2",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^10.20.0",
|
||||
"testcontainers": "^10.16.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript": "5.7.3",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
index 917a7fc..a2710c6 100644
|
||||
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||
@@ -37,7 +37,7 @@ export interface ModuleApi {
|
||||
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
|
||||
* dialog component if submitted.
|
||||
*/
|
||||
- openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||
+ openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C | null>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||
didOkOrSubmit: boolean;
|
||||
model: M;
|
||||
}>;
|
||||
@@ -1,31 +0,0 @@
|
||||
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.
|
||||
@@ -1,22 +0,0 @@
|
||||
diff --git a/node_modules/react-blurhash/dist/index.d.ts b/node_modules/react-blurhash/dist/index.d.ts
|
||||
index 3adbd0a..32e8c13 100644
|
||||
--- a/node_modules/react-blurhash/dist/index.d.ts
|
||||
+++ b/node_modules/react-blurhash/dist/index.d.ts
|
||||
@@ -19,7 +19,7 @@ declare class Blurhash extends React.PureComponent<Props$1> {
|
||||
resolutionY: number;
|
||||
};
|
||||
componentDidUpdate(): void;
|
||||
- render(): JSX.Element;
|
||||
+ render(): React.JSX.Element;
|
||||
}
|
||||
|
||||
declare type Props = React.CanvasHTMLAttributes<HTMLCanvasElement> & {
|
||||
@@ -37,7 +37,7 @@ declare class BlurhashCanvas extends React.PureComponent<Props> {
|
||||
componentDidUpdate(): void;
|
||||
handleRef: (canvas: HTMLCanvasElement) => void;
|
||||
draw: () => void;
|
||||
- render(): JSX.Element;
|
||||
+ render(): React.JSX.Element;
|
||||
}
|
||||
|
||||
export { Blurhash, BlurhashCanvas };
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
import { type WorkerOptions } from "./playwright/services";
|
||||
import { Options } from "./playwright/services";
|
||||
|
||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||
|
||||
@@ -21,7 +21,7 @@ const chromeProject = {
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig<WorkerOptions>({
|
||||
export default defineConfig<Options>({
|
||||
projects: [
|
||||
{
|
||||
name: "Chrome",
|
||||
@@ -83,7 +83,6 @@ export default defineConfig<WorkerOptions>({
|
||||
url: `${baseURL}/config.json`,
|
||||
reuseExistingServer: true,
|
||||
timeout: (process.env.CI ? 30 : 120) * 1000,
|
||||
stdout: "pipe",
|
||||
},
|
||||
testDir: "playwright/e2e",
|
||||
outputDir: "playwright/test-results",
|
||||
|
||||
12
playwright/@types/playwright-core.d.ts
vendored
Normal file
12
playwright/@types/playwright-core.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
declare module "playwright-core/lib/utils" {
|
||||
// This type is not public in playwright-core utils
|
||||
export function sanitizeForFilePath(filePath: string): string;
|
||||
}
|
||||
9
playwright/Dockerfile
Normal file
9
playwright/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.49.1-noble
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
# fonts-dejavu is needed for the same RTL rendering as on CI
|
||||
RUN apt-get update && apt-get -y install docker.io fonts-dejavu
|
||||
|
||||
COPY docker-entrypoint.sh /opt/docker-entrypoint.sh
|
||||
ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"]
|
||||
5
playwright/docker-entrypoint.sh
Normal file
5
playwright/docker-entrypoint.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
npx playwright test --update-snapshots --reporter line $@
|
||||
108
playwright/e2e/crypto/backups.spec.ts
Normal file
108
playwright/e2e/crypto/backups.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||
);
|
||||
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
||||
}
|
||||
|
||||
test.describe("Backups", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
test(
|
||||
"Create, delete and recreate a keys backup",
|
||||
{ tag: "@no-webkit" },
|
||||
async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page
|
||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
||||
.locator("..")
|
||||
.click();
|
||||
|
||||
await expectBackupVersionToBe(page, "1");
|
||||
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Delete it
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||
|
||||
// Create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// Should be successful
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page
|
||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
||||
.locator("..")
|
||||
.click();
|
||||
|
||||
await expectBackupVersionToBe(page, "2");
|
||||
|
||||
// ==
|
||||
// Ensure that if you don't have the secret storage passphrase the backup won't be created
|
||||
// ==
|
||||
|
||||
// First delete version 2
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Click "Delete Backup"
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click();
|
||||
|
||||
// Try to create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
|
||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
||||
// check that it failed
|
||||
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
|
||||
// cancel
|
||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||
|
||||
// go back to the settings to check that no backup was created (the setup button should still be there)
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -8,7 +8,14 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import {
|
||||
autoJoin,
|
||||
completeCreateSecretStorageDialog,
|
||||
copyAndContinue,
|
||||
createSharedRoomWithUser,
|
||||
enableKeyBackup,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
@@ -21,7 +28,7 @@ const checkDMRoom = async (page: Page) => {
|
||||
};
|
||||
|
||||
const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
||||
await expect(
|
||||
@@ -77,43 +84,85 @@ test.describe("Cryptography", function () {
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
||||
* @param keyType
|
||||
*/
|
||||
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
|
||||
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
|
||||
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
||||
keyType,
|
||||
);
|
||||
for (const isDeviceVerified of [true, false]) {
|
||||
test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => {
|
||||
/**
|
||||
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
||||
* @param keyType
|
||||
*/
|
||||
async function verifyKey(app: ElementAppPage, keyType: "master" | "self_signing" | "user_signing") {
|
||||
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
|
||||
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
||||
keyType,
|
||||
);
|
||||
expect(accountData.encrypted).toBeDefined();
|
||||
const keys = Object.keys(accountData.encrypted);
|
||||
const key = accountData.encrypted[keys[0]];
|
||||
expect(key.ciphertext).toBeDefined();
|
||||
expect(key.iv).toBeDefined();
|
||||
expect(key.mac).toBeDefined();
|
||||
}
|
||||
|
||||
expect(accountData.encrypted).toBeDefined();
|
||||
const keys = Object.keys(accountData.encrypted);
|
||||
const key = accountData.encrypted[keys[0]];
|
||||
expect(key.ciphertext).toBeDefined();
|
||||
expect(key.iv).toBeDefined();
|
||||
expect(key.mac).toBeDefined();
|
||||
test("by recovery code", async ({ page, app, user: aliceCredentials }) => {
|
||||
// Verified the device
|
||||
if (isDeviceVerified) {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
}
|
||||
|
||||
await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => {
|
||||
// We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
await verifyKey(app, "self_signing");
|
||||
await verifyKey(app, "user_signing");
|
||||
});
|
||||
|
||||
test("by passphrase", async ({ page, app, user: aliceCredentials }) => {
|
||||
// Verified the device
|
||||
if (isDeviceVerified) {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
}
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// Select passphrase option
|
||||
await dialog.getByText("Enter a Security Phrase").click();
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Fill passphrase input
|
||||
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
// Confirm passphrase
|
||||
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
|
||||
await copyAndContinue(page);
|
||||
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
await verifyKey(app, "self_signing");
|
||||
await verifyKey(app, "user_signing");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("Setting up key backup by recovery key", async ({ page, app, user: aliceCredentials }) => {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
|
||||
await enableKeyBackup(app);
|
||||
|
||||
// Wait for the cross signing keys to be uploaded
|
||||
// Waiting for "Change the recovery key" button ensure that all the secrets are uploaded and cached locally
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
await expect(encryptionTab.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
await verifyKey(app, "self_signing");
|
||||
await verifyKey(app, "user_signing");
|
||||
});
|
||||
|
||||
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
await enableKeyBackup(app);
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
|
||||
// Fetch the current cross-signing keys
|
||||
async function fetchMasterKey() {
|
||||
@@ -127,15 +176,18 @@ test.describe("Cryptography", function () {
|
||||
return k;
|
||||
});
|
||||
}
|
||||
|
||||
const masterKey1 = await fetchMasterKey();
|
||||
|
||||
// Find "the Reset cryptographic identity" button
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
// Find the "reset cross signing" button, and click it
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click();
|
||||
|
||||
// Confirm
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
|
||||
|
||||
// Enter the 4S key
|
||||
await page.getByPlaceholder("Recovery Key").fill(secretStorageKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Enter the password
|
||||
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
|
||||
@@ -145,6 +197,9 @@ test.describe("Cryptography", function () {
|
||||
const masterKey2 = await fetchMasterKey();
|
||||
expect(masterKey1).not.toEqual(masterKey2);
|
||||
}).toPass();
|
||||
|
||||
// The dialog should have gone away
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -8,9 +8,8 @@ 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";
|
||||
|
||||
const NAME = "Alice";
|
||||
|
||||
@@ -22,35 +21,41 @@ test.use({
|
||||
msc3814_enabled: true,
|
||||
},
|
||||
},
|
||||
config: async ({ config, context }, use) => {
|
||||
const wellKnown = {
|
||||
...config.default_server_config,
|
||||
"org.matrix.msc3814": true,
|
||||
};
|
||||
|
||||
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
|
||||
await route.fulfill({ json: wellKnown });
|
||||
});
|
||||
|
||||
await use(config);
|
||||
},
|
||||
});
|
||||
|
||||
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)
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
await app.closeDialog();
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Reset the identity key
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
// Set up recovery
|
||||
await page.getByRole("button", { name: "Set up recovery" }).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();
|
||||
// The Security tab should indicate that there is a dehydrated device present
|
||||
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
|
||||
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// the dehydrated device gets created with the name "Dehydrated
|
||||
// device". We want to make sure that it is not visible as a normal
|
||||
@@ -59,34 +64,7 @@ test.describe("Dehydration", () => {
|
||||
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
|
||||
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
|
||||
|
||||
// First it displays an informative panel about the recovery key
|
||||
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Next, it displays the new recovery key. We click on the copy button.
|
||||
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
|
||||
const recoveryKey = await app.getClipboard();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(
|
||||
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
|
||||
).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
|
||||
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
|
||||
|
||||
await app.settings.closeDialog();
|
||||
|
||||
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,60 +83,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);
|
||||
});
|
||||
|
||||
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Create a dehydrated device by setting up recovery (see "'Set up
|
||||
// recovery' creates dehydrated device" test above)
|
||||
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
|
||||
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
|
||||
|
||||
// First it displays an informative panel about the recovery key
|
||||
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Next, it displays the new recovery key. We click on the copy button.
|
||||
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
|
||||
const recoveryKey = await app.getClipboard();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(
|
||||
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
|
||||
).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
|
||||
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
|
||||
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
|
||||
// After recovery is set up, we reset our cryptographic identity, which
|
||||
// should drop the dehydrated device.
|
||||
await settingsDialogLocator.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expectDehydratedDeviceDisabled(app);
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
expect(dehydratedDeviceIds.length).toBe(1);
|
||||
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,29 +109,3 @@ async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait for our user to have a dehydrated device */
|
||||
async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise<void> {
|
||||
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
|
||||
//
|
||||
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
return dehydratedDeviceIds.length;
|
||||
})
|
||||
.toEqual(1);
|
||||
}
|
||||
|
||||
/** Wait for our user to not have a dehydrated device */
|
||||
async function expectDehydratedDeviceDisabled(app: ElementAppPage): Promise<void> {
|
||||
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
|
||||
//
|
||||
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
return dehydratedDeviceIds.length;
|
||||
})
|
||||
.toEqual(0);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
waitForVerificationRequest,
|
||||
} from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { Toasts } from "../../pages/toasts.ts";
|
||||
|
||||
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
let aliceBotClient: Bot;
|
||||
@@ -73,51 +72,6 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
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 }) => {
|
||||
// Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens
|
||||
// when we are in an encrypted room.
|
||||
await aliceBotClient.createRoom({
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: { algorithm: "m.megolm.v1.aes-sha2" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// In order to simulate a real environment more accurately, we need to slow down the arrival of the
|
||||
// `m.secret.send` to-device messages. That's slightly tricky to do directly, so instead we delay the *outgoing*
|
||||
// `m.secret.request` messages.
|
||||
await page.route("**/_matrix/client/v3/sendToDevice/m.secret.request/**", async (route) => {
|
||||
await route.fulfill({ json: {} });
|
||||
await new Promise((f) => setTimeout(f, 1000));
|
||||
await route.fetch();
|
||||
});
|
||||
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// 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 request and check that emojis are matching
|
||||
await doTwoWaySasVerification(page, verifier);
|
||||
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
|
||||
// There should be no toast (other than the notifications one)
|
||||
const toasts = new Toasts(page);
|
||||
await toasts.rejectToast("Notifications");
|
||||
await toasts.assertNoToasts();
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
@@ -324,7 +324,7 @@ test.describe("Cryptography", function () {
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity was reset",
|
||||
"Sender's verified identity has changed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,9 +29,7 @@ test.describe("Key storage out of sync toast", () => {
|
||||
});
|
||||
|
||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// We need to wait for there to be two toasts as the wait below won't work in isolation:
|
||||
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
||||
// it would always be checking the same toast, even if another one is now the first.
|
||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||
|
||||
|
||||
@@ -221,9 +221,6 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||
if (await useSecurityKey.isVisible()) {
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
@@ -292,28 +289,17 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the encryption settings and enable key storage and recovery
|
||||
* Assumes that the current device has been verified
|
||||
* Open the security settings and enable secure key backup.
|
||||
*
|
||||
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
||||
*
|
||||
* Returns the recovery key
|
||||
*/
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
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: "Set up recovery" }).click();
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText();
|
||||
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||
await encryptionTab.getByRole("textbox").fill(recoveryKey);
|
||||
await encryptionTab.getByRole("button", { name: "Finish set up" }).click();
|
||||
await app.settings.closeDialog();
|
||||
return recoveryKey;
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type APIRequestContext } from "@playwright/test";
|
||||
import { type APIRequestContext } from "playwright-core";
|
||||
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
|
||||
|
||||
import { type HomeserverInstance } from "../plugins/homeserver";
|
||||
import { ClientServerApi } from "../plugins/utils/api.ts";
|
||||
|
||||
/**
|
||||
* A small subset of the Client-Server API used to manipulate the state of the
|
||||
|
||||
@@ -267,6 +267,7 @@ test.describe("Editing", () => {
|
||||
app,
|
||||
room,
|
||||
axe,
|
||||
checkA11y,
|
||||
}) => {
|
||||
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
||||
|
||||
@@ -281,7 +282,7 @@ test.describe("Editing", () => {
|
||||
const line = tile.locator(".mx_EventTile_line");
|
||||
await line.hover();
|
||||
await line.getByRole("button", { name: "Edit" }).click();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
const editComposer = page.getByRole("textbox", { name: "Edit message" });
|
||||
await editComposer.pressSequentially("Foo");
|
||||
await editComposer.press("Backspace");
|
||||
@@ -289,7 +290,7 @@ test.describe("Editing", () => {
|
||||
await editComposer.press("Backspace");
|
||||
await editComposer.press("Enter");
|
||||
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
}
|
||||
await expect(
|
||||
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
|
||||
@@ -304,6 +305,7 @@ test.describe("Editing", () => {
|
||||
user,
|
||||
app,
|
||||
axe,
|
||||
checkA11y,
|
||||
bot: bob,
|
||||
}) => {
|
||||
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
||||
|
||||
@@ -6,21 +6,22 @@ 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 CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
|
||||
import { expect, test as base } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { type Credentials } from "../../plugins/homeserver";
|
||||
|
||||
const email = "user@nowhere.dummy";
|
||||
|
||||
const test = base.extend({
|
||||
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
credentials: async ({}, use, testInfo) => {
|
||||
await use({
|
||||
username: `user_${testInfo.testId}`,
|
||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||
password: "oETo7MPf0o",
|
||||
} as CredentialsWithDisplayName);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ test.describe("Invite dialog", function () {
|
||||
"should support inviting a user to Direct Messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
|
||||
const other = page.locator(".mx_InviteDialog_other");
|
||||
// Assert that the header is rendered
|
||||
|
||||
@@ -1,301 +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 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";
|
||||
|
||||
test.describe("Room list filters and sort", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: true,
|
||||
},
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
function getPrimaryFilters(page: Page): Locator {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
|
||||
function getSecondaryFilters(page: Page): Locator {
|
||||
return page.getByRole("button", { name: "Filter" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("Tombstoned rooms are not shown even when they receive updates", async ({ page, app, bot }) => {
|
||||
// This bug shows up with this setting turned on
|
||||
await app.settings.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, true);
|
||||
|
||||
/*
|
||||
We will first create a room named 'Old Room' and will invite the bot user to this room.
|
||||
We will also send a simple message in this room.
|
||||
*/
|
||||
const oldRoomId = await app.client.createRoom({ name: "Old Room" });
|
||||
await app.client.inviteUser(oldRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(oldRoomId);
|
||||
const response = await app.client.sendMessage(oldRoomId, "Hello!");
|
||||
|
||||
/*
|
||||
At this point, we haven't done anything interesting.
|
||||
So we expect 'Old Room' to show up in the room list.
|
||||
*/
|
||||
const roomListView = getRoomList(page);
|
||||
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
|
||||
await expect(oldRoomTile).toBeVisible();
|
||||
|
||||
/*
|
||||
Now let's tombstone 'Old Room'.
|
||||
First we create a new room ('New Room') with the predecessor set to the old room..
|
||||
*/
|
||||
const newRoomId = await bot.createRoom({
|
||||
name: "New Room",
|
||||
creation_content: {
|
||||
predecessor: {
|
||||
event_id: response.event_id,
|
||||
room_id: oldRoomId,
|
||||
},
|
||||
},
|
||||
visibility: "public" as Visibility,
|
||||
});
|
||||
|
||||
/*
|
||||
Now we can send the tombstone event itself to the 'Old Room'.
|
||||
*/
|
||||
await app.client.sendStateEvent(oldRoomId, "m.room.tombstone", {
|
||||
body: "This room has been replaced",
|
||||
replacement_room: newRoomId,
|
||||
});
|
||||
|
||||
// Let's join the replaced room.
|
||||
await app.client.joinRoom(newRoomId);
|
||||
|
||||
// We expect 'Old Room' to be hidden from the room list.
|
||||
await expect(oldRoomTile).not.toBeVisible();
|
||||
|
||||
/*
|
||||
Let's say some user in the 'Old Room' changes their display name.
|
||||
This will send events to the all the rooms including 'Old Room'.
|
||||
Nevertheless, the replaced room should not be shown in the room list.
|
||||
*/
|
||||
await bot.setDisplayName("MyNewName");
|
||||
await expect(oldRoomTile).not.toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Scroll behaviour", () => {
|
||||
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
|
||||
page,
|
||||
app,
|
||||
}) => {
|
||||
const createFavouriteRoom = async (name: string) => {
|
||||
const id = await app.client.createRoom({
|
||||
name,
|
||||
});
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, id);
|
||||
};
|
||||
|
||||
// Create 5 favourite rooms
|
||||
let i = 0;
|
||||
for (; i < 5; i++) {
|
||||
await createFavouriteRoom(`room${i}-fav`);
|
||||
}
|
||||
|
||||
// Create a non-favourite room
|
||||
await app.client.createRoom({ name: `room-non-fav` });
|
||||
|
||||
// Create rest of the favourite rooms
|
||||
for (; i < 20; i++) {
|
||||
await createFavouriteRoom(`room${i}-fav`);
|
||||
}
|
||||
|
||||
// Open the non-favourite room
|
||||
const roomListView = getRoomList(page);
|
||||
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
await tile.click();
|
||||
|
||||
// Enable Favourite filter
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(tile).not.toBeVisible();
|
||||
|
||||
// Ensure the room list is not scrolled
|
||||
const isScrolledDown = await page
|
||||
.getByRole("grid", { name: "Room list" })
|
||||
.evaluate((e) => e.scrollTop !== 0);
|
||||
expect(isScrolledDown).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
let unReadDmId: string | undefined;
|
||||
let unReadRoomId: string | undefined;
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await app.client.joinRoom(unReadDmId);
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
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);
|
||||
});
|
||||
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
const allFilters = await primaryFilters.locator("option").all();
|
||||
for (const filter of allFilters) {
|
||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||
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();
|
||||
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();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(4);
|
||||
});
|
||||
|
||||
test("should filter the list (with secondary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const secondaryFilters = getSecondaryFilters(page);
|
||||
await secondaryFilters.click();
|
||||
|
||||
await expect(page.getByRole("menu", { name: "Filter" })).toMatchScreenshot("filter-menu.png");
|
||||
|
||||
await page.getByRole("menuitem", { name: "Low priority" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
});
|
||||
|
||||
test(
|
||||
"unread filter should only match unread rooms that have a count",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||
await app.viewRoomById(unReadDmId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Let's open a room other than unread room or unread dm
|
||||
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||
|
||||
// Let's make the bot send a new message in both rooms
|
||||
await bot.sendMessage(unReadDmId, "Hello!");
|
||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||
|
||||
// Let's activate the unread filter now
|
||||
await page.getByRole("option", { name: "Unread" }).click();
|
||||
|
||||
// Unread filter should only show unread room and not unread dm!
|
||||
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
|
||||
await expect(unreadDm).toBeVisible();
|
||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Empty room list", () => {
|
||||
/**
|
||||
* Get the empty state
|
||||
* @param page
|
||||
*/
|
||||
function getEmptyRoomList(page: Page) {
|
||||
return page.getByTestId("empty-room-list");
|
||||
}
|
||||
|
||||
test(
|
||||
"should render the default placeholder when there is no filter",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
|
||||
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot("unread-empty-room-list.png");
|
||||
|
||||
await emptyRoomList.getByRole("button", { name: "show all chats" }).click();
|
||||
await expect(primaryFilters.getByRole("option", { name: "Unread" })).not.toBeChecked();
|
||||
});
|
||||
|
||||
["People", "Rooms", "Favourite"].forEach((filter) => {
|
||||
test(
|
||||
`should render the placeholder for ${filter} filter`,
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||
|
||||
const emptyRoomList = getEmptyRoomList(page);
|
||||
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,87 +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 type { Page } from "@playwright/test";
|
||||
|
||||
test.describe("Header section of the room list", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the header section of the room list
|
||||
* @param page
|
||||
*/
|
||||
function getHeaderSection(page: Page) {
|
||||
return page.getByTestId("room-list-header");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("should render the header section", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListHeader = getHeaderSection(page);
|
||||
await expect(roomListHeader).toMatchScreenshot("room-list-header.png");
|
||||
|
||||
const composeMenu = roomListHeader.getByRole("button", { name: "Add" });
|
||||
await composeMenu.click();
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||
|
||||
// New message should open the direct messages dialog
|
||||
await page.getByRole("menuitem", { name: "New message" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// New room should open the room creation dialog
|
||||
await composeMenu.click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Create a private room" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
});
|
||||
|
||||
test("should render the header section for a space", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await app.client.createSpace({ name: "MySpace" });
|
||||
await page.getByRole("button", { name: "MySpace" }).click();
|
||||
|
||||
const roomListHeader = getHeaderSection(page);
|
||||
await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png");
|
||||
|
||||
await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
|
||||
await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible();
|
||||
|
||||
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
|
||||
await spaceMenu.click();
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-space-menu.png");
|
||||
|
||||
// It should open the space home
|
||||
await page.getByRole("menuitem", { name: "Space home" }).click();
|
||||
await expect(page.getByRole("main").getByRole("heading", { name: "MySpace" })).toBeVisible();
|
||||
|
||||
// It should open the invite dialog
|
||||
await spaceMenu.click();
|
||||
await page.getByRole("menuitem", { name: "Invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to MySpace" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should open the space preferences
|
||||
await spaceMenu.click();
|
||||
await page.getByRole("menuitem", { name: "Preferences" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should open the space settings
|
||||
await spaceMenu.click();
|
||||
await page.getByRole("menuitem", { name: "Space Settings" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
});
|
||||
});
|
||||
@@ -1,402 +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 { expect, test } from "../../../element-web-test";
|
||||
|
||||
test.describe("Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test.describe("Room list", () => {
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
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();
|
||||
// Scroll to the end of the room 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");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).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" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
|
||||
// Default settings should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");
|
||||
|
||||
// It should make the room muted
|
||||
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
||||
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
// The room decoration should have the muted icon
|
||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||
|
||||
await roomItem.hover();
|
||||
// On hover, the room should show the muted icon
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");
|
||||
|
||||
roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||
await roomItemMenu.click();
|
||||
// The Mute room option should be selected
|
||||
await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
|
||||
});
|
||||
|
||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
// Put focus on the room list
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||
|
||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||
|
||||
await filters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("Shortcuts", () => {
|
||||
test("should select the next room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should select the previous room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should select the last room", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await page.keyboard.press("Alt+ArrowUp");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should select the next unread room", async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "1 notification" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
|
||||
|
||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Keyboard navigation", () => {
|
||||
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
|
||||
|
||||
// open the room
|
||||
await room29.click();
|
||||
// put focus back on the room list item
|
||||
await room29.click();
|
||||
await expect(room29).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ArrowDown");
|
||||
await expect(room28).toBeFocused();
|
||||
await expect(room29).not.toBeFocused();
|
||||
|
||||
await page.keyboard.press("ArrowUp");
|
||||
await expect(room29).toBeFocused();
|
||||
await expect(room28).not.toBeFocused();
|
||||
});
|
||||
|
||||
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||
|
||||
await room29.click();
|
||||
// put focus back on the room list item
|
||||
await room29.click();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(moreButton).toBeFocused();
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(notificationButton).toBeFocused();
|
||||
|
||||
// Open the menu
|
||||
await notificationButton.click();
|
||||
// Wait for the menu to be open
|
||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
|
||||
// Close the menu
|
||||
await page.keyboard.press("Escape");
|
||||
// Focus should be back on the room list item
|
||||
await expect(room29).toBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Avatar decoration", () => {
|
||||
test.use({ labsFlags: ["feature_video_rooms", "feature_new_room_list"] });
|
||||
|
||||
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
// @ts-ignore Visibility enum is not accessible
|
||||
await app.client.createRoom({ name: "public room", visibility: "public" });
|
||||
const roomListView = getRoomList(page);
|
||||
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||
|
||||
await expect(publicRoom).toBeVisible();
|
||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||
});
|
||||
|
||||
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
|
||||
await page.getByRole("menuitem", { name: "New video room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("video room");
|
||||
await page.getByRole("button", { name: "Create video room" }).click();
|
||||
|
||||
const roomListView = getRoomList(page);
|
||||
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||
await expect(videoRoom).toBeVisible();
|
||||
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Notification decoration", () => {
|
||||
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
await bot.createRoom({
|
||||
name: "invited room",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||
await expect(invitedRoom).toBeVisible();
|
||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||
});
|
||||
|
||||
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "2 notifications" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||
});
|
||||
|
||||
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mention" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const clientBot = await bot.prepareClient();
|
||||
await clientBot.evaluate(
|
||||
async (client, { roomId, userId }) => {
|
||||
await client.sendMessage(roomId, {
|
||||
// @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],
|
||||
},
|
||||
});
|
||||
},
|
||||
{ roomId, userId: user.userId },
|
||||
);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||
await expect(room).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||
});
|
||||
|
||||
test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
await page.getByRole("button", { name: "Room Options" }).click();
|
||||
await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click();
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "activity" });
|
||||
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);
|
||||
|
||||
const otherRoomId = await app.client.createRoom({ name: "other room" });
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "activity" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("@mentions & keywords").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
await app.settings.openUserSettings("Notifications");
|
||||
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// Switch to the other room to avoid the notification to be cleared
|
||||
await app.viewRoomById(otherRoomId);
|
||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||
});
|
||||
|
||||
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "mark as unread" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||
await room.hover();
|
||||
await room.getByRole("button", { name: "More Options" }).click();
|
||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||
|
||||
// Remove hover on the room list item
|
||||
await roomListView.hover();
|
||||
|
||||
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
|
||||
});
|
||||
|
||||
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
|
||||
const roomId = await app.client.createRoom({ name: "silent" });
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
|
||||
await app.viewRoomById(roomId);
|
||||
await app.settings.openRoomSettings("Notifications");
|
||||
await page.getByText("Off").click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Room list panel", () => {
|
||||
test.describe("Search section of the room list", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
@@ -19,23 +19,16 @@ test.describe("Room list panel", () => {
|
||||
* @param page
|
||||
*/
|
||||
function getRoomListView(page: Page) {
|
||||
return page.getByTestId("room-list-panel");
|
||||
return page.getByTestId("room-list-view");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
|
||||
// Populate the room list
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomListView(page);
|
||||
// Wait for the last room to be visible
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||
await expect(roomListView).toMatchScreenshot("room-list-view.png");
|
||||
});
|
||||
});
|
||||
@@ -6,14 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { type Page } from "playwright-core";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { createBot } from "../crypto/utils.ts";
|
||||
|
||||
// This test requires fixed credentials for the device signing keys below to work
|
||||
const username = "user1234";
|
||||
@@ -121,7 +120,7 @@ test.describe("Login", () => {
|
||||
credentials,
|
||||
page,
|
||||
homeserver,
|
||||
axe,
|
||||
checkA11y,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
@@ -150,7 +149,7 @@ test.describe("Login", () => {
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
|
||||
// cy.percySnapshot("Login");
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
@@ -259,71 +258,6 @@ test.describe("Login", () => {
|
||||
|
||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Continues to show verification prompt after cancelling device verification", async ({
|
||||
page,
|
||||
homeserver,
|
||||
credentials,
|
||||
}) => {
|
||||
// Create a different device which is cross-signed, meaning we need to verify this device
|
||||
await createBot(page, homeserver, credentials, true);
|
||||
|
||||
// Wait to avoid homeserver rate limit on logins
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Load the page and see that we are asked to verify
|
||||
await page.goto("/#/welcome");
|
||||
await login(page, homeserver, credentials);
|
||||
let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
|
||||
// Click "Verify with another device"
|
||||
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||
|
||||
// Cancel the new dialog
|
||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||
|
||||
// Check that we are still being asked to verify
|
||||
h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
import { type Locator, type Page } from "playwright-core";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
|
||||
@@ -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!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MailpitClient } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
import { type MailpitClient } from "mailpit-api";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { expect } from "../../element-web-test";
|
||||
|
||||
@@ -73,32 +73,4 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await revokeAccessTokenPromise;
|
||||
await revokeRefreshTokenPromise;
|
||||
});
|
||||
|
||||
test(
|
||||
"it should log out the user & wipe data when logging out via MAS",
|
||||
{ tag: "@screenshot" },
|
||||
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, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await expect(page.getByText("Welcome")).toBeVisible();
|
||||
await page.goto("about:blank");
|
||||
|
||||
const result = await mas.manage("kill-sessions", userId);
|
||||
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
|
||||
|
||||
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 });
|
||||
|
||||
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
||||
expect(localStorageKeys).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
|
||||
test(
|
||||
"registers an account and lands on the home page",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, mailpitClient, request, axe }) => {
|
||||
async ({ page, mailpitClient, request, checkA11y }) => {
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
||||
@@ -47,7 +47,7 @@ test.describe("Email Registration", async () => {
|
||||
|
||||
await expect(page.getByText("Check your email to continue")).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions);
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
|
||||
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ test.describe("Registration", () => {
|
||||
test(
|
||||
"registers an account and lands on the home screen",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ homeserver, page, axe, crypto }) => {
|
||||
async ({ homeserver, page, checkA11y, crypto }) => {
|
||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
@@ -52,7 +52,7 @@ test.describe("Registration", () => {
|
||||
includeDialogBackground: true,
|
||||
};
|
||||
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
|
||||
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
|
||||
@@ -62,12 +62,12 @@ test.describe("Registration", () => {
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
|
||||
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
|
||||
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link
|
||||
|
||||
@@ -43,7 +43,6 @@ test.describe("Pills", () => {
|
||||
|
||||
// go back to the message room and try to click on the pill text, as a user would
|
||||
await app.viewRoomByName(messageRoom);
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/${messageRoomId}`));
|
||||
const pillText = page.locator(".mx_EventTile_body .mx_Pill .mx_Pill_text");
|
||||
await expect(pillText).toHaveCSS("pointer-events", "none");
|
||||
await pillText.click({ force: true }); // force is to ensure we bypass pointer-events
|
||||
|
||||
@@ -15,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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -10,8 +10,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 +20,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 });
|
||||
});
|
||||
|
||||
@@ -72,15 +67,6 @@ test.describe("RightPanel", () => {
|
||||
},
|
||||
);
|
||||
|
||||
test("should have padding under leave room", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
const leaveButton = await page.getByRole("menuitem", { name: "Leave Room" });
|
||||
await leaveButton.scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png");
|
||||
});
|
||||
|
||||
test("should handle clicking add widgets", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
@@ -138,65 +124,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 }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.getByRole("menuitem", { name: "Report room" }).click();
|
||||
const dialog = await page.getByRole("dialog", { name: "Report Room" });
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await expect(dialog).toMatchScreenshot("room-report-dialog.png");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
|
||||
// Dialog should have gone
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
|
||||
});
|
||||
test("should handle reporting a room and leaving the room", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.getByRole("menuitem", { name: "Report room" }).click();
|
||||
const dialog = await page.getByRole("dialog", { name: "Report room" });
|
||||
await dialog.getByRole("switch", { name: "Leave room" }).click();
|
||||
await dialog.getByLabel("reason").fill("This room should be reported");
|
||||
await dialog.getByRole("button", { name: "Send report" }).click();
|
||||
await page.getByRole("dialog", { name: "Leave room" }).getByRole("button", { name: "Leave" }).click();
|
||||
|
||||
// Dialog should have gone
|
||||
await expect(page.locator(".mx_Dialog")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("in spaces", () => {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Invites", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
displayName: "Bob",
|
||||
},
|
||||
});
|
||||
|
||||
test("should render an invite view", { tag: "@screenshot" }, async ({ page, homeserver, user, bot, app }) => {
|
||||
const roomId = await bot.createRoom({ is_direct: true });
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
await app.viewRoomByName("Bob");
|
||||
await expect(page.locator(".mx_RoomView")).toMatchScreenshot("Invites_room_view.png", {
|
||||
// Hide the mxid, which is not stable.
|
||||
css: `
|
||||
.mx_RoomPreviewBar_inviter_mxid {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to decline an invite", async ({ page, homeserver, user, bot, app }) => {
|
||||
const roomId = await bot.createRoom({ is_direct: true });
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
await app.viewRoomByName("Bob");
|
||||
await page.getByRole("button", { name: "Decline", exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Bob", exact: true }),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should be able to decline an invite, report the room and ignore the user",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, homeserver, user, bot, app }) => {
|
||||
const roomId = await bot.createRoom({ is_direct: true });
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
await app.viewRoomByName("Bob");
|
||||
await page.getByRole("button", { name: "Decline and block" }).click();
|
||||
await page.getByLabel("Ignore user").click();
|
||||
await page.getByLabel("Report room").click();
|
||||
await page.getByLabel("Reason").fill("Do not want the room");
|
||||
const roomReported = page.waitForRequest(
|
||||
(req) =>
|
||||
req.url().endsWith(`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/report`) &&
|
||||
req.method() === "POST",
|
||||
);
|
||||
await expect(page.getByRole("dialog", { name: "Decline invitation" })).toMatchScreenshot(
|
||||
"Invites_reject_dialog.png",
|
||||
);
|
||||
await page.getByRole("button", { name: "Decline invite" }).click();
|
||||
|
||||
// Check room was reported.
|
||||
await roomReported;
|
||||
|
||||
// Check user is ignored.
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
const ignoredUsersList = page.getByRole("list", { name: "Ignored users" });
|
||||
await ignoredUsersList.scrollIntoViewIfNeeded();
|
||||
await expect(ignoredUsersList.getByRole("listitem", { name: bot.credentials.userId })).toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -7,7 +7,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { type Visibility } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
@@ -86,15 +85,6 @@ test.describe("Room Header", () => {
|
||||
await expect(header).toMatchScreenshot("room-header-long-name.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should render room header icon correctly", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
await app.client.createRoom({ name: "Test Room", visibility: "public" as Visibility });
|
||||
await app.viewRoomByName("Test Room");
|
||||
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
|
||||
await expect(header).toMatchScreenshot("room-header-with-icon.png");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("with a video room", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -50,8 +50,8 @@ test.describe("Appearance user settings tab", () => {
|
||||
// Click "Show advanced" link button
|
||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||
|
||||
await tab.getByLabel("Use bundled emoji font").click();
|
||||
await tab.getByLabel("Use a system font").click();
|
||||
await tab.locator(".mx_Checkbox", { hasText: "Use bundled emoji font" }).click();
|
||||
await tab.locator(".mx_Checkbox", { hasText: "Use a system font" }).click();
|
||||
|
||||
// Assert that the font-family value was removed
|
||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||
|
||||
@@ -17,164 +17,98 @@ import {
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Encryption tab", () => {
|
||||
test.use({ displayName: "Alice" });
|
||||
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? You’ll 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({}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
// Initially, our device is not verified
|
||||
await expect(settings.getByRole("heading", { name: "Device not verified" })).toBeVisible();
|
||||
|
||||
// We will reset our identity
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
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.locator("li").filter({ hasText: "Encryption" }).click({ force: true });
|
||||
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||
|
||||
// Finally we actually continue
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Now we are verified, so we see the Key storage toggle
|
||||
await expect(settings.getByRole("heading", { name: "Key storage" })).toBeVisible();
|
||||
});
|
||||
// The user is prompted to reset their identity
|
||||
await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,10 +28,7 @@ test.describe("Preferences user settings tab", () => {
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
|
||||
// masked due to daylight saving time
|
||||
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
|
||||
});
|
||||
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png");
|
||||
});
|
||||
|
||||
test("should be able to change the app language", { tag: ["@no-firefox", "@no-webkit"] }, async ({ uut, user }) => {
|
||||
|
||||
@@ -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 { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Quick settings menu", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
await page.getByRole("button", { name: "Quick settings" }).click();
|
||||
// Assert that the top heading is renderedc
|
||||
const settings = page.getByTestId("quick-settings-menu");
|
||||
await expect(settings).toBeVisible();
|
||||
await expect(settings).toMatchScreenshot("quick-settings.png");
|
||||
});
|
||||
});
|
||||
@@ -37,15 +37,6 @@ test.describe("Roles & Permissions room settings tab", () => {
|
||||
// Change the role of Alice to Moderator (50)
|
||||
await combobox.selectOption("Moderator");
|
||||
await expect(combobox).toHaveValue("50");
|
||||
|
||||
// Should display a modal to warn that we are demoting the only admin user
|
||||
const modal = await page.locator(".mx_Dialog", {
|
||||
hasText: "Warning",
|
||||
});
|
||||
await expect(modal).toBeVisible();
|
||||
// Click on the continue button in the modal
|
||||
await modal.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const respPromise = page.waitForRequest("**/state/**");
|
||||
await applyButton.click();
|
||||
await respPromise;
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe("Security user settings tab", () => {
|
||||
});
|
||||
|
||||
test.describe("AnalyticsLearnMoreDialog", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
await tab.getByRole("button", { name: "Learn more" }).click();
|
||||
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
|
||||
@@ -41,57 +41,16 @@ test.describe("Security user settings tab", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to set an ID server", async ({ app, context, user, page }) => {
|
||||
test("should contain section to set ID server", async ({ app }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {},
|
||||
});
|
||||
});
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2/account/register", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
token: "AToken",
|
||||
},
|
||||
});
|
||||
});
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2/account", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
user_id: user.userId,
|
||||
},
|
||||
});
|
||||
});
|
||||
await context.route("https://identity.example.org/_matrix/identity/v2/terms", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
json: {
|
||||
policies: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
const setIdServer = tab.locator(".mx_IdentityServerPicker");
|
||||
const setIdServer = tab.locator(".mx_SetIdServer");
|
||||
await setIdServer.scrollIntoViewIfNeeded();
|
||||
|
||||
const textElement = setIdServer.getByRole("textbox", { name: "Enter a new identity server" });
|
||||
await textElement.click();
|
||||
await textElement.fill("https://identity.example.org");
|
||||
await setIdServer.getByRole("button", { name: "Change" }).click();
|
||||
|
||||
await expect(setIdServer.getByText("Checking server")).toBeVisible();
|
||||
// Accept terms
|
||||
await page.getByTestId("dialog-primary-button").click();
|
||||
// Check identity has changed.
|
||||
await expect(setIdServer.getByText("Your identity server has been changed")).toBeVisible();
|
||||
// Ensure section title is updated.
|
||||
await expect(tab.getByText(`Identity server (identity.example.org)`, { exact: true })).toBeVisible();
|
||||
// Assert that an input area for identity server exists
|
||||
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show integrations as enabled", async ({ app, page, user }) => {
|
||||
test("should enable show integrations as enabled", async ({ app, page }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
|
||||
const setIntegrationManager = tab.locator(".mx_SetIntegrationManager");
|
||||
@@ -102,9 +61,7 @@ test.describe("Security user settings tab", () => {
|
||||
}),
|
||||
).toBeVisible();
|
||||
// Make sure integration manager's toggle switch is enabled
|
||||
const toggleswitch = setIntegrationManager.getByLabel("Enable the integration manager");
|
||||
await expect(toggleswitch).toBeVisible();
|
||||
await expect(toggleswitch).toBeChecked();
|
||||
await expect(setIntegrationManager.locator(".mx_ToggleSwitch_enabled")).toBeVisible();
|
||||
await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText(
|
||||
"Manage integrations(scalar.vector.im)",
|
||||
);
|
||||
|
||||
@@ -7,15 +7,47 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page, type Request } from "@playwright/test";
|
||||
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
|
||||
const test = base.extend<{
|
||||
slidingSyncProxy: StartedTestContainer;
|
||||
testRoom: { roomId: string; name: string };
|
||||
joinedBot: Bot;
|
||||
}>({
|
||||
slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => {
|
||||
const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3")
|
||||
.withNetwork(network)
|
||||
.withExposedPorts(8008)
|
||||
.withLogConsumer(logger.getConsumer("sliding-sync-proxy"))
|
||||
.withWaitStrategy(Wait.forHttp("/client/server.json", 8008))
|
||||
.withEnvironment({
|
||||
SYNCV3_SECRET: "bwahahaha",
|
||||
SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`,
|
||||
SYNCV3_SERVER: `http://homeserver:8008`,
|
||||
})
|
||||
.start();
|
||||
|
||||
const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`;
|
||||
await page.addInitScript((proxyAddress) => {
|
||||
window.localStorage.setItem(
|
||||
"mx_local_settings",
|
||||
JSON.stringify({
|
||||
feature_sliding_sync_proxy_url: proxyAddress,
|
||||
}),
|
||||
);
|
||||
window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true");
|
||||
}, proxyAddress);
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
// Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script
|
||||
credentials: async ({ slidingSyncProxy, credentials }, use) => {
|
||||
await use(credentials);
|
||||
},
|
||||
testRoom: async ({ user, app }, use) => {
|
||||
const name = "Test Room";
|
||||
const roomId = await app.client.createRoom({ name });
|
||||
@@ -50,14 +82,6 @@ test.describe("Sliding Sync", () => {
|
||||
});
|
||||
};
|
||||
|
||||
test.use({
|
||||
config: {
|
||||
features: {
|
||||
feature_simplified_sliding_sync: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Load the user fixture for all tests
|
||||
test.beforeEach(({ user }) => {});
|
||||
|
||||
@@ -164,7 +188,15 @@ test.describe("Sliding Sync", () => {
|
||||
).not.toBeAttached();
|
||||
});
|
||||
|
||||
test("should show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => {
|
||||
test("should not show unread indicators", async ({ page, app, joinedBot: bot, testRoom }) => {
|
||||
// TODO: for now. Later we should.
|
||||
|
||||
// disable notifs in this room (TODO: CS API call?)
|
||||
const locator = page.getByRole("treeitem", { name: "Test Room" });
|
||||
await locator.hover();
|
||||
await locator.getByRole("button", { name: "Notification options" }).click();
|
||||
await page.getByRole("menuitemradio", { name: "Mute room" }).click();
|
||||
|
||||
// create a new room so we know when the message has been received as it'll re-shuffle the room list
|
||||
await app.client.createRoom({ name: "Dummy" });
|
||||
|
||||
@@ -175,7 +207,9 @@ test.describe("Sliding Sync", () => {
|
||||
// wait for this message to arrive, tell by the room list resorting
|
||||
await checkOrder(["Test Room", "Dummy"], page);
|
||||
|
||||
await expect(page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge")).toBeAttached();
|
||||
await expect(
|
||||
page.getByRole("treeitem", { name: "Test Room" }).locator(".mx_NotificationBadge"),
|
||||
).not.toBeAttached();
|
||||
});
|
||||
|
||||
test("should update user settings promptly", async ({ page, app }) => {
|
||||
@@ -187,37 +221,6 @@ test.describe("Sliding Sync", () => {
|
||||
await expect(locator.locator(".mx_ToggleSwitch_on")).toBeAttached();
|
||||
});
|
||||
|
||||
test("should send subscribe_rooms on room switch if room not already subscribed", async ({ page, app }) => {
|
||||
// create rooms and check room names are correct
|
||||
const roomIds: string[] = [];
|
||||
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
|
||||
const id = await app.client.createRoom({ name: fruit });
|
||||
roomIds.push(id);
|
||||
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
||||
}
|
||||
const [roomAId, roomPId] = roomIds;
|
||||
|
||||
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return body.room_subscriptions?.[subRoomId];
|
||||
};
|
||||
|
||||
// Select the Test Room and wait for playwright to get the request
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
||||
]);
|
||||
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
||||
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
||||
|
||||
// Switch to another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
||||
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
||||
]);
|
||||
});
|
||||
|
||||
test("should show and be able to accept/reject/rescind invites", async ({
|
||||
page,
|
||||
app,
|
||||
@@ -255,8 +258,8 @@ test.describe("Sliding Sync", () => {
|
||||
// Select the room to reject
|
||||
await page.getByRole("treeitem", { name: "Room to Reject" }).click();
|
||||
|
||||
// Decline the invite
|
||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Decline", exact: true }).click();
|
||||
// Reject the invite
|
||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Reject", exact: true }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
|
||||
@@ -358,4 +361,52 @@ test.describe("Sliding Sync", () => {
|
||||
// ensure the reply-to does not disappear
|
||||
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should send unsubscribe_rooms for every room switch", async ({ page, app }) => {
|
||||
// create rooms and check room names are correct
|
||||
const roomIds: string[] = [];
|
||||
for (const fruit of ["Apple", "Pineapple", "Orange"]) {
|
||||
const id = await app.client.createRoom({ name: fruit });
|
||||
roomIds.push(id);
|
||||
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
||||
}
|
||||
const [roomAId, roomPId, roomOId] = roomIds;
|
||||
|
||||
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return body.txn_id && body.room_subscriptions?.[subRoomId];
|
||||
};
|
||||
const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return (
|
||||
body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId]
|
||||
);
|
||||
};
|
||||
|
||||
// Select the Test Room and wait for playwright to get the request
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
||||
]);
|
||||
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
||||
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
||||
|
||||
// Switch to another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
||||
page.waitForRequest(matchRoomUnsubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
||||
]);
|
||||
|
||||
// And switch to even another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomOId)),
|
||||
page.waitForRequest(matchRoomUnsubRequest(roomPId)),
|
||||
page.getByRole("treeitem", { name: "Orange", exact: true }).click(),
|
||||
]);
|
||||
|
||||
// TODO: Add tests for encrypted rooms
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024,2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -35,18 +35,17 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
|
||||
name: spaceName,
|
||||
},
|
||||
},
|
||||
...roomIds.map((r) => spaceChildInitialState(r)),
|
||||
...roomIds.map(spaceChildInitialState),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||
function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||
return {
|
||||
type: "m.space.child",
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: [roomId.split(":")[1]],
|
||||
order,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -122,10 +121,9 @@ test.describe("Spaces", () => {
|
||||
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||
|
||||
// Assert rooms exist in the room list
|
||||
const roomList = page.getByRole("tree", { name: "Rooms" });
|
||||
await expect(roomList.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
||||
await expect(roomList.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
||||
await expect(roomList.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("treeitem", { name: "General", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("treeitem", { name: "Random", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("treeitem", { name: "Projects", exact: true })).toBeVisible();
|
||||
|
||||
// Assert rooms exist in the space explorer
|
||||
await expect(
|
||||
@@ -157,7 +155,7 @@ test.describe("Spaces", () => {
|
||||
|
||||
await page.getByRole("button", { name: "Just me" }).click();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Sample Room" }).click();
|
||||
await page.getByText("Sample Room").click({ force: true }); // force click as checkbox size is zero
|
||||
|
||||
// Temporal implementation as multiple elements with the role "button" and name "Add" are found
|
||||
await page.locator(".mx_AddExistingToSpace_footer").getByRole("button", { name: "Add" }).click();
|
||||
@@ -167,50 +165,6 @@ test.describe("Spaces", () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should allow user to add an existing room to a space after creation",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
await app.client.createRoom({
|
||||
name: "Sample Room",
|
||||
});
|
||||
await app.client.createRoom({
|
||||
name: "A Room that will not be selected",
|
||||
});
|
||||
|
||||
const menu = await openSpaceCreateMenu(page);
|
||||
await menu.getByRole("button", { name: "Private" }).click();
|
||||
|
||||
await menu
|
||||
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
|
||||
await menu
|
||||
.getByRole("textbox", { name: "Description" })
|
||||
.fill("This is a personal space to mourn Riot.im...");
|
||||
await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot");
|
||||
await menu.getByRole("textbox", { name: "Name" }).press("Enter");
|
||||
|
||||
await page.getByRole("button", { name: "Just me" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Skip for now" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "Add existing room" }).click();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Sample Room" }).click();
|
||||
|
||||
await expect(page.getByRole("dialog", { name: "Avatar Add existing rooms" })).toMatchScreenshot(
|
||||
"add-existing-rooms-dialog.png",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Add" }).click();
|
||||
await expect(
|
||||
page.locator(".mx_SpaceHierarchy_list").getByRole("treeitem", { name: "Sample Room" }),
|
||||
).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
test("should allow user to invite another to a space", { tag: "@no-webkit" }, async ({ page, app, user, bot }) => {
|
||||
await app.client.createSpace({
|
||||
visibility: "public" as any,
|
||||
@@ -273,7 +227,7 @@ test.describe("Spaces", () => {
|
||||
test(
|
||||
"should render subspaces in the space panel only when expanded",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, axe }) => {
|
||||
async ({ page, app, user, axe, checkA11y }) => {
|
||||
axe.disableRules([
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
"nested-interactive",
|
||||
@@ -295,7 +249,7 @@ test.describe("Spaces", () => {
|
||||
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
|
||||
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png");
|
||||
|
||||
// This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
|
||||
@@ -307,7 +261,7 @@ test.describe("Spaces", () => {
|
||||
await expect(item).toBeVisible();
|
||||
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
|
||||
},
|
||||
);
|
||||
@@ -337,36 +291,4 @@ test.describe("Spaces", () => {
|
||||
// Assert we get shown the new room intro, and thus not the soft crash screen
|
||||
await expect(page.locator(".mx_NewRoomIntro")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should render spaces view", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
||||
axe.disableRules([
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
"nested-interactive",
|
||||
// XXX: We have some known contrast issues here
|
||||
"color-contrast",
|
||||
]);
|
||||
|
||||
const childSpaceId1 = await app.client.createSpace({
|
||||
name: "Child Space 1",
|
||||
initial_state: [],
|
||||
});
|
||||
const childSpaceId2 = await app.client.createSpace({
|
||||
name: "Child Space 2",
|
||||
initial_state: [],
|
||||
});
|
||||
const childSpaceId3 = await app.client.createSpace({
|
||||
name: "Child Space 3",
|
||||
initial_state: [],
|
||||
});
|
||||
await app.client.createSpace({
|
||||
name: "Root Space",
|
||||
initial_state: [
|
||||
spaceChildInitialState(childSpaceId1, "a"),
|
||||
spaceChildInitialState(childSpaceId2, "b"),
|
||||
spaceChildInitialState(childSpaceId3, "c"),
|
||||
],
|
||||
});
|
||||
await app.viewSpaceByName("Root Space");
|
||||
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: { displayName: "Other User" },
|
||||
labsFlags: ["threadsActivityCentre"],
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
@@ -1,139 +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 * as fs from "node:fs";
|
||||
import { type EventType, type MsgType, type RoomJoinRulesEventContent } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
const MEDIA_FILE = fs.readFileSync("playwright/sample-files/riot.png");
|
||||
|
||||
test.describe("Media preview settings", () => {
|
||||
test.use({
|
||||
displayName: "Alan",
|
||||
botCreateOpts: {
|
||||
displayName: "Bob",
|
||||
},
|
||||
room: async ({ app, page, homeserver, bot, user }, use) => {
|
||||
const mxc = (await bot.uploadContent(MEDIA_FILE, { name: "image.png", type: "image/png" })).content_uri;
|
||||
const roomId = await bot.createRoom({
|
||||
name: "Test room",
|
||||
invite: [user.userId],
|
||||
initial_state: [{ type: "m.room.avatar", content: { url: mxc }, state_key: "" }],
|
||||
});
|
||||
await bot.sendEvent(roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.image" as MsgType,
|
||||
body: "image.png",
|
||||
url: mxc,
|
||||
});
|
||||
|
||||
await use({ roomId });
|
||||
},
|
||||
});
|
||||
|
||||
test("should be able to hide avatars of inviters", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
|
||||
let settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Hide avatars of room and inviter").click();
|
||||
await app.closeDialog();
|
||||
await app.viewRoomById(room.roomId);
|
||||
await expect(
|
||||
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
|
||||
).toMatchScreenshot("invite-no-avatar.png", {
|
||||
// Hide the mxid, which is not stable.
|
||||
css: `
|
||||
.mx_RoomPreviewBar_inviter_mxid {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
|
||||
).toMatchScreenshot("invite-room-tree-no-avatar.png");
|
||||
|
||||
// And then go back to being visible
|
||||
settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Hide avatars of room and inviter").click();
|
||||
await app.closeDialog();
|
||||
await page.goto("#/home");
|
||||
await app.viewRoomById(room.roomId);
|
||||
await expect(
|
||||
page.getByRole("complementary").filter({ hasText: "Do you want to join Test room" }),
|
||||
).toMatchScreenshot("invite-with-avatar.png", {
|
||||
// Hide the mxid, which is not stable.
|
||||
css: `
|
||||
.mx_RoomPreviewBar_inviter_mxid {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("tree", { name: "Rooms" }).getByRole("treeitem", { name: "Test room" }),
|
||||
).toMatchScreenshot("invite-room-tree-with-avatar.png");
|
||||
});
|
||||
|
||||
test("should be able to hide media in rooms globally", async ({ page, app, room, user }) => {
|
||||
const settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
|
||||
await app.closeDialog();
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
await expect(page.getByText("Show image")).toBeVisible();
|
||||
});
|
||||
test("should be able to hide media in non-private rooms globally", async ({ page, app, room, user, bot }) => {
|
||||
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
|
||||
join_rule: "public",
|
||||
});
|
||||
const settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Show media in timeline").getByLabel("In private rooms").click();
|
||||
await app.closeDialog();
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
await expect(page.getByText("Show image")).toBeVisible();
|
||||
for (const joinRule of ["invite", "knock", "restricted"] as RoomJoinRulesEventContent["join_rule"][]) {
|
||||
await bot.sendStateEvent(room.roomId, "m.room.join_rules", {
|
||||
join_rule: joinRule,
|
||||
} satisfies RoomJoinRulesEventContent);
|
||||
await expect(page.getByText("Show image")).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
test("should be able to show media in rooms globally", async ({ page, app, room, user }) => {
|
||||
const settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
|
||||
await app.closeDialog();
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
await expect(page.getByText("Show image")).not.toBeVisible();
|
||||
});
|
||||
test("should be able to hide media in an individual room", async ({ page, app, room, user }) => {
|
||||
const settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
|
||||
await app.closeDialog();
|
||||
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
const roomSettings = await app.settings.openRoomSettings("General");
|
||||
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
|
||||
await app.closeDialog();
|
||||
|
||||
await expect(page.getByText("Show image")).toBeVisible();
|
||||
});
|
||||
test("should be able to show media in an individual room", async ({ page, app, room, user }) => {
|
||||
const settings = await app.settings.openUserSettings("Preferences");
|
||||
await settings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always hide" }).click();
|
||||
await app.closeDialog();
|
||||
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.getByRole("button", { name: "Accept" }).click();
|
||||
|
||||
const roomSettings = await app.settings.openRoomSettings("General");
|
||||
await roomSettings.getByLabel("Show media in timeline").getByRole("radio", { name: "Always show" }).click();
|
||||
await app.closeDialog();
|
||||
|
||||
await expect(page.getByText("Show image")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024, 2025 New Vector Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
@@ -28,8 +28,6 @@ const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
||||
const OLD_NAME = "Alan";
|
||||
const NEW_NAME = "Alan (away)";
|
||||
|
||||
const VIDEO_FILE = fs.readFileSync("playwright/sample-files/5secvid.webm");
|
||||
|
||||
const getEventTilesWithBodies = (page: Page): Locator => {
|
||||
return page.locator(".mx_EventTile").filter({ has: page.locator(".mx_EventTile_body") });
|
||||
};
|
||||
@@ -279,7 +277,7 @@ test.describe("Timeline", () => {
|
||||
test(
|
||||
"should add inline start margin to an event line on IRC layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, axe }) => {
|
||||
async ({ page, app, room, axe, checkA11y }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
@@ -320,7 +318,7 @@ test.describe("Timeline", () => {
|
||||
`,
|
||||
},
|
||||
);
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -745,64 +743,68 @@ test.describe("Timeline", () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("should render url previews", { tag: "@screenshot" }, async ({ page, app, room, axe, context }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
test(
|
||||
"should render url previews",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, axe, checkA11y, context }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
|
||||
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
|
||||
// the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it
|
||||
// post-worker, but we can't waitForResponse on that, so the page context is still used there. Because
|
||||
// the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until
|
||||
// the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
|
||||
await context.route(
|
||||
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
path: "playwright/sample-files/riot.png",
|
||||
});
|
||||
},
|
||||
);
|
||||
await page.route(
|
||||
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
"og:title": "Element Call",
|
||||
"og:description": null,
|
||||
"og:image:width": 48,
|
||||
"og:image:height": 48,
|
||||
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
|
||||
"og:image:type": "image/png",
|
||||
"matrix:image:size": 2121,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
|
||||
// the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it
|
||||
// post-worker, but we can't waitForResponse on that, so the page context is still used there. Because
|
||||
// the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until
|
||||
// the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
|
||||
await context.route(
|
||||
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
path: "playwright/sample-files/riot.png",
|
||||
});
|
||||
},
|
||||
);
|
||||
await page.route(
|
||||
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
"og:title": "Element Call",
|
||||
"og:description": null,
|
||||
"og:image:width": 48,
|
||||
"og:image:height": 48,
|
||||
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
|
||||
"og:image:type": "image/png",
|
||||
"matrix:image:size": 2121,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const requestPromises: Promise<any>[] = [
|
||||
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
|
||||
// see context.route above for why we listen for the unauthenticated endpoint
|
||||
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
|
||||
];
|
||||
const requestPromises: Promise<any>[] = [
|
||||
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
|
||||
// see context.route above for why we listen for the unauthenticated endpoint
|
||||
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
|
||||
];
|
||||
|
||||
await app.client.sendMessage(room.roomId, "https://call.element.io/");
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.client.sendMessage(room.roomId, "https://call.element.io/");
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
||||
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
|
||||
await Promise.all(requestPromises);
|
||||
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
|
||||
await Promise.all(requestPromises);
|
||||
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await checkA11y();
|
||||
|
||||
await app.timeline.scrollToBottom();
|
||||
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
await app.timeline.scrollToBottom();
|
||||
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.describe("on search results panel", () => {
|
||||
test(
|
||||
@@ -873,73 +875,6 @@ test.describe("Timeline", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("should render a code block", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
// Wait until configuration is finished
|
||||
await expect(
|
||||
page
|
||||
.locator(".mx_GenericEventListSummary_summary")
|
||||
.getByText(`${OLD_NAME} created and configured the room.`),
|
||||
).toBeVisible();
|
||||
|
||||
// Send a code block
|
||||
const composer = app.getComposerField();
|
||||
await composer.fill("```\nconsole.log('Hello, world!');\n```");
|
||||
await composer.press("Enter");
|
||||
|
||||
const tile = page.locator(".mx_EventTile");
|
||||
await expect(tile).toBeVisible();
|
||||
await expect(tile).toMatchScreenshot("code-block.png", { mask: [page.locator(".mx_MessageTimestamp")] });
|
||||
|
||||
// Edit a code block and assert the edited code block has been correctly rendered
|
||||
await tile.hover();
|
||||
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }).click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Edit message" })
|
||||
.fill("```\nconsole.log('Edited: Hello, world!');\n```");
|
||||
await page.getByRole("textbox", { name: "Edit message" }).press("Enter");
|
||||
|
||||
const newTile = page.locator(".mx_EventTile");
|
||||
await expect(newTile).toMatchScreenshot("edited-code-block.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await sendImage(app.client, room.roomId, NEW_AVATAR);
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MImageBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should be able to hide a video", async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
const upload = await app.client.uploadContent(VIDEO_FILE, { name: "bbb.webm", type: "video/webm" });
|
||||
await app.client.sendEvent(room.roomId, null, "m.room.message" as EventType, {
|
||||
msgtype: "m.video" as MsgType,
|
||||
body: "bbb.webm",
|
||||
url: upload.content_uri,
|
||||
});
|
||||
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MVideoBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the video is now hidden.
|
||||
await expect(page.getByRole("button", { name: "Show video" })).toBeVisible();
|
||||
await expect(page.locator("video")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
@@ -1341,44 +1276,4 @@ test.describe("Timeline", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("spoilers", { tag: "@screenshot" }, () => {
|
||||
test("clicking a spoiler containing the pill de-spoilers on 1st click, then follows link on 2nd", async ({
|
||||
page,
|
||||
user,
|
||||
app,
|
||||
room,
|
||||
}) => {
|
||||
// View room
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
||||
// Send a spoilered pill
|
||||
await app.client.sendMessage(room.roomId, {
|
||||
msgtype: "m.text",
|
||||
body: user.userId,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<span data-mx-spoiler>https://matrix.to/#/${user.userId}</span>`,
|
||||
});
|
||||
|
||||
const screenshotOptions = {
|
||||
css: `
|
||||
.mx_MessageTimestamp {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const eventTile = page.locator(".mx_RoomView_body .mx_EventTile_last");
|
||||
await expect(eventTile).toMatchScreenshot("spoiler.png", screenshotOptions);
|
||||
|
||||
const rightPanelButton = page.getByText("Share profile");
|
||||
const pill = page.locator(".mx_UserPill");
|
||||
await pill.click({ force: true }); // force to click the spoiler wrapper instead
|
||||
await expect(eventTile).toMatchScreenshot("spoiler-uncovered.png", screenshotOptions);
|
||||
await expect(rightPanelButton).not.toBeVisible(); // assert the right panel is not yet open
|
||||
|
||||
await pill.click();
|
||||
await expect(rightPanelButton).toBeVisible(); // assert the right panel is open
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,18 +7,18 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ExpectMatcherState,
|
||||
type MatcherReturnType,
|
||||
type Page,
|
||||
expect as baseExpect,
|
||||
type Locator,
|
||||
type Page,
|
||||
type ExpectMatcherState,
|
||||
type ElementHandle,
|
||||
type PlaywrightTestArgs,
|
||||
type Fixtures as _Fixtures,
|
||||
} from "@playwright/test";
|
||||
import {
|
||||
type TestFixtures as BaseTestFixtures,
|
||||
expect as baseExpect,
|
||||
type ToMatchScreenshotOptions,
|
||||
} from "@element-hq/element-web-playwright-common";
|
||||
import { sanitizeForFilePath } from "playwright-core/lib/utils";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import _ from "lodash";
|
||||
import { extname } from "node:path";
|
||||
|
||||
import type { IConfigOptions } from "../src/IConfigOptions";
|
||||
import { type Credentials } from "./plugins/homeserver";
|
||||
@@ -27,22 +27,71 @@ import { Crypto } from "./pages/crypto";
|
||||
import { Toasts } from "./pages/toasts";
|
||||
import { Bot, type CreateBotOpts } from "./pages/bot";
|
||||
import { Webserver } from "./plugins/webserver";
|
||||
import { type WorkerOptions, type Services, test as base } from "./services";
|
||||
import { type Options, type Services, test as base } from "./services.ts";
|
||||
|
||||
// Enable experimental service worker support
|
||||
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable
|
||||
process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1";
|
||||
|
||||
declare module "@element-hq/element-web-playwright-common" {
|
||||
// Improve the type for the config fixture based on the real type
|
||||
export interface Config extends Omit<IConfigOptions, "default_server_config"> {}
|
||||
}
|
||||
// This is deliberately quite a minimal config.json, so that we can test that the default settings actually work.
|
||||
const CONFIG_JSON: Partial<IConfigOptions> = {
|
||||
// The default language is set here for test consistency
|
||||
setting_defaults: {
|
||||
language: "en-GB",
|
||||
},
|
||||
|
||||
// the location tests want a map style url.
|
||||
map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
|
||||
|
||||
features: {
|
||||
// We don't want to go through the feature announcement during the e2e test
|
||||
feature_release_announcement: false,
|
||||
},
|
||||
};
|
||||
|
||||
export interface CredentialsWithDisplayName extends Credentials {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface TestFixtures extends BaseTestFixtures {
|
||||
export interface TestFixtures {
|
||||
axe: AxeBuilder;
|
||||
checkA11y: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* The contents of the config.json to send when the client requests it.
|
||||
*/
|
||||
config: typeof CONFIG_JSON;
|
||||
|
||||
/**
|
||||
* The displayname to use for the user registered in {@link #credentials}.
|
||||
*
|
||||
* To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block.
|
||||
* See {@link https://playwright.dev/docs/api/class-test#test-use}.
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* A test fixture which registers a test user on the {@link #homeserver} and supplies the details
|
||||
* of the registered user.
|
||||
*/
|
||||
credentials: CredentialsWithDisplayName;
|
||||
|
||||
/**
|
||||
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`},
|
||||
* but adds an initScript which will populate localStorage with the user's details from
|
||||
* {@link #credentials} and {@link #homeserver}.
|
||||
*
|
||||
* Similar to {@link #user}, but doesn't load the app.
|
||||
*/
|
||||
pageWithCredentials: Page;
|
||||
|
||||
/**
|
||||
* A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores
|
||||
* the credentials into localStorage per {@link #homeserver}, and then loads the front page of the
|
||||
* app.
|
||||
*/
|
||||
user: CredentialsWithDisplayName;
|
||||
|
||||
/**
|
||||
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`},
|
||||
* but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI,
|
||||
@@ -56,11 +105,13 @@ export interface TestFixtures extends BaseTestFixtures {
|
||||
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
|
||||
botCreateOpts: CreateBotOpts;
|
||||
bot: Bot;
|
||||
labsFlags: string[];
|
||||
webserver: Webserver;
|
||||
disablePresence: boolean;
|
||||
}
|
||||
|
||||
type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures;
|
||||
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & WorkerOptions, CombinedTestFixtures>;
|
||||
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & Options, CombinedTestFixtures>;
|
||||
export const test = base.extend<TestFixtures>({
|
||||
context: async ({ context }, use, testInfo) => {
|
||||
// We skip tests instead of using grep-invert to still surface the counts in the html report
|
||||
@@ -70,12 +121,102 @@ export const test = base.extend<TestFixtures>({
|
||||
);
|
||||
await use(context);
|
||||
},
|
||||
|
||||
axe: async ({ axe }, use) => {
|
||||
// Exclude floating UI for now
|
||||
await use(axe.exclude("[data-floating-ui-portal]"));
|
||||
disablePresence: false,
|
||||
config: {}, // We merge this atop the default CONFIG_JSON in the page fixture to make extending it easier
|
||||
page: async ({ homeserver, context, page, config, labsFlags, disablePresence }, use) => {
|
||||
await context.route(`http://localhost:8080/config.json*`, async (route) => {
|
||||
const json = {
|
||||
...CONFIG_JSON,
|
||||
...config,
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: homeserver.baseUrl,
|
||||
},
|
||||
...config.default_server_config,
|
||||
},
|
||||
};
|
||||
json["features"] = {
|
||||
...json["features"],
|
||||
// Enable the lab features
|
||||
...labsFlags.reduce((obj, flag) => {
|
||||
obj[flag] = true;
|
||||
return obj;
|
||||
}, {}),
|
||||
};
|
||||
if (disablePresence) {
|
||||
json["enable_presence_by_hs_url"] = {
|
||||
[homeserver.baseUrl]: false,
|
||||
};
|
||||
}
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
await use(page);
|
||||
},
|
||||
|
||||
displayName: undefined,
|
||||
credentials: async ({ context, homeserver, displayName: testDisplayName }, use, testInfo) => {
|
||||
const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"];
|
||||
const password = _.uniqueId("password_");
|
||||
const displayName = testDisplayName ?? _.sample(names)!;
|
||||
|
||||
const credentials = await homeserver.registerUser(`user_${testInfo.testId}`, password, displayName);
|
||||
console.log(`Registered test user ${credentials.userId} with displayname ${displayName}`);
|
||||
|
||||
await use({
|
||||
...credentials,
|
||||
displayName,
|
||||
});
|
||||
},
|
||||
labsFlags: [],
|
||||
|
||||
pageWithCredentials: async ({ page, homeserver, credentials }, use) => {
|
||||
await page.addInitScript(
|
||||
({ baseUrl, credentials }) => {
|
||||
// Seed the localStorage with the required credentials
|
||||
window.localStorage.setItem("mx_hs_url", baseUrl);
|
||||
window.localStorage.setItem("mx_user_id", credentials.userId);
|
||||
window.localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
window.localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||
window.localStorage.setItem("mx_is_guest", "false");
|
||||
window.localStorage.setItem("mx_has_pickle_key", "false");
|
||||
window.localStorage.setItem("mx_has_access_token", "true");
|
||||
|
||||
window.localStorage.setItem(
|
||||
"mx_local_settings",
|
||||
JSON.stringify({
|
||||
// Retain any other settings which may have already been set
|
||||
...JSON.parse(window.localStorage.getItem("mx_local_settings") || "{}"),
|
||||
// Ensure the language is set to a consistent value
|
||||
language: "en",
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ baseUrl: homeserver.baseUrl, credentials },
|
||||
);
|
||||
await use(page);
|
||||
},
|
||||
|
||||
user: async ({ pageWithCredentials: page, credentials }, use) => {
|
||||
await page.goto("/");
|
||||
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
|
||||
await use(credentials);
|
||||
},
|
||||
|
||||
axe: async ({ page }, use) => {
|
||||
await use(new AxeBuilder({ page }).exclude("[data-floating-ui-portal]"));
|
||||
},
|
||||
checkA11y: async ({ axe }, use, testInfo) =>
|
||||
use(async () => {
|
||||
const results = await axe.analyze();
|
||||
|
||||
await testInfo.attach("accessibility-scan-results", {
|
||||
body: JSON.stringify(results, null, 2),
|
||||
contentType: "application/json",
|
||||
});
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
}),
|
||||
|
||||
app: async ({ page }, use) => {
|
||||
const app = new ElementAppPage(page);
|
||||
await use(app);
|
||||
@@ -103,23 +244,35 @@ export const test = base.extend<TestFixtures>({
|
||||
},
|
||||
});
|
||||
|
||||
interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
|
||||
includeDialogBackground?: boolean;
|
||||
showTooltips?: boolean;
|
||||
timeout?: number;
|
||||
// Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2
|
||||
function sanitizeFilePathBeforeExtension(filePath: string): string {
|
||||
const ext = extname(filePath);
|
||||
const base = filePath.substring(0, filePath.length - ext.length);
|
||||
return sanitizeForFilePath(base) + ext;
|
||||
}
|
||||
|
||||
type Expectations = {
|
||||
toMatchScreenshot: (
|
||||
export const expect = baseExpect.extend({
|
||||
async toMatchScreenshot(
|
||||
this: ExpectMatcherState,
|
||||
receiver: Page | Locator,
|
||||
name: `${string}.png`,
|
||||
options?: ExtendedToMatchScreenshotOptions,
|
||||
) => Promise<MatcherReturnType>;
|
||||
};
|
||||
options?: {
|
||||
mask?: Array<Locator>;
|
||||
includeDialogBackground?: boolean;
|
||||
showTooltips?: boolean;
|
||||
timeout?: number;
|
||||
css?: string;
|
||||
},
|
||||
) {
|
||||
const testInfo = test.info();
|
||||
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
|
||||
|
||||
if (!testInfo.tags.includes("@screenshot")) {
|
||||
throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot");
|
||||
}
|
||||
|
||||
const page = "page" in receiver ? receiver.page() : receiver;
|
||||
|
||||
export const expect = baseExpect.extend<Expectations>({
|
||||
async toMatchScreenshot(receiver, name, options) {
|
||||
let css = `
|
||||
.mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
@@ -169,9 +322,21 @@ export const expect = baseExpect.extend<Expectations>({
|
||||
css += options.css;
|
||||
}
|
||||
|
||||
await baseExpect(receiver).toMatchScreenshot(name, {
|
||||
...options,
|
||||
css,
|
||||
// We add a custom style tag before taking screenshots
|
||||
const style = (await page.addStyleTag({
|
||||
content: css,
|
||||
})) as ElementHandle<Element>;
|
||||
|
||||
const screenshotName = sanitizeFilePathBeforeExtension(name);
|
||||
await baseExpect(receiver).toHaveScreenshot(screenshotName, options);
|
||||
|
||||
await style.evaluate((tag) => tag.remove());
|
||||
|
||||
testInfo.annotations.push({
|
||||
// `_` prefix hides it from the HTML reporter
|
||||
type: "_screenshot",
|
||||
// include a path relative to `playwright/snapshots/`
|
||||
description: testInfo.snapshotPath(screenshotName).split("/playwright/snapshots/", 2)[1],
|
||||
});
|
||||
|
||||
return { pass: true, message: () => "", name: "toMatchScreenshot" };
|
||||
|
||||
63
playwright/logger.ts
Normal file
63
playwright/logger.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2024 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 BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
||||
import { type Readable } from "stream";
|
||||
import stripAnsi from "strip-ansi";
|
||||
|
||||
export class Logger {
|
||||
private pages: Page[] = [];
|
||||
private logs: Record<string, string> = {};
|
||||
|
||||
public getConsumer(container: string) {
|
||||
this.logs[container] = "";
|
||||
return (stream: Readable) => {
|
||||
stream.on("data", (chunk) => {
|
||||
this.logs[container] += chunk.toString();
|
||||
});
|
||||
stream.on("err", (chunk) => {
|
||||
this.logs[container] += "ERR " + chunk.toString();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public async onTestStarted(context: BrowserContext) {
|
||||
this.pages = [];
|
||||
for (const id in this.logs) {
|
||||
if (id.startsWith("page-")) {
|
||||
delete this.logs[id];
|
||||
} else {
|
||||
this.logs[id] = "";
|
||||
}
|
||||
}
|
||||
|
||||
context.on("console", (msg) => {
|
||||
const page = msg.page();
|
||||
let pageIdx = this.pages.indexOf(page);
|
||||
if (pageIdx === -1) {
|
||||
this.pages.push(page);
|
||||
pageIdx = this.pages.length - 1;
|
||||
this.logs[`page-${pageIdx}`] = `Console logs for page with URL: ${page.url()}\n\n`;
|
||||
}
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
this.logs[`page-${pageIdx}`] += `${type}: ${text}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
public async onTestFinished(testInfo: TestInfo) {
|
||||
if (testInfo.status !== "passed") {
|
||||
for (const id in this.logs) {
|
||||
if (!this.logs[id]) continue;
|
||||
await testInfo.attach(id, {
|
||||
body: stripAnsi(this.logs[id]),
|
||||
contentType: "text/plain",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export class ElementAppPage {
|
||||
* @param isRightPanel whether to select the right panel composer, otherwise the main timeline composer
|
||||
*/
|
||||
public getComposerField(isRightPanel?: boolean): Locator {
|
||||
return this.getComposer(isRightPanel).locator("div[contenteditable]");
|
||||
return this.getComposer(isRightPanel).locator("[contenteditable]");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,8 +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 WorkerOptions } from "../../../services.ts";
|
||||
import { type Options } from "../../../services.ts";
|
||||
|
||||
export const isDendrite = ({ homeserverType }: WorkerOptions): boolean => {
|
||||
export const isDendrite = ({ homeserverType }: Options): boolean => {
|
||||
return homeserverType === "dendrite" || homeserverType === "pinecone";
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
|
||||
import { type ClientServerApi } from "../utils/api.ts";
|
||||
|
||||
export interface HomeserverInstance {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -6,19 +6,30 @@ 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 SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { type Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const consentHomeserver: Fixtures = {
|
||||
_homeserver: [
|
||||
async ({ _homeserver: container, mailpit }, use) => {
|
||||
(container as SynapseContainer)
|
||||
container
|
||||
.withCopyDirectoriesToContainer([
|
||||
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
|
||||
])
|
||||
.withSmtpServer(mailpit)
|
||||
.withConfig({
|
||||
email: {
|
||||
enable_notifs: false,
|
||||
smtp_host: "mailpit",
|
||||
smtp_port: 1025,
|
||||
smtp_user: "username",
|
||||
smtp_pass: "password",
|
||||
require_transport_security: false,
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
|
||||
app_name: "Matrix",
|
||||
notif_template_html: "notif_mail.html",
|
||||
notif_template_text: "notif_mail.txt",
|
||||
notif_for_new_users: true,
|
||||
client_base_url: "http://localhost/element",
|
||||
},
|
||||
user_consent: {
|
||||
template_dir: "/data/res/templates/privacy",
|
||||
version: "1.0",
|
||||
|
||||
@@ -6,8 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixAuthenticationServiceContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts";
|
||||
import { type Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const masHomeserver: Fixtures = {
|
||||
|
||||
@@ -10,7 +10,8 @@ import http from "http";
|
||||
import express from "express";
|
||||
import { type AddressInfo } from "net";
|
||||
import { type TestInfo } from "@playwright/test";
|
||||
import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js";
|
||||
|
||||
import { randB64Bytes } from "../utils/rand.ts";
|
||||
|
||||
export class OAuthServer {
|
||||
private server?: http.Server;
|
||||
|
||||
76
playwright/plugins/utils/api.ts
Normal file
76
playwright/plugins/utils/api.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
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 APIRequestContext } from "@playwright/test";
|
||||
|
||||
import { type Credentials } from "../homeserver";
|
||||
|
||||
export type Verb = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export class Api {
|
||||
private _request?: APIRequestContext;
|
||||
|
||||
constructor(private readonly baseUrl: string) {}
|
||||
|
||||
public setRequest(request: APIRequestContext): void {
|
||||
this._request = request;
|
||||
}
|
||||
|
||||
public async request<R extends {}>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
|
||||
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
|
||||
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const res = await this._request.fetch(url, {
|
||||
data,
|
||||
method: verb,
|
||||
headers: token
|
||||
? {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`,
|
||||
);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientServerApi extends Api {
|
||||
constructor(baseUrl: string) {
|
||||
super(`${baseUrl}/_matrix/client`);
|
||||
}
|
||||
|
||||
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
||||
const json = await this.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}>("POST", "/v3/login", undefined, {
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: userId,
|
||||
},
|
||||
password: password,
|
||||
});
|
||||
|
||||
return {
|
||||
password,
|
||||
accessToken: json.access_token,
|
||||
userId: json.user_id,
|
||||
deviceId: json.device_id,
|
||||
homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"),
|
||||
username: userId.slice(1).split(":")[0],
|
||||
};
|
||||
}
|
||||
}
|
||||
16
playwright/plugins/utils/object.ts
Normal file
16
playwright/plugins/utils/object.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deep copy the given object. The object MUST NOT have circular references and
|
||||
* MUST NOT have functions.
|
||||
* @param obj - The object to deep copy.
|
||||
* @returns A copy of the object without any references to the original.
|
||||
*/
|
||||
export function deepCopy<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
19
playwright/plugins/utils/port.ts
Normal file
19
playwright/plugins/utils/port.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import * as net from "net";
|
||||
|
||||
export async function getFreePort(): Promise<number> {
|
||||
return new Promise<number>((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = (<net.AddressInfo>srv.address()).port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
||||
13
playwright/plugins/utils/rand.ts
Normal file
13
playwright/plugins/utils/rand.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import crypto from "node:crypto";
|
||||
|
||||
export function randB64Bytes(numBytes: number): string {
|
||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user