Compare commits

..

10 Commits

Author SHA1 Message Date
David Baker
0a43803d46 Fix canUploadKeysWithPasswordOnly for MSC3967
Being able to upload keys without any auth works too, so return true.
Otherwise we have to go the same route via the InteractiveAuthDialog
which is way more complex.
2024-12-05 14:10:16 +00:00
David Baker
2a5af99ac2 Fix tests 2024-12-02 15:25:51 +00:00
David Baker
b5ae775d8f Add mock
so test passes again
2024-12-02 15:08:35 +00:00
David Baker
6bff653339 Merge branch 'develop' into dbkr/key_backup_by_default 2024-12-02 14:44:46 +00:00
David Baker
b8f6bd1664 Secure backup prompt 2024-12-02 14:44:00 +00:00
David Baker
cf52974e09 Merge branch 'develop' into dbkr/key_backup_by_default 2024-11-19 13:16:05 +00:00
David Baker
6a912e28be Fix test 2024-10-22 16:03:26 +01:00
David Baker
ce98a0f988 Fix import 2024-10-22 15:40:39 +01:00
David Baker
7f10cd6a9e Fix name & tests 2024-10-22 15:36:04 +01:00
David Baker
2c7247713a Enable key backup by default
When we set up cross signing, so the key backup key will be stored
locally along with the cross signing keys until the user sets up
recovery (4s). This will mean that a user can restore their backup
if they log in on a new device as long as they verify with the one
they registered on.
2024-10-22 14:58:20 +01:00
2709 changed files with 14768 additions and 18855 deletions

View File

