Compare commits
1 Commits
v1.11.93
...
t3chguy/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4f0aa7ee0 |
@@ -7,4 +7,3 @@ test/end-to-end-tests/lib/
|
||||
src/component-index.js
|
||||
# Auto-generated file
|
||||
src/modules.ts
|
||||
src/modules.js
|
||||
|
||||
15
.eslintrc.js
15
.eslintrc.js
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||
plugins: ["matrix-org"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
@@ -170,8 +170,6 @@ module.exports = {
|
||||
"jsx-a11y/role-supports-aria-props": "off",
|
||||
|
||||
"matrix-org/require-copyright-header": "error",
|
||||
|
||||
"react-compiler/react-compiler": "error",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -200,13 +198,8 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
// We do this sometimes to brand interfaces
|
||||
allowInterfaces: "with-single-extends",
|
||||
},
|
||||
],
|
||||
// We do this sometimes to brand interfaces
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
},
|
||||
},
|
||||
// temporary override for offending icon require files
|
||||
@@ -252,7 +245,6 @@ module.exports = {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
// Jest/Playwright specific
|
||||
|
||||
@@ -270,7 +262,6 @@ module.exports = {
|
||||
|
||||
// These are fine in tests
|
||||
"no-restricted-globals": "off",
|
||||
"react-compiler/react-compiler": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
20
.github/CODEOWNERS
vendored
20
.github/CODEOWNERS
vendored
@@ -3,19 +3,13 @@
|
||||
/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/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
/src/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
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
|
||||
6
.github/workflows/build_develop.yml
vendored
6
.github/workflows/build_develop.yml
vendored
@@ -26,12 +26,6 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
|
||||
- uses: unfor19/install-aws-cli-action@v1
|
||||
with:
|
||||
version: 2.22.35
|
||||
verbose: false
|
||||
arch: amd64
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -96,4 +96,3 @@ jobs:
|
||||
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||
directory: _deploy
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: main
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
name: Docker
|
||||
name: Dockerhub
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
tags: [v*]
|
||||
pull_request: {}
|
||||
schedule:
|
||||
# This job can take a while, and we have usage limits, so just publish develop only twice a day
|
||||
- cron: "0 7/12 * * *"
|
||||
@@ -13,12 +12,9 @@ jobs:
|
||||
buildx:
|
||||
name: Docker Buildx
|
||||
runs-on: ubuntu-24.04
|
||||
environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }}
|
||||
environment: dockerhub
|
||||
permissions:
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
packages: write # needed for publishing packages to GHCR
|
||||
env:
|
||||
TEST_TAG: vectorim/element-web:test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -26,7 +22,6 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
|
||||
@@ -38,52 +33,16 @@ jobs:
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and load
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
tags: ${{ env.TEST_TAG }}
|
||||
|
||||
- name: Test the image
|
||||
run: |
|
||||
# Make a fake module to test the image
|
||||
MODULE_PATH="modules/module_name/index.js"
|
||||
mkdir -p $(dirname $MODULE_PATH)
|
||||
echo 'alert("Testing");' > $MODULE_PATH
|
||||
|
||||
# Spin up a container of the image
|
||||
CONTAINER_ID=$(docker run --rm -dp 80:80 -v $(pwd)/modules:/tmp/element-web-modules ${{ env.TEST_TAG }})
|
||||
|
||||
# Run some smoke tests
|
||||
wget --retry-connrefused --tries=5 -q --wait=3 --spider http://localhost:80/modules/module_name/index.js
|
||||
MODULE_1=$(curl http://localhost:80/config.json | jq -r .modules[0])
|
||||
test "$MODULE_1" = "/${MODULE_PATH}"
|
||||
|
||||
# Clean up
|
||||
docker stop "$CONTAINER_ID"
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
images: |
|
||||
vectorim/element-web
|
||||
ghcr.io/element-hq/element-web
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
@@ -92,8 +51,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
@@ -105,7 +63,6 @@ jobs:
|
||||
env:
|
||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
||||
TAGS: ${{ steps.meta.outputs.tags }}
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
images=""
|
||||
for tag in ${TAGS}; do
|
||||
@@ -115,7 +72,6 @@ jobs:
|
||||
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
||||
if: github.event_name != 'pull_request'
|
||||
continue-on-error: true
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
16
.github/workflows/end-to-end-tests.yaml
vendored
16
.github/workflows/end-to-end-tests.yaml
vendored
@@ -114,8 +114,6 @@ jobs:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- WebKit
|
||||
- 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
|
||||
@@ -124,10 +122,6 @@ jobs:
|
||||
project: Firefox
|
||||
- runAllTests: false
|
||||
project: WebKit
|
||||
- runAllTests: false
|
||||
project: Dendrite
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -172,12 +166,22 @@ jobs:
|
||||
|
||||
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
|
||||
- name: Run Playwright tests
|
||||
env:
|
||||
PWTEST_PROFILE_DIR: profile
|
||||
run: |
|
||||
yarn playwright test \
|
||||
--shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
|
||||
--project="${{ matrix.project }}" \
|
||||
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
|
||||
|
||||
- name: Upload profile
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: profile-${{ matrix.project }}-${{ matrix.runner }}
|
||||
path: profile
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
permissions:
|
||||
checks: read
|
||||
steps:
|
||||
- name: Wait for docker build
|
||||
- name: Wait for dockerhub
|
||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
with:
|
||||
ref: master
|
||||
|
||||
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@119b3320db3f04d89e91df840844b92d57ce3468
|
||||
uses: guibranco/github-status-action-v2@56cd38caf0615dd03f49d42ed301f1469911ac61
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
Normal file
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Close stale flaky issues
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
permissions: {}
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
only-labels: "Z-Flaky-Test"
|
||||
days-before-stale: 14
|
||||
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
|
||||
27
.github/workflows/triage-stale.yml
vendored
27
.github/workflows/triage-stale.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Close stale issues & PRs
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
permissions: {}
|
||||
jobs:
|
||||
close:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
operations-per-run: 100
|
||||
# Flaky test issue closing
|
||||
only-issue-labels: "Z-Flaky-Test"
|
||||
days-before-issue-stale: 14
|
||||
days-before-issue-close: 0
|
||||
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
||||
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
||||
# Stale PR closing
|
||||
days-before-pr-stale: 180
|
||||
days-before-pr-close: 0
|
||||
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,7 +26,6 @@ electron/pub
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/build_config.yaml
|
||||
/book
|
||||
/index.html
|
||||
|
||||
@@ -17,7 +17,6 @@ electron/pub
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/src/i18n/strings
|
||||
/build_config.yaml
|
||||
# Raises an error because it contains a template var breaking the script tag
|
||||
|
||||
@@ -33,15 +33,19 @@ module.exports = {
|
||||
"import-notation": null,
|
||||
"value-keyword-case": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
// useful for fallbacks
|
||||
{ ignore: ["consecutive-duplicates-with-different-values"] },
|
||||
],
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"selector-no-vendor-prefix": null,
|
||||
"media-feature-name-no-vendor-prefix": null,
|
||||
"number-max-precision": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"media-feature-range-notation": null,
|
||||
"declaration-property-value-no-unknown": null,
|
||||
"declaration-property-value-keyword-no-deprecated": null,
|
||||
"csstools/value-no-unknown-custom-properties": [
|
||||
true,
|
||||
{
|
||||
|
||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,84 +1,3 @@
|
||||
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy.
|
||||
* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh.
|
||||
* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh.
|
||||
* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot.
|
||||
* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros.
|
||||
* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros.
|
||||
* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR.
|
||||
* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent.
|
||||
* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros.
|
||||
* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR.
|
||||
* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy.
|
||||
* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros.
|
||||
* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros.
|
||||
* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros.
|
||||
* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd.
|
||||
* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr.
|
||||
* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy.
|
||||
* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown.
|
||||
* Revert `GoToHome` keyboard shortcut to `Ctrl`–`Shift`–`H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate.
|
||||
* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros.
|
||||
|
||||
|
||||
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
|
||||
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
|
||||
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
|
||||
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
|
||||
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
|
||||
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
|
||||
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
|
||||
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
|
||||
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
|
||||
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
|
||||
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
|
||||
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
|
||||
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
|
||||
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
|
||||
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
|
||||
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
|
||||
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
|
||||
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
|
||||
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
|
||||
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
|
||||
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
|
||||
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
|
||||
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
|
||||
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
|
||||
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
|
||||
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
|
||||
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
|
||||
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
|
||||
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
|
||||
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -189,6 +189,89 @@ give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||
from your life, please mail us your shipping address to matrix at matrix.org
|
||||
and we'll try to fix it :)
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
## Private sign off
|
||||
|
||||
If you would like to provide your legal name privately to the Matrix.org
|
||||
Foundation (instead of in a public commit or comment), you can do so by emailing
|
||||
your legal name and a link to the pull request to dco@matrix.org. It helps to
|
||||
include "sign off" or similar in the subject line. You will then be instructed
|
||||
further.
|
||||
|
||||
Once private sign off is complete, doing so for future contributions will not
|
||||
be required.
|
||||
|
||||
# Review expectations
|
||||
|
||||
See https://github.com/element-hq/element-meta/wiki/Review-process
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.7-labs
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
|
||||
|
||||
@@ -10,7 +8,7 @@ ARG JS_SDK_BRANCH="master"
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY --exclude=docker . /src
|
||||
COPY . /src
|
||||
RUN /src/scripts/docker-link-repos.sh
|
||||
RUN yarn --network-timeout=200000 install
|
||||
RUN /src/scripts/docker-package.sh
|
||||
@@ -21,15 +19,11 @@ RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
# App
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
# Install jq and moreutils for sponge, both used by our entrypoints
|
||||
RUN apk add jq moreutils
|
||||
|
||||
COPY --from=builder /src/webapp /app
|
||||
|
||||
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
||||
# through `envsubst` by the nginx docker image entry point.
|
||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
|
||||
|
||||
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
||||
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
||||
|
||||
120
README.md
120
README.md
@@ -182,11 +182,123 @@ Dockerfile.
|
||||
|
||||
# Development
|
||||
|
||||
Please read through the following:
|
||||
Before attempting to develop on Element you **must** read the [developer guide
|
||||
for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which
|
||||
also defines the design, architecture and style for Element too.
|
||||
|
||||
1. [Developer guide](./developer_guide.md)
|
||||
2. [Code style](./code_style.md)
|
||||
3. [Contribution guide](./CONTRIBUTING.md)
|
||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
||||
about where to start. Before starting work on a feature, it's best to ensure
|
||||
your plan aligns well with our vision for Element. Please chat with the team in
|
||||
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
||||
you start so we can ensure it's something we'd be willing to merge.
|
||||
|
||||
You should also familiarise yourself with the ["Here be Dragons" guide
|
||||
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
||||
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
||||
|
||||
The idea of Element is to be a relatively lightweight "skin" of customisations on
|
||||
top of the underlying `matrix-react-sdk`. `matrix-react-sdk` provides both the
|
||||
higher and lower level React components useful for building Matrix communication
|
||||
apps using React.
|
||||
|
||||
Please note that Element is intended to run correctly without access to the public
|
||||
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
||||
hosted by external CDNs or servers but instead please package all dependencies
|
||||
into Element itself.
|
||||
|
||||
# Setting up a dev environment
|
||||
|
||||
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
||||
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
||||
in git and to make local changes without having to manually rebuild each time.
|
||||
|
||||
First clone and build `matrix-js-sdk`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
popd
|
||||
```
|
||||
|
||||
Clone the repo and switch to the `element-web` directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/element-hq/element-web.git
|
||||
cd element-web
|
||||
```
|
||||
|
||||
Configure the app by copying `config.sample.json` to `config.json` and
|
||||
modifying it. See the [configuration docs](docs/config.md) for details.
|
||||
|
||||
Finally, build and start Element itself:
|
||||
|
||||
```bash
|
||||
yarn link matrix-js-sdk
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
Wait a few seconds for the initial build to finish; you should see something like:
|
||||
|
||||
```
|
||||
[element-js] <s> [webpack.Progress] 100%
|
||||
[element-js]
|
||||
[element-js] ℹ 「wdm」: 1840 modules
|
||||
[element-js] ℹ 「wdm」: Compiled successfully.
|
||||
```
|
||||
|
||||
Remember, the command will not terminate since it runs the web server
|
||||
and rebuilds source files when they change. This development server also
|
||||
disables caching, so do NOT use it in production.
|
||||
|
||||
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
||||
|
||||
**Note**: The build script uses inotify by default on Linux to monitor directories
|
||||
for changes. If the inotify limits are too low your build will fail silently or with
|
||||
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
||||
of at least `128M` and instance limit around `512`.
|
||||
|
||||
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
||||
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
||||
|
||||
To set a new inotify watch and instance limit, execute:
|
||||
|
||||
```
|
||||
sudo sysctl fs.inotify.max_user_watches=131072
|
||||
sudo sysctl fs.inotify.max_user_instances=512
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
If you wish, you can make the new limits permanent, by executing:
|
||||
|
||||
```
|
||||
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
||||
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
||||
|
||||
If any of these steps error with, `file table overflow`, you are probably on a mac
|
||||
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
||||
You'll need to do this in each new terminal you open before building Element.
|
||||
|
||||
## Running the tests
|
||||
|
||||
There are a number of application-level tests in the `tests` directory; these
|
||||
are designed to run with Jest and JSDOM. To run them
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
### End-to-End tests
|
||||
|
||||
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
||||
|
||||
# Translations
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ class MockMap extends EventEmitter {
|
||||
setCenter = jest.fn();
|
||||
setStyle = jest.fn();
|
||||
fitBounds = jest.fn();
|
||||
remove = jest.fn();
|
||||
}
|
||||
const MockMapInstance = new MockMap();
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ adjacent to. As of writing, these are:
|
||||
|
||||
- element-desktop
|
||||
- element-web
|
||||
- matrix-js-sdk
|
||||
|
||||
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
|
||||
has stricter code organization to reduce the maintenance burden. These projects will declare their code
|
||||
style within their own repos.
|
||||
|
||||
Note that some requirements will be layer-specific. Where the requirements don't make sense for the
|
||||
project, they are used to the best of their ability, used in spirit, or ignored if not applicable,
|
||||
in that order.
|
||||
|
||||
## Guiding principles
|
||||
|
||||
@@ -225,19 +234,17 @@ Unless otherwise specified, the following applies to all code:
|
||||
|
||||
Inheriting all the rules of TypeScript, the following additionally apply:
|
||||
|
||||
1. Component source files are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||
2. They are organised in a typically two-level hierarchy - first whether the component is a view or a structure, and then a broad functional grouping (e.g. 'rooms' here)
|
||||
3. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||
4. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||
1. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||
2. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||
empty if the component accepts no props.
|
||||
5. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||
6. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||
3. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||
4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||
instead.
|
||||
7. One component per file, except when a component is a utility component specifically for the "primary"
|
||||
5. One component per file, except when a component is a utility component specifically for the "primary"
|
||||
component. The utility component should not be exported.
|
||||
8. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
||||
6. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
||||
or stores.
|
||||
9. Stores should use a singleton pattern with a static instance property:
|
||||
7. Stores should use a singleton pattern with a static instance property:
|
||||
|
||||
```typescript
|
||||
class FooStore {
|
||||
@@ -254,41 +261,44 @@ Inheriting all the rules of TypeScript, the following additionally apply:
|
||||
}
|
||||
```
|
||||
|
||||
10. Stores must support using an alternative MatrixClient and dispatcher instance.
|
||||
11. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||
cycles during runtime where components accidentally include more of the app than they intended.
|
||||
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||
8. Stores must support using an alternative MatrixClient and dispatcher instance.
|
||||
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||
cycles during runtime where components accidentally include more of the app than they intended.
|
||||
10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||
if at all possible.
|
||||
13. A component should only use CSS class names in line with the component name.
|
||||
11. A component should only use CSS class names in line with the component name.
|
||||
|
||||
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
||||
|
||||
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
||||
12. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
||||
See above code example.
|
||||
15. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
||||
13. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
||||
be inline unless mocking/short-circuiting the value.
|
||||
16. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
||||
14. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
||||
which should be used.
|
||||
1. Unless the component is considered a "structure", in which case use classes.
|
||||
17. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
||||
15. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
||||
isolated components.
|
||||
18. Components should serve a single, or near-single, purpose.
|
||||
19. Prefer to derive information from component properties rather than establish state.
|
||||
20. Do not use `React.Component::forceUpdate`.
|
||||
16. Components should serve a single, or near-single, purpose.
|
||||
17. Prefer to derive information from component properties rather than establish state.
|
||||
18. Do not use `React.Component::forceUpdate`.
|
||||
|
||||
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
||||
|
||||
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
|
||||
|
||||
1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component).
|
||||
2. Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual.
|
||||
3. Class names must be prefixed with "mx\_".
|
||||
4. Class names must strictly denote the component which defines them.
|
||||
For example: `mx_MyFoo` for `MyFoo` component.
|
||||
5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name - e.g. .mx_MyFoo_randomDiv is how you'd name the class of an arbitrary div within the MyFoo view.
|
||||
6. Use the `$font` variables instead of manual values.
|
||||
7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||
8. Use the whole class name instead of shortcuts:
|
||||
1. Class names must be prefixed with "mx\_".
|
||||
2. Class names must denote the component which defines them, followed by any context.
|
||||
The context is not further specified here in terms of meaning or syntax.
|
||||
Use whatever is appropriate for your implementation use case.
|
||||
Some examples:
|
||||
1. `mx_MyFoo`
|
||||
2. `mx_MyFoo_avatar`
|
||||
3. `mx_MyFoo_avatarUser`
|
||||
4. `mx_MyFoo_avatar--user`
|
||||
3. Use the `$font` variables instead of manual values.
|
||||
4. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||
5. Use the whole class name instead of shortcuts:
|
||||
|
||||
```scss
|
||||
.mx_MyFoo {
|
||||
@@ -299,7 +309,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
||||
}
|
||||
```
|
||||
|
||||
9. Break multiple selectors over multiple lines this way:
|
||||
6. Break multiple selectors over multiple lines this way:
|
||||
|
||||
```scss
|
||||
.mx_MyFoo,
|
||||
@@ -309,9 +319,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
||||
}
|
||||
```
|
||||
|
||||
10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
||||
11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||
[documented](#comments) for what the values mean:
|
||||
7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
||||
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||
[documented](#comments) for what the values mean:
|
||||
|
||||
```scss
|
||||
.mx_MyFoo {
|
||||
@@ -321,9 +331,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
||||
}
|
||||
```
|
||||
|
||||
12. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
||||
13. The CSS for a component can override the rules for child components. For instance, .mxRoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough.
|
||||
14. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places.
|
||||
9. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -8,6 +8,6 @@ Package: element-web
|
||||
Architecture: all
|
||||
Recommends: httpd, element-io-archive-keyring
|
||||
Description:
|
||||
Element: the future of secure communication
|
||||
A feature-rich client for Matrix.org
|
||||
This package contains the web-based client that can be served through a web
|
||||
server.
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Developer Guide
|
||||
|
||||
## Development
|
||||
|
||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
||||
about where to start. Before starting work on a feature, it's best to ensure
|
||||
your plan aligns well with our vision for Element. Please chat with the team in
|
||||
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
||||
you start so we can ensure it's something we'd be willing to merge.
|
||||
|
||||
You should also familiarise yourself with the ["Here be Dragons" guide
|
||||
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
||||
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
||||
|
||||
Please note that Element is intended to run correctly without access to the public
|
||||
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
||||
hosted by external CDNs or servers but instead please package all dependencies
|
||||
into Element itself.
|
||||
|
||||
## Setting up a dev environment
|
||||
|
||||
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
||||
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
||||
in git and to make local changes without having to manually rebuild each time.
|
||||
|
||||
First clone and build `matrix-js-sdk`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
popd
|
||||
```
|
||||
|
||||
Clone the repo and switch to the `element-web` directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/element-hq/element-web.git
|
||||
cd element-web
|
||||
```
|
||||
|
||||
Configure the app by copying `config.sample.json` to `config.json` and
|
||||
modifying it. See the [configuration docs](docs/config.md) for details.
|
||||
|
||||
Finally, build and start Element itself:
|
||||
|
||||
```bash
|
||||
yarn link matrix-js-sdk
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
Wait a few seconds for the initial build to finish; you should see something like:
|
||||
|
||||
```
|
||||
[element-js] <s> [webpack.Progress] 100%
|
||||
[element-js]
|
||||
[element-js] ℹ 「wdm」: 1840 modules
|
||||
[element-js] ℹ 「wdm」: Compiled successfully.
|
||||
```
|
||||
|
||||
Remember, the command will not terminate since it runs the web server
|
||||
and rebuilds source files when they change. This development server also
|
||||
disables caching, so do NOT use it in production.
|
||||
|
||||
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
||||
|
||||
**Note**: The build script uses inotify by default on Linux to monitor directories
|
||||
for changes. If the inotify limits are too low your build will fail silently or with
|
||||
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
||||
of at least `128M` and instance limit around `512`.
|
||||
|
||||
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
||||
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
||||
|
||||
To set a new inotify watch and instance limit, execute:
|
||||
|
||||
```
|
||||
sudo sysctl fs.inotify.max_user_watches=131072
|
||||
sudo sysctl fs.inotify.max_user_instances=512
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
If you wish, you can make the new limits permanent, by executing:
|
||||
|
||||
```
|
||||
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
||||
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
||||
|
||||
If any of these steps error with, `file table overflow`, you are probably on a mac
|
||||
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
||||
You'll need to do this in each new terminal you open before building Element.
|
||||
|
||||
## Running the tests
|
||||
|
||||
There are a number of application-level tests in the `tests` directory; these
|
||||
are designed to run with Jest and JSDOM. To run them
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
### End-to-End tests
|
||||
|
||||
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
||||
|
||||
## General github guidelines
|
||||
|
||||
1. **Pull requests must only be filed against the `develop` branch.**
|
||||
2. Try to keep your pull requests concise. Split them up if necessary.
|
||||
3. Ensure that you provide a description that explains the fix/feature and its intent.
|
||||
|
||||
## Adding new code
|
||||
|
||||
New code should be committed as follows:
|
||||
|
||||
- All new components: https://github.com/element-hq/element-web/tree/develop/src/components
|
||||
- CSS: https://github.com/element-hq/element-web/tree/develop/res/css
|
||||
- Theme specific CSS & resources: https://github.com/element-hq/element-web/tree/develop/res/themes
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field
|
||||
|
||||
set -e
|
||||
|
||||
entrypoint_log() {
|
||||
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
||||
echo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Copy these config files as a base
|
||||
mkdir /tmp/element-web-config
|
||||
cp /app/config*.json /tmp/element-web-config/
|
||||
|
||||
# If there are modules to be loaded
|
||||
if [ -d "/tmp/element-web-modules" ]; then
|
||||
cd /tmp/element-web-modules
|
||||
|
||||
for MODULE in *
|
||||
do
|
||||
# If the module has a package.json, use its main field as the entrypoint
|
||||
ENTRYPOINT="index.js"
|
||||
if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then
|
||||
ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json")
|
||||
fi
|
||||
|
||||
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
|
||||
|
||||
# Append the module to the config
|
||||
jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
|
||||
done
|
||||
fi
|
||||
@@ -18,12 +18,8 @@ server {
|
||||
}
|
||||
# covers config.json and config.hostname.json requests as it is prefix.
|
||||
location /config {
|
||||
root /tmp/element-web-config;
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
location /modules {
|
||||
alias /tmp/element-web-modules;
|
||||
}
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
@@ -155,7 +155,7 @@ complete re-branding/private labeling, a more personalised experience can be ach
|
||||
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
||||
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
||||
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
||||
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
||||
20. `force_verification`: If true, users must verify new logins (eg. with another device / their security key)
|
||||
|
||||
### `desktop_builds` and `mobile_builds`
|
||||
|
||||
@@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
|
||||
2. `sync_timeline_limit`
|
||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
|
||||
|
||||
@@ -66,18 +66,6 @@ on other runtimes may require root privileges. To resolve this, either run the
|
||||
image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
||||
|
||||
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
|
||||
by being made available (e.g. via bind mount) in a directory within `/tmp/element-web-modules/`.
|
||||
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
|
||||
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
|
||||
|
||||
If you wish to use docker in read-only mode,
|
||||
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
|
||||
but additionally include the following directories:
|
||||
|
||||
- /tmp/element-web-config/
|
||||
- /etc/nginx/conf.d/
|
||||
|
||||
The behaviour of the docker image can be customised via the following
|
||||
environment variables:
|
||||
|
||||
|
||||
@@ -112,7 +112,3 @@ Unreliable in encrypted rooms.
|
||||
## Knock rooms (`feature_ask_to_join`) [In Development]
|
||||
|
||||
Enables knock feature for rooms. This allows users to ask to join a room.
|
||||
|
||||
## New room list (`feature_new_room_list`) [In Development]
|
||||
|
||||
Enable the new room list that is currently in development.
|
||||
|
||||
@@ -66,11 +66,11 @@ as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
||||
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. You can specify `synapseConfig` as such:
|
||||
in different configurations. You can specify `synapseConfigOptions` as such:
|
||||
|
||||
```typescript
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
synapseConfigOptions: {
|
||||
// The config options to pass to the Synapse instance
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,13 +8,11 @@
|
||||
|
||||
#### develop
|
||||
|
||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
|
||||
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
|
||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.
|
||||
|
||||
#### staging
|
||||
|
||||
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
|
||||
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
|
||||
|
||||
#### master
|
||||
|
||||
@@ -128,7 +126,7 @@ flowchart TD
|
||||
|
||||
subgraph Deploying
|
||||
D1[\Deploy staging.element.io/]
|
||||
D2[\Check docker build/]
|
||||
D2[\Check dockerhub/]
|
||||
D3[\Deploy app.element.io/]
|
||||
D4[\Check desktop package/]
|
||||
|
||||
@@ -213,11 +211,11 @@ switched back to the version of the dependency from the master branch to not lea
|
||||
# Deploying
|
||||
|
||||
We ship the SDKs to npm, this happens as part of the release process.
|
||||
We ship Element Web to dockerhub, ghcr.io, `*.element.io`, and packages.element.io.
|
||||
We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
||||
We ship Element Desktop to packages.element.io.
|
||||
|
||||
- [ ] Check that element-web has shipped to dockerhub & ghcr.io
|
||||
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
||||
- [ ] Check that element-web has shipped to dockerhub
|
||||
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
||||
- [ ] Test staging.element.io
|
||||
|
||||
For final releases additionally do these steps:
|
||||
@@ -227,9 +225,6 @@ For final releases additionally do these steps:
|
||||
- [ ] Ensure Element Web package has shipped to packages.element.io
|
||||
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
||||
|
||||
If you need to roll back a deployment to staging.element.io,
|
||||
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
|
||||
|
||||
# Housekeeping
|
||||
|
||||
We have some manual housekeeping to do in order to prepare for the next release.
|
||||
|
||||
@@ -38,8 +38,6 @@ const config: Config = {
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
// Requires ESM which is incompatible with our current Jest setup
|
||||
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||
collectCoverageFrom: [
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as fs from "fs";
|
||||
import * as childProcess from "child_process";
|
||||
import * as semver from "semver";
|
||||
|
||||
import { type BuildConfig } from "./BuildConfig";
|
||||
import { BuildConfig } from "./BuildConfig";
|
||||
|
||||
// This expects to be run from ./scripts/install.ts
|
||||
|
||||
@@ -23,9 +23,10 @@ const MODULES_TS_HEADER = `
|
||||
* You are not a salmon.
|
||||
*/
|
||||
|
||||
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||
`;
|
||||
const MODULES_TS_DEFINITIONS = `
|
||||
export const INSTALLED_MODULES = [];
|
||||
export const INSTALLED_MODULES: RuntimeModule[] = [];
|
||||
`;
|
||||
|
||||
export function installer(config: BuildConfig): void {
|
||||
@@ -77,8 +78,8 @@ export function installer(config: BuildConfig): void {
|
||||
return; // hit the finally{} block before exiting
|
||||
}
|
||||
|
||||
// If we reach here, everything seems fine. Write modules.js and log some output
|
||||
// Note: we compile modules.js in two parts for developer friendliness if they
|
||||
// If we reach here, everything seems fine. Write modules.ts and log some output
|
||||
// Note: we compile modules.ts in two parts for developer friendliness if they
|
||||
// happen to look at it.
|
||||
console.log("The following modules have been installed: ", installedModules);
|
||||
let modulesTsHeader = MODULES_TS_HEADER;
|
||||
@@ -192,5 +193,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
|
||||
}
|
||||
|
||||
function writeModulesTs(content: string): void {
|
||||
fs.writeFileSync("./src/modules.js", content, "utf-8");
|
||||
fs.writeFileSync("./src/modules.ts", content, "utf-8");
|
||||
}
|
||||
|
||||
31
package.json
31
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.93",
|
||||
"description": "Element: the future of secure communication",
|
||||
"version": "1.11.90",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -74,25 +74,24 @@
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001697",
|
||||
"caniuse-lite": "1.0.30001690",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "^0.1.1",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@matrix-org/analytics-events": "^0.29.0",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.3",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^8.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^3.0.0",
|
||||
"@vector-im/compound-web": "^7.6.1",
|
||||
"@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",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
@@ -128,7 +127,7 @@
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "37.0.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.10.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
@@ -179,8 +178,8 @@
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@sentry/webpack-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@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",
|
||||
@@ -231,14 +230,13 @@
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-deprecate": "0.8.5",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"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",
|
||||
@@ -257,7 +255,7 @@
|
||||
"jsqr": "^1.4.0",
|
||||
"knip": "^5.36.2",
|
||||
"lint-staged": "^15.0.2",
|
||||
"mailpit-api": "^1.0.5",
|
||||
"mailhog": "^4.16.0",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimist": "^1.2.6",
|
||||
@@ -281,21 +279,20 @@
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^37.0.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",
|
||||
"typescript": "5.7.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": "^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,25 +8,19 @@ 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";
|
||||
|
||||
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>({
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: "Chrome",
|
||||
use: {
|
||||
...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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -54,22 +48,6 @@ export default defineConfig<Options>({
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
{
|
||||
name: "Dendrite",
|
||||
use: {
|
||||
...chromeProject,
|
||||
homeserverType: "dendrite",
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
{
|
||||
name: "Pinecone",
|
||||
use: {
|
||||
...chromeProject,
|
||||
homeserverType: "pinecone",
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
],
|
||||
use: {
|
||||
viewport: { width: 1280, height: 720 },
|
||||
|
||||
@@ -11,15 +11,7 @@ import type { Locator, Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { type 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();
|
||||
};
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
test.use({
|
||||
@@ -230,7 +222,8 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
// Find and click "Reply" button on MessageActionBar
|
||||
const tile = page.locator(".mx_EventTile_last");
|
||||
await clickButtonReply(tile);
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
@@ -258,12 +251,18 @@ 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.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(tile);
|
||||
await clickButtonReply();
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||
@@ -271,7 +270,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(tile);
|
||||
await clickButtonReply();
|
||||
|
||||
// Reply to the player with yet another audio file to create a reply chain
|
||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe("Create Room", () => {
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||
await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/);
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
await expect(header).toContainText(name);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { registerAccountMas } from "../oidc";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { TestClientServerAPI } from "../csAPI";
|
||||
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
|
||||
import { checkDeviceIsConnectedKeyBackup } from "./utils";
|
||||
|
||||
// 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,
|
||||
@@ -20,22 +19,19 @@ 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, mailpitClient, app }, testInfo) => {
|
||||
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, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
await registerAccountMas(page, mailhogClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
// Wait for the ui to load
|
||||
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
|
||||
|
||||
// Recovery is not set up yet
|
||||
await checkDeviceIsConnectedKeyBackup(app, "1", true, false);
|
||||
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, mailpitClient, app }, testInfo) => {
|
||||
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, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
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();
|
||||
@@ -51,7 +47,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
||||
|
||||
test("Key backup is disabled when reset from elsewhere", async ({
|
||||
page,
|
||||
mailpitClient,
|
||||
mailhogClient,
|
||||
request,
|
||||
homeserver,
|
||||
}, testInfo) => {
|
||||
@@ -64,7 +60,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
||||
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
|
||||
await registerAccountMas(page, mailhogClient, testUsername, "alice@email.com", testPassword);
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
|
||||
@@ -9,8 +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 { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
@@ -21,7 +19,6 @@ async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
}
|
||||
|
||||
test.describe("Backups", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
@@ -36,7 +33,19 @@ test.describe("Backups", () => {
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const securityKey = await app.getClipboard();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -51,15 +60,14 @@ test.describe("Backups", () => {
|
||||
await expectBackupVersionToBe(page, "1");
|
||||
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Delete it
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||
|
||||
// Create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// Should be successful
|
||||
@@ -90,8 +98,8 @@ test.describe("Backups", () => {
|
||||
|
||||
// Try to create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
||||
// But cancel the security key dialog, to simulate not having the secret storage passphrase
|
||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
||||
|
||||
@@ -8,10 +8,8 @@ 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",
|
||||
});
|
||||
|
||||
@@ -8,17 +8,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import {
|
||||
autoJoin,
|
||||
completeCreateSecretStorageDialog,
|
||||
copyAndContinue,
|
||||
createSharedRoomWithUser,
|
||||
enableKeyBackup,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
const checkDMRoom = async (page: Page) => {
|
||||
const body = page.locator(".mx_RoomView_body");
|
||||
@@ -75,7 +67,6 @@ 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: {
|
||||
@@ -118,7 +109,18 @@ test.describe("Cryptography", function () {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
await copyAndContinue(page);
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step; however, it
|
||||
// can be quite quick, and playwright can miss it, so we can't test for it.
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
@@ -186,7 +188,7 @@ test.describe("Cryptography", function () {
|
||||
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
|
||||
|
||||
// Enter the 4S key
|
||||
await page.getByPlaceholder("Recovery Key").fill(secretStorageKey);
|
||||
await page.getByPlaceholder("Security Key").fill(secretStorageKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Enter the password
|
||||
|
||||
@@ -28,8 +28,6 @@ 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,
|
||||
|
||||
@@ -6,14 +6,19 @@ 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 } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
|
||||
import { type Client } from "../../pages/client.ts";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
import { Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
|
||||
function getMemberTileByName(page: Page, name: string): Locator {
|
||||
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
|
||||
}
|
||||
|
||||
test.use({
|
||||
displayName: NAME,
|
||||
synapseConfig: {
|
||||
@@ -39,7 +44,7 @@ test.use({
|
||||
test.describe("Dehydration", () => {
|
||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -48,99 +53,47 @@ test.describe("Dehydration", () => {
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
// The Security tab should indicate that there is a dehydrated device present
|
||||
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
|
||||
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// the dehydrated device gets created with the name "Dehydrated
|
||||
// device". We want to make sure that it is not visible as a normal
|
||||
// device.
|
||||
const sessionsTab = await app.settings.openUserSettings("Sessions");
|
||||
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
|
||||
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
|
||||
|
||||
// First it displays an informative panel about the recovery key
|
||||
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Next, it displays the new recovery key. We click on the copy button.
|
||||
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
|
||||
const recoveryKey = await app.getClipboard();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(
|
||||
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
|
||||
).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
|
||||
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
|
||||
|
||||
await app.settings.closeDialog();
|
||||
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
});
|
||||
// now check that the user info right-panel shows the dehydrated device
|
||||
// as a feature rather than as a normal device
|
||||
await app.client.createRoom({ name: ROOM_NAME });
|
||||
|
||||
test("Reset recovery key during login re-creates dehydrated device", async ({
|
||||
page,
|
||||
homeserver,
|
||||
app,
|
||||
credentials,
|
||||
}) => {
|
||||
// Set up cross-signing and recovery
|
||||
const { botClient } = await createBot(page, homeserver, credentials);
|
||||
// ... and dehydration
|
||||
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
|
||||
expect(initialDehydratedDeviceIds.length).toBe(1);
|
||||
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
|
||||
await botClient.evaluate(async (client) => client.stopClient());
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
|
||||
|
||||
// Log in our client
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Oh no, we forgot our recovery key
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
|
||||
|
||||
// There should be a brand new dehydrated device
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
expect(dehydratedDeviceIds.length).toBe(1);
|
||||
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
||||
return await client.evaluate(async (client) => {
|
||||
const userId = client.getUserId();
|
||||
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
|
||||
return Array.from(
|
||||
devices
|
||||
.get(userId)
|
||||
.values()
|
||||
.filter((d) => d.dehydrated)
|
||||
.map((d) => d.deviceId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait for our user to have a dehydrated device */
|
||||
async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise<void> {
|
||||
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
|
||||
//
|
||||
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
return dehydratedDeviceIds.length;
|
||||
})
|
||||
.toEqual(1);
|
||||
}
|
||||
|
||||
@@ -15,12 +15,11 @@ import {
|
||||
awaitVerifier,
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
doTwoWaySasVerification,
|
||||
logIntoElement,
|
||||
waitForVerificationRequest,
|
||||
} from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { Bot } from "../../pages/bot";
|
||||
|
||||
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
let aliceBotClient: Bot;
|
||||
@@ -29,9 +28,29 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
const res = await createBot(page, homeserver, credentials, true);
|
||||
aliceBotClient = res.botClient;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
// 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);
|
||||
});
|
||||
|
||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||
@@ -68,8 +87,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||
// as we need to wait for the secret gossiping to happen.
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
@@ -112,14 +131,16 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||
});
|
||||
|
||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Select the security phrase
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||
|
||||
// Fill the passphrase
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
@@ -133,18 +154,18 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Select the security phrase
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||
|
||||
// Fill the recovery key
|
||||
// Fill the security key
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
await dialog.getByRole("button", { name: "use your Recovery Key" }).click();
|
||||
await dialog.getByRole("button", { name: "use your Security Key" }).click();
|
||||
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
|
||||
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
|
||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
@@ -156,7 +177,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
||||
@@ -198,10 +219,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
/* And we're all done! */
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
// 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 expect(
|
||||
infoDialog.getByText(`You've successfully verified (${aliceBotClient.credentials.deviceId})!`),
|
||||
).toBeVisible();
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Locator } from "@playwright/test";
|
||||
import { Locator } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import {
|
||||
@@ -17,10 +17,9 @@ import {
|
||||
logIntoElement,
|
||||
logOutOfElement,
|
||||
verify,
|
||||
waitForDevices,
|
||||
} from "./utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
|
||||
test.describe("Cryptography", function () {
|
||||
test.use({
|
||||
@@ -67,9 +66,6 @@ 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",
|
||||
@@ -145,8 +141,25 @@ test.describe("Cryptography", function () {
|
||||
// bob deletes his second device
|
||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||
|
||||
// wait for the logout to propagate.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
// wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info.
|
||||
async function awaitOneDevice(iterations = 1) {
|
||||
const rightPanel = page.locator(".mx_RightPanel");
|
||||
await rightPanel.getByTestId("base-card-back-button").click();
|
||||
await rightPanel.getByText("Bob").click();
|
||||
const sessionCountText = await rightPanel
|
||||
.locator(".mx_UserInfo_devices")
|
||||
.getByText(" session", { exact: false })
|
||||
.textContent();
|
||||
// cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here
|
||||
if (sessionCountText != "1 session" && sessionCountText != "1 verified session") {
|
||||
if (iterations >= 10) {
|
||||
throw new Error(`Bob still has ${sessionCountText} after 10 iterations`);
|
||||
}
|
||||
await awaitOneDevice(iterations + 1);
|
||||
}
|
||||
}
|
||||
|
||||
await awaitOneDevice();
|
||||
|
||||
// close and reopen the room, to get the shield to update.
|
||||
await app.viewRoomByName("Bob");
|
||||
@@ -269,7 +282,11 @@ test.describe("Cryptography", function () {
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
||||
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
||||
// his user info.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
await app.toggleRoomInfoPanel();
|
||||
const rightPanel = page.locator(".mx_RightPanel");
|
||||
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
|
||||
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
|
||||
|
||||
// Our app is blocked from syncing while Bob sends his messages.
|
||||
await app.client.network.goOffline();
|
||||
|
||||
@@ -8,10 +8,8 @@ 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, credentials);
|
||||
});
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
|
||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
});
|
||||
|
||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||
|
||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -8,9 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
|
||||
import { type Client } from "../../pages/client";
|
||||
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
|
||||
import { Client } from "../../pages/client";
|
||||
|
||||
test.describe("User verification", () => {
|
||||
// note that there are other tests that check user verification works in `crypto.spec.ts`.
|
||||
@@ -32,17 +33,13 @@ test.describe("User verification", () => {
|
||||
});
|
||||
|
||||
test("can receive a verification request when there is no existing DM", async ({
|
||||
app,
|
||||
page,
|
||||
bot: bob,
|
||||
user: aliceCredentials,
|
||||
toasts,
|
||||
room: { roomId: dmRoomId },
|
||||
}) => {
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||
const avatar = page.getByRole("button", { name: "Avatar" });
|
||||
await avatar.click();
|
||||
await waitForDeviceKeys(page);
|
||||
|
||||
// once Alice has joined, Bob starts the verification
|
||||
const bobVerificationRequest = await bob.evaluateHandle(
|
||||
@@ -87,17 +84,13 @@ test.describe("User verification", () => {
|
||||
});
|
||||
|
||||
test("can abort emoji verification when emoji mismatch", async ({
|
||||
app,
|
||||
page,
|
||||
bot: bob,
|
||||
user: aliceCredentials,
|
||||
toasts,
|
||||
room: { roomId: dmRoomId },
|
||||
}) => {
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||
const avatar = page.getByRole("button", { name: "Avatar" });
|
||||
await avatar.click();
|
||||
await waitForDeviceKeys(page);
|
||||
|
||||
// once Alice has joined, Bob starts the verification
|
||||
const bobVerificationRequest = await bob.evaluateHandle(
|
||||
@@ -161,3 +154,15 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until we get the other user's device keys.
|
||||
* In newer rust-crypto versions, the verification request will be ignored if we
|
||||
* don't have the sender's device keys.
|
||||
*/
|
||||
async function waitForDeviceKeys(page: Page): Promise<void> {
|
||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||
const avatar = await page.getByRole("button", { name: "Avatar" });
|
||||
await avatar.click();
|
||||
await expect(page.getByText("1 session")).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -6,66 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, type JSHandle, type Page } from "@playwright/test";
|
||||
import { expect, JSHandle, type Page } from "@playwright/test";
|
||||
|
||||
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type {
|
||||
CryptoEvent,
|
||||
EmojiMapping,
|
||||
GeneratedSecretStorageKey,
|
||||
ShowSasCallbacks,
|
||||
VerificationRequest,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { type Client } from "../../pages/client";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
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
|
||||
* @param usePassphrase - whether to use a passphrase when creating the recovery key
|
||||
*/
|
||||
export async function createBot(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
credentials: Credentials,
|
||||
usePassphrase = false,
|
||||
): 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,
|
||||
usePassphrase,
|
||||
});
|
||||
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
|
||||
*
|
||||
@@ -142,16 +98,14 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
|
||||
* Check that the current device is connected to the expected key backup.
|
||||
* Also checks that the decryption key is known and cached locally.
|
||||
*
|
||||
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
||||
* @param page - the page to check
|
||||
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
||||
* @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
|
||||
* @param checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
|
||||
* @param checkBackupKeyInCache - whether to check that the backup key is cached locally.
|
||||
*/
|
||||
export async function checkDeviceIsConnectedKeyBackup(
|
||||
app: ElementAppPage,
|
||||
page: Page,
|
||||
expectedBackupVersion: string,
|
||||
checkBackupPrivateKeyInCache: boolean,
|
||||
checkBackupKeyIn4S: boolean = true,
|
||||
checkBackupKeyInCache: boolean,
|
||||
): Promise<void> {
|
||||
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
||||
if (!expectedBackupVersion) {
|
||||
@@ -160,48 +114,23 @@ export async function checkDeviceIsConnectedKeyBackup(
|
||||
);
|
||||
}
|
||||
|
||||
const backupData = await app.client.evaluate(async (client: MatrixClient) => {
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) return;
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
|
||||
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
|
||||
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored());
|
||||
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
||||
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
|
||||
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
|
||||
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
|
||||
|
||||
return {
|
||||
backupInfo,
|
||||
hasBackupPrivateKeyFromCache,
|
||||
backupPrivateKeyWellFormed,
|
||||
backupKeyIn4S,
|
||||
activeBackupVersion,
|
||||
};
|
||||
});
|
||||
|
||||
if (!backupData) {
|
||||
throw new Error("Crypto module is not available");
|
||||
if (checkBackupKeyInCache) {
|
||||
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
|
||||
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
|
||||
}
|
||||
|
||||
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } =
|
||||
backupData;
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||
);
|
||||
|
||||
// We have a key backup
|
||||
expect(backupInfo).toBeDefined();
|
||||
// The key backup version is as expected
|
||||
expect(backupInfo.version).toBe(expectedBackupVersion);
|
||||
// The active backup version is as expected
|
||||
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
||||
// The backup key is stored in 4S
|
||||
if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
|
||||
|
||||
if (checkBackupPrivateKeyInCache) {
|
||||
// The backup key is available locally
|
||||
expect(hasBackupPrivateKeyFromCache).toBe(true);
|
||||
// The backup key is well-formed
|
||||
expect(backupPrivateKeyWellFormed).toBe(true);
|
||||
}
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,13 +147,8 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
||||
|
||||
// if a securityKey was given, verify the new device
|
||||
if (securityKey !== undefined) {
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||
if (await useSecurityKey.isVisible()) {
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
// Fill in the recovery key
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
// Fill in the security key
|
||||
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
@@ -251,19 +175,18 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the encryption settings, and verify the current session using the recovery key.
|
||||
* Open the security settings, and verify the current session using the security key.
|
||||
*
|
||||
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
||||
* @param securityKey - The recovery key (i.e., 4S key), set up during a previous session.
|
||||
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
|
||||
*/
|
||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
const settings = await app.settings.openUserSettings("Security & Privacy");
|
||||
await settings.getByRole("button", { name: "Verify this session" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await app.page.getByRole("button", { name: "Done" }).click();
|
||||
await app.settings.closeDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,61 +216,28 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
*
|
||||
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
||||
*
|
||||
* Returns the recovery key
|
||||
* Returns the security key
|
||||
*/
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
const dialog = app.page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
|
||||
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
// copy the text ourselves
|
||||
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
|
||||
await copyAndContinue(app.page);
|
||||
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
return securityKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
||||
*
|
||||
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
|
||||
*
|
||||
* @param page - The playwright `Page` fixture.
|
||||
* @param opts - Options object
|
||||
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
|
||||
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
|
||||
*
|
||||
* @returns the new recovery key.
|
||||
*/
|
||||
export async function completeCreateSecretStorageDialog(
|
||||
page: Page,
|
||||
opts?: { accountPassword?: string },
|
||||
): Promise<string> {
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
// "Generate a Recovery Key" is selected by default
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Recovery Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step.
|
||||
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
|
||||
// the step is quite quick, and playwright can miss it, so we can't test for it.
|
||||
if (opts && Object.hasOwn(opts, "accountPassword")) {
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
|
||||
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
}
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on copy and continue buttons to dismiss the recovery key dialog
|
||||
* Click on copy and continue buttons to dismiss the security key dialog
|
||||
*/
|
||||
export async function copyAndContinue(page: Page) {
|
||||
await page.getByRole("button", { name: "Copy" }).click();
|
||||
@@ -482,53 +372,3 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the given user has a given number of devices.
|
||||
* This function will check the device keys ten times and if
|
||||
* the expected number of devices were not found by then, an
|
||||
* error is thrown.
|
||||
*/
|
||||
export async function waitForDevices(
|
||||
app: ElementAppPage,
|
||||
userId: string,
|
||||
expectedNumberOfDevices: number,
|
||||
): Promise<void> {
|
||||
const result = await app.client.evaluate(
|
||||
async (cli, { userId, expectedNumberOfDevices }) => {
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], true);
|
||||
const deviceMap = userDeviceMap?.get(userId);
|
||||
if (deviceMap.size === expectedNumberOfDevices) return true;
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ userId, expectedNumberOfDevices },
|
||||
);
|
||||
if (!result) {
|
||||
throw new Error(`User ${userId} did not have ${expectedNumberOfDevices} devices within ten iterations!`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type APIRequestContext } from "playwright-core";
|
||||
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { APIRequestContext } from "playwright-core";
|
||||
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { type HomeserverInstance } from "../plugins/homeserver";
|
||||
import { HomeserverInstance } from "../plugins/homeserver";
|
||||
import { ClientServerApi } from "../plugins/utils/api.ts";
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,13 +6,12 @@ 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 Locator, type Page } from "@playwright/test";
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
|
||||
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
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, {
|
||||
@@ -32,8 +31,6 @@ 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");
|
||||
|
||||
@@ -10,7 +10,7 @@ import { expect, test as base } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { type Credentials } from "../../plugins/homeserver";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
|
||||
const email = "user@nowhere.dummy";
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@ 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"],
|
||||
@@ -81,7 +79,6 @@ 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,10 +13,8 @@ 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"],
|
||||
@@ -284,7 +282,6 @@ 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,10 +10,8 @@ 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"],
|
||||
|
||||
@@ -10,12 +10,8 @@ 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 { type 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({
|
||||
@@ -39,18 +35,12 @@ 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,
|
||||
user: Credentials,
|
||||
bob: Bot,
|
||||
charlies: Bot[],
|
||||
) {
|
||||
const alias = `#lltest:${user.homeServer}`;
|
||||
async function setupRoomWithBobAliceAndCharlies(page: Page, app: ElementAppPage, bob: Bot, charlies: Bot[]) {
|
||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
||||
roomId = await bob.createRoom({
|
||||
name,
|
||||
@@ -105,13 +95,7 @@ test.describe("Lazy Loading", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function joinCharliesWhileAliceIsOffline(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
user: Credentials,
|
||||
charlies: Bot[],
|
||||
) {
|
||||
const alias = `#lltest:${user.homeServer}`;
|
||||
async function joinCharliesWhileAliceIsOffline(page: Page, app: ElementAppPage, charlies: Bot[]) {
|
||||
await app.client.network.goOffline();
|
||||
for (const charly of charlies) {
|
||||
await charly.joinRoom(alias);
|
||||
@@ -123,19 +107,19 @@ test.describe("Lazy Loading", () => {
|
||||
await app.client.waitForNextSync();
|
||||
}
|
||||
|
||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot, user }) => {
|
||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot }) => {
|
||||
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, user, bot, charly1to5);
|
||||
await setupRoomWithBobAliceAndCharlies(page, app, 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, user, charly6to10);
|
||||
await joinCharliesWhileAliceIsOffline(page, app, charly6to10);
|
||||
await checkMemberList(page, charly6to10);
|
||||
|
||||
for (const charly of charlies) {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Search section of the room list", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the search section of the room list
|
||||
* @param page
|
||||
*/
|
||||
function getSearchSection(page: Page) {
|
||||
return page.getByRole("search");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const searchSection = getSearchSection(page);
|
||||
// exact=false to ignore the shortcut which is related to the OS
|
||||
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
|
||||
await expect(searchSection).toMatchScreenshot("search-section.png");
|
||||
});
|
||||
|
||||
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
|
||||
const searchSection = getSearchSection(page);
|
||||
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
|
||||
// The spotlight should be displayed
|
||||
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
|
||||
const searchSection = getSearchSection(page);
|
||||
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
|
||||
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
|
||||
// The room directory should be displayed
|
||||
await expect(dialog).toBeVisible();
|
||||
// The public room filter should be displayed
|
||||
await expect(dialog.getByText("Public rooms")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Search section of the room list", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list view
|
||||
* @param page
|
||||
*/
|
||||
function getRoomListView(page: Page) {
|
||||
return page.getByTestId("room-list-view");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
});
|
||||
|
||||
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomListView(page);
|
||||
await expect(roomListView).toMatchScreenshot("room-list-view.png");
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
|
||||
@@ -6,13 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "playwright-core";
|
||||
import { Page } from "playwright-core";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
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";
|
||||
@@ -114,8 +113,6 @@ test.use({
|
||||
|
||||
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,
|
||||
|
||||
@@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page, expect, type TestInfo } from "@playwright/test";
|
||||
import { Page, expect, TestInfo } from "@playwright/test";
|
||||
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
|
||||
/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element
|
||||
*/
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { type Locator, type Page } from "playwright-core";
|
||||
import { Locator, Page } from "playwright-core";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
@@ -58,16 +58,6 @@ 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" },
|
||||
@@ -89,10 +79,9 @@ 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`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -100,17 +89,14 @@ 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`, screenshotOptions());
|
||||
await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`);
|
||||
});
|
||||
|
||||
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`,
|
||||
screenshotOptions(),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rich-ltr-${direction}displayname.png`);
|
||||
});
|
||||
|
||||
test("should render an edited LTR message", async ({ page, user, app, room }) => {
|
||||
@@ -120,10 +106,9 @@ test.describe("Message rendering", () => {
|
||||
|
||||
await editMessage(page, msgTile, "Hello, universe!");
|
||||
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`edited-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`edited-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should render a reply of a LTR message", async ({ page, user, app, room }) => {
|
||||
@@ -137,37 +122,32 @@ test.describe("Message rendering", () => {
|
||||
]);
|
||||
|
||||
await replyMessage(page, msgTile, "response to multiline message");
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`reply-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`reply-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
|
||||
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`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`basic-message-rtl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
|
||||
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`, screenshotOptions());
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`);
|
||||
});
|
||||
|
||||
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`,
|
||||
screenshotOptions(),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rich-rtl-${direction}displayname.png`);
|
||||
});
|
||||
|
||||
test("should render an edited RTL message", async ({ page, user, app, room }) => {
|
||||
@@ -177,10 +157,9 @@ test.describe("Message rendering", () => {
|
||||
|
||||
await editMessage(page, msgTile, "مرحبا بالكون!");
|
||||
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`edited-message-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`edited-message-rtl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should render a reply of a RTL message", async ({ page, user, app, room }) => {
|
||||
@@ -194,10 +173,9 @@ test.describe("Message rendering", () => {
|
||||
]);
|
||||
|
||||
await replyMessage(page, msgTile, "مرحبا بالعالم!");
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`reply-message-trl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
await expect(msgTile).toMatchScreenshot(`reply-message-trl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Module loading", () => {
|
||||
test.use({
|
||||
displayName: "Manny",
|
||||
});
|
||||
|
||||
test.describe("Example Module", () => {
|
||||
test.use({
|
||||
config: {
|
||||
modules: ["/modules/example-module.js"],
|
||||
},
|
||||
page: async ({ page }, use) => {
|
||||
await page.route("/modules/example-module.js", async (route) => {
|
||||
await route.fulfill({ path: "playwright/sample-files/example-module.js" });
|
||||
});
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
|
||||
test("should show alert", async ({ page }) => {
|
||||
const dialogPromise = page.waitForEvent("dialog");
|
||||
await page.goto("/");
|
||||
const dialog = await dialogPromise;
|
||||
expect(dialog.message()).toBe("Testing module loading successful!");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MailpitClient } from "mailpit-api";
|
||||
import { type Page } from "@playwright/test";
|
||||
import { API, Messages } from "mailhog";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { expect } from "../../element-web-test";
|
||||
|
||||
export async function registerAccountMas(
|
||||
page: Page,
|
||||
mailpit: MailpitClient,
|
||||
mailhog: API,
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
@@ -27,13 +27,13 @@ export async function registerAccountMas(
|
||||
await page.getByRole("textbox", { name: "Confirm Password" }).fill(password);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
let code: string;
|
||||
let messages: Messages;
|
||||
await expect(async () => {
|
||||
const messages = await mailpit.listMessages();
|
||||
expect(messages.messages[0].To[0].Address).toEqual(email);
|
||||
const text = await mailpit.renderMessageText(messages.messages[0].ID);
|
||||
[, code] = text.match(/Your verification code to confirm this email address is: (\d{6})/);
|
||||
messages = await mailhog.messages();
|
||||
expect(messages.items).toHaveLength(1);
|
||||
}).toPass();
|
||||
expect(messages.items[0].to).toEqual(`${username} <${email}>`);
|
||||
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();
|
||||
|
||||
@@ -9,17 +9,19 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { test, expect } from "../../element-web-test.ts";
|
||||
import { registerAccountMas } from ".";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
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,
|
||||
mailpitClient,
|
||||
mailhogClient,
|
||||
mas,
|
||||
}, testInfo) => {
|
||||
await page.clock.install();
|
||||
@@ -33,7 +35,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
const userId = `alice_${testInfo.testId}`;
|
||||
await registerAccountMas(page, mailpitClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
await registerAccountMas(page, mailhogClient, userId, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
// Eventually, we should end up at the home screen.
|
||||
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
|
||||
|
||||
@@ -8,16 +8,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import { type Credentials } from "../../plugins/homeserver";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
|
||||
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, testInfo) => {
|
||||
|
||||
@@ -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:${user.homeServer}`;
|
||||
const danielleId = "@danielle:localhost";
|
||||
|
||||
const room1Id = await app.client.createRoom({ name: room1Name });
|
||||
const room2Id = await app.client.createRoom({ name: room2Name });
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import { type Client } from "../../pages/client";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { Client } from "../../pages/client";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Bot } from "../../pages/bot";
|
||||
|
||||
type RoomRef = { name: string; roomId: string };
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import type { Bot } from "../../pages/bot";
|
||||
import type { Client } from "../../pages/client";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
test.describe("Poll history", () => {
|
||||
type CreatePollOptions = {
|
||||
|
||||
@@ -11,11 +11,8 @@ 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,11 +9,8 @@ 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,11 +9,8 @@ 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,11 +9,8 @@ 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,10 +9,8 @@ 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", () => {
|
||||
|
||||
@@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
import type { JSHandle, Page } from "@playwright/test";
|
||||
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { type Client } from "../../pages/client";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { Client } from "../../pages/client";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
type RoomRef = { name: string; roomId: string };
|
||||
|
||||
@@ -526,10 +526,9 @@ class Helpers {
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await threadPanel.evaluate(($panel) => {
|
||||
const $button = $panel.querySelector<HTMLElement>('[data-testid="base-card-back-button"]');
|
||||
const title = $panel.querySelector<HTMLElement>(".mx_BaseCard_header_title")?.textContent;
|
||||
// If the Threads back button is present then click it - the
|
||||
// threads button can open either threads list or thread panel
|
||||
if ($button && title !== "Threads") {
|
||||
if ($button) {
|
||||
$button.click();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,11 +9,8 @@ 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,11 +9,8 @@ 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,11 +9,8 @@ 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 ({
|
||||
@@ -57,8 +54,8 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
await util.openThread("ThreadRoot");
|
||||
|
||||
// Then the thread root is marked as read in the main timeline,
|
||||
// 30 remaining messages are unread - 6 messages are displayed under the thread root
|
||||
await util.assertUnread(room2, 30 - 6);
|
||||
// 30 remaining messages are unread - 7 messages are displayed under the thread root
|
||||
await util.assertUnread(room2, 30 - 7);
|
||||
});
|
||||
|
||||
test("Creating a new thread based on a reply makes the room unread", async ({
|
||||
|
||||
@@ -9,11 +9,8 @@ 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 ({
|
||||
|
||||
@@ -9,11 +9,8 @@ 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,10 +9,8 @@ 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 ({
|
||||
|
||||
@@ -9,13 +9,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
import type { JSHandle } from "@playwright/test";
|
||||
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||
import { expect } from "../../element-web-test";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
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" },
|
||||
|
||||
@@ -9,11 +9,8 @@ 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,11 +9,8 @@ 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,11 +9,8 @@ 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 ({
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
|
||||
test(
|
||||
"registers an account and lands on the home page",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, mailpitClient, request, 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")] };
|
||||
@@ -51,11 +51,10 @@ test.describe("Email Registration", async () => {
|
||||
|
||||
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
||||
|
||||
const messages = await mailpitClient.listMessages();
|
||||
expect(messages.messages).toHaveLength(1);
|
||||
expect(messages.messages[0].To[0].Address).toEqual("alice@email.com");
|
||||
const text = await mailpitClient.renderMessageText(messages.messages[0].ID);
|
||||
const [emailLink] = text.match(/http.+/);
|
||||
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.getByText("Welcome alice")).toBeVisible();
|
||||
|
||||
@@ -8,7 +8,6 @@ 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({
|
||||
@@ -24,8 +23,6 @@ test.use({
|
||||
});
|
||||
|
||||
test.describe("Registration", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/#/register");
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { test as base, expect } from "../../element-web-test";
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ 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 Download, type Page } from "@playwright/test";
|
||||
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";
|
||||
@@ -182,8 +181,6 @@ 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");
|
||||
|
||||
@@ -42,7 +42,7 @@ test.describe("Memberlist", () => {
|
||||
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(memberlist.getByText("(Invited)")).toHaveCount(1);
|
||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
import { Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";
|
||||
@@ -38,34 +38,30 @@ test.describe("RightPanel", () => {
|
||||
});
|
||||
|
||||
test.describe("in rooms", () => {
|
||||
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);
|
||||
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);
|
||||
|
||||
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 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",
|
||||
);
|
||||
// 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}:localhost`)).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);
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { type Page, expect } from "@playwright/test";
|
||||
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
export async function viewRoomSummaryByName(page: Page, app: ElementAppPage, name: string): Promise<void> {
|
||||
await app.viewRoomByName(name);
|
||||
|
||||
@@ -10,7 +10,6 @@ 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" },
|
||||
@@ -33,14 +32,14 @@ test.describe("Room Directory", () => {
|
||||
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:${user.homeServer}`)).toHaveClass("mx_EditableItem_item");
|
||||
await expect(localAddresses.getByText("#gaming:localhost")).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:${user.homeServer}`);
|
||||
await expect(publishedAddresses.locator("#canonicalAlias")).toHaveValue("#gaming:localhost");
|
||||
const checkbox = publishedAddresses
|
||||
.locator(".mx_SettingsFlag", {
|
||||
hasText: `Publish this room to the public in ${user.homeServer}'s room directory?`,
|
||||
hasText: "Publish this room to the public in localhost's room directory?",
|
||||
})
|
||||
.getByRole("switch");
|
||||
await checkbox.check();
|
||||
@@ -88,7 +87,7 @@ test.describe("Room Directory", () => {
|
||||
.getByRole("button", { name: "Join" })
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL(`/#/room/#test1234:${user.homeServer}`);
|
||||
await expect(page).toHaveURL("/#/room/#test1234:localhost");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
test.describe("Room Header", () => {
|
||||
test.use({
|
||||
@@ -111,10 +111,6 @@ 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,13 +7,10 @@ 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: {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
|
||||
import { type ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { ElementAppPage } from "../../../pages/ElementAppPage";
|
||||
import { test as base, expect } from "../../../element-web-test";
|
||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../../src/settings/enums/Layout";
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "./index";
|
||||
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../../pages/client";
|
||||
|
||||
test.describe("Advanced section in Encryption tab", () => {
|
||||
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
|
||||
const clientHandle = await app.client.prepareClient();
|
||||
// Reset cross signing in order to have a verified session
|
||||
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
|
||||
});
|
||||
|
||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
|
||||
await expect(section.getByText(deviceId)).toBeVisible();
|
||||
|
||||
await expect(section).toMatchScreenshot("encryption-details.png", {
|
||||
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should show the import room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Import keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should show the export room keys dialog", async ({ page, app, util }) => {
|
||||
await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Export keys" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should reset the cryptographic identity",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, util }) => {
|
||||
const tab = await util.openEncryptionTab();
|
||||
const section = util.getEncryptionDetailsSection();
|
||||
|
||||
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
|
||||
await tab.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Fill password dialog and validate
|
||||
const dialog = page.locator(".mx_InteractiveAuthDialog");
|
||||
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
|
||||
|
||||
// After resetting the identity, the user should set up a new recovery key
|
||||
await expect(
|
||||
util.getEncryptionRecoverySection().getByRole("button", { name: "Set up recovery" }),
|
||||
).toBeVisible();
|
||||
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from ".";
|
||||
import {
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
deleteCachedSecrets,
|
||||
verifySession,
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
test(
|
||||
"should show a 'Verify this device' button if the device is unverified",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
const dialog = await util.openEncryptionTab();
|
||||
const content = util.getEncryptionTabContent();
|
||||
|
||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
||||
await expect(verifyButton).toBeVisible();
|
||||
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
|
||||
await verifyButton.click();
|
||||
|
||||
await util.verifyDevice(recoveryKey);
|
||||
|
||||
await expect(content).toMatchScreenshot("default-tab.png", {
|
||||
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
||||
//
|
||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
||||
test(
|
||||
"should prompt to enter the recovery key when the secrets are not cached locally",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
await util.openEncryptionTab();
|
||||
// We ask the user to enter the recovery key
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
||||
await expect(enterKeyButton).toBeVisible();
|
||||
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
||||
await enterKeyButton.click();
|
||||
|
||||
// Fill the recovery key
|
||||
await util.enterRecoveryKey(recoveryKey);
|
||||
await expect(dialog).toMatchScreenshot("default-tab.png", {
|
||||
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
|
||||
});
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
|
||||
page,
|
||||
app,
|
||||
util,
|
||||
}) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
// We need to delete the cached secrets
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
|
||||
await util.openEncryptionTab();
|
||||
const dialog = util.getEncryptionTabContent();
|
||||
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
// The user is prompted to reset their identity
|
||||
await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "@playwright/test";
|
||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { type 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;
|
||||
}>({
|
||||
displayName: "Alice",
|
||||
|
||||
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 Recovery Key" }).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) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recovery section
|
||||
*/
|
||||
getEncryptionRecoverySection() {
|
||||
return this.page.getByTestId("recoveryPanel");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the encryption details section
|
||||
*/
|
||||
getEncryptionDetailsSection() {
|
||||
return this.page.getByTestId("encryptionDetails");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 recovery 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(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png");
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* 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 ".";
|
||||
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
|
||||
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
test.describe("Recovery section in Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
});
|
||||
|
||||
test(
|
||||
"should change the recovery key",
|
||||
{ tag: ["@screenshot", "@no-webkit"] },
|
||||
async ({ page, app, homeserver, credentials, util, context }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
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.getEncryptionRecoverySection()).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, recoveryKey.encodedPrivateKey);
|
||||
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.getEncryptionRecoverySection()).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();
|
||||
|
||||
// Check that the current device is connected to key backup and the backup version is the expected one
|
||||
await checkDeviceIsConnectedKeyBackup(app, "1", true);
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ 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, user }) => {
|
||||
test("long address should not cause dialog to overflow", { tag: "@no-webkit" }, async ({ page, app }) => {
|
||||
const settings = await app.settings.openRoomSettings("General");
|
||||
// 1. Set the room-address to be a really long string
|
||||
const longString = "abcasdhjasjhdaj1jh1asdhasjdhajsdhjavhjksd".repeat(4);
|
||||
@@ -45,7 +45,7 @@ test.describe("General room settings tab", () => {
|
||||
await settings.locator("#roomAliases").getByText("Add", { exact: true }).click();
|
||||
|
||||
// 2. wait for the new setting to apply ...
|
||||
await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:${user.homeServer}`);
|
||||
await expect(settings.locator("#canonicalAlias")).toHaveValue(`#${longString}:localhost`);
|
||||
|
||||
// 3. Check if the dialog overflows
|
||||
const dialogBoundingBox = await page.locator(".mx_Dialog").boundingBox();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user