Compare commits

..

2 Commits

Author SHA1 Message Date
Robin
63d32efb27 Pin matrix-widget-api to v1.11.0
Until we have a matching PR to the Rust SDK that allows Element Call to take advantage of the latest version of MSC2762 in both Element Web and Element X, we should not be using matrix-widget-api v1.12.0. Element Call widgets are too sensitive to the behavior changes introduced in that version, and will just not work.
2025-01-22 16:44:12 -05:00
Robin
f05df80b46 Revert "Distinguish room state and timeline events when dealing with widgets (#28681)"
This reverts commit a0ab88943b.
2025-01-22 16:39:15 -05:00
1586 changed files with 9356 additions and 39284 deletions

View File

@@ -7,4 +7,3 @@ test/end-to-end-tests/lib/
src/component-index.js src/component-index.js
# Auto-generated file # Auto-generated file
src/modules.ts src/modules.ts
src/modules.js

View File

@@ -200,13 +200,8 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them // We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-object-type": [ // We do this sometimes to brand interfaces
"error", "@typescript-eslint/no-empty-object-type": "off",
{
// We do this sometimes to brand interfaces
allowInterfaces: "with-single-extends",
},
],
}, },
}, },
// temporary override for offending icon require files // temporary override for offending icon require files
@@ -252,7 +247,6 @@ module.exports = {
// We don't need super strict typing in test utilities // We don't need super strict typing in test utilities
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/explicit-member-accessibility": "off",
"@typescript-eslint/no-empty-object-type": "off",
// Jest/Playwright specific // Jest/Playwright specific

8
.github/CODEOWNERS vendored
View File

@@ -10,12 +10,10 @@
/test/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 /src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
/test/stores/SetupEncryptionStore-test.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/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers /src/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers /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/settings/encryption-user-tab/ @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
# Ignore translations as those will be updated by GHA for Localazy download # Ignore translations as those will be updated by GHA for Localazy download
/src/i18n/strings /src/i18n/strings

View File

@@ -26,12 +26,6 @@ jobs:
R2_URL: ${{ vars.CF_R2_S3_API }} R2_URL: ${{ vars.CF_R2_S3_API }}
R2_PUBLIC_URL: "https://element-web-develop.element.io" R2_PUBLIC_URL: "https://element-web-develop.element.io"
steps: 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/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

View File

@@ -1,9 +1,8 @@
name: Docker name: Dockerhub
on: on:
workflow_dispatch: {} workflow_dispatch: {}
push: push:
tags: [v*] tags: [v*]
pull_request: {}
schedule: schedule:
# This job can take a while, and we have usage limits, so just publish develop only twice a day # This job can take a while, and we have usage limits, so just publish develop only twice a day
- cron: "0 7/12 * * *" - cron: "0 7/12 * * *"
@@ -13,12 +12,9 @@ jobs:
buildx: buildx:
name: Docker Buildx name: Docker Buildx
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }} environment: dockerhub
permissions: permissions:
id-token: write # needed for signing the images with GitHub OIDC Token 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: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@@ -26,7 +22,6 @@ jobs:
- name: Install Cosign - name: Install Cosign
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3 uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3 uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
@@ -38,52 +33,16 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
if: github.event_name != 'pull_request'
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} 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 - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
if: github.event_name != 'pull_request'
with: with:
images: | images: |
vectorim/element-web vectorim/element-web
ghcr.io/element-hq/element-web
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=tag type=ref,event=tag
@@ -92,8 +51,7 @@ jobs:
- name: Build and push - name: Build and push
id: build-and-push id: build-and-push
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6 uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6
if: github.event_name != 'pull_request'
with: with:
context: . context: .
push: true push: true
@@ -105,7 +63,6 @@ jobs:
env: env:
DIGEST: ${{ steps.build-and-push.outputs.digest }} DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }} TAGS: ${{ steps.meta.outputs.tags }}
if: github.event_name != 'pull_request'
run: | run: |
images="" images=""
for tag in ${TAGS}; do for tag in ${TAGS}; do
@@ -115,7 +72,6 @@ jobs:
- name: Update repo description - name: Update repo description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4 uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
if: github.event_name != 'pull_request'
continue-on-error: true continue-on-error: true
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}

View File

@@ -50,7 +50,7 @@ jobs:
permissions: permissions:
checks: read checks: read
steps: steps:
- name: Wait for docker build - name: Wait for dockerhub
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
with: with:
ref: master ref: master

View File

@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue - name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@119b3320db3f04d89e91df840844b92d57ce3468 uses: guibranco/github-status-action-v2@ecd54a02cf761e85a8fb328fe937710fd4227cda
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success

View 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

View File

@@ -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
View File

@@ -26,7 +26,6 @@ electron/pub
/coverage /coverage
# Auto-generated file # Auto-generated file
/src/modules.ts /src/modules.ts
/src/modules.js
/build_config.yaml /build_config.yaml
/book /book
/index.html /index.html

View File

@@ -17,7 +17,6 @@ electron/pub
/coverage /coverage
# Auto-generated file # Auto-generated file
/src/modules.ts /src/modules.ts
/src/modules.js
/src/i18n/strings /src/i18n/strings
/build_config.yaml /build_config.yaml
# Raises an error because it contains a template var breaking the script tag # Raises an error because it contains a template var breaking the script tag

View File