@@ -42,10 +42,6 @@ module.exports = {
name: "setImmediate",
message: "Use setTimeout instead.",
},
{
name: "Buffer",
message: "Buffer is not available in the web.",
},
],
"import/no-duplicates": ["error"],
@@ -259,9 +255,6 @@ module.exports = {
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
},
],
// These are fine in tests
"no-restricted-globals": "off",
},
},
{

3
.github/CODEOWNERS vendored
View File

@@ -13,7 +13,6 @@
# Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
# Ignore the synapse plugin as this is updated by GHA for docker image updating
/playwright/testcontainers/synapse.ts
/playwright/plugins/homeserver/synapse/index.ts

View File

@@ -10,29 +10,24 @@ inputs:
runs:
using: composite
steps:
- name: Download release tarball
- name: Download current version for its old bundles
id: current_download
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
with:
tag: ${{ inputs.tag }}
tag: steps.current_version.outputs.version
fileName: element-*.tar.gz*
out-file-path: ${{ runner.temp }}/download-verify-element-tarball
- name: Verify tarball
shell: bash
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Extract tarball
shell: bash
run: |
mkdir webapp
tar xvzf element-*.tar.gz -C webapp --strip-components=1
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1
working-directory: ${{ runner.temp }}/download-verify-element-tarball
- name: Move webapp to out-file-path
shell: bash
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
- name: Clean up temp directory
shell: bash
run: rm -R ${{ runner.temp }}/download-verify-element-tarball

3
.github/labels.yml vendored
View File

@@ -210,9 +210,6 @@
- name: "X-Upcoming-Release-Blocker"
description: "This does not affect the current release cycle but will affect the next one"
color: "e99695"
- name: "X-Run-All-Tests"
description: "When applied to PRs, it'll run the full gamut of end-to-end tests on the PR"
color: "ff7979"
- name: "Z-Actions"
color: "ededed"
- name: "Z-Cache-Confusion"

View File

@@ -5,9 +5,6 @@ on:
branches: [develop, master]
merge_group:
types: [checks_requested]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# develop pushes and repository_dispatch handled in build_develop.yaml
env:
# These must be set for fetchdep.sh to get the right branch
@@ -40,38 +37,14 @@ jobs:
- 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
cache: ${{ runner.os != 'Windows' && 'yarn' || '' }}
cache: "yarn"
node-version: "lts/*"
# Workaround for yarn install timeouts, especially on Windows
- run: yarn config set network-timeout 300000
- name: Fetch layered build
id: layered_build
env:
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
run: |
scripts/layered.sh
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
VECTOR_SHA=$(git rev-parse --short=12 HEAD)
echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
- name: Copy config
run: cp element.io/develop/config.json config.json
- name: Install Dependencies
run: "./scripts/layered.sh"
- name: Build
env:
CI_PACKAGE: true
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
run: |
yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: webapp-${{ matrix.image }}
path: webapp
retention-days: 1
run: "yarn build"

View File

@@ -1,8 +1,7 @@
# Manual deploy workflow for deploying to app.element.io & staging.element.io
# Runs automatically for staging.element.io when an RC or Release is published
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
name: Deploy release
run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }}
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }}
on:
release:
types: [published]
@@ -16,11 +15,6 @@ on:
options:
- staging.element.io
- app.element.io
skip-checks:
description: Skip CI on the tagged commit
required: true
default: false
type: boolean
concurrency: ${{ inputs.site || 'staging.element.io' }}
permissions: {}
jobs:
@@ -34,40 +28,37 @@ jobs:
env:
SITE: ${{ inputs.site || 'staging.element.io' }}
steps:
- uses: actions/checkout@v4
- name: Load GPG key
run: |
curl https://packages.element.io/element-release-key.gpg | gpg --import
gpg -k "$GPG_FINGERPRINT"
env:
GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
- name: Check current version on deployment
id: current_version
run: |
version=$(curl -s https://$SITE/version)
echo "version=${version#v}" >> $GITHUB_OUTPUT
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT
# The current version bundle melding dance is skipped if the version we're deploying is the same
# as then we're just doing a re-deploy of the same version with potentially different configs.
- name: Download current version for its old bundles
id: current_download
if: steps.current_version.outputs.version != github.ref_name
uses: ./.github/actions/download-verify-element-tarball
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
with:
tag: v${{ steps.current_version.outputs.version }}
out-file-path: _current_version
tag: steps.current_version.outputs.version
out-file-path: current_version
- name: Download target version
uses: ./.github/actions/download-verify-element-tarball
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
with:
tag: ${{ github.ref_name }}
out-file-path: _deploy
- name: Merge current bundles into target
if: steps.current_download.outcome == 'success'
run: cp -vnpr _current_version/bundles/* _deploy/bundles/
run: cp -vnpr current_version/bundles/* _deploy/bundles/
- name: Copy config
run: cp element.io/app/config.json _deploy/config.json
@@ -80,10 +71,9 @@ jobs:
- name: Wait for other steps to succeed
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
if: inputs.skip-checks != true
with:
ref: ${{ github.sha }}
running-workflow-name: "Deploy to Cloudflare Pages"
running-workflow-name: "Build and Deploy ${{ env.SITE }}"
repo-token: ${{ secrets.GITHUB_TOKEN }}
wait-interval: 10
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$

View File

@@ -24,10 +24,10 @@ jobs:
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
with:
install: true
@@ -51,7 +51,7 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
with:
context: .
push: true

View File

@@ -3,9 +3,6 @@
# as an artifact and run end-to-end tests.
name: End to End Tests
on:
# CRON to run all Projects at 6am UTC
schedule:
- cron: "0 6 * * *"
pull_request: {}
merge_group:
types: [checks_requested]
@@ -35,8 +32,6 @@ concurrency:
env:
# fetchdep.sh needs to know our PR number
PR_NUMBER: ${{ github.event.pull_request.number }}
# Use 6 runners in the default case, but 4 when running on a schedule where we run all 5 projects (20 runners total)
NUM_RUNNERS: ${{ github.event_name == 'schedule' && 4 || 6 }}
permissions: {} # No permissions required
@@ -45,10 +40,6 @@ jobs:
name: "Build Element-Web"
runs-on: ubuntu-24.04
if: inputs.skip != true
outputs:
num-runners: ${{ env.NUM_RUNNERS }}
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
docker-cache-key: ${{ steps.runner-vars.outputs.docker-cache-key }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -81,12 +72,6 @@ jobs:
run: |
yarn build
# Heuristic for calculating a cache key which is based on all images we pass to testcontainers
- name: Calculate docker cache key
run: |
grep -hr "Container(\"" --exclude-dir=snapshots --exclude-dir=sample-files playwright >> _docker_cache_key
grep -hr -C1 "super(" playwright/testcontainers/ >> _docker_cache_key
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
@@ -94,23 +79,11 @@ jobs:
path: webapp
retention-days: 1
- name: Calculate runner variables
id: runner-vars
uses: actions/github-script@v7
env:
DOCKER_CACHE_KEY: ${{ hashFiles('_docker_cache_key') }}
with:
script: |
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
const matrix = Array.from({ length: numRunners }, (_, i) => i + 1);
core.setOutput("matrix", JSON.stringify(matrix));
core.setOutput("docker-cache-key", process.env.DOCKER_CACHE_KEY);
playwright:
name: "Run Tests [${{ matrix.project }}] ${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}"
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
needs: build
if: inputs.skip != true
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
permissions:
actions: read
issues: read
@@ -119,19 +92,7 @@ jobs:
fail-fast: false
matrix:
# Run multiple instances in parallel to speed up the tests
runner: ${{ fromJSON(needs.build.outputs.runners-matrix) }}
project:
- Chrome
- Firefox
- WebKit
runAllTests:
- ${{ github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }}
# Skip the Firefox & Safari runs unless this was a cron trigger or PR has X-Run-All-Tests label
exclude:
- runAllTests: false
project: Firefox
- runAllTests: false
project: WebKit
runner: [1, 2, 3, 4, 5, 6]
steps:
- uses: actions/checkout@v4
with:
@@ -167,31 +128,16 @@ jobs:
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps --no-shell
run: yarn playwright install --with-deps
- name: Install system dependencies for WebKit
# Some WebKit dependencies seem to lay outside the cache and will need to be installed separately
if: matrix.project == 'WebKit' && steps.playwright-cache.outputs.cache-hit == 'true'
run: yarn playwright install-deps webkit
- name: Docker image cache
uses: ScribeMD/docker-cache@fb28c93772363301b8d0a6072ce850224b73f74e # 0.5.0
with:
key: ${{ needs.build.outputs.docker-cache-key }}
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
- name: Run Playwright tests
run: |
yarn playwright test \
--shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
--project="${{ matrix.project }}" \
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }}
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
name: all-blob-reports-${{ matrix.runner }}
path: blob-report
retention-days: 1

View File

@@ -3,8 +3,7 @@ on:
workflow_dispatch: {}
schedule:
- cron: "0 6 * * 1,3,5" # Every Monday, Wednesday and Friday at 6am UTC
permissions:
pull-requests: write # needed to auto-approve PRs
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
download:
uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_download.yaml@main

View File

@@ -3,7 +3,7 @@
name: Upload Preview Build to Netlify
on:
workflow_run:
workflows: ["Build"]
workflows: ["End to End Tests"]
types:
- completed
jobs:
@@ -32,7 +32,7 @@ jobs:
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: webapp-ubuntu-24.04
name: webapp
path: webapp
- name: 📤 Deploy to Netlify

View File

@@ -17,13 +17,13 @@ jobs:
docker pull "$IMAGE"
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
DIGEST=${INSPECT#*@}
sed -i "s,`$IMAGE.*`,`$IMAGE@$DIGEST`," playwright/testcontainers/synapse.ts
sed -i "s/const DOCKER_TAG.*/const DOCKER_TAG = \"develop@$DIGEST\";/" playwright/plugins/homeserver/synapse/index.ts
env:
IMAGE: ghcr.io/element-hq/synapse:develop
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/playwright-image-updates

View File

@@ -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@56cd38caf0615dd03f49d42ed301f1469911ac61
uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

View File

@@ -1,6 +1,5 @@
name: Close stale flaky issues
on:
workflow_dispatch: {}
schedule:
- cron: "30 1 * * *"
permissions: {}
@@ -18,4 +17,3 @@ jobs:
days-before-close: 0
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
exempt-issue-labels: "Z-Flaky-Test-Disabled"
operations-per-run: 100

View File

@@ -23,7 +23,7 @@ jobs:
run: "yarn update:jitsi"
- name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/jitsi-update

View File

@@ -1,68 +1,3 @@
Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18)
==================================================================================================
This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room.
## 🐛 Bug Fixes
* Upgrade matrix-sdk-crypto-wasm to 1.11.0 (https://github.com/matrix-org/matrix-js-sdk/pull/4593)
* Fix url preview display ([#28766](https://github.com/element-hq/element-web/pull/28766)).
Changes in [1.11.88](https://github.com/element-hq/element-web/releases/tag/v1.11.88) (2024-12-17)
==================================================================================================
## ✨ Features
* Allow trusted Element Call widget to send and receive media encryption key to-device messages ([#28316](https://github.com/element-hq/element-web/pull/28316)). Contributed by @hughns.
* increase ringing timeout from 10 seconds to 90 seconds ([#28630](https://github.com/element-hq/element-web/pull/28630)). Contributed by @fkwp.
* Add `Close` tooltip to dialog ([#28617](https://github.com/element-hq/element-web/pull/28617)). Contributed by @florianduros.
* New UX for Share dialog ([#28598](https://github.com/element-hq/element-web/pull/28598)). Contributed by @florianduros.
* Improve performance of RoomContext in RoomHeader ([#28574](https://github.com/element-hq/element-web/pull/28574)). Contributed by @t3chguy.
* Remove `Features.RustCrypto` flag ([#28582](https://github.com/element-hq/element-web/pull/28582)). Contributed by @florianduros.
* Add Modernizr warning when running in non-secure context ([#28581](https://github.com/element-hq/element-web/pull/28581)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Fix jumpy timeline when the pinned message banner is displayed ([#28654](https://github.com/element-hq/element-web/pull/28654)). Contributed by @florianduros.
* Fix font \& spaces in settings subsection ([#28631](https://github.com/element-hq/element-web/pull/28631)). Contributed by @florianduros.
* Remove manual device verification which is not supported by the new cryptography stack ([#28588](https://github.com/element-hq/element-web/pull/28588)). Contributed by @florianduros.
* Fix code block highlighting not working reliably with many code blocks ([#28613](https://github.com/element-hq/element-web/pull/28613)). Contributed by @t3chguy.
* Remove remaining reply fallbacks code ([#28610](https://github.com/element-hq/element-web/pull/28610)). Contributed by @t3chguy.
* Provide a way to activate GIFs via the keyboard for a11y ([#28611](https://github.com/element-hq/element-web/pull/28611)). Contributed by @t3chguy.
* Fix format bar position ([#28591](https://github.com/element-hq/element-web/pull/28591)). Contributed by @florianduros.
* Fix room taking long time to load ([#28579](https://github.com/element-hq/element-web/pull/28579)). Contributed by @florianduros.
* Show the correct shield status in tooltip for more conditions ([#28476](https://github.com/element-hq/element-web/pull/28476)). Contributed by @uhoreg.
Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03)
==================================================================================================
## ✨ Features
* Send and respect MSC4230 is\_animated flag ([#28513](https://github.com/element-hq/element-web/pull/28513)). Contributed by @t3chguy.
* Display a warning when an unverified user's identity changes ([#28211](https://github.com/element-hq/element-web/pull/28211)). Contributed by @uhoreg.
* Swap out Twitter link for Mastodon on auth footer ([#28508](https://github.com/element-hq/element-web/pull/28508)). Contributed by @t3chguy.
* Consider `org.matrix.msc3417.call` as video room in create room dialog ([#28497](https://github.com/element-hq/element-web/pull/28497)). Contributed by @t3chguy.
* Standardise icons using Compound Design Tokens ([#28217](https://github.com/element-hq/element-web/pull/28217)). Contributed by @t3chguy.
* Start sending stable `m.marked_unread` events ([#28478](https://github.com/element-hq/element-web/pull/28478)). Contributed by @tulir.
* Upgrade to compound-design-tokens v2 ([#28471](https://github.com/element-hq/element-web/pull/28471)). Contributed by @t3chguy.
* Standardise icons using Compound Design Tokens ([#28286](https://github.com/element-hq/element-web/pull/28286)). Contributed by @t3chguy.
* Remove reply fallbacks as per merged MSC2781 ([#28406](https://github.com/element-hq/element-web/pull/28406)). Contributed by @t3chguy.
* Use React Suspense when rendering async modals ([#28386](https://github.com/element-hq/element-web/pull/28386)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Add spinner when room encryption is loading in room settings ([#28535](https://github.com/element-hq/element-web/pull/28535)). Contributed by @florianduros.
* Fix getOidcCallbackUrl for Element Desktop ([#28521](https://github.com/element-hq/element-web/pull/28521)). Contributed by @t3chguy.
* Filter out redacted poll votes to avoid crashing the Poll widget ([#28498](https://github.com/element-hq/element-web/pull/28498)). Contributed by @t3chguy.
* Fix force tab complete not working since switching to React 18 createRoot API ([#28505](https://github.com/element-hq/element-web/pull/28505)). Contributed by @t3chguy.
* Fix media captions in bubble layout ([#28480](https://github.com/element-hq/element-web/pull/28480)). Contributed by @tulir.
* Reset cross-signing before backup when resetting both ([#28402](https://github.com/element-hq/element-web/pull/28402)). Contributed by @uhoreg.
* Listen to events so that encryption icon updates when status changes ([#28407](https://github.com/element-hq/element-web/pull/28407)). Contributed by @uhoreg.
* Check that the file the user chose has a MIME type of `image/*` ([#28467](https://github.com/element-hq/element-web/pull/28467)). Contributed by @t3chguy.
* Fix download button size in message action bar ([#28472](https://github.com/element-hq/element-web/pull/28472)). Contributed by @t3chguy.
* Allow tab completing users in brackets ([#28460](https://github.com/element-hq/element-web/pull/28460)). Contributed by @t3chguy.
* Fix React 18 strict mode breaking spotlight dialog ([#28452](https://github.com/element-hq/element-web/pull/28452)). Contributed by @MidhunSureshR.
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
==================================================================================================
## ✨ Features

View File

@@ -1,17 +1,20 @@
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye 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
ARG JS_SDK_REPO="https://github.com/matrix-org/matrix-js-sdk.git"
ARG JS_SDK_BRANCH="master"
RUN apt-get update && apt-get install -y git dos2unix
WORKDIR /src
COPY . /src
RUN /src/scripts/docker-link-repos.sh
RUN dos2unix /src/scripts/docker-link-repos.sh && bash /src/scripts/docker-link-repos.sh
RUN yarn --network-timeout=200000 install
RUN /src/scripts/docker-package.sh
RUN dos2unix /src/scripts/docker-package.sh /src/scripts/get-version-from-git.sh /src/scripts/normalize-version.sh && bash /src/scripts/docker-package.sh
# Copy the config now so that we don't create another layer in the app image
RUN cp /src/config.sample.json /src/webapp/config.json
@@ -21,22 +24,8 @@ 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/
# 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
# Override default nginx config
COPY /nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
RUN rm -rf /usr/share/nginx/html \
&& ln -s /app /usr/share/nginx/html
# Run as nginx user by default
USER nginx
# HTTP listen port
ENV ELEMENT_WEB_PORT=80

View File

@@ -1,6 +0,0 @@
Licensees holding a valid commercial license with Element may use this
software in accordance with the terms contained in a written agreement
between you and Element.
To purchase a commercial license please contact our sales team at
licensing@element.io

View File

@@ -311,18 +311,3 @@ For a developer guide, see the [translating dev doc](docs/translating-dev.md).
Issues are triaged by community members and the Web App Team, following the [triage process](https://github.com/element-hq/element-meta/wiki/Triage-process).
We use [issue labels](https://github.com/element-hq/element-meta/wiki/Issue-labelling) to sort all incoming issues.
## Copyright & License
Copyright (c) 2014-2017 OpenMarket Ltd
Copyright (c) 2017 Vector Creations Ltd
Copyright (c) 2017-2025 New Vector Ltd
This software is multi licensed by New Vector Ltd (Element). It can be used either:
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(2) for free under the terms of the GNU General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(3) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -3,7 +3,7 @@
"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"
"license": "AGPL-3.0-only OR GPL-3.0-only"
},
"bugs": {
"list": "https://github.com/element-hq/element-web/issues",

View File

@@ -60,22 +60,6 @@ would be:
docker run --rm -p 127.0.0.1:80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web
```
The Docker image is configured to run as an unprivileged (non-root) user by
default. This should be fine on modern Docker runtimes, but binding to port 80
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.
The behaviour of the docker image can be customised via the following
environment variables:
- `ELEMENT_WEB_PORT`
The port to listen on (within the docker container) for HTTP
traffic. Defaults to `80`.
### Building the docker image
To build the image yourself:
```bash

View File

@@ -1,9 +1,29 @@
# OIDC and delegated authentication
## Compatibility/OIDC-aware mode
[MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965)
[MSC3824: OIDC aware clients](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
This mode uses an SSO flow to gain a `loginToken` from the authentication provider, then continues with SSO login.
Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
Wherever valid MSC2965 configuration is discovered, OIDC-aware login flow will be the only option offered.
## (🧪Experimental) OIDC-native flow
Can be enabled by a config-level-only setting in `config.json`
```json
{
"features": {
"feature_oidc_native_flow": true
}
}
```
See https://areweoidcyet.com/client-implementation-guide/ for implementation details.
Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
Where a valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
Where OIDC native login flow is enabled and valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
Element Web will attempt to [dynamically register](https://openid.net/specs/openid-connect-registration-1_0.html) with the configured OP.
Then, authentication will be completed [as described here](https://areweoidcyet.com/client-implementation-guide/).

View File

@@ -23,19 +23,21 @@ element-web project is fine: leave it running it a different terminal as you wou
when developing. Alternatively if you followed the development set up from element-web then
Playwright will be capable of running the webserver on its own if it isn't already running.
The tests use [testcontainers](https://node.testcontainers.org/) to launch Homeserver (Synapse or Dendrite)
instances to test against, so you'll also need to one of the
[supported container runtimes](#supporter-container-runtimes)
installed and working in order to run the Playwright tests.
The tests use Docker to launch Homeserver (Synapse or Dendrite) instances to test against, so you'll also
need to have Docker installed and working in order to run the Playwright tests.
There are a few different ways to run the tests yourself. The simplest is to run:
```shell
docker pull ghcr.io/element-hq/synapse:develop
yarn run test:playwright
```
This will run the Playwright tests once, non-interactively.
Note: you don't need to run the `docker pull` command every time, but you should
do it regularly to ensure you are running against an up-to-date Synapse.
You can also run individual tests this way too, as you'd expect:
```shell
@@ -51,33 +53,41 @@ yarn run test:playwright:open --headed --debug
See more command line options at <https://playwright.dev/docs/test-cli>.
## Projects
### Running with Rust cryptography
By default, Playwright will run all "Projects", this means tests will run against Chrome, Firefox and "Safari" (Webkit).
We only run tests against Chrome in pull request CI, but all projects in the merge queue.
Some tests are excluded from running on certain browsers due to incompatibilities in the test harness.
`matrix-js-sdk` is currently in the
[process](https://github.com/vector-im/element-web/issues/21972) of being
updated to replace its end-to-end encryption implementation to use the [Matrix
Rust SDK](https://github.com/matrix-org/matrix-rust-sdk). This is not currently
enabled by default, but it is possible to have Playwright configure Element to use
the Rust crypto implementation by passing `--project="Rust Crypto"` or using
the top left options in open mode.
## How the Tests Work
Everything Playwright-related lives in the `playwright/` subdirectory
Everything Playwright-related lives in the `playwright/` subdirectory of react-sdk
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
`playwright/testcontainers` contains the testcontainers which start instances
of Synapse/Dendrite. These servers are what Element-web runs against in the tests.
`playwright/plugins/homeservers` contains Playwright plugins that starts instances
of Synapse/Dendrite in Docker containers. These servers are what Element-web runs
against in the tests.
Synapse can be launched with different configurations in order to test element
in different configurations. You can specify `synapseConfigOptions` as such:
in different configurations. `playwright/plugins/homeserver/synapse/templates`
contains template configuration files for each different configuration.
```typescript
test.use({
synapseConfigOptions: {
// The config options to pass to the Synapse instance
},
});
```
Each test suite can then launch whatever Synapse instances it needs in whatever
configurations.
The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
The logs from testcontainers will be attached to any reports output from Playwright.
Note that although tests should stop the Homeserver instances after running and the
plugin also stop any remaining instances after all tests have run, it is possible
to be left with some stray containers if, for example, you terminate a test such
that the `after()` does not run and also exit Playwright uncleanly. All the containers
it starts are prefixed, so they are easy to recognise. They can be removed safely.
After each test run, logs from the Synapse instances are saved in `playwright/logs/synapse`
with each instance in a separate directory named after its ID. These logs are removed
at the start of each test run.
## Writing Tests
@@ -107,6 +117,25 @@ Homeserver instances should be reasonably cheap to start (you may see the first
while as it pulls the Docker image).
You do not need to explicitly clean up the instance as it will be cleaned up by the fixture.
### Synapse Config Templates
When a Synapse instance is started, it's given a config generated from one of the config
templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files
in these templates:
- `homeserver.yaml`:
Template substitution happens in this file. Template variables are:
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
- `MACAROON_SECRET_KEY`: Generated each time for security
- `FORM_SECRET`: Generated each time for security
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
Config templates should not contain a signing key and instead assume that one will exist
in this file.
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
in a template can be referenced in the config as `/data/foo.html`.
### Logging In
We again heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
@@ -188,27 +217,3 @@ instead of the native `toHaveScreenshot`.
If you are running Linux and are unfortunate that the screenshots are not rendering identically,
you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you.
## Test Tags
We use test tags to categorise tests for running subsets more efficiently.
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.
- `@no-$project`: Tests which are unsupported in $Project. These tests will be skipped when running in $Project.
Anything testing Matrix media will need to have `@no-firefox` and `@no-webkit` as those rely on the service worker which
has to be disabled in Playwright on Firefox & Webkit to retain routing functionality.
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
there at this time.
If you wish to run all tests in a PR, you can give it the label `X-Run-All-Tests`.
## Supporter container runtimes
We use testcontainers to spin up various instances of Synapse, Matrix Authentication Service, and more.
It supports Docker out of the box but also has support for Podman, Colima, Rancher, you just need to follow some instructions to achieve it:
https://node.testcontainers.org/supported-container-runtimes/
If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
within `$HOME` to allow bind mounting temporary directories into the Docker containers.

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -14,8 +14,6 @@ const config: Config = {
testEnvironment: "jsdom",
testEnvironmentOptions: {
url: "http://localhost/",
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
customExportConditions: ["browser", "node"],
},
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
globalSetup: "<rootDir>/test/globalSetup.ts",

View File

@@ -1,7 +1,7 @@
/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -1,7 +1,7 @@
/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -1,7 +1,7 @@
/*
Copyright 2022-2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -1,6 +1,6 @@
server {
listen ${ELEMENT_WEB_PORT};
listen [::]:${ELEMENT_WEB_PORT};
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;

View File

@@ -1,13 +1,13 @@
{
"name": "element-web",
"version": "1.11.89",
"version": "1.11.86",
"description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.",
"repository": {
"type": "git",
"url": "https://github.com/element-hq/element-web"
},
"license": "SEE LICENSE IN README.md",
"license": "AGPL-3.0-only OR GPL-3.0-only",
"files": [
"lib",
"res",
@@ -64,35 +64,30 @@
"test:playwright:open": "yarn test:playwright --ui",
"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",
"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",
"coverage": "yarn test --coverage",
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
"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"
},
"resolutions": {
"@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.30001690",
"caniuse-lite": "1.0.30001684",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.0",
"@matrix-org/emojibase-bindings": "^1.3.3",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@vector-im/compound-design-tokens": "^2.1.0",
"@vector-im/compound-web": "^7.5.0",
"@vector-im/matrix-wysiwyg": "2.38.0",
"@vector-im/compound-design-tokens": "^2.0.1",
"@vector-im/compound-web": "^7.4.0",
"@vector-im/matrix-wysiwyg": "2.37.13",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -119,12 +114,12 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-element": "4.2.0",
"linkify-react": "4.2.0",
"linkify-string": "4.2.0",
"linkifyjs": "4.2.0",
"linkify-element": "4.1.4",
"linkify-react": "4.1.4",
"linkify-string": "4.1.4",
"linkifyjs": "4.1.4",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
@@ -137,7 +132,7 @@
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.157.2",
"qrcode": "1.5.4",
"re-resizable": "6.10.3",
"re-resizable": "6.10.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.3.0",
@@ -146,14 +141,12 @@
"react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.14.0",
"sanitize-html": "2.13.1",
"tar-js": "^0.3.0",
"temporal-polyfill": "^0.2.5",
"ua-parser-js": "^1.0.2",
"uuid": "^11.0.0",
"what-input": "^5.2.10",
"@types/react-virtualized": "^9.21.30",
"react-virtualized": "^9.22.5"
"what-input": "^5.2.10"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -182,7 +175,6 @@
"@sentry/webpack-plugin": "^2.7.1",
"@stylistic/eslint-plugin": "^2.9.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",
@@ -194,6 +186,7 @@
"@types/escape-html": "^1.0.1",
"@types/express": "^5.0.0",
"@types/file-saver": "^2.0.3",
"@types/fs-extra": "^11.0.0",
"@types/glob-to-regexp": "^0.4.1",
"@types/jest": "29.5.12",
"@types/jitsi-meet": "^2.0.2",
@@ -206,21 +199,22 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "18.3.18",
"@types/react": "18.3.3",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "18.3.5",
"@types/react-dom": "18.3.1",
"@types/react-transition-group": "^4.4.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",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"babel-jest": "^29.0.0",
"babel-loader": "^9.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"blob-polyfill": "^9.0.0",
"buffer": "^6.0.3",
"chokidar": "^4.0.0",
"concurrently": "^9.0.0",
"copy-webpack-plugin": "^12.0.0",
@@ -245,6 +239,7 @@
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^6.0.0",
"fs-extra": "^11.0.0",
"glob": "^11.0.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
@@ -258,12 +253,12 @@
"lint-staged": "^15.0.2",
"mailhog": "^4.16.0",
"matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2",
"mini-css-extract-plugin": "2.9.0",
"minimist": "^1.2.6",
"modernizr": "^3.12.0",
"node-fetch": "^2.6.7",
"playwright-core": "^1.45.1",
"postcss": "8.4.46",
"postcss": "8.4.38",
"postcss-easings": "^4.0.0",
"postcss-hexrgba": "2.1.0",
"postcss-import": "16.1.0",
@@ -273,27 +268,25 @@
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.4.2",
"prettier": "3.4.1",
"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.1.0",
"stylelint-config-standard": "^36.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^10.16.0",
"ts-node": "^10.9.1",
"ts-prune": "^0.10.3",
"typescript": "5.7.2",
"typescript": "5.6.3",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.8.0",
"webpack-cli": "^6.0.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^5.0.0",
"webpack-version-file-plugin": "^0.5.0",
"yaml": "^2.3.3"

View File

@@ -2,58 +2,24 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { defineConfig, devices } from "@playwright/test";
import { defineConfig } from "@playwright/test";
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
export default defineConfig({
projects: [
{
name: "Chrome",
use: {
...devices["Desktop Chrome"],
channel: "chromium",
permissions: ["clipboard-write", "clipboard-read", "microphone"],
launchOptions: {
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
},
},
},
{
name: "Firefox",
use: {
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"permissions.default.microphone": 1,
},
},
// This is needed to work around an issue between Playwright routes, Firefox, and Service workers
// https://github.com/microsoft/playwright/issues/33561#issuecomment-2471642120
serviceWorkers: "block",
},
ignoreSnapshots: true,
},
{
name: "WebKit",
use: {
...devices["Desktop Safari"],
// Seemingly WebKit has the same issue as Firefox in Playwright routes not working
// https://playwright.dev/docs/network#missing-network-events-and-service-workers
serviceWorkers: "block",
},
ignoreSnapshots: true,
},
],
use: {
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
video: "retain-on-failure",
baseURL,
permissions: ["clipboard-write", "clipboard-read", "microphone"],
launchOptions: {
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
},
trace: "on-first-retry",
},
webServer: {

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.49.1-noble
FROM mcr.microsoft.com/playwright:v1.49.0-jammy
WORKDIR /work

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,13 +2,13 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => {
test(`shows error page if browser lacks Intl support`, async ({ page }) => {
await page.addInitScript({ content: `delete window.Intl;` });
await page.goto("/");
@@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, a
await expect(page).toMatchScreenshot("unsupported-browser.png");
});
test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => {
test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => {
await page.addInitScript({ content: `delete window.WebAssembly;` });
await page.goto("/");

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -13,8 +13,13 @@ Please see LICENSE files in the repository root for full details.
import { expect, test } from "../../element-web-test";
test.use({
synapseConfigOptions: {
allow_guest_access: true,
startHomeserverOpts: "guest-enabled",
config: async ({ homeserver }, use) => {
await use({
default_server_config: {
"m.homeserver": { base_url: homeserver.config.baseUrl },
},
});
},
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -3,7 +3,7 @@ Copyright 2024 New Vector Ltd.
Copyright 2023 Suguru Hirahara
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -13,7 +13,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.describe("Audio player", () => {
test.use({
displayName: "Hanako",
});
@@ -134,22 +134,18 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
).toBeVisible();
});
test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => {
test("should be correctly rendered - light theme", async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
});
test(
"should be correctly rendered - light theme with monospace font",
{ tag: "@screenshot" },
async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
},
);
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
});
test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => {
test("should be correctly rendered - high contrast theme", async ({ page, app }) => {
// Disable system theme in case ThemeWatcher enables the theme automatically,
// so that the high contrast theme can be enabled
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
@@ -165,7 +161,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
});
test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => {
test("should be correctly rendered - dark theme", async ({ page, app }) => {
// Enable dark theme
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
@@ -211,100 +207,93 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
expect(download.suggestedFilename()).toBe("1sec.ogg");
});
test(
"should support replying to audio file with another audio file",
{ tag: "@screenshot" },
async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec.ogg");
test("should support replying to audio file with another audio file", async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Assert the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Find and click "Reply" button on MessageActionBar
const tile = page.locator(".mx_EventTile_last");
// Find and click "Reply" button on MessageActionBar
const tile = page.locator(".mx_EventTile_last");
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
// Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that replied audio file is rendered as file button inside ReplyChain
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
// Assert that the file button has file name
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
});
test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => {
// Note: "mx_ReplyChain" element is used not only for replies which
// create a reply chain, but also for a single reply without a replied
// message. This test checks whether a reply chain which consists of
// multiple audio file replies is rendered properly.
const tile = page.locator(".mx_EventTile_last");
// Find and click "Reply" button
const clickButtonReply = async () => {
await tile.scrollIntoViewIfNeeded();
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
};
// Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/1sec.ogg");
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Assert that replied audio file is rendered as file button inside ReplyChain
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
// Assert that the file button has file name
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
await clickButtonReply();
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
},
);
// Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
test(
"should support creating a reply chain with multiple audio files",
{ tag: "@screenshot" },
async ({ page, app, user }) => {
// Note: "mx_ReplyChain" element is used not only for replies which
// create a reply chain, but also for a single reply without a replied
// message. This test checks whether a reply chain which consists of
// multiple audio file replies is rendered properly.
// Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
const tile = page.locator(".mx_EventTile_last");
await clickButtonReply();
// Find and click "Reply" button
const clickButtonReply = async () => {
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
};
// Reply to the player with yet another audio file to create a reply chain
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Assert that there are two "mx_ReplyChain" elements
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
await clickButtonReply();
// Assert that one line contains the user name
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
// Reply to the player with another audio file
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
// Assert that the other line contains the file button
await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible();
// Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
// Click "In reply to"
await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click();
await clickButtonReply();
const replyChain = tile.locator(".mx_ReplyChain:first-of-type");
// Assert that "In reply to" has disappeared
await expect(replyChain.getByText("In reply to")).not.toBeVisible();
// Reply to the player with yet another audio file to create a reply chain
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
// Assert that the file button contains the name of the file sent at first
await expect(
replyChain
.locator(".mx_MFileBody_info[role='button']")
.locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
).toBeVisible();
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
// Assert that there are two "mx_ReplyChain" elements
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
// Assert that one line contains the user name
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
// Assert that the other line contains the file button
await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible();
// Click "In reply to"
await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click();
const replyChain = tile.locator(".mx_ReplyChain:first-of-type");
// Assert that "In reply to" has disappeared
await expect(replyChain.getByText("In reply to")).not.toBeVisible();
// Assert that the file button contains the name of the file sent at first
await expect(
replyChain
.locator(".mx_MFileBody_info[role='button']")
.locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
).toBeVisible();
// Take snapshots
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
},
);
// Take snapshots
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
});
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -89,47 +89,43 @@ test.describe("HTML Export", () => {
},
});
test(
"should export html successfully and match screenshot",
{ tag: "@screenshot" },
async ({ page, app, room }) => {
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
// about the width changing and we can actually test this line looks correct.
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
test("should export html successfully and match screenshot", async ({ page, app, room }) => {
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
// about the width changing and we can actually test this line looks correct.
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
// Send a bunch of messages to populate the room
for (let i = 1; i < 10; i++) {
const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" });
if (i == 1) {
await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃");
}
// Send a bunch of messages to populate the room
for (let i = 1; i < 10; i++) {
const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" });
if (i == 1) {
await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃");
}
}
// Wait for all the messages to be displayed
await expect(
page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"),
).toBeVisible();
// Wait for all the messages to be displayed
await expect(
page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"),
).toBeVisible();
await app.toggleRoomInfoPanel();
await page.getByRole("menuitem", { name: "Export Chat" }).click();
await app.toggleRoomInfoPanel();
await page.getByRole("menuitem", { name: "Export Chat" }).click();
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export", exact: true }).click();
const download = await downloadPromise;
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export", exact: true }).click();
const download = await downloadPromise;
const dirPath = path.join(os.tmpdir(), "html-export-test");
const zipPath = `${dirPath}.zip`;
await download.saveAs(zipPath);
const dirPath = path.join(os.tmpdir(), "html-export-test");
const zipPath = `${dirPath}.zip`;
await download.saveAs(zipPath);
const zip = await extractZipFileToPath(zipPath, dirPath);
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
await expect(page).toMatchScreenshot("html-export.png", {
mask: [
// We need to mask the whole thing because the width of the time part changes
page.locator(".mx_TimelineSeparator"),
page.locator(".mx_MessageTimestamp"),
],
});
},
);
const zip = await extractZipFileToPath(zipPath, dirPath);
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
await expect(page).toMatchScreenshot("html-export.png", {
mask: [
// We need to mask the whole thing because the width of the time part changes
page.locator(".mx_TimelineSeparator"),
page.locator(".mx_MessageTimestamp"),
],
});
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -79,8 +79,9 @@ test.describe("Composer", () => {
// Enter some more text, then send the message
await page.getByRole("textbox").pressSequentially("this is the spoiler text ");
await page.getByRole("button", { name: "Send message" }).click();
// Check that a spoiler item has appeared in the timeline and contains the spoiler text
await expect(page.locator("button.mx_EventTile_spoiler")).toHaveText("this is the spoiler text");
// Check that a spoiler item has appeared in the timeline and locator the spoiler command text
await expect(page.locator("button.mx_EventTile_spoiler")).toBeVisible();
await expect(page.getByText("this is the spoiler text")).toBeVisible();
});
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -1,91 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { registerAccountMas } from "../oidc";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { TestClientServerAPI } from "../csAPI";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
// These tests register an account with MAS because then we go through the "normal" registration flow
// and crypto gets set up. Using the 'user' fixture create a a user an synthesizes an existing login,
// which is faster but leaves us without crypto set up.
test.use(masHomeserver);
test.describe("Encryption state after registration", () => {
test.skip(isDendrite, "does not yet support MAS");
test("Key backup is enabled by default", async ({ page, mailhogClient, app }) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!");
await app.settings.openUserSettings("Security & Privacy");
await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
});
test("user is prompted to set up recovery", async ({ page, mailhogClient, app }) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!");
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();
await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
});
});
test.describe("Key backup reset from elsewhere", () => {
test.skip(isDendrite, "does not yet support MAS");
test("Key backup is disabled when reset from elsewhere", async ({ page, mailhogClient, request, homeserver }) => {
const testUsername = "alice";
const testPassword = "Pa$sW0rD!";
// there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake
// clock so we can skip the delay
await page.clock.install();
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword);
await page.getByRole("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("test room");
await page.getByRole("button", { name: "Create room" }).click();
// @ts-ignore - this runs in the browser scope where mxMatrixClientPeg is a thing. Here, it is not.
const accessToken = await page.evaluate(() => mxMatrixClientPeg.get().getAccessToken());
const csAPI = new TestClientServerAPI(request, homeserver, accessToken);
const backupInfo = await csAPI.getCurrentBackupInfo();
await csAPI.deleteBackupVersion(backupInfo.version);
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
await page.getByRole("button", { name: "Send message" }).click();
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
await page.getByRole("button", { name: "Send message" }).click();
// Should be the message we sent plus the room creation event
await expect(page.locator(".mx_EventTile")).toHaveCount(2);
await expect(
page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"),
).toBeVisible();
// Wait for it to try uploading the key
await page.clock.fastForward(20000);
await expect(page.getByRole("heading", { level: 1, name: "New Recovery Method" })).toBeVisible();
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -23,94 +23,90 @@ test.describe("Backups", () => {
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");
test("Create, delete and recreate a keys backup", 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();
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
const currentDialogLocator = page.locator(".mx_Dialog");
const currentDialogLocator = page.locator(".mx_Dialog");
// It's the first time and secure storage is not set up, so it will create one
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
// copy the recovery key to use it later
const securityKey = await app.getClipboard();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// It's the first time and secure storage is not set up, so it will create one
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
// copy the recovery key to use it later
const securityKey = await app.getClipboard();
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
// Open the settings again
await app.settings.openUserSettings("Security & Privacy");
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
// 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();
// 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 expectBackupVersionToBe(page, "1");
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Delete it
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
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: "Security Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// Create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Security 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();
// 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();
// 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();
// 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");
await expectBackupVersionToBe(page, "2");
// ==
// Ensure that if you don't have the secret storage passphrase the backup won't be created
// ==
// ==
// 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();
// 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: "Security Key" })).toBeVisible();
// But cancel the security key dialog, to simulate not having the secret storage passphrase
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
// Try to create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
// But cancel the security 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();
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();
},
);
// 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();
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -19,9 +19,9 @@ test.describe("Complete security", () => {
homeserver,
credentials,
}) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible();
});
// see also "Verify device during login with SAS" in `verification.spec.ts`.
// see also "Verify device during login with SAS" in `verifiction.spec.ts`.
});

View File

@@ -2,7 +2,7 @@
Copyright 2024 New Vector Ltd.
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -81,7 +81,7 @@ 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") {
async function verifyKey(app: ElementAppPage, keyType: string) {
const accountData: { encrypted: Record<string, Record<string, string>> } = await app.client.evaluate(
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
keyType,
@@ -204,29 +204,30 @@ test.describe("Cryptography", function () {
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
});
test(
"creating a DM should work, being e2e-encrypted / user verification",
{ tag: "@screenshot" },
async ({ page, app, bot: bob, user: aliceCredentials }) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob);
// send first message
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
await testMessages(page, bob, bobRoomId);
await verify(app, bob);
test("creating a DM should work, being e2e-encrypted / user verification", async ({
page,
app,
bot: bob,
user: aliceCredentials,
}) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob);
// send first message
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
await testMessages(page, bob, bobRoomId);
await verify(app, bob);
// Assert that verified icon is rendered
await page.getByTestId("base-card-back-button").click();
await page.getByLabel("Room info").nth(1).click();
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted");
// Assert that verified icon is rendered
await page.getByTestId("base-card-back-button").click();
await page.getByLabel("Room info").nth(1).click();
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted");
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
},
);
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
});
test("should allow verification when there is no existing DM", async ({
page,

View File

@@ -2,7 +2,7 @@
Copyright 2024 New Vector Ltd.
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -45,7 +45,7 @@ test.describe("Cryptography", function () {
await logOutOfElement(page, true);
// Log in again, and see how the message looks.
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
await app.viewRoomByName("Test room");
const lastTile = page.locator(".mx_EventTile").last();
await expect(lastTile).toContainText("Historical messages are not available on this device");
@@ -62,7 +62,7 @@ test.describe("Cryptography", function () {
// Finally, log out again, and back in, skipping verification for now, and see what we see.
await logOutOfElement(page);
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
await app.viewRoomByName("Test room");

View File

@@ -2,34 +2,26 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { test as base, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "../right-panel/utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
const ROOM_NAME = "Test room";
const NAME = "Alice";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
}
test.use({
displayName: NAME,
synapseConfigOptions: {
experimental_features: {
msc2697_enabled: false,
msc3814_enabled: true,
},
const test = base.extend({
// eslint-disable-next-line no-empty-pattern
startHomeserverOpts: async ({}, use) => {
await use("dehydration");
},
config: async ({ config, context }, use) => {
config: async ({ homeserver, context }, use) => {
const wellKnown = {
...config.default_server_config,
"m.homeserver": {
base_url: homeserver.config.baseUrl,
},
"org.matrix.msc3814": true,
};
@@ -37,14 +29,29 @@ test.use({
await route.fulfill({ json: wellKnown });
});
await use(config);
await use({
default_server_config: wellKnown,
});
},
});
const ROOM_NAME = "Test room";
const NAME = "Alice";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_EntityTile, [title="${name}"]`);
}
test.describe("Dehydration", () => {
test.skip(isDendrite, "does not yet support dehydration v2");
test.use({
displayName: NAME,
});
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
// Create a backup (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy");
@@ -88,7 +95,7 @@ test.describe("Dehydration", () => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
await expect(page.locator(".mx_MemberListView")).toBeVisible();
await expect(page.locator(".mx_MemberList")).toBeVisible();
await getMemberTileByName(page, NAME).click();
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -21,7 +21,7 @@ import {
} from "./utils";
import { Bot } from "../../pages/bot";
test.describe("Device verification", { tag: "@no-webkit" }, () => {
test.describe("Device verification", () => {
let aliceBotClient: Bot;
/** The backup version that was set up by the bot client. */
@@ -66,7 +66,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
}
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
@@ -93,7 +93,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
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);
await logIntoElement(page, homeserver, credentials);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
@@ -102,7 +102,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// feed the QR code into the verification request.
const qrData = await readQrCode(infoDialog);
const verifier = await verificationRequest.evaluateHandle(
(request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)),
(request, qrData) => request.scanQRCode(new Uint8Array(qrData)),
[...qrData],
);
@@ -137,7 +137,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
});
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
// Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
@@ -158,7 +158,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
});
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
// Select the security phrase
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
@@ -181,7 +181,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
});
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
/* Dismiss "Verify this device" */
const authPage = page.locator(".mx_AuthPage");

View File

@@ -2,12 +2,10 @@
Copyright 2024 New Vector Ltd.
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Locator } from "@playwright/test";
import { expect, test } from "../../element-web-test";
import {
autoJoin,
@@ -19,7 +17,6 @@ import {
verify,
} from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.describe("Cryptography", function () {
test.use({
@@ -53,8 +50,6 @@ test.describe("Cryptography", function () {
// Even though Alice has seen Bob's join event, Bob may not have done so yet. Wait for the sync to arrive.
await bob.awaitRoomMembership(testRoomId);
await app.client.network.setupRoute();
});
test("should show the correct shield on e2e events", async ({
@@ -135,7 +130,8 @@ test.describe("Cryptography", function () {
"Encrypted by a device not verified by its owner.",
);
/* Should show a red padlock for a message from an unverified device.
/* In legacy crypto: should show a grey padlock for a message from a deleted device.
* In rust crypto: should show a red padlock for a message from an unverified device.
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
* unverified, even if it gets deleted. */
// bob deletes his second device
@@ -169,7 +165,9 @@ test.describe("Cryptography", function () {
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Encrypted by a device not verified by its owner.",
workerInfo.project.name === "Legacy Crypto"
? "Encrypted by an unknown or deleted device."
: "Encrypted by a device not verified by its owner.",
);
});
@@ -207,7 +205,7 @@ test.describe("Cryptography", function () {
window.localStorage.clear();
});
await page.reload();
await logIntoElement(page, aliceCredentials, securityKey);
await logIntoElement(page, homeserver, aliceCredentials, securityKey);
/* go back to the test room and find Bob's message again */
await app.viewRoomById(testRoomId);
@@ -279,15 +277,6 @@ test.describe("Cryptography", function () {
bot: bob,
homeserver,
}) => {
// Workaround for https://github.com/element-hq/element-web/issues/28640:
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
// his user info.
await app.toggleRoomInfoPanel();
const rightPanel = page.locator(".mx_RightPanel");
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
// Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline();
@@ -317,7 +306,7 @@ test.describe("Cryptography", function () {
);
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await assertNoE2EIcon(penultimate, app);
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
});
test("should show correct shields on events sent by users with changed identity", async ({
@@ -346,21 +335,3 @@ test.describe("Cryptography", function () {
});
});
});
/**
* Check that the given message doesn't have an E2E warning icon.
*
* If it does, throw an error.
*/
async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
// Make sure the message itself exists, before we check if it has any icons
await messageLocator.waitFor();
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
if ((await e2eIcon.count()) > 0) {
// uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error.
await e2eIcon.focus();
const tooltip = await app.getTooltipForElement(e2eIcon);
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
}
}

View File

@@ -1,7 +1,7 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -11,7 +11,7 @@ import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom }
test.describe("Logout tests", () => {
test.beforeEach(async ({ page, homeserver, credentials }) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
});
test("Ask to set up recovery on logout if not setup", async ({ page, app }) => {

View File

@@ -2,33 +2,34 @@
Copyright 2024 New Vector Ltd.
Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import path from "path";
import { readFile } from "node:fs/promises";
import { expect, test } from "../../element-web-test";
import { expect, test as base } from "../../element-web-test";
test.describe("migration", { tag: "@no-webkit" }, function () {
test.use({
displayName: "Alice",
const test = base.extend({
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
const resourcePath = path.join(__dirname, new URL(request.url()).pathname);
const body = await readFile(resourcePath, { encoding: "utf-8" });
await route.fulfill({ body });
});
await page.goto("/test_indexeddb_cryptostore_dump/index.html");
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
const resourcePath = path.join(__dirname, new URL(request.url()).pathname);
const body = await readFile(resourcePath, { encoding: "utf-8" });
await route.fulfill({ body });
});
await page.goto("/test_indexeddb_cryptostore_dump/index.html");
await use(credentials);
},
});
await use(credentials);
},
});
test.describe("migration", function () {
test.use({ displayName: "Alice" });
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
test.skip(workerInfo.project.name === "Legacy Crypto", "This test only works with Rust crypto.");
test.slow();
// We should see a migration progress bar

View File

@@ -2,7 +2,7 @@
Copyright 2024 New Vector Ltd.
Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,13 +2,12 @@
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
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 Preset, type Visibility } from "matrix-js-sdk/src/matrix";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
import { Client } from "../../pages/client";
@@ -39,8 +38,6 @@ test.describe("User verification", () => {
toasts,
room: { roomId: dmRoomId },
}) => {
await waitForDeviceKeys(page);
// once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle(
async (client, { dmRoomId, aliceCredentials }) => {
@@ -90,8 +87,6 @@ test.describe("User verification", () => {
toasts,
room: { roomId: dmRoomId },
}) => {
await waitForDeviceKeys(page);
// once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle(
async (client, { dmRoomId, aliceCredentials }) => {
@@ -154,15 +149,3 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
],
});
}
/**
* Wait until we get the other user's device keys.
* In newer rust-crypto versions, the verification request will be ignored if we
* don't have the sender's device keys.
*/
async function waitForDeviceKeys(page: Page): Promise<void> {
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
const avatar = await page.getByRole("button", { name: "Avatar" });
await avatar.click();
await expect(page.getByText("1 session")).toBeVisible();
}

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -138,9 +138,22 @@ export async function checkDeviceIsConnectedKeyBackup(
*
* If a `securityKey` is given, verifies the new device using the key.
*/
export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) {
export async function logIntoElement(
page: Page,
homeserver: HomeserverInstance,
credentials: Credentials,
securityKey?: string,
) {
await page.goto("/#/login");
// select homeserver
await page.getByRole("button", { name: "Edit" }).click();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click();
// wait for the dialog to go away
await expect(page.locator(".mx_ServerPickerDialog")).not.toBeVisible();
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("button", { name: "Sign in" }).click();
@@ -207,7 +220,11 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
for (let i = 0; i < emojis.length; i++) {
const emoji = emojis[i];
const emojiBlock = emojiBlocks.nth(i);
await expect(emojiBlock).toHaveText(emoji[0] + emoji[1]);
const textContent = await emojiBlock.textContent();
// VerificationShowSas munges the case of the emoji descriptions returned by the js-sdk before
// displaying them. Once we drop support for legacy crypto, that code can go away, and so can the
// case-munging here.
expect(textContent.toLowerCase()).toEqual(emoji[0] + emoji[1].toLowerCase());
}
}

View File

@@ -1,48 +0,0 @@
/*
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.
*/
import { APIRequestContext } from "playwright-core";
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { HomeserverInstance } from "../plugins/homeserver";
/**
* A small subset of the Client-Server API used to manipulate the state of the
* account on the homeserver independently of the client under test.
*/
export class TestClientServerAPI {
public constructor(
private request: APIRequestContext,
private homeserver: HomeserverInstance,
private accessToken: string,
) {}
public async getCurrentBackupInfo(): Promise<KeyBackupInfo | null> {
const res = await this.request.get(`${this.homeserver.baseUrl}/_matrix/client/v3/room_keys/version`, {
headers: { Authorization: `Bearer ${this.accessToken}` },
});
return await res.json();
}
/**
* Calls the API directly to delete the given backup version
* @param version The version to delete
*/
public async deleteBackupVersion(version: string): Promise<void> {
const res = await this.request.delete(
`${this.homeserver.baseUrl}/_matrix/client/v3/room_keys/version/${version}`,
{
headers: { Authorization: `Bearer ${this.accessToken}` },
},
);
if (!res.ok) {
throw new Error(`Failed to delete backup version: ${res.status}`);
}
}
}

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -66,130 +66,126 @@ test.describe("Editing", () => {
botCreateOpts: { displayName: "Bob" },
});
test(
"should render and interact with the message edit history dialog",
{ tag: "@screenshot" },
async ({ page, user, app, room }) => {
// Click the "Remove" button on the message edit history dialog
const clickButtonRemove = async (locator: Locator) => {
const eventTileLine = locator.locator(".mx_EventTile_line");
await eventTileLine.hover();
await eventTileLine.getByRole("button", { name: "Remove" }).click();
};
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => {
// Click the "Remove" button on the message edit history dialog
const clickButtonRemove = async (locator: Locator) => {
const eventTileLine = locator.locator(".mx_EventTile_line");
await eventTileLine.hover();
await eventTileLine.getByRole("button", { name: "Remove" }).click();
};
await page.goto(`#/room/${room.roomId}`);
await page.goto(`#/room/${room.roomId}`);
// Send "Message"
await sendEvent(app, room.roomId);
// Send "Message"
await sendEvent(app, room.roomId);
// Edit "Message" to "Massage"
await editLastMessage(page, "Massage");
// Edit "Message" to "Massage"
await editLastMessage(page, "Massage");
// Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
// Assert that the edit label is visible
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
await clickEditedMessage(page, "Massage");
await clickEditedMessage(page, "Massage");
// Assert that the message edit history dialog is rendered
const dialog = page.getByRole("dialog");
const li = dialog.getByRole("listitem").last();
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
await expect(li).toHaveCSS("clear", "both");
// Assert that the message edit history dialog is rendered
const dialog = page.getByRole("dialog");
const li = dialog.getByRole("listitem").last();
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
await expect(li).toHaveCSS("clear", "both");
const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp");
await expect(timestamp).toHaveCSS("position", "absolute");
await expect(timestamp).toHaveCSS("inset-inline-start", "0px");
await expect(timestamp).toHaveCSS("text-align", "center");
const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp");
await expect(timestamp).toHaveCSS("position", "absolute");
await expect(timestamp).toHaveCSS("inset-inline-start", "0px");
await expect(timestamp).toHaveCSS("text-align", "center");
// Assert that monospace characters can fill the content line as expected
await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px");
// Assert that monospace characters can fill the content line as expected
await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px");
// Assert that zero block start padding is applied to mx_EventTile as expected
// See: .mx_EventTile on _EventTile.pcss
await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px");
// Assert that zero block start padding is applied to mx_EventTile as expected
// See: .mx_EventTile on _EventTile.pcss
await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px");
// Assert that the date separator is rendered at the top
await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS(
"text-transform",
"capitalize",
);
// Assert that the date separator is rendered at the top
await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS(
"text-transform",
"capitalize",
);
{
// Assert that the edited message is rendered under the date separator
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
// Assert that the edited message body consists of both deleted character and inserted character
// Above the first "e" of "Message" was replaced with "a"
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
{
// Assert that the edited message is rendered under the date separator
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
// Assert that the edited message body consists of both deleted character and inserted character
// Above the first "e" of "Message" was replaced with "a"
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
const body = tile.locator(".mx_EventTile_content .mx_EventTile_body");
await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible();
await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible();
}
const body = tile.locator(".mx_EventTile_content .mx_EventTile_body");
await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible();
await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible();
}
// Assert that the original message is rendered at the bottom
await expect(
dialog
.locator("li:nth-child(3) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible();
// Assert that the original message is rendered at the bottom
await expect(
dialog
.locator("li:nth-child(3) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible();
// Take a snapshot of the dialog
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
// Take a snapshot of the dialog
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
{
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again
await clickButtonRemove(tile);
}
{
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again
await clickButtonRemove(tile);
}
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
await app.closeDialog();
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
await app.closeDialog();
{
// Assert that the message edit history dialog is rendered again after it was closed
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again
await clickButtonRemove(tile);
}
{
// Assert that the message edit history dialog is rendered again after it was closed
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
// Click the "Remove" button again
await clickButtonRemove(tile);
}
// This time remove the message really
const textInputDialog = page.locator(".mx_TextInputDialog");
await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason
await textInputDialog.getByRole("button", { name: "Remove" }).click();
// This time remove the message really
const textInputDialog = page.locator(".mx_TextInputDialog");
await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason
await textInputDialog.getByRole("button", { name: "Remove" }).click();
// Assert that the message edit history dialog is rendered again
const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog");
// Assert that the date is rendered
await expect(
messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }),
).toHaveCSS("text-transform", "capitalize");
// Assert that the message edit history dialog is rendered again
const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog");
// Assert that the date is rendered
await expect(
messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }),
).toHaveCSS("text-transform", "capitalize");
// Assert that the original message is rendered under the date on the dialog
await expect(
messageEditHistoryDialog
.locator("li:nth-child(2) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible();
// Assert that the original message is rendered under the date on the dialog
await expect(
messageEditHistoryDialog
.locator("li:nth-child(2) .mx_EventTile")
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
).toBeVisible();
// Assert that the edited message is gone
await expect(
messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }),
).not.toBeVisible();
// Assert that the edited message is gone
await expect(
messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }),
).not.toBeVisible();
await app.closeDialog();
await app.closeDialog();
// Assert that the redaction placeholder is rendered
await expect(
page
.locator(".mx_RoomView_MessageList")
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
).toBeVisible();
},
);
// Assert that the redaction placeholder is rendered
await expect(
page
.locator(".mx_RoomView_MessageList")
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
).toBeVisible();
});
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
page,

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -25,7 +25,7 @@ test.describe("Image Upload", () => {
).toBeVisible();
});
test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => {
test("should show image preview when uploading an image", async ({ page, app }) => {
await page
.locator(".mx_MessageComposer_actions input[type='file']")
.setInputFiles("playwright/sample-files/riot.png");

View File

@@ -2,50 +2,44 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { expect, test } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";
const username = "user1234";
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
const password = "oETo7MPf0o";
const email = "user@nowhere.dummy";
test.use(emailHomeserver);
test.use({
config: {
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
},
},
},
});
test.describe("Forgot Password", () => {
test.skip(isDendrite, "not yet wired up");
test.use({
startHomeserverOpts: ({ mailhog }, use) =>
use({
template: "email",
variables: {
SMTP_HOST: "host.containers.internal",
SMTP_PORT: mailhog.instance.smtpPort,
},
}),
});
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
test("renders properly", async ({ page, homeserver }) => {
await page.goto("/");
await page.getByRole("link", { name: "Sign in" }).click();
// need to select a homeserver at this stage, before entering the forgot password flow
await selectHomeserver(page, homeserver.baseUrl);
await selectHomeserver(page, homeserver.config.baseUrl);
await page.getByRole("button", { name: "Forgot password?" }).click();
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
});
test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
test("renders email verification dialog properly", async ({ page, homeserver }) => {
const user = await homeserver.registerUser(username, password);
await homeserver.setThreepid(user.userId, "email", email);
@@ -53,7 +47,7 @@ test.describe("Forgot Password", () => {
await page.goto("/");
await page.getByRole("link", { name: "Sign in" }).click();
await selectHomeserver(page, homeserver.baseUrl);
await selectHomeserver(page, homeserver.config.baseUrl);
await page.getByRole("button", { name: "Forgot password?" }).click();

View File

@@ -2,14 +2,13 @@
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
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 { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
@@ -93,7 +92,7 @@ test.describe("Integration Manager: Get OpenID Token", () => {
},
},
id: "integration-manager",
} as unknown as UserWidget,
},
});
// Succeed when checking the token is valid

View File

@@ -2,14 +2,13 @@
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
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 { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
const USER_DISPLAY_NAME = "Alice";
@@ -137,7 +136,7 @@ test.describe("Integration Manager: Kick", () => {
},
},
id: "integration-manager",
} as unknown as UserWidget,
},
});
// Succeed when checking the token is valid

View File

@@ -2,14 +2,13 @@
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
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 { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
@@ -108,7 +107,7 @@ test.describe("Integration Manager: Read Events", () => {
},
},
id: "integration-manager",
} as unknown as UserWidget,
},
});
// Succeed when checking the token is valid

View File

@@ -2,14 +2,13 @@
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
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 { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { openIntegrationManager } from "./utils";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const ROOM_NAME = "Integration Manager Test";
@@ -114,7 +113,7 @@ test.describe("Integration Manager: Send Event", () => {
},
},
id: "integration-manager",
} as unknown as UserWidget,
},
});
// Succeed when checking the token is valid

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -3,7 +3,7 @@ Copyright 2024 New Vector Ltd.
Copyright 2023 Suguru Hirahara
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -19,7 +19,7 @@ test.describe("Invite dialog", function () {
const botName = "BotAlice";
test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
test("should support inviting a user to a room", async ({ page, app, user, bot }) => {
// Create and view a room
await app.client.createRoom({ name: "Test Room" });
await app.viewRoomByName("Test Room");
@@ -73,63 +73,52 @@ test.describe("Invite dialog", function () {
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
});
test(
"should support inviting a user to Direct Messages",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => {
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
const other = page.locator(".mx_InviteDialog_other");
// Assert that the header is rendered
await expect(
other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"),
).toBeVisible();
const other = page.locator(".mx_InviteDialog_other");
// Assert that the header is rendered
await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible();
// Assert that the bar is rendered
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
// Assert that the bar is rendered
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
// Take a snapshot of the invite dialog
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png");
// Take a snapshot of the invite dialog
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png");
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
await expect(
other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
).toBeVisible();
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible();
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible();
await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible();
// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
// Open a direct message UI
await other.getByRole("button", { name: "Go" }).click();
// Open a direct message UI
await other.getByRole("button", { name: "Go" }).click();
// Assert that the invite dialog disappears
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
// Assert that the invite dialog disappears
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
// Assert that the hovered user name on invitation UI does not have background color
// TODO: implement the test on room-header.spec.ts
const roomHeader = page.locator(".mx_RoomHeader");
await roomHeader.locator(".mx_RoomHeader_heading").hover();
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS(
"background-color",
"rgba(0, 0, 0, 0)",
);
// Assert that the hovered user name on invitation UI does not have background color
// TODO: implement the test on room-header.spec.ts
const roomHeader = page.locator(".mx_RoomHeader");
await roomHeader.locator(".mx_RoomHeader_heading").hover();
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
// Send a message to invite the bots
const composer = app.getComposer().locator("[contenteditable]");
await composer.fill("Hello}");
await composer.press("Enter");
// Send a message to invite the bots
const composer = app.getComposer().locator("[contenteditable]");
await composer.fill("Hello}");
await composer.press("Enter");
// Assert that they were invited and joined
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
// Assert that they were invited and joined
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
// Assert that the message is displayed at the bottom
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
},
);
// Assert that the message is displayed at the bottom
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -4,7 +4,7 @@ Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -4,7 +4,7 @@ Copyright 2023 Mikhail Aheichyk
Copyright 2023 Nordeck IT + Consulting GmbH.
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -25,13 +25,12 @@ test.describe("Lazy Loading", () => {
});
});
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
test.beforeEach(async ({ page, homeserver, user, bot }) => {
for (let i = 1; i <= 10; i++) {
const displayName = `Charly #${i}`;
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
charlies.push(bot);
}
await app.client.network.setupRoute();
});
const name = "Lazy Loading Test";
@@ -78,7 +77,7 @@ test.describe("Lazy Loading", () => {
}
function getMemberInMemberlist(page: Page, name: string): Locator {
return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
return page.locator(".mx_MemberList .mx_EntityTile_name").filter({ hasText: name });
}
async function checkMemberList(page: Page, charlies: Bot[]) {

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,8 +10,7 @@ import { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
// Firefox headless lacks WebGL support https://bugzilla.mozilla.org/show_bug.cgi?id=1375585
test.describe("Location sharing", { tag: "@no-firefox" }, () => {
test.describe("Location sharing", () => {
const selectLocationShareTypeOption = (page: Page, shareType: string): Locator => {
return page.getByTestId(`share-location-option-${shareType}`);
};

View File

@@ -2,19 +2,18 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
test.use(consentHomeserver);
test.use({
displayName: "Bob",
});
test.describe("Consent", () => {
test.use({
startHomeserverOpts: "consent",
displayName: "Bob",
});
test("should prompt the user to consent to terms when server deems it necessary", async ({
context,
page,

View File

@@ -1,29 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { test } from "../../element-web-test";
import { doTokenRegistration } from "./utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts";
test.use(legacyOAuthHomeserver);
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
test.describe("SSO login", () => {
test.skip(isDendrite, "does not yet support SSO");
test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => {
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
// your firewall settings: Synapse is unable to reach the OIDC server.
//
// If you are using ufw, try something like:
// sudo ufw allow in on docker0
//
await doTokenRegistration(page, homeserver);
});
});

View File

@@ -2,18 +2,18 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Page } from "playwright-core";
import { expect, test } from "../../element-web-test";
import { doTokenRegistration } from "./utils";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { selectHomeserver } from "../utils";
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
// This test requires fixed credentials for the device signing keys below to work
const username = "user1234";
const password = "p4s5W0rD";
@@ -68,42 +68,26 @@ const DEVICE_SIGNING_KEYS_BODY = {
},
};
async function login(page: Page, homeserver: HomeserverInstance, credentials: Credentials) {
async function login(page: Page, homeserver: HomeserverInstance) {
await page.getByRole("link", { name: "Sign in" }).click();
await selectHomeserver(page, homeserver.baseUrl);
await selectHomeserver(page, homeserver.config.baseUrl);
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Sign in" }).click();
}
test.use(consentHomeserver);
test.use({
config: {
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
},
},
},
credentials: async ({ context, homeserver }, use) => {
const displayName = "Dave";
const credentials = await homeserver.registerUser(username, password, displayName);
console.log(`Registered test user @user:localhost with displayname ${displayName}`);
await use({
...credentials,
displayName,
});
},
});
test.describe("Login", () => {
test.describe("Password login", () => {
test.use({ startHomeserverOpts: "consent" });
let creds: Credentials;
test.beforeEach(async ({ homeserver }) => {
creds = await homeserver.registerUser(username, password);
});
test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
credentials,
page,
homeserver,
checkA11y,
@@ -117,7 +101,7 @@ test.describe("Login", () => {
await page.getByRole("link", { name: "Sign in" }).click();
// first pick the homeserver, as otherwise the user picker won't be visible
await selectHomeserver(page, homeserver.baseUrl);
await selectHomeserver(page, homeserver.config.baseUrl);
await page.getByRole("button", { name: "Edit" }).click();
@@ -130,23 +114,23 @@ test.describe("Login", () => {
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
// switch back to the custom homeserver
await selectHomeserver(page, homeserver.baseUrl);
await selectHomeserver(page, homeserver.config.baseUrl);
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 checkA11y();
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
await page.getByPlaceholder("Password").fill(credentials.password);
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL(/\/#\/home$/);
});
test("Follows the original link after login", async ({ page, homeserver, credentials }) => {
test("Follows the original link after login", async ({ page, homeserver }) => {
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
await login(page, homeserver, credentials);
await login(page, homeserver);
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
@@ -157,19 +141,18 @@ test.describe("Login", () => {
page,
homeserver,
request,
credentials,
}) => {
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
});
const res = await request.post(
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
}
expect(res.status() / 100).toEqual(2);
await page.goto("/");
await login(page, homeserver, credentials);
await login(page, homeserver);
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
@@ -187,14 +170,10 @@ test.describe("Login", () => {
page,
homeserver,
request,
credentials,
}) => {
const res = await request.post(
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
},
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
@@ -202,7 +181,7 @@ test.describe("Login", () => {
expect(res.status() / 100).toEqual(2);
await page.goto("/");
await login(page, homeserver, credentials);
await login(page, homeserver);
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
@@ -221,15 +200,11 @@ test.describe("Login", () => {
page,
homeserver,
request,
credentials,
}) => {
console.log(`uid ${credentials.userId} body`, DEVICE_SIGNING_KEYS_BODY);
console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY);
const res = await request.post(
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{
headers: { Authorization: `Bearer ${credentials.accessToken}` },
data: DEVICE_SIGNING_KEYS_BODY,
},
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
);
if (res.status() / 100 !== 2) {
console.log("Uploading dummy keys failed", await res.json());
@@ -237,9 +212,9 @@ test.describe("Login", () => {
expect(res.status() / 100).toEqual(2);
await page.goto("/");
await login(page, homeserver, credentials);
await login(page, homeserver);
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
await expect(h1).toBeVisible();
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
@@ -248,7 +223,32 @@ test.describe("Login", () => {
});
});
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
test.describe("SSO login", () => {
test.skip(isDendrite, "does not yet support SSO");
test.use({
startHomeserverOpts: ({ oAuthServer }, use) =>
use({
template: "default",
oAuthServerPort: oAuthServer.port,
}),
});
test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => {
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
// your firewall settings: Synapse is unable to reach the OIDC server.
//
// If you are using ufw, try something like:
// sudo ufw allow in on docker0
//
await doTokenRegistration(page, homeserver);
});
});
test.describe("logout", () => {
test.use({ startHomeserverOpts: "consent" });
test("should go to login page on logout", async ({ page, user }) => {
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
@@ -260,4 +260,29 @@ test.describe("Login", () => {
await expect(page).toHaveURL(/\/#\/login$/);
});
});
test.describe("logout with logout_redirect_url", () => {
test.use({
startHomeserverOpts: "consent",
config: {
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
// We could use example.org, matrix.org, or something else, however this puts dependency of external
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
// Using the decoder-ring is just as fine, and we can search for strategic names.
logout_redirect_url: "/decoder-ring/",
},
});
test("should respect logout_redirect_url", async ({ page, user }) => {
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
// give a change for the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/decoder-ring\/$/);
});
});
});

View File

@@ -1,35 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { expect, test } from "../../element-web-test";
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
test.use(consentHomeserver);
test.use({
config: {
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
// We could use example.org, matrix.org, or something else, however this puts dependency of external
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
// Using the decoder-ring is just as fine, and we can search for strategic names.
logout_redirect_url: "/decoder-ring/",
},
});
test.describe("logout with logout_redirect_url", () => {
test("should respect logout_redirect_url", async ({ page, user }) => {
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
// give a change for the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/decoder-ring\/$/);
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -13,7 +13,7 @@ test.describe("Overwrite login action", () => {
// This seems terminally flakey: https://github.com/element-hq/element-web/issues/27363
// I tried verious things to try & deflake it, to no avail: https://github.com/matrix-org/matrix-react-sdk/pull/12506
test.skip("Try replace existing login with new one", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
await logIntoElement(page, homeserver, credentials);
const userMenu = await app.openUserMenu();
await expect(userMenu.getByText(credentials.userId)).toBeVisible();
@@ -24,7 +24,7 @@ test.describe("Overwrite login action", () => {
expect(credentials.userId).not.toBe(bobRegister.userId);
const clientCredentials /* IMatrixClientCreds */ = {
homeserverUrl: homeserver.baseUrl,
homeserverUrl: homeserver.config.baseUrl,
...bobRegister,
};

View File

@@ -2,42 +2,121 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
import { interceptRequestsWithSoftLogout } from "./utils";
import { doTokenRegistration } from "./utils";
import { Credentials } from "../../plugins/homeserver";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.use({
displayName: "Alice",
config: {
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
test.describe("Soft logout", () => {
test.use({
displayName: "Alice",
startHomeserverOpts: ({ oAuthServer }, use) =>
use({
template: "default",
oAuthServerPort: oAuthServer.port,
}),
});
test.describe("with password user", () => {
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.getByPlaceholder("Password").fill(user.password);
await page.getByPlaceholder("Password").press("Enter");
// back to the welcome page
await expect(page).toHaveURL(/\/#\/home/);
await expect(page.getByRole("heading", { name: `Welcome ${user.userId}`, exact: true })).toBeVisible();
});
test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({
page,
user,
}) => {
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.reload();
await expect(page.getByText("You're signed out")).toBeVisible();
});
});
test.describe("with SSO user", () => {
test.skip(isDendrite, "does not yet support SSO");
test.use({
user: async ({ page, homeserver }, use) => {
const user = await doTokenRegistration(page, homeserver);
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
await use(user);
},
},
},
});
});
test.describe("Soft logout with password user", () => {
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.getByPlaceholder("Password").fill(user.password);
await page.getByPlaceholder("Password").press("Enter");
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
// back to the welcome page
await expect(page).toHaveURL(/\/#\/home/);
await expect(page.getByRole("heading", { name: "Now, let's help you get started", exact: true })).toBeVisible();
});
await interceptRequestsWithSoftLogout(page, user);
test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({ page, user }) => {
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.reload();
await expect(page.getByText("You're signed out")).toBeVisible();
await expect(page.getByText("You're signed out")).toBeVisible();
await page.getByRole("button", { name: "Continue with OAuth test" }).click();
// click the submit button
await page.getByRole("button", { name: "Submit" }).click();
// Synapse prompts us to grant permission to Element
await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible();
await page.getByRole("link", { name: "Continue" }).click();
// back to the welcome page
await expect(page).toHaveURL(/\/#\/home$/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
});
});
});
/**
* Intercept calls to /sync and have them fail with a soft-logout
*
* Any further requests to /sync with the same access token are blocked.
*/
async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise<void> {
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
const accessToken = await req.headerValue("Authorization");
// now, if the access token on this request matches the expired one, block it
if (accessToken === `Bearer ${user.accessToken}`) {
console.log("Intercepting request with soft-logged-out access token");
await route.fulfill({
status: 401,
json: {
errcode: "M_UNKNOWN_TOKEN",
error: "Soft logout",
soft_logout: true,
},
});
return;
}
// otherwise, pass through as normal
await route.continue();
});
const promise = page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401);
// do something to make the active /sync return: create a new room
await page.evaluate(() => {
// don't wait for this to complete: it probably won't, because of the broken sync
window.mxMatrixClientPeg.get().createRoom({});
});
await promise;
}

View File

@@ -1,59 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
import { doTokenRegistration, interceptRequestsWithSoftLogout } from "./utils";
import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts";
test.use({
displayName: "Alice",
config: {
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
// We point that to a guaranteed-invalid domain.
default_server_config: {
"m.homeserver": {
base_url: "https://server.invalid",
},
},
},
});
test.use(legacyOAuthHomeserver);
test.describe("Soft logout with SSO user", () => {
test.use({
user: async ({ page, homeserver }, use) => {
const user = await doTokenRegistration(page, homeserver);
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
await use(user);
},
});
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
await interceptRequestsWithSoftLogout(page, user);
await expect(page.getByText("You're signed out")).toBeVisible();
await page.getByRole("button", { name: "Continue with OAuth test" }).click();
// click the submit button
await page.getByRole("button", { name: "Submit" }).click();
// Synapse prompts us to grant permission to Element
await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible();
await page.getByRole("link", { name: "Continue" }).click();
// back to the welcome page
await expect(page).toHaveURL(/\/#\/home$/);
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -19,8 +19,8 @@ export async function doTokenRegistration(
await page.goto("/#/login");
await page.getByRole("button", { name: "Edit" }).click();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
await page.getByRole("button", { name: "Continue" }).click();
// wait for the dialog to go away
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
@@ -56,44 +56,5 @@ export async function doTokenRegistration(
homeServer: window.mxMatrixClientPeg.get().getHomeserverUrl(),
password: null,
displayName: "Alice",
username: window.mxMatrixClientPeg.get().getUserIdLocalpart(),
}));
}
/**
* Intercept calls to /sync and have them fail with a soft-logout
*
* Any further requests to /sync with the same access token are blocked.
*/
export async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise<void> {
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
const accessToken = await req.headerValue("Authorization");
// now, if the access token on this request matches the expired one, block it
if (accessToken === `Bearer ${user.accessToken}`) {
console.log("Intercepting request with soft-logged-out access token");
await route.fulfill({
status: 401,
json: {
errcode: "M_UNKNOWN_TOKEN",
error: "Soft logout",
soft_logout: true,
},
});
return;
}
// otherwise, pass through as normal
await route.continue();
});
const promise = page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401);
// do something to make the active /sync return: create a new room
await page.evaluate(() => {
// don't wait for this to complete: it probably won't, because of the broken sync
window.mxMatrixClientPeg.get().createRoom({});
});
await promise;
}

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -63,7 +63,7 @@ test.describe("Message rendering", () => {
{ direction: "ltr", displayName: "Quentin" },
{ direction: "rtl", displayName: "كوينتين" },
].forEach(({ direction, displayName }) => {
test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => {
test.describe(`with ${direction} display name`, () => {
test.use({
displayName,
room: async ({ user, app }, use) => {
@@ -72,18 +72,14 @@ test.describe("Message rendering", () => {
},
});
test(
"should render a basic LTR text message",
{ tag: "@screenshot" },
async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`);
test("should render a basic LTR text message", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`);
const msgTile = await sendMessage(page, "Hello, world!");
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
mask: [page.locator(".mx_MessageTimestamp")],
});
},
);
const msgTile = await sendMessage(page, "Hello, world!");
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
mask: [page.locator(".mx_MessageTimestamp")],
});
});
test("should render an LTR emote", async ({ page, user, app, room }) => {
await page.goto(`#/room/${room.roomId}`);

View File

@@ -2,14 +2,68 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { API, Messages } from "mailhog";
import { Page } from "@playwright/test";
import { expect } from "../../element-web-test";
import { test as base, expect } from "../../element-web-test";
import { MatrixAuthenticationService } from "../../plugins/matrix-authentication-service";
import { StartHomeserverOpts } from "../../plugins/homeserver";
export const test = base.extend<{
masPrepare: MatrixAuthenticationService;
mas: MatrixAuthenticationService;
}>({
// There's a bit of a chicken and egg problem between MAS & Synapse where they each need to know how to reach each other
// so spinning up a MAS is split into the prepare & start stage: prepare mas -> homeserver -> start mas to disentangle this.
masPrepare: async ({ context }, use) => {
const mas = new MatrixAuthenticationService(context);
await mas.prepare();
await use(mas);
},
mas: [
async ({ masPrepare: mas, homeserver, mailhog }, use, testInfo) => {
await mas.start(homeserver, mailhog.instance);
await use(mas);
await mas.stop(testInfo);
},
{ auto: true },
],
startHomeserverOpts: async ({ masPrepare }, use) => {
await use({
template: "mas-oidc",
variables: {
MAS_PORT: masPrepare.port,
},
});
},
config: async ({ homeserver, startHomeserverOpts, context }, use) => {
const issuer = `http://localhost:${(startHomeserverOpts as StartHomeserverOpts).variables["MAS_PORT"]}/`;
const wellKnown = {
"m.homeserver": {
base_url: homeserver.config.baseUrl,
},
"org.matrix.msc2965.authentication": {
issuer,
account: `${issuer}account`,
},
};
// Ensure org.matrix.msc2965.authentication is in well-known
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
await route.fulfill({ json: wellKnown });
});
await use({
default_server_config: wellKnown,
});
},
});
export { expect };
export async function registerAccountMas(
page: Page,

View File

@@ -0,0 +1,34 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("OIDC Aware", () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here
test("can register an account and manage it", async ({ context, page, homeserver, mailhog, app }) => {
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
await expect(page.getByRole("heading", { name: "Welcome alice", exact: true })).toBeVisible();
// Open settings and navigate to account management
await app.settings.openUserSettings("Account");
const newPagePromise = context.waitForEvent("page");
await page.getByRole("button", { name: "Manage account" }).click();
// Assert new tab opened
const newPage = await newPagePromise;
await expect(newPage.getByText("Primary email")).toBeVisible();
});
});

View File

@@ -2,30 +2,31 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test.ts";
import { registerAccountMas } from ".";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
import { test, expect, registerAccountMas } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.use(masHomeserver);
test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test.describe("OIDC Native", () => {
test.skip(isDendrite, "does not yet support MAS");
test.slow(); // trace recording takes a while here
test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhogClient, mas }) => {
const tokenUri = `${mas.baseUrl}/oauth2/token`;
test.use({
labsFlags: ["feature_oidc_native_flow"],
});
test("can register the oauth2 client and an account", async ({ context, page, homeserver, mailhog, mas }) => {
const tokenUri = `http://localhost:${mas.port}/oauth2/token`;
const tokenApiPromise = page.waitForRequest(
(request) => request.url() === tokenUri && request.postDataJSON()["grant_type"] === "authorization_code",
);
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await registerAccountMas(page, mailhogClient, "alice", "alice@email.com", "Pa$sW0rD!");
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
// Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
@@ -44,15 +45,15 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Assert MAS sees the session as OIDC Native
const newPage = await newPagePromise;
await newPage.getByText("Devices").click();
await newPage.getByText("Sessions").click();
await newPage.getByText(deviceId).click();
await expect(newPage.getByText("Element")).toBeVisible();
await expect(newPage.getByText("oauth2_session:")).toBeVisible();
await expect(newPage.getByText("http://localhost:8080/")).toBeVisible();
await expect(newPage).toHaveURL(/\/oauth2_session/);
await newPage.close();
// Assert logging out revokes both tokens
const revokeUri = `${mas.baseUrl}/oauth2/revoke`;
const revokeUri = `http://localhost:${mas.port}/oauth2/revoke`;
const revokeAccessTokenPromise = page.waitForRequest(
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token",
);

View File

@@ -3,7 +3,7 @@ Copyright 2024 New Vector Ltd.
Copyright 2023 Ahmad Kadri
Copyright 2023 Nordeck IT + Consulting GmbH.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -24,7 +24,7 @@ test.describe("permalinks", () => {
displayName: "Alice",
});
test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => {
test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => {
const bob = new Bot(page, homeserver, { displayName: "Bob" });
const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" });
await bob.prepareClient();

View File

@@ -2,7 +2,7 @@
* 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
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
@@ -13,8 +13,6 @@ import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot";
type RoomRef = { name: string; roomId: string };
/**
* Set up for pinned message tests.
*/
@@ -49,7 +47,7 @@ export class Helpers {
* @param room - the name of the room to send messages into
* @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf`
*/
async receiveMessages(room: RoomRef, messages: string[]) {
async receiveMessages(room: string | { name: string }, messages: string[]) {
await this.sendMessageAsClient(this.bot, room, messages);
}
@@ -57,8 +55,9 @@ export class Helpers {
* Use the supplied client to send messages or perform actions as specified by
* the supplied {@link Message} items.
*/
private async sendMessageAsClient(cli: Client, room: RoomRef, messages: string[]) {
const roomId = room.roomId;
private async sendMessageAsClient(cli: Client, roomName: string | { name: string }, messages: string[]) {
const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name);
const roomId = await room.evaluate((room) => room.roomId);
for (const message of messages) {
await cli.sendMessage(roomId, { body: message, msgtype: "m.text" });
@@ -74,11 +73,22 @@ export class Helpers {
}
}
/**
* Find a room by its name
* @param roomName
* @private
*/
private async findRoomByName(roomName: string) {
return this.app.client.evaluateHandle((cli, roomName) => {
return cli.getRooms().find((r) => r.name === roomName);
}, roomName);
}
/**
* Open the room with the supplied name.
*/
async goTo(room: RoomRef) {
await this.app.viewRoomByName(room.name);
async goTo(room: string | { name: string }) {
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
}
/**
@@ -119,7 +129,6 @@ export class Helpers {
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
await timelineMessage.click({ button: "right" });
await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click();
await this.assertMessageInBanner(message);
}
/**

View File

@@ -2,7 +2,7 @@
* 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
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
@@ -10,38 +10,35 @@ import { test } from "./index";
import { expect } from "../../element-web-test";
test.describe("Pinned messages", () => {
test(
"should show the empty state when there are no pinned messages",
{ tag: "@screenshot" },
async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.openRoomInfo();
await util.assertPinnedCountInRoomInfo(0);
await util.openPinnedMessagesList();
await util.assertEmptyPinnedMessagesList();
},
);
test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.openRoomInfo();
await util.assertPinnedCountInRoomInfo(0);
await util.openPinnedMessagesList();
await util.assertEmptyPinnedMessagesList();
});
test(
"should pin one message and to have the pinned message badge in the timeline",
{ tag: "@screenshot" },
async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]);
test("should pin one message and to have the pinned message badge in the timeline", async ({
page,
app,
room1,
util,
}) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]);
const tile = util.getEventTile("Msg1");
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
mask: [tile.locator(".mx_MessageTimestamp")],
// Hide the jump to bottom button in the timeline to avoid flakiness
css: `
const tile = util.getEventTile("Msg1");
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
mask: [tile.locator(".mx_MessageTimestamp")],
// Hide the jump to bottom button in the timeline to avoid flakiness
css: `
.mx_JumpToBottomButton {
display: none !important;
}
`,
});
},
);
});
});
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
await util.goTo(room1);
@@ -76,7 +73,7 @@ test.describe("Pinned messages", () => {
await util.assertPinnedCountInRoomInfo(2);
});
test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
test("should unpin all messages", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
@@ -101,7 +98,7 @@ test.describe("Pinned messages", () => {
await util.assertPinnedCountInRoomInfo(0);
});
test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
test("should display one message in the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1"]);
await util.pinMessages(["Msg1"]);
@@ -109,7 +106,7 @@ test.describe("Pinned messages", () => {
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png");
});
test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
test("should display 2 messages in the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
await util.pinMessages(["Msg1", "Msg2"]);
@@ -126,7 +123,7 @@ test.describe("Pinned messages", () => {
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png");
});
test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
test("should display 4 messages in the banner", async ({ page, app, room1, util }) => {
await util.goTo(room1);
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -93,7 +93,7 @@ test.describe("Polls", () => {
});
});
test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => {
test("should be creatable and votable", async ({ page, app, bot, user }) => {
const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId);
await page.goto("/#/room/" + roomId);
@@ -219,121 +219,107 @@ test.describe("Polls", () => {
await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
});
test(
"should be displayed correctly in thread panel",
{ tag: "@screenshot" },
async ({ page, app, user, bot, homeserver }) => {
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
await botCharlie.prepareClient();
test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => {
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
await botCharlie.prepareClient();
const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId);
await app.client.inviteUser(roomId, botCharlie.credentials.userId);
await page.goto("/#/room/" + roomId);
const roomId: string = await app.client.createRoom({});
await app.client.inviteUser(roomId, bot.credentials.userId);
await app.client.inviteUser(roomId, botCharlie.credentials.userId);
await page.goto("/#/room/" + roomId);
// wait until the bots joined
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({
timeout: 10000,
});
// wait until the bots joined
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 });
const locator = await app.openMessageComposerOptions();
await locator.getByRole("menuitem", { name: "Poll" }).click();
const locator = await app.openMessageComposerOptions();
await locator.getByRole("menuitem", { name: "Poll" }).click();
const pollParams = {
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"],
};
await createPoll(page, pollParams);
const pollParams = {
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"],
};
await createPoll(page, pollParams);
// Wait for message to send, get its ID and save as @pollId
const pollId = await page
.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
.filter({ hasText: pollParams.title })
.getAttribute("data-scroll-tokens");
// Wait for message to send, get its ID and save as @pollId
const pollId = await page
.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
.filter({ hasText: pollParams.title })
.getAttribute("data-scroll-tokens");
// Bob starts thread on the poll
await bot.sendMessage(
roomId,
{
body: "Hello there",
msgtype: "m.text",
},
pollId,
);
// Bob starts thread on the poll
await bot.sendMessage(
roomId,
{
body: "Hello there",
msgtype: "m.text",
},
pollId,
);
// open the thread summary
await page.getByRole("button", { name: "Open thread" }).click();
// open the thread summary
await page.getByRole("button", { name: "Open thread" }).click();
// Bob votes 'Maybe' in the poll
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
// Bob votes 'Maybe' in the poll
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
// Charlie votes 'No'
await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]);
// Charlie votes 'No'
await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]);
// no votes shown until I vote, check votes have arrived in main tl
await expect(
page
.locator(".mx_RoomView_body .mx_MPollBody_totalVotes")
.getByText("2 votes cast. Vote to see the results"),
).toBeAttached();
// no votes shown until I vote, check votes have arrived in main tl
await expect(
page
.locator(".mx_RoomView_body .mx_MPollBody_totalVotes")
.getByText("2 votes cast. Vote to see the results"),
).toBeAttached();
// and thread view
await expect(
page
.locator(".mx_ThreadView .mx_MPollBody_totalVotes")
.getByText("2 votes cast. Vote to see the results"),
).toBeAttached();
// and thread view
await expect(
page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"),
).toBeAttached();
// Take snapshots of poll on ThreadView
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_a_poll_on_bubble_layout.png",
{
mask: [page.locator(".mx_MessageTimestamp")],
},
);
// Take snapshots of poll on ThreadView
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_a_poll_on_group_layout.png",
{
mask: [page.locator(".mx_MessageTimestamp")],
},
);
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
const roomViewLocator = page.locator(".mx_RoomView_body");
// vote 'Maybe' in the main timeline poll
await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click();
// both me and bob have voted Maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator);
const roomViewLocator = page.locator(".mx_RoomView_body");
// vote 'Maybe' in the main timeline poll
await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click();
// both me and bob have voted Maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator);
const threadViewLocator = page.locator(".mx_ThreadView");
// votes updated in thread view too
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator);
// change my vote to 'Yes'
await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click();
const threadViewLocator = page.locator(".mx_ThreadView");
// votes updated in thread view too
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator);
// change my vote to 'Yes'
await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click();
// Bob updates vote to 'No'
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
// Bob updates vote to 'No'
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
// me: yes, bob: no, charlie: no
const expectVoteCounts = async (optLocator: Locator) => {
// I voted yes
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator);
// Bob and Charlie voted no
await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator);
// 0 for maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator);
};
// me: yes, bob: no, charlie: no
const expectVoteCounts = async (optLocator: Locator) => {
// I voted yes
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator);
// Bob and Charlie voted no
await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator);
// 0 for maybe
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator);
};
// check counts are correct in main timeline tile
await expectVoteCounts(page.locator(".mx_RoomView_body"));
// check counts are correct in main timeline tile
await expectVoteCounts(page.locator(".mx_RoomView_body"));
// and in thread view tile
await expectVoteCounts(page.locator(".mx_ThreadView"));
},
);
// and in thread view tile
await expectVoteCounts(page.locator(".mx_ThreadView"));
});
});

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("editing messages", () => {
test.describe("in threads", () => {
test("An edit of a threaded message makes the room unread", async ({

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("editing messages", () => {
test.describe("in the main timeline", () => {
test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("editing messages", () => {
test.describe("thread roots", () => {
test("An edit of a thread root leaves the room read", async ({

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,9 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { customEvent, many, test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.slow();
test.describe("Read receipts", () => {
test.describe("Ignored events", () => {
test("If all events after receipt are unimportant, the room is read", async ({
roomAlpha: room1,
@@ -122,7 +120,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.assertUnread(room2, 40);
// When I jump to a message in the middle and page up
await msg.jumpTo(room2, "x\ny\nz\nMsg0020");
await msg.jumpTo(room2.name, "x\ny\nz\nMsg0020");
await util.pageUp();
// Then the room is still unread

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -13,8 +13,6 @@ import { Bot } from "../../pages/bot";
import { Client } from "../../pages/client";
import { ElementAppPage } from "../../pages/ElementAppPage";
type RoomRef = { name: string; roomId: string };
/**
* Set up for a read receipt test:
* - Create a user with the supplied name
@@ -24,9 +22,9 @@ type RoomRef = { name: string; roomId: string };
*/
export const test = base.extend<{
roomAlphaName?: string;
roomAlpha: RoomRef;
roomAlpha: { name: string; roomId: string };
roomBetaName?: string;
roomBeta: RoomRef;
roomBeta: { name: string; roomId: string };
msg: MessageBuilder;
util: Helpers;
}>({
@@ -250,13 +248,12 @@ export class MessageBuilder {
/**
* Find and display a message.
*
* @param roomRef the ref of the room to look inside
* @param roomName the name of the room to look inside
* @param message the content of the message to fine
* @param includeThreads look for messages inside threads, not just the main timeline
*/
async jumpTo(roomRef: RoomRef, message: string, includeThreads = false) {
const room = await this.helpers.findRoomById(roomRef.roomId);
expect(room).toBeTruthy();
async jumpTo(roomName: string, message: string, includeThreads = false) {
const room = await this.helpers.findRoomByName(roomName);
const foundMessage = await this.getMessage(room, message, includeThreads);
const roomId = await room.evaluate((room) => room.roomId);
const foundMessageId = await foundMessage.evaluate((ev) => ev.getId());
@@ -336,10 +333,9 @@ class Helpers {
* Use the supplied client to send messages or perform actions as specified by
* the supplied {@link Message} items.
*/
async sendMessageAsClient(cli: Client, roomRef: RoomRef, messages: Message[]) {
const roomId = roomRef.roomId;
const room = await this.findRoomById(roomId);
expect(room).toBeTruthy();
async sendMessageAsClient(cli: Client, roomName: string | { name: string }, messages: Message[]) {
const room = await this.findRoomByName(typeof roomName === "string" ? roomName : roomName.name);
const roomId = await room.evaluate((room) => room.roomId);
for (const message of messages) {
if (typeof message === "string") {
@@ -363,7 +359,7 @@ class Helpers {
/**
* Open the room with the supplied name.
*/
async goTo(room: RoomRef) {
async goTo(room: string | { name: string }) {
await this.app.viewRoomByName(typeof room === "string" ? room : room.name);
}
@@ -427,16 +423,17 @@ class Helpers {
});
}
getRoomListTile(label: string) {
return this.page.getByRole("treeitem", { name: new RegExp("^" + label) });
getRoomListTile(room: string | { name: string }) {
const roomName = typeof room === "string" ? room : room.name;
return this.page.getByRole("treeitem", { name: new RegExp("^" + roomName) });
}
/**
* Click the "Mark as Read" context menu item on the room with the supplied name
* in the room list.
*/
async markAsRead(room: RoomRef) {
await this.getRoomListTile(room.name).click({ button: "right" });
async markAsRead(room: string | { name: string }) {
await this.getRoomListTile(room).click({ button: "right" });
await this.page.getByText("Mark as read").click();
}
@@ -444,8 +441,8 @@ class Helpers {
* Assert that the room with the supplied name is "read" in the room list - i.g.
* has not dot or count of unread messages.
*/
async assertRead(room: RoomRef) {
const tile = this.getRoomListTile(room.name);
async assertRead(room: string | { name: string }) {
const tile = this.getRoomListTile(room);
await expect(tile.locator(".mx_NotificationBadge_dot")).not.toBeVisible();
await expect(tile.locator(".mx_NotificationBadge_count")).not.toBeVisible();
}
@@ -455,7 +452,7 @@ class Helpers {
* (In practice, this just waits a short while to allow any unread marker to
* appear, and then asserts that the room is read.)
*/
async assertStillRead(room: RoomRef) {
async assertStillRead(room: string | { name: string }) {
await this.page.waitForTimeout(200);
await this.assertRead(room);
}
@@ -465,8 +462,8 @@ class Helpers {
* @param room - the name of the room to check
* @param count - the numeric count to assert, or if "." specified then a bold/dot (no count) state is asserted
*/
async assertUnread(room: RoomRef, count: number | ".") {
const tile = this.getRoomListTile(room.name);
async assertUnread(room: string | { name: string }, count: number | ".") {
const tile = this.getRoomListTile(room);
if (count === ".") {
await expect(tile.locator(".mx_NotificationBadge_dot")).toBeVisible();
} else {
@@ -481,8 +478,8 @@ class Helpers {
* @param room - the name of the room to check
* @param lessThan - the number of unread messages that is too many
*/
async assertUnreadLessThan(room: RoomRef, lessThan: number) {
const tile = this.getRoomListTile(room.name);
async assertUnreadLessThan(room: string | { name: string }, lessThan: number) {
const tile = this.getRoomListTile(room);
// https://playwright.dev/docs/test-assertions#expectpoll
// .toBeLessThan doesn't have a retry mechanism, so we use .poll
await expect
@@ -499,8 +496,8 @@ class Helpers {
* @param room - the name of the room to check
* @param greaterThan - the number of unread messages that is too few
*/
async assertUnreadGreaterThan(room: RoomRef, greaterThan: number) {
const tile = this.getRoomListTile(room.name);
async assertUnreadGreaterThan(room: string | { name: string }, greaterThan: number) {
const tile = this.getRoomListTile(room);
// https://playwright.dev/docs/test-assertions#expectpoll
// .toBeGreaterThan doesn't have a retry mechanism, so we use .poll
await expect
@@ -534,10 +531,10 @@ class Helpers {
});
}
async findRoomById(roomId: string): Promise<JSHandle<Room>> {
return this.app.client.evaluateHandle((cli, roomId) => {
return cli.getRooms().find((r) => r.roomId === roomId);
}, roomId);
async findRoomByName(roomName: string): Promise<JSHandle<Room>> {
return this.app.client.evaluateHandle((cli, roomName) => {
return cli.getRooms().find((r) => r.name === roomName);
}, roomName);
}
private async getThreadListTile(rootMessage: string) {
@@ -581,7 +578,7 @@ class Helpers {
* @param room - the name of the room to send messages into
* @param messages - the list of messages to send, these can be strings or implementations of MessageSpec like `editOf`
*/
async receiveMessages(room: RoomRef, messages: Message[]) {
async receiveMessages(room: string | { name: string }, messages: Message[]) {
await this.sendMessageAsClient(this.bot, room, messages);
}

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("Message ordering", () => {
test.describe("in the main timeline", () => {
test.fixme(

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("messages with missing referents", () => {
test.fixme(
"A message in an unknown thread is not visible and the room is read",

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { many, test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("new messages", () => {
test.describe("in threads", () => {
test("Receiving a message makes a room unread", async ({
@@ -101,7 +101,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.goTo(room1);
// When I read an older message in the thread
await msg.jumpTo(room2, "InThread0000", true);
await msg.jumpTo(room2.name, "InThread0000", true);
// Then the thread is still marked as unread
await util.backToThreadsList();

View File

@@ -2,7 +2,7 @@
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
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
@@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
import { many, test } from ".";
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
test.describe("Read receipts", () => {
test.describe("new messages", () => {
test.describe("in the main timeline", () => {
test("Receiving a message makes a room unread", async ({
@@ -59,7 +59,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.assertUnread(room2, 30);
// When I jump to one of the older messages
await msg.jumpTo(room2, "Msg0001");
await msg.jumpTo(room2.name, "Msg0001");
// Then the room is still unread, but some messages were read
await util.assertUnreadLessThan(room2, 30);

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