mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-05 01:10:40 +00:00
Compare commits
83 Commits
t3chguy/sp
...
t3chguy/en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3eddb736 | ||
|
|
72df9c9076 | ||
|
|
e42ee727b4 | ||
|
|
7d30413178 | ||
|
|
8884e77ce3 | ||
|
|
58f812ffe6 | ||
|
|
ef1597ff2d | ||
|
|
e5ca7954c8 | ||
|
|
13913ba8b2 | ||
|
|
03a1b48e1f | ||
|
|
ef3bf59656 | ||
|
|
7a3202b537 | ||
|
|
dbce48b23d | ||
|
|
bb41616d5f | ||
|
|
c75f6dc3a1 | ||
|
|
880048d998 | ||
|
|
24685dc7d1 | ||
|
|
60f70b93e0 | ||
|
|
2559cba482 | ||
|
|
5882b004f5 | ||
|
|
37f8d70d89 | ||
|
|
e2bd040c88 | ||
|
|
381b2ea343 | ||
|
|
41944e5c6e | ||
|
|
540580504d | ||
|
|
1a21b718d8 | ||
|
|
2cddb16a9f | ||
|
|
671d6de805 | ||
|
|
0f8a2e93ce | ||
|
|
bff2d680e6 | ||
|
|
5a5db19c2c | ||
|
|
11a8723c73 | ||
|
|
e14a3b64c3 | ||
|
|
f99d7ce2bb | ||
|
|
585aa75525 | ||
|
|
effef7eaa7 | ||
|
|
9826a8851d | ||
|
|
ebef0d353e | ||
|
|
f1899b9eb1 | ||
|
|
027891a35a | ||
|
|
2f7c28ded0 | ||
|
|
b6aba1477b | ||
|
|
056ecbb138 | ||
|
|
7685e547de | ||
|
|
a0a4211447 | ||
|
|
0ad4e13e2d | ||
|
|
f406305510 | ||
|
|
6cb174e3d9 | ||
|
|
c569478240 | ||
|
|
2bd8e049c7 | ||
|
|
e8d69dc592 | ||
|
|
50ac509a01 | ||
|
|
3e27a0019d | ||
|
|
5caad70191 | ||
|
|
6846679d34 | ||
|
|
7e5420100a | ||
|
|
f75d1f5a5e | ||
|
|
66bbb84e56 | ||
|
|
48152d2cd1 | ||
|
|
0d50e34763 | ||
|
|
f157c90ba9 | ||
|
|
cccb847d4e | ||
|
|
a5b739c45a | ||
|
|
9b1e165e6c | ||
|
|
0e2b16abf1 | ||
|
|
f6e999c87d | ||
|
|
3983bd5646 | ||
|
|
e8402f1657 | ||
|
|
69237e7df2 | ||
|
|
cf1c0805f1 | ||
|
|
b6a1aea825 | ||
|
|
b97005c182 | ||
|
|
9599c57a20 | ||
|
|
3eb3b936d9 | ||
|
|
b488155910 | ||
|
|
afc0dd5f86 | ||
|
|
7acadc29cc | ||
|
|
a73faffe37 | ||
|
|
1e758cacae | ||
|
|
7b565e7997 | ||
|
|
b16088d098 | ||
|
|
29624f7bcb | ||
|
|
d8f6c12c3d |
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org"],
|
||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
@@ -170,6 +170,8 @@ module.exports = {
|
||||
"jsx-a11y/role-supports-aria-props": "off",
|
||||
|
||||
"matrix-org/require-copyright-header": "error",
|
||||
|
||||
"react-compiler/react-compiler": "error",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -262,6 +264,7 @@ module.exports = {
|
||||
|
||||
// These are fine in tests
|
||||
"no-restricted-globals": "off",
|
||||
"react-compiler/react-compiler": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -271,6 +274,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": ["off"],
|
||||
"@typescript-eslint/no-floating-promises": ["error"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
20
.github/CODEOWNERS
vendored
20
.github/CODEOWNERS
vendored
@@ -3,17 +3,21 @@
|
||||
/package.json @element-hq/element-web-team
|
||||
/yarn.lock @element-hq/element-web-team
|
||||
|
||||
/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||
/src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
|
||||
# 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/plugins/homeserver/synapse/index.ts
|
||||
/playwright/testcontainers/synapse.ts
|
||||
|
||||
|
||||
12
.github/labels.yml
vendored
12
.github/labels.yml
vendored
@@ -210,6 +210,9 @@
|
||||
- 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"
|
||||
@@ -232,6 +235,15 @@
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Chrome"
|
||||
description: "Flaky playwright test in Chrome"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Firefox"
|
||||
description: "Flaky playwright test in Firefox"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Webkit"
|
||||
description: "Flaky playwright test in Webkit"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Jest-Test"
|
||||
description: "A Jest test is raising false alarms"
|
||||
color: "ededed"
|
||||
|
||||
42
.github/workflows/build.yml
vendored
42
.github/workflows/build.yml
vendored
@@ -5,6 +5,9 @@ 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
|
||||
@@ -24,10 +27,17 @@ jobs:
|
||||
- macos-14
|
||||
isDevelop:
|
||||
- ${{ github.event_name == 'push' && github.ref_name == 'develop' }}
|
||||
isPullRequest:
|
||||
- ${{ github.event_name == 'pull_request' }}
|
||||
# Skip the ubuntu-24.04 build for the develop branch as the dedicated CD build_develop workflow handles that
|
||||
# Skip the non-linux builds for pull requests as Windows is awfully slow, so run in merge queue only
|
||||
exclude:
|
||||
- isDevelop: true
|
||||
image: ubuntu-24.04
|
||||
- isPullRequest: true
|
||||
image: windows-2022
|
||||
- isPullRequest: true
|
||||
image: macos-14
|
||||
runs-on: ${{ matrix.image }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -37,14 +47,38 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
# Disable cache on Windows as it is slower than not caching
|
||||
# https://github.com/actions/setup-node/issues/975
|
||||
cache: ${{ runner.os != 'Windows' && 'yarn' || '' }}
|
||||
node-version: "lts/*"
|
||||
|
||||
# Workaround for yarn install timeouts, especially on Windows
|
||||
- run: yarn config set network-timeout 300000
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
- 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: Build
|
||||
run: "yarn 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
|
||||
|
||||
6
.github/workflows/dockerhub.yaml
vendored
6
.github/workflows/dockerhub.yaml
vendored
@@ -24,10 +24,10 @@ jobs:
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3
|
||||
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
|
||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
18
.github/workflows/end-to-end-tests.yaml
vendored
18
.github/workflows/end-to-end-tests.yaml
vendored
@@ -114,14 +114,20 @@ jobs:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- WebKit
|
||||
isCron:
|
||||
- ${{ github.event_name == 'schedule' }}
|
||||
# Skip the Firefox & Safari runs unless this was a cron trigger
|
||||
- Dendrite
|
||||
- Pinecone
|
||||
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:
|
||||
- isCron: false
|
||||
- runAllTests: false
|
||||
project: Firefox
|
||||
- isCron: false
|
||||
- runAllTests: false
|
||||
project: WebKit
|
||||
- runAllTests: false
|
||||
project: Dendrite
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -170,7 +176,7 @@ jobs:
|
||||
yarn playwright test \
|
||||
--shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
|
||||
--project="${{ matrix.project }}" \
|
||||
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
|
||||
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
|
||||
4
.github/workflows/netlify.yaml
vendored
4
.github/workflows/netlify.yaml
vendored
@@ -3,7 +3,7 @@
|
||||
name: Upload Preview Build to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["End to End Tests"]
|
||||
workflows: ["Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: webapp
|
||||
name: webapp-ubuntu-24.04
|
||||
path: webapp
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
docker pull "$IMAGE"
|
||||
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
||||
DIGEST=${INSPECT#*@}
|
||||
sed -i "s/const DOCKER_TAG.*/const DOCKER_TAG = \"develop@$DIGEST\";/" playwright/plugins/homeserver/synapse/index.ts
|
||||
sed -i "s/const TAG.*/const TAG = \"develop@$DIGEST\";/" playwright/testcontainers/synapse.ts
|
||||
env:
|
||||
IMAGE: ghcr.io/element-hq/synapse:develop
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/playwright-image-updates
|
||||
|
||||
6
.github/workflows/static_analysis.yaml
vendored
6
.github/workflows/static_analysis.yaml
vendored
@@ -132,9 +132,3 @@ jobs:
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/layered.sh"
|
||||
|
||||
- name: Dead Code Analysis
|
||||
run: "yarn run analyse:unused-exports"
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
||||
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: Close stale flaky issues
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
permissions: {}
|
||||
@@ -17,3 +18,4 @@ 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
|
||||
|
||||
2
.github/workflows/update-jitsi.yml
vendored
2
.github/workflows/update-jitsi.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
run: "yarn update:jitsi"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
||||
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Docker: run as non-root ([#28849](https://github.com/element-hq/element-web/pull/28849)). Contributed by @richvdh.
|
||||
* Docker: allow configuration of HTTP listen port via env var ([#28840](https://github.com/element-hq/element-web/pull/28840)). Contributed by @richvdh.
|
||||
* Update matrix-wysiwyg to consume WASM asset ([#28838](https://github.com/element-hq/element-web/pull/28838)). Contributed by @t3chguy.
|
||||
* OIDC settings tweaks ([#28787](https://github.com/element-hq/element-web/pull/28787)). Contributed by @t3chguy.
|
||||
* Delabs native OIDC support ([#28615](https://github.com/element-hq/element-web/pull/28615)). Contributed by @t3chguy.
|
||||
* Move room header info button to right-most position ([#28754](https://github.com/element-hq/element-web/pull/28754)). Contributed by @t3chguy.
|
||||
* Enable key backup by default ([#28691](https://github.com/element-hq/element-web/pull/28691)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix building the automations mermaid diagram ([#28881](https://github.com/element-hq/element-web/pull/28881)). Contributed by @dbkr.
|
||||
* Playwright: wait for the network listener on the postgres db ([#28808](https://github.com/element-hq/element-web/pull/28808)). Contributed by @dbkr.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -17,6 +17,7 @@ class MockMap extends EventEmitter {
|
||||
setCenter = jest.fn();
|
||||
setStyle = jest.fn();
|
||||
fitBounds = jest.fn();
|
||||
remove = jest.fn();
|
||||
}
|
||||
const MockMapInstance = new MockMap();
|
||||
|
||||
|
||||
@@ -23,21 +23,19 @@ 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 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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -61,29 +59,28 @@ Some tests are excluded from running on certain browsers due to incompatibilitie
|
||||
|
||||
## How the Tests Work
|
||||
|
||||
Everything Playwright-related lives in the `playwright/` subdirectory of react-sdk
|
||||
Everything Playwright-related lives in the `playwright/` subdirectory
|
||||
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
||||
|
||||
`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.
|
||||
`playwright/testcontainers` contains the testcontainers which start instances
|
||||
of Synapse/Dendrite. 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. `playwright/plugins/homeserver/synapse/templates`
|
||||
contains template configuration files for each different configuration.
|
||||
in different configurations. You can specify `synapseConfig` as such:
|
||||
|
||||
Each test suite can then launch whatever Synapse instances it needs in whatever
|
||||
configurations.
|
||||
```typescript
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
// The config options to pass to the Synapse instance
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
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.
|
||||
The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
|
||||
Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as
|
||||
they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||
We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||
The logs from testcontainers will be attached to any reports output from Playwright.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
@@ -113,25 +110,6 @@ 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).
|
||||
@@ -227,7 +205,13 @@ has to be disabled in Playwright on Firefox & Webkit to retain routing functiona
|
||||
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
|
||||
there at this time.
|
||||
|
||||
## Colima
|
||||
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.
|
||||
|
||||
13
knip.ts
13
knip.ts
@@ -10,13 +10,13 @@ export default {
|
||||
"playwright/**",
|
||||
"test/**",
|
||||
"res/decoder-ring/**",
|
||||
"res/jitsi_external_api.min.js",
|
||||
"docs/**",
|
||||
// Used by jest
|
||||
"__mocks__/maplibre-gl.js",
|
||||
],
|
||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||
ignore: [
|
||||
"docs/**",
|
||||
"res/jitsi_external_api.min.js",
|
||||
// Used by jest
|
||||
"__mocks__/maplibre-gl.js",
|
||||
// Keep for now
|
||||
"src/hooks/useLocalStorageState.ts",
|
||||
"src/components/views/elements/InfoTooltip.tsx",
|
||||
@@ -37,13 +37,8 @@ export default {
|
||||
// False positive
|
||||
"sw.js",
|
||||
// Used by webpack
|
||||
"buffer",
|
||||
"process",
|
||||
"util",
|
||||
// Used by workflows
|
||||
"ts-prune",
|
||||
// Required due to bug in bloom-filters https://github.com/Callidon/bloom-filters/issues/75
|
||||
"@types/seedrandom",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used in scripts & workflows
|
||||
|
||||
39
package.json
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.89",
|
||||
"version": "1.11.90",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -66,14 +66,15 @@
|
||||
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
||||
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
|
||||
"coverage": "yarn test --coverage",
|
||||
"analyse: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.30001684",
|
||||
"caniuse-lite": "1.0.30001690",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
@@ -88,7 +89,8 @@
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^8.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@vector-im/compound-design-tokens": "^2.0.1",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^2.1.0",
|
||||
"@vector-im/compound-web": "^7.5.0",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
@@ -122,7 +124,7 @@
|
||||
"linkify-string": "4.2.0",
|
||||
"linkifyjs": "4.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^4.0.0",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
@@ -135,16 +137,17 @@
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.157.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.10.1",
|
||||
"re-resizable": "6.10.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.13.1",
|
||||
"sanitize-html": "2.14.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.2.5",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
@@ -178,6 +181,7 @@
|
||||
"@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",
|
||||
@@ -189,7 +193,6 @@
|
||||
"@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",
|
||||
@@ -202,17 +205,17 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@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.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-loader": "^9.0.0",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
@@ -234,6 +237,7 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "^2.0.2",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -241,7 +245,6 @@
|
||||
"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",
|
||||
@@ -255,12 +258,12 @@
|
||||
"lint-staged": "^15.0.2",
|
||||
"mailhog": "^4.16.0",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimist": "^1.2.6",
|
||||
"modernizr": "^3.12.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"playwright-core": "^1.45.1",
|
||||
"postcss": "8.4.38",
|
||||
"postcss": "8.4.46",
|
||||
"postcss-easings": "^4.0.0",
|
||||
"postcss-hexrgba": "2.1.0",
|
||||
"postcss-import": "16.1.0",
|
||||
@@ -276,20 +279,22 @@
|
||||
"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",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^5.0.0",
|
||||
"webpack-cli": "^6.0.0",
|
||||
"webpack-dev-server": "^5.0.0",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
"webpack-version-file-plugin": "^0.5.0",
|
||||
"yaml": "^2.3.3"
|
||||
},
|
||||
|
||||
@@ -8,19 +8,25 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
import { Options } from "./playwright/services";
|
||||
|
||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||
|
||||
export default defineConfig({
|
||||
const chromeProject = {
|
||||
...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"],
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig<Options>({
|
||||
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"],
|
||||
},
|
||||
...chromeProject,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -48,6 +54,22 @@ export default defineConfig({
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
{
|
||||
name: "Dendrite",
|
||||
use: {
|
||||
...chromeProject,
|
||||
homeserverType: "dendrite",
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
{
|
||||
name: "Pinecone",
|
||||
use: {
|
||||
...chromeProject,
|
||||
homeserverType: "pinecone",
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
],
|
||||
use: {
|
||||
viewport: { width: 1280, height: 720 },
|
||||
|
||||
@@ -123,7 +123,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
|
||||
// Close the room
|
||||
page.goto("/#/home");
|
||||
await page.goto("/#/home");
|
||||
|
||||
// Pressing Control+F6 will first focus the space button
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
|
||||
@@ -13,13 +13,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { expect, test } from "../../element-web-test";
|
||||
|
||||
test.use({
|
||||
startHomeserverOpts: "guest-enabled",
|
||||
config: async ({ homeserver }, use) => {
|
||||
await use({
|
||||
default_server_config: {
|
||||
"m.homeserver": { base_url: homeserver.config.baseUrl },
|
||||
},
|
||||
});
|
||||
synapseConfig: {
|
||||
allow_guest_access: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -13,6 +13,14 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
// Find and click "Reply" button
|
||||
const clickButtonReply = async (tile: Locator) => {
|
||||
await expect(async () => {
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
}).toPass();
|
||||
};
|
||||
|
||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
@@ -222,8 +230,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
// 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();
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
@@ -251,19 +258,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
||||
await clickButtonReply();
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||
@@ -271,7 +271,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
||||
await clickButtonReply();
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with yet another audio file to create a reply chain
|
||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||
|
||||
@@ -95,7 +95,7 @@ test.describe("HTML Export", () => {
|
||||
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"));
|
||||
await 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++) {
|
||||
|
||||
@@ -79,9 +79,8 @@ 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 locator the spoiler command text
|
||||
await expect(page.locator("button.mx_EventTile_spoiler")).toBeVisible();
|
||||
await expect(page.getByText("this is the spoiler text")).toBeVisible();
|
||||
// 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,7 +165,7 @@ test.describe("Composer", () => {
|
||||
// Type another
|
||||
await page.locator("div[contenteditable=true]").pressSequentially("my message 1");
|
||||
// Send message
|
||||
page.locator("div[contenteditable=true]").press("Enter");
|
||||
await page.locator("div[contenteditable=true]").press("Enter");
|
||||
// It was sent
|
||||
await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe("Create Room", () => {
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/);
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
await expect(header).toContainText(name);
|
||||
});
|
||||
|
||||
95
playwright/e2e/crypto/backups-mas.spec.ts
Normal file
95
playwright/e2e/crypto/backups-mas.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 user and 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 }, testInfo) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "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 }, testInfo) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "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,
|
||||
}, testInfo) => {
|
||||
const testUsername = `alice_${testInfo.testId}`;
|
||||
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();
|
||||
|
||||
const accessToken = await page.evaluate(() => window.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();
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,6 @@ 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 { test as masTest, registerAccountMas } from "../oidc";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
@@ -20,33 +19,8 @@ async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
||||
}
|
||||
|
||||
masTest.describe("Encryption state after registration", () => {
|
||||
masTest.skip(isDendrite, "does not yet support MAS");
|
||||
|
||||
masTest("Key backup is enabled by default", async ({ page, mailhog, app }) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
expect(page.getByText("This session is backing up your keys.")).toBeVisible();
|
||||
});
|
||||
|
||||
masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailhog.api, "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("Backups", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
|
||||
@@ -8,8 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { logIntoElement } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Complete security", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Jeff",
|
||||
});
|
||||
@@ -19,9 +21,9 @@ test.describe("Complete security", () => {
|
||||
homeserver,
|
||||
credentials,
|
||||
}) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
// see also "Verify device during login with SAS" in `verifiction.spec.ts`.
|
||||
// see also "Verify device during login with SAS" in `verification.spec.ts`.
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { expect, test } from "../../element-web-test";
|
||||
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const checkDMRoom = async (page: Page) => {
|
||||
const body = page.locator(".mx_RoomView_body");
|
||||
@@ -67,6 +68,7 @@ const bobJoin = async (page: Page, bob: Bot) => {
|
||||
};
|
||||
|
||||
test.describe("Cryptography", function () {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
|
||||
@@ -28,6 +28,8 @@ test.describe("Cryptography", function () {
|
||||
});
|
||||
|
||||
test.describe("decryption failure messages", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
test("should handle device-relative historical messages", async ({
|
||||
homeserver,
|
||||
page,
|
||||
@@ -45,7 +47,7 @@ test.describe("Cryptography", function () {
|
||||
await logOutOfElement(page, true);
|
||||
|
||||
// Log in again, and see how the message looks.
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, 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 +64,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, homeserver, credentials);
|
||||
await logIntoElement(page, 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");
|
||||
|
||||
@@ -8,20 +8,28 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test as base, expect, Fixtures } from "../../element-web-test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const test = base.extend<Fixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
startHomeserverOpts: async ({}, use) => {
|
||||
await use("dehydration");
|
||||
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,
|
||||
synapseConfig: {
|
||||
experimental_features: {
|
||||
msc2697_enabled: false,
|
||||
msc3814_enabled: true,
|
||||
},
|
||||
},
|
||||
config: async ({ homeserver, context }, use) => {
|
||||
config: async ({ config, context }, use) => {
|
||||
const wellKnown = {
|
||||
"m.homeserver": {
|
||||
base_url: homeserver.config.baseUrl,
|
||||
},
|
||||
...config.default_server_config,
|
||||
"org.matrix.msc3814": true,
|
||||
};
|
||||
|
||||
@@ -29,26 +37,13 @@ const test = base.extend<Fixtures>({
|
||||
await route.fulfill({ json: wellKnown });
|
||||
});
|
||||
|
||||
await use({
|
||||
default_server_config: wellKnown,
|
||||
});
|
||||
await use(config);
|
||||
},
|
||||
});
|
||||
|
||||
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) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
@@ -93,7 +88,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_MemberList")).toBeVisible();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
awaitVerifier,
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
doTwoWaySasVerification,
|
||||
logIntoElement,
|
||||
waitForVerificationRequest,
|
||||
@@ -28,29 +29,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// Visit the login page of the app, to load the matrix sdk
|
||||
await page.goto("/#/login");
|
||||
|
||||
// wait for the page to load
|
||||
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
||||
|
||||
// Create a new device for alice
|
||||
aliceBotClient = new Bot(page, homeserver, {
|
||||
bootstrapCrossSigning: true,
|
||||
bootstrapSecretStorage: true,
|
||||
});
|
||||
aliceBotClient.setCredentials(credentials);
|
||||
|
||||
// Backup is prepared in the background. Poll until it is ready.
|
||||
const botClientHandle = await aliceBotClient.prepareClient();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
||||
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
||||
);
|
||||
return expectedBackupVersion;
|
||||
})
|
||||
.not.toBe(null);
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
aliceBotClient = res.botClient;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||
@@ -66,7 +47,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
}
|
||||
|
||||
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Launch the verification request between alice and the bot
|
||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||
@@ -93,7 +74,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, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Launch the verification request between alice and the bot
|
||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||
@@ -137,7 +118,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
});
|
||||
|
||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Select the security phrase
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||
@@ -158,7 +139,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
});
|
||||
|
||||
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Select the security phrase
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||
@@ -181,7 +162,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
});
|
||||
|
||||
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
/* Dismiss "Verify this device" */
|
||||
const authPage = page.locator(".mx_AuthPage");
|
||||
@@ -212,16 +193,17 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
/* on the bot side, wait for the verifier to exist ... */
|
||||
const verifier = await awaitVerifier(botVerificationRequest);
|
||||
// ... confirm ...
|
||||
botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify());
|
||||
void botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify());
|
||||
// ... and then check the emoji match
|
||||
await doTwoWaySasVerification(page, verifier);
|
||||
|
||||
/* And we're all done! */
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
await expect(
|
||||
infoDialog.getByText(`You've successfully verified (${aliceBotClient.credentials.deviceId})!`),
|
||||
).toBeVisible();
|
||||
// We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite
|
||||
await expect(infoDialog.getByText(`You've successfully verified`)).toContainText(
|
||||
`(${aliceBotClient.credentials.deviceId})`,
|
||||
);
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,9 @@ test.describe("Cryptography", function () {
|
||||
// Bob has a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
|
||||
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list
|
||||
await page.getByRole("button", { name: "Not now" }).click();
|
||||
|
||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "the bird is in the hand",
|
||||
@@ -207,7 +210,7 @@ test.describe("Cryptography", function () {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
await page.reload();
|
||||
await logIntoElement(page, homeserver, aliceCredentials, securityKey);
|
||||
await logIntoElement(page, aliceCredentials, securityKey);
|
||||
|
||||
/* go back to the test room and find Bob's message again */
|
||||
await app.viewRoomById(testRoomId);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||
|
||||
/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
|
||||
test.describe("Invisible cryptography", () => {
|
||||
test.slow();
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: { displayName: "Bob" },
|
||||
|
||||
@@ -8,10 +8,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Logout tests", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
});
|
||||
|
||||
test("Ask to set up recovery on logout if not setup", async ({ page, app }) => {
|
||||
|
||||
@@ -9,24 +9,24 @@ Please see LICENSE files in the repository root for full details.
|
||||
import path from "path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { expect, Fixtures, test as base } from "../../element-web-test";
|
||||
|
||||
const test = base.extend<Fixtures>({
|
||||
// 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);
|
||||
},
|
||||
});
|
||||
import { expect, test } from "../../element-web-test";
|
||||
|
||||
test.describe("migration", { tag: "@no-webkit" }, function () {
|
||||
test.use({ displayName: "Alice" });
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
|
||||
// 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);
|
||||
},
|
||||
});
|
||||
|
||||
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
|
||||
test.slow();
|
||||
|
||||
@@ -74,7 +74,7 @@ test.describe("User verification", () => {
|
||||
/* on the bot side, wait for the verifier to exist ... */
|
||||
const botVerifier = await awaitVerifier(bobVerificationRequest);
|
||||
// ... confirm ...
|
||||
botVerifier.evaluate((verifier) => verifier.verify());
|
||||
void botVerifier.evaluate((verifier) => verifier.verify());
|
||||
// ... and then check the emoji match
|
||||
await doTwoWaySasVerification(page, botVerifier);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type {
|
||||
CryptoEvent,
|
||||
EmojiMapping,
|
||||
GeneratedSecretStorageKey,
|
||||
ShowSasCallbacks,
|
||||
VerificationRequest,
|
||||
Verifier,
|
||||
@@ -22,6 +23,46 @@ import { Client } from "../../pages/client";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Bot } from "../../pages/bot";
|
||||
|
||||
/**
|
||||
* Create a bot client using the supplied credentials, and wait for the key backup to be ready.
|
||||
* @param page - the playwright `page` fixture
|
||||
* @param homeserver - the homeserver to use
|
||||
* @param credentials - the credentials to use for the bot client
|
||||
*/
|
||||
export async function createBot(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
credentials: Credentials,
|
||||
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
|
||||
// Visit the login page of the app, to load the matrix sdk
|
||||
await page.goto("/#/login");
|
||||
|
||||
// wait for the page to load
|
||||
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
||||
|
||||
// Create a new bot client
|
||||
const botClient = new Bot(page, homeserver, {
|
||||
bootstrapCrossSigning: true,
|
||||
bootstrapSecretStorage: true,
|
||||
});
|
||||
botClient.setCredentials(credentials);
|
||||
// Backup is prepared in the background. Poll until it is ready.
|
||||
const botClientHandle = await botClient.prepareClient();
|
||||
let expectedBackupVersion: string;
|
||||
await expect
|
||||
.poll(async () => {
|
||||
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
||||
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
||||
);
|
||||
return expectedBackupVersion;
|
||||
})
|
||||
.not.toBe(null);
|
||||
|
||||
const recoveryKey = await botClient.getRecoveryKey();
|
||||
|
||||
return { botClient, recoveryKey, expectedBackupVersion };
|
||||
}
|
||||
|
||||
/**
|
||||
* wait for the given client to receive an incoming verification request, and automatically accept it
|
||||
*
|
||||
@@ -59,7 +100,7 @@ export function handleSasVerification(verifier: JSHandle<Verifier>): Promise<Emo
|
||||
return new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ShowSasCallbacks) => {
|
||||
verifier.off("show_sas" as VerifierEvent, onShowSas);
|
||||
event.confirm();
|
||||
void event.confirm();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
@@ -138,22 +179,9 @@ export async function checkDeviceIsConnectedKeyBackup(
|
||||
*
|
||||
* If a `securityKey` is given, verifies the new device using the key.
|
||||
*/
|
||||
export async function logIntoElement(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
credentials: Credentials,
|
||||
securityKey?: string,
|
||||
) {
|
||||
export async function logIntoElement(page: Page, 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();
|
||||
@@ -326,7 +354,7 @@ export async function autoJoin(client: Client) {
|
||||
await client.evaluate((cli) => {
|
||||
cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||
if (member.membership === "invite" && member.userId === cli.getUserId()) {
|
||||
cli.joinRoom(member.roomId);
|
||||
void cli.joinRoom(member.roomId);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -385,3 +413,25 @@ export async function createSecondBotDevice(page: Page, homeserver: HomeserverIn
|
||||
await bobSecondDevice.prepareClient();
|
||||
return bobSecondDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cached secrets from the indexedDB
|
||||
* This is a workaround to simulate the case where the secrets are not cached.
|
||||
*/
|
||||
export async function deleteCachedSecrets(page: Page) {
|
||||
await page.evaluate(async () => {
|
||||
const removeCachedSecrets = new Promise((resolve) => {
|
||||
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
|
||||
request.onsuccess = (event: Event & { target: { result: IDBDatabase } }) => {
|
||||
const db = event.target.result;
|
||||
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
|
||||
request.onsuccess = () => {
|
||||
db.close();
|
||||
resolve(undefined);
|
||||
};
|
||||
};
|
||||
});
|
||||
await removeCachedSecrets;
|
||||
});
|
||||
await page.reload();
|
||||
}
|
||||
|
||||
39
playwright/e2e/csAPI.ts
Normal file
39
playwright/e2e/csAPI.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
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";
|
||||
import { ClientServerApi } from "../plugins/utils/api.ts";
|
||||
|
||||
/**
|
||||
* 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 extends ClientServerApi {
|
||||
public constructor(
|
||||
request: APIRequestContext,
|
||||
homeserver: HomeserverInstance,
|
||||
private accessToken: string,
|
||||
) {
|
||||
super(homeserver.baseUrl);
|
||||
this.setRequest(request);
|
||||
}
|
||||
|
||||
public async getCurrentBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||
return this.request("GET", `/v3/room_keys/version`, this.accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API directly to delete the given backup version
|
||||
* @param version The version to delete
|
||||
*/
|
||||
public async deleteBackupVersion(version: string): Promise<void> {
|
||||
await this.request("DELETE", `/v3/room_keys/version/${version}`, this.accessToken);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } fro
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
async function sendEvent(app: ElementAppPage, roomId: string): Promise<ISendEventResponse> {
|
||||
return app.client.sendEvent(roomId, null, "m.room.message" as EventType, {
|
||||
@@ -31,6 +32,8 @@ function mkPadding(n: number): IContent {
|
||||
}
|
||||
|
||||
test.describe("Editing", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||
|
||||
// Edit "Message"
|
||||
const editLastMessage = async (page: Page, edit: string) => {
|
||||
const eventTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
|
||||
|
||||
@@ -6,25 +6,40 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { expect, test as base } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
|
||||
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";
|
||||
|
||||
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
credentials: async ({}, use, testInfo) => {
|
||||
await use({
|
||||
username: `user_${testInfo.testId}`,
|
||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||
password: "oETo7MPf0o",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
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.use({
|
||||
startHomeserverOpts: ({ mailhog }, use) =>
|
||||
use({
|
||||
template: "email",
|
||||
variables: {
|
||||
SMTP_HOST: "host.containers.internal",
|
||||
SMTP_PORT: mailhog.instance.smtpPort,
|
||||
},
|
||||
}),
|
||||
});
|
||||
test.skip(isDendrite, "not yet wired up");
|
||||
|
||||
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||
await page.goto("/");
|
||||
@@ -32,38 +47,42 @@ test.describe("Forgot Password", () => {
|
||||
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.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.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 }) => {
|
||||
const user = await homeserver.registerUser(username, password);
|
||||
test(
|
||||
"renders email verification dialog properly",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, homeserver, credentials }) => {
|
||||
const user = await homeserver.registerUser(credentials.username, credentials.password);
|
||||
|
||||
await homeserver.setThreepid(user.userId, "email", email);
|
||||
await homeserver.setThreepid(user.userId, "email", email);
|
||||
|
||||
await page.goto("/");
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Email address" }).fill(email);
|
||||
await page.getByRole("textbox", { name: "Email address" }).fill(email);
|
||||
|
||||
await page.getByRole("button", { name: "Send email" }).click();
|
||||
await page.getByRole("button", { name: "Send email" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password);
|
||||
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password);
|
||||
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(credentials.password);
|
||||
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(credentials.password);
|
||||
|
||||
await page.getByRole("button", { name: "Reset password" }).click();
|
||||
await page.getByRole("button", { name: "Reset password" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
||||
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
||||
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
||||
});
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -69,29 +69,13 @@ async function sendActionFromIntegrationManager(
|
||||
await iframe.getByRole("button", { name: "Press to send action" }).click();
|
||||
}
|
||||
|
||||
async function clickUntilGone(page: Page, selector: string, attempt = 0) {
|
||||
if (attempt === 11) {
|
||||
throw new Error("clickUntilGone attempt count exceeded");
|
||||
}
|
||||
|
||||
await page.locator(selector).last().click();
|
||||
|
||||
const count = await page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
return clickUntilGone(page, selector, ++attempt);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectKickedMessage(page: Page, shouldExist: boolean) {
|
||||
// Expand any event summaries, we can't use a click multiple here because clicking one might de-render others
|
||||
// This is quite horrible but seems the most stable way of clicking 0-N buttons,
|
||||
// one at a time with a full re-evaluation after each click
|
||||
await clickUntilGone(page, ".mx_GenericEventListSummary_toggle[aria-expanded=false]");
|
||||
|
||||
// Check for the event message (or lack thereof)
|
||||
await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
|
||||
visible: shouldExist,
|
||||
});
|
||||
await expect(async () => {
|
||||
await page.locator(".mx_GenericEventListSummary_toggle[aria-expanded=false]").last().click();
|
||||
await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
|
||||
visible: shouldExist,
|
||||
});
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
test.describe("Integration Manager: Kick", () => {
|
||||
|
||||
@@ -9,8 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { waitForRoom } from "../utils";
|
||||
import { Filter } from "../../pages/Spotlight";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Create Knock Room", () => {
|
||||
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_ask_to_join"],
|
||||
@@ -79,6 +81,7 @@ test.describe("Create Knock Room", () => {
|
||||
|
||||
const spotlightDialog = await app.openSpotlight();
|
||||
await spotlightDialog.filter(Filter.PublicRooms);
|
||||
await spotlightDialog.search("Cyber");
|
||||
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,8 +13,10 @@ import { type Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { waitForRoom } from "../utils";
|
||||
import { Filter } from "../../pages/Spotlight";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Knock Into Room", () => {
|
||||
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_ask_to_join"],
|
||||
@@ -282,6 +284,7 @@ test.describe("Knock Into Room", () => {
|
||||
|
||||
const spotlightDialog = await app.openSpotlight();
|
||||
await spotlightDialog.filter(Filter.PublicRooms);
|
||||
await spotlightDialog.search("Cyber");
|
||||
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
||||
await spotlightDialog.results.nth(0).click();
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { waitForRoom } from "../utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Manage Knocks", () => {
|
||||
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_ask_to_join"],
|
||||
@@ -50,7 +52,7 @@ test.describe("Manage Knocks", () => {
|
||||
});
|
||||
|
||||
test("should deny knock using bar", async ({ page, app, bot, room }) => {
|
||||
bot.knockRoom(room.roomId);
|
||||
await bot.knockRoom(room.roomId);
|
||||
|
||||
const roomKnocksBar = page.locator(".mx_RoomKnocksBar");
|
||||
await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible();
|
||||
|
||||
@@ -10,8 +10,12 @@ import { Bot } from "../../pages/bot";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Lazy Loading", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||
|
||||
const charlies: Bot[] = [];
|
||||
|
||||
test.use({
|
||||
@@ -35,12 +39,18 @@ test.describe("Lazy Loading", () => {
|
||||
});
|
||||
|
||||
const name = "Lazy Loading Test";
|
||||
const alias = "#lltest:localhost";
|
||||
const charlyMsg1 = "hi bob!";
|
||||
const charlyMsg2 = "how's it going??";
|
||||
let roomId: string;
|
||||
|
||||
async function setupRoomWithBobAliceAndCharlies(page: Page, app: ElementAppPage, bob: Bot, charlies: Bot[]) {
|
||||
async function setupRoomWithBobAliceAndCharlies(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
user: Credentials,
|
||||
bob: Bot,
|
||||
charlies: Bot[],
|
||||
) {
|
||||
const alias = `#lltest:${user.homeServer}`;
|
||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
||||
roomId = await bob.createRoom({
|
||||
name,
|
||||
@@ -78,7 +88,7 @@ test.describe("Lazy Loading", () => {
|
||||
}
|
||||
|
||||
function getMemberInMemberlist(page: Page, name: string): Locator {
|
||||
return page.locator(".mx_MemberList .mx_EntityTile_name").filter({ hasText: name });
|
||||
return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
|
||||
}
|
||||
|
||||
async function checkMemberList(page: Page, charlies: Bot[]) {
|
||||
@@ -95,7 +105,13 @@ test.describe("Lazy Loading", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function joinCharliesWhileAliceIsOffline(page: Page, app: ElementAppPage, charlies: Bot[]) {
|
||||
async function joinCharliesWhileAliceIsOffline(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
user: Credentials,
|
||||
charlies: Bot[],
|
||||
) {
|
||||
const alias = `#lltest:${user.homeServer}`;
|
||||
await app.client.network.goOffline();
|
||||
for (const charly of charlies) {
|
||||
await charly.joinRoom(alias);
|
||||
@@ -107,19 +123,19 @@ test.describe("Lazy Loading", () => {
|
||||
await app.client.waitForNextSync();
|
||||
}
|
||||
|
||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot }) => {
|
||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot, user }) => {
|
||||
test.slow();
|
||||
const charly1to5 = charlies.slice(0, 5);
|
||||
const charly6to10 = charlies.slice(5);
|
||||
|
||||
// Set up room with alice, bob & charlies 1-5
|
||||
await setupRoomWithBobAliceAndCharlies(page, app, bot, charly1to5);
|
||||
await setupRoomWithBobAliceAndCharlies(page, app, user, bot, charly1to5);
|
||||
// Alice should see 2 messages from every charly with the correct display name
|
||||
await checkPaginatedDisplayNames(app, charly1to5);
|
||||
|
||||
await openMemberlist(app);
|
||||
await checkMemberList(page, charly1to5);
|
||||
await joinCharliesWhileAliceIsOffline(page, app, charly6to10);
|
||||
await joinCharliesWhileAliceIsOffline(page, app, user, charly6to10);
|
||||
await checkMemberList(page, charly6to10);
|
||||
|
||||
for (const charly of charlies) {
|
||||
|
||||
@@ -7,13 +7,14 @@ 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,
|
||||
|
||||
@@ -9,11 +9,12 @@ 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";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
// This test requires fixed credentials for the device signing keys below to work
|
||||
const username = "user1234";
|
||||
const password = "p4s5W0rD";
|
||||
|
||||
@@ -68,26 +69,55 @@ const DEVICE_SIGNING_KEYS_BODY = {
|
||||
},
|
||||
};
|
||||
|
||||
async function login(page: Page, homeserver: HomeserverInstance) {
|
||||
async function login(page: Page, homeserver: HomeserverInstance, credentials: Credentials) {
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
}
|
||||
|
||||
test.describe("Login", () => {
|
||||
test.describe("Password login", () => {
|
||||
test.use({ startHomeserverOpts: "consent" });
|
||||
// This test suite uses the same userId for all tests in the suite
|
||||
// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId,
|
||||
// so we restart the Synapse container to make it forget everything.
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
context: async ({ context, homeserver }, use) => {
|
||||
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||
await homeserver.restart();
|
||||
await use(context);
|
||||
},
|
||||
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}`);
|
||||
|
||||
let creds: Credentials;
|
||||
|
||||
test.beforeEach(async ({ homeserver }) => {
|
||||
creds = await homeserver.registerUser(username, password);
|
||||
await use({
|
||||
...credentials,
|
||||
displayName,
|
||||
});
|
||||
|
||||
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||
await homeserver.restart();
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("Login", () => {
|
||||
test.describe("Password login", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
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,
|
||||
@@ -101,7 +131,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.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("button", { name: "Edit" }).click();
|
||||
|
||||
@@ -114,23 +144,23 @@ test.describe("Login", () => {
|
||||
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
|
||||
|
||||
// switch back to the custom homeserver
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.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(username);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
});
|
||||
|
||||
test("Follows the original link after login", async ({ page, homeserver }) => {
|
||||
test("Follows the original link after login", async ({ page, homeserver, credentials }) => {
|
||||
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
||||
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
||||
@@ -141,18 +171,19 @@ test.describe("Login", () => {
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
credentials,
|
||||
}) => {
|
||||
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 },
|
||||
);
|
||||
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
|
||||
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||
data: DEVICE_SIGNING_KEYS_BODY,
|
||||
});
|
||||
if (res.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);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
|
||||
@@ -170,10 +201,14 @@ test.describe("Login", () => {
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
credentials,
|
||||
}) => {
|
||||
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 },
|
||||
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||
data: DEVICE_SIGNING_KEYS_BODY,
|
||||
},
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
@@ -181,7 +216,7 @@ test.describe("Login", () => {
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
|
||||
@@ -200,11 +235,15 @@ test.describe("Login", () => {
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
credentials,
|
||||
}) => {
|
||||
console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY);
|
||||
console.log(`uid ${credentials.userId} body`, 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 },
|
||||
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||
data: DEVICE_SIGNING_KEYS_BODY,
|
||||
},
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
@@ -212,9 +251,9 @@ test.describe("Login", () => {
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
|
||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||
@@ -223,32 +262,7 @@ 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,29 +274,4 @@ 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\/$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
29
playwright/e2e/login/login-sso.spec.ts
Normal file
29
playwright/e2e/login/login-sso.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
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 }, testInfo) => {
|
||||
// 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, testInfo);
|
||||
});
|
||||
});
|
||||
35
playwright/e2e/login/logout_redirect_url.spec.ts
Normal file
35
playwright/e2e/login/logout_redirect_url.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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\/$/);
|
||||
});
|
||||
});
|
||||
@@ -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, homeserver, credentials);
|
||||
await logIntoElement(page, 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.config.baseUrl,
|
||||
homeserverUrl: homeserver.baseUrl,
|
||||
...bobRegister,
|
||||
};
|
||||
|
||||
|
||||
@@ -6,117 +6,38 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { doTokenRegistration } from "./utils";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { interceptRequestsWithSoftLogout } from "./utils";
|
||||
|
||||
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.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("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();
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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");
|
||||
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");
|
||||
|
||||
// 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();
|
||||
// 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();
|
||||
});
|
||||
|
||||
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({});
|
||||
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 promise;
|
||||
}
|
||||
});
|
||||
|
||||
59
playwright/e2e/login/soft_logout_oauth.spec.ts
Normal file
59
playwright/e2e/login/soft_logout_oauth.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
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, testInfo) => {
|
||||
const user = await doTokenRegistration(page, homeserver, testInfo);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { Page, expect, TestInfo } from "@playwright/test";
|
||||
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
|
||||
@@ -15,12 +15,13 @@ import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
export async function doTokenRegistration(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
testInfo: TestInfo,
|
||||
): Promise<Credentials & { displayName: string }> {
|
||||
await page.goto("/#/login");
|
||||
|
||||
await page.getByRole("button", { name: "Edit" }).click();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
// wait for the dialog to go away
|
||||
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
|
||||
|
||||
@@ -35,7 +36,7 @@ export async function doTokenRegistration(
|
||||
|
||||
// Synapse prompts us to pick a user ID
|
||||
await expect(page.getByRole("heading", { name: "Create your account" })).toBeVisible();
|
||||
await page.getByRole("textbox", { name: "Username (required)" }).fill("alice");
|
||||
await page.getByRole("textbox", { name: "Username (required)" }).fill(`alice_${testInfo.testId}`);
|
||||
|
||||
// wait for username validation to start, and complete
|
||||
await expect(page.locator("#field-username-output")).toHaveText("");
|
||||
@@ -56,5 +57,44 @@ 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
|
||||
void window.mxMatrixClientPeg.get().createRoom({});
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,16 @@ async function editMessage(page: Page, message: Locator, newMsg: string): Promis
|
||||
await editComposer.press("Enter");
|
||||
}
|
||||
|
||||
const screenshotOptions = (page?: Page) => ({
|
||||
mask: page ? [page.locator(".mx_MessageTimestamp")] : undefined,
|
||||
// Hide the jump to bottom button in the timeline to avoid flakiness
|
||||
css: `
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
test.describe("Message rendering", () => {
|
||||
[
|
||||
{ direction: "ltr", displayName: "Quentin" },
|
||||
@@ -79,9 +89,10 @@ test.describe("Message rendering", () => {
|
||||
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")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`basic-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -89,14 +100,17 @@ test.describe("Message rendering", () => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me lays an egg");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`, screenshotOptions());
|
||||
});
|
||||
|
||||
test("should render an LTR rich text emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me lays a *free range* egg");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rich-ltr-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`emote-rich-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render an edited LTR message", async ({ page, user, app, room }) => {
|
||||
@@ -106,9 +120,10 @@ test.describe("Message rendering", () => {
|
||||
|
||||
await editMessage(page, msgTile, "Hello, universe!");
|
||||
|
||||
await expect(msgTile).toMatchScreenshot(`edited-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`edited-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a reply of a LTR message", async ({ page, user, app, room }) => {
|
||||
@@ -122,32 +137,37 @@ test.describe("Message rendering", () => {
|
||||
]);
|
||||
|
||||
await replyMessage(page, msgTile, "response to multiline message");
|
||||
await expect(msgTile).toMatchScreenshot(`reply-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`reply-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a basic RTL text message", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "مرحبا بالعالم!");
|
||||
await expect(msgTile).toMatchScreenshot(`basic-message-rtl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`basic-message-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render an RTL emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me يضع بيضة");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`, screenshotOptions());
|
||||
});
|
||||
|
||||
test("should render a richtext RTL emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me أضع بيضة *حرة النطاق*");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rich-rtl-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`emote-rich-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render an edited RTL message", async ({ page, user, app, room }) => {
|
||||
@@ -157,9 +177,10 @@ test.describe("Message rendering", () => {
|
||||
|
||||
await editMessage(page, msgTile, "مرحبا بالكون!");
|
||||
|
||||
await expect(msgTile).toMatchScreenshot(`edited-message-rtl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`edited-message-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a reply of a RTL message", async ({ page, user, app, room }) => {
|
||||
@@ -173,9 +194,10 @@ test.describe("Message rendering", () => {
|
||||
]);
|
||||
|
||||
await replyMessage(page, msgTile, "مرحبا بالعالم!");
|
||||
await expect(msgTile).toMatchScreenshot(`reply-message-trl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`reply-message-trl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,61 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { API, Messages } from "mailhog";
|
||||
import { Page } from "@playwright/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 };
|
||||
import { expect } from "../../element-web-test";
|
||||
|
||||
export async function registerAccountMas(
|
||||
page: Page,
|
||||
@@ -87,7 +33,7 @@ export async function registerAccountMas(
|
||||
expect(messages.items).toHaveLength(1);
|
||||
}).toPass();
|
||||
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
|
||||
const [code] = messages.items[0].text.match(/(\d{6})/);
|
||||
const [, code] = messages.items[0].text.match(/Your verification code to confirm this email address is: (\d{6})/);
|
||||
|
||||
await page.getByRole("textbox", { name: "6-digit code" }).fill(code);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
@@ -6,27 +6,39 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect, registerAccountMas } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { test, expect } from "../../element-web-test.ts";
|
||||
import { registerAccountMas } from ".";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
|
||||
|
||||
test.use(masHomeserver);
|
||||
test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
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, mailhog, mas }) => {
|
||||
const tokenUri = `http://localhost:${mas.port}/oauth2/token`;
|
||||
test("can register the oauth2 client and an account", async ({
|
||||
context,
|
||||
page,
|
||||
homeserver,
|
||||
mailhogClient,
|
||||
mas,
|
||||
}, testInfo) => {
|
||||
await page.clock.install();
|
||||
|
||||
const tokenUri = `${mas.baseUrl}/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, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
const userId = `alice_${testInfo.testId}`;
|
||||
await registerAccountMas(page, mailhogClient, userId, "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();
|
||||
await expect(page.getByRole("heading", { name: `Welcome ${userId}`, exact: true })).toBeVisible();
|
||||
await page.clock.runFor(20000); // run the timer so we see the token request
|
||||
|
||||
const tokenApiRequest = await tokenApiPromise;
|
||||
expect(tokenApiRequest.postDataJSON()["grant_type"]).toBe("authorization_code");
|
||||
@@ -41,15 +53,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("Sessions").click();
|
||||
await newPage.getByText("Devices").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 = `http://localhost:${mas.port}/oauth2/revoke`;
|
||||
const revokeUri = `${mas.baseUrl}/oauth2/revoke`;
|
||||
const revokeAccessTokenPromise = page.waitForRequest(
|
||||
(request) => request.url() === revokeUri && request.postDataJSON()["token_type_hint"] === "access_token",
|
||||
);
|
||||
|
||||
@@ -9,16 +9,19 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const test = base.extend<{
|
||||
user2?: Credentials;
|
||||
}>({});
|
||||
|
||||
test.describe("1:1 chat room", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3492");
|
||||
|
||||
test.use({
|
||||
displayName: "Jeff",
|
||||
user2: async ({ homeserver }, use) => {
|
||||
const credentials = await homeserver.registerUser("user1234", "p4s5W0rD", "Timmy");
|
||||
user2: async ({ homeserver }, use, testInfo) => {
|
||||
const credentials = await homeserver.registerUser(`user2_${testInfo.testId}`, "p4s5W0rD", "Timmy");
|
||||
await use(credentials);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe("permalinks", () => {
|
||||
await charlotte.prepareClient();
|
||||
|
||||
// We don't use a bot for danielle as we want a stable MXID.
|
||||
const danielleId = "@danielle:localhost";
|
||||
const danielleId = `@danielle:${user.homeServer}`;
|
||||
|
||||
const room1Id = await app.client.createRoom({ name: room1Name });
|
||||
const room2Id = await app.client.createRoom({ name: room2Name });
|
||||
|
||||
@@ -35,10 +35,10 @@ test.describe("Pinned messages", () => {
|
||||
mask: [tile.locator(".mx_MessageTimestamp")],
|
||||
// Hide the jump to bottom button in the timeline to avoid flakiness
|
||||
css: `
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -134,7 +134,7 @@ test.describe("Poll history", () => {
|
||||
|
||||
await expect(dialog.getByText(pollParams2.title)).toBeAttached();
|
||||
await expect(dialog.getByText(pollParams1.title)).toBeAttached();
|
||||
dialog.getByText("Active polls").click();
|
||||
await dialog.getByText("Active polls").click();
|
||||
|
||||
// no more active polls
|
||||
await expect(page.getByText("There are no active polls in this room")).toBeAttached();
|
||||
|
||||
@@ -11,8 +11,11 @@ import { Bot } from "../../pages/bot";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Polls", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3492");
|
||||
|
||||
type CreatePollOptions = {
|
||||
title: string;
|
||||
options: string[];
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("editing messages", () => {
|
||||
test.describe("in threads", () => {
|
||||
test("An edit of a threaded message makes the room unread", async ({
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
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 }) => {
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("editing messages", () => {
|
||||
test.describe("thread roots", () => {
|
||||
test("An edit of a thread root leaves the room read", async ({
|
||||
|
||||
@@ -9,8 +9,12 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { customEvent, many, test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
test.slow();
|
||||
|
||||
test.describe("Ignored events", () => {
|
||||
test("If all events after receipt are unimportant, the room is read", async ({
|
||||
roomAlpha: room1,
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { many, test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("new messages", () => {
|
||||
test.describe("in threads", () => {
|
||||
test("Receiving a message makes a room unread", async ({
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { many, test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("new messages", () => {
|
||||
test.describe("in the main timeline", () => {
|
||||
test("Receiving a message makes a room unread", async ({
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { many, test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("new messages", () => {
|
||||
test.describe("thread roots", () => {
|
||||
test("Reading a thread root does not mark the thread as read", async ({
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test, expect } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("reactions", () => {
|
||||
test.describe("in threads", () => {
|
||||
test("A reaction to a threaded message does not make the room unread", async ({
|
||||
@@ -70,11 +73,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
// Given a thread exists and I have marked it as read
|
||||
await util.goTo(room1);
|
||||
await util.assertRead(room2);
|
||||
await util.receiveMessages(room2, [
|
||||
"Msg1",
|
||||
msg.threadedOff("Msg1", "Reply1"),
|
||||
msg.reactionTo("Reply1", "🪿"),
|
||||
]);
|
||||
await util.receiveMessages(room2, ["Msg1", msg.threadedOff("Msg1", "Reply1")]);
|
||||
await util.assertUnread(room2, 1);
|
||||
await util.markAsRead(room2);
|
||||
await util.assertRead(room2);
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("reactions", () => {
|
||||
test.describe("in the main timeline", () => {
|
||||
test("Receiving a reaction to a message does not make a room unread", async ({
|
||||
|
||||
@@ -9,8 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
test.describe("reactions", () => {
|
||||
test.describe("thread roots", () => {
|
||||
test("A reaction to a thread root does not make the room unread", async ({
|
||||
|
||||
@@ -12,8 +12,10 @@ import { expect } from "../../element-web-test";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
test.use({
|
||||
displayName: "Mae",
|
||||
botCreateOpts: { displayName: "Other User" },
|
||||
@@ -100,12 +102,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
await page.goto(`/#/room/${selectedRoomId}`);
|
||||
});
|
||||
|
||||
// Disabled due to flakiness: https://github.com/element-hq/element-web/issues/26895
|
||||
test.skip("With sync accumulator, considers main thread and unthreaded receipts #24629", async ({
|
||||
page,
|
||||
app,
|
||||
bot,
|
||||
}) => {
|
||||
test("With sync accumulator, considers main thread and unthreaded receipts #24629", async ({ page, app, bot }) => {
|
||||
// Details are in https://github.com/vector-im/element-web/issues/24629
|
||||
// This proves we've fixed one of the "stuck unreads" issues.
|
||||
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("redactions", () => {
|
||||
test.describe("in threads", () => {
|
||||
test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("redactions", () => {
|
||||
test.describe("in the main timeline", () => {
|
||||
test("Redacting the message pointed to by my receipt leaves the room read", async ({
|
||||
|
||||
@@ -9,8 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { test } from ".";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.describe("redactions", () => {
|
||||
test.describe("thread roots", () => {
|
||||
test("Redacting a thread root after it was read leaves the room read", async ({
|
||||
|
||||
@@ -7,41 +7,34 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.use(emailHomeserver);
|
||||
test.use({
|
||||
config: ({ config }, use) =>
|
||||
use({
|
||||
...config,
|
||||
default_server_config: {
|
||||
...config.default_server_config,
|
||||
"m.identity_server": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
test.describe("Email Registration", async () => {
|
||||
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,
|
||||
},
|
||||
}),
|
||||
config: ({ homeserver }, use) =>
|
||||
use({
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: homeserver.config.baseUrl,
|
||||
},
|
||||
"m.identity_server": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.beforeEach(async ({ homeserver, page }) => {
|
||||
await page.goto("/#/register");
|
||||
});
|
||||
|
||||
test(
|
||||
"registers an account and lands on the use case selection screen",
|
||||
"registers an account and lands on the home page",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, mailhog, request, checkA11y }) => {
|
||||
async ({ page, mailhogClient, request, checkA11y }) => {
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
||||
@@ -58,13 +51,13 @@ test.describe("Email Registration", async () => {
|
||||
|
||||
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
||||
|
||||
const messages = await mailhog.api.messages();
|
||||
const messages = await mailhogClient.messages();
|
||||
expect(messages.items).toHaveLength(1);
|
||||
expect(messages.items[0].to).toEqual("alice@email.com");
|
||||
const [emailLink] = messages.items[0].text.match(/http.+/);
|
||||
await request.get(emailLink); // "Click" the link in the email
|
||||
|
||||
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
||||
await expect(page.getByText("Welcome alice")).toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,11 +7,24 @@ 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";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("Registration", () => {
|
||||
test.use({
|
||||
startHomeserverOpts: "consent",
|
||||
});
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/#/register");
|
||||
@@ -27,7 +40,7 @@ test.describe("Registration", () => {
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
|
||||
await checkA11y();
|
||||
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
// wait for the dialog to go away
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
@@ -61,12 +74,6 @@ test.describe("Registration", () => {
|
||||
await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Accept", exact: true }).click();
|
||||
|
||||
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions);
|
||||
await checkA11y();
|
||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
|
||||
/*
|
||||
@@ -88,7 +95,7 @@ test.describe("Registration", () => {
|
||||
test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
|
||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
// wait for the dialog to go away
|
||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Download, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
@@ -181,6 +182,8 @@ test.describe("FilePanel", () => {
|
||||
});
|
||||
|
||||
test.describe("download", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite sending Content-Disposition inline");
|
||||
|
||||
test("should download an image via the link on the panel", async ({ page, context }) => {
|
||||
// Upload an image file
|
||||
await uploadFile(page, "playwright/sample-files/riot.png");
|
||||
|
||||
48
playwright/e2e/right-panel/memberlist.spec.ts
Normal file
48
playwright/e2e/right-panel/memberlist.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { Bot } from "../../pages/bot";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
presence: {
|
||||
enabled: false,
|
||||
include_offline_users_on_sync: false,
|
||||
},
|
||||
},
|
||||
displayName: NAME,
|
||||
disablePresence: true,
|
||||
});
|
||||
|
||||
test.describe("Memberlist", () => {
|
||||
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
||||
testInfo.setTimeout(testInfo.timeout + 30_000);
|
||||
const id = await app.client.createRoom({ name: ROOM_NAME });
|
||||
const newBots: Bot[] = [];
|
||||
const names = ["Bob", "Bob", "Susan"];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const displayName = names[i];
|
||||
const autoAcceptInvites = displayName !== "Susan";
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites });
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(id, bot.credentials?.userId);
|
||||
newBots.push(bot);
|
||||
}
|
||||
});
|
||||
|
||||
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.viewRoomByName(ROOM_NAME);
|
||||
const memberlist = await app.toggleMemberlistPanel();
|
||||
await expect(memberlist.locator(".mx_MemberTileView")).toHaveCount(4);
|
||||
await expect(memberlist.getByText("(Invited)")).toHaveCount(1);
|
||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,7 @@ const ROOM_ADDRESS_LONG =
|
||||
"loremIpsumDolorSitAmetConsecteturAdipisicingElitSedDoEiusmodTemporIncididuntUtLaboreEtDoloreMagnaAliqua";
|
||||
|
||||
function getMemberTileByName(page: Page, name: string): Locator {
|
||||
return page.locator(`.mx_EntityTile, [title="${name}"]`);
|
||||
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
|
||||
}
|
||||
|
||||
test.describe("RightPanel", () => {
|
||||
@@ -38,29 +38,34 @@ test.describe("RightPanel", () => {
|
||||
});
|
||||
|
||||
test.describe("in rooms", () => {
|
||||
test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.client.createRoom({ name: ROOM_NAME_LONG });
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);
|
||||
test(
|
||||
"should handle long room address and long room name",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user }) => {
|
||||
await app.client.createRoom({ name: ROOM_NAME_LONG });
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);
|
||||
|
||||
await app.settings.openRoomSettings();
|
||||
await app.settings.openRoomSettings();
|
||||
|
||||
// Set a local room address
|
||||
const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
|
||||
await localAddresses.getByRole("textbox").fill(ROOM_ADDRESS_LONG);
|
||||
await localAddresses.getByRole("button", { name: "Add" }).click();
|
||||
await expect(localAddresses.getByText(`#${ROOM_ADDRESS_LONG}:localhost`)).toHaveClass(
|
||||
"mx_EditableItem_item",
|
||||
);
|
||||
// Set a local room address
|
||||
const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
|
||||
await localAddresses.getByRole("textbox").fill(ROOM_ADDRESS_LONG);
|
||||
await expect(page.getByText("This address is available to use")).toBeVisible();
|
||||
await localAddresses.getByRole("button", { name: "Add" }).click();
|
||||
await expect(localAddresses.getByText(`#${ROOM_ADDRESS_LONG}:${user.homeServer}`)).toHaveClass(
|
||||
"mx_EditableItem_item",
|
||||
);
|
||||
|
||||
await app.closeDialog();
|
||||
await app.closeDialog();
|
||||
|
||||
// Close and reopen the right panel to render the room address
|
||||
await app.toggleRoomInfoPanel();
|
||||
await expect(page.locator(".mx_RightPanel")).not.toBeVisible();
|
||||
await app.toggleRoomInfoPanel();
|
||||
// Close and reopen the right panel to render the room address
|
||||
await app.toggleRoomInfoPanel();
|
||||
await expect(page.locator(".mx_RightPanel")).not.toBeVisible();
|
||||
await app.toggleRoomInfoPanel();
|
||||
|
||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-name-and-address.png");
|
||||
});
|
||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-name-and-address.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should handle clicking add widgets", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
@@ -107,14 +112,14 @@ test.describe("RightPanel", () => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
await expect(page.locator(".mx_UserInfo")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_profile").getByText(NAME)).toBeVisible();
|
||||
|
||||
await page.getByTestId("base-card-back-button").click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
|
||||
await page.getByLabel("Room info").nth(1).click();
|
||||
await checkRoomSummaryCard(page, ROOM_NAME);
|
||||
@@ -130,14 +135,14 @@ test.describe("RightPanel", () => {
|
||||
.locator(".mx_RoomInfoLine_private")
|
||||
.getByRole("button", { name: /\d member/ })
|
||||
.click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
await expect(page.locator(".mx_UserInfo")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_profile").getByText(NAME)).toBeVisible();
|
||||
|
||||
await page.getByTestId("base-card-back-button").click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Preset, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Room Directory", () => {
|
||||
test.skip(({ homeserverType }) => homeserverType === "pinecone", "Pinecone's /publicRooms API takes forever");
|
||||
test.use({
|
||||
displayName: "Ray",
|
||||
botCreateOpts: { displayName: "Paul" },
|
||||
@@ -30,15 +31,16 @@ test.describe("Room Directory", () => {
|
||||
// First add a local address `gaming`
|
||||
const localAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Local Addresses" });
|
||||
await localAddresses.getByRole("textbox").fill("gaming");
|
||||
await expect(page.getByText("This address is available to use")).toBeVisible();
|
||||
await localAddresses.getByRole("button", { name: "Add" }).click();
|
||||
await expect(localAddresses.getByText("#gaming:localhost")).toHaveClass("mx_EditableItem_item");
|
||||
await expect(localAddresses.getByText(`#gaming:${user.homeServer}`)).toHaveClass("mx_EditableItem_item");
|
||||
|
||||
// Publish into the public rooms directory
|
||||
const publishedAddresses = page.locator(".mx_SettingsFieldset", { hasText: "Published Addresses" });
|
||||
await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost");
|
||||
await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue(`#gaming:${user.homeServer}`);
|
||||
const checkbox = publishedAddresses
|
||||
.locator(".mx_SettingsFlag", {
|
||||
hasText: "Publish this room to the public in localhost's room directory?",
|
||||
hasText: `Publish this room to the public in ${user.homeServer}'s room directory?`,
|
||||
})
|
||||
.getByRole("switch");
|
||||
await checkbox.check();
|
||||
@@ -86,7 +88,7 @@ test.describe("Room Directory", () => {
|
||||
.getByRole("button", { name: "Join" })
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL("/#/room/#test1234:localhost");
|
||||
await expect(page).toHaveURL(`/#/room/#test1234:${user.homeServer}`);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -111,6 +111,10 @@ test.describe("Room Header", () => {
|
||||
async ({ page, app, user }) => {
|
||||
await createVideoRoom(page, app);
|
||||
|
||||
// Dismiss a toast that is otherwise in the way (it's the other
|
||||
// side but there's no need to have it in the screenshot)
|
||||
await page.getByRole("button", { name: "Later" }).click();
|
||||
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
|
||||
// There's two room info button - the header itself and the i button
|
||||
|
||||
@@ -7,10 +7,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const TEST_ROOM_NAME = "The mark unread test room";
|
||||
|
||||
test.describe("Mark as Unread", () => {
|
||||
test.skip(isDendrite, "due to Dendrite bug https://github.com/element-hq/dendrite/issues/2970");
|
||||
|
||||
test.use({
|
||||
displayName: "Tom",
|
||||
botCreateOpts: {
|
||||
@@ -48,6 +51,6 @@ test.describe("Mark as Unread", () => {
|
||||
await roomTile.getByRole("button", { name: "Room options" }).click();
|
||||
await page.getByRole("menuitem", { name: "Mark as unread" }).click();
|
||||
|
||||
expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible();
|
||||
await expect(page.getByLabel(TEST_ROOM_NAME + " Unread messages.")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,14 +34,14 @@ test.describe("Account user settings tab", () => {
|
||||
await expect(profile.getByRole("textbox", { name: "Display Name" })).toHaveValue(USER_NAME);
|
||||
|
||||
// Assert that a userId is rendered
|
||||
expect(uut.getByLabel("Username")).toHaveText(user.userId);
|
||||
await expect(uut.getByLabel("Username")).toHaveText(user.userId);
|
||||
|
||||
// Wait until spinners disappear
|
||||
await expect(uut.getByTestId("accountSection").locator(".mx_Spinner")).not.toBeVisible();
|
||||
await expect(uut.getByTestId("discoverySection").locator(".mx_Spinner")).not.toBeVisible();
|
||||
|
||||
const accountSection = uut.getByTestId("accountSection");
|
||||
accountSection.scrollIntoViewIfNeeded();
|
||||
await accountSection.scrollIntoViewIfNeeded();
|
||||
// Assert that input areas for changing a password exists
|
||||
await expect(accountSection.getByLabel("Current password")).toBeVisible();
|
||||
await expect(accountSection.getByLabel("New Password")).toBeVisible();
|
||||
|
||||
@@ -7,7 +7,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { uiaLongSessionTimeoutHomeserver } from "../../plugins/homeserver/synapse/uiaLongSessionTimeoutHomeserver.ts";
|
||||
|
||||
// This is needed to not get stopped by UIA when deleting other devices
|
||||
test.use(uiaLongSessionTimeoutHomeserver);
|
||||
test.describe("Device manager", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
|
||||
97
playwright/e2e/settings/encryption-user-tab/index.ts
Normal file
97
playwright/e2e/settings/encryption-user-tab/index.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { test as base, expect } from "../../../element-web-test";
|
||||
export { expect };
|
||||
|
||||
/**
|
||||
* Set up for the encryption tab test
|
||||
*/
|
||||
export const test = base.extend<{
|
||||
util: Helpers;
|
||||
}>({
|
||||
util: async ({ page, app, bot }, use) => {
|
||||
await use(new Helpers(page, app));
|
||||
},
|
||||
});
|
||||
|
||||
class Helpers {
|
||||
constructor(
|
||||
private page: Page,
|
||||
private app: ElementAppPage,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Open the encryption tab
|
||||
*/
|
||||
openEncryptionTab() {
|
||||
return this.app.settings.openUserSettings("Encryption");
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the device verification flow using the recovery key.
|
||||
*/
|
||||
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
|
||||
// Select the security phrase
|
||||
await this.page.getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||
await this.enterRecoveryKey(recoveryKey);
|
||||
await this.page.getByRole("button", { name: "Done" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the recovery key in the dialog
|
||||
* @param recoveryKey
|
||||
*/
|
||||
async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) {
|
||||
// Select to use recovery key
|
||||
await this.page.getByRole("button", { name: "use your Security Key" }).click();
|
||||
|
||||
// Fill the recovery key
|
||||
const dialog = this.page.locator(".mx_Dialog");
|
||||
await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey);
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption tab content
|
||||
*/
|
||||
getEncryptionTabContent() {
|
||||
return this.page.getByTestId("encryptionTab");
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default key id of the secret storage to `null`
|
||||
*/
|
||||
async removeSecretStorageDefaultKeyId() {
|
||||
const client = await this.app.client.prepareClient();
|
||||
await client.evaluate(async (client) => {
|
||||
await client.secretStorage.setDefaultKeyId(null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the security key from the clipboard and fill in the input field
|
||||
* Then click on the finish button
|
||||
* @param title - The title of the dialog
|
||||
* @param confirmButtonLabel - The label of the confirm button
|
||||
* @param screenshot
|
||||
*/
|
||||
async confirmRecoveryKey(title: string, confirmButtonLabel: string, screenshot: `${string}.png`) {
|
||||
const dialog = this.getEncryptionTabContent();
|
||||
await expect(dialog.getByText(title, { exact: true })).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot(screenshot);
|
||||
|
||||
const clipboardContent = await this.app.getClipboard();
|
||||
await dialog.getByRole("textbox").fill(clipboardContent);
|
||||
await dialog.getByRole("button", { name: confirmButtonLabel }).click();
|
||||
await expect(dialog).toMatchScreenshot("default-recovery.png");
|
||||
}
|
||||
}
|
||||
156
playwright/e2e/settings/encryption-user-tab/recovery.spec.ts
Normal file
156
playwright/e2e/settings/encryption-user-tab/recovery.spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from ".";
|
||||
import {
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
deleteCachedSecrets,
|
||||
verifySession,
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Recovery section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
|
||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||
await expect(verifyButton).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||
await verifyButton.click();
|
||||
|
||||
await util.verifyDevice(recoveryKey);
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await app.closeDialog();
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test(
|
||||
"should change the recovery key",
|
||||
{ tag: ["@screenshot", "@no-webkit"] },
|
||||
async ({ page, app, homeserver, credentials, util, context }) => {
|
||||
await verifySession(app, "new passphrase");
|
||||
const dialog = await util.openEncryptionTab();
|
||||
|
||||
// The user can only change the recovery key
|
||||
const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
|
||||
await expect(changeButton).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
|
||||
await changeButton.click();
|
||||
|
||||
// Display the new recovery key and click on the copy button
|
||||
await expect(dialog.getByText("Change recovery key?")).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("change-key-1-encryption-tab.png", {
|
||||
mask: [dialog.getByTestId("recoveryKey")],
|
||||
});
|
||||
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Confirm the recovery key
|
||||
await util.confirmRecoveryKey(
|
||||
"Enter your new recovery key",
|
||||
"Confirm new recovery key",
|
||||
"change-key-2-encryption-tab.png",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => {
|
||||
await verifySession(app, "new passphrase");
|
||||
await util.removeSecretStorageDefaultKeyId();
|
||||
|
||||
// The key backup is deleted and the user needs to set it up
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
|
||||
await expect(setupButton).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
|
||||
await setupButton.click();
|
||||
|
||||
// Display an informative panel about the recovery key
|
||||
await expect(dialog.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-1-encryption-tab.png");
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Display the new recovery key and click on the copy button
|
||||
await expect(dialog.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-key-2-encryption-tab.png", {
|
||||
mask: [dialog.getByTestId("recoveryKey")],
|
||||
});
|
||||
await dialog.getByRole("button", { name: "Copy" }).click();
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Confirm the recovery key
|
||||
await util.confirmRecoveryKey(
|
||||
"Enter your recovery key to confirm",
|
||||
"Finish set up",
|
||||
"set-up-key-3-encryption-tab.png",
|
||||
);
|
||||
|
||||
// The recovery key is now set up and the user can change it
|
||||
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||
|
||||
await app.closeDialog();
|
||||
// Check that the current device is connected to key backup and the backup version is the expected one
|
||||
await checkDeviceIsConnectedKeyBackup(page, "1", true);
|
||||
});
|
||||
|
||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||
//
|
||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||
test(
|
||||
"should enter the recovery key when the secrets are not cached",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await verifySession(app, "new passphrase");
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
await util.openEncryptionTab();
|
||||
// We ask the user to enter the recovery key
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
||||
await expect(enterKeyButton).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
||||
await enterKeyButton.click();
|
||||
|
||||
// Fill the recovery key
|
||||
await util.enterRecoveryKey(recoveryKey);
|
||||
await expect(dialog).toMatchScreenshot("default-recovery.png");
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await app.closeDialog();
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -36,15 +36,16 @@ test.describe("General room settings tab", () => {
|
||||
await expect(settings.getByText("Show more")).toBeVisible();
|
||||
});
|
||||
|
||||
test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app }) => {
|
||||
test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app, user }) => {
|
||||
const settings = await app.settings.openRoomSettings("General");
|
||||
// 1. Set the room-address to be a really long string
|
||||
const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4);
|
||||
await settings.locator("#roomAliases input[label='Room address']").fill(longString);
|
||||
await expect(page.getByText("This address is available to use")).toBeVisible();
|
||||
await settings.locator("#roomAliases").getByText("Add", { exact: true }).click();
|
||||
|
||||
// 2. wait for the new setting to apply ...
|
||||
await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:localhost`);
|
||||
await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:${user.homeServer}`);
|
||||
|
||||
// 3. Check if the dialog overflows
|
||||
const dialogBoundingBox = await page.locator(".mx_Dialog").boundingBox();
|
||||
|
||||
@@ -24,7 +24,7 @@ test.describe("Preferences user settings tab", () => {
|
||||
});
|
||||
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
page.setViewportSize({ width: 1024, height: 3300 });
|
||||
await page.setViewportSize({ width: 1024, height: 3300 });
|
||||
const tab = await app.settings.openUserSettings("Preferences");
|
||||
// Assert that the top heading is rendered
|
||||
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
|
||||
@@ -61,7 +61,7 @@ test.describe("Preferences user settings tab", () => {
|
||||
// Click the button to display the dropdown menu
|
||||
await timezoneInput.getByRole("button", { name: "Set timezone" }).click();
|
||||
// Select a different value
|
||||
timezoneInput.getByRole("option", { name: /Africa\/Abidjan/ }).click();
|
||||
await timezoneInput.getByRole("option", { name: /Africa\/Abidjan/ }).click();
|
||||
// Check the new value
|
||||
await expect(timezoneValue.getByText("Africa/Abidjan")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -37,7 +37,9 @@ test.describe("Roles & Permissions room settings tab", () => {
|
||||
// Change the role of Alice to Moderator (50)
|
||||
await combobox.selectOption("Moderator");
|
||||
await expect(combobox).toHaveValue("50");
|
||||
const respPromise = page.waitForRequest("**/state/**");
|
||||
await applyButton.click();
|
||||
await respPromise;
|
||||
|
||||
// Reload and check Alice is still Moderator (50)
|
||||
await page.reload();
|
||||
|
||||
@@ -23,7 +23,7 @@ test.describe("Share dialog", () => {
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share room" });
|
||||
await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible();
|
||||
expect(dialog).toMatchScreenshot("share-dialog-room.png", {
|
||||
await expect(dialog).toMatchScreenshot("share-dialog-room.png", {
|
||||
// QRCode and url changes at every run
|
||||
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
|
||||
});
|
||||
@@ -40,7 +40,7 @@ test.describe("Share dialog", () => {
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share User" });
|
||||
await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible();
|
||||
expect(dialog).toMatchScreenshot("share-dialog-user.png", {
|
||||
await expect(dialog).toMatchScreenshot("share-dialog-user.png", {
|
||||
// QRCode changes at every run
|
||||
mask: [page.locator(".mx_QRCode")],
|
||||
});
|
||||
@@ -57,7 +57,7 @@ test.describe("Share dialog", () => {
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share Room Message" });
|
||||
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
|
||||
expect(dialog).toMatchScreenshot("share-dialog-event.png", {
|
||||
await expect(dialog).toMatchScreenshot("share-dialog-event.png", {
|
||||
// QRCode and url changes at every run
|
||||
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
|
||||
});
|
||||
|
||||
@@ -7,21 +7,31 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page, Request } from "@playwright/test";
|
||||
import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
import { ProxyInstance, SlidingSyncProxy } from "../../plugins/sliding-sync-proxy";
|
||||
|
||||
const test = base.extend<{
|
||||
slidingSyncProxy: ProxyInstance;
|
||||
slidingSyncProxy: StartedTestContainer;
|
||||
testRoom: { roomId: string; name: string };
|
||||
joinedBot: Bot;
|
||||
}>({
|
||||
slidingSyncProxy: async ({ context, page, homeserver }, use) => {
|
||||
const proxy = new SlidingSyncProxy(homeserver.config.dockerUrl, context);
|
||||
const proxyInstance = await proxy.start();
|
||||
const proxyAddress = `http://localhost:${proxyInstance.port}`;
|
||||
slidingSyncProxy: async ({ logger, network, postgres, page, homeserver }, use, testInfo) => {
|
||||
const container = await new GenericContainer("ghcr.io/matrix-org/sliding-sync:v0.99.3")
|
||||
.withNetwork(network)
|
||||
.withExposedPorts(8008)
|
||||
.withLogConsumer(logger.getConsumer("sliding-sync-proxy"))
|
||||
.withWaitStrategy(Wait.forHttp("/client/server.json", 8008))
|
||||
.withEnvironment({
|
||||
SYNCV3_SECRET: "bwahahaha",
|
||||
SYNCV3_DB: `user=${postgres.getUsername()} dbname=postgres password=${postgres.getPassword()} host=postgres sslmode=disable`,
|
||||
SYNCV3_SERVER: `http://homeserver:8008`,
|
||||
})
|
||||
.start();
|
||||
|
||||
const proxyAddress = `http://${container.getHost()}:${container.getMappedPort(8008)}`;
|
||||
await page.addInitScript((proxyAddress) => {
|
||||
window.localStorage.setItem(
|
||||
"mx_local_settings",
|
||||
@@ -31,8 +41,8 @@ const test = base.extend<{
|
||||
);
|
||||
window.localStorage.setItem("mx_labs_feature_feature_sliding_sync", "true");
|
||||
}, proxyAddress);
|
||||
await use(proxyInstance);
|
||||
await proxy.stop();
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
// Ensure slidingSyncProxy is set up before the user fixture as it relies on an init script
|
||||
credentials: async ({ slidingSyncProxy, credentials }, use) => {
|
||||
@@ -98,7 +108,6 @@ test.describe("Sliding Sync", () => {
|
||||
await page.getByRole("menuitemradio", { name: "A-Z" }).dispatchEvent("click");
|
||||
await expect(page.locator(".mx_StyledRadioButton_checked").getByText("A-Z")).toBeVisible();
|
||||
|
||||
await page.pause();
|
||||
await checkOrder(["Apple", "Orange", "Pineapple", "Test Room"], page);
|
||||
});
|
||||
|
||||
@@ -266,7 +275,7 @@ test.describe("Sliding Sync", () => {
|
||||
// now rescind the invite
|
||||
await bot.evaluate(
|
||||
async (client, { roomRescind, clientUserId }) => {
|
||||
client.kick(roomRescind, clientUserId);
|
||||
await client.kick(roomRescind, clientUserId);
|
||||
},
|
||||
{ roomRescind, clientUserId },
|
||||
);
|
||||
@@ -285,7 +294,7 @@ test.describe("Sliding Sync", () => {
|
||||
is_direct: true,
|
||||
});
|
||||
await app.client.evaluate(async (client, roomId) => {
|
||||
client.setRoomTag(roomId, "m.favourite", { order: 0.5 });
|
||||
await client.setRoomTag(roomId, "m.favourite", { order: 0.5 });
|
||||
}, roomId);
|
||||
await expect(page.getByRole("group", { name: "Favourites" }).getByText("Favourite DM")).toBeVisible();
|
||||
await expect(page.getByRole("group", { name: "People" }).getByText("Favourite DM")).not.toBeAttached();
|
||||
@@ -361,37 +370,42 @@ test.describe("Sliding Sync", () => {
|
||||
roomIds.push(id);
|
||||
await expect(page.getByRole("treeitem", { name: fruit })).toBeVisible();
|
||||
}
|
||||
const [roomAId, roomPId] = roomIds;
|
||||
const [roomAId, roomPId, roomOId] = roomIds;
|
||||
|
||||
const assertUnsubExists = (request: Request, subRoomId: string, unsubRoomId: string) => {
|
||||
const matchRoomSubRequest = (subRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
// There may be a request without a txn_id, ignore it, as there won't be any subscription changes
|
||||
if (body.txn_id === undefined) {
|
||||
return;
|
||||
}
|
||||
expect(body.unsubscribe_rooms).toEqual([unsubRoomId]);
|
||||
expect(body.room_subscriptions).not.toHaveProperty(unsubRoomId);
|
||||
expect(body.room_subscriptions).toHaveProperty(subRoomId);
|
||||
return body.txn_id && body.room_subscriptions?.[subRoomId];
|
||||
};
|
||||
const matchRoomUnsubRequest = (unsubRoomId: string) => (request: Request) => {
|
||||
if (!request.url().includes("/sync")) return false;
|
||||
const body = request.postDataJSON();
|
||||
return (
|
||||
body.txn_id && body.unsubscribe_rooms?.includes(unsubRoomId) && !body.room_subscriptions?.[unsubRoomId]
|
||||
);
|
||||
};
|
||||
|
||||
let promise = page.waitForRequest(/sync/);
|
||||
|
||||
// Select the Test Room
|
||||
await page.getByRole("treeitem", { name: "Apple", exact: true }).click();
|
||||
|
||||
// and wait for playwright to get the request
|
||||
const roomSubscriptions = (await promise).postDataJSON().room_subscriptions;
|
||||
// Select the Test Room and wait for playwright to get the request
|
||||
const [request] = await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Apple", exact: true }).click(),
|
||||
]);
|
||||
const roomSubscriptions = request.postDataJSON().room_subscriptions;
|
||||
expect(roomSubscriptions, "room_subscriptions is object").toBeDefined();
|
||||
|
||||
// Switch to another room
|
||||
promise = page.waitForRequest(/sync/);
|
||||
await page.getByRole("treeitem", { name: "Pineapple", exact: true }).click();
|
||||
assertUnsubExists(await promise, roomPId, roomAId);
|
||||
// Switch to another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomPId)),
|
||||
page.waitForRequest(matchRoomUnsubRequest(roomAId)),
|
||||
page.getByRole("treeitem", { name: "Pineapple", exact: true }).click(),
|
||||
]);
|
||||
|
||||
// And switch to even another room
|
||||
promise = page.waitForRequest(/sync/);
|
||||
await page.getByRole("treeitem", { name: "Apple", exact: true }).click();
|
||||
assertUnsubExists(await promise, roomPId, roomAId);
|
||||
// And switch to even another room and wait for playwright to get the request
|
||||
await Promise.all([
|
||||
page.waitForRequest(matchRoomSubRequest(roomOId)),
|
||||
page.waitForRequest(matchRoomUnsubRequest(roomPId)),
|
||||
page.getByRole("treeitem", { name: "Orange", exact: true }).click(),
|
||||
]);
|
||||
|
||||
// TODO: Add tests for encrypted rooms
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Locator, Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
async function openSpaceCreateMenu(page: Page): Promise<Locator> {
|
||||
await page.getByRole("button", { name: "Create a space" }).click();
|
||||
@@ -50,6 +51,7 @@ function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"
|
||||
}
|
||||
|
||||
test.describe("Spaces", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||
test.use({
|
||||
displayName: "Sue",
|
||||
botCreateOpts: { displayName: "BotBob" },
|
||||
@@ -82,7 +84,7 @@ test.describe("Spaces", () => {
|
||||
|
||||
// Copy matrix.to link
|
||||
await page.getByRole("button", { name: "Share invite link" }).click();
|
||||
expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#lets-have-a-riot:localhost");
|
||||
expect(await app.getClipboard()).toEqual(`https://matrix.to/#/#lets-have-a-riot:${user.homeServer}`);
|
||||
|
||||
// Go to space home
|
||||
await page.getByRole("button", { name: "Go to my first room" }).click();
|
||||
@@ -169,13 +171,13 @@ test.describe("Spaces", () => {
|
||||
room_alias_name: "space",
|
||||
});
|
||||
|
||||
const menu = await openSpaceContextMenu(page, app, "#space:localhost");
|
||||
const menu = await openSpaceContextMenu(page, app, `#space:${user.homeServer}`);
|
||||
await menu.getByRole("menuitem", { name: "Invite" }).click();
|
||||
|
||||
const shareDialog = page.locator(".mx_SpacePublicShare");
|
||||
// Copy link first
|
||||
await shareDialog.getByRole("button", { name: "Share invite link" }).click();
|
||||
expect(await app.getClipboardText()).toEqual("https://matrix.to/#/#space:localhost");
|
||||
expect(await app.getClipboard()).toEqual(`https://matrix.to/#/#space:${user.homeServer}`);
|
||||
// Start Matrix invite flow
|
||||
await shareDialog.getByRole("button", { name: "Invite people" }).click();
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { test as base, expect } from "../../../element-web-test";
|
||||
import { Bot } from "../../../pages/bot";
|
||||
import { Client } from "../../../pages/client";
|
||||
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { Credentials } from "../../../plugins/homeserver";
|
||||
|
||||
type RoomRef = { name: string; roomId: string };
|
||||
|
||||
@@ -37,11 +38,13 @@ export const test = base.extend<{
|
||||
room1Name: "Room 1",
|
||||
room1: async ({ room1Name: name, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
|
||||
await bot.awaitRoomMembership(roomId);
|
||||
await use({ name, roomId });
|
||||
},
|
||||
room2Name: "Room 2",
|
||||
room2: async ({ room2Name: name, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
|
||||
await bot.awaitRoomMembership(roomId);
|
||||
await use({ name, roomId });
|
||||
},
|
||||
msg: async ({ page, app, util }, use) => {
|
||||
@@ -336,12 +339,14 @@ export class Helpers {
|
||||
* @param room1
|
||||
* @param room2
|
||||
* @param msg - MessageBuilder
|
||||
* @param user - the user to mention in the first message
|
||||
* @param hasMention - whether to include a mention in the first message
|
||||
*/
|
||||
async populateThreads(
|
||||
room1: { name: string; roomId: string },
|
||||
room2: { name: string; roomId: string },
|
||||
msg: MessageBuilder,
|
||||
user: Credentials,
|
||||
hasMention = true,
|
||||
) {
|
||||
if (hasMention) {
|
||||
@@ -350,9 +355,9 @@ export class Helpers {
|
||||
msg.threadedOff("Msg1", {
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href='https://matrix.to/#/@user:localhost'>User</a>",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${user.userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: ["@user:localhost"],
|
||||
user_ids: [user.userId],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
|
||||
import { expect, test } from ".";
|
||||
import { CommandOrControl } from "../../utils";
|
||||
import { isDendrite } from "../../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
|
||||
test.skip(
|
||||
isDendrite,
|
||||
"due to Dendrite lacking full threads support https://github.com/element-hq/dendrite/issues/3283",
|
||||
);
|
||||
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: { displayName: "Other User" },
|
||||
@@ -46,16 +52,21 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
|
||||
await util.assertNotificationTac();
|
||||
});
|
||||
|
||||
test("should show a highlight indicator when there is a mention in a thread", async ({ room1, util, msg }) => {
|
||||
test("should show a highlight indicator when there is a mention in a thread", async ({
|
||||
room1,
|
||||
util,
|
||||
msg,
|
||||
user,
|
||||
}) => {
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room1, [
|
||||
"Msg1",
|
||||
msg.threadedOff("Msg1", {
|
||||
"body": "User",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href='https://matrix.to/#/@user:localhost'>User</a>",
|
||||
"formatted_body": `<a href="https://matrix.to/#/${user.userId}">User</a>`,
|
||||
"m.mentions": {
|
||||
user_ids: ["@user:localhost"],
|
||||
user_ids: [user.userId],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
@@ -64,26 +75,30 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
|
||||
await util.assertHighlightIndicator();
|
||||
});
|
||||
|
||||
test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
|
||||
test(
|
||||
"should show the rooms with unread threads",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ room1, room2, util, msg, user }) => {
|
||||
await util.goTo(room2);
|
||||
await util.populateThreads(room1, room2, msg, user);
|
||||
// The indicator should be shown
|
||||
await util.assertHighlightIndicator();
|
||||
|
||||
// Verify that we have the expected rooms in the TAC
|
||||
await util.openTac();
|
||||
await util.assertRoomsInTac([
|
||||
{ room: room2.name, notificationLevel: "highlight" },
|
||||
{ room: room1.name, notificationLevel: "notification" },
|
||||
]);
|
||||
|
||||
// Verify that we don't have a visual regression
|
||||
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, user }) => {
|
||||
await util.goTo(room2);
|
||||
await util.populateThreads(room1, room2, msg);
|
||||
// The indicator should be shown
|
||||
await util.assertHighlightIndicator();
|
||||
|
||||
// Verify that we have the expected rooms in the TAC
|
||||
await util.openTac();
|
||||
await util.assertRoomsInTac([
|
||||
{ room: room2.name, notificationLevel: "highlight" },
|
||||
{ room: room1.name, notificationLevel: "notification" },
|
||||
]);
|
||||
|
||||
// Verify that we don't have a visual regression
|
||||
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
|
||||
});
|
||||
|
||||
test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
|
||||
await util.goTo(room2);
|
||||
await util.populateThreads(room1, room2, msg);
|
||||
await util.populateThreads(room1, room2, msg, user);
|
||||
|
||||
// Click on the first room in TAC
|
||||
await util.openTac();
|
||||
@@ -104,9 +119,9 @@ test.describe("Threads Activity Centre", { tag: "@no-firefox" }, () => {
|
||||
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-notification-unread.png");
|
||||
});
|
||||
|
||||
test("should order by recency after notification level", async ({ room1, room2, util, msg }) => {
|
||||
test("should order by recency after notification level", async ({ room1, room2, util, msg, user }) => {
|
||||
await util.goTo(room2);
|
||||
await util.populateThreads(room1, room2, msg, false);
|
||||
await util.populateThreads(room1, room2, msg, user, false);
|
||||
|
||||
await util.openTac();
|
||||
await util.assertRoomsInTac([
|
||||
|
||||
@@ -6,12 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import type { AccountDataEvents } from "matrix-js-sdk/src/matrix";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import type { AccountDataEvents, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import { Filter } from "../../pages/Spotlight";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
function roomHeaderName(page: Page): Locator {
|
||||
return page.locator(".mx_RoomHeader_heading");
|
||||
@@ -38,41 +39,37 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
|
||||
await expect(page.getByRole("group", { name: "People" }).getByText(name)).toBeAttached();
|
||||
}
|
||||
|
||||
test.describe("Spotlight", () => {
|
||||
const bot1Name = "BotBob";
|
||||
let bot1: Bot;
|
||||
|
||||
const bot2Name = "ByteBot";
|
||||
let bot2: Bot;
|
||||
|
||||
const room1Name = "247";
|
||||
let room1Id: string;
|
||||
|
||||
const room2Name = "Lounge";
|
||||
let room2Id: string;
|
||||
|
||||
const room3Name = "Public";
|
||||
let room3Id: string;
|
||||
|
||||
test.use({
|
||||
displayName: "Jim",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, app, user }) => {
|
||||
bot1 = new Bot(page, homeserver, { displayName: bot1Name, autoAcceptInvites: true });
|
||||
bot2 = new Bot(page, homeserver, { displayName: bot2Name, autoAcceptInvites: true });
|
||||
const Visibility = await page.evaluate(() => (window as any).matrixcs.Visibility);
|
||||
|
||||
room1Id = await app.client.createRoom({ name: room1Name, visibility: Visibility.Public });
|
||||
|
||||
await bot1.joinRoom(room1Id);
|
||||
const bot1UserId = await bot1.evaluate((client) => client.getUserId());
|
||||
room2Id = await bot2.createRoom({ name: room2Name, visibility: Visibility.Public });
|
||||
await bot2.inviteUser(room2Id, bot1UserId);
|
||||
|
||||
room3Id = await bot2.createRoom({
|
||||
name: room3Name,
|
||||
visibility: Visibility.Public,
|
||||
type RoomRef = { name: string; roomId: string };
|
||||
const test = base.extend<{
|
||||
bot1: Bot;
|
||||
bot2: Bot;
|
||||
room1: RoomRef;
|
||||
room2: RoomRef;
|
||||
room3: RoomRef;
|
||||
}>({
|
||||
bot1: async ({ page, homeserver }, use, testInfo) => {
|
||||
const bot = new Bot(page, homeserver, { displayName: `BotBob_${testInfo.testId}`, autoAcceptInvites: true });
|
||||
await use(bot);
|
||||
},
|
||||
bot2: async ({ page, homeserver }, use, testInfo) => {
|
||||
const bot = new Bot(page, homeserver, { displayName: `ByteBot_${testInfo.testId}`, autoAcceptInvites: true });
|
||||
await use(bot);
|
||||
},
|
||||
room1: async ({ app }, use) => {
|
||||
const name = "247";
|
||||
const roomId = await app.client.createRoom({ name, visibility: "public" as Visibility });
|
||||
await use({ name, roomId });
|
||||
},
|
||||
room2: async ({ bot2 }, use) => {
|
||||
const name = "Lounge";
|
||||
const roomId = await bot2.createRoom({ name, visibility: "public" as Visibility });
|
||||
await use({ name, roomId });
|
||||
},
|
||||
room3: async ({ bot2 }, use) => {
|
||||
const name = "Public";
|
||||
const roomId = await bot2.createRoom({
|
||||
name,
|
||||
visibility: "public" as Visibility,
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
@@ -83,9 +80,27 @@ test.describe("Spotlight", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
await bot2.inviteUser(room3Id, bot1UserId);
|
||||
await use({ name, roomId });
|
||||
},
|
||||
context: async ({ context, homeserver }, use) => {
|
||||
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||
await homeserver.restart();
|
||||
await use(context);
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto("/#/room/" + room1Id);
|
||||
test.describe("Spotlight", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||
test.use({
|
||||
displayName: "Jim",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, user, bot1, bot2, room1, room2, room3 }) => {
|
||||
await bot1.joinRoom(room1.roomId);
|
||||
await bot2.inviteUser(room2.roomId, bot1.credentials.userId);
|
||||
await bot2.inviteUser(room3.roomId, bot1.credentials.userId);
|
||||
|
||||
await page.goto(`/#/room/${room1.roomId}`);
|
||||
await expect(page.locator(".mx_RoomSublist_skeletonUI")).not.toBeAttached();
|
||||
});
|
||||
|
||||
@@ -117,69 +132,69 @@ test.describe("Spotlight", () => {
|
||||
await expect(spotlight.dialog.locator(".mx_SpotlightDialog_filter")).not.toBeAttached();
|
||||
});
|
||||
|
||||
test("should find joined rooms", async ({ page, app }) => {
|
||||
test("should find joined rooms", async ({ page, app, room1 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.search(room1Name);
|
||||
await spotlight.search(room1.name);
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(room1Name);
|
||||
await expect(resultLocator.first()).toContainText(room1.name);
|
||||
await resultLocator.first().click();
|
||||
expect(page.url()).toContain(room1Id);
|
||||
await expect(roomHeaderName(page)).toContainText(room1Name);
|
||||
await expect(page).toHaveURL(new RegExp(`#/room/${room1.roomId}`));
|
||||
await expect(roomHeaderName(page)).toContainText(room1.name);
|
||||
});
|
||||
|
||||
test("should find known public rooms", async ({ page, app }) => {
|
||||
test("should find known public rooms", async ({ page, app, room1 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.PublicRooms);
|
||||
await spotlight.search(room1Name);
|
||||
await spotlight.search(room1.name);
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(room1Name);
|
||||
await expect(resultLocator.first()).toContainText(room1.name);
|
||||
await expect(resultLocator.first()).toContainText("View");
|
||||
await resultLocator.first().click();
|
||||
expect(page.url()).toContain(room1Id);
|
||||
await expect(roomHeaderName(page)).toContainText(room1Name);
|
||||
await expect(page).toHaveURL(new RegExp(`#/room/${room1.roomId}`));
|
||||
await expect(roomHeaderName(page)).toContainText(room1.name);
|
||||
});
|
||||
|
||||
test("should find unknown public rooms", async ({ page, app }) => {
|
||||
test("should find unknown public rooms", async ({ page, app, room2 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.PublicRooms);
|
||||
await spotlight.search(room2Name);
|
||||
await spotlight.search(room2.name);
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(room2Name);
|
||||
await expect(resultLocator.first()).toContainText(room2.name);
|
||||
await expect(resultLocator.first()).toContainText("Join");
|
||||
await resultLocator.first().click();
|
||||
expect(page.url()).toContain(room2Id);
|
||||
await expect(page).toHaveURL(new RegExp(`#/room/${room2.roomId}`));
|
||||
await expect(page.locator(".mx_RoomView_MessageList")).toHaveCount(1);
|
||||
await expect(roomHeaderName(page)).toContainText(room2Name);
|
||||
await expect(roomHeaderName(page)).toContainText(room2.name);
|
||||
});
|
||||
|
||||
test("should find unknown public world readable rooms", async ({ page, app }) => {
|
||||
test("should find unknown public world readable rooms", async ({ page, app, room3 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.PublicRooms);
|
||||
await spotlight.search(room3Name);
|
||||
await spotlight.search(room3.name);
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(room3Name);
|
||||
await expect(resultLocator.first()).toContainText(room3.name);
|
||||
await expect(resultLocator.first()).toContainText("View");
|
||||
await resultLocator.first().click();
|
||||
expect(page.url()).toContain(room3Id);
|
||||
await expect(page).toHaveURL(new RegExp(`#/room/${room3.roomId}`));
|
||||
await page.getByRole("button", { name: "Join the discussion" }).click();
|
||||
await expect(roomHeaderName(page)).toHaveText(room3Name);
|
||||
await expect(roomHeaderName(page)).toHaveText(room3.name);
|
||||
});
|
||||
|
||||
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
||||
// We obviously don’t have federation or bridges in local e2e tests
|
||||
test.skip("should find unknown public rooms on other homeservers", async ({ page, app }) => {
|
||||
test.skip("should find unknown public rooms on other homeservers", async ({ page, app, room3 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.PublicRooms);
|
||||
await spotlight.search(room3Name);
|
||||
await spotlight.search(room3.name);
|
||||
await page.locator("[aria-haspopup=true][role=button]").click();
|
||||
|
||||
await page
|
||||
@@ -194,20 +209,20 @@ test.describe("Spotlight", () => {
|
||||
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(room3Name);
|
||||
await expect(resultLocator.first()).toContainText(room3Id);
|
||||
await expect(resultLocator.first()).toContainText(room3.name);
|
||||
await expect(resultLocator.first()).toContainText(room3.roomId);
|
||||
});
|
||||
|
||||
test("should find known people", async ({ page, app }) => {
|
||||
test("should find known people", async ({ page, app, bot1 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot1Name);
|
||||
await spotlight.search(bot1.credentials.displayName);
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(bot1Name);
|
||||
await expect(resultLocator.first()).toContainText(bot1.credentials.displayName);
|
||||
await resultLocator.first().click();
|
||||
await expect(roomHeaderName(page)).toHaveText(bot1Name);
|
||||
await expect(roomHeaderName(page)).toHaveText(bot1.credentials.displayName);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -217,42 +232,41 @@ test.describe("Spotlight", () => {
|
||||
*
|
||||
* https://github.com/matrix-org/synapse/issues/16472
|
||||
*/
|
||||
test("should find unknown people", async ({ page, app }) => {
|
||||
test("should find unknown people", async ({ page, app, bot2 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot2Name);
|
||||
await spotlight.search(bot2.credentials.displayName);
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(bot2Name);
|
||||
await expect(resultLocator.first()).toContainText(bot2.credentials.displayName);
|
||||
await resultLocator.first().click();
|
||||
await expect(roomHeaderName(page)).toHaveText(bot2Name);
|
||||
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
||||
});
|
||||
|
||||
test("should find group DMs by usernames or user ids", async ({ page, app }) => {
|
||||
test("should find group DMs by usernames or user ids", async ({ page, app, bot1, bot2, room1 }) => {
|
||||
// First we want to share a room with both bots to ensure we’ve got their usernames cached
|
||||
const bot2UserId = await bot2.evaluate((client) => client.getUserId());
|
||||
await app.client.inviteUser(room1Id, bot2UserId);
|
||||
await app.client.inviteUser(room1.roomId, bot2.credentials.userId);
|
||||
|
||||
// Starting a DM with ByteBot (will be turned into a group dm later)
|
||||
let spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot2Name);
|
||||
await spotlight.search(bot2.credentials.displayName);
|
||||
let resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(bot2Name);
|
||||
await expect(resultLocator.first()).toContainText(bot2.credentials.displayName);
|
||||
await resultLocator.first().click();
|
||||
|
||||
// Send first message to actually start DM
|
||||
await expect(roomHeaderName(page)).toHaveText(bot2Name);
|
||||
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
||||
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||
await locator.fill("Hey!");
|
||||
await locator.press("Enter");
|
||||
|
||||
// Assert DM exists by checking for the first message and the room being in the room list
|
||||
await expect(page.locator(".mx_EventTile_body").filter({ hasText: "Hey!" })).toBeAttached({ timeout: 3000 });
|
||||
await expect(page.getByRole("group", { name: "People" })).toContainText(bot2Name);
|
||||
await expect(page.getByRole("group", { name: "People" })).toContainText(bot2.credentials.displayName);
|
||||
|
||||
// Invite BotBob into existing DM with ByteBot
|
||||
const dmRooms = await app.client.evaluate((client, userId) => {
|
||||
@@ -260,18 +274,17 @@ test.describe("Spotlight", () => {
|
||||
.getAccountData("m.direct" as keyof AccountDataEvents)
|
||||
?.getContent<Record<string, string[]>>();
|
||||
return map[userId] ?? [];
|
||||
}, bot2UserId);
|
||||
}, bot2.credentials.userId);
|
||||
expect(dmRooms).toHaveLength(1);
|
||||
const groupDmName = await app.client.evaluate((client, id) => client.getRoom(id).name, dmRooms[0]);
|
||||
const bot1UserId = await bot1.evaluate((client) => client.getUserId());
|
||||
await app.client.inviteUser(dmRooms[0], bot1UserId);
|
||||
await app.client.inviteUser(dmRooms[0], bot1.credentials.userId);
|
||||
await expect(roomHeaderName(page).first()).toContainText(groupDmName);
|
||||
await expect(page.getByRole("group", { name: "People" }).first()).toContainText(groupDmName);
|
||||
|
||||
// Search for BotBob by id, should return group DM and user
|
||||
spotlight = await app.openSpotlight();
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot1UserId);
|
||||
await spotlight.search(bot1.credentials.userId);
|
||||
await page.waitForTimeout(1000); // wait for the dialog to settle
|
||||
resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(2);
|
||||
@@ -284,7 +297,7 @@ test.describe("Spotlight", () => {
|
||||
// Search for ByteBot by id, should return group DM and user
|
||||
spotlight = await app.openSpotlight();
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot2UserId);
|
||||
await spotlight.search(bot2.credentials.userId);
|
||||
await page.waitForTimeout(1000); // wait for the dialog to settle
|
||||
resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(2);
|
||||
@@ -297,11 +310,10 @@ test.describe("Spotlight", () => {
|
||||
});
|
||||
|
||||
// Test against https://github.com/vector-im/element-web/issues/22851
|
||||
test("should show each person result only once", async ({ page, app }) => {
|
||||
test("should show each person result only once", async ({ page, app, bot1 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.People);
|
||||
const bot1UserId = await bot1.evaluate((client) => client.getUserId());
|
||||
|
||||
// 2 rounds of search to simulate the bug conditions. Specifically, the first search
|
||||
// should have 1 result (not 2) and the second search should also have 1 result (instead
|
||||
@@ -310,24 +322,24 @@ test.describe("Spotlight", () => {
|
||||
// We search for user ID to trigger the profile lookup within the dialog.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
console.log("Iteration: " + i);
|
||||
await spotlight.search(bot1UserId);
|
||||
await spotlight.search(bot1.credentials.userId);
|
||||
await page.waitForTimeout(1000); // wait for the dialog to settle
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(bot1UserId);
|
||||
await expect(resultLocator.first()).toContainText(bot1.credentials.userId);
|
||||
}
|
||||
});
|
||||
|
||||
test("should allow opening group chat dialog", async ({ page, app }) => {
|
||||
test("should allow opening group chat dialog", async ({ page, app, bot2 }) => {
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot2Name);
|
||||
await spotlight.search(bot2.credentials.displayName);
|
||||
await page.waitForTimeout(3000); // wait for the dialog to settle
|
||||
|
||||
const resultLocator = spotlight.results;
|
||||
await expect(resultLocator).toHaveCount(1);
|
||||
await expect(resultLocator.first()).toContainText(bot2Name);
|
||||
await expect(resultLocator.first()).toContainText(bot2.credentials.displayName);
|
||||
|
||||
await expect(spotlight.dialog.locator(".mx_SpotlightDialog_startGroupChat")).toContainText(
|
||||
"Start a group chat",
|
||||
@@ -336,18 +348,18 @@ test.describe("Spotlight", () => {
|
||||
await expect(page.getByRole("dialog")).toContainText("Direct Messages");
|
||||
});
|
||||
|
||||
test("should close spotlight after starting a DM", async ({ page, app }) => {
|
||||
await startDM(app, page, bot1Name);
|
||||
test("should close spotlight after starting a DM", async ({ page, app, bot1 }) => {
|
||||
await startDM(app, page, bot1.credentials.displayName);
|
||||
await expect(page.locator(".mx_SpotlightDialog")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("should show the same user only once", async ({ page, app }) => {
|
||||
await startDM(app, page, bot1Name);
|
||||
test("should show the same user only once", async ({ page, app, bot1 }) => {
|
||||
await startDM(app, page, bot1.credentials.displayName);
|
||||
await page.goto("/#/home");
|
||||
const spotlight = await app.openSpotlight();
|
||||
await page.waitForTimeout(500); // wait for the dialog to settle
|
||||
await spotlight.filter(Filter.People);
|
||||
await spotlight.search(bot1Name);
|
||||
await spotlight.search(bot1.credentials.displayName);
|
||||
await page.waitForTimeout(3000); // wait for the dialog to settle
|
||||
await expect(spotlight.dialog.locator(".mx_Spinner")).not.toBeAttached();
|
||||
const resultLocator = spotlight.results;
|
||||
|
||||
@@ -8,8 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Threads", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3489");
|
||||
test.use({
|
||||
displayName: "Tom",
|
||||
botCreateOpts: {
|
||||
@@ -24,8 +26,7 @@ test.describe("Threads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Flaky: https://github.com/vector-im/element-web/issues/26452
|
||||
test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => {
|
||||
test("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => {
|
||||
const roomId = await app.client.createRoom({});
|
||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||
await bot.joinRoom(roomId);
|
||||
@@ -76,7 +77,7 @@ test.describe("Threads", () => {
|
||||
mask: mask,
|
||||
});
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toBeVisible();
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toHaveCount(2);
|
||||
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_bubble_layout.png", {
|
||||
mask: mask,
|
||||
@@ -136,8 +137,8 @@ test.describe("Threads", () => {
|
||||
await page.getByRole("gridcell", { name: "👋" }).click();
|
||||
|
||||
locator = page.locator(".mx_ThreadView");
|
||||
// Make sure the CSS style for spacing is applied to mx_ReactionsRow on group/modern layout
|
||||
await expect(locator.locator(".mx_EventTile[data-layout=group] .mx_ReactionsRow")).toHaveCSS(
|
||||
// Make sure the CSS style for spacing is applied to mx_EventTile_footer on group/modern layout
|
||||
await expect(locator.locator(".mx_EventTile[data-layout=group] .mx_EventTile_footer")).toHaveCSS(
|
||||
"margin-inline-start",
|
||||
ThreadViewGroupSpacingStart,
|
||||
);
|
||||
@@ -164,7 +165,7 @@ test.describe("Threads", () => {
|
||||
locator = page.locator(
|
||||
".mx_ThreadView .mx_GenericEventListSummary[data-layout=bubble] .mx_EventTile_info.mx_EventTile_last",
|
||||
);
|
||||
expect(locator.locator(".mx_EventTile_line .mx_EventTile_content"))
|
||||
await expect(locator.locator(".mx_EventTile_line .mx_EventTile_content"))
|
||||
// 76px: ThreadViewGroupSpacingStart + 14px + 6px
|
||||
// 14px: avatar width
|
||||
// See: _EventTile.pcss
|
||||
@@ -202,12 +203,14 @@ test.describe("Threads", () => {
|
||||
await locator.click();
|
||||
|
||||
// Wait until the response is redacted
|
||||
await expect(
|
||||
page.locator(".mx_ThreadView").locator(".mx_EventTile_last .mx_EventTile_receiptSent"),
|
||||
).toBeVisible();
|
||||
// XXX: one would expect this redaction to be shown in the thread the message was in, but due to redactions
|
||||
// stripping the thread_id, it is instead shown in the main timeline
|
||||
await expect(page.locator(".mx_MainSplit_timeline").locator(".mx_EventTile_last")).toContainText(
|
||||
"Message deleted",
|
||||
);
|
||||
|
||||
// Take snapshots in group layout and bubble layout (IRC layout is not available on ThreadView)
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']")).toBeVisible();
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']")).toHaveCount(2);
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_redacted_messages_on_group_layout.png",
|
||||
{
|
||||
@@ -215,7 +218,7 @@ test.describe("Threads", () => {
|
||||
},
|
||||
);
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toBeVisible();
|
||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toHaveCount(2);
|
||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||
"ThreadView_with_redacted_messages_on_bubble_layout.png",
|
||||
{
|
||||
@@ -233,8 +236,8 @@ test.describe("Threads", () => {
|
||||
|
||||
// User closes right panel after clicking back to thread list
|
||||
locator = page.locator(".mx_ThreadPanel");
|
||||
locator.getByRole("button", { name: "Threads" }).click();
|
||||
locator.getByRole("button", { name: "Close" }).click();
|
||||
await locator.getByRole("button", { name: "Threads" }).click();
|
||||
await locator.getByRole("button", { name: "Close" }).click();
|
||||
|
||||
// Bot responds to thread
|
||||
await bot.sendMessage(roomId, "How are things?", threadId);
|
||||
@@ -243,9 +246,8 @@ test.describe("Threads", () => {
|
||||
await expect(locator.locator(".mx_ThreadSummary_sender").getByText("BotBob")).toBeAttached();
|
||||
await expect(locator.locator(".mx_ThreadSummary_content").getByText("How are things?")).toBeAttached();
|
||||
|
||||
locator = page.getByRole("button", { name: "Threads" });
|
||||
await expect(locator).toHaveAttribute("data-indicator", "default"); // User asserts thread list unread indicator
|
||||
// await expect(locator).toHaveClass(/mx_LegacyRoomHeader_button--unread/);
|
||||
locator = page.getByRole("banner").getByRole("button", { name: "Threads" });
|
||||
await expect(locator).toHaveAttribute("data-indicator", "success"); // User asserts thread list unread indicator
|
||||
await locator.click(); // User opens thread list
|
||||
|
||||
// User asserts thread with correct root & latest events & unread dot
|
||||
@@ -273,20 +275,18 @@ test.describe("Threads", () => {
|
||||
await expect(locator.getByText("Great!")).toBeAttached();
|
||||
await locator.locator(".mx_EventTile_line").hover();
|
||||
await locator.locator(".mx_EventTile_line").getByRole("button", { name: "Edit" }).click();
|
||||
await locator.getByRole("textbox").fill(" How about yourself?{enter}");
|
||||
await locator.getByRole("textbox").pressSequentially(" How about yourself?"); // fill would overwrite the original text
|
||||
await locator.getByRole("textbox").press("Enter");
|
||||
|
||||
locator = page.locator(".mx_RoomView_body .mx_ThreadSummary");
|
||||
await expect(locator.locator(".mx_ThreadSummary_sender").getByText("Tom")).toBeAttached();
|
||||
await expect(
|
||||
locator.locator(".mx_ThreadSummary_content").getByText("Great! How about yourself?"),
|
||||
).toBeAttached();
|
||||
await expect(locator.locator(".mx_ThreadSummary_content")).toHaveText("Great! How about yourself?");
|
||||
|
||||
// User closes right panel
|
||||
await page.locator(".mx_ThreadPanel").getByRole("button", { name: "Close" }).click();
|
||||
|
||||
// Bot responds to thread and saves the id of their message to @eventId
|
||||
const { event_id: eventId } = await bot.sendMessage(roomId, threadId, "I'm very good thanks");
|
||||
const { event_id: eventId } = await bot.sendMessage(roomId, "I'm very good thanks", threadId);
|
||||
|
||||
// User asserts
|
||||
locator = page.locator(".mx_RoomView_body .mx_ThreadSummary");
|
||||
@@ -344,7 +344,7 @@ test.describe("Threads", () => {
|
||||
|
||||
await expect(page.locator(".mx_ThreadView_timelinePanelWrapper")).toHaveCount(1);
|
||||
|
||||
(await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Voice Message" }).click();
|
||||
await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Voice Message" }).click();
|
||||
await page.waitForTimeout(3000);
|
||||
await app.getComposer(true).getByRole("button", { name: "Send voice message" }).click();
|
||||
await expect(page.locator(".mx_ThreadView .mx_MVoiceMessageBody")).toHaveCount(1);
|
||||
|
||||
@@ -590,10 +590,6 @@ test.describe("Timeline", () => {
|
||||
"should set inline start padding to a hidden event line",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room }) => {
|
||||
test.skip(
|
||||
true,
|
||||
"Disabled due to screenshot test being flaky - https://github.com/element-hq/element-web/issues/26890",
|
||||
);
|
||||
await sendEvent(app.client, room.roomId);
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.settings.setValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
|
||||
@@ -607,7 +603,12 @@ test.describe("Timeline", () => {
|
||||
await messageEdit(page);
|
||||
|
||||
// Click timestamp to highlight hidden event line
|
||||
await page.locator(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
|
||||
const timestamp = page.locator(".mx_RoomView_body .mx_EventTile_info a", {
|
||||
has: page.locator(".mx_MessageTimestamp"),
|
||||
});
|
||||
// wait for the remote echo otherwise we get an error modal due to a 404 on the /event/ API
|
||||
await expect(timestamp).not.toHaveAttribute("href", /~!/);
|
||||
await timestamp.locator(".mx_MessageTimestamp").click();
|
||||
|
||||
// should not add inline start padding to a hidden event line on IRC layout
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
@@ -1194,6 +1195,7 @@ test.describe("Timeline", () => {
|
||||
});
|
||||
|
||||
await sendImage(app.client, room.roomId, NEW_AVATAR);
|
||||
await app.timeline.scrollToBottom();
|
||||
await expect(page.locator(".mx_MImageBody").first()).toBeVisible();
|
||||
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("User Onboarding (new user)", () => {
|
||||
test.use({
|
||||
displayName: "Jane Doe",
|
||||
});
|
||||
|
||||
// This first beforeEach happens before the `user` fixture runs
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("mx_registration_time", "1656633601");
|
||||
});
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, user }) => {
|
||||
await expect(page.locator(".mx_UserOnboardingPage")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Welcome" })).toBeVisible();
|
||||
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
|
||||
});
|
||||
|
||||
test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
|
||||
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png",
|
||||
);
|
||||
await app.settings.openUserSettings("Preferences");
|
||||
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
|
||||
});
|
||||
|
||||
test("app download dialog", { tag: "@screenshot" }, async ({ page }) => {
|
||||
await page.getByRole("button", { name: "Download apps" }).click();
|
||||
await expect(
|
||||
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot(
|
||||
"User-Onboarding-new-user-app-download-dialog-1.png",
|
||||
{
|
||||
// Set a constant bg behind the modal to ensure screenshot stability
|
||||
css: `
|
||||
.mx_AppDownloadDialog_wrapper {
|
||||
background: black;
|
||||
}
|
||||
`,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test("using find friends action should increase progress", async ({ page, homeserver }) => {
|
||||
const bot = await homeserver.registerUser("botbob", "password", "BotBob");
|
||||
|
||||
const oldProgress = parseFloat(await page.getByRole("progressbar").getAttribute("value"));
|
||||
await page.getByRole("button", { name: "Find friends" }).click();
|
||||
await page.locator(".mx_InviteDialog_editor").getByRole("textbox").fill(bot.userId);
|
||||
await page.getByRole("button", { name: "Go" }).click();
|
||||
await expect(page.locator(".mx_InviteDialog_buttonAndSpinner")).not.toBeVisible();
|
||||
|
||||
const message = "Hi!";
|
||||
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||
await composer.fill(`${message}`);
|
||||
await composer.press("Enter");
|
||||
await expect(page.locator(".mx_MTextBody.mx_EventTile_content", { hasText: message })).toBeVisible();
|
||||
|
||||
await page.goto("/#/home");
|
||||
await expect(page.locator(".mx_UserOnboardingPage")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Welcome" })).toBeVisible();
|
||||
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
|
||||
|
||||
await page.waitForTimeout(500); // await progress bar animation
|
||||
const progress = parseFloat(await page.getByRole("progressbar").getAttribute("value"));
|
||||
expect(progress).toBeGreaterThan(oldProgress);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("User Onboarding (old user)", () => {
|
||||
test.use({
|
||||
displayName: "Jane Doe",
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem("mx_registration_time", "2");
|
||||
});
|
||||
});
|
||||
|
||||
test("page and preference are hidden", async ({ page, user, app }) => {
|
||||
await expect(page.locator(".mx_UserOnboardingPage")).not.toBeVisible();
|
||||
await expect(page.locator(".mx_UserOnboardingButton")).not.toBeVisible();
|
||||
await app.settings.openUserSettings("Preferences");
|
||||
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user