Compare commits
71 Commits
matthew/mo
...
robin/call
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
569d525c6e | ||
|
|
7799cb2ec5 | ||
|
|
e47d7aaaff | ||
|
|
8857c07acb | ||
|
|
28ed506fe1 | ||
|
|
76b3be6263 | ||
|
|
6c768b8b32 | ||
|
|
809ada17a4 | ||
|
|
c7762a80f1 | ||
|
|
261923832d | ||
|
|
3daa1bf06a | ||
|
|
e5c8d7dbf0 | ||
|
|
441119ca3a | ||
|
|
acb3e781a4 | ||
|
|
3fb1f6ef4d | ||
|
|
cbfbfad959 | ||
|
|
7546bbc1f0 | ||
|
|
6a10c86d7a | ||
|
|
6f9e3bfe3e | ||
|
|
61d55462df | ||
|
|
d391c69e53 | ||
|
|
a6afff9759 | ||
|
|
073d8e0b86 | ||
|
|
ecf5d720b0 | ||
|
|
b7fea97bb6 | ||
|
|
90801eb38b | ||
|
|
e02da752f0 | ||
|
|
dd2da5c132 | ||
|
|
52060235e4 | ||
|
|
8d67e88b1d | ||
|
|
a365533367 | ||
|
|
6dbc3b489a | ||
|
|
f822653d65 | ||
|
|
09db599fe0 | ||
|
|
c47ce59478 | ||
|
|
f9a85d37fa | ||
|
|
2abd5342c2 | ||
|
|
85f80b1d0a | ||
|
|
4b9382f888 | ||
|
|
03a5ee1c5b | ||
|
|
902146a829 | ||
|
|
43e918b71e | ||
|
|
53c97dfa50 | ||
|
|
f7b010a0b3 | ||
|
|
161323b595 | ||
|
|
8bf3ec29b9 | ||
|
|
039b95eba0 | ||
|
|
6b80d3fca0 | ||
|
|
784abbbe14 | ||
|
|
4d55e8f433 | ||
|
|
02990bd275 | ||
|
|
6fa8032caa | ||
|
|
67658aef56 | ||
|
|
f2fae82e32 | ||
|
|
ef69c0ddc7 | ||
|
|
bc7fe25974 | ||
|
|
426a2066d9 | ||
|
|
047e8e8a9c | ||
|
|
4de9fe60ae | ||
|
|
52b42c0b1c | ||
|
|
bb8b4d7991 | ||
|
|
ec994884fb | ||
|
|
4a8ba810a9 | ||
|
|
40a6c69f1a | ||
|
|
eed6b6df12 | ||
|
|
7b3ce5d9b2 | ||
|
|
8941724020 | ||
|
|
4a231c6450 | ||
|
|
3c690e685a | ||
|
|
0a8393c9e1 | ||
|
|
0fa52e610e |
5
.github/CODEOWNERS
vendored
@@ -11,10 +11,11 @@
|
|||||||
/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/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
|
||||||
/src/components/views/dialogs/devtools/Crypto.tsx @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
|
||||||
|
|
||||||
# Ignore translations as those will be updated by GHA for Localazy download
|
# Ignore translations as those will be updated by GHA for Localazy download
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Dockerhub
|
name: Docker
|
||||||
on:
|
on:
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
push:
|
push:
|
||||||
@@ -15,19 +15,20 @@ jobs:
|
|||||||
environment: 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
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
|
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
|
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
|
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
@@ -37,12 +38,20 @@ jobs:
|
|||||||
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
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||||
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
|
||||||
2
.github/workflows/release.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
checks: read
|
checks: read
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for dockerhub
|
- name: Wait for docker build
|
||||||
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
@@ -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@7ca807c2ba3401be532d29a876b93262108099fb
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
21
.github/workflows/triage-stale-flaky-tests.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
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
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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."
|
||||||
33
CHANGELOG.md
@@ -1,3 +1,36 @@
|
|||||||
|
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)
|
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|||||||
@@ -189,89 +189,6 @@ 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
|
||||||
|
|||||||
120
README.md
@@ -182,123 +182,11 @@ Dockerfile.
|
|||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
Before attempting to develop on Element you **must** read the [developer guide
|
Please read through the following:
|
||||||
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.
|
|
||||||
|
|
||||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
1. [Developer guide](./developer_guide.md)
|
||||||
about where to start. Before starting work on a feature, it's best to ensure
|
2. [Code style](./code_style.md)
|
||||||
your plan aligns well with our vision for Element. Please chat with the team in
|
3. [Contribution guide](./CONTRIBUTING.md)
|
||||||
[#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,15 +5,6 @@ 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
|
||||||
|
|
||||||
@@ -234,17 +225,19 @@ 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. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
1. Component source files are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||||
2. Class components must always have a `Props` interface declared immediately above them. It can be
|
2. They are organised in a typically two-level hierarchy - first whether the component is a view or a structure, and then a broad functional grouping (e.g. 'rooms' here)
|
||||||
|
3. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||||
|
4. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||||
empty if the component accepts no props.
|
empty if the component accepts no props.
|
||||||
3. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
5. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||||
4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
6. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||||
instead.
|
instead.
|
||||||
5. One component per file, except when a component is a utility component specifically for the "primary"
|
7. 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.
|
||||||
6. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
8. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
||||||
or stores.
|
or stores.
|
||||||
7. Stores should use a singleton pattern with a static instance property:
|
9. Stores should use a singleton pattern with a static instance property:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class FooStore {
|
class FooStore {
|
||||||
@@ -261,44 +254,41 @@ Inheriting all the rules of TypeScript, the following additionally apply:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Stores must support using an alternative MatrixClient and dispatcher instance.
|
10. Stores must support using an alternative MatrixClient and dispatcher instance.
|
||||||
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
11. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||||
cycles during runtime where components accidentally include more of the app than they intended.
|
cycles during runtime where components accidentally include more of the app than they intended.
|
||||||
10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||||
if at all possible.
|
if at all possible.
|
||||||
11. A component should only use CSS class names in line with the component name.
|
13. 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).
|
||||||
|
|
||||||
12. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
14. 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.
|
||||||
13. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
15. 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.
|
||||||
14. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
16. 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.
|
||||||
15. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
17. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
||||||
isolated components.
|
isolated components.
|
||||||
16. Components should serve a single, or near-single, purpose.
|
18. Components should serve a single, or near-single, purpose.
|
||||||
17. Prefer to derive information from component properties rather than establish state.
|
19. Prefer to derive information from component properties rather than establish state.
|
||||||
18. Do not use `React.Component::forceUpdate`.
|
20. 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. Class names must be prefixed with "mx\_".
|
1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component).
|
||||||
2. Class names must denote the component which defines them, followed by any context.
|
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.
|
||||||
The context is not further specified here in terms of meaning or syntax.
|
3. Class names must be prefixed with "mx\_".
|
||||||
Use whatever is appropriate for your implementation use case.
|
4. Class names must strictly denote the component which defines them.
|
||||||
Some examples:
|
For example: `mx_MyFoo` for `MyFoo` component.
|
||||||
1. `mx_MyFoo`
|
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.
|
||||||
2. `mx_MyFoo_avatar`
|
6. Use the `$font` variables instead of manual values.
|
||||||
3. `mx_MyFoo_avatarUser`
|
7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||||
4. `mx_MyFoo_avatar--user`
|
8. Use the whole class name instead of shortcuts:
|
||||||
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 {
|
||||||
@@ -309,7 +299,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Break multiple selectors over multiple lines this way:
|
9. Break multiple selectors over multiple lines this way:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo,
|
.mx_MyFoo,
|
||||||
@@ -319,9 +309,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
||||||
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||||
[documented](#comments) for what the values mean:
|
[documented](#comments) for what the values mean:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo {
|
.mx_MyFoo {
|
||||||
@@ -331,7 +321,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
9. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
12. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
||||||
|
13. The CSS for a component can override the rules for child components. For instance, .mxRoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough.
|
||||||
|
14. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
126
developer_guide.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# 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
|
||||||
67
docs/MVVM.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# MVVM
|
||||||
|
|
||||||
|
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
|
||||||
|
|
||||||
|
1. Model: This is where the business logic and data resides.
|
||||||
|
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
|
||||||
|
3. View: This is the UI code itself and depends on the view model.
|
||||||
|
|
||||||
|
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
||||||
|
|
||||||
|
### Practical guidelines for MVVM in element-web
|
||||||
|
|
||||||
|
#### Model
|
||||||
|
|
||||||
|
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
||||||
|
|
||||||
|
#### View Model
|
||||||
|
|
||||||
|
1. View model is always a custom react hook named like `useFooViewModel()`.
|
||||||
|
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
||||||
|
```ts
|
||||||
|
inteface FooViewState {
|
||||||
|
somethingUseful: string;
|
||||||
|
somethingElse: BarType;
|
||||||
|
update: () => Promise<void>
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Any react state that your UI needs must be in the view model.
|
||||||
|
|
||||||
|
#### View
|
||||||
|
|
||||||
|
1. Views are simple react components (eg: `FooView`).
|
||||||
|
2. Views usually start by calling the view model hook, eg:
|
||||||
|
```tsx
|
||||||
|
const FooView: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const vm = useFooViewModel();
|
||||||
|
....
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
{vm.somethingUseful}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Views are also allowed to accept the view model as a prop, eg:
|
||||||
|
```tsx
|
||||||
|
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
||||||
|
....
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
{vm.somethingUseful}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Multiple views can share the same view model if necessary.
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
|
||||||
|
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||||
|
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||||
|
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
||||||
@@ -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 security key)
|
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
||||||
|
|
||||||
### `desktop_builds` and `mobile_builds`
|
### `desktop_builds` and `mobile_builds`
|
||||||
|
|
||||||
@@ -163,14 +163,14 @@ These two options describe the various availability for the application. When th
|
|||||||
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
|
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
|
||||||
at to see if the link should be to somewhere else.
|
at to see if the link should be to somewhere else.
|
||||||
|
|
||||||
Starting with `desktop_builds`, the following subproperties are available:
|
Starting with `desktop_builds`, the following sub-properties are available:
|
||||||
|
|
||||||
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
|
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
|
||||||
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
|
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
|
||||||
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
|
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
|
||||||
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
||||||
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
|
5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
|
||||||
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
|
6. `url_win64arm`: Optional. Direct link to download Windows ARM 64-bit desktop app.
|
||||||
7. `url_linux`: Optional. Direct link to download Linux desktop app.
|
7. `url_linux`: Optional. Direct link to download Linux desktop app.
|
||||||
|
|
||||||
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
|
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
|
||||||
@@ -592,3 +592,4 @@ 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.
|
||||||
|
|||||||
@@ -112,3 +112,7 @@ 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 dockerhub/]
|
D2[\Check docker build/]
|
||||||
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, `*.element.io`, and packages.element.io.
|
We ship Element Web to dockerhub, ghcr.io, `*.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
|
- [ ] Check that element-web has shipped to dockerhub & ghcr.io
|
||||||
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
- [ ] Check that 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,6 +38,8 @@ 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: [
|
||||||
|
|||||||
@@ -74,12 +74,13 @@
|
|||||||
"@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.30001699",
|
||||||
"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",
|
||||||
@@ -87,7 +88,7 @@
|
|||||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||||
"@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": "^9.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": "^3.0.0",
|
||||||
@@ -273,7 +274,7 @@
|
|||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"postcss-scss": "^4.0.4",
|
"postcss-scss": "^4.0.4",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.5.1",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/playwright:v1.49.1-noble
|
FROM mcr.microsoft.com/playwright:v1.50.1-noble
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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,
|
||||||
@@ -24,8 +25,11 @@ test.describe("Encryption state after registration", () => {
|
|||||||
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, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||||
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
// Wait for the ui to load
|
||||||
await expect(page.getByText("This session is backing up your keys.")).toBeVisible();
|
await expect(page.locator(".mx_MatrixChat")).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, mailpitClient, app }, testInfo) => {
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ test.describe("Backups", () => {
|
|||||||
|
|
||||||
// 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: "Security Key" })).toBeVisible();
|
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||||
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
|
await currentDialogLocator.getByLabel("Recovery 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 +90,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: "Security Key" })).toBeVisible();
|
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||||
// But cancel the security key dialog, to simulate not having the secret storage passphrase
|
// But cancel the recovery 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();
|
||||||
|
|||||||
@@ -186,7 +186,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("Security Key").fill(secretStorageKey);
|
await page.getByPlaceholder("Recovery Key").fill(secretStorageKey);
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// Enter the password
|
// Enter the password
|
||||||
|
|||||||
@@ -6,21 +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 Locator, type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
|
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
|
||||||
import { type Client } from "../../pages/client.ts";
|
import { type Client } from "../../pages/client.ts";
|
||||||
|
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
|
|
||||||
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: {
|
||||||
@@ -57,36 +50,40 @@ test.describe("Dehydration", () => {
|
|||||||
|
|
||||||
await completeCreateSecretStorageDialog(page);
|
await completeCreateSecretStorageDialog(page);
|
||||||
|
|
||||||
// Open the settings again
|
await expectDehydratedDeviceEnabled(app);
|
||||||
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();
|
||||||
|
|
||||||
// now check that the user info right-panel shows the dehydrated device
|
await expectDehydratedDeviceEnabled(app);
|
||||||
// as a feature rather than as a normal device
|
|
||||||
await app.client.createRoom({ name: ROOM_NAME });
|
|
||||||
|
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
|
||||||
|
|
||||||
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
|
|
||||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
|
||||||
|
|
||||||
await getMemberTileByName(page, NAME).click();
|
|
||||||
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
|
|
||||||
|
|
||||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
|
||||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Reset recovery key during login re-creates dehydrated device", async ({
|
test("Reset recovery key during login re-creates dehydrated device", async ({
|
||||||
@@ -134,3 +131,16 @@ async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
const res = await createBot(page, homeserver, credentials, true);
|
||||||
aliceBotClient = res.botClient;
|
aliceBotClient = res.botClient;
|
||||||
expectedBackupVersion = res.expectedBackupVersion;
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
});
|
});
|
||||||
@@ -119,7 +119,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
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 Security Key or Phrase" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||||
|
|
||||||
// Fill the passphrase
|
// Fill the passphrase
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
@@ -136,15 +136,15 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with Recovery 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 Security Key or Phrase" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||||
|
|
||||||
// Fill the security key
|
// Fill the recovery key
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
await dialog.getByRole("button", { name: "use your Security Key" }).click();
|
await dialog.getByRole("button", { name: "use your Recovery 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();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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 { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
@@ -144,25 +145,8 @@ 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. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info.
|
// wait for the logout to propagate.
|
||||||
async function awaitOneDevice(iterations = 1) {
|
await waitForDevices(app, bob.credentials.userId, 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");
|
||||||
@@ -285,11 +269,7 @@ 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 app.toggleRoomInfoPanel();
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
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();
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ test.describe("Key storage out of sync toast", () => {
|
|||||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
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("button", { name: "Enter recovery key" }).click();
|
||||||
await page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" }).click();
|
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Security key" }).fill(recoveryKey.encodedPrivateKey);
|
await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ 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 } from "./utils";
|
import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
|
||||||
import { type Client } from "../../pages/client";
|
import { type Client } from "../../pages/client";
|
||||||
|
|
||||||
test.describe("User verification", () => {
|
test.describe("User verification", () => {
|
||||||
@@ -33,13 +32,17 @@ 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 waitForDeviceKeys(page);
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
|
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||||
|
const avatar = page.getByRole("button", { name: "Avatar" });
|
||||||
|
await avatar.click();
|
||||||
|
|
||||||
// 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(
|
||||||
@@ -84,13 +87,17 @@ 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 waitForDeviceKeys(page);
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
|
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||||
|
const avatar = page.getByRole("button", { name: "Avatar" });
|
||||||
|
await avatar.click();
|
||||||
|
|
||||||
// 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(
|
||||||
@@ -154,15 +161,3 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait until we get the other user's device keys.
|
|
||||||
* In newer rust-crypto versions, the verification request will be ignored if we
|
|
||||||
* don't have the sender's device keys.
|
|
||||||
*/
|
|
||||||
async function waitForDeviceKeys(page: Page): Promise<void> {
|
|
||||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
|
||||||
const avatar = await page.getByRole("button", { name: "Avatar" });
|
|
||||||
await avatar.click();
|
|
||||||
await expect(page.getByText("1 session")).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ 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");
|
||||||
@@ -44,6 +46,7 @@ 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,11 +145,13 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
|
|||||||
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
||||||
* @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 checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
|
||||||
|
* @param checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
|
||||||
*/
|
*/
|
||||||
export async function checkDeviceIsConnectedKeyBackup(
|
export async function checkDeviceIsConnectedKeyBackup(
|
||||||
app: ElementAppPage,
|
app: ElementAppPage,
|
||||||
expectedBackupVersion: string,
|
expectedBackupVersion: string,
|
||||||
checkBackupPrivateKeyInCache: boolean,
|
checkBackupPrivateKeyInCache: 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) {
|
||||||
@@ -189,7 +194,7 @@ export async function checkDeviceIsConnectedKeyBackup(
|
|||||||
// The active backup version is as expected
|
// The active backup version is as expected
|
||||||
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
||||||
// The backup key is stored in 4S
|
// The backup key is stored in 4S
|
||||||
expect(backupKeyIn4S).toBe(true);
|
if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
|
||||||
|
|
||||||
if (checkBackupPrivateKeyInCache) {
|
if (checkBackupPrivateKeyInCache) {
|
||||||
// The backup key is available locally
|
// The backup key is available locally
|
||||||
@@ -213,13 +218,13 @@ 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 Security Key" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
|
|
||||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Security Key" });
|
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||||
if (await useSecurityKey.isVisible()) {
|
if (await useSecurityKey.isVisible()) {
|
||||||
await useSecurityKey.click();
|
await useSecurityKey.click();
|
||||||
}
|
}
|
||||||
// Fill in the security key
|
// 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();
|
||||||
@@ -246,15 +251,15 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the encryption settings, and verify the current session using the security key.
|
* Open the encryption settings, and verify the current session using the recovery key.
|
||||||
*
|
*
|
||||||
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
||||||
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
|
* @param securityKey - The recovery 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("Encryption");
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||||
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
await app.page.getByRole("button", { name: "Verify with Recovery 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();
|
||||||
@@ -288,7 +293,7 @@ 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 security key
|
* Returns the recovery 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");
|
||||||
@@ -316,9 +321,9 @@ export async function completeCreateSecretStorageDialog(
|
|||||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||||
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||||
// "Generate a Security Key" is selected by default
|
// "Generate a Recovery Key" is selected by default
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Recovery Key" })).toBeVisible();
|
||||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||||
// copy the recovery key to use it later
|
// copy the recovery key to use it later
|
||||||
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
@@ -342,7 +347,7 @@ export async function completeCreateSecretStorageDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click on copy and continue buttons to dismiss the security key dialog
|
* 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();
|
||||||
@@ -499,3 +504,31 @@ 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!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,22 +6,21 @@ 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, test as base } from "../../element-web-test";
|
import { type CredentialsWithDisplayName, 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";
|
|
||||||
|
|
||||||
const email = "user@nowhere.dummy";
|
const email = "user@nowhere.dummy";
|
||||||
|
|
||||||
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
|
const test = base.extend({
|
||||||
// eslint-disable-next-line no-empty-pattern
|
// eslint-disable-next-line no-empty-pattern
|
||||||
credentials: async ({}, use, testInfo) => {
|
credentials: async ({}, use, testInfo) => {
|
||||||
await use({
|
await use({
|
||||||
username: `user_${testInfo.testId}`,
|
username: `user_${testInfo.testId}`,
|
||||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||||
password: "oETo7MPf0o",
|
password: "oETo7MPf0o",
|
||||||
});
|
} as CredentialsWithDisplayName);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
35
playwright/e2e/modules/loader.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
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!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 - 7 messages are displayed under the thread root
|
// 30 remaining messages are unread - 6 messages are displayed under the thread root
|
||||||
await util.assertUnread(room2, 30 - 7);
|
await util.assertUnread(room2, 30 - 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
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 ({
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ test.describe("Encryption tab", () => {
|
|||||||
"should prompt to enter the recovery key when the secrets are not cached locally",
|
"should prompt to enter the recovery key when the secrets are not cached locally",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, util }) => {
|
async ({ page, app, util }) => {
|
||||||
await verifySession(app, "new passphrase");
|
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||||
// We need to delete the cached secrets
|
// We need to delete the cached secrets
|
||||||
await deleteCachedSecrets(page);
|
await deleteCachedSecrets(page);
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ test.describe("Encryption tab", () => {
|
|||||||
app,
|
app,
|
||||||
util,
|
util,
|
||||||
}) => {
|
}) => {
|
||||||
await verifySession(app, "new passphrase");
|
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||||
// We need to delete the cached secrets
|
// We need to delete the cached secrets
|
||||||
await deleteCachedSecrets(page);
|
await deleteCachedSecrets(page);
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,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 Security Key or Phrase" }).click();
|
await this.page.getByRole("button", { name: "Verify with Recovery Key" }).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,9 +53,6 @@ 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);
|
||||||
@@ -94,7 +91,7 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the security key from the clipboard and fill in the input field
|
* Get the recovery 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
|
||||||
|
|||||||
@@ -7,22 +7,25 @@
|
|||||||
|
|
||||||
import { test, expect } from ".";
|
import { test, expect } from ".";
|
||||||
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
|
import { checkDeviceIsConnectedKeyBackup, createBot, verifySession } from "../../crypto/utils";
|
||||||
|
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
test.describe("Recovery section in Encryption tab", () => {
|
test.describe("Recovery section in Encryption tab", () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let recoveryKey: GeneratedSecretStorageKey;
|
||||||
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
|
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||||
await createBot(page, homeserver, credentials);
|
const res = await createBot(page, homeserver, credentials);
|
||||||
|
recoveryKey = res.recoveryKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
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, "new passphrase");
|
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||||
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
|
||||||
@@ -49,7 +52,7 @@ 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, "new passphrase");
|
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||||
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
|
||||||
|
|||||||
@@ -25,13 +25,9 @@ test.describe("Security user settings tab", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async ({ page, user }) => {
|
test.beforeEach(async ({ page, app, user }) => {
|
||||||
// Dismiss "Notification" toast
|
// Dismiss "Notification" toast
|
||||||
await page
|
await app.closeNotificationToast();
|
||||||
.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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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: `
|
||||||
|
|||||||
@@ -202,4 +202,15 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ 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 = {
|
||||||
@@ -48,6 +52,7 @@ 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 };
|
||||||
@@ -206,8 +211,8 @@ export class Bot extends Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.opts.bootstrapSecretStorage) {
|
if (this.opts.bootstrapSecretStorage) {
|
||||||
await clientHandle.evaluate(async (cli) => {
|
await clientHandle.evaluate(async (cli, usePassphrase) => {
|
||||||
const passphrase = "new passphrase";
|
const passphrase = usePassphrase ? "new passphrase" : undefined;
|
||||||
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 });
|
||||||
|
|
||||||
@@ -216,7 +221,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;
|
||||||
|
|||||||
16
playwright/sample-files/example-module.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 983 KiB After Width: | Height: | Size: 985 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 62 KiB |