@@ -33,15 +33,19 @@ module.exports = {
"import-notation": null, "import-notation": null,
"value-keyword-case": null, "value-keyword-case": null,
"declaration-block-no-redundant-longhand-properties": 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, "shorthand-property-no-redundant-values": null,
"property-no-vendor-prefix": null, "property-no-vendor-prefix": null,
"value-no-vendor-prefix": null,
"selector-no-vendor-prefix": null, "selector-no-vendor-prefix": null,
"media-feature-name-no-vendor-prefix": null, "media-feature-name-no-vendor-prefix": null,
"number-max-precision": null, "number-max-precision": null,
"no-invalid-double-slash-comments": true, "no-invalid-double-slash-comments": true,
"media-feature-range-notation": null, "media-feature-range-notation": null,
"declaration-property-value-no-unknown": null,
"declaration-property-value-keyword-no-deprecated": null,
"csstools/value-no-unknown-custom-properties": [ "csstools/value-no-unknown-custom-properties": [
true, true,
{ {

View File

@@ -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) Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
================================================================================================== ==================================================================================================
## ✨ Features ## ✨ Features

View File

@@ -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 from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :) 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 # Review expectations
See https://github.com/element-hq/element-meta/wiki/Review-process See https://github.com/element-hq/element-meta/wiki/Review-process

View File

@@ -1,5 +1,3 @@
# syntax=docker.io/docker/dockerfile:1.7-labs
# Builder # Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
@@ -10,7 +8,7 @@ ARG JS_SDK_BRANCH="master"
WORKDIR /src WORKDIR /src
COPY --exclude=docker . /src COPY . /src
RUN /src/scripts/docker-link-repos.sh RUN /src/scripts/docker-link-repos.sh
RUN yarn --network-timeout=200000 install RUN yarn --network-timeout=200000 install
RUN /src/scripts/docker-package.sh RUN /src/scripts/docker-package.sh
@@ -21,15 +19,11 @@ RUN cp /src/config.sample.json /src/webapp/config.json
# App # App
FROM nginx:alpine-slim 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 COPY --from=builder /src/webapp /app
# Override default nginx config. Templates in `/etc/nginx/templates` are passed # Override default nginx config. Templates in `/etc/nginx/templates` are passed
# through `envsubst` by the nginx docker image entry point. # through `envsubst` by the nginx docker image entry point.
COPY /docker/nginx-templates/* /etc/nginx/templates/ 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 # 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 RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf

120
README.md
View File

@@ -182,11 +182,123 @@ Dockerfile.
# Development # 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) Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
2. [Code style](./code_style.md) about where to start. Before starting work on a feature, it's best to ensure
3. [Contribution guide](./CONTRIBUTING.md) 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 # Translations

View File

@@ -5,6 +5,15 @@ adjacent to. As of writing, these are:
- element-desktop - element-desktop
- element-web - 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 ## 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: 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) 1. Types for lifecycle functions are not required (render, componentDidMount, and so on).
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) 2. Class components must always have a `Props` interface declared immediately above them. It can be
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
empty if the component accepts no props. empty if the component accepts no props.
5. Class components should have an `State` interface declared immediately above them, but after `Props`. 3. 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>` 4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
instead. 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. 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. 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 ```typescript
class FooStore { 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. 8. 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 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. 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 10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
if at all possible. 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). 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. 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. 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. which should be used.
1. Unless the component is considered a "structure", in which case use classes. 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. isolated components.
18. Components should serve a single, or near-single, purpose. 16. Components should serve a single, or near-single, purpose.
19. Prefer to derive information from component properties rather than establish state. 17. Prefer to derive information from component properties rather than establish state.
20. Do not use `React.Component::forceUpdate`. 18. Do not use `React.Component::forceUpdate`.
## Stylesheets (\*.pcss = PostCSS + Plugins) ## Stylesheets (\*.pcss = PostCSS + Plugins)
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not. 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). 1. Class names must be prefixed with "mx\_".
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. 2. Class names must denote the component which defines them, followed by any context.
3. Class names must be prefixed with "mx\_". The context is not further specified here in terms of meaning or syntax.
4. Class names must strictly denote the component which defines them. Use whatever is appropriate for your implementation use case.
For example: `mx_MyFoo` for `MyFoo` component. Some examples:
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. 1. `mx_MyFoo`
6. Use the `$font` variables instead of manual values. 2. `mx_MyFoo_avatar`
7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers. 3. `mx_MyFoo_avatarUser`
8. Use the whole class name instead of shortcuts: 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 ```scss
.mx_MyFoo { .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 ```scss
.mx_MyFoo, .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. 7. 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 8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
[documented](#comments) for what the values mean: [documented](#comments) for what the values mean:
```scss ```scss
.mx_MyFoo { .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. 9. 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.
## Tests ## Tests

2
debian/control vendored
View File

@@ -8,6 +8,6 @@ Package: element-web
Architecture: all Architecture: all
Recommends: httpd, element-io-archive-keyring Recommends: httpd, element-io-archive-keyring
Description: 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 This package contains the web-based client that can be served through a web
server. server.

View File

@@ -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

View File

@@ -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

View File

@@ -18,12 +18,8 @@ server {
} }
# covers config.json and config.hostname.json requests as it is prefix. # covers config.json and config.hostname.json requests as it is prefix.
location /config { location /config {
root /tmp/element-web-config;
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
} }
location /modules {
alias /tmp/element-web-modules;
}
# redirect server error pages to the static page /50x.html # redirect server error pages to the static page /50x.html
# #
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;

View File

@@ -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. 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`. 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`. 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` ### `desktop_builds` and `mobile_builds`
@@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit` 2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords` 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. 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.

View File

@@ -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 image as root (`docker run --user 0`) or, better, change the port that nginx
listens on via the `ELEMENT_WEB_PORT` environment variable. 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 The behaviour of the docker image can be customised via the following
environment variables: environment variables:

View File

@@ -112,7 +112,3 @@ Unreliable in encrypted rooms.
## Knock rooms (`feature_ask_to_join`) [In Development] ## Knock rooms (`feature_ask_to_join`) [In Development]
Enables knock feature for rooms. This allows users to ask to join a room. 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.

View File

@@ -128,7 +128,7 @@ flowchart TD
subgraph Deploying subgraph Deploying
D1[\Deploy staging.element.io/] D1[\Deploy staging.element.io/]
D2[\Check docker build/] D2[\Check dockerhub/]
D3[\Deploy app.element.io/] D3[\Deploy app.element.io/]
D4[\Check desktop package/] D4[\Check desktop package/]
@@ -213,10 +213,10 @@ switched back to the version of the dependency from the master branch to not lea
# Deploying # Deploying
We ship the SDKs to npm, this happens as part of the release process. 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. We ship Element Desktop to packages.element.io.
- [ ] Check that element-web has shipped to dockerhub & ghcr.io - [ ] Check that element-web has shipped to dockerhub
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully - [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
- [ ] Test staging.element.io - [ ] Test staging.element.io

View File

@@ -38,8 +38,6 @@ const config: Config = {
"^!!raw-loader!.*": "jest-raw-loader", "^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js", "recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock", "^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)).+$"], transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [ collectCoverageFrom: [

View File

@@ -9,7 +9,7 @@ import * as fs from "fs";
import * as childProcess from "child_process"; import * as childProcess from "child_process";
import * as semver from "semver"; import * as semver from "semver";
import { type BuildConfig } from "./BuildConfig"; import { BuildConfig } from "./BuildConfig";
// This expects to be run from ./scripts/install.ts // This expects to be run from ./scripts/install.ts
@@ -23,9 +23,10 @@ const MODULES_TS_HEADER = `
* You are not a salmon. * You are not a salmon.
*/ */
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
`; `;
const MODULES_TS_DEFINITIONS = ` const MODULES_TS_DEFINITIONS = `
export const INSTALLED_MODULES = []; export const INSTALLED_MODULES: RuntimeModule[] = [];
`; `;
export function installer(config: BuildConfig): void { export function installer(config: BuildConfig): void {
@@ -77,8 +78,8 @@ export function installer(config: BuildConfig): void {
return; // hit the finally{} block before exiting return; // hit the finally{} block before exiting
} }
// If we reach here, everything seems fine. Write modules.js and log some output // If we reach here, everything seems fine. Write modules.ts and log some output
// Note: we compile modules.js in two parts for developer friendliness if they // Note: we compile modules.ts in two parts for developer friendliness if they
// happen to look at it. // happen to look at it.
console.log("The following modules have been installed: ", installedModules); console.log("The following modules have been installed: ", installedModules);
let modulesTsHeader = MODULES_TS_HEADER; let modulesTsHeader = MODULES_TS_HEADER;
@@ -192,5 +193,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
} }
function writeModulesTs(content: string): void { function writeModulesTs(content: string): void {
fs.writeFileSync("./src/modules.js", content, "utf-8"); fs.writeFileSync("./src/modules.ts", content, "utf-8");
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "element-web", "name": "element-web",
"version": "1.11.93", "version": "1.11.90",
"description": "Element: the future of secure communication", "description": "A feature-rich client for Matrix.org",
"author": "New Vector Ltd.", "author": "New Vector Ltd.",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -74,25 +74,24 @@
"@types/react-dom": "18.3.5", "@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001697", "caniuse-lite": "1.0.30001692",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "^0.1.1",
"@fontsource/inconsolata": "^5", "@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5", "@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7", "@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.0", "@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/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0", "@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0", "@sentry/browser": "^8.0.0",
"@types/png-chunks-extract": "^1.0.2", "@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30", "@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^3.0.0", "@vector-im/compound-design-tokens": "^2.1.0",
"@vector-im/compound-web": "^7.6.1", "@vector-im/compound-web": "^7.5.0",
"@vector-im/matrix-wysiwyg": "2.38.0", "@vector-im/matrix-wysiwyg": "2.38.0",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",
@@ -128,8 +127,8 @@
"maplibre-gl": "^5.0.0", "maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1", "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", "matrix-widget-api": "1.11.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"mime": "^4.0.4", "mime": "^4.0.4",
"oidc-client-ts": "^3.0.1", "oidc-client-ts": "^3.0.1",
@@ -180,7 +179,7 @@
"@playwright/test": "^1.40.1", "@playwright/test": "^1.40.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7", "@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^3.0.0", "@sentry/webpack-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^3.0.0", "@stylistic/eslint-plugin": "^2.9.0",
"@svgr/webpack": "^8.0.0", "@svgr/webpack": "^8.0.0",
"@testcontainers/postgresql": "^10.16.0", "@testcontainers/postgresql": "^10.16.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
@@ -257,7 +256,7 @@
"jsqr": "^1.4.0", "jsqr": "^1.4.0",
"knip": "^5.36.2", "knip": "^5.36.2",
"lint-staged": "^15.0.2", "lint-staged": "^15.0.2",
"mailpit-api": "^1.0.5", "mailhog": "^4.16.0",
"matrix-web-i18n": "^3.2.1", "matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"minimist": "^1.2.6", "minimist": "^1.2.6",
@@ -281,8 +280,8 @@
"semver": "^7.5.2", "semver": "^7.5.2",
"source-map-loader": "^5.0.0", "source-map-loader": "^5.0.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"stylelint": "^16.13.0", "stylelint": "^16.1.0",
"stylelint-config-standard": "^37.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-scss": "^6.0.0", "stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1", "stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9", "terser-webpack-plugin": "^5.3.9",

View File

@@ -11,7 +11,7 @@ import type { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout"; import { Layout } from "../../../src/settings/enums/Layout";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
// Find and click "Reply" button // Find and click "Reply" button
const clickButtonReply = async (tile: Locator) => { const clickButtonReply = async (tile: Locator) => {

View File

@@ -11,7 +11,6 @@ import { registerAccountMas } from "../oidc";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { TestClientServerAPI } from "../csAPI"; import { TestClientServerAPI } from "../csAPI";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts"; 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 // 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, // 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.describe("Encryption state after registration", () => {
test.skip(isDendrite, "does not yet support MAS"); 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.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click(); 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 app.settings.openUserSettings("Security & Privacy");
await expect(page.locator(".mx_MatrixChat")).toBeVisible(); await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
// Recovery is not set up yet
await checkDeviceIsConnectedKeyBackup(app, "1", true, false);
}); });
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.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click(); 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("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New 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 ({ test("Key backup is disabled when reset from elsewhere", async ({
page, page,
mailpitClient, mailhogClient,
request, request,
homeserver, homeserver,
}, testInfo) => { }, testInfo) => {
@@ -64,7 +60,7 @@ test.describe("Key backup reset from elsewhere", () => {
await page.goto("/#/login"); await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click(); 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("button", { name: "Add room" }).click();
await page.getByRole("menuitem", { name: "New room" }).click(); await page.getByRole("menuitem", { name: "New room" }).click();

View File

@@ -10,7 +10,6 @@ import { type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { completeCreateSecretStorageDialog } from "./utils.ts";
async function expectBackupVersionToBe(page: Page, version: string) { async function expectBackupVersionToBe(page: Page, version: string) {
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText( await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
@@ -36,7 +35,19 @@ test.describe("Backups", () => {
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible(); await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); 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 // Open the settings again
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
@@ -51,15 +62,14 @@ test.describe("Backups", () => {
await expectBackupVersionToBe(page, "1"); await expectBackupVersionToBe(page, "1");
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click(); 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(); await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
// Delete it // Delete it
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup" await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
// Create another // Create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey); await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click(); await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
// Should be successful // Should be successful
@@ -90,8 +100,8 @@ test.describe("Backups", () => {
// Try to create another // Try to create another
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase // But cancel the security key dialog, to simulate not having the secret storage passphrase
await currentDialogLocator.getByTestId("dialog-cancel-button").click(); await currentDialogLocator.getByTestId("dialog-cancel-button").click();
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible(); await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();

View File

@@ -8,16 +8,9 @@ Please see LICENSE files in the repository root for full details.
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
import { expect, test } from "../../element-web-test"; import { expect, test } from "../../element-web-test";
import { import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
autoJoin, import { Bot } from "../../pages/bot";
completeCreateSecretStorageDialog, import { ElementAppPage } from "../../pages/ElementAppPage";
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 { isDendrite } from "../../plugins/homeserver/dendrite";
const checkDMRoom = async (page: Page) => { const checkDMRoom = async (page: Page) => {
@@ -118,7 +111,18 @@ test.describe("Cryptography", function () {
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
await page.getByRole("button", { name: "Set up Secure Backup" }).click(); 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 // Verify that the SSSS keys are in the account data stored in the server
await verifyKey(app, "master"); await verifyKey(app, "master");
@@ -186,7 +190,7 @@ test.describe("Cryptography", function () {
await page.getByRole("button", { name: "Clear cross-signing keys" }).click(); await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
// Enter the 4S key // 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(); await page.getByRole("button", { name: "Continue" }).click();
// Enter the password // Enter the password

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { test, expect } from "../../element-web-test"; import { Locator, type Page } from "@playwright/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 { 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"; const NAME = "Alice";
function getMemberTileByName(page: Page, name: string): Locator {
return page.locator(`.mx_MemberTileView, [title="${name}"]`);
}
test.use({ test.use({
displayName: NAME, displayName: NAME,
synapseConfig: { synapseConfig: {
@@ -39,7 +44,7 @@ test.use({
test.describe("Dehydration", () => { test.describe("Dehydration", () => {
test.skip(isDendrite, "does not yet support dehydration v2"); 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) // Create a backup (which will create SSSS, and dehydrated device)
const securityTab = await app.settings.openUserSettings("Security & Privacy"); 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 expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
await securityTab.getByRole("button", { name: "Set up", exact: true }).click(); 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 // the dehydrated device gets created with the name "Dehydrated
// device". We want to make sure that it is not visible as a normal // device". We want to make sure that it is not visible as a normal
// device. // device.
const sessionsTab = await app.settings.openUserSettings("Sessions"); const sessionsTab = await app.settings.openUserSettings("Sessions");
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible(); 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 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 ({ await viewRoomSummaryByName(page, app, ROOM_NAME);
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());
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient); await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
expect(initialDehydratedDeviceIds.length).toBe(1); 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 expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
await logIntoElement(page, credentials); await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
// 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]);
}); });
}); });
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);
}

View File

@@ -20,7 +20,7 @@ import {
logIntoElement, logIntoElement,
waitForVerificationRequest, waitForVerificationRequest,
} from "./utils"; } from "./utils";
import { type Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
test.describe("Device verification", { tag: "@no-webkit" }, () => { test.describe("Device verification", { tag: "@no-webkit" }, () => {
let aliceBotClient: Bot; let aliceBotClient: Bot;
@@ -29,7 +29,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
let expectedBackupVersion: string; let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => { test.beforeEach(async ({ page, homeserver, credentials }) => {
const res = await createBot(page, homeserver, credentials, true); const res = await createBot(page, homeserver, credentials);
aliceBotClient = res.botClient; aliceBotClient = res.botClient;
expectedBackupVersion = res.expectedBackupVersion; expectedBackupVersion = res.expectedBackupVersion;
}); });
@@ -68,8 +68,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup // 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, // 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. // as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false); await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
}); });
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => { test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
@@ -112,14 +112,16 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
await checkDeviceIsCrossSigned(app); await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup // 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 }) => { test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials); await logIntoElement(page, credentials);
// Select the security phrase // 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 // Fill the passphrase
const dialog = page.locator(".mx_Dialog"); const dialog = page.locator(".mx_Dialog");
@@ -133,18 +135,18 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup // 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 // 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); await logIntoElement(page, credentials);
// Select the security phrase // 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"); 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(); const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey); await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
@@ -156,7 +158,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
// Check that the current device is connected to key backup // 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 // 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 }) => { test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {

View File

@@ -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. 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 { expect, test } from "../../element-web-test";
import { import {
@@ -17,10 +17,9 @@ import {
logIntoElement, logIntoElement,
logOutOfElement, logOutOfElement,
verify, verify,
waitForDevices,
} from "./utils"; } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts"; import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
import { type ElementAppPage } from "../../pages/ElementAppPage.ts"; import { ElementAppPage } from "../../pages/ElementAppPage.ts";
test.describe("Cryptography", function () { test.describe("Cryptography", function () {
test.use({ test.use({
@@ -145,8 +144,25 @@ test.describe("Cryptography", function () {
// bob deletes his second device // bob deletes his second device
await bobSecondDevice.evaluate((cli) => cli.logout(true)); await bobSecondDevice.evaluate((cli) => cli.logout(true));
// wait for the logout to propagate. // 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.
await waitForDevices(app, bob.credentials.userId, 1); 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. // close and reopen the room, to get the shield to update.
await app.viewRoomByName("Bob"); await app.viewRoomByName("Bob");
@@ -269,7 +285,11 @@ test.describe("Cryptography", function () {
// Workaround for https://github.com/element-hq/element-web/issues/28640: // 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 // make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
// his user info. // 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. // Our app is blocked from syncing while Bob sends his messages.
await app.client.network.goOffline(); await app.client.network.goOffline();

View File

@@ -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? Youll need to reset your identity." }),
).toBeVisible();
});
});

View File

@@ -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 Preset, type Visibility } from "matrix-js-sdk/src/matrix";
import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils"; import { doTwoWaySasVerification, awaitVerifier } from "./utils";
import { type Client } from "../../pages/client"; import { Client } from "../../pages/client";
test.describe("User verification", () => { test.describe("User verification", () => {
// note that there are other tests that check user verification works in `crypto.spec.ts`. // 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 ({ test("can receive a verification request when there is no existing DM", async ({
app,
page, page,
bot: bob, bot: bob,
user: aliceCredentials, user: aliceCredentials,
toasts, toasts,
room: { roomId: dmRoomId }, room: { roomId: dmRoomId },
}) => { }) => {
await waitForDevices(app, bob.credentials.userId, 1); await waitForDeviceKeys(page);
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
const avatar = page.getByRole("button", { name: "Avatar" });
await avatar.click();
// once Alice has joined, Bob starts the verification // once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle( const bobVerificationRequest = await bob.evaluateHandle(
@@ -87,17 +84,13 @@ test.describe("User verification", () => {
}); });
test("can abort emoji verification when emoji mismatch", async ({ test("can abort emoji verification when emoji mismatch", async ({
app,
page, page,
bot: bob, bot: bob,
user: aliceCredentials, user: aliceCredentials,
toasts, toasts,
room: { roomId: dmRoomId }, room: { roomId: dmRoomId },
}) => { }) => {
await waitForDevices(app, bob.credentials.userId, 1); await waitForDeviceKeys(page);
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
const avatar = page.getByRole("button", { name: "Avatar" });
await avatar.click();
// once Alice has joined, Bob starts the verification // once Alice has joined, Bob starts the verification
const bobVerificationRequest = await bob.evaluateHandle( 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();
}

View File

@@ -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. 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 { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
import type { import type {
@@ -18,9 +18,9 @@ import type {
Verifier, Verifier,
VerifierEvent, VerifierEvent,
} from "matrix-js-sdk/src/crypto-api"; } from "matrix-js-sdk/src/crypto-api";
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver"; import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
import { type Client } from "../../pages/client"; import { Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
/** /**
@@ -28,13 +28,11 @@ import { Bot } from "../../pages/bot";
* @param page - the playwright `page` fixture * @param page - the playwright `page` fixture
* @param homeserver - the homeserver to use * @param homeserver - the homeserver to use
* @param credentials - the credentials to use for the bot client * @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( export async function createBot(
page: Page, page: Page,
homeserver: HomeserverInstance, homeserver: HomeserverInstance,
credentials: Credentials, credentials: Credentials,
usePassphrase = false,
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> { ): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
// Visit the login page of the app, to load the matrix sdk // Visit the login page of the app, to load the matrix sdk
await page.goto("/#/login"); await page.goto("/#/login");
@@ -46,7 +44,6 @@ export async function createBot(
const botClient = new Bot(page, homeserver, { const botClient = new Bot(page, homeserver, {
bootstrapCrossSigning: true, bootstrapCrossSigning: true,
bootstrapSecretStorage: true, bootstrapSecretStorage: true,
usePassphrase,
}); });
botClient.setCredentials(credentials); botClient.setCredentials(credentials);
// Backup is prepared in the background. Poll until it is ready. // Backup is prepared in the background. Poll until it is ready.
@@ -142,16 +139,14 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
* Check that the current device is connected to the expected key backup. * Check that the current device is connected to the expected key backup.
* Also checks that the decryption key is known and cached locally. * 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 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 checkBackupKeyInCache - whether to check that the backup key is cached locally.
* @param checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
*/ */
export async function checkDeviceIsConnectedKeyBackup( export async function checkDeviceIsConnectedKeyBackup(
app: ElementAppPage, page: Page,
expectedBackupVersion: string, expectedBackupVersion: string,
checkBackupPrivateKeyInCache: boolean, checkBackupKeyInCache: boolean,
checkBackupKeyIn4S: boolean = true,
): Promise<void> { ): Promise<void> {
// Sanity check the given backup version: if it's null, something went wrong earlier in the test. // Sanity check the given backup version: if it's null, something went wrong earlier in the test.
if (!expectedBackupVersion) { if (!expectedBackupVersion) {
@@ -160,48 +155,23 @@ export async function checkDeviceIsConnectedKeyBackup(
); );
} }
const backupData = await app.client.evaluate(async (client: MatrixClient) => { await page.getByRole("button", { name: "User menu" }).click();
const crypto = client.getCrypto(); await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
if (!crypto) return; await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
const backupInfo = await crypto.getKeyBackupInfo(); // expand the advanced section to see the active version in the reports
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored()); await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
return { if (checkBackupKeyInCache) {
backupInfo, const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
hasBackupPrivateKeyFromCache, await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
backupPrivateKeyWellFormed,
backupKeyIn4S,
activeBackupVersion,
};
});
if (!backupData) {
throw new Error("Crypto module is not available");
} }
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } = await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
backupData; expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
);
// We have a key backup await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
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);
}
} }
/** /**
@@ -218,13 +188,8 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
// if a securityKey was given, verify the new device // if a securityKey was given, verify the new device
if (securityKey !== undefined) { if (securityKey !== undefined) {
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
// Fill in the security key
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_Dialog").locator('input[type="password"]').fill(securityKey); await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click(); await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
await page.getByRole("button", { name: "Done" }).click(); await page.getByRole("button", { name: "Done" }).click();
@@ -251,19 +216,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 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) { export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption"); const settings = await app.settings.openUserSettings("Security & Privacy");
await settings.getByRole("button", { name: "Verify this device" }).click(); await settings.getByRole("button", { name: "Verify this session" }).click();
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).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.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: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click(); await app.page.getByRole("button", { name: "Done" }).click();
await app.settings.closeDialog();
} }
/** /**
@@ -293,61 +257,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). * 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> { export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
await app.settings.openUserSettings("Security & Privacy"); await app.settings.openUserSettings("Security & Privacy");
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click(); 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`). * Click on copy and continue buttons to dismiss the security key dialog
*
* 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
*/ */
export async function copyAndContinue(page: Page) { export async function copyAndContinue(page: Page) {
await page.getByRole("button", { name: "Copy" }).click(); await page.getByRole("button", { name: "Copy" }).click();
@@ -504,31 +435,3 @@ export async function deleteCachedSecrets(page: Page) {
}); });
await page.reload(); 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!`);
}
}

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type APIRequestContext } from "playwright-core"; import { APIRequestContext } from "playwright-core";
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api"; 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"; import { ClientServerApi } from "../plugins/utils/api.ts";
/** /**

View File

@@ -6,11 +6,11 @@ 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. 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 type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
import { expect, test } from "../../element-web-test"; 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 { SettingLevel } from "../../../src/settings/SettingLevel";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";

View File

@@ -10,7 +10,7 @@ import { expect, test as base } from "../../element-web-test";
import { selectHomeserver } from "../utils"; import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts"; import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
import { type Credentials } from "../../plugins/homeserver"; import { Credentials } from "../../plugins/homeserver";
const email = "user@nowhere.dummy"; const email = "user@nowhere.dummy";

View File

@@ -10,7 +10,7 @@ import { Bot } from "../../pages/bot";
import type { Locator, Page } from "@playwright/test"; import type { Locator, Page } from "@playwright/test";
import type { ElementAppPage } from "../../pages/ElementAppPage"; import type { ElementAppPage } from "../../pages/ElementAppPage";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { type Credentials } from "../../plugins/homeserver"; import { Credentials } from "../../plugins/homeserver";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
test.describe("Lazy Loading", () => { test.describe("Lazy Loading", () => {

View File

@@ -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();
});
});

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { 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");
});
});

View File

@@ -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. 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"; import { test, expect } from "../../element-web-test";

View File

@@ -6,11 +6,11 @@ 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. 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 { expect, test } from "../../element-web-test";
import { selectHomeserver } from "../utils"; 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 { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";

View File

@@ -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. 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 /** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element
*/ */

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
/* See readme.md for tips on writing these tests. */ /* 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"; import { test, expect } from "../../element-web-test";

View File

@@ -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!");
});
});
});

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type MailpitClient } from "mailpit-api"; import { API, Messages } from "mailhog";
import { type Page } from "@playwright/test"; import { Page } from "@playwright/test";
import { expect } from "../../element-web-test"; import { expect } from "../../element-web-test";
export async function registerAccountMas( export async function registerAccountMas(
page: Page, page: Page,
mailpit: MailpitClient, mailhog: API,
username: string, username: string,
email: string, email: string,
password: string, password: string,
@@ -27,13 +27,13 @@ export async function registerAccountMas(
await page.getByRole("textbox", { name: "Confirm Password" }).fill(password); await page.getByRole("textbox", { name: "Confirm Password" }).fill(password);
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
let code: string; let messages: Messages;
await expect(async () => { await expect(async () => {
const messages = await mailpit.listMessages(); messages = await mailhog.messages();
expect(messages.messages[0].To[0].Address).toEqual(email); expect(messages.items).toHaveLength(1);
const text = await mailpit.renderMessageText(messages.messages[0].ID);
[, code] = text.match(/Your verification code to confirm this email address is: (\d{6})/);
}).toPass(); }).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("textbox", { name: "6-digit code" }).fill(code);
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();

View File

@@ -19,7 +19,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
context, context,
page, page,
homeserver, homeserver,
mailpitClient, mailhogClient,
mas, mas,
}, testInfo) => { }, testInfo) => {
await page.clock.install(); await page.clock.install();
@@ -33,7 +33,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`; 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. // Eventually, we should end up at the home screen.
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 }); await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { test as base, expect } from "../../element-web-test"; import { test as base, expect } from "../../element-web-test";
import { type Credentials } from "../../plugins/homeserver"; import { Credentials } from "../../plugins/homeserver";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
const test = base.extend<{ const test = base.extend<{

View File

@@ -6,12 +6,12 @@
* Please see LICENSE files in the repository root for full details. * 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 { test as base, expect } from "../../element-web-test";
import { type Client } from "../../pages/client"; import { Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { type Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
type RoomRef = { name: string; roomId: string }; type RoomRef = { name: string; roomId: string };

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import type { Bot } from "../../pages/bot"; import type { Bot } from "../../pages/bot";
import type { Client } from "../../pages/client"; import type { Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Poll history", () => { test.describe("Poll history", () => {
type CreatePollOptions = { type CreatePollOptions = {

View File

@@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
import type { JSHandle, Page } from "@playwright/test"; import type { JSHandle, Page } from "@playwright/test";
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix"; import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../element-web-test"; import { test as base, expect } from "../../element-web-test";
import { type Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
import { type Client } from "../../pages/client"; import { Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
type RoomRef = { name: string; roomId: string }; type RoomRef = { name: string; roomId: string };
@@ -526,10 +526,9 @@ class Helpers {
await expect(threadPanel).toBeVisible(); await expect(threadPanel).toBeVisible();
await threadPanel.evaluate(($panel) => { await threadPanel.evaluate(($panel) => {
const $button = $panel.querySelector<HTMLElement>('[data-testid="base-card-back-button"]'); 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 // If the Threads back button is present then click it - the
// threads button can open either threads list or thread panel // threads button can open either threads list or thread panel
if ($button && title !== "Threads") { if ($button) {
$button.click(); $button.click();
} }
}); });

View File

@@ -57,8 +57,8 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
await util.openThread("ThreadRoot"); await util.openThread("ThreadRoot");
// Then the thread root is marked as read in the main timeline, // 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 // 30 remaining messages are unread - 7 messages are displayed under the thread root
await util.assertUnread(room2, 30 - 6); await util.assertUnread(room2, 30 - 7);
}); });
test("Creating a new thread based on a reply makes the room unread", async ({ test("Creating a new thread based on a reply makes the room unread", async ({

View File

@@ -9,8 +9,8 @@ Please see LICENSE files in the repository root for full details.
import type { JSHandle } from "@playwright/test"; import type { JSHandle } from "@playwright/test";
import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix"; import type { MatrixEvent, ISendEventResponse, ReceiptType } from "matrix-js-sdk/src/matrix";
import { expect } from "../../element-web-test"; import { expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { type Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
import { test } from "."; import { test } from ".";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";

View File

@@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
test( test(
"registers an account and lands on the home page", "registers an account and lands on the home page",
{ tag: "@screenshot" }, { tag: "@screenshot" },
async ({ page, mailpitClient, request, checkA11y }) => { async ({ page, mailhogClient, request, checkA11y }) => {
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible(); await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Hide the server text as it contains the randomly allocated Homeserver port // Hide the server text as it contains the randomly allocated Homeserver port
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] }; 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(); await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
const messages = await mailpitClient.listMessages(); const messages = await mailhogClient.messages();
expect(messages.messages).toHaveLength(1); expect(messages.items).toHaveLength(1);
expect(messages.messages[0].To[0].Address).toEqual("alice@email.com"); expect(messages.items[0].to).toEqual("alice@email.com");
const text = await mailpitClient.renderMessageText(messages.messages[0].ID); const [emailLink] = messages.items[0].text.match(/http.+/);
const [emailLink] = text.match(/http.+/);
await request.get(emailLink); // "Click" the link in the email await request.get(emailLink); // "Click" the link in the email
await expect(page.getByText("Welcome alice")).toBeVisible(); await expect(page.getByText("Welcome alice")).toBeVisible();

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * 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 { test as base, expect } from "../../element-web-test";

View File

@@ -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. 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 { test, expect } from "../../element-web-test";
import { viewRoomSummaryByName } from "./utils"; import { viewRoomSummaryByName } from "./utils";

View File

@@ -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. 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 { test, expect } from "../../element-web-test";
import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils"; import { checkRoomSummaryCard, viewRoomSummaryByName } from "./utils";

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { type Page, expect } from "@playwright/test"; 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> { export async function viewRoomSummaryByName(page: Page, app: ElementAppPage, name: string): Promise<void> {
await app.viewRoomByName(name); await app.viewRoomByName(name);

View File

@@ -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. 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 { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
test.describe("Room Header", () => { test.describe("Room Header", () => {
test.use({ test.use({

View File

@@ -6,9 +6,9 @@
* Please see LICENSE files in the repository root for full details. * 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 { test as base, expect } from "../../../element-web-test";
import { SettingLevel } from "../../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { Layout } from "../../../../src/settings/enums/Layout"; import { Layout } from "../../../../src/settings/enums/Layout";

View File

@@ -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);
},
);
});

View File

@@ -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? Youll need to reset your identity.")).toBeVisible();
});
});

View File

@@ -5,10 +5,10 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import { type Page } from "@playwright/test"; import { Page } from "@playwright/test";
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { type ElementAppPage } from "../../../pages/ElementAppPage"; import { ElementAppPage } from "../../../pages/ElementAppPage";
import { test as base, expect } from "../../../element-web-test"; import { test as base, expect } from "../../../element-web-test";
export { expect }; export { expect };
@@ -18,8 +18,6 @@ export { expect };
export const test = base.extend<{ export const test = base.extend<{
util: Helpers; util: Helpers;
}>({ }>({
displayName: "Alice",
util: async ({ page, app, bot }, use) => { util: async ({ page, app, bot }, use) => {
await use(new Helpers(page, app)); await use(new Helpers(page, app));
}, },
@@ -43,7 +41,7 @@ class Helpers {
*/ */
async verifyDevice(recoveryKey: GeneratedSecretStorageKey) { async verifyDevice(recoveryKey: GeneratedSecretStorageKey) {
// Select the security phrase // Select the security phrase
await this.page.getByRole("button", { name: "Verify with Recovery Key" }).click(); await this.page.getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
await this.enterRecoveryKey(recoveryKey); await this.enterRecoveryKey(recoveryKey);
await this.page.getByRole("button", { name: "Done" }).click(); await this.page.getByRole("button", { name: "Done" }).click();
} }
@@ -53,6 +51,9 @@ class Helpers {
* @param recoveryKey * @param recoveryKey
*/ */
async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) { async enterRecoveryKey(recoveryKey: GeneratedSecretStorageKey) {
// Select to use recovery key
await this.page.getByRole("button", { name: "use your Security Key" }).click();
// Fill the recovery key // Fill the recovery key
const dialog = this.page.locator(".mx_Dialog"); const dialog = this.page.locator(".mx_Dialog");
await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey); await dialog.getByRole("textbox").fill(recoveryKey.encodedPrivateKey);
@@ -66,20 +67,6 @@ class Helpers {
return this.page.getByTestId("encryptionTab"); 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` * Set the default key id of the secret storage to `null`
*/ */
@@ -91,7 +78,7 @@ class Helpers {
} }
/** /**
* Get the recovery key from the clipboard and fill in the input field * Get the security key from the clipboard and fill in the input field
* Then click on the finish button * Then click on the finish button
* @param title - The title of the dialog * @param title - The title of the dialog
* @param confirmButtonLabel - The label of the confirm button * @param confirmButtonLabel - The label of the confirm button
@@ -105,6 +92,6 @@ class Helpers {
const clipboardContent = await this.app.getClipboard(); const clipboardContent = await this.app.getClipboard();
await dialog.getByRole("textbox").fill(clipboardContent); await dialog.getByRole("textbox").fill(clipboardContent);
await dialog.getByRole("button", { name: confirmButtonLabel }).click(); await dialog.getByRole("button", { name: confirmButtonLabel }).click();
await expect(this.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png"); await expect(dialog).toMatchScreenshot("default-recovery.png");
} }
} }

View File

@@ -5,9 +5,16 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
import { test, expect } from "."; import { test, expect } from ".";
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils"; import {
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api"; checkDeviceIsConnectedKeyBackup,
checkDeviceIsCrossSigned,
createBot,
deleteCachedSecrets,
verifySession,
} from "../../crypto/utils";
test.describe("Recovery section in Encryption tab", () => { test.describe("Recovery section in Encryption tab", () => {
test.use({ test.use({
@@ -15,23 +22,46 @@ test.describe("Recovery section in Encryption tab", () => {
}); });
let recoveryKey: GeneratedSecretStorageKey; let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
test.beforeEach(async ({ page, homeserver, credentials }) => { 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); const res = await createBot(page, homeserver, credentials);
recoveryKey = res.recoveryKey; recoveryKey = res.recoveryKey;
expectedBackupVersion = res.expectedBackupVersion;
});
test("should verify the device", { tag: "@screenshot" }, async ({ page, app, util }) => {
const dialog = await util.openEncryptionTab();
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
await expect(verifyButton).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("verify-device-encryption-tab.png");
await verifyButton.click();
await util.verifyDevice(recoveryKey);
await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await app.closeDialog();
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
}); });
test( test(
"should change the recovery key", "should change the recovery key",
{ tag: ["@screenshot", "@no-webkit"] }, { tag: ["@screenshot", "@no-webkit"] },
async ({ page, app, homeserver, credentials, util, context }) => { async ({ page, app, homeserver, credentials, util, context }) => {
await verifySession(app, recoveryKey.encodedPrivateKey); await verifySession(app, "new passphrase");
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
// The user can only change the recovery key // The user can only change the recovery key
const changeButton = dialog.getByRole("button", { name: "Change recovery key" }); const changeButton = dialog.getByRole("button", { name: "Change recovery key" });
await expect(changeButton).toBeVisible(); await expect(changeButton).toBeVisible();
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("default-recovery.png"); await expect(util.getEncryptionTabContent()).toMatchScreenshot("default-recovery.png");
await changeButton.click(); await changeButton.click();
// Display the new recovery key and click on the copy button // Display the new recovery key and click on the copy button
@@ -52,14 +82,14 @@ test.describe("Recovery section in Encryption tab", () => {
); );
test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => { test("should setup the recovery key", { tag: ["@screenshot", "@no-webkit"] }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey); await verifySession(app, "new passphrase");
await util.removeSecretStorageDefaultKeyId(); await util.removeSecretStorageDefaultKeyId();
// The key backup is deleted and the user needs to set it up // The key backup is deleted and the user needs to set it up
const dialog = await util.openEncryptionTab(); const dialog = await util.openEncryptionTab();
const setupButton = dialog.getByRole("button", { name: "Set up recovery" }); const setupButton = dialog.getByRole("button", { name: "Set up recovery" });
await expect(setupButton).toBeVisible(); await expect(setupButton).toBeVisible();
await expect(util.getEncryptionRecoverySection()).toMatchScreenshot("set-up-recovery.png"); await expect(util.getEncryptionTabContent()).toMatchScreenshot("set-up-recovery.png");
await setupButton.click(); await setupButton.click();
// Display an informative panel about the recovery key // Display an informative panel about the recovery key
@@ -85,7 +115,42 @@ test.describe("Recovery section in Encryption tab", () => {
// The recovery key is now set up and the user can change it // The recovery key is now set up and the user can change it
await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible(); await expect(dialog.getByRole("button", { name: "Change recovery key" })).toBeVisible();
await app.closeDialog();
// Check that the current device is connected to key backup and the backup version is the expected one // Check that the current device is connected to key backup and the backup version is the expected one
await checkDeviceIsConnectedKeyBackup(app, "1", true); await checkDeviceIsConnectedKeyBackup(page, "1", true);
}); });
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
//
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
// We simulate this case by deleting the cached secrets in the indexedDB.
test(
"should enter the recovery key when the secrets are not cached",
{ tag: "@screenshot" },
async ({ page, app, util }) => {
await verifySession(app, "new passphrase");
// We need to delete the cached secrets
await deleteCachedSecrets(page);
await util.openEncryptionTab();
// We ask the user to enter the recovery key
const dialog = util.getEncryptionTabContent();
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
await expect(enterKeyButton).toBeVisible();
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
await enterKeyButton.click();
// Fill the recovery key
await util.enterRecoveryKey(recoveryKey);
await expect(dialog).toMatchScreenshot("default-recovery.png");
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// The backup decryption key should be in cache also, as we got it directly from the 4S
await app.closeDialog();
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
},
);
}); });

View File

@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details. * Please see LICENSE files in the repository root for full details.
*/ */
import { type Locator } from "@playwright/test"; import { Locator } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";

View File

@@ -25,9 +25,13 @@ test.describe("Security user settings tab", () => {
}, },
}); });
test.beforeEach(async ({ page, app, user }) => { test.beforeEach(async ({ page, user }) => {
// Dismiss "Notification" toast // Dismiss "Notification" toast
await app.closeNotificationToast(); await page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics await page.locator(".mx_Toast_buttons").getByRole("button", { name: "Yes" }).click(); // Allow analytics
}); });

View File

@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { type Page, type Request } from "@playwright/test"; import { Page, Request } from "@playwright/test";
import { GenericContainer, type StartedTestContainer, Wait } from "testcontainers"; import { GenericContainer, StartedTestContainer, Wait } from "testcontainers";
import { test as base, expect } from "../../element-web-test"; import { test as base, expect } from "../../element-web-test";
import type { ElementAppPage } from "../../pages/ElementAppPage"; import type { ElementAppPage } from "../../pages/ElementAppPage";

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import type { Locator, Page } from "@playwright/test"; import type { Locator, Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix"; import type { Preset, ICreateRoomOpts } from "matrix-js-sdk/src/matrix";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { isDendrite } from "../../plugins/homeserver/dendrite"; import { isDendrite } from "../../plugins/homeserver/dendrite";
async function openSpaceCreateMenu(page: Page): Promise<Locator> { async function openSpaceCreateMenu(page: Page): Promise<Locator> {

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type JSHandle, type Locator, type Page } from "@playwright/test"; import { JSHandle, Locator, Page } from "@playwright/test";
import type { MatrixEvent, IContent, Room } from "matrix-js-sdk/src/matrix"; import type { MatrixEvent, IContent, Room } from "matrix-js-sdk/src/matrix";
import { test as base, expect } from "../../../element-web-test"; import { test as base, expect } from "../../../element-web-test";
import { type Bot } from "../../../pages/bot"; import { Bot } from "../../../pages/bot";
import { type Client } from "../../../pages/client"; import { Client } from "../../../pages/client";
import { type ElementAppPage } from "../../../pages/ElementAppPage"; import { ElementAppPage } from "../../../pages/ElementAppPage";
import { type Credentials } from "../../../plugins/homeserver"; import { Credentials } from "../../../plugins/homeserver";
type RoomRef = { name: string; roomId: string }; type RoomRef = { name: string; roomId: string };

View File

@@ -13,8 +13,8 @@ import type { ISendEventResponse, EventType, MsgType } from "matrix-js-sdk/src/m
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { SettingLevel } from "../../../src/settings/SettingLevel"; import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout"; import { Layout } from "../../../src/settings/enums/Layout";
import { type Client } from "../../pages/client"; import { Client } from "../../pages/client";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { Bot } from "../../pages/bot"; import { Bot } from "../../pages/bot";
// The avatar size used in the timeline // The avatar size used in the timeline

View File

@@ -19,6 +19,7 @@ test.describe("UserView", () => {
const rightPanel = page.locator("#mx_RightPanel"); const rightPanel = page.locator("#mx_RightPanel");
await expect(rightPanel.getByRole("heading", { name: bot.credentials.displayName, exact: true })).toBeVisible(); await expect(rightPanel.getByRole("heading", { name: bot.credentials.displayName, exact: true })).toBeVisible();
await expect(rightPanel.getByText("1 session")).toBeVisible();
await expect(rightPanel).toMatchScreenshot("user-info.png", { await expect(rightPanel).toMatchScreenshot("user-info.png", {
mask: [page.locator(".mx_UserInfo_profile_mxid")], mask: [page.locator(".mx_UserInfo_profile_mxid")],
css: ` css: `

View File

@@ -12,7 +12,7 @@ import { uniqueId } from "lodash";
import { expect, type Page } from "@playwright/test"; import { expect, type Page } from "@playwright/test";
import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; import type { ClientEvent, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { type Client } from "../pages/client"; import { Client } from "../pages/client";
/** /**
* Resolves when room state matches predicate. * Resolves when room state matches predicate.

View File

@@ -10,8 +10,8 @@ import * as fs from "node:fs";
import type { Page } from "@playwright/test"; import type { Page } from "@playwright/test";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage"; import { ElementAppPage } from "../../pages/ElementAppPage";
import { type Credentials } from "../../plugins/homeserver"; import { Credentials } from "../../plugins/homeserver";
import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts"; import type { UserWidget } from "../../../src/utils/WidgetUtils-types.ts";
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker"; const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";

View File

@@ -11,7 +11,7 @@ Please see LICENSE files in the repository root for full details.
import type { IWidget } from "matrix-widget-api/src/interfaces/IWidget"; import type { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
import type { MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix"; import type { MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test"; import { test, expect } from "../../element-web-test";
import { type Client } from "../../pages/client"; import { Client } from "../../pages/client";
const DEMO_WIDGET_ID = "demo-widget-id"; const DEMO_WIDGET_ID = "demo-widget-id";
const DEMO_WIDGET_NAME = "Demo Widget"; const DEMO_WIDGET_NAME = "Demo Widget";

View File

@@ -8,12 +8,12 @@ Please see LICENSE files in the repository root for full details.
import { import {
expect as baseExpect, expect as baseExpect,
type Locator, Locator,
type Page, Page,
type ExpectMatcherState, ExpectMatcherState,
type ElementHandle, ElementHandle,
type PlaywrightTestArgs, PlaywrightTestArgs,
type Fixtures as _Fixtures, Fixtures as _Fixtures,
} from "@playwright/test"; } from "@playwright/test";
import { sanitizeForFilePath } from "playwright-core/lib/utils"; import { sanitizeForFilePath } from "playwright-core/lib/utils";
import AxeBuilder from "@axe-core/playwright"; import AxeBuilder from "@axe-core/playwright";
@@ -21,13 +21,13 @@ import _ from "lodash";
import { extname } from "node:path"; import { extname } from "node:path";
import type { IConfigOptions } from "../src/IConfigOptions"; import type { IConfigOptions } from "../src/IConfigOptions";
import { type Credentials } from "./plugins/homeserver"; import { Credentials } from "./plugins/homeserver";
import { ElementAppPage } from "./pages/ElementAppPage"; import { ElementAppPage } from "./pages/ElementAppPage";
import { Crypto } from "./pages/crypto"; import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts"; import { Toasts } from "./pages/toasts";
import { Bot, type CreateBotOpts } from "./pages/bot"; import { Bot, CreateBotOpts } from "./pages/bot";
import { Webserver } from "./plugins/webserver"; import { Webserver } from "./plugins/webserver";
import { type Options, type Services, test as base } from "./services.ts"; import { Options, Services, test as base } from "./services.ts";
// Enable experimental service worker support // Enable experimental service worker support
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable // See https://playwright.dev/docs/service-workers-experimental#how-to-enable

View File

@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { type BrowserContext, type Page, type TestInfo } from "@playwright/test"; import { BrowserContext, Page, TestInfo } from "@playwright/test";
import { type Readable } from "stream"; import { Readable } from "stream";
import stripAnsi from "strip-ansi"; import stripAnsi from "strip-ansi";
export class Logger { export class Logger {

View File

@@ -202,15 +202,4 @@ export class ElementAppPage {
} }
return this.page.locator(`id=${labelledById ?? describedById}`); return this.page.locator(`id=${labelledById ?? describedById}`);
} }
/**
* Close the notification toast
*/
public closeNotificationToast(): Promise<void> {
// Dismiss "Notification" toast
return this.page
.locator(".mx_Toast_toast", { hasText: "Notifications" })
.getByRole("button", { name: "Dismiss" })
.click();
}
} }

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type JSHandle, type Page } from "@playwright/test"; import { JSHandle, Page } from "@playwright/test";
import { uniqueId } from "lodash"; import { uniqueId } from "lodash";
import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { type MatrixClient } from "matrix-js-sdk/src/matrix";
@@ -41,10 +41,6 @@ export interface CreateBotOpts {
* Whether to bootstrap the secret storage * Whether to bootstrap the secret storage
*/ */
bootstrapSecretStorage?: boolean; bootstrapSecretStorage?: boolean;
/**
* Whether to use a passphrase when creating the recovery key
*/
usePassphrase?: boolean;
} }
const defaultCreateBotOptions = { const defaultCreateBotOptions = {
@@ -52,7 +48,6 @@ const defaultCreateBotOptions = {
autoAcceptInvites: true, autoAcceptInvites: true,
startClient: true, startClient: true,
bootstrapCrossSigning: true, bootstrapCrossSigning: true,
usePassphrase: false,
} satisfies CreateBotOpts; } satisfies CreateBotOpts;
type ExtendedMatrixClient = MatrixClient & { __playwright_recovery_key: GeneratedSecretStorageKey }; type ExtendedMatrixClient = MatrixClient & { __playwright_recovery_key: GeneratedSecretStorageKey };
@@ -197,6 +192,7 @@ export class Bot extends Client {
await clientHandle.evaluate(async (cli) => { await clientHandle.evaluate(async (cli) => {
await cli.initRustCrypto({ useIndexedDB: false }); await cli.initRustCrypto({ useIndexedDB: false });
cli.setGlobalErrorOnUnknownDevices(false);
await cli.startClient(); await cli.startClient();
}); });
@@ -211,8 +207,8 @@ export class Bot extends Client {
} }
if (this.opts.bootstrapSecretStorage) { if (this.opts.bootstrapSecretStorage) {
await clientHandle.evaluate(async (cli, usePassphrase) => { await clientHandle.evaluate(async (cli) => {
const passphrase = usePassphrase ? "new passphrase" : undefined; const passphrase = "new passphrase";
const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase); const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase);
Object.assign(cli, { __playwright_recovery_key: recoveryKey }); Object.assign(cli, { __playwright_recovery_key: recoveryKey });
@@ -221,7 +217,7 @@ export class Bot extends Client {
setupNewKeyBackup: true, setupNewKeyBackup: true,
createSecretStorageKey: () => Promise.resolve(recoveryKey), createSecretStorageKey: () => Promise.resolve(recoveryKey),
}); });
}, this.opts.usePassphrase); });
} }
return clientHandle; return clientHandle;

View File

@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { type JSHandle, type Page } from "@playwright/test"; import { JSHandle, Page } from "@playwright/test";
import { type PageFunctionOn } from "playwright-core/types/structs"; import { PageFunctionOn } from "playwright-core/types/structs";
import { Network } from "./network"; import { Network } from "./network";
import type { import type {
@@ -25,10 +25,9 @@ import type {
StateEvents, StateEvents,
TimelineEvents, TimelineEvents,
AccountDataEvents, AccountDataEvents,
EmptyObject,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types"; import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
import { type Credentials } from "../plugins/homeserver"; import { Credentials } from "../plugins/homeserver";
export class Client { export class Client {
public network: Network; public network: Network;
@@ -364,7 +363,7 @@ export class Client {
event: JSHandle<MatrixEvent>, event: JSHandle<MatrixEvent>,
receiptType?: ReceiptType, receiptType?: ReceiptType,
unthreaded?: boolean, unthreaded?: boolean,
): Promise<EmptyObject> { ): Promise<{}> {
const client = await this.prepareClient(); const client = await this.prepareClient();
return client.evaluate( return client.evaluate(
(client, { event, receiptType, unthreaded }) => { (client, { event, receiptType, unthreaded }) => {
@@ -387,7 +386,7 @@ export class Client {
* @return {Promise} Resolves: {} an empty object. * @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public async setDisplayName(name: string): Promise<EmptyObject> { public async setDisplayName(name: string): Promise<{}> {
const client = await this.prepareClient(); const client = await this.prepareClient();
return client.evaluate(async (cli: MatrixClient, name) => cli.setDisplayName(name), name); return client.evaluate(async (cli: MatrixClient, name) => cli.setDisplayName(name), name);
} }
@@ -398,7 +397,7 @@ export class Client {
* @return {Promise} Resolves: {} an empty object. * @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public async setAvatarUrl(url: string): Promise<EmptyObject> { public async setAvatarUrl(url: string): Promise<{}> {
const client = await this.prepareClient(); const client = await this.prepareClient();
return client.evaluate(async (cli: MatrixClient, url) => cli.setAvatarUrl(url), url); return client.evaluate(async (cli: MatrixClient, url) => cli.setAvatarUrl(url), url);
} }

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type APIRequestContext, type Page, expect } from "@playwright/test"; import { APIRequestContext, Page, expect } from "@playwright/test";
import { type HomeserverInstance } from "../plugins/homeserver"; import { HomeserverInstance } from "../plugins/homeserver";
export class Crypto { export class Crypto {
public constructor( public constructor(

View File

@@ -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. 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 { SettingLevel } from "../../src/settings/SettingLevel"; import type { SettingLevel } from "../../src/settings/SettingLevel";

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type Page, expect, type Locator } from "@playwright/test"; import { Page, expect, Locator } from "@playwright/test";
export class Toasts { export class Toasts {
public constructor(private readonly page: Page) {} public constructor(private readonly page: Page) {}

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type Options } from "../../../services.ts"; import { Options } from "../../../services.ts";
export const isDendrite = ({ homeserverType }: Options): boolean => { export const isDendrite = ({ homeserverType }: Options): boolean => {
return homeserverType === "dendrite" || homeserverType === "pinecone"; return homeserverType === "dendrite" || homeserverType === "pinecone";

View File

@@ -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. Please see LICENSE files in the repository root for full details.
*/ */
import { type ClientServerApi } from "../utils/api.ts"; import { ClientServerApi } from "../utils/api.ts";
export interface HomeserverInstance { export interface HomeserverInstance {
readonly baseUrl: string; readonly baseUrl: string;

View File

@@ -6,11 +6,11 @@ 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. Please see LICENSE files in the repository root for full details.
*/ */
import { type Fixtures } from "../../../element-web-test.ts"; import { Fixtures } from "../../../element-web-test.ts";
export const consentHomeserver: Fixtures = { export const consentHomeserver: Fixtures = {
_homeserver: [ _homeserver: [
async ({ _homeserver: container, mailpit }, use) => { async ({ _homeserver: container, mailhog }, use) => {
container container
.withCopyDirectoriesToContainer([ .withCopyDirectoriesToContainer([
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" }, { source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
@@ -18,7 +18,7 @@ export const consentHomeserver: Fixtures = {
.withConfig({ .withConfig({
email: { email: {
enable_notifs: false, enable_notifs: false,
smtp_host: "mailpit", smtp_host: "mailhog",
smtp_port: 1025, smtp_port: 1025,
smtp_user: "username", smtp_user: "username",
smtp_pass: "password", smtp_pass: "password",

View File

@@ -6,17 +6,17 @@ 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. Please see LICENSE files in the repository root for full details.
*/ */
import { type Fixtures } from "../../../element-web-test.ts"; import { Fixtures } from "../../../element-web-test.ts";
export const emailHomeserver: Fixtures = { export const emailHomeserver: Fixtures = {
_homeserver: [ _homeserver: [
async ({ _homeserver: container, mailpit }, use) => { async ({ _homeserver: container, mailhog }, use) => {
container.withConfig({ container.withConfig({
enable_registration_without_verification: undefined, enable_registration_without_verification: undefined,
disable_msisdn_registration: undefined, disable_msisdn_registration: undefined,
registrations_require_3pid: ["email"], registrations_require_3pid: ["email"],
email: { email: {
smtp_host: "mailpit", smtp_host: "mailhog",
smtp_port: 1025, smtp_port: 1025,
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>", notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
app_name: "my_branded_matrix_server", app_name: "my_branded_matrix_server",

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import { TestContainers } from "testcontainers"; import { TestContainers } from "testcontainers";
import { OAuthServer } from "../../oauth_server"; import { OAuthServer } from "../../oauth_server";
import { type Fixtures } from "../../../element-web-test.ts"; import { Fixtures } from "../../../element-web-test.ts";
export const legacyOAuthHomeserver: Fixtures = { export const legacyOAuthHomeserver: Fixtures = {
oAuthServer: [ oAuthServer: [

View File

@@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts"; import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts";
import { type Fixtures } from "../../../element-web-test.ts"; import { Fixtures } from "../../../element-web-test.ts";
export const masHomeserver: Fixtures = { export const masHomeserver: Fixtures = {
mas: [ mas: [
async ({ _homeserver: homeserver, logger, network, postgres, mailpit }, use) => { async ({ _homeserver: homeserver, logger, network, postgres, mailhog }, use) => {
const config = { const config = {
clients: [ clients: [
{ {

View File

@@ -5,7 +5,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. Please see LICENSE files in the repository root for full details.
*/ */
import { type Fixtures } from "../../../element-web-test.ts"; import { Fixtures } from "../../../element-web-test.ts";
export const uiaLongSessionTimeoutHomeserver: Fixtures = { export const uiaLongSessionTimeoutHomeserver: Fixtures = {
synapseConfig: [ synapseConfig: [

View File

@@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
import http from "http"; import http from "http";
import express from "express"; import express from "express";
import { type AddressInfo } from "net"; import { AddressInfo } from "net";
import { type TestInfo } from "@playwright/test"; import { TestInfo } from "@playwright/test";
import { randB64Bytes } from "../utils/rand.ts"; import { randB64Bytes } from "../utils/rand.ts";

View File

@@ -5,9 +5,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. Please see LICENSE files in the repository root for full details.
*/ */
import { type APIRequestContext } from "@playwright/test"; import { APIRequestContext } from "@playwright/test";
import { type Credentials } from "../homeserver"; import { Credentials } from "../homeserver";
export type Verb = "GET" | "POST" | "PUT" | "DELETE"; export type Verb = "GET" | "POST" | "PUT" | "DELETE";

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/ */
import * as http from "http"; import * as http from "http";
import { type AddressInfo } from "net"; import { AddressInfo } from "net";
export class Webserver { export class Webserver {
private server?: http.Server; private server?: http.Server;

View File

@@ -1,16 +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.
*/
export default class ExampleModule {
static moduleApiVersion = "^0.1.0";
constructor(api) {
this.api = api;
}
async load() {
alert("Testing module loading successful!");
}
}

View File

@@ -6,21 +6,21 @@ Please see LICENSE files in the repository root for full details.
*/ */
import { test as base } from "@playwright/test"; import { test as base } from "@playwright/test";
import { type MailpitClient } from "mailpit-api"; import mailhog from "mailhog";
import { Network, type StartedNetwork } from "testcontainers"; import { Network, StartedNetwork } from "testcontainers";
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"; import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import { type SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts"; import { SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts";
import { Logger } from "./logger.ts"; import { Logger } from "./logger.ts";
import { type StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts"; import { StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts";
import { type HomeserverContainer, type StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts"; import { HomeserverContainer, StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts";
import { MailhogContainer, type StartedMailhogContainer } from "./testcontainers/mailpit.ts"; import { MailhogContainer, StartedMailhogContainer } from "./testcontainers/mailhog.ts";
import { type OAuthServer } from "./plugins/oauth_server"; import { OAuthServer } from "./plugins/oauth_server";
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts"; import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts";
import { type HomeserverType } from "./plugins/homeserver"; import { HomeserverType } from "./plugins/homeserver";
export interface TestFixtures { export interface TestFixtures {
mailpitClient: MailpitClient; mailhogClient: mailhog.API;
} }
export interface Services { export interface Services {
@@ -28,7 +28,7 @@ export interface Services {
network: StartedNetwork; network: StartedNetwork;
postgres: StartedPostgreSqlContainer; postgres: StartedPostgreSqlContainer;
mailpit: StartedMailhogContainer; mailhog: StartedMailhogContainer;
synapseConfig: SynapseConfig; synapseConfig: SynapseConfig;
_homeserver: HomeserverContainer<any>; _homeserver: HomeserverContainer<any>;
@@ -90,20 +90,20 @@ export const test = base.extend<TestFixtures, Services & Options>({
{ scope: "worker" }, { scope: "worker" },
], ],
mailpit: [ mailhog: [
async ({ logger, network }, use) => { async ({ logger, network }, use) => {
const container = await new MailhogContainer() const container = await new MailhogContainer()
.withNetwork(network) .withNetwork(network)
.withNetworkAliases("mailpit") .withNetworkAliases("mailhog")
.withLogConsumer(logger.getConsumer("mailpit")) .withLogConsumer(logger.getConsumer("mailhog"))
.start(); .start();
await use(container); await use(container);
await container.stop(); await container.stop();
}, },
{ scope: "worker" }, { scope: "worker" },
], ],
mailpitClient: async ({ mailpit: container }, use) => { mailhogClient: async ({ mailhog: container }, use) => {
await container.client.deleteMessages(); await container.client.deleteAll();
await use(container.client); await use(container.client);
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

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