mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-19 02:20:17 +00:00
Compare commits
2 Commits
v1.11.93
...
robin/reve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d32efb27 | ||
|
|
f05df80b46 |
@@ -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
|
|
||||||
|
|||||||
@@ -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": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
// We do this sometimes to brand interfaces
|
// We do this sometimes to brand interfaces
|
||||||
allowInterfaces: "with-single-extends",
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 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
|
||||||
|
|
||||||
|
|||||||
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -11,10 +11,8 @@
|
|||||||
/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/crypto/ @element-hq/element-crypto-web-reviewers
|
|
||||||
/playwright/e2e/settings/encryption-user-tab/ @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
|
||||||
|
|||||||
6
.github/workflows/build_develop.yml
vendored
6
.github/workflows/build_develop.yml
vendored
@@ -26,12 +26,6 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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
|
||||||
|
|||||||
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
Normal file
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Close stale flaky issues
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
close:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
only-labels: "Z-Flaky-Test"
|
||||||
|
days-before-stale: 14
|
||||||
|
days-before-close: 0
|
||||||
|
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
||||||
|
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
||||||
|
operations-per-run: 100
|
||||||
27
.github/workflows/triage-stale.yml
vendored
27
.github/workflows/triage-stale.yml
vendored
@@ -1,27 +0,0 @@
|
|||||||
name: Close stale issues & PRs
|
|
||||||
on:
|
|
||||||
workflow_dispatch: {}
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
permissions: {}
|
|
||||||
jobs:
|
|
||||||
close:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
actions: write
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v9
|
|
||||||
with:
|
|
||||||
operations-per-run: 100
|
|
||||||
# Flaky test issue closing
|
|
||||||
only-issue-labels: "Z-Flaky-Test"
|
|
||||||
days-before-issue-stale: 14
|
|
||||||
days-before-issue-close: 0
|
|
||||||
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
|
||||||
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
|
||||||
# Stale PR closing
|
|
||||||
days-before-pr-stale: 180
|
|
||||||
days-before-pr-close: 0
|
|
||||||
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,7 +26,6 @@ electron/pub
|
|||||||
/coverage
|
/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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,84 +1,3 @@
|
|||||||
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy.
|
|
||||||
* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh.
|
|
||||||
* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh.
|
|
||||||
* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot.
|
|
||||||
* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros.
|
|
||||||
* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros.
|
|
||||||
* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR.
|
|
||||||
* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent.
|
|
||||||
* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros.
|
|
||||||
* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR.
|
|
||||||
* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy.
|
|
||||||
* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros.
|
|
||||||
* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros.
|
|
||||||
* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros.
|
|
||||||
* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd.
|
|
||||||
* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr.
|
|
||||||
* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy.
|
|
||||||
* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown.
|
|
||||||
* Revert `GoToHome` keyboard shortcut to `Ctrl`–`Shift`–`H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate.
|
|
||||||
* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
|
|
||||||
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
|
|
||||||
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
|
|
||||||
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
|
|
||||||
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
|
|
||||||
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
|
|
||||||
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
|
|
||||||
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
|
|
||||||
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
|
|
||||||
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
|
|
||||||
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
|
|
||||||
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
|
|
||||||
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
|
|
||||||
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
|
|
||||||
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
|
|
||||||
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
|
|
||||||
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
|
|
||||||
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
|
|
||||||
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
|
|
||||||
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
|
|
||||||
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
|
|
||||||
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
|
|
||||||
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
|
||||||
==================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
|
|
||||||
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
|
|
||||||
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
|
|
||||||
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
|
|
||||||
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
|
|
||||||
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
|
|
||||||
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
|
|
||||||
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|||||||
@@ -189,6 +189,89 @@ give away to contributors - if you feel that Matrix-branded apparel is missing
|
|||||||
from your life, please mail us your shipping address to matrix at matrix.org
|
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
|
||||||
|
|||||||
@@ -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
120
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +319,8 @@ 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
|
||||||
@@ -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
2
debian/control
vendored
@@ -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.
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
# Developer Guide
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
|
||||||
about where to start. Before starting work on a feature, it's best to ensure
|
|
||||||
your plan aligns well with our vision for Element. Please chat with the team in
|
|
||||||
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
|
||||||
you start so we can ensure it's something we'd be willing to merge.
|
|
||||||
|
|
||||||
You should also familiarise yourself with the ["Here be Dragons" guide
|
|
||||||
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
|
||||||
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
|
||||||
|
|
||||||
Please note that Element is intended to run correctly without access to the public
|
|
||||||
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
|
||||||
hosted by external CDNs or servers but instead please package all dependencies
|
|
||||||
into Element itself.
|
|
||||||
|
|
||||||
## Setting up a dev environment
|
|
||||||
|
|
||||||
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
|
||||||
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
|
||||||
in git and to make local changes without having to manually rebuild each time.
|
|
||||||
|
|
||||||
First clone and build `matrix-js-sdk`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
|
||||||
pushd matrix-js-sdk
|
|
||||||
yarn link
|
|
||||||
yarn install
|
|
||||||
popd
|
|
||||||
```
|
|
||||||
|
|
||||||
Clone the repo and switch to the `element-web` directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/element-hq/element-web.git
|
|
||||||
cd element-web
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure the app by copying `config.sample.json` to `config.json` and
|
|
||||||
modifying it. See the [configuration docs](docs/config.md) for details.
|
|
||||||
|
|
||||||
Finally, build and start Element itself:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn install
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait a few seconds for the initial build to finish; you should see something like:
|
|
||||||
|
|
||||||
```
|
|
||||||
[element-js] <s> [webpack.Progress] 100%
|
|
||||||
[element-js]
|
|
||||||
[element-js] ℹ 「wdm」: 1840 modules
|
|
||||||
[element-js] ℹ 「wdm」: Compiled successfully.
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember, the command will not terminate since it runs the web server
|
|
||||||
and rebuilds source files when they change. This development server also
|
|
||||||
disables caching, so do NOT use it in production.
|
|
||||||
|
|
||||||
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
|
||||||
|
|
||||||
**Note**: The build script uses inotify by default on Linux to monitor directories
|
|
||||||
for changes. If the inotify limits are too low your build will fail silently or with
|
|
||||||
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
|
||||||
of at least `128M` and instance limit around `512`.
|
|
||||||
|
|
||||||
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
|
||||||
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
|
||||||
|
|
||||||
To set a new inotify watch and instance limit, execute:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo sysctl fs.inotify.max_user_watches=131072
|
|
||||||
sudo sysctl fs.inotify.max_user_instances=512
|
|
||||||
sudo sysctl -p
|
|
||||||
```
|
|
||||||
|
|
||||||
If you wish, you can make the new limits permanent, by executing:
|
|
||||||
|
|
||||||
```
|
|
||||||
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
|
||||||
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
|
||||||
sudo sysctl -p
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
|
||||||
|
|
||||||
If any of these steps error with, `file table overflow`, you are probably on a mac
|
|
||||||
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
|
||||||
You'll need to do this in each new terminal you open before building Element.
|
|
||||||
|
|
||||||
## Running the tests
|
|
||||||
|
|
||||||
There are a number of application-level tests in the `tests` directory; these
|
|
||||||
are designed to run with Jest and JSDOM. To run them
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn test
|
|
||||||
```
|
|
||||||
|
|
||||||
### End-to-End tests
|
|
||||||
|
|
||||||
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
|
||||||
|
|
||||||
## General github guidelines
|
|
||||||
|
|
||||||
1. **Pull requests must only be filed against the `develop` branch.**
|
|
||||||
2. Try to keep your pull requests concise. Split them up if necessary.
|
|
||||||
3. Ensure that you provide a description that explains the fix/feature and its intent.
|
|
||||||
|
|
||||||
## Adding new code
|
|
||||||
|
|
||||||
New code should be committed as follows:
|
|
||||||
|
|
||||||
- All new components: https://github.com/element-hq/element-web/tree/develop/src/components
|
|
||||||
- CSS: https://github.com/element-hq/element-web/tree/develop/res/css
|
|
||||||
- Theme specific CSS & resources: https://github.com/element-hq/element-web/tree/develop/res/themes
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
entrypoint_log() {
|
|
||||||
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
|
||||||
echo "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Copy these config files as a base
|
|
||||||
mkdir /tmp/element-web-config
|
|
||||||
cp /app/config*.json /tmp/element-web-config/
|
|
||||||
|
|
||||||
# If there are modules to be loaded
|
|
||||||
if [ -d "/tmp/element-web-modules" ]; then
|
|
||||||
cd /tmp/element-web-modules
|
|
||||||
|
|
||||||
for MODULE in *
|
|
||||||
do
|
|
||||||
# If the module has a package.json, use its main field as the entrypoint
|
|
||||||
ENTRYPOINT="index.js"
|
|
||||||
if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then
|
|
||||||
ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json")
|
|
||||||
fi
|
|
||||||
|
|
||||||
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
|
|
||||||
|
|
||||||
# Append the module to the config
|
|
||||||
jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
@@ -18,12 +18,8 @@ server {
|
|||||||
}
|
}
|
||||||
# covers config.json and config.hostname.json requests as it is prefix.
|
# 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;
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
25
package.json
25
package.json
@@ -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",
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
|
|
||||||
|
|
||||||
test.describe("Key storage out of sync toast", () => {
|
|
||||||
let recoveryKey: GeneratedSecretStorageKey;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
|
||||||
const res = await createBot(page, homeserver, credentials);
|
|
||||||
recoveryKey = res.recoveryKey;
|
|
||||||
|
|
||||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
|
||||||
|
|
||||||
await deleteCachedSecrets(page);
|
|
||||||
|
|
||||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
|
||||||
await page.getByRole("button", { name: "Add room" }).click();
|
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
|
||||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
|
||||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
|
||||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
|
||||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -8,9 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
import { type 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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -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!`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { test, expect } from "../../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Search section of the room list", () => {
|
|
||||||
test.use({
|
|
||||||
labsFlags: ["feature_new_room_list"],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the search section of the room list
|
|
||||||
* @param page
|
|
||||||
*/
|
|
||||||
function getSearchSection(page: Page) {
|
|
||||||
return page.getByRole("search");
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, app, user }) => {
|
|
||||||
// The notification toast is displayed above the search section
|
|
||||||
await app.closeNotificationToast();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
|
||||||
const searchSection = getSearchSection(page);
|
|
||||||
// exact=false to ignore the shortcut which is related to the OS
|
|
||||||
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
|
|
||||||
await expect(searchSection).toMatchScreenshot("search-section.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
|
|
||||||
const searchSection = getSearchSection(page);
|
|
||||||
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
|
|
||||||
// The spotlight should be displayed
|
|
||||||
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
|
|
||||||
const searchSection = getSearchSection(page);
|
|
||||||
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
|
|
||||||
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
|
|
||||||
// The room directory should be displayed
|
|
||||||
await expect(dialog).toBeVisible();
|
|
||||||
// The public room filter should be displayed
|
|
||||||
await expect(dialog.getByText("Public rooms")).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { test, expect } from "../../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Search section of the room list", () => {
|
|
||||||
test.use({
|
|
||||||
labsFlags: ["feature_new_room_list"],
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the room list view
|
|
||||||
* @param page
|
|
||||||
*/
|
|
||||||
function getRoomListView(page: Page) {
|
|
||||||
return page.getByTestId("room-list-view");
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, app, user }) => {
|
|
||||||
// The notification toast is displayed above the search section
|
|
||||||
await app.closeNotificationToast();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
|
||||||
const roomListView = getRoomListView(page);
|
|
||||||
await expect(roomListView).toMatchScreenshot("room-list-view.png");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Module loading", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Manny",
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Example Module", () => {
|
|
||||||
test.use({
|
|
||||||
config: {
|
|
||||||
modules: ["/modules/example-module.js"],
|
|
||||||
},
|
|
||||||
page: async ({ page }, use) => {
|
|
||||||
await page.route("/modules/example-module.js", async (route) => {
|
|
||||||
await route.fulfill({ path: "playwright/sample-files/example-module.js" });
|
|
||||||
});
|
|
||||||
await use(page);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should show alert", async ({ page }) => {
|
|
||||||
const dialogPromise = page.waitForEvent("dialog");
|
|
||||||
await page.goto("/");
|
|
||||||
const dialog = await dialogPromise;
|
|
||||||
expect(dialog.message()).toBe("Testing module loading successful!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -6,14 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
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();
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 ({
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "./index";
|
|
||||||
import { checkDeviceIsCrossSigned } from "../../crypto/utils";
|
|
||||||
import { bootstrapCrossSigningForClient } from "../../../pages/client";
|
|
||||||
|
|
||||||
test.describe("Advanced section in Encryption tab", () => {
|
|
||||||
test.beforeEach(async ({ page, app, homeserver, credentials, util }) => {
|
|
||||||
const clientHandle = await app.client.prepareClient();
|
|
||||||
// Reset cross signing in order to have a verified session
|
|
||||||
await bootstrapCrossSigningForClient(clientHandle, credentials, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should show the encryption details", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
|
||||||
await util.openEncryptionTab();
|
|
||||||
const section = util.getEncryptionDetailsSection();
|
|
||||||
|
|
||||||
const deviceId = await page.evaluate(() => window.mxMatrixClientPeg.get().getDeviceId());
|
|
||||||
await expect(section.getByText(deviceId)).toBeVisible();
|
|
||||||
|
|
||||||
await expect(section).toMatchScreenshot("encryption-details.png", {
|
|
||||||
mask: [section.getByTestId("deviceId"), section.getByTestId("sessionKey")],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should show the import room keys dialog", async ({ page, app, util }) => {
|
|
||||||
await util.openEncryptionTab();
|
|
||||||
const section = util.getEncryptionDetailsSection();
|
|
||||||
|
|
||||||
await section.getByRole("button", { name: "Import keys" }).click();
|
|
||||||
await expect(page.getByRole("heading", { name: "Import room keys" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should show the export room keys dialog", async ({ page, app, util }) => {
|
|
||||||
await util.openEncryptionTab();
|
|
||||||
const section = util.getEncryptionDetailsSection();
|
|
||||||
|
|
||||||
await section.getByRole("button", { name: "Export keys" }).click();
|
|
||||||
await expect(page.getByRole("heading", { name: "Export room keys" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should reset the cryptographic identity",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, credentials, util }) => {
|
|
||||||
const tab = await util.openEncryptionTab();
|
|
||||||
const section = util.getEncryptionDetailsSection();
|
|
||||||
|
|
||||||
await section.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
|
||||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("reset-cryptographic-identity.png");
|
|
||||||
await tab.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// Fill password dialog and validate
|
|
||||||
const dialog = page.locator(".mx_InteractiveAuthDialog");
|
|
||||||
await dialog.getByRole("textbox", { name: "Password" }).fill(credentials.password);
|
|
||||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
await expect(section.getByRole("button", { name: "Reset cryptographic identity" })).toBeVisible();
|
|
||||||
|
|
||||||
// After resetting the identity, the user should set up a new recovery key
|
|
||||||
await expect(
|
|
||||||
util.getEncryptionRecoverySection().getByRole("button", { name: "Set up recovery" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await checkDeviceIsCrossSigned(app);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
|
||||||
|
|
||||||
import { test, expect } from ".";
|
|
||||||
import {
|
|
||||||
checkDeviceIsConnectedKeyBackup,
|
|
||||||
checkDeviceIsCrossSigned,
|
|
||||||
createBot,
|
|
||||||
deleteCachedSecrets,
|
|
||||||
verifySession,
|
|
||||||
} from "../../crypto/utils";
|
|
||||||
|
|
||||||
test.describe("Encryption tab", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
});
|
|
||||||
|
|
||||||
let recoveryKey: GeneratedSecretStorageKey;
|
|
||||||
let expectedBackupVersion: string;
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
|
||||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
|
||||||
const res = await createBot(page, homeserver, credentials);
|
|
||||||
recoveryKey = res.recoveryKey;
|
|
||||||
expectedBackupVersion = res.expectedBackupVersion;
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should show a 'Verify this device' button if the device is unverified",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, util }) => {
|
|
||||||
const dialog = await util.openEncryptionTab();
|
|
||||||
const content = util.getEncryptionTabContent();
|
|
||||||
|
|
||||||
// The user's device is in an unverified state, therefore the only option available to them here is to verify it
|
|
||||||
const verifyButton = dialog.getByRole("button", { name: "Verify this device" });
|
|
||||||
await expect(verifyButton).toBeVisible();
|
|
||||||
await expect(content).toMatchScreenshot("verify-device-encryption-tab.png");
|
|
||||||
await verifyButton.click();
|
|
||||||
|
|
||||||
await util.verifyDevice(recoveryKey);
|
|
||||||
|
|
||||||
await expect(content).toMatchScreenshot("default-tab.png", {
|
|
||||||
mask: [content.getByTestId("deviceId"), content.getByTestId("sessionKey")],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check that our device is now cross-signed
|
|
||||||
await checkDeviceIsCrossSigned(app);
|
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
|
||||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Test what happens if the cross-signing secrets are in secret storage but are not cached in the local DB.
|
|
||||||
//
|
|
||||||
// This can happen if we verified another device and secret-gossiping failed, or the other device itself lacked the secrets.
|
|
||||||
// We simulate this case by deleting the cached secrets in the indexedDB.
|
|
||||||
test(
|
|
||||||
"should prompt to enter the recovery key when the secrets are not cached locally",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, util }) => {
|
|
||||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
|
||||||
// We need to delete the cached secrets
|
|
||||||
await deleteCachedSecrets(page);
|
|
||||||
|
|
||||||
await util.openEncryptionTab();
|
|
||||||
// We ask the user to enter the recovery key
|
|
||||||
const dialog = util.getEncryptionTabContent();
|
|
||||||
const enterKeyButton = dialog.getByRole("button", { name: "Enter recovery key" });
|
|
||||||
await expect(enterKeyButton).toBeVisible();
|
|
||||||
await expect(dialog).toMatchScreenshot("out-of-sync-recovery.png");
|
|
||||||
await enterKeyButton.click();
|
|
||||||
|
|
||||||
// Fill the recovery key
|
|
||||||
await util.enterRecoveryKey(recoveryKey);
|
|
||||||
await expect(dialog).toMatchScreenshot("default-tab.png", {
|
|
||||||
mask: [dialog.getByTestId("deviceId"), dialog.getByTestId("sessionKey")],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check that our device is now cross-signed
|
|
||||||
await checkDeviceIsCrossSigned(app);
|
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
|
||||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test("should display the reset identity panel when the user clicks on 'Forgot recovery key?'", async ({
|
|
||||||
page,
|
|
||||||
app,
|
|
||||||
util,
|
|
||||||
}) => {
|
|
||||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
|
||||||
// We need to delete the cached secrets
|
|
||||||
await deleteCachedSecrets(page);
|
|
||||||
|
|
||||||
// The "Key storage is out sync" section is displayed and the user click on the "Forgot recovery key?" button
|
|
||||||
await util.openEncryptionTab();
|
|
||||||
const dialog = util.getEncryptionTabContent();
|
|
||||||
await dialog.getByRole("button", { name: "Forgot recovery key?" }).click();
|
|
||||||
|
|
||||||
// The user is prompted to reset their identity
|
|
||||||
await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: `
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user