mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-05 01:10:40 +00:00
Compare commits
274 Commits
t3chguy/re
...
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 | ||
|
|
53f83124a0 | ||
|
|
63a3a6c454 | ||
|
|
0358b7f93c | ||
|
|
4a381c2a10 | ||
|
|
7cafa0d1a4 | ||
|
|
9ae4388bef | ||
|
|
511c7ca6ab | ||
|
|
2dca721ae7 | ||
|
|
272bb6c5a2 | ||
|
|
fc0797a98d | ||
|
|
5a2edba21b | ||
|
|
9657d39cd6 | ||
|
|
1c4e35606c | ||
|
|
23a3bcfc73 | ||
|
|
25fba1f8ec | ||
|
|
409c0869ce | ||
|
|
c1cc6ab391 | ||
|
|
b4832fd936 | ||
|
|
6b3ae95e8b | ||
|
|
10e91b6e63 | ||
|
|
56083777ef | ||
|
|
70df19406e | ||
|
|
2673085afa | ||
|
|
d9001d177c | ||
|
|
7eb969bbc2 | ||
|
|
0a8393c9e1 | ||
|
|
0fa52e610e | ||
|
|
8cae1e9f5e | ||
|
|
1ea1d386ab | ||
|
|
afa6f377ea | ||
|
|
b7f8623617 | ||
|
|
e75ba356d3 | ||
|
|
4f1eac67a8 | ||
|
|
aa01b17f9e | ||
|
|
4cba79ddcc | ||
|
|
b64471e4f6 | ||
|
|
d3a6f34881 | ||
|
|
dcce9c70dc | ||
|
|
f06ed2fa1f | ||
|
|
099c3073b6 | ||
|
|
12932e2dc6 | ||
|
|
a7de29429c | ||
|
|
d3ea250d77 | ||
|
|
f243fee5a6 | ||
|
|
296d0074ed | ||
|
|
df83338f26 | ||
|
|
c0336f21f6 | ||
|
|
d88f47bdbc | ||
|
|
4a26414957 | ||
|
|
886d0e1241 | ||
|
|
c453d33456 | ||
|
|
ddf221b813 | ||
|
|
08238bb883 | ||
|
|
c390ec333e | ||
|
|
3c22e5dc68 | ||
|
|
f29ce94dd4 | ||
|
|
76485cfb17 | ||
|
|
c0567fc5f4 | ||
|
|
95da3834f2 | ||
|
|
b8a3468485 | ||
|
|
b7f6e0f88c | ||
|
|
b36d9ce32e | ||
|
|
9b6be0f5a9 | ||
|
|
cae0da8f00 | ||
|
|
25689de34a | ||
|
|
ce6cb47943 | ||
|
|
850c1a5b3a | ||
|
|
ec4ae9e58a | ||
|
|
a73eb378d7 | ||
|
|
197afd6a9e | ||
|
|
ac565dca80 | ||
|
|
a0044d6b5f | ||
|
|
68c03db557 | ||
|
|
9a109cdce8 | ||
|
|
a0ab88943b | ||
|
|
ad01218942 | ||
|
|
e1e4d26154 | ||
|
|
84c614676d | ||
|
|
29d9e98111 | ||
|
|
9f5f898ed8 | ||
|
|
78251a3a8a | ||
|
|
1b077c53f5 | ||
|
|
68828a2326 | ||
|
|
af8d93f58a | ||
|
|
c0a097867e | ||
|
|
0b13e57518 | ||
|
|
8615b411b2 | ||
|
|
3d31376b1d | ||
|
|
43e5124cd4 | ||
|
|
19674cca08 | ||
|
|
6ca6cb0fbe | ||
|
|
d92fc5a595 | ||
|
|
b9d411eecc | ||
|
|
3da6619bcf | ||
|
|
f33e7c9782 | ||
|
|
1ebae09834 | ||
|
|
790a976421 | ||
|
|
1e1d66924f | ||
|
|
63ecb48d7d | ||
|
|
5e3fc8aa19 | ||
|
|
56eafc908e | ||
|
|
1644169ff3 | ||
|
|
cf895b4296 | ||
|
|
e9d4f39e9d | ||
|
|
7c0ec21365 | ||
|
|
72df9c9076 | ||
|
|
e42ee727b4 | ||
|
|
7d30413178 | ||
|
|
8884e77ce3 | ||
|
|
58f812ffe6 | ||
|
|
ef1597ff2d | ||
|
|
e5ca7954c8 | ||
|
|
13913ba8b2 | ||
|
|
03a1b48e1f | ||
|
|
ef3bf59656 | ||
|
|
7a3202b537 | ||
|
|
dbce48b23d | ||
|
|
bb41616d5f | ||
|
|
c75f6dc3a1 | ||
|
|
880048d998 | ||
|
|
24685dc7d1 | ||
|
|
60f70b93e0 | ||
|
|
2559cba482 | ||
|
|
5882b004f5 | ||
|
|
37f8d70d89 | ||
|
|
e2bd040c88 | ||
|
|
381b2ea343 | ||
|
|
41944e5c6e | ||
|
|
540580504d | ||
|
|
1a21b718d8 | ||
|
|
2cddb16a9f | ||
|
|
671d6de805 | ||
|
|
0f8a2e93ce | ||
|
|
bff2d680e6 | ||
|
|
5a5db19c2c | ||
|
|
11a8723c73 | ||
|
|
e14a3b64c3 | ||
|
|
f99d7ce2bb | ||
|
|
585aa75525 | ||
|
|
effef7eaa7 | ||
|
|
9826a8851d | ||
|
|
ebef0d353e | ||
|
|
f1899b9eb1 | ||
|
|
027891a35a | ||
|
|
2f7c28ded0 | ||
|
|
b6aba1477b | ||
|
|
056ecbb138 | ||
|
|
7685e547de | ||
|
|
a0a4211447 | ||
|
|
0ad4e13e2d | ||
|
|
f406305510 | ||
|
|
6cb174e3d9 | ||
|
|
c569478240 | ||
|
|
2bd8e049c7 | ||
|
|
e8d69dc592 | ||
|
|
50ac509a01 | ||
|
|
3e27a0019d | ||
|
|
5caad70191 | ||
|
|
6846679d34 | ||
|
|
7e5420100a | ||
|
|
f75d1f5a5e | ||
|
|
66bbb84e56 | ||
|
|
48152d2cd1 | ||
|
|
0d50e34763 | ||
|
|
f157c90ba9 | ||
|
|
cccb847d4e | ||
|
|
a5b739c45a | ||
|
|
9b1e165e6c | ||
|
|
0e2b16abf1 | ||
|
|
f6e999c87d | ||
|
|
3983bd5646 | ||
|
|
e8402f1657 | ||
|
|
69237e7df2 | ||
|
|
cf1c0805f1 | ||
|
|
b6a1aea825 | ||
|
|
b97005c182 | ||
|
|
9599c57a20 | ||
|
|
3eb3b936d9 | ||
|
|
b488155910 | ||
|
|
afc0dd5f86 | ||
|
|
7acadc29cc | ||
|
|
a73faffe37 | ||
|
|
1e758cacae | ||
|
|
7b565e7997 | ||
|
|
b16088d098 | ||
|
|
29624f7bcb | ||
|
|
d8f6c12c3d | ||
|
|
69ee8fd96a | ||
|
|
3fb10baedf | ||
|
|
cf49f9e22c | ||
|
|
703149d76d | ||
|
|
bd3e93e8dd | ||
|
|
0555701829 | ||
|
|
417db4c9b2 | ||
|
|
4e151f8d03 | ||
|
|
afa7ec695d | ||
|
|
e98529824e | ||
|
|
16d2cccb73 | ||
|
|
9d5141cfaa | ||
|
|
1e42f28a69 | ||
|
|
4e1bd69e4d | ||
|
|
12943954c6 | ||
|
|
ec95435724 | ||
|
|
7e1927d388 | ||
|
|
0b24d33c64 |
@@ -7,3 +7,4 @@ test/end-to-end-tests/lib/
|
||||
src/component-index.js
|
||||
# Auto-generated file
|
||||
src/modules.ts
|
||||
src/modules.js
|
||||
|
||||
16
.eslintrc.js
16
.eslintrc.js
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ["matrix-org"],
|
||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
@@ -170,6 +170,8 @@ module.exports = {
|
||||
"jsx-a11y/role-supports-aria-props": "off",
|
||||
|
||||
"matrix-org/require-copyright-header": "error",
|
||||
|
||||
"react-compiler/react-compiler": "error",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -198,8 +200,13 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
// We do this sometimes to brand interfaces
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/no-empty-object-type": [
|
||||
"error",
|
||||
{
|
||||
// We do this sometimes to brand interfaces
|
||||
allowInterfaces: "with-single-extends",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// temporary override for offending icon require files
|
||||
@@ -245,6 +252,7 @@ module.exports = {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
|
||||
// Jest/Playwright specific
|
||||
|
||||
@@ -262,6 +270,7 @@ module.exports = {
|
||||
|
||||
// These are fine in tests
|
||||
"no-restricted-globals": "off",
|
||||
"react-compiler/react-compiler": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -271,6 +280,7 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": ["off"],
|
||||
"@typescript-eslint/no-floating-promises": ["error"],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
22
.github/CODEOWNERS
vendored
22
.github/CODEOWNERS
vendored
@@ -3,17 +3,23 @@
|
||||
/package.json @element-hq/element-web-team
|
||||
/yarn.lock @element-hq/element-web-team
|
||||
|
||||
/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/SecurityManager.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/SecurityManager-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/async-components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||
|
||||
# Ignore translations as those will be updated by GHA for Localazy download
|
||||
/src/i18n/strings
|
||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||
# Ignore the synapse plugin as this is updated by GHA for docker image updating
|
||||
/playwright/plugins/homeserver/synapse/index.ts
|
||||
/playwright/testcontainers/synapse.ts
|
||||
|
||||
|
||||
12
.github/labels.yml
vendored
12
.github/labels.yml
vendored
@@ -210,6 +210,9 @@
|
||||
- name: "X-Upcoming-Release-Blocker"
|
||||
description: "This does not affect the current release cycle but will affect the next one"
|
||||
color: "e99695"
|
||||
- name: "X-Run-All-Tests"
|
||||
description: "When applied to PRs, it'll run the full gamut of end-to-end tests on the PR"
|
||||
color: "ff7979"
|
||||
- name: "Z-Actions"
|
||||
color: "ededed"
|
||||
- name: "Z-Cache-Confusion"
|
||||
@@ -232,6 +235,15 @@
|
||||
- name: "Z-Flaky-Test"
|
||||
description: "A test is raising false alarms"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Chrome"
|
||||
description: "Flaky playwright test in Chrome"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Firefox"
|
||||
description: "Flaky playwright test in Firefox"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Test-Webkit"
|
||||
description: "Flaky playwright test in Webkit"
|
||||
color: "ededed"
|
||||
- name: "Z-Flaky-Jest-Test"
|
||||
description: "A Jest test is raising false alarms"
|
||||
color: "ededed"
|
||||
|
||||
42
.github/workflows/build.yml
vendored
42
.github/workflows/build.yml
vendored
@@ -5,6 +5,9 @@ on:
|
||||
branches: [develop, master]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
# develop pushes and repository_dispatch handled in build_develop.yaml
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
@@ -24,10 +27,17 @@ jobs:
|
||||
- macos-14
|
||||
isDevelop:
|
||||
- ${{ github.event_name == 'push' && github.ref_name == 'develop' }}
|
||||
isPullRequest:
|
||||
- ${{ github.event_name == 'pull_request' }}
|
||||
# Skip the ubuntu-24.04 build for the develop branch as the dedicated CD build_develop workflow handles that
|
||||
# Skip the non-linux builds for pull requests as Windows is awfully slow, so run in merge queue only
|
||||
exclude:
|
||||
- isDevelop: true
|
||||
image: ubuntu-24.04
|
||||
- isPullRequest: true
|
||||
image: windows-2022
|
||||
- isPullRequest: true
|
||||
image: macos-14
|
||||
runs-on: ${{ matrix.image }}
|
||||
defaults:
|
||||
run:
|
||||
@@ -37,14 +47,38 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
# Disable cache on Windows as it is slower than not caching
|
||||
# https://github.com/actions/setup-node/issues/975
|
||||
cache: ${{ runner.os != 'Windows' && 'yarn' || '' }}
|
||||
node-version: "lts/*"
|
||||
|
||||
# Workaround for yarn install timeouts, especially on Windows
|
||||
- run: yarn config set network-timeout 300000
|
||||
|
||||
- name: Install Dependencies
|
||||
run: "./scripts/layered.sh"
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
run: |
|
||||
scripts/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git rev-parse --short=12 HEAD)
|
||||
echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
|
||||
- name: Build
|
||||
run: "yarn build"
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: webapp-${{ matrix.image }}
|
||||
path: webapp
|
||||
retention-days: 1
|
||||
|
||||
6
.github/workflows/build_develop.yml
vendored
6
.github/workflows/build_develop.yml
vendored
@@ -26,6 +26,12 @@ jobs:
|
||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||
steps:
|
||||
# Workaround for https://www.cloudflarestatus.com/incidents/t5nrjmpxc1cj
|
||||
- uses: unfor19/install-aws-cli-action@v1
|
||||
with:
|
||||
version: 2.22.35
|
||||
verbose: false
|
||||
arch: amd64
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
|
||||
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -96,3 +96,4 @@ jobs:
|
||||
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||
directory: _deploy
|
||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: main
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Dockerhub
|
||||
name: Docker
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
push:
|
||||
@@ -15,19 +15,20 @@ jobs:
|
||||
environment: dockerhub
|
||||
permissions:
|
||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
||||
packages: write # needed for publishing packages to GHCR
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
|
||||
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3
|
||||
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
|
||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
@@ -37,12 +38,20 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
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
|
||||
id: meta
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||
with:
|
||||
images: |
|
||||
vectorim/element-web
|
||||
ghcr.io/element-hq/element-web
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
@@ -51,7 +60,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
18
.github/workflows/end-to-end-tests.yaml
vendored
18
.github/workflows/end-to-end-tests.yaml
vendored
@@ -114,14 +114,20 @@ jobs:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- WebKit
|
||||
isCron:
|
||||
- ${{ github.event_name == 'schedule' }}
|
||||
# Skip the Firefox & Safari runs unless this was a cron trigger
|
||||
- Dendrite
|
||||
- Pinecone
|
||||
runAllTests:
|
||||
- ${{ github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }}
|
||||
# Skip the Firefox & Safari runs unless this was a cron trigger or PR has X-Run-All-Tests label
|
||||
exclude:
|
||||
- isCron: false
|
||||
- runAllTests: false
|
||||
project: Firefox
|
||||
- isCron: false
|
||||
- runAllTests: false
|
||||
project: WebKit
|
||||
- runAllTests: false
|
||||
project: Dendrite
|
||||
- runAllTests: false
|
||||
project: Pinecone
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -170,7 +176,7 @@ jobs:
|
||||
yarn playwright test \
|
||||
--shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
|
||||
--project="${{ matrix.project }}" \
|
||||
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
|
||||
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
|
||||
4
.github/workflows/netlify.yaml
vendored
4
.github/workflows/netlify.yaml
vendored
@@ -3,7 +3,7 @@
|
||||
name: Upload Preview Build to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["End to End Tests"]
|
||||
workflows: ["Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: webapp
|
||||
name: webapp-ubuntu-24.04
|
||||
path: webapp
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
docker pull "$IMAGE"
|
||||
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
||||
DIGEST=${INSPECT#*@}
|
||||
sed -i "s/const DOCKER_TAG.*/const DOCKER_TAG = \"develop@$DIGEST\";/" playwright/plugins/homeserver/synapse/index.ts
|
||||
sed -i "s/const TAG.*/const TAG = \"develop@$DIGEST\";/" playwright/testcontainers/synapse.ts
|
||||
env:
|
||||
IMAGE: ghcr.io/element-hq/synapse:develop
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/playwright-image-updates
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
permissions:
|
||||
checks: read
|
||||
steps:
|
||||
- name: Wait for dockerhub
|
||||
- name: Wait for docker build
|
||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||
with:
|
||||
ref: master
|
||||
|
||||
6
.github/workflows/static_analysis.yaml
vendored
6
.github/workflows/static_analysis.yaml
vendored
@@ -132,9 +132,3 @@ jobs:
|
||||
|
||||
- name: Run linter
|
||||
run: "yarn run lint:knip"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/layered.sh"
|
||||
|
||||
- name: Dead Code Analysis
|
||||
run: "yarn run analyse:unused-exports"
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
||||
uses: guibranco/github-status-action-v2@7ca807c2ba3401be532d29a876b93262108099fb
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
19
.github/workflows/triage-stale-flaky-tests.yml
vendored
19
.github/workflows/triage-stale-flaky-tests.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: Close stale flaky issues
|
||||
on:
|
||||
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"
|
||||
27
.github/workflows/triage-stale.yml
vendored
Normal file
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."
|
||||
2
.github/workflows/update-jitsi.yml
vendored
2
.github/workflows/update-jitsi.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
run: "yarn update:jitsi"
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ electron/pub
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/build_config.yaml
|
||||
/book
|
||||
/index.html
|
||||
|
||||
@@ -17,6 +17,7 @@ electron/pub
|
||||
/coverage
|
||||
# Auto-generated file
|
||||
/src/modules.ts
|
||||
/src/modules.js
|
||||
/src/i18n/strings
|
||||
/build_config.yaml
|
||||
# Raises an error because it contains a template var breaking the script tag
|
||||
|
||||
@@ -33,19 +33,15 @@ module.exports = {
|
||||
"import-notation": null,
|
||||
"value-keyword-case": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
// useful for fallbacks
|
||||
{ ignore: ["consecutive-duplicates-with-different-values"] },
|
||||
],
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"selector-no-vendor-prefix": null,
|
||||
"media-feature-name-no-vendor-prefix": null,
|
||||
"number-max-precision": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"media-feature-range-notation": null,
|
||||
"declaration-property-value-no-unknown": null,
|
||||
"declaration-property-value-keyword-no-deprecated": null,
|
||||
"csstools/value-no-unknown-custom-properties": [
|
||||
true,
|
||||
{
|
||||
|
||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -1,3 +1,72 @@
|
||||
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
|
||||
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
|
||||
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
|
||||
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
|
||||
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
|
||||
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
|
||||
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
|
||||
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
|
||||
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
|
||||
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
|
||||
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
|
||||
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
|
||||
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
|
||||
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
|
||||
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
|
||||
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
|
||||
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
|
||||
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
|
||||
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
|
||||
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
|
||||
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
|
||||
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
|
||||
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
|
||||
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
|
||||
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
|
||||
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
|
||||
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
|
||||
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
|
||||
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
|
||||
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Docker: run as non-root ([#28849](https://github.com/element-hq/element-web/pull/28849)). Contributed by @richvdh.
|
||||
* Docker: allow configuration of HTTP listen port via env var ([#28840](https://github.com/element-hq/element-web/pull/28840)). Contributed by @richvdh.
|
||||
* Update matrix-wysiwyg to consume WASM asset ([#28838](https://github.com/element-hq/element-web/pull/28838)). Contributed by @t3chguy.
|
||||
* OIDC settings tweaks ([#28787](https://github.com/element-hq/element-web/pull/28787)). Contributed by @t3chguy.
|
||||
* Delabs native OIDC support ([#28615](https://github.com/element-hq/element-web/pull/28615)). Contributed by @t3chguy.
|
||||
* Move room header info button to right-most position ([#28754](https://github.com/element-hq/element-web/pull/28754)). Contributed by @t3chguy.
|
||||
* Enable key backup by default ([#28691](https://github.com/element-hq/element-web/pull/28691)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix building the automations mermaid diagram ([#28881](https://github.com/element-hq/element-web/pull/28881)). Contributed by @dbkr.
|
||||
* Playwright: wait for the network listener on the postgres db ([#28808](https://github.com/element-hq/element-web/pull/28808)). Contributed by @dbkr.
|
||||
|
||||
|
||||
Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18)
|
||||
==================================================================================================
|
||||
This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room.
|
||||
|
||||
@@ -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
|
||||
and we'll try to fix it :)
|
||||
|
||||
## Sign off
|
||||
|
||||
In order to have a concrete record that your contribution is intentional
|
||||
and you agree to license it under the same terms as the project's license, we've
|
||||
adopted the same lightweight approach that the Linux Kernel
|
||||
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
|
||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||
projects use: the DCO (Developer Certificate of Origin:
|
||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||
the contribution or otherwise have the right to contribute it to Matrix:
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
If you agree to this for your contribution, then all that's needed is to
|
||||
include the line in your commit or pull request comment:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your@email.example.org>
|
||||
```
|
||||
|
||||
We accept contributions under a legally identifiable name, such as your name on
|
||||
government documentation or common-law names (names claimed by legitimate usage
|
||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
||||
time.
|
||||
|
||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
||||
`git commit`, which uses the name and email set in your `user.name` and
|
||||
`user.email` git configs.
|
||||
|
||||
If you forgot to sign off your commits before making your pull request and are
|
||||
on Git 2.17+ you can mass signoff using rebase:
|
||||
|
||||
```
|
||||
git rebase --signoff origin/develop
|
||||
```
|
||||
|
||||
## Private sign off
|
||||
|
||||
If you would like to provide your legal name privately to the Matrix.org
|
||||
Foundation (instead of in a public commit or comment), you can do so by emailing
|
||||
your legal name and a link to the pull request to dco@matrix.org. It helps to
|
||||
include "sign off" or similar in the subject line. You will then be instructed
|
||||
further.
|
||||
|
||||
Once private sign off is complete, doing so for future contributions will not
|
||||
be required.
|
||||
|
||||
# Review expectations
|
||||
|
||||
See https://github.com/element-hq/element-meta/wiki/Review-process
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,20 +1,17 @@
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye as builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
|
||||
|
||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||
ARG USE_CUSTOM_SDKS=false
|
||||
ARG JS_SDK_REPO="https://github.com/matrix-org/matrix-js-sdk.git"
|
||||
ARG JS_SDK_BRANCH="master"
|
||||
|
||||
RUN apt-get update && apt-get install -y git dos2unix
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . /src
|
||||
RUN dos2unix /src/scripts/docker-link-repos.sh && bash /src/scripts/docker-link-repos.sh
|
||||
RUN /src/scripts/docker-link-repos.sh
|
||||
RUN yarn --network-timeout=200000 install
|
||||
|
||||
RUN dos2unix /src/scripts/docker-package.sh /src/scripts/get-version-from-git.sh /src/scripts/normalize-version.sh && bash /src/scripts/docker-package.sh
|
||||
RUN /src/scripts/docker-package.sh
|
||||
|
||||
# Copy the config now so that we don't create another layer in the app image
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
@@ -24,8 +21,22 @@ FROM nginx:alpine-slim
|
||||
|
||||
COPY --from=builder /src/webapp /app
|
||||
|
||||
# Override default nginx config
|
||||
COPY /nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
||||
# through `envsubst` by the nginx docker image entry point.
|
||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
||||
|
||||
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
|
||||
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
|
||||
|
||||
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
||||
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
||||
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html \
|
||||
&& ln -s /app /usr/share/nginx/html
|
||||
|
||||
# Run as nginx user by default
|
||||
USER nginx
|
||||
|
||||
# HTTP listen port
|
||||
ENV ELEMENT_WEB_PORT=80
|
||||
|
||||
6
LICENSE-COMMERCIAL
Normal file
6
LICENSE-COMMERCIAL
Normal file
@@ -0,0 +1,6 @@
|
||||
Licensees holding a valid commercial license with Element may use this
|
||||
software in accordance with the terms contained in a written agreement
|
||||
between you and Element.
|
||||
|
||||
To purchase a commercial license please contact our sales team at
|
||||
licensing@element.io
|
||||
135
README.md
135
README.md
@@ -182,123 +182,11 @@ Dockerfile.
|
||||
|
||||
# Development
|
||||
|
||||
Before attempting to develop on Element you **must** read the [developer guide
|
||||
for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which
|
||||
also defines the design, architecture and style for Element too.
|
||||
Please read through the following:
|
||||
|
||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
||||
about where to start. Before starting work on a feature, it's best to ensure
|
||||
your plan aligns well with our vision for Element. Please chat with the team in
|
||||
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
||||
you start so we can ensure it's something we'd be willing to merge.
|
||||
|
||||
You should also familiarise yourself with the ["Here be Dragons" guide
|
||||
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
||||
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
||||
|
||||
The idea of Element is to be a relatively lightweight "skin" of customisations on
|
||||
top of the underlying `matrix-react-sdk`. `matrix-react-sdk` provides both the
|
||||
higher and lower level React components useful for building Matrix communication
|
||||
apps using React.
|
||||
|
||||
Please note that Element is intended to run correctly without access to the public
|
||||
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
||||
hosted by external CDNs or servers but instead please package all dependencies
|
||||
into Element itself.
|
||||
|
||||
# Setting up a dev environment
|
||||
|
||||
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
||||
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
||||
in git and to make local changes without having to manually rebuild each time.
|
||||
|
||||
First clone and build `matrix-js-sdk`:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||
pushd matrix-js-sdk
|
||||
yarn link
|
||||
yarn install
|
||||
popd
|
||||
```
|
||||
|
||||
Clone the repo and switch to the `element-web` directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/element-hq/element-web.git
|
||||
cd element-web
|
||||
```
|
||||
|
||||
Configure the app by copying `config.sample.json` to `config.json` and
|
||||
modifying it. See the [configuration docs](docs/config.md) for details.
|
||||
|
||||
Finally, build and start Element itself:
|
||||
|
||||
```bash
|
||||
yarn link matrix-js-sdk
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
Wait a few seconds for the initial build to finish; you should see something like:
|
||||
|
||||
```
|
||||
[element-js] <s> [webpack.Progress] 100%
|
||||
[element-js]
|
||||
[element-js] ℹ 「wdm」: 1840 modules
|
||||
[element-js] ℹ 「wdm」: Compiled successfully.
|
||||
```
|
||||
|
||||
Remember, the command will not terminate since it runs the web server
|
||||
and rebuilds source files when they change. This development server also
|
||||
disables caching, so do NOT use it in production.
|
||||
|
||||
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
||||
|
||||
**Note**: The build script uses inotify by default on Linux to monitor directories
|
||||
for changes. If the inotify limits are too low your build will fail silently or with
|
||||
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
||||
of at least `128M` and instance limit around `512`.
|
||||
|
||||
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
||||
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
||||
|
||||
To set a new inotify watch and instance limit, execute:
|
||||
|
||||
```
|
||||
sudo sysctl fs.inotify.max_user_watches=131072
|
||||
sudo sysctl fs.inotify.max_user_instances=512
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
If you wish, you can make the new limits permanent, by executing:
|
||||
|
||||
```
|
||||
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
||||
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
||||
sudo sysctl -p
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
||||
|
||||
If any of these steps error with, `file table overflow`, you are probably on a mac
|
||||
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
||||
You'll need to do this in each new terminal you open before building Element.
|
||||
|
||||
## Running the tests
|
||||
|
||||
There are a number of application-level tests in the `tests` directory; these
|
||||
are designed to run with Jest and JSDOM. To run them
|
||||
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
### End-to-End tests
|
||||
|
||||
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
||||
1. [Developer guide](./developer_guide.md)
|
||||
2. [Code style](./code_style.md)
|
||||
3. [Contribution guide](./CONTRIBUTING.md)
|
||||
|
||||
# Translations
|
||||
|
||||
@@ -311,3 +199,18 @@ For a developer guide, see the [translating dev doc](docs/translating-dev.md).
|
||||
Issues are triaged by community members and the Web App Team, following the [triage process](https://github.com/element-hq/element-meta/wiki/Triage-process).
|
||||
|
||||
We use [issue labels](https://github.com/element-hq/element-meta/wiki/Issue-labelling) to sort all incoming issues.
|
||||
|
||||
## Copyright & License
|
||||
|
||||
Copyright (c) 2014-2017 OpenMarket Ltd
|
||||
Copyright (c) 2017 Vector Creations Ltd
|
||||
Copyright (c) 2017-2025 New Vector Ltd
|
||||
|
||||
This software is multi licensed by New Vector Ltd (Element). It can be used either:
|
||||
|
||||
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
|
||||
|
||||
(2) for free under the terms of the GNU General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
|
||||
|
||||
(3) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ class MockMap extends EventEmitter {
|
||||
setCenter = jest.fn();
|
||||
setStyle = jest.fn();
|
||||
fitBounds = jest.fn();
|
||||
remove = jest.fn();
|
||||
}
|
||||
const MockMapInstance = new MockMap();
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,15 +5,6 @@ adjacent to. As of writing, these are:
|
||||
|
||||
- element-desktop
|
||||
- element-web
|
||||
- matrix-js-sdk
|
||||
|
||||
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
|
||||
has stricter code organization to reduce the maintenance burden. These projects will declare their code
|
||||
style within their own repos.
|
||||
|
||||
Note that some requirements will be layer-specific. Where the requirements don't make sense for the
|
||||
project, they are used to the best of their ability, used in spirit, or ignored if not applicable,
|
||||
in that order.
|
||||
|
||||
## Guiding principles
|
||||
|
||||
@@ -234,17 +225,19 @@ Unless otherwise specified, the following applies to all code:
|
||||
|
||||
Inheriting all the rules of TypeScript, the following additionally apply:
|
||||
|
||||
1. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||
2. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||
1. Component source files are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||
2. They are organised in a typically two-level hierarchy - first whether the component is a view or a structure, and then a broad functional grouping (e.g. 'rooms' here)
|
||||
3. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||
4. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||
empty if the component accepts no props.
|
||||
3. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||
4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||
5. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||
6. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||
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.
|
||||
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.
|
||||
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
|
||||
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.
|
||||
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||
cycles during runtime where components accidentally include more of the app than they intended.
|
||||
10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||
10. Stores must support using an alternative MatrixClient and dispatcher instance.
|
||||
11. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||
cycles during runtime where components accidentally include more of the app than they intended.
|
||||
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||
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).
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
16. Components should serve a single, or near-single, purpose.
|
||||
17. Prefer to derive information from component properties rather than establish state.
|
||||
18. Do not use `React.Component::forceUpdate`.
|
||||
18. Components should serve a single, or near-single, purpose.
|
||||
19. Prefer to derive information from component properties rather than establish state.
|
||||
20. Do not use `React.Component::forceUpdate`.
|
||||
|
||||
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
||||
|
||||
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\_".
|
||||
2. Class names must denote the component which defines them, followed by any context.
|
||||
The context is not further specified here in terms of meaning or syntax.
|
||||
Use whatever is appropriate for your implementation use case.
|
||||
Some examples:
|
||||
1. `mx_MyFoo`
|
||||
2. `mx_MyFoo_avatar`
|
||||
3. `mx_MyFoo_avatarUser`
|
||||
4. `mx_MyFoo_avatar--user`
|
||||
3. Use the `$font` variables instead of manual values.
|
||||
4. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||
5. Use the whole class name instead of shortcuts:
|
||||
1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component).
|
||||
2. Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual.
|
||||
3. Class names must be prefixed with "mx\_".
|
||||
4. Class names must strictly denote the component which defines them.
|
||||
For example: `mx_MyFoo` for `MyFoo` component.
|
||||
5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name - e.g. .mx_MyFoo_randomDiv is how you'd name the class of an arbitrary div within the MyFoo view.
|
||||
6. Use the `$font` variables instead of manual values.
|
||||
7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||
8. Use the whole class name instead of shortcuts:
|
||||
|
||||
```scss
|
||||
.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
|
||||
.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.
|
||||
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||
[documented](#comments) for what the values mean:
|
||||
10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
||||
11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||
[documented](#comments) for what the values mean:
|
||||
|
||||
```scss
|
||||
.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
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"description": "A glossy Matrix collaboration client for the web.",
|
||||
"repository": {
|
||||
"url": "https://github.com/element-hq/element-web",
|
||||
"license": "AGPL-3.0-only OR GPL-3.0-only"
|
||||
"license": "AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial"
|
||||
},
|
||||
"bugs": {
|
||||
"list": "https://github.com/element-hq/element-web/issues",
|
||||
|
||||
2
debian/control
vendored
2
debian/control
vendored
@@ -8,6 +8,6 @@ Package: element-web
|
||||
Architecture: all
|
||||
Recommends: httpd, element-io-archive-keyring
|
||||
Description:
|
||||
A feature-rich client for Matrix.org
|
||||
Element: the future of secure communication
|
||||
This package contains the web-based client that can be served through a web
|
||||
server.
|
||||
|
||||
126
developer_guide.md
Normal file
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
|
||||
@@ -1,6 +1,6 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
listen ${ELEMENT_WEB_PORT};
|
||||
listen [::]:${ELEMENT_WEB_PORT};
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
67
docs/MVVM.md
Normal file
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.
|
||||
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
||||
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
||||
20. `force_verification`: If true, users must verify new logins (eg. with another device / their security key)
|
||||
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
||||
|
||||
### `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
|
||||
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.
|
||||
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.
|
||||
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
||||
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
|
||||
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
|
||||
5. `url_win64`: Optional. Direct link to download Windows x86 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.
|
||||
|
||||
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`
|
||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
|
||||
|
||||
@@ -60,6 +60,22 @@ would be:
|
||||
docker run --rm -p 127.0.0.1:80:80 -v /etc/element-web/config.json:/app/config.json vectorim/element-web
|
||||
```
|
||||
|
||||
The Docker image is configured to run as an unprivileged (non-root) user by
|
||||
default. This should be fine on modern Docker runtimes, but binding to port 80
|
||||
on other runtimes may require root privileges. To resolve this, either run the
|
||||
image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
||||
|
||||
The behaviour of the docker image can be customised via the following
|
||||
environment variables:
|
||||
|
||||
- `ELEMENT_WEB_PORT`
|
||||
|
||||
The port to listen on (within the docker container) for HTTP
|
||||
traffic. Defaults to `80`.
|
||||
|
||||
### Building the docker image
|
||||
|
||||
To build the image yourself:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -112,3 +112,7 @@ Unreliable in encrypted rooms.
|
||||
## Knock rooms (`feature_ask_to_join`) [In Development]
|
||||
|
||||
Enables knock feature for rooms. This allows users to ask to join a room.
|
||||
|
||||
## New room list (`feature_new_room_list`) [In Development]
|
||||
|
||||
Enable the new room list that is currently in development.
|
||||
|
||||
22
docs/oidc.md
22
docs/oidc.md
@@ -1,29 +1,9 @@
|
||||
# OIDC and delegated authentication
|
||||
|
||||
## Compatibility/OIDC-aware mode
|
||||
|
||||
[MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965)
|
||||
[MSC3824: OIDC aware clients](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
|
||||
This mode uses an SSO flow to gain a `loginToken` from the authentication provider, then continues with SSO login.
|
||||
Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
|
||||
Wherever valid MSC2965 configuration is discovered, OIDC-aware login flow will be the only option offered.
|
||||
|
||||
## (🧪Experimental) OIDC-native flow
|
||||
|
||||
Can be enabled by a config-level-only setting in `config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"feature_oidc_native_flow": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See https://areweoidcyet.com/client-implementation-guide/ for implementation details.
|
||||
|
||||
Element Web uses [MSC2965: OIDC provider discovery](https://github.com/matrix-org/matrix-spec-proposals/pull/2965) to discover the configured provider.
|
||||
Where OIDC native login flow is enabled and valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
|
||||
Where a valid MSC2965 configuration is discovered, OIDC native login flow will be the only login option offered.
|
||||
Element Web will attempt to [dynamically register](https://openid.net/specs/openid-connect-registration-1_0.html) with the configured OP.
|
||||
Then, authentication will be completed [as described here](https://areweoidcyet.com/client-implementation-guide/).
|
||||
|
||||
|
||||
@@ -23,21 +23,19 @@ element-web project is fine: leave it running it a different terminal as you wou
|
||||
when developing. Alternatively if you followed the development set up from element-web then
|
||||
Playwright will be capable of running the webserver on its own if it isn't already running.
|
||||
|
||||
The tests use Docker to launch Homeserver (Synapse or Dendrite) instances to test against, so you'll also
|
||||
need to have Docker installed and working in order to run the Playwright tests.
|
||||
The tests use [testcontainers](https://node.testcontainers.org/) to launch Homeserver (Synapse or Dendrite)
|
||||
instances to test against, so you'll also need to one of the
|
||||
[supported container runtimes](#supporter-container-runtimes)
|
||||
installed and working in order to run the Playwright tests.
|
||||
|
||||
There are a few different ways to run the tests yourself. The simplest is to run:
|
||||
|
||||
```shell
|
||||
docker pull ghcr.io/element-hq/synapse:develop
|
||||
yarn run test:playwright
|
||||
```
|
||||
|
||||
This will run the Playwright tests once, non-interactively.
|
||||
|
||||
Note: you don't need to run the `docker pull` command every time, but you should
|
||||
do it regularly to ensure you are running against an up-to-date Synapse.
|
||||
|
||||
You can also run individual tests this way too, as you'd expect:
|
||||
|
||||
```shell
|
||||
@@ -61,29 +59,28 @@ Some tests are excluded from running on certain browsers due to incompatibilitie
|
||||
|
||||
## How the Tests Work
|
||||
|
||||
Everything Playwright-related lives in the `playwright/` subdirectory of react-sdk
|
||||
Everything Playwright-related lives in the `playwright/` subdirectory
|
||||
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
||||
|
||||
`playwright/plugins/homeservers` contains Playwright plugins that starts instances
|
||||
of Synapse/Dendrite in Docker containers. These servers are what Element-web runs
|
||||
against in the tests.
|
||||
`playwright/testcontainers` contains the testcontainers which start instances
|
||||
of Synapse/Dendrite. These servers are what Element-web runs against in the tests.
|
||||
|
||||
Synapse can be launched with different configurations in order to test element
|
||||
in different configurations. `playwright/plugins/homeserver/synapse/templates`
|
||||
contains template configuration files for each different configuration.
|
||||
in different configurations. You can specify `synapseConfig` as such:
|
||||
|
||||
Each test suite can then launch whatever Synapse instances it needs in whatever
|
||||
configurations.
|
||||
```typescript
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
// The config options to pass to the Synapse instance
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Note that although tests should stop the Homeserver instances after running and the
|
||||
plugin also stop any remaining instances after all tests have run, it is possible
|
||||
to be left with some stray containers if, for example, you terminate a test such
|
||||
that the `after()` does not run and also exit Playwright uncleanly. All the containers
|
||||
it starts are prefixed, so they are easy to recognise. They can be removed safely.
|
||||
|
||||
After each test run, logs from the Synapse instances are saved in `playwright/logs/synapse`
|
||||
with each instance in a separate directory named after its ID. These logs are removed
|
||||
at the start of each test run.
|
||||
The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
|
||||
Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as
|
||||
they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||
We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||
The logs from testcontainers will be attached to any reports output from Playwright.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
@@ -113,25 +110,6 @@ Homeserver instances should be reasonably cheap to start (you may see the first
|
||||
while as it pulls the Docker image).
|
||||
You do not need to explicitly clean up the instance as it will be cleaned up by the fixture.
|
||||
|
||||
### Synapse Config Templates
|
||||
|
||||
When a Synapse instance is started, it's given a config generated from one of the config
|
||||
templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files
|
||||
in these templates:
|
||||
|
||||
- `homeserver.yaml`:
|
||||
Template substitution happens in this file. Template variables are:
|
||||
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
||||
- `MACAROON_SECRET_KEY`: Generated each time for security
|
||||
- `FORM_SECRET`: Generated each time for security
|
||||
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
||||
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
||||
Config templates should not contain a signing key and instead assume that one will exist
|
||||
in this file.
|
||||
|
||||
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
|
||||
in a template can be referenced in the config as `/data/foo.html`.
|
||||
|
||||
### Logging In
|
||||
|
||||
We again heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
||||
@@ -227,7 +205,13 @@ has to be disabled in Playwright on Firefox & Webkit to retain routing functiona
|
||||
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
|
||||
there at this time.
|
||||
|
||||
## Colima
|
||||
If you wish to run all tests in a PR, you can give it the label `X-Run-All-Tests`.
|
||||
|
||||
## Supporter container runtimes
|
||||
|
||||
We use testcontainers to spin up various instances of Synapse, Matrix Authentication Service, and more.
|
||||
It supports Docker out of the box but also has support for Podman, Colima, Rancher, you just need to follow some instructions to achieve it:
|
||||
https://node.testcontainers.org/supported-container-runtimes/
|
||||
|
||||
If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
|
||||
within `$HOME` to allow bind mounting temporary directories into the Docker containers.
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
#### develop
|
||||
|
||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.
|
||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
|
||||
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
|
||||
|
||||
#### staging
|
||||
|
||||
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
|
||||
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
|
||||
|
||||
#### master
|
||||
|
||||
@@ -126,7 +128,7 @@ flowchart TD
|
||||
|
||||
subgraph Deploying
|
||||
D1[\Deploy staging.element.io/]
|
||||
D2[\Check dockerhub/]
|
||||
D2[\Check docker build/]
|
||||
D3[\Deploy app.element.io/]
|
||||
D4[\Check desktop package/]
|
||||
|
||||
@@ -211,11 +213,11 @@ switched back to the version of the dependency from the master branch to not lea
|
||||
# Deploying
|
||||
|
||||
We ship the SDKs to npm, this happens as part of the release process.
|
||||
We ship Element Web to dockerhub, `*.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.
|
||||
|
||||
- [ ] Check that element-web has shipped to dockerhub
|
||||
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
||||
- [ ] 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
|
||||
- [ ] Test staging.element.io
|
||||
|
||||
For final releases additionally do these steps:
|
||||
@@ -225,6 +227,9 @@ For final releases additionally do these steps:
|
||||
- [ ] Ensure Element Web package has shipped to packages.element.io
|
||||
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
||||
|
||||
If you need to roll back a deployment to staging.element.io,
|
||||
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
|
||||
|
||||
# Housekeeping
|
||||
|
||||
We have some manual housekeeping to do in order to prepare for the next release.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,8 @@ const config: Config = {
|
||||
testEnvironment: "jsdom",
|
||||
testEnvironmentOptions: {
|
||||
url: "http://localhost/",
|
||||
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
||||
customExportConditions: ["browser", "node"],
|
||||
},
|
||||
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
|
||||
globalSetup: "<rootDir>/test/globalSetup.ts",
|
||||
@@ -36,6 +38,8 @@ const config: Config = {
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
// Requires ESM which is incompatible with our current Jest setup
|
||||
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||
collectCoverageFrom: [
|
||||
|
||||
13
knip.ts
13
knip.ts
@@ -10,13 +10,13 @@ export default {
|
||||
"playwright/**",
|
||||
"test/**",
|
||||
"res/decoder-ring/**",
|
||||
"res/jitsi_external_api.min.js",
|
||||
"docs/**",
|
||||
// Used by jest
|
||||
"__mocks__/maplibre-gl.js",
|
||||
],
|
||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||
ignore: [
|
||||
"docs/**",
|
||||
"res/jitsi_external_api.min.js",
|
||||
// Used by jest
|
||||
"__mocks__/maplibre-gl.js",
|
||||
// Keep for now
|
||||
"src/hooks/useLocalStorageState.ts",
|
||||
"src/components/views/elements/InfoTooltip.tsx",
|
||||
@@ -37,13 +37,8 @@ export default {
|
||||
// False positive
|
||||
"sw.js",
|
||||
// Used by webpack
|
||||
"buffer",
|
||||
"process",
|
||||
"util",
|
||||
// Used by workflows
|
||||
"ts-prune",
|
||||
// Required due to bug in bloom-filters https://github.com/Callidon/bloom-filters/issues/75
|
||||
"@types/seedrandom",
|
||||
],
|
||||
ignoreBinaries: [
|
||||
// Used in scripts & workflows
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2022-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2022-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ import * as fs from "fs";
|
||||
import * as childProcess from "child_process";
|
||||
import * as semver from "semver";
|
||||
|
||||
import { BuildConfig } from "./BuildConfig";
|
||||
import { type BuildConfig } from "./BuildConfig";
|
||||
|
||||
// This expects to be run from ./scripts/install.ts
|
||||
|
||||
@@ -23,10 +23,9 @@ const MODULES_TS_HEADER = `
|
||||
* You are not a salmon.
|
||||
*/
|
||||
|
||||
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
||||
`;
|
||||
const MODULES_TS_DEFINITIONS = `
|
||||
export const INSTALLED_MODULES: RuntimeModule[] = [];
|
||||
export const INSTALLED_MODULES = [];
|
||||
`;
|
||||
|
||||
export function installer(config: BuildConfig): void {
|
||||
@@ -78,8 +77,8 @@ export function installer(config: BuildConfig): void {
|
||||
return; // hit the finally{} block before exiting
|
||||
}
|
||||
|
||||
// If we reach here, everything seems fine. Write modules.ts and log some output
|
||||
// Note: we compile modules.ts in two parts for developer friendliness if they
|
||||
// If we reach here, everything seems fine. Write modules.js and log some output
|
||||
// Note: we compile modules.js in two parts for developer friendliness if they
|
||||
// happen to look at it.
|
||||
console.log("The following modules have been installed: ", installedModules);
|
||||
let modulesTsHeader = MODULES_TS_HEADER;
|
||||
@@ -193,5 +192,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
|
||||
}
|
||||
|
||||
function writeModulesTs(content: string): void {
|
||||
fs.writeFileSync("./src/modules.ts", content, "utf-8");
|
||||
fs.writeFileSync("./src/modules.js", content, "utf-8");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2022-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
69
package.json
69
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.89",
|
||||
"description": "A feature-rich client for Matrix.org",
|
||||
"version": "1.11.91",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/element-hq/element-web"
|
||||
},
|
||||
"license": "AGPL-3.0-only OR GPL-3.0-only",
|
||||
"license": "SEE LICENSE IN README.md",
|
||||
"files": [
|
||||
"lib",
|
||||
"res",
|
||||
@@ -66,30 +66,34 @@
|
||||
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
||||
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
|
||||
"coverage": "yarn test --coverage",
|
||||
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
|
||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"oidc-client-ts": "3.1.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001684",
|
||||
"caniuse-lite": "1.0.30001699",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "^0.1.1",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@matrix-org/analytics-events": "^0.29.0",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.3",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^8.0.0",
|
||||
"@vector-im/compound-design-tokens": "^2.0.1",
|
||||
"@vector-im/compound-web": "^7.5.0",
|
||||
"@vector-im/matrix-wysiwyg": "2.37.13",
|
||||
"@sentry/browser": "^9.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^3.0.0",
|
||||
"@vector-im/compound-web": "^7.6.1",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -121,7 +125,7 @@
|
||||
"linkify-string": "4.2.0",
|
||||
"linkifyjs": "4.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^4.0.0",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
@@ -134,16 +138,17 @@
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.157.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.10.1",
|
||||
"re-resizable": "6.10.3",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.13.1",
|
||||
"sanitize-html": "2.14.0",
|
||||
"tar-js": "^0.3.0",
|
||||
"temporal-polyfill": "^0.2.5",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
@@ -174,9 +179,10 @@
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@sentry/webpack-plugin": "^2.7.1",
|
||||
"@stylistic/eslint-plugin": "^2.9.0",
|
||||
"@sentry/webpack-plugin": "^3.0.0",
|
||||
"@stylistic/eslint-plugin": "^3.0.0",
|
||||
"@svgr/webpack": "^8.0.0",
|
||||
"@testcontainers/postgresql": "^10.16.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
@@ -188,7 +194,6 @@
|
||||
"@types/escape-html": "^1.0.1",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/fs-extra": "^11.0.0",
|
||||
"@types/glob-to-regexp": "^0.4.1",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/jitsi-meet": "^2.0.2",
|
||||
@@ -201,17 +206,17 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/tar-js": "^0.3.5",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-loader": "^9.0.0",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
@@ -226,13 +231,14 @@
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-deprecate": "0.8.5",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^28.0.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "^2.0.2",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"express": "^4.18.2",
|
||||
@@ -240,7 +246,6 @@
|
||||
"fetch-mock": "9.11.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"file-loader": "^6.0.0",
|
||||
"fs-extra": "^11.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"husky": "^9.0.0",
|
||||
@@ -252,14 +257,14 @@
|
||||
"jsqr": "^1.4.0",
|
||||
"knip": "^5.36.2",
|
||||
"lint-staged": "^15.0.2",
|
||||
"mailhog": "^4.16.0",
|
||||
"mailpit-api": "^1.0.5",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.9.0",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"minimist": "^1.2.6",
|
||||
"modernizr": "^3.12.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"playwright-core": "^1.45.1",
|
||||
"postcss": "8.4.38",
|
||||
"postcss": "8.4.46",
|
||||
"postcss-easings": "^4.0.0",
|
||||
"postcss-hexrgba": "2.1.0",
|
||||
"postcss-import": "16.1.0",
|
||||
@@ -269,26 +274,28 @@
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.4.2",
|
||||
"prettier": "3.5.1",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"stylelint": "^16.1.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^10.16.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typescript": "5.7.2",
|
||||
"typescript": "5.7.3",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^5.0.0",
|
||||
"webpack-cli": "^6.0.0",
|
||||
"webpack-dev-server": "^5.0.0",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
"webpack-version-file-plugin": "^0.5.0",
|
||||
"yaml": "^2.3.3"
|
||||
},
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
import { Options } from "./playwright/services";
|
||||
|
||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||
|
||||
export default defineConfig({
|
||||
const chromeProject = {
|
||||
...devices["Desktop Chrome"],
|
||||
channel: "chromium",
|
||||
permissions: ["clipboard-write", "clipboard-read", "microphone"],
|
||||
launchOptions: {
|
||||
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig<Options>({
|
||||
projects: [
|
||||
{
|
||||
name: "Chrome",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
channel: "chromium",
|
||||
permissions: ["clipboard-write", "clipboard-read", "microphone"],
|
||||
launchOptions: {
|
||||
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
|
||||
},
|
||||
...chromeProject,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -48,6 +54,22 @@ export default defineConfig({
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
{
|
||||
name: "Dendrite",
|
||||
use: {
|
||||
...chromeProject,
|
||||
homeserverType: "dendrite",
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
{
|
||||
name: "Pinecone",
|
||||
use: {
|
||||
...chromeProject,
|
||||
homeserverType: "pinecone",
|
||||
},
|
||||
ignoreSnapshots: true,
|
||||
},
|
||||
],
|
||||
use: {
|
||||
viewport: { width: 1280, height: 720 },
|
||||
|
||||
2
playwright/@types/playwright-core.d.ts
vendored
2
playwright/@types/playwright-core.d.ts
vendored
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.49.1-noble
|
||||
FROM mcr.microsoft.com/playwright:v1.50.1-noble
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -123,7 +123,7 @@ test.describe("Landmark navigation tests", () => {
|
||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||
|
||||
// Close the room
|
||||
page.goto("/#/home");
|
||||
await page.goto("/#/home");
|
||||
|
||||
// Pressing Control+F6 will first focus the space button
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -13,13 +13,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { expect, test } from "../../element-web-test";
|
||||
|
||||
test.use({
|
||||
startHomeserverOpts: "guest-enabled",
|
||||
config: async ({ homeserver }, use) => {
|
||||
await use({
|
||||
default_server_config: {
|
||||
"m.homeserver": { base_url: homeserver.config.baseUrl },
|
||||
},
|
||||
});
|
||||
synapseConfig: {
|
||||
allow_guest_access: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,15 @@ import type { Locator, Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
|
||||
// Find and click "Reply" button
|
||||
const clickButtonReply = async (tile: Locator) => {
|
||||
await expect(async () => {
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
}).toPass();
|
||||
};
|
||||
|
||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
test.use({
|
||||
@@ -222,8 +230,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
// Find and click "Reply" button on MessageActionBar
|
||||
const tile = page.locator(".mx_EventTile_last");
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
@@ -251,19 +258,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
const tile = page.locator(".mx_EventTile_last");
|
||||
|
||||
// Find and click "Reply" button
|
||||
const clickButtonReply = async () => {
|
||||
await tile.scrollIntoViewIfNeeded();
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
};
|
||||
|
||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
||||
await clickButtonReply();
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||
@@ -271,7 +271,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
||||
await clickButtonReply();
|
||||
await clickButtonReply(tile);
|
||||
|
||||
// Reply to the player with yet another audio file to create a reply chain
|
||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -95,7 +95,7 @@ test.describe("HTML Export", () => {
|
||||
async ({ page, app, room }) => {
|
||||
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
|
||||
// about the width changing and we can actually test this line looks correct.
|
||||
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
||||
await page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
||||
|
||||
// Send a bunch of messages to populate the room
|
||||
for (let i = 1; i < 10; i++) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -79,9 +79,8 @@ test.describe("Composer", () => {
|
||||
// Enter some more text, then send the message
|
||||
await page.getByRole("textbox").pressSequentially("this is the spoiler text ");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
// Check that a spoiler item has appeared in the timeline and locator the spoiler command text
|
||||
await expect(page.locator("button.mx_EventTile_spoiler")).toBeVisible();
|
||||
await expect(page.getByText("this is the spoiler text")).toBeVisible();
|
||||
// Check that a spoiler item has appeared in the timeline and contains the spoiler text
|
||||
await expect(page.locator("button.mx_EventTile_spoiler")).toHaveText("this is the spoiler text");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,7 +165,7 @@ test.describe("Composer", () => {
|
||||
// Type another
|
||||
await page.locator("div[contenteditable=true]").pressSequentially("my message 1");
|
||||
// Send message
|
||||
page.locator("div[contenteditable=true]").press("Enter");
|
||||
await page.locator("div[contenteditable=true]").press("Enter");
|
||||
// It was sent
|
||||
await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe("Create Room", () => {
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/);
|
||||
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||
const header = page.locator(".mx_RoomHeader");
|
||||
await expect(header).toContainText(name);
|
||||
});
|
||||
|
||||
99
playwright/e2e/crypto/backups-mas.spec.ts
Normal file
99
playwright/e2e/crypto/backups-mas.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { registerAccountMas } from "../oidc";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { TestClientServerAPI } from "../csAPI";
|
||||
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
|
||||
import { checkDeviceIsConnectedKeyBackup } from "./utils";
|
||||
|
||||
// These tests register an account with MAS because then we go through the "normal" registration flow
|
||||
// and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login,
|
||||
// which is faster but leaves us without crypto set up.
|
||||
test.use(masHomeserver);
|
||||
test.describe("Encryption state after registration", () => {
|
||||
test.skip(isDendrite, "does not yet support MAS");
|
||||
|
||||
test("Key backup is enabled by default", async ({ page, mailpitClient, app }, testInfo) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
// Wait for the ui to load
|
||||
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
|
||||
|
||||
// Recovery is not set up yet
|
||||
await checkDeviceIsConnectedKeyBackup(app, "1", true, false);
|
||||
});
|
||||
|
||||
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Key backup reset from elsewhere", () => {
|
||||
test.skip(isDendrite, "does not yet support MAS");
|
||||
|
||||
test("Key backup is disabled when reset from elsewhere", async ({
|
||||
page,
|
||||
mailpitClient,
|
||||
request,
|
||||
homeserver,
|
||||
}, testInfo) => {
|
||||
const testUsername = `alice_${testInfo.testId}`;
|
||||
const testPassword = "Pa$sW0rD!";
|
||||
|
||||
// there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake
|
||||
// clock so we can skip the delay
|
||||
await page.clock.install();
|
||||
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
const accessToken = await page.evaluate(() => window.mxMatrixClientPeg.get().getAccessToken());
|
||||
|
||||
const csAPI = new TestClientServerAPI(request, homeserver, accessToken);
|
||||
|
||||
const backupInfo = await csAPI.getCurrentBackupInfo();
|
||||
|
||||
await csAPI.deleteBackupVersion(backupInfo.version);
|
||||
|
||||
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
// Should be the message we sent plus the room creation event
|
||||
await expect(page.locator(".mx_EventTile")).toHaveCount(2);
|
||||
await expect(
|
||||
page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"),
|
||||
).toBeVisible();
|
||||
|
||||
// Wait for it to try uploading the key
|
||||
await page.clock.fastForward(20000);
|
||||
|
||||
await expect(page.getByRole("heading", { level: 1, name: "New Recovery Method" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -2,15 +2,15 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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";
|
||||
import { test as masTest, registerAccountMas } from "../oidc";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog } from "./utils.ts";
|
||||
|
||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
@@ -20,33 +20,8 @@ async function expectBackupVersionToBe(page: Page, version: string) {
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
||||
}
|
||||
|
||||
masTest.describe("Encryption state after registration", () => {
|
||||
masTest.skip(isDendrite, "does not yet support MAS");
|
||||
|
||||
masTest("Key backup is enabled by default", async ({ page, mailhog, app }) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
expect(page.getByText("This session is backing up your keys.")).toBeVisible();
|
||||
});
|
||||
|
||||
masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => {
|
||||
await page.goto("/#/login");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
|
||||
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Backups", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Hanako",
|
||||
});
|
||||
@@ -61,19 +36,7 @@ test.describe("Backups", () => {
|
||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const securityKey = await app.getClipboard();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
const securityKey = await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -88,14 +51,15 @@ test.describe("Backups", () => {
|
||||
await expectBackupVersionToBe(page, "1");
|
||||
|
||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
||||
// Delete it
|
||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
||||
|
||||
// Create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByLabel("Recovery Key").fill(securityKey);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// Should be successful
|
||||
@@ -126,8 +90,8 @@ test.describe("Backups", () => {
|
||||
|
||||
// Try to create another
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
||||
// But cancel the security key dialog, to simulate not having the secret storage passphrase
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Recovery Key" })).toBeVisible();
|
||||
// But cancel the recovery key dialog, to simulate not having the secret storage passphrase
|
||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { logIntoElement } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Complete security", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Jeff",
|
||||
});
|
||||
@@ -19,9 +21,9 @@ test.describe("Complete security", () => {
|
||||
homeserver,
|
||||
credentials,
|
||||
}) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
// see also "Verify device during login with SAS" in `verifiction.spec.ts`.
|
||||
// see also "Verify device during login with SAS" in `verification.spec.ts`.
|
||||
});
|
||||
|
||||
@@ -2,15 +2,23 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { expect, test } from "../../element-web-test";
|
||||
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import {
|
||||
autoJoin,
|
||||
completeCreateSecretStorageDialog,
|
||||
copyAndContinue,
|
||||
createSharedRoomWithUser,
|
||||
enableKeyBackup,
|
||||
verify,
|
||||
} from "./utils";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const checkDMRoom = async (page: Page) => {
|
||||
const body = page.locator(".mx_RoomView_body");
|
||||
@@ -67,6 +75,7 @@ const bobJoin = async (page: Page, bob: Bot) => {
|
||||
};
|
||||
|
||||
test.describe("Cryptography", function () {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
@@ -109,18 +118,7 @@ test.describe("Cryptography", function () {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
||||
await copyAndContinue(page);
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step; however, it
|
||||
// can be quite quick, and playwright can miss it, so we can't test for it.
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// Verify that the SSSS keys are in the account data stored in the server
|
||||
await verifyKey(app, "master");
|
||||
@@ -188,7 +186,7 @@ test.describe("Cryptography", function () {
|
||||
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
|
||||
|
||||
// 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();
|
||||
|
||||
// Enter the password
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,8 @@ test.describe("Cryptography", function () {
|
||||
});
|
||||
|
||||
test.describe("decryption failure messages", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
test("should handle device-relative historical messages", async ({
|
||||
homeserver,
|
||||
page,
|
||||
@@ -45,7 +47,7 @@ test.describe("Cryptography", function () {
|
||||
await logOutOfElement(page, true);
|
||||
|
||||
// Log in again, and see how the message looks.
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
await app.viewRoomByName("Test room");
|
||||
const lastTile = page.locator(".mx_EventTile").last();
|
||||
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
||||
@@ -62,7 +64,7 @@ test.describe("Cryptography", function () {
|
||||
|
||||
// Finally, log out again, and back in, skipping verification for now, and see what we see.
|
||||
await logOutOfElement(page);
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
||||
await app.viewRoomByName("Test room");
|
||||
|
||||
@@ -2,26 +2,29 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test as base, expect, Fixtures } from "../../element-web-test";
|
||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { completeCreateSecretStorageDialog, createBot, logIntoElement } from "./utils.ts";
|
||||
import { type Client } from "../../pages/client.ts";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
|
||||
const test = base.extend<Fixtures>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
startHomeserverOpts: async ({}, use) => {
|
||||
await use("dehydration");
|
||||
const NAME = "Alice";
|
||||
|
||||
test.use({
|
||||
displayName: NAME,
|
||||
synapseConfig: {
|
||||
experimental_features: {
|
||||
msc2697_enabled: false,
|
||||
msc3814_enabled: true,
|
||||
},
|
||||
},
|
||||
config: async ({ homeserver, context }, use) => {
|
||||
config: async ({ config, context }, use) => {
|
||||
const wellKnown = {
|
||||
"m.homeserver": {
|
||||
base_url: homeserver.config.baseUrl,
|
||||
},
|
||||
...config.default_server_config,
|
||||
"org.matrix.msc3814": true,
|
||||
};
|
||||
|
||||
@@ -29,27 +32,14 @@ const test = base.extend<Fixtures>({
|
||||
await route.fulfill({ json: wellKnown });
|
||||
});
|
||||
|
||||
await use({
|
||||
default_server_config: wellKnown,
|
||||
});
|
||||
await use(config);
|
||||
},
|
||||
});
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
|
||||
function getMemberTileByName(page: Page, name: string): Locator {
|
||||
return page.locator(`.mx_EntityTile, [title="${name}"]`);
|
||||
}
|
||||
|
||||
test.describe("Dehydration", () => {
|
||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||
|
||||
test.use({
|
||||
displayName: NAME,
|
||||
});
|
||||
|
||||
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
test("'Set up secure backup' creates dehydrated device", async ({ page, user, app }, workerInfo) => {
|
||||
// Create a backup (which will create SSSS, and dehydrated device)
|
||||
|
||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||
@@ -58,47 +48,99 @@ test.describe("Dehydration", () => {
|
||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
||||
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
await completeCreateSecretStorageDialog(page);
|
||||
|
||||
// It's the first time and secure storage is not set up, so it will create one
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
|
||||
// Open the settings again
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
|
||||
// The Security tab should indicate that there is a dehydrated device present
|
||||
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
|
||||
|
||||
await app.settings.closeDialog();
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
|
||||
// the dehydrated device gets created with the name "Dehydrated
|
||||
// device". We want to make sure that it is not visible as a normal
|
||||
// device.
|
||||
const sessionsTab = await app.settings.openUserSettings("Sessions");
|
||||
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
|
||||
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
|
||||
|
||||
// First it displays an informative panel about the recovery key
|
||||
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Next, it displays the new recovery key. We click on the copy button.
|
||||
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
|
||||
const recoveryKey = await app.getClipboard();
|
||||
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(
|
||||
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
|
||||
).toBeVisible();
|
||||
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
|
||||
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
|
||||
|
||||
await app.settings.closeDialog();
|
||||
|
||||
// now check that the user info right-panel shows the dehydrated device
|
||||
// as a feature rather than as a normal device
|
||||
await app.client.createRoom({ name: ROOM_NAME });
|
||||
await expectDehydratedDeviceEnabled(app);
|
||||
});
|
||||
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
test("Reset recovery key during login re-creates dehydrated device", async ({
|
||||
page,
|
||||
homeserver,
|
||||
app,
|
||||
credentials,
|
||||
}) => {
|
||||
// Set up cross-signing and recovery
|
||||
const { botClient } = await createBot(page, homeserver, credentials);
|
||||
// ... and dehydration
|
||||
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
|
||||
|
||||
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
|
||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
||||
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
|
||||
expect(initialDehydratedDeviceIds.length).toBe(1);
|
||||
|
||||
await getMemberTileByName(page, NAME).click();
|
||||
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
|
||||
await botClient.evaluate(async (client) => client.stopClient());
|
||||
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
||||
// Log in our client
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Oh no, we forgot our recovery key
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Proceed with reset" }).click();
|
||||
|
||||
await completeCreateSecretStorageDialog(page, { accountPassword: credentials.password });
|
||||
|
||||
// There should be a brand new dehydrated device
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
expect(dehydratedDeviceIds.length).toBe(1);
|
||||
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
|
||||
});
|
||||
});
|
||||
|
||||
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
|
||||
return await client.evaluate(async (client) => {
|
||||
const userId = client.getUserId();
|
||||
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
|
||||
return Array.from(
|
||||
devices
|
||||
.get(userId)
|
||||
.values()
|
||||
.filter((d) => d.dehydrated)
|
||||
.map((d) => d.deviceId),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait for our user to have a dehydrated device */
|
||||
async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise<void> {
|
||||
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
|
||||
//
|
||||
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
|
||||
return dehydratedDeviceIds.length;
|
||||
})
|
||||
.toEqual(1);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
awaitVerifier,
|
||||
checkDeviceIsConnectedKeyBackup,
|
||||
checkDeviceIsCrossSigned,
|
||||
createBot,
|
||||
doTwoWaySasVerification,
|
||||
logIntoElement,
|
||||
waitForVerificationRequest,
|
||||
} from "./utils";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { type Bot } from "../../pages/bot";
|
||||
|
||||
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
let aliceBotClient: Bot;
|
||||
@@ -28,29 +29,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
let expectedBackupVersion: string;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// Visit the login page of the app, to load the matrix sdk
|
||||
await page.goto("/#/login");
|
||||
|
||||
// wait for the page to load
|
||||
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
||||
|
||||
// Create a new device for alice
|
||||
aliceBotClient = new Bot(page, homeserver, {
|
||||
bootstrapCrossSigning: true,
|
||||
bootstrapSecretStorage: true,
|
||||
});
|
||||
aliceBotClient.setCredentials(credentials);
|
||||
|
||||
// Backup is prepared in the background. Poll until it is ready.
|
||||
const botClientHandle = await aliceBotClient.prepareClient();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
||||
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
||||
);
|
||||
return expectedBackupVersion;
|
||||
})
|
||||
.not.toBe(null);
|
||||
const res = await createBot(page, homeserver, credentials, true);
|
||||
aliceBotClient = res.botClient;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||
@@ -66,7 +47,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
}
|
||||
|
||||
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Launch the verification request between alice and the bot
|
||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||
@@ -87,13 +68,13 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||
// as we need to wait for the secret gossiping to happen.
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Launch the verification request between alice and the bot
|
||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||
@@ -131,16 +112,14 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Select the security phrase
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||
|
||||
// Fill the passphrase
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
@@ -154,18 +133,18 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Select the security phrase
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with 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");
|
||||
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();
|
||||
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
|
||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
@@ -177,11 +156,11 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
});
|
||||
|
||||
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
/* Dismiss "Verify this device" */
|
||||
const authPage = page.locator(".mx_AuthPage");
|
||||
@@ -212,16 +191,17 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
/* on the bot side, wait for the verifier to exist ... */
|
||||
const verifier = await awaitVerifier(botVerificationRequest);
|
||||
// ... confirm ...
|
||||
botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify());
|
||||
void botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify());
|
||||
// ... and then check the emoji match
|
||||
await doTwoWaySasVerification(page, verifier);
|
||||
|
||||
/* And we're all done! */
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
await expect(
|
||||
infoDialog.getByText(`You've successfully verified (${aliceBotClient.credentials.deviceId})!`),
|
||||
).toBeVisible();
|
||||
// We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite
|
||||
await expect(infoDialog.getByText(`You've successfully verified`)).toContainText(
|
||||
`(${aliceBotClient.credentials.deviceId})`,
|
||||
);
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { Locator } from "@playwright/test";
|
||||
import { type Locator } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import {
|
||||
@@ -17,9 +17,10 @@ import {
|
||||
logIntoElement,
|
||||
logOutOfElement,
|
||||
verify,
|
||||
waitForDevices,
|
||||
} from "./utils";
|
||||
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||
|
||||
test.describe("Cryptography", function () {
|
||||
test.use({
|
||||
@@ -53,6 +54,8 @@ test.describe("Cryptography", function () {
|
||||
|
||||
// Even though Alice has seen Bob's join event, Bob may not have done so yet. Wait for the sync to arrive.
|
||||
await bob.awaitRoomMembership(testRoomId);
|
||||
|
||||
await app.client.network.setupRoute();
|
||||
});
|
||||
|
||||
test("should show the correct shield on e2e events", async ({
|
||||
@@ -64,6 +67,9 @@ test.describe("Cryptography", function () {
|
||||
// Bob has a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
|
||||
// Dismiss the toast nagging us to set up recovery otherwise it gets in the way of clicking the room list
|
||||
await page.getByRole("button", { name: "Not now" }).click();
|
||||
|
||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "the bird is in the hand",
|
||||
@@ -139,25 +145,8 @@ test.describe("Cryptography", function () {
|
||||
// bob deletes his second device
|
||||
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.
|
||||
async function awaitOneDevice(iterations = 1) {
|
||||
const rightPanel = page.locator(".mx_RightPanel");
|
||||
await rightPanel.getByTestId("base-card-back-button").click();
|
||||
await rightPanel.getByText("Bob").click();
|
||||
const sessionCountText = await rightPanel
|
||||
.locator(".mx_UserInfo_devices")
|
||||
.getByText(" session", { exact: false })
|
||||
.textContent();
|
||||
// cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here
|
||||
if (sessionCountText != "1 session" && sessionCountText != "1 verified session") {
|
||||
if (iterations >= 10) {
|
||||
throw new Error(`Bob still has ${sessionCountText} after 10 iterations`);
|
||||
}
|
||||
await awaitOneDevice(iterations + 1);
|
||||
}
|
||||
}
|
||||
|
||||
await awaitOneDevice();
|
||||
// wait for the logout to propagate.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
|
||||
// close and reopen the room, to get the shield to update.
|
||||
await app.viewRoomByName("Bob");
|
||||
@@ -205,7 +194,7 @@ test.describe("Cryptography", function () {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
await page.reload();
|
||||
await logIntoElement(page, homeserver, aliceCredentials, securityKey);
|
||||
await logIntoElement(page, aliceCredentials, securityKey);
|
||||
|
||||
/* go back to the test room and find Bob's message again */
|
||||
await app.viewRoomById(testRoomId);
|
||||
@@ -280,11 +269,7 @@ test.describe("Cryptography", function () {
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
||||
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
||||
// his user info.
|
||||
await app.toggleRoomInfoPanel();
|
||||
const rightPanel = page.locator(".mx_RightPanel");
|
||||
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
|
||||
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
|
||||
// Our app is blocked from syncing while Bob sends his messages.
|
||||
await app.client.network.goOffline();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||
|
||||
/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
|
||||
test.describe("Invisible cryptography", () => {
|
||||
test.slow();
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: { displayName: "Bob" },
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Logout tests", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
});
|
||||
|
||||
test("Ask to set up recovery on logout if not setup", async ({ page, app }) => {
|
||||
|
||||
@@ -2,31 +2,31 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 path from "path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { expect, Fixtures, test as base } from "../../element-web-test";
|
||||
|
||||
const test = base.extend<Fixtures>({
|
||||
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
|
||||
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
|
||||
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
|
||||
const resourcePath = path.join(__dirname, new URL(request.url()).pathname);
|
||||
const body = await readFile(resourcePath, { encoding: "utf-8" });
|
||||
await route.fulfill({ body });
|
||||
});
|
||||
await page.goto("/test_indexeddb_cryptostore_dump/index.html");
|
||||
|
||||
await use(credentials);
|
||||
},
|
||||
});
|
||||
import { expect, test } from "../../element-web-test";
|
||||
|
||||
test.describe("migration", { tag: "@no-webkit" }, function () {
|
||||
test.use({ displayName: "Alice" });
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
|
||||
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
|
||||
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
|
||||
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
|
||||
const resourcePath = path.join(__dirname, new URL(request.url()).pathname);
|
||||
const body = await readFile(resourcePath, { encoding: "utf-8" });
|
||||
await route.fulfill({ body });
|
||||
});
|
||||
await page.goto("/test_indexeddb_cryptostore_dump/index.html");
|
||||
|
||||
await use(credentials);
|
||||
},
|
||||
});
|
||||
|
||||
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
|
||||
test.slow();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
53
playwright/e2e/crypto/toasts.spec.ts
Normal file
53
playwright/e2e/crypto/toasts.spec.ts
Normal file
@@ -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 GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { createBot, deleteCachedSecrets, logIntoElement } from "./utils";
|
||||
|
||||
test.describe("Key storage out of sync toast", () => {
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
|
||||
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||
|
||||
await deleteCachedSecrets(page);
|
||||
|
||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||
await page.getByRole("button", { name: "Add room" }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
});
|
||||
|
||||
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||
// Need to wait for 2 to appear since playwright only evaluates 'first()' initially, so the waiting won't work
|
||||
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||
|
||||
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
||||
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -2,16 +2,15 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
|
||||
import { Client } from "../../pages/client";
|
||||
import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
|
||||
import { type Client } from "../../pages/client";
|
||||
|
||||
test.describe("User verification", () => {
|
||||
// note that there are other tests that check user verification works in `crypto.spec.ts`.
|
||||
@@ -33,13 +32,17 @@ test.describe("User verification", () => {
|
||||
});
|
||||
|
||||
test("can receive a verification request when there is no existing DM", async ({
|
||||
app,
|
||||
page,
|
||||
bot: bob,
|
||||
user: aliceCredentials,
|
||||
toasts,
|
||||
room: { roomId: dmRoomId },
|
||||
}) => {
|
||||
await 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
|
||||
const bobVerificationRequest = await bob.evaluateHandle(
|
||||
@@ -74,7 +77,7 @@ test.describe("User verification", () => {
|
||||
/* on the bot side, wait for the verifier to exist ... */
|
||||
const botVerifier = await awaitVerifier(bobVerificationRequest);
|
||||
// ... confirm ...
|
||||
botVerifier.evaluate((verifier) => verifier.verify());
|
||||
void botVerifier.evaluate((verifier) => verifier.verify());
|
||||
// ... and then check the emoji match
|
||||
await doTwoWaySasVerification(page, botVerifier);
|
||||
|
||||
@@ -84,13 +87,17 @@ test.describe("User verification", () => {
|
||||
});
|
||||
|
||||
test("can abort emoji verification when emoji mismatch", async ({
|
||||
app,
|
||||
page,
|
||||
bot: bob,
|
||||
user: aliceCredentials,
|
||||
toasts,
|
||||
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
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -2,26 +2,70 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { expect, JSHandle, type Page } from "@playwright/test";
|
||||
import { expect, type JSHandle, type Page } from "@playwright/test";
|
||||
|
||||
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import type {
|
||||
CryptoEvent,
|
||||
EmojiMapping,
|
||||
GeneratedSecretStorageKey,
|
||||
ShowSasCallbacks,
|
||||
VerificationRequest,
|
||||
Verifier,
|
||||
VerifierEvent,
|
||||
} from "matrix-js-sdk/src/crypto-api";
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { Client } from "../../pages/client";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { type Client } from "../../pages/client";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { Bot } from "../../pages/bot";
|
||||
|
||||
/**
|
||||
* Create a bot client using the supplied credentials, and wait for the key backup to be ready.
|
||||
* @param page - the playwright `page` fixture
|
||||
* @param homeserver - the homeserver to use
|
||||
* @param credentials - the credentials to use for the bot client
|
||||
* @param usePassphrase - whether to use a passphrase when creating the recovery key
|
||||
*/
|
||||
export async function createBot(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
credentials: Credentials,
|
||||
usePassphrase = false,
|
||||
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
|
||||
// Visit the login page of the app, to load the matrix sdk
|
||||
await page.goto("/#/login");
|
||||
|
||||
// wait for the page to load
|
||||
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
||||
|
||||
// Create a new bot client
|
||||
const botClient = new Bot(page, homeserver, {
|
||||
bootstrapCrossSigning: true,
|
||||
bootstrapSecretStorage: true,
|
||||
usePassphrase,
|
||||
});
|
||||
botClient.setCredentials(credentials);
|
||||
// Backup is prepared in the background. Poll until it is ready.
|
||||
const botClientHandle = await botClient.prepareClient();
|
||||
let expectedBackupVersion: string;
|
||||
await expect
|
||||
.poll(async () => {
|
||||
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
||||
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
||||
);
|
||||
return expectedBackupVersion;
|
||||
})
|
||||
.not.toBe(null);
|
||||
|
||||
const recoveryKey = await botClient.getRecoveryKey();
|
||||
|
||||
return { botClient, recoveryKey, expectedBackupVersion };
|
||||
}
|
||||
|
||||
/**
|
||||
* wait for the given client to receive an incoming verification request, and automatically accept it
|
||||
*
|
||||
@@ -59,7 +103,7 @@ export function handleSasVerification(verifier: JSHandle<Verifier>): Promise<Emo
|
||||
return new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ShowSasCallbacks) => {
|
||||
verifier.off("show_sas" as VerifierEvent, onShowSas);
|
||||
event.confirm();
|
||||
void event.confirm();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
@@ -98,14 +142,16 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
|
||||
* Check that the current device is connected to the expected key backup.
|
||||
* Also checks that the decryption key is known and cached locally.
|
||||
*
|
||||
* @param page - the page to check
|
||||
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
||||
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
||||
* @param checkBackupKeyInCache - whether to check that the backup 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(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
expectedBackupVersion: string,
|
||||
checkBackupKeyInCache: boolean,
|
||||
checkBackupPrivateKeyInCache: boolean,
|
||||
checkBackupKeyIn4S: boolean = true,
|
||||
): Promise<void> {
|
||||
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
||||
if (!expectedBackupVersion) {
|
||||
@@ -114,23 +160,48 @@ export async function checkDeviceIsConnectedKeyBackup(
|
||||
);
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
|
||||
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
|
||||
const backupData = await app.client.evaluate(async (client: MatrixClient) => {
|
||||
const crypto = client.getCrypto();
|
||||
if (!crypto) return;
|
||||
|
||||
// expand the advanced section to see the active version in the reports
|
||||
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
|
||||
const backupInfo = await crypto.getKeyBackupInfo();
|
||||
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored());
|
||||
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
||||
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
|
||||
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
|
||||
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||
|
||||
if (checkBackupKeyInCache) {
|
||||
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
|
||||
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
|
||||
return {
|
||||
backupInfo,
|
||||
hasBackupPrivateKeyFromCache,
|
||||
backupPrivateKeyWellFormed,
|
||||
backupKeyIn4S,
|
||||
activeBackupVersion,
|
||||
};
|
||||
});
|
||||
|
||||
if (!backupData) {
|
||||
throw new Error("Crypto module is not available");
|
||||
}
|
||||
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
||||
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
||||
);
|
||||
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } =
|
||||
backupData;
|
||||
|
||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
|
||||
// We have a key backup
|
||||
expect(backupInfo).toBeDefined();
|
||||
// The key backup version is as expected
|
||||
expect(backupInfo.version).toBe(expectedBackupVersion);
|
||||
// The active backup version is as expected
|
||||
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
||||
// The backup key is stored in 4S
|
||||
if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
|
||||
|
||||
if (checkBackupPrivateKeyInCache) {
|
||||
// The backup key is available locally
|
||||
expect(hasBackupPrivateKeyFromCache).toBe(true);
|
||||
// The backup key is well-formed
|
||||
expect(backupPrivateKeyWellFormed).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,30 +209,22 @@ export async function checkDeviceIsConnectedKeyBackup(
|
||||
*
|
||||
* If a `securityKey` is given, verifies the new device using the key.
|
||||
*/
|
||||
export async function logIntoElement(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
credentials: Credentials,
|
||||
securityKey?: string,
|
||||
) {
|
||||
export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) {
|
||||
await page.goto("/#/login");
|
||||
|
||||
// select homeserver
|
||||
await page.getByRole("button", { name: "Edit" }).click();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// wait for the dialog to go away
|
||||
await expect(page.locator(".mx_ServerPickerDialog")).not.toBeVisible();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
|
||||
// if a securityKey was given, verify the new device
|
||||
if (securityKey !== undefined) {
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
// Fill in the security key
|
||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
|
||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||
if (await useSecurityKey.isVisible()) {
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
// Fill in the recovery key
|
||||
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
@@ -188,18 +251,19 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the security 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 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) {
|
||||
const settings = await app.settings.openUserSettings("Security & Privacy");
|
||||
await settings.getByRole("button", { name: "Verify this session" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await app.page.getByRole("button", { name: "Done" }).click();
|
||||
await app.settings.closeDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,28 +293,61 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
||||
*
|
||||
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
||||
*
|
||||
* Returns the security key
|
||||
* Returns the recovery key
|
||||
*/
|
||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||
await app.settings.openUserSettings("Security & Privacy");
|
||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
||||
const dialog = app.page.locator(".mx_Dialog");
|
||||
// Recovery key is selected by default
|
||||
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
|
||||
|
||||
// copy the text ourselves
|
||||
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
|
||||
await copyAndContinue(app.page);
|
||||
|
||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
||||
await dialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
return securityKey;
|
||||
return await completeCreateSecretStorageDialog(app.page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on copy and continue buttons to dismiss the security key dialog
|
||||
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
||||
*
|
||||
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
|
||||
*
|
||||
* @param page - The playwright `Page` fixture.
|
||||
* @param opts - Options object
|
||||
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
|
||||
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
|
||||
*
|
||||
* @returns the new recovery key.
|
||||
*/
|
||||
export async function completeCreateSecretStorageDialog(
|
||||
page: Page,
|
||||
opts?: { accountPassword?: string },
|
||||
): Promise<string> {
|
||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||
// "Generate a Recovery Key" is selected by default
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Recovery Key" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||
// copy the recovery key to use it later
|
||||
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
// If the device is unverified, there should be a "Setting up keys" step.
|
||||
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
|
||||
// the step is quite quick, and playwright can miss it, so we can't test for it.
|
||||
if (opts && Object.hasOwn(opts, "accountPassword")) {
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
|
||||
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
|
||||
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||
}
|
||||
|
||||
// Either way, we end up at a success dialog:
|
||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
|
||||
|
||||
return recoveryKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on copy and continue buttons to dismiss the recovery key dialog
|
||||
*/
|
||||
export async function copyAndContinue(page: Page) {
|
||||
await page.getByRole("button", { name: "Copy" }).click();
|
||||
@@ -326,7 +423,7 @@ export async function autoJoin(client: Client) {
|
||||
await client.evaluate((cli) => {
|
||||
cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||
if (member.membership === "invite" && member.userId === cli.getUserId()) {
|
||||
cli.joinRoom(member.roomId);
|
||||
void cli.joinRoom(member.roomId);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -385,3 +482,53 @@ export async function createSecondBotDevice(page: Page, homeserver: HomeserverIn
|
||||
await bobSecondDevice.prepareClient();
|
||||
return bobSecondDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the cached secrets from the indexedDB
|
||||
* This is a workaround to simulate the case where the secrets are not cached.
|
||||
*/
|
||||
export async function deleteCachedSecrets(page: Page) {
|
||||
await page.evaluate(async () => {
|
||||
const removeCachedSecrets = new Promise((resolve) => {
|
||||
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
|
||||
request.onsuccess = (event: Event & { target: { result: IDBDatabase } }) => {
|
||||
const db = event.target.result;
|
||||
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
|
||||
request.onsuccess = () => {
|
||||
db.close();
|
||||
resolve(undefined);
|
||||
};
|
||||
};
|
||||
});
|
||||
await removeCachedSecrets;
|
||||
});
|
||||
await page.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the given user has a given number of devices.
|
||||
* This function will check the device keys ten times and if
|
||||
* the expected number of devices were not found by then, an
|
||||
* error is thrown.
|
||||
*/
|
||||
export async function waitForDevices(
|
||||
app: ElementAppPage,
|
||||
userId: string,
|
||||
expectedNumberOfDevices: number,
|
||||
): Promise<void> {
|
||||
const result = await app.client.evaluate(
|
||||
async (cli, { userId, expectedNumberOfDevices }) => {
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], true);
|
||||
const deviceMap = userDeviceMap?.get(userId);
|
||||
if (deviceMap.size === expectedNumberOfDevices) return true;
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ userId, expectedNumberOfDevices },
|
||||
);
|
||||
if (!result) {
|
||||
throw new Error(`User ${userId} did not have ${expectedNumberOfDevices} devices within ten iterations!`);
|
||||
}
|
||||
}
|
||||
|
||||
39
playwright/e2e/csAPI.ts
Normal file
39
playwright/e2e/csAPI.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type APIRequestContext } from "playwright-core";
|
||||
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
|
||||
import { type HomeserverInstance } from "../plugins/homeserver";
|
||||
import { ClientServerApi } from "../plugins/utils/api.ts";
|
||||
|
||||
/**
|
||||
* A small subset of the Client-Server API used to manipulate the state of the
|
||||
* account on the homeserver independently of the client under test.
|
||||
*/
|
||||
export class TestClientServerAPI extends ClientServerApi {
|
||||
public constructor(
|
||||
request: APIRequestContext,
|
||||
homeserver: HomeserverInstance,
|
||||
private accessToken: string,
|
||||
) {
|
||||
super(homeserver.baseUrl);
|
||||
this.setRequest(request);
|
||||
}
|
||||
|
||||
public async getCurrentBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||
return this.request("GET", `/v3/room_keys/version`, this.accessToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the API directly to delete the given backup version
|
||||
* @param version The version to delete
|
||||
*/
|
||||
public async deleteBackupVersion(version: string): Promise<void> {
|
||||
await this.request("DELETE", `/v3/room_keys/version/${version}`, this.accessToken);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,17 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { Locator, Page } from "@playwright/test";
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
async function sendEvent(app: ElementAppPage, roomId: string): Promise<ISendEventResponse> {
|
||||
return app.client.sendEvent(roomId, null, "m.room.message" as EventType, {
|
||||
@@ -31,6 +32,8 @@ function mkPadding(n: number): IContent {
|
||||
}
|
||||
|
||||
test.describe("Editing", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||
|
||||
// Edit "Message"
|
||||
const editLastMessage = async (page: Page, edit: string) => {
|
||||
const eventTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,29 +2,43 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { expect, test } from "../../element-web-test";
|
||||
import { type CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
const username = "user1234";
|
||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||
const password = "oETo7MPf0o";
|
||||
const email = "user@nowhere.dummy";
|
||||
|
||||
const test = base.extend({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
credentials: async ({}, use, testInfo) => {
|
||||
await use({
|
||||
username: `user_${testInfo.testId}`,
|
||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||
password: "oETo7MPf0o",
|
||||
} as CredentialsWithDisplayName);
|
||||
},
|
||||
});
|
||||
|
||||
test.use(emailHomeserver);
|
||||
test.use({
|
||||
config: {
|
||||
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||
// We point that to a guaranteed-invalid domain.
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("Forgot Password", () => {
|
||||
test.use({
|
||||
startHomeserverOpts: ({ mailhog }, use) =>
|
||||
use({
|
||||
template: "email",
|
||||
variables: {
|
||||
SMTP_HOST: "host.containers.internal",
|
||||
SMTP_PORT: mailhog.instance.smtpPort,
|
||||
},
|
||||
}),
|
||||
});
|
||||
test.skip(isDendrite, "not yet wired up");
|
||||
|
||||
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||
await page.goto("/");
|
||||
@@ -32,38 +46,42 @@ test.describe("Forgot Password", () => {
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
|
||||
// need to select a homeserver at this stage, before entering the forgot password flow
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||
|
||||
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
||||
});
|
||||
|
||||
test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||
const user = await homeserver.registerUser(username, password);
|
||||
test(
|
||||
"renders email verification dialog properly",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, homeserver, credentials }) => {
|
||||
const user = await homeserver.registerUser(credentials.username, credentials.password);
|
||||
|
||||
await homeserver.setThreepid(user.userId, "email", email);
|
||||
await homeserver.setThreepid(user.userId, "email", email);
|
||||
|
||||
await page.goto("/");
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Email address" }).fill(email);
|
||||
await page.getByRole("textbox", { name: "Email address" }).fill(email);
|
||||
|
||||
await page.getByRole("button", { name: "Send email" }).click();
|
||||
await page.getByRole("button", { name: "Send email" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password);
|
||||
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password);
|
||||
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(credentials.password);
|
||||
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(credentials.password);
|
||||
|
||||
await page.getByRole("button", { name: "Reset password" }).click();
|
||||
await page.getByRole("button", { name: "Reset password" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
||||
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
||||
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
||||
});
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -69,29 +69,13 @@ async function sendActionFromIntegrationManager(
|
||||
await iframe.getByRole("button", { name: "Press to send action" }).click();
|
||||
}
|
||||
|
||||
async function clickUntilGone(page: Page, selector: string, attempt = 0) {
|
||||
if (attempt === 11) {
|
||||
throw new Error("clickUntilGone attempt count exceeded");
|
||||
}
|
||||
|
||||
await page.locator(selector).last().click();
|
||||
|
||||
const count = await page.locator(selector).count();
|
||||
if (count > 0) {
|
||||
return clickUntilGone(page, selector, ++attempt);
|
||||
}
|
||||
}
|
||||
|
||||
async function expectKickedMessage(page: Page, shouldExist: boolean) {
|
||||
// Expand any event summaries, we can't use a click multiple here because clicking one might de-render others
|
||||
// This is quite horrible but seems the most stable way of clicking 0-N buttons,
|
||||
// one at a time with a full re-evaluation after each click
|
||||
await clickUntilGone(page, ".mx_GenericEventListSummary_toggle[aria-expanded=false]");
|
||||
|
||||
// Check for the event message (or lack thereof)
|
||||
await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
|
||||
visible: shouldExist,
|
||||
});
|
||||
await expect(async () => {
|
||||
await page.locator(".mx_GenericEventListSummary_toggle[aria-expanded=false]").last().click();
|
||||
await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
|
||||
visible: shouldExist,
|
||||
});
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
test.describe("Integration Manager: Kick", () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { waitForRoom } from "../utils";
|
||||
import { Filter } from "../../pages/Spotlight";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Create Knock Room", () => {
|
||||
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_ask_to_join"],
|
||||
@@ -79,6 +81,7 @@ test.describe("Create Knock Room", () => {
|
||||
|
||||
const spotlightDialog = await app.openSpotlight();
|
||||
await spotlightDialog.filter(Filter.PublicRooms);
|
||||
await spotlightDialog.search("Cyber");
|
||||
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -13,8 +13,10 @@ import { type Visibility } from "matrix-js-sdk/src/matrix";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { waitForRoom } from "../utils";
|
||||
import { Filter } from "../../pages/Spotlight";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Knock Into Room", () => {
|
||||
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_ask_to_join"],
|
||||
@@ -282,6 +284,7 @@ test.describe("Knock Into Room", () => {
|
||||
|
||||
const spotlightDialog = await app.openSpotlight();
|
||||
await spotlightDialog.filter(Filter.PublicRooms);
|
||||
await spotlightDialog.search("Cyber");
|
||||
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
||||
await spotlightDialog.results.nth(0).click();
|
||||
|
||||
|
||||
@@ -4,14 +4,16 @@ Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 Nordeck IT + Consulting GmbH.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { waitForRoom } from "../utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Manage Knocks", () => {
|
||||
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_ask_to_join"],
|
||||
@@ -50,7 +52,7 @@ test.describe("Manage Knocks", () => {
|
||||
});
|
||||
|
||||
test("should deny knock using bar", async ({ page, app, bot, room }) => {
|
||||
bot.knockRoom(room.roomId);
|
||||
await bot.knockRoom(room.roomId);
|
||||
|
||||
const roomKnocksBar = page.locator(".mx_RoomKnocksBar");
|
||||
await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -10,8 +10,12 @@ import { Bot } from "../../pages/bot";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { type Credentials } from "../../plugins/homeserver";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
test.describe("Lazy Loading", () => {
|
||||
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||
|
||||
const charlies: Bot[] = [];
|
||||
|
||||
test.use({
|
||||
@@ -25,21 +29,28 @@ test.describe("Lazy Loading", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, user, bot }) => {
|
||||
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const displayName = `Charly #${i}`;
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
||||
charlies.push(bot);
|
||||
}
|
||||
await app.client.network.setupRoute();
|
||||
});
|
||||
|
||||
const name = "Lazy Loading Test";
|
||||
const alias = "#lltest:localhost";
|
||||
const charlyMsg1 = "hi bob!";
|
||||
const charlyMsg2 = "how's it going??";
|
||||
let roomId: string;
|
||||
|
||||
async function setupRoomWithBobAliceAndCharlies(page: Page, app: ElementAppPage, bob: Bot, charlies: Bot[]) {
|
||||
async function setupRoomWithBobAliceAndCharlies(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
user: Credentials,
|
||||
bob: Bot,
|
||||
charlies: Bot[],
|
||||
) {
|
||||
const alias = `#lltest:${user.homeServer}`;
|
||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
||||
roomId = await bob.createRoom({
|
||||
name,
|
||||
@@ -77,7 +88,7 @@ test.describe("Lazy Loading", () => {
|
||||
}
|
||||
|
||||
function getMemberInMemberlist(page: Page, name: string): Locator {
|
||||
return page.locator(".mx_MemberList .mx_EntityTile_name").filter({ hasText: name });
|
||||
return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
|
||||
}
|
||||
|
||||
async function checkMemberList(page: Page, charlies: Bot[]) {
|
||||
@@ -94,7 +105,13 @@ test.describe("Lazy Loading", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function joinCharliesWhileAliceIsOffline(page: Page, app: ElementAppPage, charlies: Bot[]) {
|
||||
async function joinCharliesWhileAliceIsOffline(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
user: Credentials,
|
||||
charlies: Bot[],
|
||||
) {
|
||||
const alias = `#lltest:${user.homeServer}`;
|
||||
await app.client.network.goOffline();
|
||||
for (const charly of charlies) {
|
||||
await charly.joinRoom(alias);
|
||||
@@ -106,19 +123,19 @@ test.describe("Lazy Loading", () => {
|
||||
await app.client.waitForNextSync();
|
||||
}
|
||||
|
||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot }) => {
|
||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot, user }) => {
|
||||
test.slow();
|
||||
const charly1to5 = charlies.slice(0, 5);
|
||||
const charly6to10 = charlies.slice(5);
|
||||
|
||||
// Set up room with alice, bob & charlies 1-5
|
||||
await setupRoomWithBobAliceAndCharlies(page, app, bot, charly1to5);
|
||||
await setupRoomWithBobAliceAndCharlies(page, app, user, bot, charly1to5);
|
||||
// Alice should see 2 messages from every charly with the correct display name
|
||||
await checkPaginatedDisplayNames(app, charly1to5);
|
||||
|
||||
await openMemberlist(app);
|
||||
await checkMemberList(page, charly1to5);
|
||||
await joinCharliesWhileAliceIsOffline(page, app, charly6to10);
|
||||
await joinCharliesWhileAliceIsOffline(page, app, user, charly6to10);
|
||||
await checkMemberList(page, charly6to10);
|
||||
|
||||
for (const charly of charlies) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 Suguru Hirahara
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,11 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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 { Locator, Page } from "@playwright/test";
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||
|
||||
test.use(consentHomeserver);
|
||||
test.use({
|
||||
displayName: "Bob",
|
||||
});
|
||||
|
||||
test.describe("Consent", () => {
|
||||
test.use({
|
||||
startHomeserverOpts: "consent",
|
||||
displayName: "Bob",
|
||||
});
|
||||
|
||||
test("should prompt the user to consent to terms when server deems it necessary", async ({
|
||||
context,
|
||||
page,
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page } from "playwright-core";
|
||||
import { type Page } from "playwright-core";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { doTokenRegistration } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { selectHomeserver } from "../utils";
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
// This test requires fixed credentials for the device signing keys below to work
|
||||
const username = "user1234";
|
||||
const password = "p4s5W0rD";
|
||||
|
||||
@@ -68,26 +69,55 @@ const DEVICE_SIGNING_KEYS_BODY = {
|
||||
},
|
||||
};
|
||||
|
||||
async function login(page: Page, homeserver: HomeserverInstance) {
|
||||
async function login(page: Page, homeserver: HomeserverInstance, credentials: Credentials) {
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
}
|
||||
|
||||
test.describe("Login", () => {
|
||||
test.describe("Password login", () => {
|
||||
test.use({ startHomeserverOpts: "consent" });
|
||||
// This test suite uses the same userId for all tests in the suite
|
||||
// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId,
|
||||
// so we restart the Synapse container to make it forget everything.
|
||||
test.use(consentHomeserver);
|
||||
test.use({
|
||||
config: {
|
||||
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||
// We point that to a guaranteed-invalid domain.
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
context: async ({ context, homeserver }, use) => {
|
||||
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||
await homeserver.restart();
|
||||
await use(context);
|
||||
},
|
||||
credentials: async ({ context, homeserver }, use) => {
|
||||
const displayName = "Dave";
|
||||
const credentials = await homeserver.registerUser(username, password, displayName);
|
||||
console.log(`Registered test user @user:localhost with displayname ${displayName}`);
|
||||
|
||||
let creds: Credentials;
|
||||
|
||||
test.beforeEach(async ({ homeserver }) => {
|
||||
creds = await homeserver.registerUser(username, password);
|
||||
await use({
|
||||
...credentials,
|
||||
displayName,
|
||||
});
|
||||
|
||||
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||
await homeserver.restart();
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("Login", () => {
|
||||
test.describe("Password login", () => {
|
||||
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||
|
||||
test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
|
||||
credentials,
|
||||
page,
|
||||
homeserver,
|
||||
checkA11y,
|
||||
@@ -101,7 +131,7 @@ test.describe("Login", () => {
|
||||
await page.getByRole("link", { name: "Sign in" }).click();
|
||||
|
||||
// first pick the homeserver, as otherwise the user picker won't be visible
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await page.getByRole("button", { name: "Edit" }).click();
|
||||
|
||||
@@ -114,23 +144,23 @@ test.describe("Login", () => {
|
||||
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
|
||||
|
||||
// switch back to the custom homeserver
|
||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
||||
await selectHomeserver(page, homeserver.baseUrl);
|
||||
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
|
||||
// cy.percySnapshot("Login");
|
||||
await checkA11y();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
||||
await page.getByPlaceholder("Password").fill(password);
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
});
|
||||
|
||||
test("Follows the original link after login", async ({ page, homeserver }) => {
|
||||
test("Follows the original link after login", async ({ page, homeserver, credentials }) => {
|
||||
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
||||
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
||||
@@ -141,18 +171,19 @@ test.describe("Login", () => {
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
credentials,
|
||||
}) => {
|
||||
const res = await request.post(
|
||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
||||
);
|
||||
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
|
||||
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||
data: DEVICE_SIGNING_KEYS_BODY,
|
||||
});
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
}
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
|
||||
@@ -170,10 +201,14 @@ test.describe("Login", () => {
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
credentials,
|
||||
}) => {
|
||||
const res = await request.post(
|
||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
||||
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||
data: DEVICE_SIGNING_KEYS_BODY,
|
||||
},
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
@@ -181,7 +216,7 @@ test.describe("Login", () => {
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||
|
||||
@@ -200,11 +235,15 @@ test.describe("Login", () => {
|
||||
page,
|
||||
homeserver,
|
||||
request,
|
||||
credentials,
|
||||
}) => {
|
||||
console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY);
|
||||
console.log(`uid ${credentials.userId} body`, DEVICE_SIGNING_KEYS_BODY);
|
||||
const res = await request.post(
|
||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
||||
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||
data: DEVICE_SIGNING_KEYS_BODY,
|
||||
},
|
||||
);
|
||||
if (res.status() / 100 !== 2) {
|
||||
console.log("Uploading dummy keys failed", await res.json());
|
||||
@@ -212,9 +251,9 @@ test.describe("Login", () => {
|
||||
expect(res.status() / 100).toEqual(2);
|
||||
|
||||
await page.goto("/");
|
||||
await login(page, homeserver);
|
||||
await login(page, homeserver, credentials);
|
||||
|
||||
const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||
await expect(h1).toBeVisible();
|
||||
|
||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||
@@ -223,32 +262,7 @@ test.describe("Login", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
|
||||
test.describe("SSO login", () => {
|
||||
test.skip(isDendrite, "does not yet support SSO");
|
||||
|
||||
test.use({
|
||||
startHomeserverOpts: ({ oAuthServer }, use) =>
|
||||
use({
|
||||
template: "default",
|
||||
oAuthServerPort: oAuthServer.port,
|
||||
}),
|
||||
});
|
||||
|
||||
test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => {
|
||||
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
|
||||
// your firewall settings: Synapse is unable to reach the OIDC server.
|
||||
//
|
||||
// If you are using ufw, try something like:
|
||||
// sudo ufw allow in on docker0
|
||||
//
|
||||
await doTokenRegistration(page, homeserver);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("logout", () => {
|
||||
test.use({ startHomeserverOpts: "consent" });
|
||||
|
||||
test("should go to login page on logout", async ({ page, user }) => {
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
|
||||
@@ -260,29 +274,4 @@ test.describe("Login", () => {
|
||||
await expect(page).toHaveURL(/\/#\/login$/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("logout with logout_redirect_url", () => {
|
||||
test.use({
|
||||
startHomeserverOpts: "consent",
|
||||
config: {
|
||||
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
|
||||
// We could use example.org, matrix.org, or something else, however this puts dependency of external
|
||||
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
|
||||
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
|
||||
// Using the decoder-ring is just as fine, and we can search for strategic names.
|
||||
logout_redirect_url: "/decoder-ring/",
|
||||
},
|
||||
});
|
||||
|
||||
test("should respect logout_redirect_url", async ({ page, user }) => {
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
|
||||
|
||||
// give a change for the outstanding requests queue to settle before logging out
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
|
||||
await expect(page).toHaveURL(/\/decoder-ring\/$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
29
playwright/e2e/login/login-sso.spec.ts
Normal file
29
playwright/e2e/login/login-sso.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test } from "../../element-web-test";
|
||||
import { doTokenRegistration } from "./utils";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts";
|
||||
|
||||
test.use(legacyOAuthHomeserver);
|
||||
|
||||
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
|
||||
test.describe("SSO login", () => {
|
||||
test.skip(isDendrite, "does not yet support SSO");
|
||||
|
||||
test("logs in with SSO and lands on the home screen", async ({ page, homeserver }, testInfo) => {
|
||||
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
|
||||
// your firewall settings: Synapse is unable to reach the OIDC server.
|
||||
//
|
||||
// If you are using ufw, try something like:
|
||||
// sudo ufw allow in on docker0
|
||||
//
|
||||
await doTokenRegistration(page, homeserver, testInfo);
|
||||
});
|
||||
});
|
||||
35
playwright/e2e/login/logout_redirect_url.spec.ts
Normal file
35
playwright/e2e/login/logout_redirect_url.spec.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||
|
||||
test.use(consentHomeserver);
|
||||
test.use({
|
||||
config: {
|
||||
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
|
||||
// We could use example.org, matrix.org, or something else, however this puts dependency of external
|
||||
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
|
||||
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
|
||||
// Using the decoder-ring is just as fine, and we can search for strategic names.
|
||||
logout_redirect_url: "/decoder-ring/",
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("logout with logout_redirect_url", () => {
|
||||
test("should respect logout_redirect_url", async ({ page, user }) => {
|
||||
await page.getByRole("button", { name: "User menu" }).click();
|
||||
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
|
||||
|
||||
// give a change for the outstanding requests queue to settle before logging out
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
|
||||
await expect(page).toHaveURL(/\/decoder-ring\/$/);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ test.describe("Overwrite login action", () => {
|
||||
// This seems terminally flakey: https://github.com/element-hq/element-web/issues/27363
|
||||
// I tried verious things to try & deflake it, to no avail: https://github.com/matrix-org/matrix-react-sdk/pull/12506
|
||||
test.skip("Try replace existing login with new one", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, homeserver, credentials);
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
const userMenu = await app.openUserMenu();
|
||||
await expect(userMenu.getByText(credentials.userId)).toBeVisible();
|
||||
@@ -24,7 +24,7 @@ test.describe("Overwrite login action", () => {
|
||||
expect(credentials.userId).not.toBe(bobRegister.userId);
|
||||
|
||||
const clientCredentials /* IMatrixClientCreds */ = {
|
||||
homeserverUrl: homeserver.config.baseUrl,
|
||||
homeserverUrl: homeserver.baseUrl,
|
||||
...bobRegister,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,121 +2,42 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { doTokenRegistration } from "./utils";
|
||||
import { Credentials } from "../../plugins/homeserver";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
import { interceptRequestsWithSoftLogout } from "./utils";
|
||||
|
||||
test.describe("Soft logout", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
startHomeserverOpts: ({ oAuthServer }, use) =>
|
||||
use({
|
||||
template: "default",
|
||||
oAuthServerPort: oAuthServer.port,
|
||||
}),
|
||||
});
|
||||
|
||||
test.describe("with password user", () => {
|
||||
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
|
||||
await interceptRequestsWithSoftLogout(page, user);
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
await page.getByPlaceholder("Password").fill(user.password);
|
||||
await page.getByPlaceholder("Password").press("Enter");
|
||||
|
||||
// back to the welcome page
|
||||
await expect(page).toHaveURL(/\/#\/home/);
|
||||
await expect(page.getByRole("heading", { name: `Welcome ${user.userId}`, exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({
|
||||
page,
|
||||
user,
|
||||
}) => {
|
||||
await interceptRequestsWithSoftLogout(page, user);
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("with SSO user", () => {
|
||||
test.skip(isDendrite, "does not yet support SSO");
|
||||
|
||||
test.use({
|
||||
user: async ({ page, homeserver }, use) => {
|
||||
const user = await doTokenRegistration(page, homeserver);
|
||||
|
||||
// Eventually, we should end up at the home screen.
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
|
||||
await use(user);
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
config: {
|
||||
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||
// We point that to a guaranteed-invalid domain.
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
});
|
||||
|
||||
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
|
||||
await interceptRequestsWithSoftLogout(page, user);
|
||||
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Continue with OAuth test" }).click();
|
||||
|
||||
// click the submit button
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
|
||||
// Synapse prompts us to grant permission to Element
|
||||
await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "Continue" }).click();
|
||||
|
||||
// back to the welcome page
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Intercept calls to /sync and have them fail with a soft-logout
|
||||
*
|
||||
* Any further requests to /sync with the same access token are blocked.
|
||||
*/
|
||||
async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise<void> {
|
||||
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
|
||||
const accessToken = await req.headerValue("Authorization");
|
||||
test.describe("Soft logout with password user", () => {
|
||||
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
|
||||
await interceptRequestsWithSoftLogout(page, user);
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
await page.getByPlaceholder("Password").fill(user.password);
|
||||
await page.getByPlaceholder("Password").press("Enter");
|
||||
|
||||
// now, if the access token on this request matches the expired one, block it
|
||||
if (accessToken === `Bearer ${user.accessToken}`) {
|
||||
console.log("Intercepting request with soft-logged-out access token");
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
json: {
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
error: "Soft logout",
|
||||
soft_logout: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, pass through as normal
|
||||
await route.continue();
|
||||
// back to the welcome page
|
||||
await expect(page).toHaveURL(/\/#\/home/);
|
||||
await expect(page.getByRole("heading", { name: "Now, let's help you get started", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
const promise = page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401);
|
||||
|
||||
// do something to make the active /sync return: create a new room
|
||||
await page.evaluate(() => {
|
||||
// don't wait for this to complete: it probably won't, because of the broken sync
|
||||
window.mxMatrixClientPeg.get().createRoom({});
|
||||
test("still shows the soft-logout page when the page is reloaded after a soft-logout", async ({ page, user }) => {
|
||||
await interceptRequestsWithSoftLogout(page, user);
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
await page.reload();
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
});
|
||||
|
||||
59
playwright/e2e/login/soft_logout_oauth.spec.ts
Normal file
59
playwright/e2e/login/soft_logout_oauth.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
import { doTokenRegistration, interceptRequestsWithSoftLogout } from "./utils";
|
||||
import { legacyOAuthHomeserver } from "../../plugins/homeserver/synapse/legacyOAuthHomeserver.ts";
|
||||
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
config: {
|
||||
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||
// We point that to a guaranteed-invalid domain.
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test.use(legacyOAuthHomeserver);
|
||||
test.describe("Soft logout with SSO user", () => {
|
||||
test.use({
|
||||
user: async ({ page, homeserver }, use, testInfo) => {
|
||||
const user = await doTokenRegistration(page, homeserver, testInfo);
|
||||
|
||||
// Eventually, we should end up at the home screen.
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
|
||||
await use(user);
|
||||
},
|
||||
});
|
||||
|
||||
test("shows the soft-logout page when a request fails, and allows a re-login", async ({ page, user }) => {
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
|
||||
await interceptRequestsWithSoftLogout(page, user);
|
||||
|
||||
await expect(page.getByText("You're signed out")).toBeVisible();
|
||||
await page.getByRole("button", { name: "Continue with OAuth test" }).click();
|
||||
|
||||
// click the submit button
|
||||
await page.getByRole("button", { name: "Submit" }).click();
|
||||
|
||||
// Synapse prompts us to grant permission to Element
|
||||
await expect(page.getByRole("heading", { name: "Continue to your account" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "Continue" }).click();
|
||||
|
||||
// back to the welcome page
|
||||
await expect(page).toHaveURL(/\/#\/home$/);
|
||||
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -2,25 +2,26 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { Page, expect } from "@playwright/test";
|
||||
import { type Page, expect, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
||||
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||
|
||||
/** Visit the login page, choose to log in with "OAuth test", register a new account, and redirect back to Element
|
||||
*/
|
||||
export async function doTokenRegistration(
|
||||
page: Page,
|
||||
homeserver: HomeserverInstance,
|
||||
testInfo: TestInfo,
|
||||
): Promise<Credentials & { displayName: string }> {
|
||||
await page.goto("/#/login");
|
||||
|
||||
await page.getByRole("button", { name: "Edit" }).click();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
// wait for the dialog to go away
|
||||
await expect(page.locator(".mx_ServerPickerDialog")).toHaveCount(0);
|
||||
|
||||
@@ -35,7 +36,7 @@ export async function doTokenRegistration(
|
||||
|
||||
// Synapse prompts us to pick a user ID
|
||||
await expect(page.getByRole("heading", { name: "Create your account" })).toBeVisible();
|
||||
await page.getByRole("textbox", { name: "Username (required)" }).fill("alice");
|
||||
await page.getByRole("textbox", { name: "Username (required)" }).fill(`alice_${testInfo.testId}`);
|
||||
|
||||
// wait for username validation to start, and complete
|
||||
await expect(page.locator("#field-username-output")).toHaveText("");
|
||||
@@ -56,5 +57,44 @@ export async function doTokenRegistration(
|
||||
homeServer: window.mxMatrixClientPeg.get().getHomeserverUrl(),
|
||||
password: null,
|
||||
displayName: "Alice",
|
||||
username: window.mxMatrixClientPeg.get().getUserIdLocalpart(),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept calls to /sync and have them fail with a soft-logout
|
||||
*
|
||||
* Any further requests to /sync with the same access token are blocked.
|
||||
*/
|
||||
export async function interceptRequestsWithSoftLogout(page: Page, user: Credentials): Promise<void> {
|
||||
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
|
||||
const accessToken = await req.headerValue("Authorization");
|
||||
|
||||
// now, if the access token on this request matches the expired one, block it
|
||||
if (accessToken === `Bearer ${user.accessToken}`) {
|
||||
console.log("Intercepting request with soft-logged-out access token");
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
json: {
|
||||
errcode: "M_UNKNOWN_TOKEN",
|
||||
error: "Soft logout",
|
||||
soft_logout: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, pass through as normal
|
||||
await route.continue();
|
||||
});
|
||||
|
||||
const promise = page.waitForResponse((resp) => resp.url().includes("/sync") && resp.status() === 401);
|
||||
|
||||
// do something to make the active /sync return: create a new room
|
||||
await page.evaluate(() => {
|
||||
// don't wait for this to complete: it probably won't, because of the broken sync
|
||||
void window.mxMatrixClientPeg.get().createRoom({});
|
||||
});
|
||||
|
||||
await promise;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||
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.
|
||||
*/
|
||||
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { Locator, Page } from "playwright-core";
|
||||
import { type Locator, type Page } from "playwright-core";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
@@ -58,6 +58,16 @@ async function editMessage(page: Page, message: Locator, newMsg: string): Promis
|
||||
await editComposer.press("Enter");
|
||||
}
|
||||
|
||||
const screenshotOptions = (page?: Page) => ({
|
||||
mask: page ? [page.locator(".mx_MessageTimestamp")] : undefined,
|
||||
// Hide the jump to bottom button in the timeline to avoid flakiness
|
||||
css: `
|
||||
.mx_JumpToBottomButton {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
test.describe("Message rendering", () => {
|
||||
[
|
||||
{ direction: "ltr", displayName: "Quentin" },
|
||||
@@ -79,9 +89,10 @@ test.describe("Message rendering", () => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "Hello, world!");
|
||||
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`basic-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -89,14 +100,17 @@ test.describe("Message rendering", () => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me lays an egg");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(`emote-ltr-${direction}displayname.png`, screenshotOptions());
|
||||
});
|
||||
|
||||
test("should render an LTR rich text emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me lays a *free range* egg");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rich-ltr-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`emote-rich-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render an edited LTR message", async ({ page, user, app, room }) => {
|
||||
@@ -106,9 +120,10 @@ test.describe("Message rendering", () => {
|
||||
|
||||
await editMessage(page, msgTile, "Hello, universe!");
|
||||
|
||||
await expect(msgTile).toMatchScreenshot(`edited-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`edited-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a reply of a LTR message", async ({ page, user, app, room }) => {
|
||||
@@ -122,32 +137,37 @@ test.describe("Message rendering", () => {
|
||||
]);
|
||||
|
||||
await replyMessage(page, msgTile, "response to multiline message");
|
||||
await expect(msgTile).toMatchScreenshot(`reply-message-ltr-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`reply-message-ltr-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a basic RTL text message", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "مرحبا بالعالم!");
|
||||
await expect(msgTile).toMatchScreenshot(`basic-message-rtl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`basic-message-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render an RTL emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me يضع بيضة");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rtl-${direction}displayname.png`, screenshotOptions());
|
||||
});
|
||||
|
||||
test("should render a richtext RTL emote", async ({ page, user, app, room }) => {
|
||||
await page.goto(`#/room/${room.roomId}`);
|
||||
|
||||
const msgTile = await sendMessage(page, "/me أضع بيضة *حرة النطاق*");
|
||||
await expect(msgTile).toMatchScreenshot(`emote-rich-rtl-${direction}displayname.png`);
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`emote-rich-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render an edited RTL message", async ({ page, user, app, room }) => {
|
||||
@@ -157,9 +177,10 @@ test.describe("Message rendering", () => {
|
||||
|
||||
await editMessage(page, msgTile, "مرحبا بالكون!");
|
||||
|
||||
await expect(msgTile).toMatchScreenshot(`edited-message-rtl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`edited-message-rtl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
|
||||
test("should render a reply of a RTL message", async ({ page, user, app, room }) => {
|
||||
@@ -173,9 +194,10 @@ test.describe("Message rendering", () => {
|
||||
]);
|
||||
|
||||
await replyMessage(page, msgTile, "مرحبا بالعالم!");
|
||||
await expect(msgTile).toMatchScreenshot(`reply-message-trl-${direction}displayname.png`, {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
await expect(msgTile).toMatchScreenshot(
|
||||
`reply-message-trl-${direction}displayname.png`,
|
||||
screenshotOptions(page),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
35
playwright/e2e/modules/loader.spec.ts
Normal file
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!");
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user