Compare commits
115 Commits
t3chguy/s3
...
hs/add-hid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7a185baea | ||
|
|
93009d4613 | ||
|
|
71257d97e7 | ||
|
|
60eeb8a7de | ||
|
|
ff1da50dd9 | ||
|
|
571a2e373d | ||
|
|
4af5d4ac80 | ||
|
|
d0b8564660 | ||
|
|
28ea91566a | ||
|
|
ef32747473 | ||
|
|
7696516e8b | ||
|
|
a858fed321 | ||
|
|
f3dbe81ef4 | ||
|
|
ceba762caf | ||
|
|
9fb52e984c | ||
|
|
c31f5521ec | ||
|
|
66d9d717c4 | ||
|
|
4e3daa5df5 | ||
|
|
f9a0bb2904 | ||
|
|
f4b03a1b06 | ||
|
|
be3778bef0 | ||
|
|
973d639d01 | ||
|
|
20d8abf7c2 | ||
|
|
46b1234a1d | ||
|
|
fda658182a | ||
|
|
9bfea92b66 | ||
|
|
962136d453 | ||
|
|
917d53a56f | ||
|
|
e44ca88a7e | ||
|
|
cb7d77de45 | ||
|
|
cd6737942f | ||
|
|
a058d85c21 | ||
|
|
bf6ae73d39 | ||
|
|
273cdf41e9 | ||
|
|
3ab3041c45 | ||
|
|
2052080d7d | ||
|
|
b9c0d63e3e | ||
|
|
cf7e52c6fc | ||
|
|
e87eb127ee | ||
|
|
83e421daf2 | ||
|
|
d6fb24dea7 | ||
|
|
a518c8d662 | ||
|
|
c759e516bd | ||
|
|
c8b55c3dfe | ||
|
|
7197093744 | ||
|
|
cc95d154fb | ||
|
|
4e696d2dc6 | ||
|
|
610b14adc1 | ||
|
|
324dd5a858 | ||
|
|
bc4bc6c25e | ||
|
|
3f3fba99eb | ||
|
|
4e34adb854 | ||
|
|
72c2a3eb07 | ||
|
|
4d290461c4 | ||
|
|
0cc06450d7 | ||
|
|
9376d71831 | ||
|
|
26a17f9314 | ||
|
|
6d5442a87b | ||
|
|
fd91e78152 | ||
|
|
af476905b6 | ||
|
|
da87bbe854 | ||
|
|
47976447b5 | ||
|
|
53065f9437 | ||
|
|
179b368809 | ||
|
|
82957507d0 | ||
|
|
27c1b38dab | ||
|
|
7ff1fd259d | ||
|
|
8d891cde53 | ||
|
|
90cc44b340 | ||
|
|
b6c872142b | ||
|
|
3762d40620 | ||
|
|
42192cbe06 | ||
|
|
aa996010b4 | ||
|
|
e9a3625bd6 | ||
|
|
343dd06929 | ||
|
|
77b6c3b526 | ||
|
|
b721505211 | ||
|
|
56c7fc1948 | ||
|
|
9d8efacede | ||
|
|
1770b94ed3 | ||
|
|
dfdac8ef63 | ||
|
|
f1ebd85af1 | ||
|
|
4776a9971d | ||
|
|
20ac69f379 | ||
|
|
8c42b0bed8 | ||
|
|
fbc6f12408 | ||
|
|
b82c8554e3 | ||
|
|
3d705b1895 | ||
|
|
81c12db5ee | ||
|
|
e1d76e77a5 | ||
|
|
54e015706c | ||
|
|
cef25c2cab | ||
|
|
59c26fc3ad | ||
|
|
31af8b07dd | ||
|
|
c0d14daa17 | ||
|
|
ed35a7cba4 | ||
|
|
21e9d93e69 | ||
|
|
ffa8971195 | ||
|
|
072ee0cf36 | ||
|
|
4b02520453 | ||
|
|
bf48100d31 | ||
|
|
2da21248bb | ||
|
|
3c57323595 | ||
|
|
4c72f0c0b2 | ||
|
|
dfd08a8c01 | ||
|
|
7db909a47d | ||
|
|
1ad1387e05 | ||
|
|
c6b3bf962a | ||
|
|
e749b017c9 | ||
|
|
c00262f0c5 | ||
|
|
7a513a2dc2 | ||
|
|
808412c6be | ||
|
|
45497905be | ||
|
|
0997e0a747 | ||
|
|
6173c1224b |
@@ -8,3 +8,6 @@ src/component-index.js
|
||||
# Auto-generated file
|
||||
src/modules.ts
|
||||
src/modules.js
|
||||
# Test result files
|
||||
/playwright/test-results/
|
||||
/playwright/html-report/
|
||||
|
||||
10
.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
|
||||
@@ -111,8 +117,8 @@ jobs:
|
||||
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
||||
- name: Deploy to R2
|
||||
run: |
|
||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
|
||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||
|
||||
19
.github/workflows/docker.yaml
vendored
@@ -25,14 +25,14 @@ jobs:
|
||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3
|
||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3
|
||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||
with:
|
||||
install: true
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Build and load
|
||||
id: test-build
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
@@ -61,6 +61,7 @@ jobs:
|
||||
- name: Test the image
|
||||
env:
|
||||
IMAGEID: ${{ steps.test-build.outputs.imageid }}
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
set -x
|
||||
|
||||
@@ -76,7 +77,7 @@ jobs:
|
||||
--rm \
|
||||
-e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \
|
||||
-dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \
|
||||
-v $(pwd)/modules:/tmp/element-web-modules \
|
||||
-v $(pwd)/modules:/modules \
|
||||
"$IMAGEID" \
|
||||
)
|
||||
|
||||
@@ -86,14 +87,16 @@ jobs:
|
||||
test "$MODULE_0" = "/${MODULE_PATH}"
|
||||
|
||||
# Check healthcheck
|
||||
test "$(docker inspect -f {{.State.Running}} $CONTAINER_ID)" == "true"
|
||||
until test "$(docker inspect -f {{.State.Health.Status}} $CONTAINER_ID)" == "healthy"; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Clean up
|
||||
docker stop "$CONTAINER_ID"
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
images: |
|
||||
@@ -107,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Build and push
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
|
||||
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
context: .
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/playwright-image-updates
|
||||
|
||||
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@7ca807c2ba3401be532d29a876b93262108099fb
|
||||
uses: guibranco/github-status-action-v2@5ef6e175c333bc629f3718b083c8a2ff6e0bbfbc
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
|
||||
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@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/jitsi-update
|
||||
|
||||
32
CHANGELOG.md
@@ -1,3 +1,35 @@
|
||||
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Room List Store: Filter rooms by active space ([#29399](https://github.com/element-hq/element-web/pull/29399)). Contributed by @MidhunSureshR.
|
||||
* Room List - Update the room list store on actions from the dispatcher ([#29397](https://github.com/element-hq/element-web/pull/29397)). Contributed by @MidhunSureshR.
|
||||
* Room List - Implement a minimal view model ([#29357](https://github.com/element-hq/element-web/pull/29357)). Contributed by @MidhunSureshR.
|
||||
* New room list: add space menu in room header ([#29352](https://github.com/element-hq/element-web/pull/29352)). Contributed by @florianduros.
|
||||
* Room List - Store sorted rooms in skip list ([#29345](https://github.com/element-hq/element-web/pull/29345)). Contributed by @MidhunSureshR.
|
||||
* New room list: add dial to search section ([#29359](https://github.com/element-hq/element-web/pull/29359)). Contributed by @florianduros.
|
||||
* New room list: add compose menu for spaces in header ([#29347](https://github.com/element-hq/element-web/pull/29347)). Contributed by @florianduros.
|
||||
* Use EditInPlace control for Identity Server picker to improve a11y ([#29280](https://github.com/element-hq/element-web/pull/29280)). Contributed by @Half-Shot.
|
||||
* First step to add header to new room list ([#29320](https://github.com/element-hq/element-web/pull/29320)). Contributed by @florianduros.
|
||||
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page ([#29312](https://github.com/element-hq/element-web/pull/29312)). Contributed by @t3chguy.
|
||||
* Honour the backup disable flag from Element X ([#29290](https://github.com/element-hq/element-web/pull/29290)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Fix edited code block width ([#29394](https://github.com/element-hq/element-web/pull/29394)). Contributed by @florianduros.
|
||||
* new room list: keep space name in one line in header ([#29369](https://github.com/element-hq/element-web/pull/29369)). Contributed by @florianduros.
|
||||
* Dismiss "Key storage out of sync" toast when secrets received ([#29348](https://github.com/element-hq/element-web/pull/29348)). Contributed by @richvdh.
|
||||
* Minor CSS fixes for the new room list ([#29334](https://github.com/element-hq/element-web/pull/29334)). Contributed by @florianduros.
|
||||
* Add padding to room header icon ([#29271](https://github.com/element-hq/element-web/pull/29271)). Contributed by @langleyd.
|
||||
|
||||
|
||||
Changes in [1.11.94](https://github.com/element-hq/element-web/releases/tag/v1.11.94) (2025-02-27)
|
||||
==================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] fix: /tmp/element-web-config may already exist preventing the container from booting up ([#29377](https://github.com/element-hq/element-web/pull/29377)). Contributed by @RiotRobot.
|
||||
|
||||
|
||||
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
|
||||
==================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.7-labs
|
||||
# syntax=docker.io/docker/dockerfile:1.14-labs
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
|
||||
@@ -47,4 +47,4 @@ USER nginx
|
||||
# HTTP listen port
|
||||
ENV ELEMENT_WEB_PORT=80
|
||||
|
||||
HEALTHCHECK --start-period=5s CMD wget --retry-connrefused --tries=5 -q --wait=3 --spider http://localhost:$ELEMENT_WEB_PORT/config.json
|
||||
HEALTHCHECK --start-period=5s CMD wget -q --spider http://localhost:$ELEMENT_WEB_PORT/config.json
|
||||
|
||||
@@ -31,5 +31,7 @@ module.exports = {
|
||||
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-transform-runtime",
|
||||
["@babel/plugin-proposal-decorators", { version: "2023-11" }], // only needed by the js-sdk
|
||||
"@babel/plugin-transform-class-static-block", // only needed by the js-sdk for decorators
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Loads modules from `/tmp/element-web-modules` into config.json's `modules` field
|
||||
# Loads modules from `/modules` into config.json's `modules` field
|
||||
|
||||
set -e
|
||||
|
||||
@@ -11,19 +11,19 @@ entrypoint_log() {
|
||||
}
|
||||
|
||||
# Copy these config files as a base
|
||||
mkdir /tmp/element-web-config
|
||||
mkdir -p /tmp/element-web-config
|
||||
cp /app/config*.json /tmp/element-web-config/
|
||||
|
||||
# If there are modules to be loaded
|
||||
if [ -d "/tmp/element-web-modules" ]; then
|
||||
cd /tmp/element-web-modules
|
||||
if [ -d "/modules" ]; then
|
||||
cd /modules
|
||||
|
||||
for MODULE in *
|
||||
do
|
||||
# If the module has a package.json, use its main field as the entrypoint
|
||||
ENTRYPOINT="index.js"
|
||||
if [ -f "/tmp/element-web-modules/$MODULE/package.json" ]; then
|
||||
ENTRYPOINT=$(jq -r '.main' "/tmp/element-web-modules/$MODULE/package.json")
|
||||
if [ -f "/modules/$MODULE/package.json" ]; then
|
||||
ENTRYPOINT=$(jq -r '.main' "/modules/$MODULE/package.json")
|
||||
fi
|
||||
|
||||
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
|
||||
|
||||
@@ -22,7 +22,7 @@ server {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
location /modules {
|
||||
alias /tmp/element-web-modules;
|
||||
alias /modules;
|
||||
}
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
|
||||
@@ -67,7 +67,7 @@ image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
||||
|
||||
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
|
||||
by being made available (e.g. via bind mount) in a directory within `/tmp/element-web-modules/`.
|
||||
by being made available (e.g. via bind mount) in a directory within `/modules/`.
|
||||
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
|
||||
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
|
||||
|
||||
@@ -75,7 +75,7 @@ If you wish to use docker in read-only mode,
|
||||
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
|
||||
but additionally include the following directories:
|
||||
|
||||
- /tmp/element-web-config/
|
||||
- /tmp/
|
||||
- /etc/nginx/conf.d/
|
||||
|
||||
The behaviour of the docker image can be customised via the following
|
||||
|
||||
1
knip.ts
@@ -19,6 +19,7 @@ export default {
|
||||
ignore: [
|
||||
// Keep for now
|
||||
"src/hooks/useLocalStorageState.ts",
|
||||
"src/hooks/useTimeout.ts",
|
||||
"src/components/views/elements/InfoTooltip.tsx",
|
||||
"src/components/views/elements/StyledCheckbox.tsx",
|
||||
],
|
||||
|
||||
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.93",
|
||||
"version": "1.11.95",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -62,19 +62,19 @@
|
||||
"test": "jest",
|
||||
"test:playwright": "playwright test",
|
||||
"test:playwright:open": "yarn test:playwright --ui",
|
||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||
"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",
|
||||
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
||||
"coverage": "yarn test --coverage",
|
||||
"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": {
|
||||
"@playwright/test": "1.50.1",
|
||||
"@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.30001699",
|
||||
"caniuse-lite": "1.0.30001701",
|
||||
"testcontainers": "10.20.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
@@ -84,7 +84,7 @@
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
"@matrix-org/analytics-events": "^0.29.0",
|
||||
"@matrix-org/analytics-events": "^0.29.2",
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
@@ -92,8 +92,8 @@
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||
"@vector-im/compound-web": "^7.6.4",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.0",
|
||||
"@vector-im/compound-web": "^7.7.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.2",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -138,7 +138,7 @@
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.157.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.10.3",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
@@ -158,13 +158,14 @@
|
||||
"devDependencies": {
|
||||
"@action-validator/cli": "^0.6.0",
|
||||
"@action-validator/core": "^0.6.0",
|
||||
"@axe-core/playwright": "^4.8.1",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-decorators": "^7.25.9",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
||||
"@babel/plugin-transform-class-static-block": "^7.26.0",
|
||||
"@babel/plugin-transform-logical-assignment-operators": "^7.20.7",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||
@@ -176,13 +177,13 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
"@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",
|
||||
@@ -218,12 +219,12 @@
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"babel-jest": "^29.0.0",
|
||||
"babel-loader": "^9.0.0",
|
||||
"babel-loader": "^10.0.0",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"blob-polyfill": "^9.0.0",
|
||||
"chokidar": "^4.0.0",
|
||||
"concurrently": "^9.0.0",
|
||||
"copy-webpack-plugin": "^12.0.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"core-js": "^3.38.1",
|
||||
"cronstrue": "^2.41.0",
|
||||
"css-loader": "^7.0.0",
|
||||
@@ -257,13 +258,12 @@
|
||||
"jsqr": "^1.4.0",
|
||||
"knip": "^5.36.2",
|
||||
"lint-staged": "^15.0.2",
|
||||
"mailpit-api": "^1.0.5",
|
||||
"matrix-web-i18n": "^3.2.1",
|
||||
"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",
|
||||
"playwright-core": "^1.51.0",
|
||||
"postcss": "8.4.46",
|
||||
"postcss-easings": "^4.0.0",
|
||||
"postcss-hexrgba": "2.1.0",
|
||||
@@ -274,21 +274,20 @@
|
||||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.5.1",
|
||||
"prettier": "3.5.2",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"testcontainers": "^10.16.0",
|
||||
"testcontainers": "^10.20.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.7.3",
|
||||
"typescript": "5.8.2",
|
||||
"util": "^0.12.5",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.89.0",
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
import { Options } from "./playwright/services";
|
||||
import { type WorkerOptions } from "./playwright/services";
|
||||
|
||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||
|
||||
@@ -21,7 +21,7 @@ const chromeProject = {
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig<Options>({
|
||||
export default defineConfig<WorkerOptions>({
|
||||
projects: [
|
||||
{
|
||||
name: "Chrome",
|
||||
@@ -83,6 +83,7 @@ export default defineConfig<Options>({
|
||||
url: `${baseURL}/config.json`,
|
||||
reuseExistingServer: true,
|
||||
timeout: (process.env.CI ? 30 : 120) * 1000,
|
||||
stdout: "pipe",
|
||||
},
|
||||
testDir: "playwright/e2e",
|
||||
outputDir: "playwright/test-results",
|
||||
|
||||
12
playwright/@types/playwright-core.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
declare module "playwright-core/lib/utils" {
|
||||
// This type is not public in playwright-core utils
|
||||
export function sanitizeForFilePath(filePath: string): string;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
FROM mcr.microsoft.com/playwright:v1.50.1-noble
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
# fonts-dejavu is needed for the same RTL rendering as on CI
|
||||
RUN apt-get update && apt-get -y install docker.io fonts-dejavu
|
||||
|
||||
COPY docker-entrypoint.sh /opt/docker-entrypoint.sh
|
||||
ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"]
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
npx playwright test --update-snapshots --reporter line $@
|
||||
@@ -28,7 +28,7 @@ const checkDMRoom = async (page: Page) => {
|
||||
};
|
||||
|
||||
const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
||||
await expect(
|
||||
|
||||
@@ -22,18 +22,6 @@ test.use({
|
||||
msc3814_enabled: true,
|
||||
},
|
||||
},
|
||||
config: async ({ config, context }, use) => {
|
||||
const wellKnown = {
|
||||
...config.default_server_config,
|
||||
"org.matrix.msc3814": true,
|
||||
};
|
||||
|
||||
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
|
||||
await route.fulfill({ json: wellKnown });
|
||||
});
|
||||
|
||||
await use(config);
|
||||
},
|
||||
});
|
||||
|
||||
test.describe("Dehydration", () => {
|
||||
|
||||
@@ -5,11 +5,11 @@ 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 APIRequestContext } from "@playwright/test";
|
||||
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||
import { ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
|
||||
|
||||
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
|
||||
|
||||
@@ -267,7 +267,6 @@ test.describe("Editing", () => {
|
||||
app,
|
||||
room,
|
||||
axe,
|
||||
checkA11y,
|
||||
}) => {
|
||||
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
||||
|
||||
@@ -282,7 +281,7 @@ test.describe("Editing", () => {
|
||||
const line = tile.locator(".mx_EventTile_line");
|
||||
await line.hover();
|
||||
await line.getByRole("button", { name: "Edit" }).click();
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
const editComposer = page.getByRole("textbox", { name: "Edit message" });
|
||||
await editComposer.pressSequentially("Foo");
|
||||
await editComposer.press("Backspace");
|
||||
@@ -290,7 +289,7 @@ test.describe("Editing", () => {
|
||||
await editComposer.press("Backspace");
|
||||
await editComposer.press("Enter");
|
||||
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
}
|
||||
await expect(
|
||||
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
|
||||
@@ -305,7 +304,6 @@ test.describe("Editing", () => {
|
||||
user,
|
||||
app,
|
||||
axe,
|
||||
checkA11y,
|
||||
bot: bob,
|
||||
}) => {
|
||||
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
||||
|
||||
@@ -77,7 +77,7 @@ test.describe("Invite dialog", function () {
|
||||
"should support inviting a user to Direct Messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
||||
|
||||
const other = page.locator(".mx_InviteDialog_other");
|
||||
// Assert that the header is rendered
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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 { expect, test } from "../../../element-web-test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
test.describe("Room list filters and sort", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
botCreateOpts: {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: true,
|
||||
},
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
function getPrimaryFilters(page: Page) {
|
||||
return page.getByRole("listbox", { name: "Room list filters" });
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
|
||||
await app.client.createRoom({ name: "empty room" });
|
||||
|
||||
const unReadDmId = await bot.createRoom({
|
||||
name: "unread dm",
|
||||
invite: [user.userId],
|
||||
is_direct: true,
|
||||
});
|
||||
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||
|
||||
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
|
||||
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
|
||||
await bot.joinRoom(unReadRoomId);
|
||||
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
|
||||
|
||||
const favouriteId = await app.client.createRoom({ name: "favourite room" });
|
||||
await app.client.evaluate(async (client, favouriteId) => {
|
||||
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||
}, favouriteId);
|
||||
});
|
||||
|
||||
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomList = getRoomList(page);
|
||||
const primaryFilters = getPrimaryFilters(page);
|
||||
|
||||
const allFilters = await primaryFilters.locator("option").all();
|
||||
for (const filter of allFilters) {
|
||||
expect(await filter.getAttribute("aria-selected")).toBe("false");
|
||||
}
|
||||
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||
// only one room should be visible
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||
|
||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||
expect(await roomList.locator("role=gridcell").count()).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import { type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../../element-web-test";
|
||||
|
||||
test.describe("Search section of the room list", () => {
|
||||
test.describe("Room list panel", () => {
|
||||
test.use({
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
@@ -19,16 +19,23 @@ test.describe("Search section of the room list", () => {
|
||||
* @param page
|
||||
*/
|
||||
function getRoomListView(page: Page) {
|
||||
return page.getByTestId("room-list-view");
|
||||
return page.getByTestId("room-list-panel");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
|
||||
// Populate the room list
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomListView(page);
|
||||
await expect(roomListView).toMatchScreenshot("room-list-view.png");
|
||||
// Wait for the last room to be visible
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||
});
|
||||
});
|
||||
80
playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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("Room list", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the room list
|
||||
* @param page
|
||||
*/
|
||||
function getRoomList(page: Page) {
|
||||
return page.getByTestId("room-list");
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, app, user }) => {
|
||||
// The notification toast is displayed above the search section
|
||||
await app.closeNotificationToast();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
await app.client.createRoom({ name: `room${i}` });
|
||||
}
|
||||
});
|
||||
|
||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||
|
||||
await roomListView.hover();
|
||||
// Scroll to the end of the room list
|
||||
await page.mouse.wheel(0, 1000);
|
||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||
});
|
||||
|
||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||
const roomListView = getRoomList(page);
|
||||
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||
await roomItem.hover();
|
||||
|
||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
|
||||
await roomItemMenu.click();
|
||||
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
|
||||
|
||||
// It should make the room favourited
|
||||
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
|
||||
|
||||
// Check that the room is favourited
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
|
||||
// It should show the invite dialog
|
||||
await page.getByRole("menuitem", { name: "invite" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
// It should leave the room
|
||||
await roomItem.hover();
|
||||
await roomItemMenu.click();
|
||||
await page.getByRole("menuitem", { name: "leave room" }).click();
|
||||
await expect(roomItem).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "playwright-core";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { expect, test } from "../../element-web-test";
|
||||
import { selectHomeserver } from "../utils";
|
||||
@@ -120,7 +120,7 @@ test.describe("Login", () => {
|
||||
credentials,
|
||||
page,
|
||||
homeserver,
|
||||
checkA11y,
|
||||
axe,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
@@ -149,7 +149,7 @@ test.describe("Login", () => {
|
||||
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 expect(axe).toHaveNoViolations();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||
|
||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
/* See readme.md for tips on writing these tests. */
|
||||
|
||||
import { type Locator, type Page } from "playwright-core";
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MailpitClient } from "mailpit-api";
|
||||
import { type MailpitClient } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
import { type Page } from "@playwright/test";
|
||||
|
||||
import { expect } from "../../element-web-test";
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
|
||||
test(
|
||||
"registers an account and lands on the home page",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, mailpitClient, request, checkA11y }) => {
|
||||
async ({ page, mailpitClient, request, axe }) => {
|
||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
||||
@@ -47,7 +47,7 @@ test.describe("Email Registration", async () => {
|
||||
|
||||
await expect(page.getByText("Check your email to continue")).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions);
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
|
||||
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ test.describe("Registration", () => {
|
||||
test(
|
||||
"registers an account and lands on the home screen",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ homeserver, page, checkA11y, crypto }) => {
|
||||
async ({ homeserver, page, axe, crypto }) => {
|
||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
|
||||
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
|
||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
|
||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
@@ -52,7 +52,7 @@ test.describe("Registration", () => {
|
||||
includeDialogBackground: true,
|
||||
};
|
||||
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
|
||||
await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
|
||||
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
|
||||
@@ -62,12 +62,12 @@ test.describe("Registration", () => {
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
|
||||
|
||||
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
|
||||
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
|
||||
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
|
||||
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link
|
||||
|
||||
@@ -67,6 +67,15 @@ test.describe("RightPanel", () => {
|
||||
},
|
||||
);
|
||||
|
||||
test("should have padding under leave room", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
const leaveButton = await page.getByRole("menuitem", { name: "Leave Room" });
|
||||
await leaveButton.scrollIntoViewIfNeeded();
|
||||
|
||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png");
|
||||
});
|
||||
|
||||
test("should handle clicking add widgets", async ({ page, app }) => {
|
||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
||||
|
||||
|
||||
@@ -17,9 +17,7 @@ import {
|
||||
} from "../../crypto/utils";
|
||||
|
||||
test.describe("Encryption tab", () => {
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
test.use({ displayName: "Alice" });
|
||||
|
||||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
let expectedBackupVersion: string;
|
||||
@@ -111,4 +109,36 @@ test.describe("Encryption tab", () => {
|
||||
// The user is prompted to reset their identity
|
||||
await expect(dialog.getByText("Forgot your recovery key? You’ll need to reset your identity.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||
await verifySession(app, recoveryKey.encodedPrivateKey);
|
||||
await util.openEncryptionTab();
|
||||
|
||||
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
|
||||
|
||||
const deleteRequestPromises = [
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
|
||||
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
|
||||
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
|
||||
];
|
||||
|
||||
await page.getByRole("button", { name: "Delete key storage" }).click();
|
||||
|
||||
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
|
||||
|
||||
for (const prom of deleteRequestPromises) {
|
||||
const request = await prom;
|
||||
expect(request.method()).toBe("PUT");
|
||||
expect(request.postData()).toBe(JSON.stringify({}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
18
playwright/e2e/settings/quick-settings-menu.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
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("Quick settings menu", () => {
|
||||
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||
await page.getByRole("button", { name: "Quick settings" }).click();
|
||||
// Assert that the top heading is renderedc
|
||||
const settings = page.getByTestId("quick-settings-menu");
|
||||
await expect(settings).toBeVisible();
|
||||
await expect(settings).toMatchScreenshot("quick-settings.png");
|
||||
});
|
||||
});
|
||||
@@ -91,7 +91,7 @@ test.describe("Security user settings tab", () => {
|
||||
await expect(tab.getByText(`Identity server (identity.example.org)`, { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should enable show integrations as enabled", async ({ app, page, user }) => {
|
||||
test("should show integrations as enabled", async ({ app, page, user }) => {
|
||||
const tab = await app.settings.openUserSettings("Security");
|
||||
|
||||
const setIntegrationManager = tab.locator(".mx_SetIntegrationManager");
|
||||
@@ -102,7 +102,9 @@ test.describe("Security user settings tab", () => {
|
||||
}),
|
||||
).toBeVisible();
|
||||
// Make sure integration manager's toggle switch is enabled
|
||||
await expect(setIntegrationManager.locator(".mx_ToggleSwitch_enabled")).toBeVisible();
|
||||
const toggleswitch = setIntegrationManager.getByLabel("Enable the integration manager");
|
||||
await expect(toggleswitch).toBeVisible();
|
||||
await expect(toggleswitch).toBeChecked();
|
||||
await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText(
|
||||
"Manage integrations(scalar.vector.im)",
|
||||
);
|
||||
|
||||
@@ -227,7 +227,7 @@ test.describe("Spaces", () => {
|
||||
test(
|
||||
"should render subspaces in the space panel only when expanded",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, axe, checkA11y }) => {
|
||||
async ({ page, app, user, axe }) => {
|
||||
axe.disableRules([
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
"nested-interactive",
|
||||
@@ -249,7 +249,7 @@ test.describe("Spaces", () => {
|
||||
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
|
||||
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
|
||||
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png");
|
||||
|
||||
// This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
|
||||
@@ -261,7 +261,7 @@ test.describe("Spaces", () => {
|
||||
await expect(item).toBeVisible();
|
||||
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
|
||||
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024, 2025 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 OR LicenseRef-Element-Commercial
|
||||
@@ -277,7 +277,7 @@ test.describe("Timeline", () => {
|
||||
test(
|
||||
"should add inline start margin to an event line on IRC layout",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, axe, checkA11y }) => {
|
||||
async ({ page, app, room, axe }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
@@ -318,7 +318,7 @@ test.describe("Timeline", () => {
|
||||
`,
|
||||
},
|
||||
);
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -743,68 +743,64 @@ test.describe("Timeline", () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test(
|
||||
"should render url previews",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, axe, checkA11y, context }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
test("should render url previews", { tag: "@screenshot" }, async ({ page, app, room, axe, context }) => {
|
||||
axe.disableRules("color-contrast");
|
||||
|
||||
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
|
||||
// the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it
|
||||
// post-worker, but we can't waitForResponse on that, so the page context is still used there. Because
|
||||
// the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until
|
||||
// the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
|
||||
await context.route(
|
||||
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
path: "playwright/sample-files/riot.png",
|
||||
});
|
||||
},
|
||||
);
|
||||
await page.route(
|
||||
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
"og:title": "Element Call",
|
||||
"og:description": null,
|
||||
"og:image:width": 48,
|
||||
"og:image:height": 48,
|
||||
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
|
||||
"og:image:type": "image/png",
|
||||
"matrix:image:size": 2121,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
|
||||
// the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it
|
||||
// post-worker, but we can't waitForResponse on that, so the page context is still used there. Because
|
||||
// the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until
|
||||
// the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
|
||||
await context.route(
|
||||
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
path: "playwright/sample-files/riot.png",
|
||||
});
|
||||
},
|
||||
);
|
||||
await page.route(
|
||||
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
"og:title": "Element Call",
|
||||
"og:description": null,
|
||||
"og:image:width": 48,
|
||||
"og:image:height": 48,
|
||||
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
|
||||
"og:image:type": "image/png",
|
||||
"matrix:image:size": 2121,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const requestPromises: Promise<any>[] = [
|
||||
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
|
||||
// see context.route above for why we listen for the unauthenticated endpoint
|
||||
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
|
||||
];
|
||||
const requestPromises: Promise<any>[] = [
|
||||
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
|
||||
// see context.route above for why we listen for the unauthenticated endpoint
|
||||
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
|
||||
];
|
||||
|
||||
await app.client.sendMessage(room.roomId, "https://call.element.io/");
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.client.sendMessage(room.roomId, "https://call.element.io/");
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
|
||||
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
|
||||
await Promise.all(requestPromises);
|
||||
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
|
||||
await Promise.all(requestPromises);
|
||||
|
||||
await checkA11y();
|
||||
await expect(axe).toHaveNoViolations();
|
||||
|
||||
await app.timeline.scrollToBottom();
|
||||
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
await app.timeline.scrollToBottom();
|
||||
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
|
||||
// Exclude timestamp and read marker from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
css: `
|
||||
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("on search results panel", () => {
|
||||
test(
|
||||
@@ -875,6 +871,53 @@ test.describe("Timeline", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("should render a code block", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||
await page.goto(`/#/room/${room.roomId}`);
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
// Wait until configuration is finished
|
||||
await expect(
|
||||
page
|
||||
.locator(".mx_GenericEventListSummary_summary")
|
||||
.getByText(`${OLD_NAME} created and configured the room.`),
|
||||
).toBeVisible();
|
||||
|
||||
// Send a code block
|
||||
const composer = app.getComposerField();
|
||||
await composer.fill("```\nconsole.log('Hello, world!');\n```");
|
||||
await composer.press("Enter");
|
||||
|
||||
const tile = page.locator(".mx_EventTile");
|
||||
await expect(tile).toBeVisible();
|
||||
await expect(tile).toMatchScreenshot("code-block.png", { mask: [page.locator(".mx_MessageTimestamp")] });
|
||||
|
||||
// Edit a code block and assert the edited code block has been correctly rendered
|
||||
await tile.hover();
|
||||
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }).click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Edit message" })
|
||||
.fill("```\nconsole.log('Edited: Hello, world!');\n```");
|
||||
await page.getByRole("textbox", { name: "Edit message" }).press("Enter");
|
||||
|
||||
const newTile = page.locator(".mx_EventTile");
|
||||
await expect(newTile).toMatchScreenshot("edited-code-block.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
});
|
||||
|
||||
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await sendImage(app.client, room.roomId, NEW_AVATAR);
|
||||
await app.timeline.scrollToBottom();
|
||||
const imgTile = page.locator(".mx_MImageBody").first();
|
||||
await expect(imgTile).toBeVisible();
|
||||
await imgTile.hover();
|
||||
await page.getByRole("button", { name: "Hide" }).click();
|
||||
|
||||
// Check that the image is now hidden.
|
||||
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
|
||||
@@ -7,18 +7,18 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
expect as baseExpect,
|
||||
type Locator,
|
||||
type Page,
|
||||
type ExpectMatcherState,
|
||||
type ElementHandle,
|
||||
type MatcherReturnType,
|
||||
type Page,
|
||||
type Locator,
|
||||
type PlaywrightTestArgs,
|
||||
type Fixtures as _Fixtures,
|
||||
} from "@playwright/test";
|
||||
import { sanitizeForFilePath } from "playwright-core/lib/utils";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
import _ from "lodash";
|
||||
import { extname } from "node:path";
|
||||
import {
|
||||
type TestFixtures as BaseTestFixtures,
|
||||
expect as baseExpect,
|
||||
type ToMatchScreenshotOptions,
|
||||
} from "@element-hq/element-web-playwright-common";
|
||||
|
||||
import type { IConfigOptions } from "../src/IConfigOptions";
|
||||
import { type Credentials } from "./plugins/homeserver";
|
||||
@@ -27,71 +27,22 @@ import { Crypto } from "./pages/crypto";
|
||||
import { Toasts } from "./pages/toasts";
|
||||
import { Bot, type CreateBotOpts } from "./pages/bot";
|
||||
import { Webserver } from "./plugins/webserver";
|
||||
import { type Options, type Services, test as base } from "./services.ts";
|
||||
import { type WorkerOptions, type Services, test as base } from "./services";
|
||||
|
||||
// Enable experimental service worker support
|
||||
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable
|
||||
process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1";
|
||||
|
||||
// This is deliberately quite a minimal config.json, so that we can test that the default settings actually work.
|
||||
const CONFIG_JSON: Partial<IConfigOptions> = {
|
||||
// The default language is set here for test consistency
|
||||
setting_defaults: {
|
||||
language: "en-GB",
|
||||
},
|
||||
|
||||
// the location tests want a map style url.
|
||||
map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
|
||||
|
||||
features: {
|
||||
// We don't want to go through the feature announcement during the e2e test
|
||||
feature_release_announcement: false,
|
||||
},
|
||||
};
|
||||
declare module "@element-hq/element-web-playwright-common" {
|
||||
// Improve the type for the config fixture based on the real type
|
||||
export interface Config extends Omit<IConfigOptions, "default_server_config"> {}
|
||||
}
|
||||
|
||||
export interface CredentialsWithDisplayName extends Credentials {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface TestFixtures {
|
||||
axe: AxeBuilder;
|
||||
checkA11y: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* The contents of the config.json to send when the client requests it.
|
||||
*/
|
||||
config: typeof CONFIG_JSON;
|
||||
|
||||
/**
|
||||
* The displayname to use for the user registered in {@link #credentials}.
|
||||
*
|
||||
* To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block.
|
||||
* See {@link https://playwright.dev/docs/api/class-test#test-use}.
|
||||
*/
|
||||
displayName?: string;
|
||||
|
||||
/**
|
||||
* A test fixture which registers a test user on the {@link #homeserver} and supplies the details
|
||||
* of the registered user.
|
||||
*/
|
||||
credentials: CredentialsWithDisplayName;
|
||||
|
||||
/**
|
||||
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`},
|
||||
* but adds an initScript which will populate localStorage with the user's details from
|
||||
* {@link #credentials} and {@link #homeserver}.
|
||||
*
|
||||
* Similar to {@link #user}, but doesn't load the app.
|
||||
*/
|
||||
pageWithCredentials: Page;
|
||||
|
||||
/**
|
||||
* A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores
|
||||
* the credentials into localStorage per {@link #homeserver}, and then loads the front page of the
|
||||
* app.
|
||||
*/
|
||||
user: CredentialsWithDisplayName;
|
||||
|
||||
export interface TestFixtures extends BaseTestFixtures {
|
||||
/**
|
||||
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`},
|
||||
* but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI,
|
||||
@@ -105,13 +56,11 @@ export interface TestFixtures {
|
||||
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
|
||||
botCreateOpts: CreateBotOpts;
|
||||
bot: Bot;
|
||||
labsFlags: string[];
|
||||
webserver: Webserver;
|
||||
disablePresence: boolean;
|
||||
}
|
||||
|
||||
type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures;
|
||||
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & Options, CombinedTestFixtures>;
|
||||
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & WorkerOptions, CombinedTestFixtures>;
|
||||
export const test = base.extend<TestFixtures>({
|
||||
context: async ({ context }, use, testInfo) => {
|
||||
// We skip tests instead of using grep-invert to still surface the counts in the html report
|
||||
@@ -121,102 +70,12 @@ export const test = base.extend<TestFixtures>({
|
||||
);
|
||||
await use(context);
|
||||
},
|
||||
disablePresence: false,
|
||||
config: {}, // We merge this atop the default CONFIG_JSON in the page fixture to make extending it easier
|
||||
page: async ({ homeserver, context, page, config, labsFlags, disablePresence }, use) => {
|
||||
await context.route(`http://localhost:8080/config.json*`, async (route) => {
|
||||
const json = {
|
||||
...CONFIG_JSON,
|
||||
...config,
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: homeserver.baseUrl,
|
||||
},
|
||||
...config.default_server_config,
|
||||
},
|
||||
};
|
||||
json["features"] = {
|
||||
...json["features"],
|
||||
// Enable the lab features
|
||||
...labsFlags.reduce((obj, flag) => {
|
||||
obj[flag] = true;
|
||||
return obj;
|
||||
}, {}),
|
||||
};
|
||||
if (disablePresence) {
|
||||
json["enable_presence_by_hs_url"] = {
|
||||
[homeserver.baseUrl]: false,
|
||||
};
|
||||
}
|
||||
await route.fulfill({ json });
|
||||
});
|
||||
await use(page);
|
||||
|
||||
axe: async ({ axe }, use) => {
|
||||
// Exclude floating UI for now
|
||||
await use(axe.exclude("[data-floating-ui-portal]"));
|
||||
},
|
||||
|
||||
displayName: undefined,
|
||||
credentials: async ({ context, homeserver, displayName: testDisplayName }, use, testInfo) => {
|
||||
const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"];
|
||||
const password = _.uniqueId("password_");
|
||||
const displayName = testDisplayName ?? _.sample(names)!;
|
||||
|
||||
const credentials = await homeserver.registerUser(`user_${testInfo.testId}`, password, displayName);
|
||||
console.log(`Registered test user ${credentials.userId} with displayname ${displayName}`);
|
||||
|
||||
await use({
|
||||
...credentials,
|
||||
displayName,
|
||||
});
|
||||
},
|
||||
labsFlags: [],
|
||||
|
||||
pageWithCredentials: async ({ page, homeserver, credentials }, use) => {
|
||||
await page.addInitScript(
|
||||
({ baseUrl, credentials }) => {
|
||||
// Seed the localStorage with the required credentials
|
||||
window.localStorage.setItem("mx_hs_url", baseUrl);
|
||||
window.localStorage.setItem("mx_user_id", credentials.userId);
|
||||
window.localStorage.setItem("mx_access_token", credentials.accessToken);
|
||||
window.localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||
window.localStorage.setItem("mx_is_guest", "false");
|
||||
window.localStorage.setItem("mx_has_pickle_key", "false");
|
||||
window.localStorage.setItem("mx_has_access_token", "true");
|
||||
|
||||
window.localStorage.setItem(
|
||||
"mx_local_settings",
|
||||
JSON.stringify({
|
||||
// Retain any other settings which may have already been set
|
||||
...JSON.parse(window.localStorage.getItem("mx_local_settings") || "{}"),
|
||||
// Ensure the language is set to a consistent value
|
||||
language: "en",
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ baseUrl: homeserver.baseUrl, credentials },
|
||||
);
|
||||
await use(page);
|
||||
},
|
||||
|
||||
user: async ({ pageWithCredentials: page, credentials }, use) => {
|
||||
await page.goto("/");
|
||||
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
|
||||
await use(credentials);
|
||||
},
|
||||
|
||||
axe: async ({ page }, use) => {
|
||||
await use(new AxeBuilder({ page }).exclude("[data-floating-ui-portal]"));
|
||||
},
|
||||
checkA11y: async ({ axe }, use, testInfo) =>
|
||||
use(async () => {
|
||||
const results = await axe.analyze();
|
||||
|
||||
await testInfo.attach("accessibility-scan-results", {
|
||||
body: JSON.stringify(results, null, 2),
|
||||
contentType: "application/json",
|
||||
});
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
}),
|
||||
|
||||
app: async ({ page }, use) => {
|
||||
const app = new ElementAppPage(page);
|
||||
await use(app);
|
||||
@@ -244,35 +103,23 @@ export const test = base.extend<TestFixtures>({
|
||||
},
|
||||
});
|
||||
|
||||
// Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2
|
||||
function sanitizeFilePathBeforeExtension(filePath: string): string {
|
||||
const ext = extname(filePath);
|
||||
const base = filePath.substring(0, filePath.length - ext.length);
|
||||
return sanitizeForFilePath(base) + ext;
|
||||
interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
|
||||
includeDialogBackground?: boolean;
|
||||
showTooltips?: boolean;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export const expect = baseExpect.extend({
|
||||
async toMatchScreenshot(
|
||||
type Expectations = {
|
||||
toMatchScreenshot: (
|
||||
this: ExpectMatcherState,
|
||||
receiver: Page | Locator,
|
||||
name: `${string}.png`,
|
||||
options?: {
|
||||
mask?: Array<Locator>;
|
||||
includeDialogBackground?: boolean;
|
||||
showTooltips?: boolean;
|
||||
timeout?: number;
|
||||
css?: string;
|
||||
},
|
||||
) {
|
||||
const testInfo = test.info();
|
||||
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
|
||||
|
||||
if (!testInfo.tags.includes("@screenshot")) {
|
||||
throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot");
|
||||
}
|
||||
|
||||
const page = "page" in receiver ? receiver.page() : receiver;
|
||||
options?: ExtendedToMatchScreenshotOptions,
|
||||
) => Promise<MatcherReturnType>;
|
||||
};
|
||||
|
||||
export const expect = baseExpect.extend<Expectations>({
|
||||
async toMatchScreenshot(receiver, name, options) {
|
||||
let css = `
|
||||
.mx_MessagePanel_myReadMarker {
|
||||
display: none !important;
|
||||
@@ -322,21 +169,9 @@ export const expect = baseExpect.extend({
|
||||
css += options.css;
|
||||
}
|
||||
|
||||
// We add a custom style tag before taking screenshots
|
||||
const style = (await page.addStyleTag({
|
||||
content: css,
|
||||
})) as ElementHandle<Element>;
|
||||
|
||||
const screenshotName = sanitizeFilePathBeforeExtension(name);
|
||||
await baseExpect(receiver).toHaveScreenshot(screenshotName, options);
|
||||
|
||||
await style.evaluate((tag) => tag.remove());
|
||||
|
||||
testInfo.annotations.push({
|
||||
// `_` prefix hides it from the HTML reporter
|
||||
type: "_screenshot",
|
||||
// include a path relative to `playwright/snapshots/`
|
||||
description: testInfo.snapshotPath(screenshotName).split("/playwright/snapshots/", 2)[1],
|
||||
await baseExpect(receiver).toMatchScreenshot(name, {
|
||||
...options,
|
||||
css,
|
||||
});
|
||||
|
||||
return { pass: true, message: () => "", name: "toMatchScreenshot" };
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type BrowserContext, type Page, type TestInfo } from "@playwright/test";
|
||||
import { type Readable } from "stream";
|
||||
import stripAnsi from "strip-ansi";
|
||||
|
||||
export class Logger {
|
||||
private pages: Page[] = [];
|
||||
private logs: Record<string, string> = {};
|
||||
|
||||
public getConsumer(container: string) {
|
||||
this.logs[container] = "";
|
||||
return (stream: Readable) => {
|
||||
stream.on("data", (chunk) => {
|
||||
this.logs[container] += chunk.toString();
|
||||
});
|
||||
stream.on("err", (chunk) => {
|
||||
this.logs[container] += "ERR " + chunk.toString();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public async onTestStarted(context: BrowserContext) {
|
||||
this.pages = [];
|
||||
for (const id in this.logs) {
|
||||
if (id.startsWith("page-")) {
|
||||
delete this.logs[id];
|
||||
} else {
|
||||
this.logs[id] = "";
|
||||
}
|
||||
}
|
||||
|
||||
context.on("console", (msg) => {
|
||||
const page = msg.page();
|
||||
let pageIdx = this.pages.indexOf(page);
|
||||
if (pageIdx === -1) {
|
||||
this.pages.push(page);
|
||||
pageIdx = this.pages.length - 1;
|
||||
this.logs[`page-${pageIdx}`] = `Console logs for page with URL: ${page.url()}\n\n`;
|
||||
}
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
this.logs[`page-${pageIdx}`] += `${type}: ${text}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
public async onTestFinished(testInfo: TestInfo) {
|
||||
if (testInfo.status !== "passed") {
|
||||
for (const id in this.logs) {
|
||||
if (!this.logs[id]) continue;
|
||||
await testInfo.attach(id, {
|
||||
body: stripAnsi(this.logs[id]),
|
||||
contentType: "text/plain",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Options } from "../../../services.ts";
|
||||
import { type WorkerOptions } from "../../../services.ts";
|
||||
|
||||
export const isDendrite = ({ homeserverType }: Options): boolean => {
|
||||
export const isDendrite = ({ homeserverType }: WorkerOptions): boolean => {
|
||||
return homeserverType === "dendrite" || homeserverType === "pinecone";
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type ClientServerApi } from "../utils/api.ts";
|
||||
import { type ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
|
||||
|
||||
export interface HomeserverInstance {
|
||||
readonly baseUrl: string;
|
||||
|
||||
@@ -6,30 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { type Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const consentHomeserver: Fixtures = {
|
||||
_homeserver: [
|
||||
async ({ _homeserver: container, mailpit }, use) => {
|
||||
container
|
||||
(container as SynapseContainer)
|
||||
.withCopyDirectoriesToContainer([
|
||||
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
|
||||
])
|
||||
.withSmtpServer(mailpit)
|
||||
.withConfig({
|
||||
email: {
|
||||
enable_notifs: false,
|
||||
smtp_host: "mailpit",
|
||||
smtp_port: 1025,
|
||||
smtp_user: "username",
|
||||
smtp_pass: "password",
|
||||
require_transport_security: false,
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
|
||||
app_name: "Matrix",
|
||||
notif_template_html: "notif_mail.html",
|
||||
notif_template_text: "notif_mail.txt",
|
||||
notif_for_new_users: true,
|
||||
client_base_url: "http://localhost/element",
|
||||
},
|
||||
user_consent: {
|
||||
template_dir: "/data/res/templates/privacy",
|
||||
version: "1.0",
|
||||
|
||||
@@ -6,7 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts";
|
||||
import { MatrixAuthenticationServiceContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { type Fixtures } from "../../../element-web-test.ts";
|
||||
|
||||
export const masHomeserver: Fixtures = {
|
||||
|
||||
@@ -10,8 +10,7 @@ import http from "http";
|
||||
import express from "express";
|
||||
import { type AddressInfo } from "net";
|
||||
import { type TestInfo } from "@playwright/test";
|
||||
|
||||
import { randB64Bytes } from "../utils/rand.ts";
|
||||
import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js";
|
||||
|
||||
export class OAuthServer {
|
||||
private server?: http.Server;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type APIRequestContext } from "@playwright/test";
|
||||
|
||||
import { type Credentials } from "../homeserver";
|
||||
|
||||
export type Verb = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export class Api {
|
||||
private _request?: APIRequestContext;
|
||||
|
||||
constructor(private readonly baseUrl: string) {}
|
||||
|
||||
public setRequest(request: APIRequestContext): void {
|
||||
this._request = request;
|
||||
}
|
||||
|
||||
public async request<R extends {}>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
|
||||
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
|
||||
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const res = await this._request.fetch(url, {
|
||||
data,
|
||||
method: verb,
|
||||
headers: token
|
||||
? {
|
||||
Authorization: `Bearer ${token}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
if (!res.ok()) {
|
||||
throw new Error(
|
||||
`Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`,
|
||||
);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
}
|
||||
|
||||
export class ClientServerApi extends Api {
|
||||
constructor(baseUrl: string) {
|
||||
super(`${baseUrl}/_matrix/client`);
|
||||
}
|
||||
|
||||
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
||||
const json = await this.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}>("POST", "/v3/login", undefined, {
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: userId,
|
||||
},
|
||||
password: password,
|
||||
});
|
||||
|
||||
return {
|
||||
password,
|
||||
accessToken: json.access_token,
|
||||
userId: json.user_id,
|
||||
deviceId: json.device_id,
|
||||
homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"),
|
||||
username: userId.slice(1).split(":")[0],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Deep copy the given object. The object MUST NOT have circular references and
|
||||
* MUST NOT have functions.
|
||||
* @param obj - The object to deep copy.
|
||||
* @returns A copy of the object without any references to the original.
|
||||
*/
|
||||
export function deepCopy<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import * as net from "net";
|
||||
|
||||
export async function getFreePort(): Promise<number> {
|
||||
return new Promise<number>((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = (<net.AddressInfo>srv.address()).port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
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 crypto from "node:crypto";
|
||||
|
||||
export function randB64Bytes(numBytes: number): string {
|
||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||
}
|
||||
@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import * as http from "http";
|
||||
import { type AddressInfo } from "net";
|
||||
import * as http from "node:http";
|
||||
import { type AddressInfo } from "node:net";
|
||||
|
||||
export class Webserver {
|
||||
private server?: http.Server;
|
||||
|
||||
@@ -5,113 +5,32 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test as base } from "@playwright/test";
|
||||
import { type MailpitClient } from "mailpit-api";
|
||||
import { Network, type StartedNetwork } from "testcontainers";
|
||||
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
|
||||
import { test as base } from "@element-hq/element-web-playwright-common";
|
||||
import {
|
||||
type Services as BaseServices,
|
||||
type WorkerOptions as BaseWorkerOptions,
|
||||
} from "@element-hq/element-web-playwright-common/lib/fixtures";
|
||||
import { type HomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { type SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts";
|
||||
import { Logger } from "./logger.ts";
|
||||
import { type StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts";
|
||||
import { type HomeserverContainer, type StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts";
|
||||
import { MailhogContainer, type StartedMailhogContainer } from "./testcontainers/mailpit.ts";
|
||||
import { type OAuthServer } from "./plugins/oauth_server";
|
||||
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts";
|
||||
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite";
|
||||
import { type HomeserverType } from "./plugins/homeserver";
|
||||
import { SynapseContainer } from "./testcontainers/synapse";
|
||||
|
||||
export interface TestFixtures {
|
||||
mailpitClient: MailpitClient;
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
logger: Logger;
|
||||
|
||||
network: StartedNetwork;
|
||||
postgres: StartedPostgreSqlContainer;
|
||||
mailpit: StartedMailhogContainer;
|
||||
|
||||
synapseConfig: SynapseConfig;
|
||||
_homeserver: HomeserverContainer<any>;
|
||||
homeserver: StartedHomeserverContainer;
|
||||
// Set in masHomeserver only
|
||||
mas?: StartedMatrixAuthenticationServiceContainer;
|
||||
export interface Services extends BaseServices {
|
||||
// Set in legacyOAuthHomeserver only
|
||||
oAuthServer?: OAuthServer;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
export interface WorkerOptions extends BaseWorkerOptions {
|
||||
homeserverType: HomeserverType;
|
||||
}
|
||||
|
||||
export const test = base.extend<TestFixtures, Services & Options>({
|
||||
logger: [
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
async ({}, use) => {
|
||||
const logger = new Logger();
|
||||
await use(logger);
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
network: [
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
async ({}, use) => {
|
||||
const network = await new Network().start();
|
||||
await use(network);
|
||||
await network.stop();
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
postgres: [
|
||||
async ({ logger, network }, use) => {
|
||||
const container = await new PostgreSqlContainer()
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("postgres")
|
||||
.withLogConsumer(logger.getConsumer("postgres"))
|
||||
.withTmpFs({
|
||||
"/dev/shm/pgdata/data": "",
|
||||
})
|
||||
.withEnvironment({
|
||||
PG_DATA: "/dev/shm/pgdata/data",
|
||||
})
|
||||
.withCommand([
|
||||
"-c",
|
||||
"shared_buffers=128MB",
|
||||
"-c",
|
||||
`fsync=off`,
|
||||
"-c",
|
||||
`synchronous_commit=off`,
|
||||
"-c",
|
||||
"full_page_writes=off",
|
||||
])
|
||||
.start();
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
mailpit: [
|
||||
async ({ logger, network }, use) => {
|
||||
const container = await new MailhogContainer()
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("mailpit")
|
||||
.withLogConsumer(logger.getConsumer("mailpit"))
|
||||
.start();
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
mailpitClient: async ({ mailpit: container }, use) => {
|
||||
await container.client.deleteMessages();
|
||||
await use(container.client);
|
||||
},
|
||||
|
||||
synapseConfig: [{}, { scope: "worker" }],
|
||||
export const test = base.extend<{}, Services & WorkerOptions>({
|
||||
homeserverType: ["synapse", { option: true, scope: "worker" }],
|
||||
_homeserver: [
|
||||
async ({ homeserverType }, use) => {
|
||||
let container: HomeserverContainer<any>;
|
||||
let container: HomeserverContainer<unknown>;
|
||||
switch (homeserverType) {
|
||||
case "synapse":
|
||||
container = new SynapseContainer();
|
||||
@@ -128,46 +47,12 @@ export const test = base.extend<TestFixtures, Services & Options>({
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
homeserver: [
|
||||
async ({ homeserverType, logger, network, _homeserver: homeserver, synapseConfig, mas }, use) => {
|
||||
if (homeserver instanceof SynapseContainer) {
|
||||
homeserver.withConfig(synapseConfig);
|
||||
}
|
||||
const container = await homeserver
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("homeserver")
|
||||
.withLogConsumer(logger.getConsumer(homeserverType))
|
||||
.withMatrixAuthenticationService(mas)
|
||||
.start();
|
||||
|
||||
await use(container);
|
||||
await container.stop();
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
mas: [
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
async ({}, use) => {
|
||||
// we stub the mas fixture to allow `homeserver` to depend on it to ensure
|
||||
// when it is specified by `masHomeserver` it is started before the homeserver
|
||||
await use(undefined);
|
||||
},
|
||||
{ scope: "worker" },
|
||||
],
|
||||
|
||||
context: async (
|
||||
{ homeserverType, synapseConfig, logger, context, request, _homeserver, homeserver },
|
||||
use,
|
||||
testInfo,
|
||||
) => {
|
||||
context: async ({ homeserverType, synapseConfig, context, _homeserver }, use, testInfo) => {
|
||||
testInfo.skip(
|
||||
!(_homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0,
|
||||
`Test specifies Synapse config options so is unsupported with ${homeserverType}`,
|
||||
);
|
||||
homeserver.setRequest(request);
|
||||
await logger.onTestStarted(context);
|
||||
await use(context);
|
||||
await logger.onTestFinished(testInfo);
|
||||
await homeserver.onTestFinished(testInfo);
|
||||
},
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type AbstractStartedContainer, type GenericContainer } from "testcontainers";
|
||||
import { type APIRequestContext, type TestInfo } from "@playwright/test";
|
||||
|
||||
import { type HomeserverInstance } from "../plugins/homeserver";
|
||||
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
|
||||
|
||||
export interface HomeserverContainer<Config> extends GenericContainer {
|
||||
withConfigField(key: string, value: any): this;
|
||||
withConfig(config: Partial<Config>): this;
|
||||
withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this;
|
||||
start(): Promise<StartedHomeserverContainer>;
|
||||
}
|
||||
|
||||
export interface StartedHomeserverContainer extends AbstractStartedContainer, HomeserverInstance {
|
||||
setRequest(request: APIRequestContext): void;
|
||||
onTestFinished(testInfo: TestInfo): Promise<void>;
|
||||
}
|
||||
@@ -8,12 +8,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
import { GenericContainer, Wait } from "testcontainers";
|
||||
import * as YAML from "yaml";
|
||||
import { set } from "lodash";
|
||||
|
||||
import { randB64Bytes } from "../plugins/utils/rand.ts";
|
||||
import { StartedSynapseContainer } from "./synapse.ts";
|
||||
import { deepCopy } from "../plugins/utils/object.ts";
|
||||
import { type HomeserverContainer } from "./HomeserverContainer.ts";
|
||||
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
|
||||
import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js";
|
||||
import { deepCopy } from "@element-hq/element-web-playwright-common/lib/utils/object.js";
|
||||
import {
|
||||
StartedSynapseContainer,
|
||||
type HomeserverContainer,
|
||||
type StartedMatrixAuthenticationServiceContainer,
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
version: 2,
|
||||
@@ -223,7 +224,7 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
|
||||
.withWaitStrategy(Wait.forHttp("/_matrix/client/versions", 8008));
|
||||
}
|
||||
|
||||
public withConfigField(key: string, value: any): this {
|
||||
public withConfigField(key: string, value: unknown): this {
|
||||
set(this.config, key, value);
|
||||
return this;
|
||||
}
|
||||
@@ -236,6 +237,11 @@ export class DendriteContainer extends GenericContainer implements HomeserverCon
|
||||
return this;
|
||||
}
|
||||
|
||||
// Dendrite does not support SMTP at this time - https://github.com/element-hq/dendrite/issues/1298
|
||||
public withSmtpServer(): this {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Dendrite does not support MAS at this time
|
||||
public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this {
|
||||
return this;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { AbstractStartedContainer, GenericContainer, type StartedTestContainer, Wait } from "testcontainers";
|
||||
import { MailpitClient } from "mailpit-api";
|
||||
|
||||
export class MailhogContainer extends GenericContainer {
|
||||
constructor() {
|
||||
super("axllent/mailpit:latest");
|
||||
|
||||
this.withExposedPorts(8025).withWaitStrategy(Wait.forListeningPorts()).withEnvironment({
|
||||
MP_SMTP_AUTH_ALLOW_INSECURE: "true",
|
||||
MP_SMTP_AUTH_ACCEPT_ANY: "true",
|
||||
});
|
||||
}
|
||||
|
||||
public override async start(): Promise<StartedMailhogContainer> {
|
||||
return new StartedMailhogContainer(await super.start());
|
||||
}
|
||||
}
|
||||
|
||||
export class StartedMailhogContainer extends AbstractStartedContainer {
|
||||
public readonly client: MailpitClient;
|
||||
|
||||
constructor(container: StartedTestContainer) {
|
||||
super(container);
|
||||
this.client = new MailpitClient(`http://${container.getHost()}:${container.getMappedPort(8025)}`);
|
||||
}
|
||||
}
|
||||
@@ -1,346 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractStartedContainer,
|
||||
GenericContainer,
|
||||
type StartedTestContainer,
|
||||
Wait,
|
||||
type ExecResult,
|
||||
} from "testcontainers";
|
||||
import { type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
|
||||
import * as YAML from "yaml";
|
||||
|
||||
import { getFreePort } from "../plugins/utils/port.ts";
|
||||
import { deepCopy } from "../plugins/utils/object.ts";
|
||||
import { type Credentials } from "../plugins/homeserver";
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
http: {
|
||||
listeners: [
|
||||
{
|
||||
name: "web",
|
||||
resources: [
|
||||
{ name: "discovery" },
|
||||
{ name: "human" },
|
||||
{ name: "oauth" },
|
||||
{ name: "compat" },
|
||||
{
|
||||
name: "graphql",
|
||||
playground: true,
|
||||
},
|
||||
{
|
||||
name: "assets",
|
||||
path: "/usr/local/share/mas-cli/assets/",
|
||||
},
|
||||
],
|
||||
binds: [
|
||||
{
|
||||
address: "[::]:8080",
|
||||
},
|
||||
],
|
||||
proxy_protocol: false,
|
||||
},
|
||||
{
|
||||
name: "internal",
|
||||
resources: [
|
||||
{
|
||||
name: "health",
|
||||
},
|
||||
],
|
||||
binds: [
|
||||
{
|
||||
address: "[::]:8081",
|
||||
},
|
||||
],
|
||||
proxy_protocol: false,
|
||||
},
|
||||
],
|
||||
trusted_proxies: ["192.128.0.0/16", "172.16.0.0/12", "10.0.0.0/10", "127.0.0.1/8", "fd00::/8", "::1/128"],
|
||||
public_base: "", // Needs to be set
|
||||
issuer: "", // Needs to be set
|
||||
},
|
||||
database: {
|
||||
host: "postgres",
|
||||
port: 5432,
|
||||
database: "postgres",
|
||||
username: "postgres",
|
||||
password: "p4S5w0rD",
|
||||
max_connections: 10,
|
||||
min_connections: 0,
|
||||
connect_timeout: 30,
|
||||
idle_timeout: 600,
|
||||
max_lifetime: 1800,
|
||||
},
|
||||
telemetry: {
|
||||
tracing: {
|
||||
exporter: "none",
|
||||
propagators: [],
|
||||
},
|
||||
metrics: {
|
||||
exporter: "none",
|
||||
},
|
||||
sentry: {
|
||||
dsn: null,
|
||||
},
|
||||
},
|
||||
templates: {
|
||||
path: "/usr/local/share/mas-cli/templates/",
|
||||
assets_manifest: "/usr/local/share/mas-cli/manifest.json",
|
||||
translations_path: "/usr/local/share/mas-cli/translations/",
|
||||
},
|
||||
email: {
|
||||
from: '"Authentication Service" <root@localhost>',
|
||||
reply_to: '"Authentication Service" <root@localhost>',
|
||||
transport: "smtp",
|
||||
mode: "plain",
|
||||
hostname: "mailpit",
|
||||
port: 1025,
|
||||
username: "username",
|
||||
password: "password",
|
||||
},
|
||||
secrets: {
|
||||
encryption: "984b18e207c55ad5fbb2a49b217481a722917ee87b2308d4cf314c83fed8e3b5",
|
||||
keys: [
|
||||
{
|
||||
kid: "YEAhzrKipJ",
|
||||
key: "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuIV+AW5vx52I4CuumgSxp6yvKfIAnRdALeZZCoFkIGxUli1B\nS79NJ3ls46oLh1pSD9RrhaMp6HTNoi4K3hnP9Q9v77pD7KwdFKG3UdG1zksIB0s/\n+/Ey/DmX4LPluwBBS7r/LkQ1jk745lENA++oiDqZf2D/uP8jCHlvaSNyVKTqi1ki\nOXPd4T4xBUjzuas9ze5jQVSYtfOidgnv1EzUipbIxgvH1jNt4raRlmP8mOq7xEnW\nR+cF5x6n/g17PdSEfrwO4kz6aKGZuMP5lVlDEEnMHKabFSQDBl7+Mpok6jXutbtA\nuiBnsKEahF9eoj4na4fpbRNPdIVyoaN5eGvm5wIDAQABAoIBAApyFCYEmHNWaa83\nCdVSOrRhRDE9r+c0r79pcNT1ajOjrk4qFa4yEC4R46YntCtfY5Hd1pBkIjU0l4d8\nz8Su9WTMEOwjQUEepS7L0NLi6kXZXYT8L40VpGs+32grBvBFHW0qEtQNrHJ36gMv\nx2rXoFTF7HaXiSJx3wvVxAbRqOE9tBXLsmNHaWaAdWQG5o77V9+zvMri3cAeEg2w\nVkKokb0dza7es7xG3tqS26k69SrwGeeuKo7qCHPH2cfyWmY5Yhv8iOoA59JzzbiK\nUdxyzCHskrPSpRKVkVVwmY3RBt282TmSRG7td7e5ESSj50P2e5BI5uu1Hp/dvU4F\nvYjV7kECgYEA6WqYoUpVsgQiqhvJwJIc/8gRm0mUy8TenI36z4Iim01Nt7fibWH7\nXnsFqLGjXtYNVWvBcCrUl9doEnRbJeG2eRGbGKYAWVrOeFvwM4fYvw9GoOiJdDj4\ncgWDe7eHbHE+UTqR7Nnr/UBfipoNWDh6X68HRBuXowh0Q6tOfxsrRFECgYEAyl/V\n4b8bFp3pKZZCb+KPSYsQf793cRmrBexPcLWcDPYbMZQADEZ/VLjbrNrpTOWxUWJT\nhr8MrWswnHO+l5AFu5CNO+QgV2dHLk+2w8qpdpFRPJCfXfo2D3wZ0c4cv3VCwv1V\n5y7f6XWVjDWZYV4wj6c3shxZJjZ+9Hbhf3/twbcCgYA6fuRRR3fCbRbi2qPtBrEN\nyO3gpMgNaQEA6vP4HPzfPrhDWmn8T5nXS61XYW03zxz4U1De81zj0K/cMBzHmZFJ\nNghQXQmpWwBzWVcREvJWr1Vb7erEnaJlsMwKrSvbGWYspSj82oAxr3hCG+lMOpsw\nb4S6pM+TpAK/EqdRY1WsgQKBgQCGoMaaTRXqL9bC0bEU2XVVCWxKb8c3uEmrwQ7/\n/fD4NmjUzI5TnDps1CVfkqoNe+hAKddDFqmKXHqUOfOaxDbsFje+lf5l5tDVoDYH\nfjTKKdYPIm7CiAeauYY7qpA5Vfq52Opixy4yEwUPp0CII67OggFtPaqY3zwJyWQt\n+57hdQKBgGCXM/KKt7ceUDcNJxSGjvu0zD9D5Sv2ihYlEBT/JLaTCCJdvzREevaJ\n1d+mpUAt0Lq6A8NWOMq8HPaxAik3rMQ0WtM5iG+XgsUqvTSb7NcshArDLuWGnW3m\nMC4rM0UBYAS4QweduUSH1imrwH/1Gu5+PxbiecceRMMggWpzu0Bq\n-----END RSA PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
kid: "8J1AxrlNZT",
|
||||
key: "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIF1cjfIOEdy3BXJ72x6fKpEB8WP1ddZAUJAaqqr/6CpToAoGCCqGSM49\nAwEHoUQDQgAEfHdNuI1Yeh3/uOq2PlnW2vymloOVpwBYebbw4VVsna9xhnutIdQW\ndE8hkX8Yb0pIDasrDiwllVLzSvsWJAI0Kw==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
kid: "3BW6un1EBi",
|
||||
key: "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDA+3ZV17r8TsiMdw1cpbTSNbyEd5SMy3VS1Mk/kz6O2Ev/3QZut8GE2\nq3eGtLBoVQigBwYFK4EEACKhZANiAASs8Wxjk/uRimRKXnPr2/wDaXkN9wMDjYQK\nmZULb+0ZP1/cXmuXuri8hUGhQvIU8KWY9PkpV+LMPEdpE54mHPKSLjq5CDXoSZ/P\n9f7cdRaOZ000KQPZfIFR9ujJTtDN7Vs=\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
kid: "pkZ0pTKK0X",
|
||||
key: "-----BEGIN EC PRIVATE KEY-----\nMHQCAQEEIHenfsXYPc5yzjZKUfvmydDR1YRwdsfZYvwHf/2wsYxooAcGBSuBBAAK\noUQDQgAEON1x7Vlu+nA0KvC5vYSOHhDUkfLYNZwYSLPFVT02h9E13yFFMIJegIBl\nAer+6PMZpPc8ycyeH9N+U9NAyliBhQ==\n-----END EC PRIVATE KEY-----\n",
|
||||
},
|
||||
],
|
||||
},
|
||||
passwords: {
|
||||
enabled: true,
|
||||
schemes: [
|
||||
{
|
||||
version: 1,
|
||||
algorithm: "argon2id",
|
||||
},
|
||||
],
|
||||
minimum_complexity: 0,
|
||||
},
|
||||
policy: {
|
||||
wasm_module: "/usr/local/share/mas-cli/policy.wasm",
|
||||
client_registration_entrypoint: "client_registration/violation",
|
||||
register_entrypoint: "register/violation",
|
||||
authorization_grant_entrypoint: "authorization_grant/violation",
|
||||
password_entrypoint: "password/violation",
|
||||
email_entrypoint: "email/violation",
|
||||
data: {
|
||||
client_registration: {
|
||||
// allow non-SSL and localhost URIs
|
||||
allow_insecure_uris: true,
|
||||
// EW doesn't have contacts at this time
|
||||
allow_missing_contacts: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
upstream_oauth2: {
|
||||
providers: [],
|
||||
},
|
||||
branding: {
|
||||
service_name: null,
|
||||
policy_uri: null,
|
||||
tos_uri: null,
|
||||
imprint: null,
|
||||
logo_uri: null,
|
||||
},
|
||||
account: {
|
||||
password_registration_enabled: true,
|
||||
},
|
||||
experimental: {
|
||||
access_token_ttl: 300,
|
||||
compat_token_ttl: 300,
|
||||
},
|
||||
rate_limiting: {
|
||||
login: {
|
||||
burst: 10,
|
||||
per_second: 1,
|
||||
},
|
||||
registration: {
|
||||
burst: 10,
|
||||
per_second: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class MatrixAuthenticationServiceContainer extends GenericContainer {
|
||||
private config: typeof DEFAULT_CONFIG;
|
||||
private readonly args = ["-c", "/config/config.yaml"];
|
||||
|
||||
constructor(db: StartedPostgreSqlContainer) {
|
||||
// We rely on `mas-cli manage add-email` which isn't in a release yet
|
||||
// https://github.com/element-hq/matrix-authentication-service/pull/3235
|
||||
super("ghcr.io/element-hq/matrix-authentication-service:sha-0b90c33");
|
||||
|
||||
this.config = deepCopy(DEFAULT_CONFIG);
|
||||
this.config.database.username = db.getUsername();
|
||||
this.config.database.password = db.getPassword();
|
||||
|
||||
this.withExposedPorts(8080, 8081)
|
||||
.withWaitStrategy(Wait.forHttp("/health", 8081))
|
||||
.withCommand(["server", ...this.args]);
|
||||
}
|
||||
|
||||
public withConfig(config: object): this {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...config,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public override async start(): Promise<StartedMatrixAuthenticationServiceContainer> {
|
||||
// MAS config issuer needs to know what URL it'll be accessed from, so we have to map the port manually
|
||||
const port = await getFreePort();
|
||||
|
||||
this.config.http.public_base = `http://localhost:${port}/`;
|
||||
this.config.http.issuer = `http://localhost:${port}/`;
|
||||
|
||||
this.withExposedPorts({
|
||||
container: 8080,
|
||||
host: port,
|
||||
}).withCopyContentToContainer([
|
||||
{
|
||||
target: "/config/config.yaml",
|
||||
content: YAML.stringify(this.config),
|
||||
},
|
||||
]);
|
||||
|
||||
return new StartedMatrixAuthenticationServiceContainer(
|
||||
await super.start(),
|
||||
`http://localhost:${port}`,
|
||||
this.args,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class StartedMatrixAuthenticationServiceContainer extends AbstractStartedContainer {
|
||||
private adminTokenPromise?: Promise<string>;
|
||||
|
||||
constructor(
|
||||
container: StartedTestContainer,
|
||||
public readonly baseUrl: string,
|
||||
private readonly args: string[],
|
||||
) {
|
||||
super(container);
|
||||
}
|
||||
|
||||
public async getAdminToken(): Promise<string> {
|
||||
if (this.adminTokenPromise === undefined) {
|
||||
this.adminTokenPromise = this.registerUserInternal(
|
||||
"admin",
|
||||
"totalyinsecureadminpassword",
|
||||
undefined,
|
||||
true,
|
||||
).then((res) => res.accessToken);
|
||||
}
|
||||
return this.adminTokenPromise;
|
||||
}
|
||||
|
||||
private async manage(cmd: string, ...args: string[]): Promise<ExecResult> {
|
||||
const result = await this.exec(["mas-cli", "manage", cmd, ...this.args, ...args]);
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`Failed mas-cli manage ${cmd}: ${result.output}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private async manageRegisterUser(
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
admin = false,
|
||||
): Promise<string> {
|
||||
const args: string[] = [];
|
||||
if (admin) args.push("-a");
|
||||
const result = await this.manage(
|
||||
"register-user",
|
||||
...args,
|
||||
"-y",
|
||||
"-p",
|
||||
password,
|
||||
"-d",
|
||||
displayName ?? "",
|
||||
username,
|
||||
);
|
||||
|
||||
const registerLines = result.output.trim().split("\n");
|
||||
const userId = registerLines
|
||||
.find((line) => line.includes("Matrix ID: "))
|
||||
?.split(": ")
|
||||
.pop();
|
||||
|
||||
if (!userId) {
|
||||
throw new Error(`Failed to register user: ${result.output}`);
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
private async manageIssueCompatibilityToken(
|
||||
username: string,
|
||||
admin = false,
|
||||
): Promise<{ accessToken: string; deviceId: string }> {
|
||||
const args: string[] = [];
|
||||
if (admin) args.push("--yes-i-want-to-grant-synapse-admin-privileges");
|
||||
const result = await this.manage("issue-compatibility-token", ...args, username);
|
||||
|
||||
const parts = result.output.trim().split(/\s+/);
|
||||
const accessToken = parts.find((part) => part.startsWith("mct_"));
|
||||
const deviceId = parts.find((part) => part.startsWith("compat_session.device="))?.split("=")[1];
|
||||
|
||||
if (!accessToken || !deviceId) {
|
||||
throw new Error(`Failed to issue compatibility token: ${result.output}`);
|
||||
}
|
||||
|
||||
return { accessToken, deviceId };
|
||||
}
|
||||
|
||||
private async registerUserInternal(
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
admin = false,
|
||||
): Promise<Credentials> {
|
||||
const userId = await this.manageRegisterUser(username, password, displayName, admin);
|
||||
const { deviceId, accessToken } = await this.manageIssueCompatibilityToken(username, admin);
|
||||
|
||||
return {
|
||||
userId,
|
||||
accessToken,
|
||||
deviceId,
|
||||
homeServer: userId.slice(1).split(":").slice(1).join(":"),
|
||||
displayName,
|
||||
username,
|
||||
password,
|
||||
};
|
||||
}
|
||||
|
||||
public async registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
||||
return this.registerUserInternal(username, password, displayName, false);
|
||||
}
|
||||
|
||||
public async setThreepid(username: string, medium: string, address: string): Promise<void> {
|
||||
if (medium !== "email") {
|
||||
throw new Error("Only email threepids are supported by MAS");
|
||||
}
|
||||
|
||||
await this.manage("add-email", username, address);
|
||||
}
|
||||
}
|
||||
@@ -1,394 +1,20 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2024-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 {
|
||||
AbstractStartedContainer,
|
||||
GenericContainer,
|
||||
type RestartOptions,
|
||||
type StartedTestContainer,
|
||||
Wait,
|
||||
} from "testcontainers";
|
||||
import { type APIRequestContext, type TestInfo } from "@playwright/test";
|
||||
import crypto from "node:crypto";
|
||||
import * as YAML from "yaml";
|
||||
import { set } from "lodash";
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
import { getFreePort } from "../plugins/utils/port.ts";
|
||||
import { randB64Bytes } from "../plugins/utils/rand.ts";
|
||||
import { type Credentials } from "../plugins/homeserver";
|
||||
import { deepCopy } from "../plugins/utils/object.ts";
|
||||
import { type HomeserverContainer, type StartedHomeserverContainer } from "./HomeserverContainer.ts";
|
||||
import { type StartedMatrixAuthenticationServiceContainer } from "./mas.ts";
|
||||
import { Api, ClientServerApi, type Verb } from "../plugins/utils/api.ts";
|
||||
const TAG = "develop@sha256:2ea87d45fc7ff3327c671b3b4447e6b2032d4f5ca07d62d8aef0d900e105c2f4";
|
||||
|
||||
const TAG = "develop@sha256:8d1c531cf6010b63142a04e1b138a60720946fa131ad404813232f02db4ce7ba";
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
server_name: "localhost",
|
||||
public_baseurl: "", // set by start method
|
||||
pid_file: "/homeserver.pid",
|
||||
web_client: false,
|
||||
soft_file_limit: 0,
|
||||
// Needs to be configured to log to the console like a good docker process
|
||||
log_config: "/data/log.config",
|
||||
listeners: [
|
||||
{
|
||||
// Listener is always port 8008 (configured in the container)
|
||||
port: 8008,
|
||||
tls: false,
|
||||
bind_addresses: ["::"],
|
||||
type: "http",
|
||||
x_forwarded: true,
|
||||
resources: [
|
||||
{
|
||||
names: ["client"],
|
||||
compress: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
database: {
|
||||
// An sqlite in-memory database is fast & automatically wipes each time
|
||||
name: "sqlite3",
|
||||
args: {
|
||||
database: ":memory:",
|
||||
},
|
||||
},
|
||||
rc_messages_per_second: 10000,
|
||||
rc_message_burst_count: 10000,
|
||||
rc_registration: {
|
||||
per_second: 10000,
|
||||
burst_count: 10000,
|
||||
},
|
||||
rc_joins: {
|
||||
local: {
|
||||
per_second: 9999,
|
||||
burst_count: 9999,
|
||||
},
|
||||
remote: {
|
||||
per_second: 9999,
|
||||
burst_count: 9999,
|
||||
},
|
||||
},
|
||||
rc_joins_per_room: {
|
||||
per_second: 9999,
|
||||
burst_count: 9999,
|
||||
},
|
||||
rc_3pid_validation: {
|
||||
per_second: 1000,
|
||||
burst_count: 1000,
|
||||
},
|
||||
rc_invites: {
|
||||
per_room: {
|
||||
per_second: 1000,
|
||||
burst_count: 1000,
|
||||
},
|
||||
per_user: {
|
||||
per_second: 1000,
|
||||
burst_count: 1000,
|
||||
},
|
||||
},
|
||||
rc_login: {
|
||||
address: {
|
||||
per_second: 10000,
|
||||
burst_count: 10000,
|
||||
},
|
||||
account: {
|
||||
per_second: 10000,
|
||||
burst_count: 10000,
|
||||
},
|
||||
failed_attempts: {
|
||||
per_second: 10000,
|
||||
burst_count: 10000,
|
||||
},
|
||||
},
|
||||
media_store_path: "/tmp/media_store",
|
||||
max_upload_size: "50M",
|
||||
max_image_pixels: "32M",
|
||||
dynamic_thumbnails: false,
|
||||
enable_registration: true,
|
||||
enable_registration_without_verification: true,
|
||||
disable_msisdn_registration: false,
|
||||
registrations_require_3pid: [],
|
||||
enable_metrics: false,
|
||||
report_stats: false,
|
||||
// These placeholders will be replaced with values generated at start
|
||||
registration_shared_secret: "secret",
|
||||
macaroon_secret_key: "secret",
|
||||
form_secret: "secret",
|
||||
// Signing key must be here: it will be generated to this file
|
||||
signing_key_path: "/data/localhost.signing.key",
|
||||
trusted_key_servers: [],
|
||||
password_config: {
|
||||
enabled: true,
|
||||
},
|
||||
ui_auth: {},
|
||||
background_updates: {
|
||||
// Inhibit background updates as this Synapse isn't long-lived
|
||||
min_batch_size: 100000,
|
||||
sleep_duration_ms: 100000,
|
||||
},
|
||||
enable_authenticated_media: true,
|
||||
email: undefined,
|
||||
user_consent: undefined,
|
||||
server_notices: undefined,
|
||||
allow_guest_access: false,
|
||||
experimental_features: {},
|
||||
oidc_providers: [],
|
||||
serve_server_wellknown: true,
|
||||
presence: {
|
||||
enabled: true,
|
||||
include_offline_users_on_sync: true,
|
||||
},
|
||||
};
|
||||
|
||||
export type SynapseConfig = Partial<typeof DEFAULT_CONFIG>;
|
||||
|
||||
export class SynapseContainer extends GenericContainer implements HomeserverContainer<typeof DEFAULT_CONFIG> {
|
||||
private config: typeof DEFAULT_CONFIG;
|
||||
private mas?: StartedMatrixAuthenticationServiceContainer;
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
* updated periodically by the `playwright-image-updates.yaml` workflow.
|
||||
*/
|
||||
export class SynapseContainer extends BaseSynapseContainer {
|
||||
public constructor() {
|
||||
super(`ghcr.io/element-hq/synapse:${TAG}`);
|
||||
|
||||
this.config = deepCopy(DEFAULT_CONFIG);
|
||||
this.config.registration_shared_secret = randB64Bytes(16);
|
||||
this.config.macaroon_secret_key = randB64Bytes(16);
|
||||
this.config.form_secret = randB64Bytes(16);
|
||||
|
||||
const signingKey = randB64Bytes(32);
|
||||
this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([
|
||||
{ target: this.config.signing_key_path, content: `ed25519 x ${signingKey}` },
|
||||
{
|
||||
target: this.config.log_config,
|
||||
content: YAML.stringify({
|
||||
version: 1,
|
||||
formatters: {
|
||||
precise: {
|
||||
format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s",
|
||||
},
|
||||
},
|
||||
handlers: {
|
||||
console: {
|
||||
class: "logging.StreamHandler",
|
||||
formatter: "precise",
|
||||
},
|
||||
},
|
||||
loggers: {
|
||||
"synapse.storage.SQL": {
|
||||
level: "DEBUG",
|
||||
},
|
||||
"twisted": {
|
||||
handlers: ["console"],
|
||||
propagate: false,
|
||||
},
|
||||
},
|
||||
root: {
|
||||
level: "DEBUG",
|
||||
handlers: ["console"],
|
||||
},
|
||||
disable_existing_loggers: false,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
public withConfigField(key: string, value: any): this {
|
||||
set(this.config, key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public withConfig(config: Partial<typeof DEFAULT_CONFIG>): this {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...config,
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public withMatrixAuthenticationService(mas?: StartedMatrixAuthenticationServiceContainer): this {
|
||||
this.mas = mas;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override async start(): Promise<StartedSynapseContainer> {
|
||||
// Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually
|
||||
const port = await getFreePort();
|
||||
|
||||
this.withExposedPorts({
|
||||
container: 8008,
|
||||
host: port,
|
||||
})
|
||||
.withConfig({
|
||||
public_baseurl: `http://localhost:${port}`,
|
||||
})
|
||||
.withCopyContentToContainer([
|
||||
{
|
||||
target: "/data/homeserver.yaml",
|
||||
content: YAML.stringify(this.config),
|
||||
},
|
||||
]);
|
||||
|
||||
const container = await super.start();
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
if (this.mas) {
|
||||
return new StartedSynapseWithMasContainer(
|
||||
container,
|
||||
baseUrl,
|
||||
this.config.registration_shared_secret,
|
||||
this.mas,
|
||||
);
|
||||
}
|
||||
|
||||
return new StartedSynapseContainer(container, baseUrl, this.config.registration_shared_secret);
|
||||
}
|
||||
}
|
||||
|
||||
export class StartedSynapseContainer extends AbstractStartedContainer implements StartedHomeserverContainer {
|
||||
protected adminTokenPromise?: Promise<string>;
|
||||
protected readonly adminApi: Api;
|
||||
public readonly csApi: ClientServerApi;
|
||||
|
||||
constructor(
|
||||
container: StartedTestContainer,
|
||||
public readonly baseUrl: string,
|
||||
private readonly registrationSharedSecret: string,
|
||||
) {
|
||||
super(container);
|
||||
this.adminApi = new Api(`${this.baseUrl}/_synapse/admin`);
|
||||
this.csApi = new ClientServerApi(this.baseUrl);
|
||||
}
|
||||
|
||||
public restart(options?: Partial<RestartOptions>): Promise<void> {
|
||||
this.adminTokenPromise = undefined;
|
||||
return super.restart(options);
|
||||
}
|
||||
|
||||
public setRequest(request: APIRequestContext): void {
|
||||
this.csApi.setRequest(request);
|
||||
this.adminApi.setRequest(request);
|
||||
}
|
||||
|
||||
public async onTestFinished(testInfo: TestInfo): Promise<void> {
|
||||
// Clean up the server to prevent rooms leaking between tests
|
||||
await this.deletePublicRooms();
|
||||
}
|
||||
|
||||
protected async deletePublicRooms(): Promise<void> {
|
||||
const token = await this.getAdminToken();
|
||||
// We hide the rooms from the room directory to save time between tests and for portability between homeservers
|
||||
const { chunk: rooms } = await this.csApi.request<{
|
||||
chunk: { room_id: string }[];
|
||||
}>("GET", "/v3/publicRooms", token, {});
|
||||
await Promise.all(
|
||||
rooms.map((room) =>
|
||||
this.csApi.request("PUT", `/v3/directory/list/room/${room.room_id}`, token, { visibility: "private" }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async registerUserInternal(
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
admin = false,
|
||||
): Promise<Credentials> {
|
||||
const path = "/v1/register";
|
||||
const { nonce } = await this.adminApi.request<{ nonce: string }>("GET", path, undefined, {});
|
||||
const mac = crypto
|
||||
.createHmac("sha1", this.registrationSharedSecret)
|
||||
.update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`)
|
||||
.digest("hex");
|
||||
const data = await this.adminApi.request<{
|
||||
home_server: string;
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
}>("POST", path, undefined, {
|
||||
nonce,
|
||||
username,
|
||||
password,
|
||||
mac,
|
||||
admin,
|
||||
displayname: displayName,
|
||||
});
|
||||
|
||||
return {
|
||||
homeServer: data.home_server || data.user_id.split(":").slice(1).join(":"),
|
||||
accessToken: data.access_token,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
password,
|
||||
displayName,
|
||||
username,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getAdminToken(): Promise<string> {
|
||||
if (this.adminTokenPromise === undefined) {
|
||||
this.adminTokenPromise = this.registerUserInternal(
|
||||
"admin",
|
||||
"totalyinsecureadminpassword",
|
||||
undefined,
|
||||
true,
|
||||
).then((res) => res.accessToken);
|
||||
}
|
||||
return this.adminTokenPromise;
|
||||
}
|
||||
|
||||
private async adminRequest<R extends {}>(verb: "GET", path: string, data?: never): Promise<R>;
|
||||
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R>;
|
||||
private async adminRequest<R extends {}>(verb: Verb, path: string, data?: object): Promise<R> {
|
||||
const adminToken = await this.getAdminToken();
|
||||
return this.adminApi.request(verb, path, adminToken, data);
|
||||
}
|
||||
|
||||
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
||||
return this.registerUserInternal(username, password, displayName, false);
|
||||
}
|
||||
|
||||
public async loginUser(userId: string, password: string): Promise<Credentials> {
|
||||
return this.csApi.loginUser(userId, password);
|
||||
}
|
||||
|
||||
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
|
||||
await this.adminRequest("PUT", `/v2/users/${userId}`, {
|
||||
threepids: [
|
||||
{
|
||||
medium,
|
||||
address,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class StartedSynapseWithMasContainer extends StartedSynapseContainer {
|
||||
constructor(
|
||||
container: StartedTestContainer,
|
||||
baseUrl: string,
|
||||
registrationSharedSecret: string,
|
||||
private readonly mas: StartedMatrixAuthenticationServiceContainer,
|
||||
) {
|
||||
super(container, baseUrl, registrationSharedSecret);
|
||||
}
|
||||
|
||||
protected async getAdminToken(): Promise<string> {
|
||||
if (this.adminTokenPromise === undefined) {
|
||||
this.adminTokenPromise = this.mas.getAdminToken();
|
||||
}
|
||||
return this.adminTokenPromise;
|
||||
}
|
||||
|
||||
public registerUser(username: string, password: string, displayName?: string): Promise<Credentials> {
|
||||
return this.mas.registerUser(username, password, displayName);
|
||||
}
|
||||
|
||||
public async setThreepid(userId: string, medium: string, address: string): Promise<void> {
|
||||
return this.mas.setThreepid(userId, medium, address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
@import "./components/views/settings/devices/_FilteredDeviceListHeader.pcss";
|
||||
@import "./components/views/settings/devices/_SecurityRecommendations.pcss";
|
||||
@import "./components/views/settings/devices/_SelectableDeviceTile.pcss";
|
||||
@import "./components/views/settings/encryption/_KeyStoragePanel.pcss";
|
||||
@import "./components/views/settings/shared/_SettingsSubsection.pcss";
|
||||
@import "./components/views/settings/shared/_SettingsSubsectionHeading.pcss";
|
||||
@import "./components/views/spaces/_QuickThemeSwitcher.pcss";
|
||||
@@ -269,9 +270,13 @@
|
||||
@import "./views/right_panel/_VerificationPanel.pcss";
|
||||
@import "./views/right_panel/_WidgetCard.pcss";
|
||||
@import "./views/room_settings/_AliasSettings.pcss";
|
||||
@import "./views/rooms/RoomListView/_RoomListHeaderView.pcss";
|
||||
@import "./views/rooms/RoomListView/_RoomListSearch.pcss";
|
||||
@import "./views/rooms/RoomListView/_RoomListView.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomList.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListHeaderView.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListItemMenuView.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListItemView.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListPanel.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListPrimaryFilters.pcss";
|
||||
@import "./views/rooms/RoomListPanel/_RoomListSearch.pcss";
|
||||
@import "./views/rooms/_AppsDrawer.pcss";
|
||||
@import "./views/rooms/_Autocomplete.pcss";
|
||||
@import "./views/rooms/_AuxPanel.pcss";
|
||||
@@ -289,6 +294,7 @@
|
||||
@import "./views/rooms/_IRCLayout.pcss";
|
||||
@import "./views/rooms/_InvitedIconView.pcss";
|
||||
@import "./views/rooms/_JumpToBottomButton.pcss";
|
||||
@import "./views/rooms/_LegacyRoomList.pcss";
|
||||
@import "./views/rooms/_LegacyRoomListHeader.pcss";
|
||||
@import "./views/rooms/_LinkPreviewGroup.pcss";
|
||||
@import "./views/rooms/_LinkPreviewWidget.pcss";
|
||||
@@ -313,7 +319,6 @@
|
||||
@import "./views/rooms/_RoomHeader.pcss";
|
||||
@import "./views/rooms/_RoomInfoLine.pcss";
|
||||
@import "./views/rooms/_RoomKnocksBar.pcss";
|
||||
@import "./views/rooms/_RoomList.pcss";
|
||||
@import "./views/rooms/_RoomPreviewBar.pcss";
|
||||
@import "./views/rooms/_RoomPreviewCard.pcss";
|
||||
@import "./views/rooms/_RoomSearchAuxPanel.pcss";
|
||||
@@ -362,6 +367,7 @@
|
||||
@import "./views/settings/encryption/_EncryptionCard.pcss";
|
||||
@import "./views/settings/encryption/_EncryptionCardEmphasisedContent.pcss";
|
||||
@import "./views/settings/encryption/_RecoveryPanelOutOfSync.pcss";
|
||||
@import "./views/settings/encryption/_ResetIdentityPanel.pcss";
|
||||
@import "./views/settings/tabs/_SettingsBanner.pcss";
|
||||
@import "./views/settings/tabs/_SettingsIndent.pcss";
|
||||
@import "./views/settings/tabs/_SettingsSection.pcss";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.mx_KeyStoragePanel_toggleRow {
|
||||
flex-direction: row;
|
||||
}
|
||||
@@ -12,4 +12,5 @@ Please see LICENSE files in the repository root for full details.
|
||||
align-items: var(--mx-flex-align, unset);
|
||||
justify-content: var(--mx-flex-justify, unset);
|
||||
gap: var(--mx-flex-gap, unset);
|
||||
flex-wrap: var(--mx-flex-wrap, unset);
|
||||
}
|
||||
|
||||
@@ -100,3 +100,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_RoomSummaryCard_roomName {
|
||||
margin: $spacing-12 0 $spacing-4;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_leave {
|
||||
margin: 0 0 var(--cpd-space-8x);
|
||||
}
|
||||
|
||||
15
res/css/views/rooms/RoomListPanel/_RoomList.pcss
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.mx_RoomList {
|
||||
height: 100%;
|
||||
|
||||
.mx_RoomList_List {
|
||||
/* Avoid when on hover, the background color to be on top of the right border */
|
||||
padding-right: 1px;
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,19 @@
|
||||
*/
|
||||
|
||||
.mx_RoomListHeaderView {
|
||||
height: 60px;
|
||||
flex: 0 0 60px;
|
||||
padding: 0 var(--cpd-space-3x);
|
||||
|
||||
h1 {
|
||||
all: unset;
|
||||
font: var(--cpd-font-heading-sm-semibold);
|
||||
.mx_RoomListHeaderView_title {
|
||||
min-width: 0;
|
||||
|
||||
h1 {
|
||||
all: unset;
|
||||
font: var(--cpd-font-heading-sm-semibold);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
12
res/css/views/rooms/RoomListPanel/_RoomListItemMenuView.pcss
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.mx_RoomListItemMenuView {
|
||||
svg {
|
||||
fill: var(--cpd-color-icon-primary);
|
||||
}
|
||||
}
|
||||
49
res/css/views/rooms/RoomListPanel/_RoomListItemView.pcss
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The RoomListItemView has the following structure:
|
||||
* button----------------------------------------|
|
||||
* | <-12px-> container--------------------------|
|
||||
* | | room avatar <-12px-> content-----|
|
||||
* | | | room_name |
|
||||
* | | | ----------| <-- border
|
||||
* |---------------------------------------------|
|
||||
*/
|
||||
.mx_RoomListItemView {
|
||||
all: unset;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_container {
|
||||
padding-left: var(--cpd-space-3x);
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
height: 100%;
|
||||
|
||||
.mx_RoomListItemView_content {
|
||||
padding-right: var(--cpd-space-3x);
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
/* The border is only under the room name and the future hover menu */
|
||||
border-bottom: var(--cpd-border-width-0-5) solid var(--cpd-color-bg-subtle-secondary);
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
|
||||
span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomListItemView_menu_open {
|
||||
background-color: var(--cpd-color-bg-action-secondary-hovered);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_RoomListView {
|
||||
.mx_RoomListPanel {
|
||||
background-color: var(--cpd-color-bg-canvas-default);
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--cpd-color-bg-subtle-primary);
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.mx_RoomListPrimaryFilters {
|
||||
margin: unset;
|
||||
list-style-type: none;
|
||||
padding: var(--cpd-space-2x) var(--cpd-space-3x);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
.mx_RoomListSearch {
|
||||
/* From figma, this should be aligned with the room header */
|
||||
height: 64px;
|
||||
flex: 0 0 64px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
|
||||
padding: 0 var(--cpd-space-3x);
|
||||