mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
27 Commits
v1.12.1-rc
...
hs/add-cus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eb5e8965a | ||
|
|
6d2f1c2e9a | ||
|
|
f44308b9a9 | ||
|
|
bba40ca706 | ||
|
|
706b33fcf4 | ||
|
|
66e73818a8 | ||
|
|
d97d999ef2 | ||
|
|
294857209d | ||
|
|
391bd15258 | ||
|
|
42edbab715 | ||
|
|
5aee224169 | ||
|
|
ff986e4317 | ||
|
|
c6d4f38a04 | ||
|
|
62f62601ef | ||
|
|
e60a68ea1a | ||
|
|
ec13bdc910 | ||
|
|
9136d841ee | ||
|
|
f740dc3829 | ||
|
|
ce428b5e2d | ||
|
|
757e4e1395 | ||
|
|
1b2d9b392c | ||
|
|
1c5bc4a7be | ||
|
|
1ed3f205f3 | ||
|
|
afab6c29dc | ||
|
|
4d81b36270 | ||
|
|
3281a4128f | ||
|
|
213a191b8c |
@@ -1,11 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||||
extends: [
|
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||||
"plugin:matrix-org/babel",
|
|
||||||
"plugin:matrix-org/react",
|
|
||||||
"plugin:matrix-org/a11y",
|
|
||||||
"plugin:storybook/recommended",
|
|
||||||
],
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.json"],
|
project: ["./tsconfig.json"],
|
||||||
},
|
},
|
||||||
|
|||||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@@ -17,16 +17,9 @@
|
|||||||
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
||||||
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||||
|
|
||||||
|
|
||||||
/src/models/Call.ts @element-hq/element-call-reviewers
|
|
||||||
/src/call-types.ts @element-hq/element-call-reviewers
|
|
||||||
/src/components/views/voip @element-hq/element-call-reviewers
|
|
||||||
/playwright/e2e/voip/element-call.spec.ts @element-hq/element-call-reviewers
|
|
||||||
|
|
||||||
# Ignore translations as those will be updated by GHA for Localazy download
|
# Ignore translations as those will be updated by GHA for Localazy download
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||||
# Ignore the synapse & mas plugins as this is updated by GHA for docker image updating
|
# Ignore the synapse plugin as this is updated by GHA for docker image updating
|
||||||
/playwright/testcontainers/synapse.ts
|
/playwright/testcontainers/synapse.ts
|
||||||
/playwright/testcontainers/mas.ts
|
|
||||||
|
|
||||||
|
|||||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] I have read through [review guidelines](../docs/review.md) and [CONTRIBUTING.md](../CONTRIBUTING.md).
|
|
||||||
- [ ] Tests written for new code (and old code if feasible).
|
- [ ] Tests written for new code (and old code if feasible).
|
||||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||||
- [ ] Linter and other CI checks pass.
|
- [ ] Linter and other CI checks pass.
|
||||||
|
|||||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -10,7 +10,8 @@ concurrency:
|
|||||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
# develop pushes and repository_dispatch handled in build_develop.yaml
|
# develop pushes and repository_dispatch handled in build_develop.yaml
|
||||||
env:
|
env:
|
||||||
# This must be set for fetchdep.sh to get the right branch
|
# These must be set for fetchdep.sh to get the right branch
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
permissions: {} # No permissions required
|
permissions: {} # No permissions required
|
||||||
jobs:
|
jobs:
|
||||||
@@ -42,9 +43,9 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
# Disable cache on Windows as it is slower than not caching
|
# Disable cache on Windows as it is slower than not caching
|
||||||
# https://github.com/actions/setup-node/issues/975
|
# https://github.com/actions/setup-node/issues/975
|
||||||
@@ -55,7 +56,15 @@ jobs:
|
|||||||
- run: yarn config set network-timeout 300000
|
- run: yarn config set network-timeout 300000
|
||||||
|
|
||||||
- name: Fetch layered build
|
- name: Fetch layered build
|
||||||
run: ./scripts/layered.sh
|
id: layered_build
|
||||||
|
env:
|
||||||
|
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||||
|
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||||
|
run: |
|
||||||
|
scripts/layered.sh
|
||||||
|
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||||
|
VECTOR_SHA=$(git rev-parse --short=12 HEAD)
|
||||||
|
echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Copy config
|
- name: Copy config
|
||||||
run: cp element.io/develop/config.json config.json
|
run: cp element.io/develop/config.json config.json
|
||||||
@@ -63,7 +72,9 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
CI_PACKAGE: true
|
CI_PACKAGE: true
|
||||||
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
|
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||||
|
run: |
|
||||||
|
yarn build
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
|||||||
2
.github/workflows/build_debian.yaml
vendored
2
.github/workflows/build_debian.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
VERSION: ${{ github.ref_name }}
|
VERSION: ${{ github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Download package
|
- name: Download package
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
.github/workflows/build_develop.yml
vendored
4
.github/workflows/build_develop.yml
vendored
@@ -26,9 +26,9 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|||||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Load GPG key
|
- name: Load GPG key
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
29
.github/workflows/docker.yaml
vendored
29
.github/workflows/docker.yaml
vendored
@@ -20,31 +20,31 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TEST_TAG: vectorim/element-web:test
|
TEST_TAG: vectorim/element-web:test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
|
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
with:
|
with:
|
||||||
install: true
|
install: true
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and load
|
- name: Build and load
|
||||||
id: test-build
|
id: test-build
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
load: true
|
load: true
|
||||||
@@ -96,7 +96,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -139,16 +139,3 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
repository: vectorim/element-web
|
repository: vectorim/element-web
|
||||||
|
|
||||||
- name: Repository Dispatch
|
|
||||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
repository: element-hq/element-web-pro
|
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
|
||||||
event-type: image-built
|
|
||||||
# Stable way to determine the :version
|
|
||||||
client-payload: |-
|
|
||||||
{
|
|
||||||
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
|
|
||||||
}
|
|
||||||
|
|||||||
10
.github/workflows/docs.yml
vendored
10
.github/workflows/docs.yml
vendored
@@ -17,23 +17,23 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Fetch element-desktop
|
- name: Fetch element-desktop
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-desktop
|
repository: element-hq/element-desktop
|
||||||
path: element-desktop
|
path: element-desktop
|
||||||
|
|
||||||
- name: Fetch element-web
|
- name: Fetch element-web
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
path: element-web
|
path: element-web
|
||||||
|
|
||||||
- name: Fetch matrix-js-sdk
|
- name: Fetch matrix-js-sdk
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
path: matrix-js-sdk
|
path: matrix-js-sdk
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: element-web/yarn.lock
|
cache-dependency-path: element-web/yarn.lock
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
run: mdbook build
|
run: mdbook build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||||
with:
|
with:
|
||||||
path: ./book
|
path: ./book
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Download HTML report
|
- name: Download HTML report
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|||||||
33
.github/workflows/end-to-end-tests.yaml
vendored
33
.github/workflows/end-to-end-tests.yaml
vendored
@@ -50,20 +50,25 @@ jobs:
|
|||||||
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|
||||||
- name: Fetch layered build
|
- name: Fetch layered build
|
||||||
|
id: layered_build
|
||||||
env:
|
env:
|
||||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||||
run: scripts/layered.sh
|
run: |
|
||||||
|
scripts/layered.sh
|
||||||
|
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||||
|
VECTOR_SHA=$(git rev-parse --short=12 HEAD)
|
||||||
|
echo "VERSION=$VECTOR_SHA--js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Copy config
|
- name: Copy config
|
||||||
run: cp element.io/develop/config.json config.json
|
run: cp element.io/develop/config.json config.json
|
||||||
@@ -71,7 +76,9 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
CI_PACKAGE: true
|
CI_PACKAGE: true
|
||||||
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
|
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||||
|
run: |
|
||||||
|
yarn build
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
@@ -82,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Calculate runner variables
|
- name: Calculate runner variables
|
||||||
id: runner-vars
|
id: runner-vars
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
||||||
@@ -122,18 +129,18 @@ jobs:
|
|||||||
- runAllTests: false
|
- runAllTests: false
|
||||||
project: Pinecone
|
project: Pinecone
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: webapp
|
path: webapp
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
@@ -147,7 +154,7 @@ jobs:
|
|||||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/ms-playwright
|
path: ~/.cache/ms-playwright
|
||||||
@@ -194,13 +201,13 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -212,7 +219,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
pattern: all-blob-reports-*
|
pattern: all-blob-reports-*
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
@@ -220,7 +227,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Merge into HTML Report
|
- name: Merge into HTML Report
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
|
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports
|
||||||
env:
|
env:
|
||||||
# Only pass creds to the flaky-reporter on main branch runs
|
# Only pass creds to the flaky-reporter on main branch runs
|
||||||
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
||||||
|
|||||||
4
.github/workflows/issue_closed.yml
vendored
4
.github/workflows/issue_closed.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
name: Tidy closed issues
|
name: Tidy closed issues
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
id: main
|
id: main
|
||||||
with:
|
with:
|
||||||
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
||||||
@@ -142,7 +142,7 @@ jobs:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
name: Close duplicate as Not Planned
|
name: Close duplicate as Not Planned
|
||||||
if: steps.main.outputs.closeAsNotPlanned
|
if: steps.main.outputs.closeAsNotPlanned
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/netlify.yaml
vendored
2
.github/workflows/netlify.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
Exercise caution. Use test accounts.
|
Exercise caution. Use test accounts.
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
|||||||
2
.github/workflows/pending-reviews.yaml
vendored
2
.github/workflows/pending-reviews.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
||||||
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||||
ROOM_ID: ${{ secrets.ROOM_ID }}
|
ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||||
|
|||||||
11
.github/workflows/playwright-image-updates.yaml
vendored
11
.github/workflows/playwright-image-updates.yaml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Update synapse image
|
- name: Update synapse image
|
||||||
run: |
|
run: |
|
||||||
@@ -21,15 +21,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IMAGE: ghcr.io/element-hq/synapse:develop
|
IMAGE: ghcr.io/element-hq/synapse:develop
|
||||||
|
|
||||||
- name: Update MAS image
|
|
||||||
run: |
|
|
||||||
docker pull "$IMAGE"
|
|
||||||
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
|
||||||
DIGEST=${INSPECT#*@}
|
|
||||||
sed -i "s/const TAG.*/const TAG = \"main@$DIGEST\";/" playwright/testcontainers/mas.ts
|
|
||||||
env:
|
|
||||||
IMAGE: ghcr.io/element-hq/matrix-authentication-service:main
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
name: Check PR base branch
|
name: Check PR base branch
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const baseBranch = context.payload.pull_request.base.ref;
|
const baseBranch = context.payload.pull_request.base.ref;
|
||||||
|
|||||||
6
.github/workflows/release_prepare.yml
vendored
6
.github/workflows/release_prepare.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
REPOS: matrix-js-sdk element-web element-desktop
|
REPOS: matrix-js-sdk element-web element-desktop
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Element Desktop
|
- name: Checkout Element Desktop
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.element-desktop
|
if: inputs.element-desktop
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-desktop
|
repository: element-hq/element-desktop
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
- name: Checkout Element Web
|
- name: Checkout Element Web
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.element-web
|
if: inputs.element-web
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
- name: Checkout Matrix JS SDK
|
- name: Checkout Matrix JS SDK
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.matrix-js-sdk
|
if: inputs.matrix-js-sdk
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# Triggers after the shared component tests have finished,
|
|
||||||
# It uploads the received images and diffs to netlify, printing the URLs to the console
|
|
||||||
name: Upload Shared Component Visual Test Diffs
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Shared Component Visual Tests"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
|
||||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
report:
|
|
||||||
if: github.event.workflow_run.conclusion == 'failure'
|
|
||||||
name: Upload Diffs
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
environment: Netlify
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
deployments: write
|
|
||||||
steps:
|
|
||||||
- name: Install tree
|
|
||||||
run: "sudo apt-get install -y tree"
|
|
||||||
|
|
||||||
- name: Download Diffs
|
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
name: received-images
|
|
||||||
path: received-images
|
|
||||||
|
|
||||||
- name: Generate Index
|
|
||||||
run: "cd received-images && tree -L 1 --noreport -H '' -o index.html ."
|
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
|
||||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
|
||||||
with:
|
|
||||||
path: received-images
|
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
|
||||||
branch: ${{ github.event.workflow_run.head_branch }}
|
|
||||||
revision: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site_id: ${{ vars.NETLIFY_SITE_ID }}
|
|
||||||
desc: Shared Component Visual Diffs
|
|
||||||
deployment_env: SharedComponentDiffs
|
|
||||||
prefix: "diffs-"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
name: Shared Component Visual Tests
|
|
||||||
on:
|
|
||||||
pull_request: {}
|
|
||||||
merge_group:
|
|
||||||
types: [checks_requested]
|
|
||||||
push:
|
|
||||||
branches: [develop, master]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions: {} # No permissions required
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testStorybook:
|
|
||||||
name: "Run Visual Tests"
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
issues: read
|
|
||||||
pull-requests: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
repository: element-hq/element-web
|
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
||||||
with:
|
|
||||||
cache: "yarn"
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Get installed Playwright version
|
|
||||||
id: playwright
|
|
||||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: ~/.cache/ms-playwright
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: "yarn playwright install --with-deps --only-shell"
|
|
||||||
|
|
||||||
- name: Build Element Web resources
|
|
||||||
# Needed to prepare language files
|
|
||||||
run: "yarn build:res"
|
|
||||||
|
|
||||||
- name: Build storybook dependencies
|
|
||||||
# When the first test is ran, it will fail because the dependencies are not yet built.
|
|
||||||
# This step is to ensure that the dependencies are built before running the tests.
|
|
||||||
run: "yarn test:storybook:ci"
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Run Visual tests
|
|
||||||
run: "yarn test:storybook:ci"
|
|
||||||
|
|
||||||
- name: Upload received images & diffs
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: received-images
|
|
||||||
path: playwright/shared-component-received
|
|
||||||
26
.github/workflows/static_analysis.yaml
vendored
26
.github/workflows/static_analysis.yaml
vendored
@@ -12,7 +12,8 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# This must be set for fetchdep.sh to get the right branch
|
# These must be set for fetchdep.sh to get the right branch
|
||||||
|
REPOSITORY: ${{ github.repository }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
permissions: {} # No permissions required
|
permissions: {} # No permissions required
|
||||||
@@ -22,9 +23,9 @@ jobs:
|
|||||||
name: "Typescript Syntax Check"
|
name: "Typescript Syntax Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -51,13 +52,12 @@ jobs:
|
|||||||
error|misconfigured
|
error|misconfigured
|
||||||
welcome_to_element
|
welcome_to_element
|
||||||
devtools|settings|elementCallUrl
|
devtools|settings|elementCallUrl
|
||||||
labs|sliding_sync_description
|
|
||||||
|
|
||||||
rethemendex_lint:
|
rethemendex_lint:
|
||||||
name: "Rethemendex Check"
|
name: "Rethemendex Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- run: ./res/css/rethemendex.sh
|
- run: ./res/css/rethemendex.sh
|
||||||
|
|
||||||
@@ -67,9 +67,9 @@ jobs:
|
|||||||
name: "ESLint"
|
name: "ESLint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -85,9 +85,9 @@ jobs:
|
|||||||
name: "Style Lint"
|
name: "Style Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -103,9 +103,9 @@ jobs:
|
|||||||
name: "Workflow Lint"
|
name: "Workflow Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -121,9 +121,9 @@ jobs:
|
|||||||
name: "Analyse Dead Code"
|
name: "Analyse Dead Code"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|||||||
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -39,12 +39,12 @@ jobs:
|
|||||||
runner: [1, 2]
|
runner: [1, 2]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||||
|
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||||
|
|
||||||
- name: Jest Cache
|
- name: Jest Cache
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/jest_cache
|
path: /tmp/jest_cache
|
||||||
key: ${{ hashFiles('**/yarn.lock') }}
|
key: ${{ hashFiles('**/yarn.lock') }}
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Skip SonarCloud in merge queue
|
- name: Skip SonarCloud in merge queue
|
||||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||||
uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
|
uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
2
.github/workflows/triage-assigned.yml
vendored
2
.github/workflows/triage-assigned.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
contains(github.event.issue.assignees.*.login, 'dbkr') ||
|
contains(github.event.issue.assignees.*.login, 'dbkr') ||
|
||||||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/67
|
project-url: https://github.com/orgs/element-hq/projects/67
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/triage-incoming.yml
vendored
2
.github/workflows/triage-incoming.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
automate-project-columns:
|
automate-project-columns:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/120
|
project-url: https://github.com/orgs/element-hq/projects/120
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
16
.github/workflows/triage-labelled.yml
vendored
16
.github/workflows/triage-labelled.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
||||||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -112,7 +112,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
contains(github.event.issue.labels.*.name, 'O-Frequent') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A11y'))
|
contains(github.event.issue.labels.*.name, 'A11y'))
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/18
|
project-url: https://github.com/orgs/element-hq/projects/18
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
|
contains(github.event.issue.labels.*.name, 'X-Needs-Product')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/28
|
project-url: https://github.com/orgs/element-hq/projects/28
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
|
contains(github.event.issue.labels.*.name, 'A-New-Search-Experience')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/48
|
project-url: https://github.com/orgs/element-hq/projects/48
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -145,7 +145,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'Team: VoIP')
|
contains(github.event.issue.labels.*.name, 'Team: VoIP')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/41
|
project-url: https://github.com/orgs/element-hq/projects/41
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -156,7 +156,7 @@ jobs:
|
|||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'Team: Crypto')
|
contains(github.event.issue.labels.*.name, 'Team: Crypto')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/76
|
project-url: https://github.com/orgs/element-hq/projects/76
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -172,7 +172,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Testing') ||
|
contains(github.event.issue.labels.*.name, 'A-Testing') ||
|
||||||
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/101
|
project-url: https://github.com/orgs/element-hq/projects/101
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
name: Move PRs asking for design review to the design board
|
name: Move PRs asking for design review to the design board
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: find_team_members
|
id: find_team_members
|
||||||
with:
|
with:
|
||||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
TEAM: "design"
|
TEAM: "design"
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: add_to_project
|
id: add_to_project
|
||||||
if: steps.any_matching_reviewers.outputs.match == 'true'
|
if: steps.any_matching_reviewers.outputs.match == 'true'
|
||||||
with:
|
with:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
name: Move PRs asking for design review to the design board
|
name: Move PRs asking for design review to the design board
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: find_team_members
|
id: find_team_members
|
||||||
with:
|
with:
|
||||||
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
headers: '{"GraphQL-Features": "projects_next_graphql"}'
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
TEAM: "product"
|
TEAM: "product"
|
||||||
- uses: octokit/graphql-action@8ad880e4d437783ea2ab17010324de1075228110 # v2.3.2
|
- uses: octokit/graphql-action@v2.x
|
||||||
id: add_to_project
|
id: add_to_project
|
||||||
if: steps.any_matching_reviewers.outputs.match == 'true'
|
if: steps.any_matching_reviewers.outputs.match == 'true'
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/triage-stale.yml
vendored
6
.github/workflows/triage-stale.yml
vendored
@@ -12,17 +12,15 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
|
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||||
with:
|
with:
|
||||||
operations-per-run: 100
|
operations-per-run: 100
|
||||||
|
|
||||||
# Flaky test issue closing
|
# Flaky test issue closing
|
||||||
any-of-issue-labels: "Z-Flaky-Test-Chrome,Z-Flaky-Test-Firefox,Z-Flaky-Test-Webkit"
|
only-issue-labels: "Z-Flaky-Test"
|
||||||
days-before-issue-stale: 14
|
days-before-issue-stale: 14
|
||||||
days-before-issue-close: 0
|
days-before-issue-close: 0
|
||||||
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
||||||
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
||||||
|
|
||||||
# Stale PR closing
|
# Stale PR closing
|
||||||
days-before-pr-stale: 180
|
days-before-pr-stale: 180
|
||||||
days-before-pr-close: 0
|
days-before-pr-close: 0
|
||||||
|
|||||||
51
.github/workflows/triage-unlabelled.yml
vendored
51
.github/workflows/triage-unlabelled.yml
vendored
@@ -5,25 +5,44 @@ on:
|
|||||||
types: [unlabeled]
|
types: [unlabeled]
|
||||||
permissions: {}
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
move_no_longer_needs_info_issues:
|
Move_Unabeled_Issue_On_Project_Board:
|
||||||
name: Move no longer X-Needs-Info issues to Triaged
|
name: Move no longer X-Needs-Info issues to Triaged
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
repository-projects: read
|
||||||
if: >
|
if: >
|
||||||
!contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
${{
|
||||||
steps:
|
!contains(github.event.issue.labels.*.name, 'X-Needs-Info') }}
|
||||||
- id: set_fields
|
|
||||||
uses: nipe0324/update-project-v2-item-field@c4af58452d1c5a788c1ea4f20e073fa722ec4a6b #v2.0.2
|
|
||||||
with:
|
|
||||||
project-url: ${{ env.PROJECT_URL }}
|
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
|
||||||
skip-update-script: |
|
|
||||||
const isIssue = item.type === 'ISSUE'
|
|
||||||
const status = item.fieldValues['Status']
|
|
||||||
return !isIssue || status !== 'Needs info'
|
|
||||||
field-name: Status
|
|
||||||
field-value: "Triaged"
|
|
||||||
env:
|
env:
|
||||||
PROJECT_URL: https://github.com/orgs/element-hq/projects/120
|
BOARD_NAME: "Issue triage"
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
REPO: ${{ github.event.repository.name }}
|
||||||
|
ISSUE: ${{ github.event.issue.number }}
|
||||||
|
steps:
|
||||||
|
- name: Check if issue is already in "${{ env.BOARD_NAME }}"
|
||||||
|
run: |
|
||||||
|
json=$(curl -s -H 'Content-Type: application/json' -H "Authorization: bearer ${{ secrets.GITHUB_TOKEN }}" -X POST -d '{"query": "query($issue: Int!, $owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { issue(number: $issue) { projectCards { nodes { project { name } isArchived } } } } } ", "variables" : "{ \"issue\": '${ISSUE}', \"owner\": \"'${OWNER}'\", \"repo\": \"'${REPO}'\" }" }' https://api.github.com/graphql)
|
||||||
|
if echo $json | jq '.data.repository.issue.projectCards.nodes | length'; then
|
||||||
|
if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].project.name') =~ "${BOARD_NAME}" ]]; then
|
||||||
|
if [[ $(echo $json | jq '.data.repository.issue.projectCards.nodes[0].isArchived') == 'true' ]]; then
|
||||||
|
echo "Issue is already in Project '$BOARD_NAME', but is archived - skipping workflow";
|
||||||
|
echo "SKIP_ACTION=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "Issue is already in Project '$BOARD_NAME', proceeding";
|
||||||
|
echo "ALREADY_IN_BOARD=true" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Issue is not in project '$BOARD_NAME', cancelling this workflow"
|
||||||
|
echo "ALREADY_IN_BOARD=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
- name: Move issue
|
||||||
|
uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36
|
||||||
|
if: ${{ env.ALREADY_IN_BOARD == 'true' && env.SKIP_ACTION != 'true' }}
|
||||||
|
with:
|
||||||
|
project: Issue triage
|
||||||
|
column: Triaged
|
||||||
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
remove_Z-Labs_label:
|
remove_Z-Labs_label:
|
||||||
name: Remove Z-Labs label when features behind labs flags are removed
|
name: Remove Z-Labs label when features behind labs flags are removed
|
||||||
@@ -43,7 +62,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
||||||
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.removeLabel({
|
github.rest.issues.removeLabel({
|
||||||
|
|||||||
4
.github/workflows/update-jitsi.yml
vendored
4
.github/workflows/update-jitsi.yml
vendored
@@ -9,9 +9,9 @@ jobs:
|
|||||||
update:
|
update:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|||||||
9
.github/workflows/update-topics.yaml
vendored
9
.github/workflows/update-topics.yaml
vendored
@@ -22,11 +22,11 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
environment: Matrix
|
environment: Matrix
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||||
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||||
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
|
PUBLIC_ROOM_ID: "!YTvKGNlinIzlkMTVRl:matrix.org"
|
||||||
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
|
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
|
||||||
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
|
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
|
||||||
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
|
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
|
||||||
@@ -81,11 +81,6 @@ jobs:
|
|||||||
d.body = d.body.replace(regex, releaseTopic);
|
d.body = d.body.replace(regex, releaseTopic);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (data["m.topic"]) {
|
|
||||||
data["m.topic"].forEach(d => {
|
|
||||||
d.body = d.body.replace(regex, releaseTopic);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res = await fetch(apiUrl, {
|
res = await fetch(apiUrl, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,6 +31,3 @@ electron/pub
|
|||||||
/index.html
|
/index.html
|
||||||
# version file and tarball created by `npm pack` / `yarn pack`
|
# version file and tarball created by `npm pack` / `yarn pack`
|
||||||
/git-revision.txt
|
/git-revision.txt
|
||||||
|
|
||||||
*storybook.log
|
|
||||||
storybook-static
|
|
||||||
|
|||||||
@@ -1,28 +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 { create } from "storybook/theming";
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
base: "light",
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
textColor: "#1b1d22",
|
|
||||||
colorSecondary: "#111111",
|
|
||||||
|
|
||||||
// UI
|
|
||||||
appBg: "#ffffff",
|
|
||||||
appContentBg: "#ffffff",
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
barBg: "#ffffff",
|
|
||||||
|
|
||||||
brandTitle: "Element Web",
|
|
||||||
brandUrl: "https://github.com/element-hq/element-web",
|
|
||||||
brandImage: "https://element.io/images/logo-ele-secondary.svg",
|
|
||||||
brandTarget: "_self",
|
|
||||||
});
|
|
||||||
@@ -1,61 +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 { Addon, types, useGlobals } from "storybook/manager-api";
|
|
||||||
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
|
|
||||||
import React from "react";
|
|
||||||
import { GlobeIcon } from "@storybook/icons";
|
|
||||||
|
|
||||||
// We can't import `shared/i18n.tsx` directly here.
|
|
||||||
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
|
|
||||||
import json from "../webapp/i18n/languages.json";
|
|
||||||
const languages = Object.keys(json).filter((lang) => lang !== "default");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the title of a language in the user's locale.
|
|
||||||
*/
|
|
||||||
function languageTitle(language: string): string {
|
|
||||||
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const languageAddon: Addon = {
|
|
||||||
title: "Language Selector",
|
|
||||||
type: types.TOOL,
|
|
||||||
render: ({ active }) => {
|
|
||||||
const [globals, updateGlobals] = useGlobals();
|
|
||||||
const selectedLanguage = globals.language || "en";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WithTooltip
|
|
||||||
placement="top"
|
|
||||||
trigger="click"
|
|
||||||
closeOnOutsideClick
|
|
||||||
tooltip={({ onHide }) => {
|
|
||||||
return (
|
|
||||||
<TooltipLinkList
|
|
||||||
links={languages.map((language) => ({
|
|
||||||
id: language,
|
|
||||||
title: languageTitle(language),
|
|
||||||
active: selectedLanguage === language,
|
|
||||||
onClick: async () => {
|
|
||||||
// Update the global state with the selected language
|
|
||||||
updateGlobals({ language });
|
|
||||||
onHide();
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton title="Language">
|
|
||||||
<GlobeIcon />
|
|
||||||
{languageTitle(selectedLanguage)}
|
|
||||||
</IconButton>
|
|
||||||
</WithTooltip>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,40 +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 { StorybookConfig } from "@storybook/react-vite";
|
|
||||||
import path from "node:path";
|
|
||||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
||||||
import { mergeConfig } from "vite";
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
||||||
staticDirs: ["../webapp"],
|
|
||||||
addons: ["@storybook/addon-docs", "@storybook/addon-designs", "@storybook/addon-a11y"],
|
|
||||||
framework: "@storybook/react-vite",
|
|
||||||
core: {
|
|
||||||
disableTelemetry: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
reactDocgen: "react-docgen-typescript",
|
|
||||||
},
|
|
||||||
async viteFinal(config) {
|
|
||||||
return mergeConfig(config, {
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
// Alias used by i18n.tsx
|
|
||||||
$webapp: path.resolve("webapp"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Needed for counterpart to work
|
|
||||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
|
||||||
server: {
|
|
||||||
allowedHosts: ["localhost", ".docker.internal"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,18 +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 React from "react";
|
|
||||||
|
|
||||||
import { addons } from "storybook/manager-api";
|
|
||||||
import ElementTheme from "./ElementTheme";
|
|
||||||
import { languageAddon } from "./languageAddon";
|
|
||||||
|
|
||||||
addons.setConfig({
|
|
||||||
theme: ElementTheme,
|
|
||||||
});
|
|
||||||
|
|
||||||
addons.register("elementhq/language", () => addons.add("language", languageAddon));
|
|
||||||
@@ -1,10 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.docs-story {
|
|
||||||
background: var(--cpd-color-bg-canvas-default);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import type { ArgTypes, Preview, Decorator, ReactRenderer, StrictArgs } from "@storybook/react-vite";
|
|
||||||
|
|
||||||
import "../res/css/shared.pcss";
|
|
||||||
import "./preview.css";
|
|
||||||
import React, { useLayoutEffect } from "react";
|
|
||||||
import { setLanguage } from "../src/shared-components/utils/i18n";
|
|
||||||
import { TooltipProvider } from "@vector-im/compound-web";
|
|
||||||
import { StoryContext } from "storybook/internal/csf";
|
|
||||||
|
|
||||||
export const globalTypes = {
|
|
||||||
theme: {
|
|
||||||
name: "Theme",
|
|
||||||
description: "Global theme for components",
|
|
||||||
toolbar: {
|
|
||||||
icon: "circlehollow",
|
|
||||||
title: "Theme",
|
|
||||||
items: [
|
|
||||||
{ title: "System", value: "system", icon: "browser" },
|
|
||||||
{ title: "Light", value: "light", icon: "sun" },
|
|
||||||
{ title: "Light (high contrast)", value: "light-hc", icon: "sun" },
|
|
||||||
{ title: "Dark", value: "dark", icon: "moon" },
|
|
||||||
{ title: "Dark (high contrast)", value: "dark-hc", icon: "moon" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
name: "Language",
|
|
||||||
description: "Global language for components",
|
|
||||||
},
|
|
||||||
initialGlobals: {
|
|
||||||
theme: "system",
|
|
||||||
language: "en",
|
|
||||||
},
|
|
||||||
} satisfies ArgTypes;
|
|
||||||
|
|
||||||
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
|
|
||||||
|
|
||||||
const ThemeSwitcher: React.FC<{
|
|
||||||
theme: string;
|
|
||||||
}> = ({ theme }) => {
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
document.documentElement.classList.remove(...allThemesClasses);
|
|
||||||
if (theme !== "system") {
|
|
||||||
document.documentElement.classList.add(`cpd-theme-${theme}`);
|
|
||||||
}
|
|
||||||
return () => document.documentElement.classList.remove(...allThemesClasses);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withThemeProvider: Decorator = (Story, context) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ThemeSwitcher theme={context.globals.theme} />
|
|
||||||
<Story />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function languageLoader(context: StoryContext<ReactRenderer, StrictArgs>): Promise<void> {
|
|
||||||
await setLanguage(context.globals.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
const withTooltipProvider: Decorator = (Story) => {
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Story />
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
tags: ["autodocs"],
|
|
||||||
decorators: [withThemeProvider, withTooltipProvider],
|
|
||||||
parameters: {
|
|
||||||
options: {
|
|
||||||
storySort: {
|
|
||||||
method: "alphabetical",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
a11y: {
|
|
||||||
/*
|
|
||||||
* Configure test behavior
|
|
||||||
* See: https://storybook.js.org/docs/next/writing-tests/accessibility-testing#test-behavior
|
|
||||||
*/
|
|
||||||
test: "error",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
loaders: [languageLoader],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
@@ -1,37 +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 { waitForPageReady } from "@storybook/test-runner";
|
|
||||||
import { toMatchImageSnapshot } from "jest-image-snapshot";
|
|
||||||
|
|
||||||
const customSnapshotsDir = `${process.cwd()}/playwright/shared-component-snapshots/`;
|
|
||||||
const customReceivedDir = `${process.cwd()}/playwright/shared-component-received/`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import('@storybook/test-runner').TestRunnerConfig}
|
|
||||||
*/
|
|
||||||
const config = {
|
|
||||||
setup(page) {
|
|
||||||
expect.extend({ toMatchImageSnapshot });
|
|
||||||
},
|
|
||||||
async postVisit(page, context) {
|
|
||||||
await waitForPageReady(page);
|
|
||||||
|
|
||||||
// If you want to take screenshot of multiple browsers, use
|
|
||||||
// page.context().browser().browserType().name() to get the browser name to prefix the file name
|
|
||||||
const image = await page.screenshot();
|
|
||||||
expect(image).toMatchImageSnapshot({
|
|
||||||
customSnapshotsDir,
|
|
||||||
customSnapshotIdentifier: `${context.id}-${process.platform}`,
|
|
||||||
storeReceivedOnFailure: true,
|
|
||||||
customReceivedDir,
|
|
||||||
customDiffDir: customReceivedDir,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -70,13 +70,5 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"property-no-deprecated": [
|
|
||||||
true,
|
|
||||||
{
|
|
||||||
ignoreProperties: ["-webkit-box-orient", "word-wrap"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"nesting-selector-no-missing-scoping-root": null,
|
|
||||||
"no-invalid-position-declaration": null,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,3 @@ include:
|
|||||||
|
|
||||||
* Thom Cleary (https://github.com/thomcatdotrocks)
|
* Thom Cleary (https://github.com/thomcatdotrocks)
|
||||||
Small update for tarball deployment
|
Small update for tarball deployment
|
||||||
|
|
||||||
* Alexander (https://github.com/ioalexander)
|
|
||||||
Save image on CTRL + S shortcut
|
|
||||||
|
|||||||
190
CHANGELOG.md
190
CHANGELOG.md
@@ -1,193 +1,3 @@
|
|||||||
Changes in [1.12.0](https://github.com/element-hq/element-web/releases/tag/v1.12.0) (2025-09-23)
|
|
||||||
================================================================================================
|
|
||||||
## 🦖 Deprecations
|
|
||||||
|
|
||||||
* Remove remaining support for outdated .well-known settings ([#30702](https://github.com/element-hq/element-web/pull/30702)). Contributed by @richvdh.
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Add decline button to call notification toast (use new notification event) ([#30729](https://github.com/element-hq/element-web/pull/30729)). Contributed by @toger5.
|
|
||||||
* Use the new room list by default ([#30640](https://github.com/element-hq/element-web/pull/30640)). Contributed by @langleyd.
|
|
||||||
* "Verify this device" redesign ([#30596](https://github.com/element-hq/element-web/pull/30596)). Contributed by @uhoreg.
|
|
||||||
* Set Element Call "intents" when starting and answering DM calls. ([#30730](https://github.com/element-hq/element-web/pull/30730)). Contributed by @Half-Shot.
|
|
||||||
* Add axe compliance for new room list ([#30700](https://github.com/element-hq/element-web/pull/30700)). Contributed by @langleyd.
|
|
||||||
* Stop ringing and remove toast if another device answers a RTC call. ([#30728](https://github.com/element-hq/element-web/pull/30728)). Contributed by @Half-Shot.
|
|
||||||
* Automatically adjust history visibility when making a room private ([#30713](https://github.com/element-hq/element-web/pull/30713)). Contributed by @Half-Shot.
|
|
||||||
* Release announcement for new room list ([#30675](https://github.com/element-hq/element-web/pull/30675)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Room list: make the filter resize correctly ([#30795](https://github.com/element-hq/element-web/pull/30795)). Contributed by @RiotRobot.
|
|
||||||
* [Backport staging] Avoid flicker of the room list filter on resize ([#30794](https://github.com/element-hq/element-web/pull/30794)). Contributed by @RiotRobot.
|
|
||||||
* Don't show release announcements while toasts are displayed ([#30770](https://github.com/element-hq/element-web/pull/30770)). Contributed by @dbkr.
|
|
||||||
* Fix enabling key backup not working if there is an untrusted key backup ([#30707](https://github.com/element-hq/element-web/pull/30707)). Contributed by @Half-Shot.
|
|
||||||
* Force `preload` to be false when setting an intent on an Element Call. ([#30759](https://github.com/element-hq/element-web/pull/30759)). Contributed by @Half-Shot.
|
|
||||||
* Fix handling of 413 server response when uploading media ([#30737](https://github.com/element-hq/element-web/pull/30737)). Contributed by @hughns.
|
|
||||||
* Make landmark navigation work with new room list ([#30747](https://github.com/element-hq/element-web/pull/30747)). Contributed by @dbkr.
|
|
||||||
* Prevent voice message from displaying spurious errors ([#30736](https://github.com/element-hq/element-web/pull/30736)). Contributed by @florianduros.
|
|
||||||
* Align default avatar and fix colors in composer pills ([#30739](https://github.com/element-hq/element-web/pull/30739)). Contributed by @florianduros.
|
|
||||||
* Use configured URL for link to desktop app in message search settings ([#30742](https://github.com/element-hq/element-web/pull/30742)). Contributed by @t3chguy.
|
|
||||||
* Fix history visibility when creating space rooms ([#30745](https://github.com/element-hq/element-web/pull/30745)). Contributed by @dbkr.
|
|
||||||
* Check HTML-encoded quotes when handling translations for embedded pages (such as welcome.html) ([#30743](https://github.com/element-hq/element-web/pull/30743)). Contributed by @Half-Shot.
|
|
||||||
* Fix local room encryption status always not enabled ([#30461](https://github.com/element-hq/element-web/pull/30461)). Contributed by @BillCarsonFr.
|
|
||||||
* fix: make url in topic in room intro clickable ([#30686](https://github.com/element-hq/element-web/pull/30686)). Contributed by @florianduros.
|
|
||||||
* Block change recovery key button while a change is ongoing. ([#30664](https://github.com/element-hq/element-web/pull/30664)). Contributed by @Half-Shot.
|
|
||||||
* Hide advanced settings during room creation when `UIFeature.advancedSettings=false` ([#30684](https://github.com/element-hq/element-web/pull/30684)). Contributed by @florianduros.
|
|
||||||
* A11y: improve accessibility of pinned messages ([#30558](https://github.com/element-hq/element-web/pull/30558)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.112](https://github.com/element-hq/element-web/releases/tag/v1.11.112) (2025-09-16)
|
|
||||||
====================================================================================================
|
|
||||||
Fix [CVE-2025-59161](https://www.cve.org/CVERecord?id=CVE-2025-59161) / [GHSA-m6c8-98f4-75rr](https://github.com/element-hq/element-web/security/advisories/GHSA-m6c8-98f4-75rr)
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.111](https://github.com/element-hq/element-web/releases/tag/v1.11.111) (2025-09-10)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Do not hide media from your own user by default ([#29797](https://github.com/element-hq/element-web/pull/29797)). Contributed by @Half-Shot.
|
|
||||||
* Remember whether sidebar is shown for calls when switching rooms ([#30262](https://github.com/element-hq/element-web/pull/30262)). Contributed by @bojidar-bg.
|
|
||||||
* Open the proper integration settings on integrations disabled error ([#30538](https://github.com/element-hq/element-web/pull/30538)). Contributed by @Half-Shot.
|
|
||||||
* Show a "progress" dialog while invites are being sent ([#30561](https://github.com/element-hq/element-web/pull/30561)). Contributed by @richvdh.
|
|
||||||
* Move the room list to the new ListView(backed by react-virtuoso) ([#30515](https://github.com/element-hq/element-web/pull/30515)). Contributed by @langleyd.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Ensure container starts if it is mounted with an empty /modules directory. ([#30705](https://github.com/element-hq/element-web/pull/30705)). Contributed by @RiotRobot.
|
|
||||||
* Fix room joining over federation not specifying vias or using aliases ([#30641](https://github.com/element-hq/element-web/pull/30641)). Contributed by @t3chguy.
|
|
||||||
* Fix stable-suffixed MSC4133 support ([#30649](https://github.com/element-hq/element-web/pull/30649)). Contributed by @dbkr.
|
|
||||||
* Fix i18n of message when a setting is disabled ([#30646](https://github.com/element-hq/element-web/pull/30646)). Contributed by @dbkr.
|
|
||||||
* ListView should not handle the arrow keys if there is a modifier applied ([#30633](https://github.com/element-hq/element-web/pull/30633)). Contributed by @langleyd.
|
|
||||||
* Make BaseDialog's div keyboard focusable and fix test. ([#30631](https://github.com/element-hq/element-web/pull/30631)). Contributed by @langleyd.
|
|
||||||
* Fix: Allow triple-click text selection to flow around pills ([#30349](https://github.com/element-hq/element-web/pull/30349)). Contributed by @AlirezaMrtz.
|
|
||||||
* Watch for a 'join' action to know when the call is connected ([#29492](https://github.com/element-hq/element-web/pull/29492)). Contributed by @robintown.
|
|
||||||
* Fix: add missing tooltip and aria-label to lock icon next to composer ([#30623](https://github.com/element-hq/element-web/pull/30623)). Contributed by @florianduros.
|
|
||||||
* Don't render context menu when scrolling ([#30613](https://github.com/element-hq/element-web/pull/30613)). Contributed by @langleyd.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.110](https://github.com/element-hq/element-web/releases/tag/v1.11.110) (2025-08-27)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Hide recovery key when re-entering it while creating or changing it ([#30499](https://github.com/element-hq/element-web/pull/30499)). Contributed by @andybalaam.
|
|
||||||
* Add `?no_universal_links=true` to OIDC url so EX doesn't try to handle it ([#29439](https://github.com/element-hq/element-web/pull/29439)). Contributed by @t3chguy.
|
|
||||||
* Show a blue lock for unencrypted rooms and hide the grey shield for encrypted rooms ([#30440](https://github.com/element-hq/element-web/pull/30440)). Contributed by @langleyd.
|
|
||||||
* Add support for Module API 1.4 ([#30185](https://github.com/element-hq/element-web/pull/30185)). Contributed by @t3chguy.
|
|
||||||
* MVVM - Introduce some helpers for snapshot management ([#30398](https://github.com/element-hq/element-web/pull/30398)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* A11y: move focus to right panel when opened ([#30553](https://github.com/element-hq/element-web/pull/30553)). Contributed by @florianduros.
|
|
||||||
* Fix e2e warning icon should be white ([#30539](https://github.com/element-hq/element-web/pull/30539)). Contributed by @florianduros.
|
|
||||||
* Remove NoOneHere disabled reason. ([#30524](https://github.com/element-hq/element-web/pull/30524)). Contributed by @toger5.
|
|
||||||
* Fix downloading files with authenticated media API ([#30520](https://github.com/element-hq/element-web/pull/30520)). Contributed by @t3chguy.
|
|
||||||
* Fix call permissions check confusion around element call ([#30521](https://github.com/element-hq/element-web/pull/30521)). Contributed by @t3chguy.
|
|
||||||
* Fix line wrap around emoji verification ([#30523](https://github.com/element-hq/element-web/pull/30523)). Contributed by @t3chguy.
|
|
||||||
* Don't highlight redacted events ([#30519](https://github.com/element-hq/element-web/pull/30519)). Contributed by @t3chguy.
|
|
||||||
* Fix matrix.to links not being handled in the app ([#30522](https://github.com/element-hq/element-web/pull/30522)). Contributed by @t3chguy.
|
|
||||||
* Fix issue of new room list taking up the full width ([#30459](https://github.com/element-hq/element-web/pull/30459)). Contributed by @langleyd.
|
|
||||||
* Fix widget persistence in React development mode ([#30509](https://github.com/element-hq/element-web/pull/30509)). Contributed by @robintown.
|
|
||||||
* Fix widget initialization in React development mode ([#30463](https://github.com/element-hq/element-web/pull/30463)). Contributed by @robintown.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.109](https://github.com/element-hq/element-web/releases/tag/v1.11.109) (2025-08-11)
|
|
||||||
====================================================================================================
|
|
||||||
This release supports the upcoming v12 ("hydra") Matrix room version and is necessary to view and participate in these rooms.
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Allow /upgraderoom command without developer mode enabled ([#30529](https://github.com/element-hq/element-web/pull/30529)). Contributed by @RiotRobot.
|
|
||||||
* [Backport staging] Support for creator/owner power level ([#30526](https://github.com/element-hq/element-web/pull/30526)). Contributed by @RiotRobot.
|
|
||||||
* New room list: change icon and label of menu item for to start a DM ([#30470](https://github.com/element-hq/element-web/pull/30470)). Contributed by @florianduros.
|
|
||||||
* Implement the member list with virtuoso ([#29869](https://github.com/element-hq/element-web/pull/29869)). Contributed by @langleyd.
|
|
||||||
* Add labs option for history sharing on invite ([#30313](https://github.com/element-hq/element-web/pull/30313)). Contributed by @richvdh.
|
|
||||||
* Bump wysiwyg to 2.39.0 adding support for pasting rich text content in the Rich Text Edtior ([#30421](https://github.com/element-hq/element-web/pull/30421)). Contributed by @langleyd.
|
|
||||||
* Support `EventShieldReason.MISMATCHED_SENDER` ([#30403](https://github.com/element-hq/element-web/pull/30403)). Contributed by @richvdh.
|
|
||||||
* Change unencrypted and public pills to blue ([#30399](https://github.com/element-hq/element-web/pull/30399)). Contributed by @florianduros.
|
|
||||||
* Change color of public room icon ([#30390](https://github.com/element-hq/element-web/pull/30390)). Contributed by @florianduros.
|
|
||||||
* Script for updating storybook screenshots ([#30340](https://github.com/element-hq/element-web/pull/30340)). Contributed by @dbkr.
|
|
||||||
* Add toggle to hide empty state in devtools ([#30352](https://github.com/element-hq/element-web/pull/30352)). Contributed by @toger5.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Use userId to filter users in non-federated rooms when showing the InviteDialog ([#30537](https://github.com/element-hq/element-web/pull/30537)). Contributed by @RiotRobot.
|
|
||||||
* [Backport staging] Catch error when encountering invalid m.room.pinned\_events event ([#30536](https://github.com/element-hq/element-web/pull/30536)). Contributed by @RiotRobot.
|
|
||||||
* Update for compatibility with v12 rooms ([#30452](https://github.com/element-hq/element-web/pull/30452)). Contributed by @dbkr.
|
|
||||||
* New room list: fix tooltip on presence ([#30474](https://github.com/element-hq/element-web/pull/30474)). Contributed by @florianduros.
|
|
||||||
* New room list: add tooltip for presence and room status ([#30472](https://github.com/element-hq/element-web/pull/30472)). Contributed by @florianduros.
|
|
||||||
* Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view ([#30455](https://github.com/element-hq/element-web/pull/30455)). Contributed by @langleyd.
|
|
||||||
* Put the 'decrypting' tooltip back ([#30446](https://github.com/element-hq/element-web/pull/30446)). Contributed by @dbkr.
|
|
||||||
* Use server name explicitly for via. ([#30362](https://github.com/element-hq/element-web/pull/30362)). Contributed by @Half-Shot.
|
|
||||||
* fix: replace hardcoded string in poll history dialog ([#30402](https://github.com/element-hq/element-web/pull/30402)). Contributed by @florianduros.
|
|
||||||
* fix: replace hardcoded string on qr code back button ([#30401](https://github.com/element-hq/element-web/pull/30401)). Contributed by @florianduros.
|
|
||||||
* Fix color of icon button with outline ([#30361](https://github.com/element-hq/element-web/pull/30361)). Contributed by @florianduros.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
|
|
||||||
====================================================================================================
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* Message preview should show tooltip with the full message on hover ([#30265](https://github.com/element-hq/element-web/pull/30265)). Contributed by @MidhunSureshR.
|
|
||||||
* Support rendering notification badges on platforms that do their own icon overlays ([#30315](https://github.com/element-hq/element-web/pull/30315)). Contributed by @Half-Shot.
|
|
||||||
* Add SubscriptionViewModel base class ([#30297](https://github.com/element-hq/element-web/pull/30297)). Contributed by @dbkr.
|
|
||||||
* Enhancement: Save image on CTRL+S ([#30330](https://github.com/element-hq/element-web/pull/30330)). Contributed by @ioalexander.
|
|
||||||
* Add quote functionality to MessageContextMenu (#29893) ([#30323](https://github.com/element-hq/element-web/pull/30323)). Contributed by @AlirezaMrtz.
|
|
||||||
* Initial structure for shared component views ([#30216](https://github.com/element-hq/element-web/pull/30216)). Contributed by @dbkr.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix e2e shield being invisible in white mode for encrypted room ([#30411](https://github.com/element-hq/element-web/pull/30411)). Contributed by @RiotRobot.
|
|
||||||
* Force ED titlebar color for new room list ([#30332](https://github.com/element-hq/element-web/pull/30332)). Contributed by @florianduros.
|
|
||||||
* Add a background color to left panel for macos titlebar in element desktop ([#30328](https://github.com/element-hq/element-web/pull/30328)). Contributed by @florianduros.
|
|
||||||
* Fix: Prevent page refresh on Enter key in right panel member search ([#30312](https://github.com/element-hq/element-web/pull/30312)). Contributed by @AlirezaMrtz.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Fix e2e icon colour ([#30304](https://github.com/element-hq/element-web/pull/30304)). Contributed by @RiotRobot.
|
|
||||||
* Add support for module message hint `allowDownloadingMedia` ([#30252](https://github.com/element-hq/element-web/pull/30252)). Contributed by @Half-Shot.
|
|
||||||
* Update the mobile\_guide page to the new design and link out to Element X by default. ([#30172](https://github.com/element-hq/element-web/pull/30172)). Contributed by @pixlwave.
|
|
||||||
* Filter settings exported when rageshaking ([#30236](https://github.com/element-hq/element-web/pull/30236)). Contributed by @Half-Shot.
|
|
||||||
* Allow Element Call to learn the room name ([#30213](https://github.com/element-hq/element-web/pull/30213)). Contributed by @robintown.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix missing image download button ([#30322](https://github.com/element-hq/element-web/pull/30322)). Contributed by @RiotRobot.
|
|
||||||
* Fix transparent verification checkmark in dark mode ([#30235](https://github.com/element-hq/element-web/pull/30235)). Contributed by @Banbuii.
|
|
||||||
* Fix logic in DeviceListener ([#30230](https://github.com/element-hq/element-web/pull/30230)). Contributed by @uhoreg.
|
|
||||||
* Disable file drag-and-drop if insufficient permissions ([#30186](https://github.com/element-hq/element-web/pull/30186)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* New room list: add context menu to room list item ([#29952](https://github.com/element-hq/element-web/pull/29952)). Contributed by @florianduros.
|
|
||||||
* Support for custom message components via Module API ([#30074](https://github.com/element-hq/element-web/pull/30074)). Contributed by @Half-Shot.
|
|
||||||
* Prompt users to set up recovery ([#30075](https://github.com/element-hq/element-web/pull/30075)). Contributed by @uhoreg.
|
|
||||||
* Update `IconButton` colors ([#30124](https://github.com/element-hq/element-web/pull/30124)). Contributed by @florianduros.
|
|
||||||
* New room list: filter list can be collapsed ([#29992](https://github.com/element-hq/element-web/pull/29992)). Contributed by @florianduros.
|
|
||||||
* Show `EmptyRoomListView` when low priority filter matches zero rooms ([#30122](https://github.com/element-hq/element-web/pull/30122)). Contributed by @MidhunSureshR.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* Fix untranslatable string "People" in notifications beta ([#30165](https://github.com/element-hq/element-web/pull/30165)). Contributed by @t3chguy.
|
|
||||||
* Force verification even after logging in via delegate ([#30141](https://github.com/element-hq/element-web/pull/30141)). Contributed by @andybalaam.
|
|
||||||
* Hide add integrations button based on UIComponent.AddIntegrations ([#30140](https://github.com/element-hq/element-web/pull/30140)). Contributed by @t3chguy.
|
|
||||||
* Use nav for new room list and label sections ([#30134](https://github.com/element-hq/element-web/pull/30134)). Contributed by @dbkr.
|
|
||||||
* Spacestore should emit event after rebuilding home space ([#30132](https://github.com/element-hq/element-web/pull/30132)). Contributed by @MidhunSureshR.
|
|
||||||
* Handle m.room.pinned\_events being invalid ([#30129](https://github.com/element-hq/element-web/pull/30129)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
|
Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
|
||||||
====================================================================================================
|
====================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
|
|
||||||
Everyone is welcome to contribute code to Element Web, provided that they are willing to license their contributions to Element under a [Contributor License Agreement](https://cla-assistant.io/element-hq/element-web) (CLA). This ensures that their contribution will be made available under an OSI-approved open-source license, currently licensed under Affero General Public License v3 (AGPLv3) or General Public License v3 (GPLv3) at your choice.
|
Everyone is welcome to contribute code to Element Web, provided that they are willing to license their contributions to Element under a [Contributor License Agreement](https://cla-assistant.io/element-hq/element-web) (CLA). This ensures that their contribution will be made available under an OSI-approved open-source license, currently licensed under Affero General Public License v3 (AGPLv3) or General Public License v3 (GPLv3) at your choice.
|
||||||
|
|
||||||
If you're contributing, or thinking about contributing, please come & chat to
|
|
||||||
us in our development room, [#element-dev](https://matrix.to/#/#element-dev:matrix.org).
|
|
||||||
This is the best place to ask questions about the code, how to work on the project
|
|
||||||
or whether a change is likely to be accepted.
|
|
||||||
|
|
||||||
## How to contribute
|
## How to contribute
|
||||||
|
|
||||||
The preferred and easiest way to contribute changes to the project is to fork
|
The preferred and easiest way to contribute changes to the project is to fork
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# syntax=docker.io/docker/dockerfile:1.18-labs@sha256:79cdc14e1c220efb546ad14a8ebc816e3277cd72d27195ced5bebdd226dd1025
|
# syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458
|
||||||
|
|
||||||
# Builder
|
# Builder
|
||||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f8c398a3ad2612293e8827915c056ed0f5cc708b0f676274bb6c732e3c10f93d AS builder
|
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 AS builder
|
||||||
|
|
||||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||||
ARG USE_CUSTOM_SDKS=false
|
ARG USE_CUSTOM_SDKS=false
|
||||||
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
|
|||||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:14b127ed799301a21a1798516443c675237120c76b9a738d43c5e4747de4b1c9
|
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:66e34aa81c2faf290ea4e4c28a490f2b35a07478265a2d5994c8637506045eee
|
||||||
|
|
||||||
# Need root user to install packages & manipulate the usr directory
|
# Need root user to install packages & manipulate the usr directory
|
||||||
USER root
|
USER root
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Element has several tiers of support for different environments:
|
|||||||
- Best effort
|
- Best effort
|
||||||
- Definition:
|
- Definition:
|
||||||
- Issues **accepted**, regressions **do not block** the release
|
- Issues **accepted**, regressions **do not block** the release
|
||||||
- The wider Element Products (including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
|
- The wider Element Products(including Element Call and the Enterprise Server Suite) do still not officially support these browsers.
|
||||||
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
|
- The element web project and its contributors should keep the client functioning and gracefully degrade where other sibling features (E.g. Element Call) may not function.
|
||||||
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
|
- Last major release of Firefox ESR and Chrome/Edge Extended Stable
|
||||||
- Community Supported
|
- Community Supported
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
|
||||||
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
|
||||||
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
|
||||||
|
|
||||||
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
1. Note that an explicit type is optional if not expected to be used outside of the function call,
|
||||||
unlike in this example:
|
unlike in this example:
|
||||||
|
|
||||||
@@ -160,6 +161,7 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
28. Export only what can be reused.
|
28. Export only what can be reused.
|
||||||
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
|
||||||
of truly optional parameters.
|
of truly optional parameters.
|
||||||
|
|
||||||
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
|
||||||
takes an argument that is more often not required than required. An example where the
|
takes an argument that is more often not required than required. An example where the
|
||||||
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
`?` operator is inappropriate is when taking a room ID: typically the caller should
|
||||||
@@ -258,6 +260,7 @@ Inheriting all the rules of TypeScript, the following additionally apply:
|
|||||||
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||||
if at all possible.
|
if at all possible.
|
||||||
13. A component should only use CSS class names in line with the component name.
|
13. A component should only use CSS class names in line with the component name.
|
||||||
|
|
||||||
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
||||||
|
|
||||||
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
||||||
@@ -385,6 +388,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
properties should be clearly documented.
|
properties should be clearly documented.
|
||||||
|
|
||||||
4. Inside a function, there is no need to comment every line, but consider:
|
4. Inside a function, there is no need to comment every line, but consider:
|
||||||
|
|
||||||
- before a particular multiline section of code within the function, give an overview of what it does,
|
- before a particular multiline section of code within the function, give an overview of what it does,
|
||||||
to make it easier for a reader to follow the flow through the function as a whole.
|
to make it easier for a reader to follow the flow through the function as a whole.
|
||||||
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"default_widget_container_height": 280,
|
"default_widget_container_height": 280,
|
||||||
"default_country_code": "GB",
|
"default_country_code": "GB",
|
||||||
|
|||||||
8
declaration.d.ts
vendored
8
declaration.d.ts
vendored
@@ -1,8 +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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare module "*.module.css";
|
|
||||||
@@ -109,7 +109,7 @@ yarn test
|
|||||||
|
|
||||||
### End-to-End tests
|
### End-to-End tests
|
||||||
|
|
||||||
See [`docs/playwright.md`](./docs/playwright.md) for how to run the end-to-end tests.
|
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
||||||
|
|
||||||
## General github guidelines
|
## General github guidelines
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ entrypoint_log() {
|
|||||||
mkdir -p /tmp/element-web-config
|
mkdir -p /tmp/element-web-config
|
||||||
cp /app/config*.json /tmp/element-web-config/
|
cp /app/config*.json /tmp/element-web-config/
|
||||||
|
|
||||||
# If the module directory exists AND the module directory has modules in it
|
# If there are modules to be loaded
|
||||||
if [ -d "/modules" ] && [ "$( ls -A '/modules' )" ]; then
|
if [ -d "/modules" ]; then
|
||||||
cd /modules
|
cd /modules
|
||||||
|
|
||||||
for MODULE in *
|
for MODULE in *
|
||||||
do
|
do
|
||||||
# If the module has a package.json, use its main field as the entrypoint
|
# If the module has a package.json, use its main field as the entrypoint
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
# MVVM
|
|
||||||
|
|
||||||
_Deprecated_, see [MVVM.md](./MVVM.md) for the current version.
|
|
||||||
|
|
||||||
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
|
|
||||||
|
|
||||||
1. Model: This is where the business logic and data resides.
|
|
||||||
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
|
|
||||||
3. View: This is the UI code itself and depends on the view model.
|
|
||||||
|
|
||||||
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
|
|
||||||
|
|
||||||
### Practical guidelines for MVVM in element-web
|
|
||||||
|
|
||||||
#### Model
|
|
||||||
|
|
||||||
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
|
||||||
|
|
||||||
#### View Model
|
|
||||||
|
|
||||||
1. View model is always a custom react hook named like `useFooViewModel()`.
|
|
||||||
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
|
||||||
```ts
|
|
||||||
inteface FooViewState {
|
|
||||||
somethingUseful: string;
|
|
||||||
somethingElse: BarType;
|
|
||||||
update: () => Promise<void>
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. Any react state that your UI needs must be in the view model.
|
|
||||||
|
|
||||||
#### View
|
|
||||||
|
|
||||||
1. Views are simple react components (eg: `FooView`).
|
|
||||||
2. Views usually start by calling the view model hook, eg:
|
|
||||||
```tsx
|
|
||||||
const FooView: React.FC<IProps> = (props: IProps) => {
|
|
||||||
const vm = useFooViewModel();
|
|
||||||
....
|
|
||||||
return(
|
|
||||||
<div>
|
|
||||||
{vm.somethingUseful}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
3. Views are also allowed to accept the view model as a prop, eg:
|
|
||||||
```tsx
|
|
||||||
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
|
||||||
....
|
|
||||||
return(
|
|
||||||
<div>
|
|
||||||
{vm.somethingUseful}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
4. Multiple views can share the same view model if necessary.
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
|
||||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
|
||||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
|
||||||
100
docs/MVVM.md
100
docs/MVVM.md
@@ -10,80 +10,58 @@ If you do MVVM right, your view should be dumb i.e it gets data from the view mo
|
|||||||
|
|
||||||
### Practical guidelines for MVVM in element-web
|
### Practical guidelines for MVVM in element-web
|
||||||
|
|
||||||
A first documentation and implementation of MVVM was done in [MVVM-v1.md](MVVM-v1.md). This v1 version is now deprecated and this document describes the current implementation.
|
|
||||||
|
|
||||||
#### Model
|
#### Model
|
||||||
|
|
||||||
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
|
||||||
|
|
||||||
#### View
|
|
||||||
|
|
||||||
1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/src/shared-components). Develop it in storybook!
|
|
||||||
2. Views are simple react components (eg: `FooView`).
|
|
||||||
3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
|
|
||||||
4. Views should define the interface of the view model they expect:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Snapshot is the return type of your view model
|
|
||||||
interface FooViewSnapshot {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// To call function on the view model
|
|
||||||
interface FooViewActions {
|
|
||||||
doSomething: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewModel is a type defining the methods needed for `useSyncExternalStore`
|
|
||||||
// https://github.com/element-hq/element-web/blob/develop/src/shared-components/ViewModel.ts
|
|
||||||
type FooViewModel = ViewModel<FooViewSnapshot> & FooViewActions;
|
|
||||||
|
|
||||||
interface FooViewProps {
|
|
||||||
vm: FooViewModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function FooView({ vm }: FooViewProps) {
|
|
||||||
// useViewModel is a helper function that uses useSyncExternalStore under the hood
|
|
||||||
const { value } = useViewModel(vm);
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={() => vm.doSomething()}>
|
|
||||||
{value}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Multiple views can share the same view model if necessary.
|
|
||||||
6. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/shared-components/audio/AudioPlayerView/AudioPlayerView.tsx)
|
|
||||||
|
|
||||||
#### View Model
|
#### View Model
|
||||||
|
|
||||||
1. A View model is a class extending [`BaseViewModel`](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/base/BaseViewModel.ts).
|
1. View model is always a custom react hook named like `useFooViewModel()`.
|
||||||
2. Implements the interface defined in the view (e.g `FooViewModel` in the example above).
|
2. The return type of your view model (known as view state) must be defined as a typescript interface:
|
||||||
3. View models define a snapshot type that defines the data the view will consume. The snapshot is immutable and can only be changed by calling `this.snapshot.set(...)` in the view model. This will trigger a re-render in the view.
|
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface Props {
|
inteface FooViewState {
|
||||||
propsValue: string;
|
somethingUseful: string;
|
||||||
}
|
somethingElse: BarType;
|
||||||
|
update: () => Promise<void>
|
||||||
class FooViewModel extends BaseViewModel<FooViewSnapshot, Props> implements FooViewModel {
|
...
|
||||||
constructor(props: Props) {
|
|
||||||
// Call super with initial snapshot
|
|
||||||
super(props, { value: "initial" });
|
|
||||||
}
|
|
||||||
|
|
||||||
public doSomething() {
|
|
||||||
// Call this.snapshot.set to update the snapshot
|
|
||||||
this.snapshot.set({ value: "changed" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
3. Any react state that your UI needs must be in the view model.
|
||||||
|
|
||||||
4. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/viewmodels/audio/AudioPlayerViewModel.ts)
|
#### View
|
||||||
|
|
||||||
|
1. Views are simple react components (eg: `FooView`).
|
||||||
|
2. Views usually start by calling the view model hook, eg:
|
||||||
|
```tsx
|
||||||
|
const FooView: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const vm = useFooViewModel();
|
||||||
|
....
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
{vm.somethingUseful}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Views are also allowed to accept the view model as a prop, eg:
|
||||||
|
```tsx
|
||||||
|
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
|
||||||
|
....
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
{vm.somethingUseful}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. Multiple views can share the same view model if necessary.
|
||||||
|
|
||||||
### Benefits
|
### Benefits
|
||||||
|
|
||||||
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
|
||||||
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
|
||||||
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).
|
||||||
|
|||||||
@@ -130,37 +130,32 @@ complete re-branding/private labeling, a more personalised experience can be ach
|
|||||||
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
|
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
|
||||||
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
|
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
|
||||||
download the app instead.
|
download the app instead.
|
||||||
8. `mobile_guide_app_variant`: Optional. The mobile app that the user is prompted to download from the `/mobile_guide` page. When omitted
|
8. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
|
||||||
the mobile guide will be configured for the new Element X apps. Allowed values are as follows:
|
|
||||||
1. `element`: Element X Android/iOS.
|
|
||||||
2. `element-classic`: Element Classic Android/iOS.
|
|
||||||
3. `element-pro`: Element Pro Android/iOS.
|
|
||||||
9. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
|
|
||||||
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
|
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
|
||||||
in production.
|
in production.
|
||||||
10. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
|
9. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
|
||||||
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
|
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
|
||||||
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
|
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
|
||||||
configuration found in the well-known location is used instead.
|
configuration found in the well-known location is used instead.
|
||||||
11. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
|
10. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
|
||||||
12. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
|
11. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
|
||||||
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
|
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
|
||||||
13. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
|
12. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
|
||||||
14. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
|
13. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
|
||||||
15. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
|
14. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
|
||||||
`true` to hide these options.
|
`true` to hide these options.
|
||||||
16. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
|
15. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
|
||||||
to hide this dropdown.
|
to hide this dropdown.
|
||||||
17. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
|
16. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
|
||||||
users. Set to `true` to disable this functionality.
|
users. Set to `true` to disable this functionality.
|
||||||
18. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
|
17. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
|
||||||
Takes a configuration object as below:
|
Takes a configuration object as below:
|
||||||
1. `title`: Required. Title to show at the top of the notice.
|
1. `title`: Required. Title to show at the top of the notice.
|
||||||
2. `description`: Required. The description to use for the notice.
|
2. `description`: Required. The description to use for the notice.
|
||||||
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
||||||
19. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
||||||
20. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
||||||
21. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
||||||
|
|
||||||
### `desktop_builds` and `mobile_builds`
|
### `desktop_builds` and `mobile_builds`
|
||||||
|
|
||||||
@@ -450,7 +445,8 @@ If you would like to use Scalar, the integration manager maintained by Element,
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -585,8 +581,6 @@ Currently, the following UI feature flags are supported:
|
|||||||
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
|
- `UIFeature.BulkUnverifiedSessionsReminder` - Display popup reminders to verify or remove unverified sessions. Defaults
|
||||||
to true.
|
to true.
|
||||||
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
|
- `UIFeature.locationSharing` - Whether or not location sharing menus will be shown.
|
||||||
- `UIFeature.allowCreatingPublicRooms` - Whether or not public rooms can be created.
|
|
||||||
- `UIFeature.allowCreatingPublicSpaces` - Whether or not public spaces can be created.
|
|
||||||
|
|
||||||
## Undocumented / developer options
|
## Undocumented / developer options
|
||||||
|
|
||||||
|
|||||||
43
docs/e2ee.md
43
docs/e2ee.md
@@ -38,20 +38,45 @@ When `force_disable` is true:
|
|||||||
Note: If the server is configured to forcibly enable encryption for some or all rooms,
|
Note: If the server is configured to forcibly enable encryption for some or all rooms,
|
||||||
this behaviour will be overridden.
|
this behaviour will be overridden.
|
||||||
|
|
||||||
# Setting up recovery
|
# Secure backup
|
||||||
|
|
||||||
By default, Element strongly encourages (but does not require) users to set up
|
By default, Element strongly encourages (but does not require) users to set up
|
||||||
recovery so that you can access history on your new devices as well as retain access to your message history and cryptographic identity when you lose all of your devices.
|
Secure Backup so that cross-signing identity key and message keys can be
|
||||||
|
recovered in case of a disaster where you lose access to all active devices.
|
||||||
|
|
||||||
## Removal of old settings
|
## Requiring secure backup
|
||||||
|
|
||||||
Support for the configuration options `secure_backup_required` and `secure_backup_setup_methods`
|
To require Secure Backup to be configured before Element can be used, set the
|
||||||
in the `/.well-known/matrix/client` config has been removed.
|
following on your homeserver's `/.well-known/matrix/client` config:
|
||||||
|
|
||||||
Setting up recovery is now always recommended to all users by showing a one-off toast and a
|
```json
|
||||||
permanent red dot on the _Encryption_ tab in the _Settings_ dialog. When creating a new
|
{
|
||||||
recovery key, the UI only supports auto-generated keys. Using an existing (custom) passphrase
|
"io.element.e2ee": {
|
||||||
still works, but is not exposed in the UI when setting up recovery.
|
"secure_backup_required": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preferring setup methods
|
||||||
|
|
||||||
|
By default, Element offers users a choice of a random key or user-chosen
|
||||||
|
passphrase when setting up Secure Backup. If a homeserver admin would like to
|
||||||
|
only offer one of these, you can signal this via the
|
||||||
|
`/.well-known/matrix/client` config, for example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"io.element.e2ee": {
|
||||||
|
"secure_backup_setup_methods": ["passphrase"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The field `secure_backup_setup_methods` is an array listing the methods the
|
||||||
|
client should display. Supported values currently include `key` and
|
||||||
|
`passphrase`. If the `secure_backup_setup_methods` field is not present or
|
||||||
|
exists but does not contain any supported methods, Element will fallback to the
|
||||||
|
default value of: `["key", "passphrase"]`.
|
||||||
|
|
||||||
# Compatibility
|
# Compatibility
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
||||||
"defaultCountryCode": "GB",
|
"defaultCountryCode": "GB",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
||||||
"uisi_autorageshake_app": "element-auto-uisi",
|
"uisi_autorageshake_app": "element-auto-uisi",
|
||||||
|
|||||||
@@ -15,7 +15,8 @@
|
|||||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar.vector.im/api",
|
"https://scalar.vector.im/api",
|
||||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||||
"https://scalar-staging.vector.im/api"
|
"https://scalar-staging.vector.im/api",
|
||||||
|
"https://scalar-staging.riot.im/scalar/api"
|
||||||
],
|
],
|
||||||
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
|
||||||
"uisi_autorageshake_app": "element-auto-uisi",
|
"uisi_autorageshake_app": "element-auto-uisi",
|
||||||
|
|||||||
@@ -17,13 +17,11 @@ const config: Config = {
|
|||||||
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
||||||
customExportConditions: ["browser", "node"],
|
customExportConditions: ["browser", "node"],
|
||||||
},
|
},
|
||||||
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/src/shared-components/**/*.test.[t]s?(x)"],
|
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
|
||||||
globalSetup: "<rootDir>/test/globalSetup.ts",
|
globalSetup: "<rootDir>/test/globalSetup.ts",
|
||||||
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
// Support CSS module
|
|
||||||
"\\.(module.css)$": "identity-obj-proxy",
|
|
||||||
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
||||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||||
@@ -40,8 +38,10 @@ const config: Config = {
|
|||||||
"^!!raw-loader!.*": "jest-raw-loader",
|
"^!!raw-loader!.*": "jest-raw-loader",
|
||||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||||
|
// Requires ESM which is incompatible with our current Jest setup
|
||||||
|
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error)).+$"],
|
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||||
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
|
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
|
||||||
|
|||||||
11
knip.ts
11
knip.ts
@@ -2,6 +2,7 @@ import { KnipConfig } from "knip";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
entry: [
|
entry: [
|
||||||
|
"src/vector/index.ts",
|
||||||
"src/serviceworker/index.ts",
|
"src/serviceworker/index.ts",
|
||||||
"src/workers/*.worker.ts",
|
"src/workers/*.worker.ts",
|
||||||
"src/utils/exportUtils/exportJS.js",
|
"src/utils/exportUtils/exportJS.js",
|
||||||
@@ -11,6 +12,8 @@ export default {
|
|||||||
"res/decoder-ring/**",
|
"res/decoder-ring/**",
|
||||||
"res/jitsi_external_api.min.js",
|
"res/jitsi_external_api.min.js",
|
||||||
"docs/**",
|
"docs/**",
|
||||||
|
// Used by jest
|
||||||
|
"__mocks__/maplibre-gl.js",
|
||||||
],
|
],
|
||||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||||
ignore: [
|
ignore: [
|
||||||
@@ -39,18 +42,10 @@ export default {
|
|||||||
"util",
|
"util",
|
||||||
// Embedded into webapp
|
// Embedded into webapp
|
||||||
"@element-hq/element-call-embedded",
|
"@element-hq/element-call-embedded",
|
||||||
|
|
||||||
// Used by matrix-js-sdk, which means we have to include them as a
|
|
||||||
// dependency so that // we can run `tsc` (since we import the typescript
|
|
||||||
// source of js-sdk, rather than the transpiled and annotated JS like you
|
|
||||||
// would with a normal library).
|
|
||||||
"@types/content-type",
|
|
||||||
"@types/sdp-transform",
|
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used in scripts & workflows
|
// Used in scripts & workflows
|
||||||
"jq",
|
"jq",
|
||||||
"wait-on",
|
|
||||||
],
|
],
|
||||||
ignoreExportsUsedInFile: true,
|
ignoreExportsUsedInFile: true,
|
||||||
} satisfies KnipConfig;
|
} satisfies KnipConfig;
|
||||||
|
|||||||
90
package.json
90
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.12.1-rc.1",
|
"version": "1.11.104",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -65,28 +65,23 @@
|
|||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
"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",
|
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package"
|
||||||
"storybook": "storybook dev -p 6007",
|
|
||||||
"build-storybook": "storybook build",
|
|
||||||
"test:storybook": "test-storybook --url http://localhost:6007/",
|
|
||||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
|
|
||||||
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/pretty-format/react-is": "19.1.1",
|
"**/pretty-format/react-is": "19.1.0",
|
||||||
"@playwright/test": "1.54.2",
|
"@playwright/test": "1.52.0",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.6",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.6",
|
||||||
"oidc-client-ts": "3.3.0",
|
"oidc-client-ts": "3.2.1",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001741",
|
"caniuse-lite": "1.0.30001721",
|
||||||
"testcontainers": "^11.0.0",
|
"testcontainers": "^11.0.0",
|
||||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@element-hq/element-web-module-api": "1.4.1",
|
"@element-hq/element-web-module-api": "1.2.0",
|
||||||
"@fontsource/inconsolata": "^5",
|
"@fontsource/inconsolata": "^5",
|
||||||
"@fontsource/inter": "^5",
|
"@fontsource/inter": "^5",
|
||||||
"@formatjs/intl-segmenter": "^11.5.7",
|
"@formatjs/intl-segmenter": "^11.5.7",
|
||||||
@@ -94,11 +89,12 @@
|
|||||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||||
"@matrix-org/spec": "^1.7.0",
|
"@matrix-org/spec": "^1.7.0",
|
||||||
"@sentry/browser": "^10.0.0",
|
"@sentry/browser": "^9.0.0",
|
||||||
"@types/png-chunks-extract": "^1.0.2",
|
"@types/png-chunks-extract": "^1.0.2",
|
||||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
"@types/react-virtualized": "^9.21.30",
|
||||||
"@vector-im/compound-web": "^8.1.2",
|
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||||
"@vector-im/matrix-wysiwyg": "2.40.0",
|
"@vector-im/compound-web": "^8.0.0",
|
||||||
|
"@vector-im/matrix-wysiwyg": "2.38.3",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||||
@@ -116,7 +112,7 @@
|
|||||||
"emojibase-regex": "15.3.2",
|
"emojibase-regex": "15.3.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "11.0.2",
|
"filesize": "10.1.6",
|
||||||
"github-markdown-css": "^5.5.1",
|
"github-markdown-css": "^5.5.1",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
@@ -127,14 +123,14 @@
|
|||||||
"jsrsasign": "^11.0.0",
|
"jsrsasign": "^11.0.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
"linkify-react": "4.3.2",
|
"linkify-react": "4.3.1",
|
||||||
"linkify-string": "4.3.2",
|
"linkify-string": "4.3.1",
|
||||||
"linkifyjs": "4.3.2",
|
"linkifyjs": "4.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"maplibre-gl": "^5.0.0",
|
"maplibre-gl": "^5.0.0",
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-js-sdk": "38.4.0-rc.0",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"matrix-widget-api": "^1.10.0",
|
"matrix-widget-api": "^1.10.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@@ -142,7 +138,7 @@
|
|||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.265.1",
|
"posthog-js": "1.249.4",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"re-resizable": "6.11.2",
|
"re-resizable": "6.11.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -152,14 +148,14 @@
|
|||||||
"react-focus-lock": "^2.5.1",
|
"react-focus-lock": "^2.5.1",
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"react-virtuoso": "^4.14.0",
|
"react-virtualized": "^9.22.5",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"temporal-polyfill": "^0.3.0",
|
"temporal-polyfill": "^0.3.0",
|
||||||
"ua-parser-js": "1.0.40",
|
"ua-parser-js": "^1.0.2",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^11.0.0",
|
||||||
"what-input": "^5.2.10"
|
"what-input": "^5.2.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -184,27 +180,20 @@
|
|||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
"@element-hq/element-call-embedded": "0.16.0",
|
"@element-hq/element-call-embedded": "0.12.2",
|
||||||
"@element-hq/element-web-playwright-common": "^1.4.6",
|
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.50.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||||
"@rrweb/types": "^2.0.0-alpha.18",
|
"@rrweb/types": "^2.0.0-alpha.18",
|
||||||
"@sentry/webpack-plugin": "^4.0.0",
|
"@sentry/webpack-plugin": "^3.0.0",
|
||||||
"@storybook/addon-a11y": "^9.0.18",
|
"@stylistic/eslint-plugin": "^4.0.0",
|
||||||
"@storybook/addon-designs": "^10.0.1",
|
|
||||||
"@storybook/addon-docs": "^9.0.12",
|
|
||||||
"@storybook/icons": "^1.4.0",
|
|
||||||
"@storybook/react-vite": "^9.0.15",
|
|
||||||
"@storybook/test-runner": "^0.23.0",
|
|
||||||
"@stylistic/eslint-plugin": "^5.0.0",
|
|
||||||
"@svgr/webpack": "^8.0.0",
|
"@svgr/webpack": "^8.0.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.4.8",
|
"@testing-library/jest-dom": "^6.4.8",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/commonmark": "^0.27.4",
|
"@types/commonmark": "^0.27.4",
|
||||||
"@types/content-type": "^1.1.9",
|
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/css-tree": "^2.3.8",
|
"@types/css-tree": "^2.3.8",
|
||||||
"@types/diff-match-patch": "^1.0.32",
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
@@ -223,15 +212,15 @@
|
|||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.6",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/react-dom": "19.1.6",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "2.16.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/sdp-transform": "^2.4.10",
|
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/tar-js": "^0.3.5",
|
"@types/tar-js": "^0.3.5",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||||
"@typescript-eslint/parser": "^8.19.0",
|
"@typescript-eslint/parser": "^8.19.0",
|
||||||
"babel-jest": "^29.0.0",
|
"babel-jest": "^29.0.0",
|
||||||
@@ -242,10 +231,10 @@
|
|||||||
"concurrently": "^9.0.0",
|
"concurrently": "^9.0.0",
|
||||||
"copy-webpack-plugin": "^13.0.0",
|
"copy-webpack-plugin": "^13.0.0",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
"cronstrue": "^3.0.0",
|
"cronstrue": "^2.41.0",
|
||||||
"css-loader": "^7.0.0",
|
"css-loader": "^7.0.0",
|
||||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
"dotenv": "^17.0.0",
|
"dotenv": "^16.0.2",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
@@ -257,20 +246,18 @@
|
|||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-storybook": "^9.0.12",
|
|
||||||
"eslint-plugin-unicorn": "^56.0.0",
|
"eslint-plugin-unicorn": "^56.0.0",
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"fetch-mock-jest": "^1.5.1",
|
"fetch-mock-jest": "^1.5.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
|
"glob": "^11.0.0",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
"identity-obj-proxy": "^3.0.0",
|
|
||||||
"jest": "^29.6.2",
|
"jest": "^29.6.2",
|
||||||
"jest-canvas-mock": "^2.5.2",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-image-snapshot": "^6.5.1",
|
|
||||||
"jest-mock": "^29.6.2",
|
"jest-mock": "^29.6.2",
|
||||||
"jest-raw-loader": "^1.0.1",
|
"jest-raw-loader": "^1.0.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
@@ -288,20 +275,19 @@
|
|||||||
"postcss-hexrgba": "2.1.0",
|
"postcss-hexrgba": "2.1.0",
|
||||||
"postcss-import": "16.1.0",
|
"postcss-import": "16.1.0",
|
||||||
"postcss-loader": "8.1.1",
|
"postcss-loader": "8.1.1",
|
||||||
"postcss-mixins": "^12.0.0",
|
"postcss-mixins": "^11.0.0",
|
||||||
"postcss-nested": "^7.0.0",
|
"postcss-nested": "^7.0.0",
|
||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"postcss-scss": "^4.0.4",
|
"postcss-scss": "^4.0.4",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"source-map-loader": "^5.0.0",
|
"source-map-loader": "^5.0.0",
|
||||||
"storybook": "^9.0.12",
|
"stylelint": "^16.13.0",
|
||||||
"stylelint": "^16.23.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-config-standard": "^39.0.0",
|
|
||||||
"stylelint-scss": "^6.0.0",
|
"stylelint-scss": "^6.0.0",
|
||||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||||
"terser-webpack-plugin": "^5.3.9",
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
@@ -309,8 +295,6 @@
|
|||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vite": "^7.0.1",
|
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
|
||||||
"web-streams-polyfill": "^4.0.0",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
|
|||||||
@@ -11,42 +11,3 @@ index 917a7fc..a2710c6 100644
|
|||||||
didOkOrSubmit: boolean;
|
didOkOrSubmit: boolean;
|
||||||
model: M;
|
model: M;
|
||||||
}>;
|
}>;
|
||||||
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
|
|
||||||
index 5d422ed..b823add 100644
|
|
||||||
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
|
|
||||||
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/lifecycles/CryptoSetupExtensions.js
|
|
||||||
@@ -124,34 +124,28 @@ var DefaultCryptoSetupExtensions = /*#__PURE__*/function (_CryptoSetupExtension)
|
|
||||||
(0, _createClass2["default"])(DefaultCryptoSetupExtensions, [{
|
|
||||||
key: "examineLoginResponse",
|
|
||||||
value: function examineLoginResponse(response, credentials) {
|
|
||||||
- console.log("Default empty examineLoginResponse() => void");
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
key: "persistCredentials",
|
|
||||||
value: function persistCredentials(credentials) {
|
|
||||||
- console.log("Default empty persistCredentials() => void");
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
key: "getSecretStorageKey",
|
|
||||||
value: function getSecretStorageKey() {
|
|
||||||
- console.log("Default empty getSecretStorageKey() => null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
key: "createSecretStorageKey",
|
|
||||||
value: function createSecretStorageKey() {
|
|
||||||
- console.log("Default empty createSecretStorageKey() => null");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
key: "catchAccessSecretStorageError",
|
|
||||||
value: function catchAccessSecretStorageError(e) {
|
|
||||||
- console.log("Default catchAccessSecretStorageError() => void");
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
key: "setupEncryptionNeeded",
|
|
||||||
value: function setupEncryptionNeeded(args) {
|
|
||||||
- console.log("Default setupEncryptionNeeded() => false");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
diff --git a/node_modules/@types/mdx/types.d.ts b/node_modules/@types/mdx/types.d.ts
|
|
||||||
index 498bb69..4e89216 100644
|
|
||||||
--- a/node_modules/@types/mdx/types.d.ts
|
|
||||||
+++ b/node_modules/@types/mdx/types.d.ts
|
|
||||||
@@ -5,7 +5,7 @@
|
|
||||||
*/
|
|
||||||
// @ts-ignore JSX runtimes may optionally define JSX.ElementType. The MDX types need to work regardless whether this is
|
|
||||||
// defined or not.
|
|
||||||
-type ElementType = any extends JSX.ElementType ? never : JSX.ElementType;
|
|
||||||
+type ElementType = any extends JSX.ElementType ? never : React.JSX.ElementType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This matches any function component types that ar part of `ElementType`.
|
|
||||||
@@ -20,12 +20,12 @@ type ClassElementType = Extract<ElementType, new(props: Record<string, any>) =>
|
|
||||||
/**
|
|
||||||
* A valid JSX string component.
|
|
||||||
*/
|
|
||||||
-type StringComponent = Extract<keyof JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
|
|
||||||
+type StringComponent = Extract<keyof React.JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A JSX element returned by MDX content.
|
|
||||||
*/
|
|
||||||
-export type Element = JSX.Element;
|
|
||||||
+export type Element = React.JSX.Element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A valid JSX function component.
|
|
||||||
@@ -44,7 +44,7 @@ type FunctionComponent<Props> = ElementType extends never
|
|
||||||
*/
|
|
||||||
type ClassComponent<Props> = ElementType extends never
|
|
||||||
// If JSX.ElementType isn’t defined, the valid return type is a constructor that returns JSX.ElementClass
|
|
||||||
- ? new(props: Props) => JSX.ElementClass
|
|
||||||
+ ? new(props: Props) => React.JSX.ElementClass
|
|
||||||
: ClassElementType extends never
|
|
||||||
// If JSX.ElementType is defined, but doesn’t allow constructors, function components are disallowed.
|
|
||||||
? never
|
|
||||||
@@ -70,7 +70,7 @@ interface NestedMDXComponents {
|
|
||||||
export type MDXComponents =
|
|
||||||
& NestedMDXComponents
|
|
||||||
& {
|
|
||||||
- [Key in StringComponent]?: Component<JSX.IntrinsicElements[Key]>;
|
|
||||||
+ [Key in StringComponent]?: Component<React.JSX.IntrinsicElements[Key]>;
|
|
||||||
}
|
|
||||||
& {
|
|
||||||
/**
|
|
||||||
31
patches/@types+react+19.1.4.patch
Normal file
31
patches/@types+react+19.1.4.patch
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
|
||||||
|
index d3318dc..c2b2c77 100644
|
||||||
|
--- a/node_modules/@types/react/index.d.ts
|
||||||
|
+++ b/node_modules/@types/react/index.d.ts
|
||||||
|
@@ -134,7 +134,7 @@ declare namespace React {
|
||||||
|
props: P,
|
||||||
|
) => ReactNode | Promise<ReactNode>)
|
||||||
|
// constructor signature must match React.Component
|
||||||
|
- | (new(props: P) => Component<any, any>);
|
||||||
|
+ | (new(props: P, context?: any) => Component<any, any>);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by {@link createRef}, or {@link useRef} when passed `null`.
|
||||||
|
@@ -945,7 +945,7 @@ declare namespace React {
|
||||||
|
context: unknown;
|
||||||
|
|
||||||
|
// Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
|
||||||
|
- constructor(props: P);
|
||||||
|
+ constructor(props: P, context?: unknown);
|
||||||
|
|
||||||
|
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
|
||||||
|
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
|
||||||
|
@@ -1117,7 +1117,7 @@ declare namespace React {
|
||||||
|
*/
|
||||||
|
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
|
||||||
|
// constructor signature must match React.Component
|
||||||
|
- new(props: P): Component<P, S>;
|
||||||
|
+ new(props: P, context?: any): Component<P, S>;
|
||||||
|
/**
|
||||||
|
* Ignored by React.
|
||||||
|
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
|
||||||
@@ -29,7 +29,7 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
|
|
||||||
// Pressing Control+F6 again will focus room search
|
// Pressing Control+F6 again will focus room search
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
await expect(page.locator(".mx_RoomSearch")).toBeFocused();
|
||||||
|
|
||||||
// Pressing Control+F6 again will focus the message composer
|
// Pressing Control+F6 again will focus the message composer
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
@@ -44,7 +44,7 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
await expect(page.locator(".mx_HomePage")).toBeFocused();
|
await expect(page.locator(".mx_HomePage")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
await expect(page.locator(".mx_RoomSearch")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||||
@@ -75,11 +75,11 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
|
|
||||||
// Pressing Control+F6 again will focus room search
|
// Pressing Control+F6 again will focus room search
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
await expect(page.locator(".mx_RoomSearch")).toBeFocused();
|
||||||
|
|
||||||
// Pressing Control+F6 again will focus the room tile in the room list
|
// Pressing Control+F6 again will focus the room tile in the room list
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
await expect(page.locator(".mx_RoomListItemView_selected")).toBeFocused();
|
await expect(page.locator(".mx_RoomTile_selected")).toBeFocused();
|
||||||
|
|
||||||
// Pressing Control+F6 again will focus the message composer
|
// Pressing Control+F6 again will focus the message composer
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
@@ -94,10 +94,10 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused();
|
await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_RoomListItemView_selected")).toBeFocused();
|
await expect(page.locator(".mx_RoomTile_selected")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
await expect(page.locator(".mx_RoomSearch")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||||
@@ -131,11 +131,11 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
|
|
||||||
// Pressing Control+F6 again will focus room search
|
// Pressing Control+F6 again will focus room search
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
await expect(page.locator(".mx_RoomSearch")).toBeFocused();
|
||||||
|
|
||||||
// Pressing Control+F6 again will focus the room tile in the room list
|
// Pressing Control+F6 again will focus the room tile in the room list
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
await expect(page.locator(".mx_RoomListItemView")).toBeFocused();
|
await expect(page.locator(".mx_RoomTile")).toBeFocused();
|
||||||
|
|
||||||
// Pressing Control+F6 again will focus the home section
|
// Pressing Control+F6 again will focus the home section
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
@@ -150,10 +150,10 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
await expect(page.locator(".mx_HomePage")).toBeFocused();
|
await expect(page.locator(".mx_HomePage")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_RoomListItemView")).toBeFocused();
|
await expect(page.locator(".mx_RoomTile")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
await expect(page.locator(".mx_RoomSearch")).toBeFocused();
|
||||||
|
|
||||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const clickButtonReply = async (tile: Locator) => {
|
|||||||
await tile.hover();
|
await tile.hover();
|
||||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||||
}).toPass();
|
}).toPass();
|
||||||
await expect(tile.page().getByText("Replying", { exact: true })).toBeVisible();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||||
@@ -40,7 +39,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// wait for the tile to finish loading
|
// wait for the tile to finish loading
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.getByTestId("audio-player-name")
|
.locator(".mx_AudioPlayer_mediaName")
|
||||||
.last()
|
.last()
|
||||||
.filter({ hasText: file.split("/").at(-1) }),
|
.filter({ hasText: file.split("/").at(-1) }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -55,10 +54,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// Check that the audio player is rendered and its button becomes visible
|
// Check that the audio player is rendered and its button becomes visible
|
||||||
const checkPlayerVisibility = async (locator: Locator) => {
|
const checkPlayerVisibility = async (locator: Locator) => {
|
||||||
// Assert that the audio player and media information are visible
|
// Assert that the audio player and media information are visible
|
||||||
const mediaInfo = locator.getByRole("region", { name: "Audio player" });
|
const mediaInfo = locator.locator(
|
||||||
await expect(mediaInfo.getByText(".ogg")).toBeVisible(); // extension
|
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo",
|
||||||
await expect(mediaInfo.getByRole("time")).toHaveText("00:01"); // duration
|
);
|
||||||
await expect(mediaInfo.getByText("(3.56 KB)")).toBeVisible(); // actual size;
|
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName", { hasText: ".ogg" })).toBeVisible(); // extension
|
||||||
|
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
|
||||||
|
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
|
||||||
|
|
||||||
// Assert that the play button can be found and is visible
|
// Assert that the play button can be found and is visible
|
||||||
await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -77,7 +78,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the status of the seek bar
|
// Check the status of the seek bar
|
||||||
expect(await page.getByRole("region", { name: "Audio player" }).getByRole("slider").count()).toBeGreaterThan(0);
|
expect(await page.locator(".mx_AudioPlayer_seek input[type='range']").count()).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Enable IRC layout
|
// Enable IRC layout
|
||||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||||
@@ -99,7 +100,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
mask: [page.getByTestId("audio-player-seek")],
|
mask: [page.locator(".mx_AudioPlayer_seek")],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Take a snapshot of mx_EventTile_last on IRC layout
|
// Take a snapshot of mx_EventTile_last on IRC layout
|
||||||
@@ -185,9 +186,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" });
|
const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container");
|
||||||
// Assert that the counter is zero before clicking the play button
|
// Assert that the counter is zero before clicking the play button
|
||||||
await expect(container.getByRole("timer")).toHaveText("00:00");
|
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Find and click "Play" button, the wait is to make the test less flaky
|
// Find and click "Play" button, the wait is to make the test less flaky
|
||||||
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -197,7 +198,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
|
await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the timer is reset when the audio file finished playing
|
// Assert that the timer is reset when the audio file finished playing
|
||||||
await expect(container.getByRole("timer")).toHaveText("00:00");
|
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that "Play" button can be found
|
// Assert that "Play" button can be found
|
||||||
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -225,7 +226,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert the audio player is rendered
|
// Assert the audio player is rendered
|
||||||
await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Find and click "Reply" button on MessageActionBar
|
// Find and click "Reply" button on MessageActionBar
|
||||||
const tile = page.locator(".mx_EventTile_last");
|
const tile = page.locator(".mx_EventTile_last");
|
||||||
@@ -235,7 +236,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Assert that replied audio file is rendered as file button inside ReplyChain
|
// Assert that replied audio file is rendered as file button inside ReplyChain
|
||||||
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
||||||
@@ -260,9 +261,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await clickButtonReply(tile);
|
await clickButtonReply(tile);
|
||||||
|
|
||||||
@@ -270,9 +269,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await clickButtonReply(tile);
|
await clickButtonReply(tile);
|
||||||
|
|
||||||
@@ -280,7 +277,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Assert that there are two "mx_ReplyChain" elements
|
// Assert that there are two "mx_ReplyChain" elements
|
||||||
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
||||||
@@ -316,9 +313,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// On the main timeline
|
// On the main timeline
|
||||||
const messageList = page.locator(".mx_RoomView_MessageList");
|
const messageList = page.locator(".mx_RoomView_MessageList");
|
||||||
// Assert the audio player is rendered
|
// Assert the audio player is rendered
|
||||||
await expect(
|
await expect(messageList.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
messageList.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
|
||||||
).toBeVisible();
|
|
||||||
// Find and click "Reply in thread" button
|
// Find and click "Reply in thread" button
|
||||||
await messageList.locator(".mx_EventTile_last").hover();
|
await messageList.locator(".mx_EventTile_last").hover();
|
||||||
await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
|
await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
|
||||||
@@ -326,10 +321,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// On a thread
|
// On a thread
|
||||||
const thread = page.locator(".mx_ThreadView");
|
const thread = page.locator(".mx_ThreadView");
|
||||||
const threadTile = thread.locator(".mx_EventTile_last");
|
const threadTile = thread.locator(".mx_EventTile_last");
|
||||||
const audioPlayer = threadTile.getByRole("region", { name: "Audio player" });
|
const audioPlayer = threadTile.locator(".mx_AudioPlayer_container");
|
||||||
|
|
||||||
// Assert that the counter is zero before clicking the play button
|
// Assert that the counter is zero before clicking the play button
|
||||||
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
|
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Find and click "Play" button, the wait is to make the test less flaky
|
// Find and click "Play" button, the wait is to make the test less flaky
|
||||||
await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -339,7 +334,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
|
await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the timer is reset when the audio file finished playing
|
// Assert that the timer is reset when the audio file finished playing
|
||||||
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
|
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that "Play" button can be found
|
// Assert that "Play" button can be found
|
||||||
await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();
|
await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control";
|
|||||||
test.describe("Composer", () => {
|
test.describe("Composer", () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Janet",
|
displayName: "Janet",
|
||||||
botCreateOpts: {
|
|
||||||
displayName: "Bob",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
@@ -31,7 +28,7 @@ test.describe("Composer", () => {
|
|||||||
|
|
||||||
test.describe("CIDER", () => {
|
test.describe("CIDER", () => {
|
||||||
test("sends a message when you click send or press Enter", async ({ page }) => {
|
test("sends a message when you click send or press Enter", async ({ page }) => {
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
|
|
||||||
// Type a message
|
// Type a message
|
||||||
await composer.pressSequentially("my message 0");
|
await composer.pressSequentially("my message 0");
|
||||||
@@ -55,7 +52,7 @@ test.describe("Composer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can write formatted text", async ({ page }) => {
|
test("can write formatted text", async ({ page }) => {
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
|
|
||||||
await composer.pressSequentially("my bold");
|
await composer.pressSequentially("my bold");
|
||||||
await composer.press(`${CtrlOrMeta}+KeyB`);
|
await composer.press(`${CtrlOrMeta}+KeyB`);
|
||||||
@@ -71,7 +68,7 @@ test.describe("Composer", () => {
|
|||||||
await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click();
|
await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click();
|
||||||
|
|
||||||
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
|
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Send message
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message
|
||||||
|
|
||||||
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
||||||
});
|
});
|
||||||
@@ -82,7 +79,7 @@ test.describe("Composer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("only sends when you press Control+Enter", async ({ page }) => {
|
test("only sends when you press Control+Enter", async ({ page }) => {
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||||
// Type a message and press Enter
|
// Type a message and press Enter
|
||||||
await composer.pressSequentially("my message 3");
|
await composer.pressSequentially("my message 3");
|
||||||
await composer.press("Enter");
|
await composer.press("Enter");
|
||||||
@@ -97,25 +94,5 @@ test.describe("Composer", () => {
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can send mention", { tag: "@screenshot" }, async ({ page, bot, app }) => {
|
|
||||||
// Set up a private room so we have another user to mention
|
|
||||||
await app.client.createRoom({
|
|
||||||
is_direct: true,
|
|
||||||
invite: [bot.credentials.userId],
|
|
||||||
});
|
|
||||||
await app.viewRoomByName("Bob");
|
|
||||||
|
|
||||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
|
||||||
await composer.pressSequentially("@bob");
|
|
||||||
|
|
||||||
// Note that we include the user ID here as the room tile is also an 'option' role
|
|
||||||
// with text 'Bob'
|
|
||||||
await page.getByRole("option", { name: `Bob ${bot.credentials.userId}` }).click();
|
|
||||||
await expect(composer.getByText("Bob")).toBeVisible();
|
|
||||||
await expect(composer).toMatchScreenshot("mention.png");
|
|
||||||
await composer.press("Enter");
|
|
||||||
await expect(page.locator(".mx_EventTile_body", { hasText: "Bob" })).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
34
playwright/e2e/create-room/create-room.spec.ts
Normal file
34
playwright/e2e/create-room/create-room.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Create Room", () => {
|
||||||
|
test.use({ displayName: "Jim" });
|
||||||
|
|
||||||
|
test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => {
|
||||||
|
const name = "Test room 1";
|
||||||
|
const topic = "This room is dedicated to this test and this test only!";
|
||||||
|
|
||||||
|
const dialog = await app.openCreateRoomDialog();
|
||||||
|
// Fill name & topic
|
||||||
|
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
||||||
|
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
||||||
|
// Change room to public
|
||||||
|
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
||||||
|
await dialog.getByRole("option", { name: "Public room" }).click();
|
||||||
|
// Fill room address
|
||||||
|
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
|
||||||
|
// Submit
|
||||||
|
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||||
|
const header = page.locator(".mx_RoomHeader");
|
||||||
|
await expect(header).toContainText(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -49,7 +49,7 @@ test.describe("Encryption state after registration", () => {
|
|||||||
"Pa$sW0rD!",
|
"Pa$sW0rD!",
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
@@ -78,7 +78,7 @@ test.describe("Key backup reset from elsewhere", () => {
|
|||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
|
await registerAccountMas(page, mailpitClient, testUsername, `${testUsername}@email.com`, testPassword);
|
||||||
|
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
@@ -91,10 +91,10 @@ test.describe("Key backup reset from elsewhere", () => {
|
|||||||
|
|
||||||
await csAPI.deleteBackupVersion(backupInfo.version);
|
await csAPI.deleteBackupVersion(backupInfo.version);
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("/discardsession");
|
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("Message with broken key backup");
|
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
// Should be the message we sent plus the room creation event
|
// Should be the message we sent plus the room creation event
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ const checkDMRoom = async (page: Page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDMWithBob = async (page: Page, bob: Bot) => {
|
const startDMWithBob = async (page: Page, bob: Bot) => {
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
||||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
|
||||||
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||||
await page.getByRole("option", { name: bob.credentials.displayName }).click();
|
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
|
page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -159,9 +158,6 @@ test.describe("Cryptography", function () {
|
|||||||
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||||
await checkDMRoom(page);
|
await checkDMRoom(page);
|
||||||
const bobRoomId = await bobJoin(page, bob);
|
const bobRoomId = await bobJoin(page, bob);
|
||||||
// We no longer show the grey badge in the composer, check that it is not there.
|
|
||||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toHaveCount(0);
|
|
||||||
|
|
||||||
await testMessages(page, bob, bobRoomId);
|
await testMessages(page, bob, bobRoomId);
|
||||||
await verify(app, bob);
|
await verify(app, bob);
|
||||||
|
|
||||||
@@ -172,7 +168,6 @@ test.describe("Cryptography", function () {
|
|||||||
|
|
||||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
||||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon.png");
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,10 @@ test.describe("Cryptography", function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Alice accepts the invite
|
// Alice accepts the invite
|
||||||
await page.getByRole("option", { name: "Test room" }).click();
|
await expect(
|
||||||
|
page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
|
||||||
|
).toHaveCount(1);
|
||||||
|
await page.getByRole("treeitem", { name: "Test room" }).click();
|
||||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
||||||
|
|
||||||
// Bob sends an encrypted event and an undecryptable event
|
// Bob sends an encrypted event and an undecryptable event
|
||||||
@@ -277,7 +280,10 @@ test.describe("Cryptography", function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Alice accepts the invite
|
// Alice accepts the invite
|
||||||
await page.getByRole("option", { name: "Test room" }).click();
|
await expect(
|
||||||
|
page.getByRole("group", { name: "Invites" }).locator(".mx_RoomSublist_tiles").getByRole("treeitem"),
|
||||||
|
).toHaveCount(1);
|
||||||
|
await page.getByRole("treeitem", { name: "Test room" }).click();
|
||||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
||||||
|
|
||||||
// wait until we're joined and see the timeline
|
// wait until we're joined and see the timeline
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test.describe("Dehydration", () => {
|
|||||||
// Reset the identity key
|
// Reset the identity key
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
const settings = await app.settings.openUserSettings("Encryption");
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// Set up recovery
|
// Set up recovery
|
||||||
@@ -106,7 +106,7 @@ test.describe("Dehydration", () => {
|
|||||||
await logIntoElement(page, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
// Oh no, we forgot our recovery key - reset our identity
|
// Oh no, we forgot our recovery key - reset our identity
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Can't confirm" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|||||||
@@ -36,50 +36,43 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
expectedBackupVersion = res.expectedBackupVersion;
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the "Use another device" button, and have the bot client auto-accept it.
|
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||||
async function initiateAliceVerificationRequest(page: Page): Promise<JSHandle<VerificationRequest>> {
|
async function initiateAliceVerificationRequest(page: Page): Promise<JSHandle<VerificationRequest>> {
|
||||||
// alice bot waits for verification request
|
// alice bot waits for verification request
|
||||||
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
|
const promiseVerificationRequest = waitForVerificationRequest(aliceBotClient);
|
||||||
|
|
||||||
// Click on "Use another device"
|
// Click on "Verify with another device"
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Use another device" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with another device" }).click();
|
||||||
|
|
||||||
// alice bot responds yes to verification request from alice
|
// alice bot responds yes to verification request from alice
|
||||||
return promiseVerificationRequest;
|
return promiseVerificationRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
"Verify device with SAS during login",
|
await logIntoElement(page, credentials);
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, credentials, homeserver }) => {
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
// Launch the verification request between alice and the bot
|
// Launch the verification request between alice and the bot
|
||||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||||
|
|
||||||
// Handle emoji SAS verification
|
// Handle emoji SAS verification
|
||||||
const infoDialog = page.locator(".mx_InfoDialog");
|
const infoDialog = page.locator(".mx_InfoDialog");
|
||||||
// the bot chooses to do an emoji verification
|
// the bot chooses to do an emoji verification
|
||||||
const verifier = await verificationRequest.evaluateHandle((request) =>
|
const verifier = await verificationRequest.evaluateHandle((request) => request.startVerification("m.sas.v1"));
|
||||||
request.startVerification("m.sas.v1"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle emoji request and check that emojis are matching
|
// Handle emoji request and check that emojis are matching
|
||||||
await doTwoWaySasVerification(page, verifier);
|
await doTwoWaySasVerification(page, verifier);
|
||||||
|
|
||||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||||
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
|
||||||
|
|
||||||
// Check that our device is now cross-signed
|
// Check that our device is now cross-signed
|
||||||
await checkDeviceIsCrossSigned(app);
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||||
// as we need to wait for the secret gossiping to happen.
|
// as we need to wait for the secret gossiping to happen.
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Regression test for https://github.com/element-hq/element-web/issues/29110
|
// Regression test for https://github.com/element-hq/element-web/issues/29110
|
||||||
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
|
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
|
||||||
@@ -124,10 +117,6 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
const toasts = new Toasts(page);
|
const toasts = new Toasts(page);
|
||||||
await toasts.rejectToast("Notifications");
|
await toasts.rejectToast("Notifications");
|
||||||
await toasts.assertNoToasts();
|
await toasts.assertNoToasts();
|
||||||
|
|
||||||
// There may still be a `/sendToDevice/m.secret.request` in flight, which will later throw an error and cause
|
|
||||||
// a *subsequent* test to fail. Tell playwright to ignore any errors resulting from in-flight routes.
|
|
||||||
await page.unrouteAll({ behavior: "ignoreErrors" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
@@ -146,8 +135,8 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Confirm that the bot user scanned successfully
|
// Confirm that the bot user scanned successfully
|
||||||
await expect(infoDialog.getByText("Confirm that you see a green shield on your other device")).toBeVisible();
|
await expect(infoDialog.getByText("Almost there! Is your other device showing the same shield?")).toBeVisible();
|
||||||
await infoDialog.getByRole("button", { name: "Yes, I see a green shield" }).click();
|
await infoDialog.getByRole("button", { name: "Yes" }).click();
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
|
|
||||||
// wait for the bot to see we have finished
|
// wait for the bot to see we have finished
|
||||||
@@ -201,39 +190,15 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
|
await enterRecoveryKeyAndCheckVerified(page, app, recoveryKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("After cancelling verify with another device, I can try again #29882", async ({ page, app, credentials }) => {
|
|
||||||
// Regression test for https://github.com/element-hq/element-web/issues/29882
|
|
||||||
|
|
||||||
// Log in without verifying
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
const authPage = page.locator(".mx_AuthPage");
|
|
||||||
await authPage.getByRole("button", { name: "Skip verification for now" }).click();
|
|
||||||
await authPage.getByRole("button", { name: "I'll verify later" }).click();
|
|
||||||
await page.waitForSelector(".mx_MatrixChat");
|
|
||||||
|
|
||||||
// Start to verify with "Use another device" but cancel
|
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
|
||||||
await page.getByRole("button", { name: "Use another device" }).click();
|
|
||||||
await page.locator("#mx_Dialog_Container").getByRole("button", { name: "Close dialog" }).click();
|
|
||||||
|
|
||||||
// Start again
|
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
|
||||||
|
|
||||||
// We should be offered to use another device again.
|
|
||||||
// (In the bug, we were immediately told that verification has been cancelled.)
|
|
||||||
await expect(page.getByRole("button", { name: "Use another device" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
/** Helper for the three tests above which verify by recovery key */
|
/** Helper for the three tests above which verify by recovery key */
|
||||||
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
||||||
await page.getByRole("button", { name: "Use recovery key" }).click();
|
await page.getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||||
|
|
||||||
// Enter the recovery key
|
// Enter the recovery key
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
|
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
|
||||||
// (cf https://github.com/element-hq/element-web/issues/30089)
|
// (cf https://github.com/element-hq/element-web/issues/30089)
|
||||||
await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
|
await dialog.locator("textarea").pressSequentially(recoveryKey);
|
||||||
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Done" }).click();
|
await page.getByRole("button", { name: "Done" }).click();
|
||||||
@@ -270,7 +235,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
// it should contain the device ID of the requesting device
|
// it should contain the device ID of the requesting device
|
||||||
await expect(toast.getByText(`${aliceBotClient.credentials.deviceId} from `)).toBeVisible();
|
await expect(toast.getByText(`${aliceBotClient.credentials.deviceId} from `)).toBeVisible();
|
||||||
// Accept
|
// Accept
|
||||||
await toast.getByRole("button", { name: "Start verification" }).click();
|
await toast.getByRole("button", { name: "Verify Session" }).click();
|
||||||
|
|
||||||
/* Click 'Start' to start SAS verification */
|
/* Click 'Start' to start SAS verification */
|
||||||
await page.getByRole("button", { name: "Start" }).click();
|
await page.getByRole("button", { name: "Start" }).click();
|
||||||
@@ -285,7 +250,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
/* And we're all done! */
|
/* And we're all done! */
|
||||||
const infoDialog = page.locator(".mx_InfoDialog");
|
const infoDialog = page.locator(".mx_InfoDialog");
|
||||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||||
await expect(infoDialog.getByText("Device verified")).toBeVisible();
|
// We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite
|
||||||
|
await expect(infoDialog.getByText(`You've successfully verified`)).toContainText(
|
||||||
|
`(${aliceBotClient.credentials.deviceId})`,
|
||||||
|
);
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
createSecondBotDevice,
|
createSecondBotDevice,
|
||||||
createSharedRoomWithUser,
|
createSharedRoomWithUser,
|
||||||
enableKeyBackup,
|
enableKeyBackup,
|
||||||
logIntoElementAndVerify,
|
logIntoElement,
|
||||||
logOutOfElement,
|
logOutOfElement,
|
||||||
verify,
|
verify,
|
||||||
waitForDevices,
|
waitForDevices,
|
||||||
@@ -58,108 +58,108 @@ test.describe("Cryptography", function () {
|
|||||||
await app.client.network.setupRoute();
|
await app.client.network.setupRoute();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test("should show the correct shield on e2e events", async ({
|
||||||
"should show the correct shield on e2e events",
|
page,
|
||||||
{ tag: "@screenshot" },
|
app,
|
||||||
async ({ page, app, bot: bob, homeserver }, workerInfo) => {
|
bot: bob,
|
||||||
// Bob has a second, not cross-signed, device
|
homeserver,
|
||||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
}, workerInfo) => {
|
||||||
|
// Bob has a second, not cross-signed, device
|
||||||
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
|
|
||||||
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
||||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||||
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||||
|
|
||||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||||
algorithm: "m.megolm.v1.aes-sha2",
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
ciphertext: "the bird is in the hand",
|
ciphertext: "the bird is in the hand",
|
||||||
});
|
});
|
||||||
|
|
||||||
const last = page.locator(".mx_EventTile_last");
|
const last = page.locator(".mx_EventTile_last");
|
||||||
await expect(last).toContainText("Unable to decrypt message");
|
await expect(last).toContainText("Unable to decrypt message");
|
||||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"This message could not be decrypted",
|
"This message could not be decrypted",
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Should show a red padlock for an unencrypted message in an e2e room */
|
/* Should show a red padlock for an unencrypted message in an e2e room */
|
||||||
await bob.evaluate(
|
await bob.evaluate(
|
||||||
(cli, testRoomId) =>
|
(cli, testRoomId) =>
|
||||||
cli.http.authedRequest(
|
cli.http.authedRequest(
|
||||||
window.matrixcs.Method.Put,
|
window.matrixcs.Method.Put,
|
||||||
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
|
`/rooms/${encodeURIComponent(testRoomId)}/send/m.room.message/test_txn_1`,
|
||||||
undefined,
|
undefined,
|
||||||
{
|
{
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: "test unencrypted",
|
body: "test unencrypted",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
testRoomId,
|
testRoomId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(last).toContainText("test unencrypted");
|
await expect(last).toContainText("test unencrypted");
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png");
|
await lastE2eIcon.focus();
|
||||||
await lastE2eIcon.focus();
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
|
||||||
|
|
||||||
/* Should show no padlock for an unverified user */
|
/* Should show no padlock for an unverified user */
|
||||||
// bob sends a valid event
|
// bob sends a valid event
|
||||||
await bob.sendMessage(testRoomId, "test encrypted 1");
|
await bob.sendMessage(testRoomId, "test encrypted 1");
|
||||||
|
|
||||||
// the message should appear, decrypted, with no warning, but also no "verified"
|
// the message should appear, decrypted, with no warning, but also no "verified"
|
||||||
const lastTile = page.locator(".mx_EventTile_last");
|
const lastTile = page.locator(".mx_EventTile_last");
|
||||||
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
||||||
await expect(lastTile).toContainText("test encrypted 1");
|
await expect(lastTile).toContainText("test encrypted 1");
|
||||||
// no e2e icon
|
// no e2e icon
|
||||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||||
|
|
||||||
/* Now verify Bob */
|
/* Now verify Bob */
|
||||||
await verify(app, bob);
|
await verify(app, bob);
|
||||||
|
|
||||||
/* Existing message should be updated when user is verified. */
|
/* Existing message should be updated when user is verified. */
|
||||||
await expect(last).toContainText("test encrypted 1");
|
await expect(last).toContainText("test encrypted 1");
|
||||||
// still no e2e icon
|
// still no e2e icon
|
||||||
await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
await expect(last.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||||
|
|
||||||
/* should show no padlock, and be verified, for a message from a verified device */
|
/* should show no padlock, and be verified, for a message from a verified device */
|
||||||
await bob.sendMessage(testRoomId, "test encrypted 2");
|
await bob.sendMessage(testRoomId, "test encrypted 2");
|
||||||
|
|
||||||
await expect(lastTile).toContainText("test encrypted 2");
|
await expect(lastTile).toContainText("test encrypted 2");
|
||||||
// no e2e icon
|
// no e2e icon
|
||||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||||
|
|
||||||
/* should show red padlock for a message from an unverified device */
|
/* should show red padlock for a message from an unverified device */
|
||||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||||
await expect(lastTile).toContainText("test encrypted from unverified");
|
await expect(lastTile).toContainText("test encrypted from unverified");
|
||||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastTileE2eIcon.focus();
|
await lastTileE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||||
"Encrypted by a device not verified by its owner.",
|
"Encrypted by a device not verified by its owner.",
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Should show a red padlock for a message from an unverified device.
|
/* Should show a red padlock for a message from an unverified device.
|
||||||
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||||
* unverified, even if it gets deleted. */
|
* unverified, even if it gets deleted. */
|
||||||
// bob deletes his second device
|
// bob deletes his second device
|
||||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||||
|
|
||||||
// wait for the logout to propagate.
|
// wait for the logout to propagate.
|
||||||
await waitForDevices(app, bob.credentials.userId, 1);
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
|
|
||||||
// close and reopen the room, to get the shield to update.
|
// close and reopen the room, to get the shield to update.
|
||||||
await app.viewRoomByName("Bob");
|
await app.viewRoomByName("Bob");
|
||||||
await app.viewRoomByName("TestRoom");
|
await app.viewRoomByName("TestRoom");
|
||||||
|
|
||||||
await expect(last).toContainText("test encrypted from unverified");
|
await expect(last).toContainText("test encrypted from unverified");
|
||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"Encrypted by a device not verified by its owner.",
|
"Encrypted by a device not verified by its owner.",
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
test("Should show a grey padlock for a key restored from backup", async ({
|
test("Should show a grey padlock for a key restored from backup", async ({
|
||||||
page,
|
page,
|
||||||
@@ -195,7 +195,7 @@ test.describe("Cryptography", function () {
|
|||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
});
|
});
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
|
await logIntoElement(page, aliceCredentials, securityKey);
|
||||||
|
|
||||||
/* go back to the test room and find Bob's message again */
|
/* go back to the test room and find Bob's message again */
|
||||||
await app.viewRoomById(testRoomId);
|
await app.viewRoomById(testRoomId);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
import { type GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElementAndVerify } from "./utils";
|
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
|
||||||
import { type Bot } from "../../pages/bot";
|
import { type Bot } from "../../pages/bot";
|
||||||
|
|
||||||
test.describe("Key storage out of sync toast", () => {
|
test.describe("Key storage out of sync toast", () => {
|
||||||
@@ -18,12 +18,12 @@ test.describe("Key storage out of sync toast", () => {
|
|||||||
const res = await createBot(page, homeserver, credentials);
|
const res = await createBot(page, homeserver, credentials);
|
||||||
recoveryKey = res.recoveryKey;
|
recoveryKey = res.recoveryKey;
|
||||||
|
|
||||||
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
|
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||||
|
|
||||||
await deleteCachedSecrets(page);
|
await deleteCachedSecrets(page);
|
||||||
|
|
||||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
@@ -65,10 +65,10 @@ test.describe("'Turn on key storage' toast", () => {
|
|||||||
const recoveryKey = res.recoveryKey;
|
const recoveryKey = res.recoveryKey;
|
||||||
botClient = res.botClient;
|
botClient = res.botClient;
|
||||||
|
|
||||||
await logIntoElementAndVerify(page, credentials, recoveryKey.encodedPrivateKey);
|
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||||
|
|
||||||
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
@@ -126,7 +126,7 @@ test.describe("'Turn on key storage' toast", () => {
|
|||||||
await toast.getByRole("button", { name: "Continue" }).click();
|
await toast.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// Then we see the Encryption settings dialog with an option to turn on key storage
|
// Then we see the Encryption settings dialog with an option to turn on key storage
|
||||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||||
|
|
||||||
// And when we close that
|
// And when we close that
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
@@ -153,7 +153,7 @@ test.describe("'Turn on key storage' toast", () => {
|
|||||||
await page.getByRole("button", { name: "Go to Settings" }).click();
|
await page.getByRole("button", { name: "Go to Settings" }).click();
|
||||||
|
|
||||||
// Then we see Encryption settings again
|
// Then we see Encryption settings again
|
||||||
await expect(page.getByRole("switch", { name: "Allow key storage" })).toBeVisible();
|
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||||
|
|
||||||
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
|||||||
@@ -206,42 +206,32 @@ export async function checkDeviceIsConnectedKeyBackup(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fill in the login form in element with the given creds.
|
* Fill in the login form in element with the given creds.
|
||||||
|
*
|
||||||
|
* If a `securityKey` is given, verifies the new device using the key.
|
||||||
*/
|
*/
|
||||||
export async function logIntoElement(page: Page, credentials: Credentials) {
|
export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) {
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
||||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
await page.getByRole("button", { name: "Sign in" }).click();
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// if a securityKey was given, verify the new device
|
||||||
* Fill in the login form in Element with the given creds, and then complete the `CompleteSecurity` step, using the
|
if (securityKey !== undefined) {
|
||||||
* given recovery key. (Normally this will verify the new device using the secrets from 4S.)
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
*
|
|
||||||
* Afterwards, waits for the application to redirect to the home page.
|
|
||||||
*/
|
|
||||||
export async function logIntoElementAndVerify(page: Page, credentials: Credentials, recoveryKey: string) {
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Use recovery key" }).click();
|
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||||
|
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||||
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "Use recovery key" });
|
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||||
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||||
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
if (await useSecurityKey.isVisible()) {
|
||||||
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
await useSecurityKey.click();
|
||||||
if (await useSecurityKey.isVisible()) {
|
}
|
||||||
await useSecurityKey.click();
|
// Fill in the recovery key
|
||||||
|
await page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
|
||||||
|
await page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
|
await page.getByRole("button", { name: "Done" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill in the recovery key
|
|
||||||
await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(recoveryKey);
|
|
||||||
await page.getByRole("button", { name: "Continue", disabled: false }).click();
|
|
||||||
await page.getByRole("button", { name: "Done" }).click();
|
|
||||||
|
|
||||||
// The application should now redirect to `/#/home`. Wait for that to happen, otherwise if a test immediately does
|
|
||||||
// a `viewRoomById` or similar, it could race.
|
|
||||||
await page.waitForURL("/#/home");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -272,8 +262,8 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
|||||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||||
const settings = await app.settings.openUserSettings("Encryption");
|
const settings = await app.settings.openUserSettings("Encryption");
|
||||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||||
await app.page.getByRole("button", { name: "Use recovery key" }).click();
|
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
|
await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
|
||||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
await app.page.getByRole("button", { name: "Done" }).click();
|
await app.page.getByRole("button", { name: "Done" }).click();
|
||||||
await app.settings.closeDialog();
|
await app.settings.closeDialog();
|
||||||
@@ -310,9 +300,9 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
|||||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
|
|
||||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||||
if (!(await keyStorageToggle.isChecked())) {
|
if (!(await keyStorageToggle.isChecked())) {
|
||||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
||||||
@@ -333,11 +323,11 @@ export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
|||||||
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
||||||
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
|
|
||||||
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
|
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||||
if (await keyStorageToggle.isChecked()) {
|
if (await keyStorageToggle.isChecked()) {
|
||||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).click();
|
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||||
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
||||||
await encryptionTab.getByRole("switch", { name: "Allow key storage" }).isVisible();
|
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
|
||||||
|
|
||||||
// Wait for the update to account data to stick
|
// Wait for the update to account data to stick
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
@@ -438,8 +428,8 @@ export async function sendMessageInCurrentRoom(page: Page, message: string): Pro
|
|||||||
* @param isEncrypted - Whether the room should be encrypted
|
* @param isEncrypted - Whether the room should be encrypted
|
||||||
*/
|
*/
|
||||||
export async function createRoom(page: Page, roomName: string, isEncrypted: boolean): Promise<void> {
|
export async function createRoom(page: Page, roomName: string, isEncrypted: boolean): Promise<void> {
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
await page.locator(".mx_IconizedContextMenu").getByRole("menuitem", { name: "New room" }).click();
|
||||||
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Devtools", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should render the devtools", { tag: "@screenshot" }, async ({ page, homeserver, user, app, axe }) => {
|
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
const composer = app.getComposer().locator("[contenteditable]");
|
|
||||||
await composer.fill("/devtools");
|
|
||||||
await composer.press("Enter");
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
|
||||||
await dialog.getByLabel("Developer mode").check();
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(dialog).toMatchScreenshot("devtools-dialog.png", {
|
|
||||||
css: `.mx_CopyableText {
|
|
||||||
display: none;
|
|
||||||
}`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,38 +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 { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Room upgrade dialog", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should render the room upgrade dialog",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, homeserver, user, app, axe }) => {
|
|
||||||
// Enable developer mode
|
|
||||||
await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true);
|
|
||||||
|
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
const composer = app.getComposer().locator("[contenteditable]");
|
|
||||||
// Pick a room version that is likely to be supported by all our target homeservers.
|
|
||||||
await composer.fill("/upgraderoom 5");
|
|
||||||
await composer.press("Enter");
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
|
||||||
await dialog.getByLabel("Automatically invite members from this room to the new one").check();
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(dialog).toMatchScreenshot("upgrade-room.png");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Decline and block invite dialog", function () {
|
|
||||||
test.use({
|
|
||||||
displayName: "Hanako",
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should show decline and block dialog for a room",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, user, bot, axe }) => {
|
|
||||||
await bot.createRoom({ name: "Test Room", invite: [user.userId] });
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
await page.getByRole("button", { name: "Decline and block" }).click();
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("decline-and-block-invite-empty.png");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -50,9 +50,11 @@ test.describe("Invite dialog", function () {
|
|||||||
await expect(other.locator(".mx_InviteDialog_identityServer")).toBeVisible();
|
await expect(other.locator(".mx_InviteDialog_identityServer")).toBeVisible();
|
||||||
|
|
||||||
// Assert that the bot id is rendered properly
|
// Assert that the bot id is rendered properly
|
||||||
await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
|
await expect(
|
||||||
|
other.locator(".mx_InviteDialog_tile_nameStack_userId").getByText(bot.credentials.userId),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await other.getByRole("option", { name: botName }).click();
|
await other.locator(".mx_InviteDialog_tile_nameStack_name").getByText(botName).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
|
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
|
||||||
@@ -75,8 +77,7 @@ test.describe("Invite dialog", function () {
|
|||||||
"should support inviting a user to Direct Messages",
|
"should support inviting a user to Direct Messages",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, user, bot }) => {
|
async ({ page, app, user, bot }) => {
|
||||||
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
|
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
|
||||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
|
||||||
|
|
||||||
const other = page.locator(".mx_InviteDialog_other");
|
const other = page.locator(".mx_InviteDialog_other");
|
||||||
// Assert that the header is rendered
|
// Assert that the header is rendered
|
||||||
@@ -92,8 +93,10 @@ test.describe("Invite dialog", function () {
|
|||||||
|
|
||||||
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
||||||
|
|
||||||
await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
|
await expect(
|
||||||
await other.getByRole("option", { name: botName }).click();
|
other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId),
|
||||||
|
).toBeVisible();
|
||||||
|
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(botName).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
|
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
|
|
||||||
// Knocked room should appear in Rooms
|
// Knocked room should appear in Rooms
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// bot waits for knock request from Alice
|
// bot waits for knock request from Alice
|
||||||
@@ -77,7 +77,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
await bot.inviteUser(room.roomId, user.userId);
|
await bot.inviteUser(room.roomId, user.userId);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Invites" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Alice have to accept invitation in order to join the room.
|
// Alice have to accept invitation in order to join the room.
|
||||||
@@ -85,7 +85,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByText("Alice joined the room")).toBeVisible();
|
await expect(page.getByText("Alice joined the room")).toBeVisible();
|
||||||
@@ -136,7 +136,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
|
|
||||||
// Knocked room should appear in Rooms
|
// Knocked room should appear in Rooms
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// bot waits for knock request from Alice
|
// bot waits for knock request from Alice
|
||||||
@@ -154,7 +154,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
await bot.inviteUser(room.roomId, user.userId);
|
await bot.inviteUser(room.roomId, user.userId);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Invites" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Alice have to accept invitation in order to join the room.
|
// Alice have to accept invitation in order to join the room.
|
||||||
@@ -162,7 +162,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
await page.locator(".mx_RoomView").getByRole("button", { name: "Accept" }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByText("Alice joined the room")).toBeVisible();
|
await expect(page.getByText("Alice joined the room")).toBeVisible();
|
||||||
@@ -215,14 +215,14 @@ test.describe("Knock Into Room", () => {
|
|||||||
await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible();
|
await expect(roomPreviewBar.getByRole("heading", { name: "Request to join sent" })).toBeVisible();
|
||||||
|
|
||||||
// Knocked room should appear in Rooms
|
// Knocked room should appear in Rooms
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" });
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" });
|
||||||
|
|
||||||
await roomPreviewBar.getByRole("button", { name: "Cancel request" }).click();
|
await roomPreviewBar.getByRole("button", { name: "Cancel request" }).click();
|
||||||
await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join Cybersecurity?" })).toBeVisible();
|
await expect(roomPreviewBar.getByRole("heading", { name: "Ask to join Cybersecurity?" })).toBeVisible();
|
||||||
await expect(roomPreviewBar.getByRole("button", { name: "Request access" })).toBeVisible();
|
await expect(roomPreviewBar.getByRole("button", { name: "Request access" })).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
|
|
||||||
// Knocked room should appear in Rooms
|
// Knocked room should appear in Rooms
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("room-list").getByRole("option", { name: "Open room Cybersecurity" }),
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// bot waits for knock request from Alice
|
// bot waits for knock request from Alice
|
||||||
@@ -262,10 +262,13 @@ test.describe("Knock Into Room", () => {
|
|||||||
await bot.kick(room.roomId, user.userId);
|
await bot.kick(room.roomId, user.userId);
|
||||||
|
|
||||||
// Room should stay in Rooms and have red badge when knock is denied
|
// Room should stay in Rooms and have red badge when knock is denied
|
||||||
|
await expect(
|
||||||
|
page.getByRole("group", { name: "Rooms" }).getByRole("treeitem", { name: "Cybersecurity", exact: true }),
|
||||||
|
).not.toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.getByTestId("room-list")
|
.getByRole("group", { name: "Rooms" })
|
||||||
.getByRole("option", { name: "Open room Cybersecurity with 1 unread mention." }),
|
.getByRole("treeitem", { name: "Cybersecurity 1 unread mention." }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(roomPreviewBar.getByRole("heading", { name: "You have been denied access" })).toBeVisible();
|
await expect(roomPreviewBar.getByRole("heading", { name: "You have been denied access" })).toBeVisible();
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ test.describe("Lazy Loading", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
|
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
|
||||||
// The charlies were running off the bottom of the screen.
|
|
||||||
// We no longer overscan the member list so the result is they are not in the dom.
|
|
||||||
// Increase the viewport size to ensure they are.
|
|
||||||
await page.setViewportSize({ width: 1000, height: 1000 });
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
for (let i = 1; i <= 10; i++) {
|
||||||
const displayName = `Charly #${i}`;
|
const displayName = `Charly #${i}`;
|
||||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ test.describe("LeftPanel", () => {
|
|||||||
// create rooms and check room names are correct
|
// create rooms and check room names are correct
|
||||||
for (const name of ["Apple", "Pineapple", "Orange"]) {
|
for (const name of ["Apple", "Pineapple", "Orange"]) {
|
||||||
await app.client.createRoom({ name });
|
await app.client.createRoom({ name });
|
||||||
await expect(page.getByRole("option", { name: `Open room ${name}` })).toBeVisible();
|
await expect(page.getByRole("treeitem", { name })).toBeVisible();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ test.describe("Room list filters and sort", () => {
|
|||||||
So we expect 'Old Room' to show up in the room list.
|
So we expect 'Old Room' to show up in the room list.
|
||||||
*/
|
*/
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const oldRoomTile = roomListView.getByRole("option", { name: "Open room Old Room" });
|
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
|
||||||
await expect(oldRoomTile).toBeVisible();
|
await expect(oldRoomTile).toBeVisible();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -139,9 +139,8 @@ test.describe("Room list filters and sort", () => {
|
|||||||
|
|
||||||
// Open the non-favourite room
|
// Open the non-favourite room
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const tile = roomListView.getByRole("option", { name: "Open room room-non-fav" });
|
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||||
// item may not be in the DOM using scrollListToBottom rather than scrollIntoViewIfNeeded
|
await tile.scrollIntoViewIfNeeded();
|
||||||
await app.scrollListToBottom(roomListView);
|
|
||||||
await tile.click();
|
await tile.click();
|
||||||
|
|
||||||
// Enable Favourite filter
|
// Enable Favourite filter
|
||||||
@@ -152,7 +151,7 @@ test.describe("Room list filters and sort", () => {
|
|||||||
|
|
||||||
// Ensure the room list is not scrolled
|
// Ensure the room list is not scrolled
|
||||||
const isScrolledDown = await page
|
const isScrolledDown = await page
|
||||||
.getByRole("listbox", { name: "Room list", exact: true })
|
.getByRole("grid", { name: "Room list" })
|
||||||
.evaluate((e) => e.scrollTop !== 0);
|
.evaluate((e) => e.scrollTop !== 0);
|
||||||
expect(isScrolledDown).toStrictEqual(false);
|
expect(isScrolledDown).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
@@ -228,37 +227,37 @@ test.describe("Room list filters and sort", () => {
|
|||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
||||||
// only one room should be visible
|
// only one room should be visible
|
||||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(4);
|
expect(await roomList.locator("role=gridcell").count()).toBe(4);
|
||||||
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "People" }).click();
|
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||||
await expect(roomList.getByRole("option", { name: "unread dm" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(2);
|
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
|
||||||
await expect(roomList.getByRole("option", { name: "unread room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
|
||||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||||
await expect(roomList.getByRole("option", { name: "empty room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
|
||||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||||
await expect(roomList.getByRole("option", { name: "Low prio room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(5);
|
expect(await roomList.locator("role=gridcell").count()).toBe(5);
|
||||||
|
|
||||||
await getFilterExpandButton(page).click();
|
await getFilterExpandButton(page).click();
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||||
await expect(roomList.getByRole("option", { name: "favourite room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||||
await expect(roomList.getByRole("option", { name: "room with mention" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||||
await expect(roomList.getByRole("option", { name: "invited room" })).toBeVisible();
|
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||||
await expect.poll(() => roomList.locator("role=option").count()).toBe(1);
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
await getFilterCollapseButton(page).click();
|
await getFilterCollapseButton(page).click();
|
||||||
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
|
||||||
@@ -269,7 +268,6 @@ test.describe("Room list filters and sort", () => {
|
|||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, bot }) => {
|
async ({ page, app, bot }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
|
|
||||||
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||||
await app.viewRoomById(unReadDmId);
|
await app.viewRoomById(unReadDmId);
|
||||||
@@ -278,20 +276,20 @@ test.describe("Room list filters and sort", () => {
|
|||||||
await app.settings.closeDialog();
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
// Let's open a room other than unread room or unread dm
|
// Let's open a room other than unread room or unread dm
|
||||||
await roomListView.getByRole("option", { name: "Open room favourite room" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||||
|
|
||||||
// Let's make the bot send a new message in both rooms
|
// Let's make the bot send a new message in both rooms
|
||||||
await bot.sendMessage(unReadDmId, "Hello!");
|
await bot.sendMessage(unReadDmId, "Hello!");
|
||||||
await bot.sendMessage(unReadRoomId, "Hello!");
|
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||||
|
|
||||||
// Let's activate the unread filter now
|
// Let's activate the unread filter now
|
||||||
await primaryFilters.getByRole("option", { name: "Unread" }).click();
|
await page.getByRole("option", { name: "Unread" }).click();
|
||||||
|
|
||||||
// Unread filter should only show unread room and not unread dm!
|
// Unread filter should only show unread room and not unread dm!
|
||||||
const unreadDm = roomListView.getByRole("option", { name: "Open room unread room" });
|
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
|
||||||
await expect(unreadDm).toBeVisible();
|
await expect(unreadDm).toBeVisible();
|
||||||
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room unread dm" })).not.toBeVisible();
|
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -301,7 +299,7 @@ test.describe("Room list filters and sort", () => {
|
|||||||
await getRoomOptionsMenu(page).click();
|
await getRoomOptionsMenu(page).click();
|
||||||
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
||||||
|
|
||||||
await expect(roomListView.getByRole("option").first()).toHaveText(/empty room/);
|
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
||||||
@@ -309,7 +307,7 @@ test.describe("Room list filters and sort", () => {
|
|||||||
|
|
||||||
await bot.sendMessage(unReadDmId, "Hello!");
|
await bot.sendMessage(unReadDmId, "Hello!");
|
||||||
|
|
||||||
await expect(roomListView.getByRole("option").first()).toHaveText(/unread dm/);
|
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -322,13 +320,6 @@ test.describe("Room list filters and sort", () => {
|
|||||||
return page.getByTestId("empty-room-list");
|
return page.getByTestId("empty-room-list");
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should render the primary filters", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
|
||||||
const primaryFilters = getPrimaryFilters(page);
|
|
||||||
await expect(primaryFilters).toMatchScreenshot("collapsed-primary-filters.png");
|
|
||||||
await getFilterExpandButton(page).click();
|
|
||||||
await expect(primaryFilters).toMatchScreenshot("expanded-primary-filters.png");
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"should render the default placeholder when there is no filter",
|
"should render the default placeholder when there is no filter",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ test.describe("Header section of the room list", () => {
|
|||||||
|
|
||||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||||
|
|
||||||
// Start chat should open the direct messages dialog
|
// New message should open the direct messages dialog
|
||||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
await page.getByRole("menuitem", { name: "New message" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
||||||
await app.closeDialog();
|
await app.closeDialog();
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ test.describe("Room list panel", () => {
|
|||||||
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomListView(page);
|
const roomListView = getRoomListView(page);
|
||||||
// Wait for the last room to be visible
|
// Wait for the last room to be visible
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room room19" })).toBeVisible();
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
|
||||||
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,38 +41,28 @@ test.describe("Room list", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
|
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
|
||||||
await expect(roomListView).toMatchScreenshot("room-list.png");
|
await expect(roomListView).toMatchScreenshot("room-list.png");
|
||||||
|
|
||||||
// Put focus on the room list
|
// Put focus on the room list
|
||||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
// Scroll to the end of the room list
|
// Scroll to the end of the room list
|
||||||
await app.scrollListToBottom(roomListView);
|
await page.mouse.wheel(0, 1000);
|
||||||
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||||
// scrollListToBottom seems to leave the mouse hovered over the list, move it away.
|
|
||||||
await page.getByRole("button", { name: "User menu" }).hover();
|
|
||||||
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
test("should open the room when it is clicked", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" });
|
|
||||||
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||||
await roomItem.hover();
|
await roomItem.hover();
|
||||||
|
|
||||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||||
@@ -102,7 +92,7 @@ test.describe("Room list", () => {
|
|||||||
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
|
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||||
await roomItem.hover();
|
await roomItem.hover();
|
||||||
|
|
||||||
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
|
||||||
@@ -119,13 +109,10 @@ test.describe("Room list", () => {
|
|||||||
// It should make the room muted
|
// It should make the room muted
|
||||||
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
||||||
|
|
||||||
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
|
|
||||||
|
|
||||||
// Put focus on the room list
|
// Put focus on the room list
|
||||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||||
|
|
||||||
// Scroll to the end of the room list
|
// Scroll to the end of the room list
|
||||||
await app.scrollListToBottom(roomListView);
|
await page.mouse.wheel(0, 1000);
|
||||||
|
|
||||||
// The room decoration should have the muted icon
|
// The room decoration should have the muted icon
|
||||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||||
@@ -144,25 +131,24 @@ test.describe("Room list", () => {
|
|||||||
test("should scroll to the current room", async ({ page, app, user }) => {
|
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
// Put focus on the room list
|
// Put focus on the room list
|
||||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
// Scroll to the end of the room list
|
// Scroll to the end of the room list
|
||||||
await app.scrollListToBottom(roomListView);
|
await page.mouse.wheel(0, 1000);
|
||||||
|
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||||
await roomListView.getByRole("option", { name: "Open room room0" }).click();
|
|
||||||
|
|
||||||
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||||
await filters.getByRole("option", { name: "People" }).click();
|
await filters.getByRole("option", { name: "People" }).click();
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible();
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||||
|
|
||||||
await filters.getByRole("option", { name: "People" }).click();
|
await filters.getByRole("option", { name: "People" }).click();
|
||||||
await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Shortcuts", () => {
|
test.describe("Shortcuts", () => {
|
||||||
test("should select the next room", async ({ page, app, user }) => {
|
test("should select the next room", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
await page.keyboard.press("Alt+ArrowDown");
|
await page.keyboard.press("Alt+ArrowDown");
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||||
@@ -170,7 +156,7 @@ test.describe("Room list", () => {
|
|||||||
|
|
||||||
test("should select the previous room", async ({ page, app, user }) => {
|
test("should select the previous room", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
await roomListView.getByRole("option", { name: "Open room room28" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||||
await page.keyboard.press("Alt+ArrowUp");
|
await page.keyboard.press("Alt+ArrowUp");
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||||
@@ -178,7 +164,7 @@ test.describe("Room list", () => {
|
|||||||
|
|
||||||
test("should select the last room", async ({ page, app, user }) => {
|
test("should select the last room", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
await roomListView.getByRole("option", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
await page.keyboard.press("Alt+ArrowUp");
|
await page.keyboard.press("Alt+ArrowUp");
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||||
@@ -192,10 +178,7 @@ test.describe("Room list", () => {
|
|||||||
await bot.joinRoom(roomId);
|
await bot.joinRoom(roomId);
|
||||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
await roomListView.getByRole("option", { name: "Open room room20" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
|
||||||
|
|
||||||
// Make sure the room with the unread is visible before we press the keyboard action to select it
|
|
||||||
await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();
|
|
||||||
|
|
||||||
await page.keyboard.press("Alt+Shift+ArrowDown");
|
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||||
|
|
||||||
@@ -207,8 +190,8 @@ test.describe("Room list", () => {
|
|||||||
test("should navigate to the room list", async ({ page, app, user }) => {
|
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||||
const room28 = roomListView.getByRole("option", { name: "Open room room28" });
|
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
|
||||||
|
|
||||||
// open the room
|
// open the room
|
||||||
await room29.click();
|
await room29.click();
|
||||||
@@ -227,7 +210,7 @@ test.describe("Room list", () => {
|
|||||||
|
|
||||||
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const room29 = roomListView.getByRole("option", { name: "Open room room29" });
|
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||||
const moreButton = room29.getByRole("button", { name: "More options" });
|
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||||
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||||
|
|
||||||
@@ -240,37 +223,17 @@ test.describe("Room list", () => {
|
|||||||
await expect(notificationButton).toBeFocused();
|
await expect(notificationButton).toBeFocused();
|
||||||
|
|
||||||
// Open the menu
|
// Open the menu
|
||||||
await page.keyboard.press("Enter");
|
await notificationButton.click();
|
||||||
// Wait for the menu to be open
|
// Wait for the menu to be open
|
||||||
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||||
"aria-selected",
|
"aria-selected",
|
||||||
"true",
|
"true",
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.keyboard.press("ArrowDown");
|
// Close the menu
|
||||||
await page.keyboard.press("Escape");
|
await page.keyboard.press("Escape");
|
||||||
// Focus should be back on the notification button
|
// Focus should be back on the room list item
|
||||||
await expect(notificationButton).toBeFocused();
|
await expect(room29).toBeFocused();
|
||||||
});
|
|
||||||
|
|
||||||
test("should navigate to the top and then bottom of the room list", async ({ page, app, user }) => {
|
|
||||||
const roomListView = getRoomList(page);
|
|
||||||
|
|
||||||
const topRoom = roomListView.getByRole("option", { name: "Open room room29" });
|
|
||||||
|
|
||||||
// open the room
|
|
||||||
await topRoom.click();
|
|
||||||
// put focus back on the room list item
|
|
||||||
await topRoom.click();
|
|
||||||
await expect(topRoom).toBeFocused();
|
|
||||||
|
|
||||||
await page.keyboard.press("End");
|
|
||||||
const bottomRoom = roomListView.getByRole("option", { name: "Open room room0" });
|
|
||||||
await expect(bottomRoom).toBeFocused();
|
|
||||||
|
|
||||||
await page.keyboard.press("Home");
|
|
||||||
const topRoomAgain = roomListView.getByRole("option", { name: "Open room room29" });
|
|
||||||
await expect(topRoomAgain).toBeFocused();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -286,7 +249,7 @@ test.describe("Room list", () => {
|
|||||||
await page.getByRole("button", { name: "User menu" }).focus();
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const publicRoom = roomListView.getByRole("option", { name: "public room" });
|
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||||
|
|
||||||
await expect(publicRoom).toBeVisible();
|
await expect(publicRoom).toBeVisible();
|
||||||
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||||
@@ -296,7 +259,7 @@ test.describe("Room list", () => {
|
|||||||
// @ts-ignore Visibility enum is not accessible
|
// @ts-ignore Visibility enum is not accessible
|
||||||
await app.client.createRoom({ name: "low priority room", visibility: "public" });
|
await app.client.createRoom({ name: "low priority room", visibility: "public" });
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const publicRoom = roomListView.getByRole("option", { name: "low priority room" });
|
const publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
|
||||||
|
|
||||||
// Make room low priority
|
// Make room low priority
|
||||||
await publicRoom.hover();
|
await publicRoom.hover();
|
||||||
@@ -321,7 +284,7 @@ test.describe("Room list", () => {
|
|||||||
await page.getByRole("button", { name: "Create video room" }).click();
|
await page.getByRole("button", { name: "Create video room" }).click();
|
||||||
|
|
||||||
const roomListView = getRoomList(page);
|
const roomListView = getRoomList(page);
|
||||||
const videoRoom = roomListView.getByRole("option", { name: "video room" });
|
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||||
|
|
||||||
// focus the user menu to avoid to have hover decoration
|
// focus the user menu to avoid to have hover decoration
|
||||||
await page.getByRole("button", { name: "User menu" }).focus();
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
@@ -340,7 +303,7 @@ test.describe("Room list", () => {
|
|||||||
invite: [user.userId],
|
invite: [user.userId],
|
||||||
is_direct: true,
|
is_direct: true,
|
||||||
});
|
});
|
||||||
const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
|
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||||
await expect(invitedRoom).toBeVisible();
|
await expect(invitedRoom).toBeVisible();
|
||||||
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||||
});
|
});
|
||||||
@@ -355,7 +318,7 @@ test.describe("Room list", () => {
|
|||||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
const room = roomListView.getByRole("option", { name: "2 notifications" });
|
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||||
await expect(room).toBeVisible();
|
await expect(room).toBeVisible();
|
||||||
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||||
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||||
@@ -386,7 +349,7 @@ test.describe("Room list", () => {
|
|||||||
);
|
);
|
||||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
const room = roomListView.getByRole("option", { name: "mention" });
|
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||||
await expect(room).toBeVisible();
|
await expect(room).toBeVisible();
|
||||||
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||||
});
|
});
|
||||||
@@ -407,7 +370,7 @@ test.describe("Room list", () => {
|
|||||||
await bot.joinRoom(roomId);
|
await bot.joinRoom(roomId);
|
||||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
const room = roomListView.getByRole("option", { name: "activity" });
|
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||||
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
||||||
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
||||||
});
|
});
|
||||||
@@ -434,7 +397,7 @@ test.describe("Room list", () => {
|
|||||||
await app.viewRoomById(otherRoomId);
|
await app.viewRoomById(otherRoomId);
|
||||||
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
const room = roomListView.getByRole("option", { name: "activity" });
|
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||||
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||||
});
|
});
|
||||||
@@ -446,7 +409,7 @@ test.describe("Room list", () => {
|
|||||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
await bot.joinRoom(roomId);
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
const room = roomListView.getByRole("option", { name: "mark as unread" });
|
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||||
await room.hover();
|
await room.hover();
|
||||||
await room.getByRole("button", { name: "More Options" }).click();
|
await room.getByRole("button", { name: "More Options" }).click();
|
||||||
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||||
@@ -469,7 +432,7 @@ test.describe("Room list", () => {
|
|||||||
await page.getByText("Off").click();
|
await page.getByText("Off").click();
|
||||||
await app.settings.closeDialog();
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
const room = roomListView.getByRole("option", { name: "silent" });
|
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||||
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||||
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024, 2025 New Vector Ltd.
|
Copyright 2024 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
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
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -57,26 +57,4 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
|
|||||||
|
|
||||||
await expect(page.locator(".mx_Marker")).toBeVisible();
|
await expect(page.locator(".mx_Marker")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
|
||||||
"is prompted for and can consent to live location sharing",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, user, app, axe }) => {
|
|
||||||
await app.viewRoomById(await app.client.createRoom({}));
|
|
||||||
|
|
||||||
const composerOptions = await app.openMessageComposerOptions();
|
|
||||||
await composerOptions.getByRole("menuitem", { name: "Location", exact: true }).click();
|
|
||||||
const menu = page.locator(".mx_LocationShareMenu");
|
|
||||||
|
|
||||||
await menu.getByRole("button", { name: "My live location" }).click();
|
|
||||||
await menu.getByLabel("Enable live location sharing").check();
|
|
||||||
|
|
||||||
axe.disableRules([
|
|
||||||
"color-contrast", // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
"region", // XXX: ContextMenu managed=false does not provide a role.
|
|
||||||
]);
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
await expect(menu).toMatchScreenshot("location-live-share-dialog.png");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ test.describe("Login", () => {
|
|||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver, credentials);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
||||||
});
|
});
|
||||||
@@ -219,7 +219,7 @@ test.describe("Login", () => {
|
|||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver, credentials);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Skip verification for now" })).toBeVisible();
|
||||||
});
|
});
|
||||||
@@ -254,10 +254,10 @@ test.describe("Login", () => {
|
|||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver, credentials);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
const h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
|
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
await expect(h2).toBeVisible();
|
await expect(h1).toBeVisible();
|
||||||
|
|
||||||
await expect(h2.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Continues to show verification prompt after cancelling device verification", async ({
|
test("Continues to show verification prompt after cancelling device verification", async ({
|
||||||
@@ -274,18 +274,18 @@ test.describe("Login", () => {
|
|||||||
// Load the page and see that we are asked to verify
|
// Load the page and see that we are asked to verify
|
||||||
await page.goto("/#/welcome");
|
await page.goto("/#/welcome");
|
||||||
await login(page, homeserver, credentials);
|
await login(page, homeserver, credentials);
|
||||||
let h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
|
let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
await expect(h2).toBeVisible();
|
await expect(h1).toBeVisible();
|
||||||
|
|
||||||
// Click "Use another device"
|
// Click "Verify with another device"
|
||||||
await page.getByRole("button", { name: "Use another device" }).click();
|
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||||
|
|
||||||
// Cancel the new dialog
|
// Cancel the new dialog
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
|
||||||
// Check that we are still being asked to verify
|
// Check that we are still being asked to verify
|
||||||
h2 = page.getByRole("heading", { name: "Confirm your identity", level: 2 });
|
h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
await expect(h2).toBeVisible();
|
await expect(h1).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,18 +303,18 @@ test.describe("Login", () => {
|
|||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver, credentials);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Confirm your identity", level: 2 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||||
|
|
||||||
// Start the reset process
|
// Start the reset process
|
||||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
|
|
||||||
// First try cancelling and restarting
|
// First try cancelling and restarting
|
||||||
await page.getByRole("button", { name: "Cancel" }).click();
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
|
|
||||||
// Then click outside the dialog and restart
|
// Then click outside the dialog and restart
|
||||||
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
|
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
|
||||||
await page.getByRole("button", { name: "Can't confirm?" }).click();
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
|
|
||||||
// Finally we actually continue
|
// Finally we actually continue
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { type Locator, type Page } from "@playwright/test";
|
|||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
async function sendMessage(page: Page, message: string): Promise<Locator> {
|
async function sendMessage(page: Page, message: string): Promise<Locator> {
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill(message);
|
await page.getByRole("textbox", { name: "Send a message…" }).fill(message);
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
const msgTile = page.locator(".mx_EventTile_last");
|
const msgTile = page.locator(".mx_EventTile_last");
|
||||||
@@ -22,7 +22,7 @@ async function sendMessage(page: Page, message: string): Promise<Locator> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendMultilineMessages(page: Page, messages: string[]) {
|
async function sendMultilineMessages(page: Page, messages: string[]) {
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).focus();
|
await page.getByRole("textbox", { name: "Send a message…" }).focus();
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
await page.keyboard.type(messages[i]);
|
await page.keyboard.type(messages[i]);
|
||||||
if (i < messages.length - 1) await page.keyboard.press("Shift+Enter");
|
if (i < messages.length - 1) await page.keyboard.press("Shift+Enter");
|
||||||
@@ -40,7 +40,7 @@ async function replyMessage(page: Page, message: Locator, replyMessage: string):
|
|||||||
await line.hover();
|
await line.hover();
|
||||||
await line.getByRole("button", { name: "Reply", exact: true }).click();
|
await line.getByRole("button", { name: "Reply", exact: true }).click();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Send an unencrypted reply…" }).fill(replyMessage);
|
await page.getByRole("textbox", { name: "Send a reply…" }).fill(replyMessage);
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
const msgTile = page.locator(".mx_EventTile_last");
|
const msgTile = page.locator(".mx_EventTile_last");
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
import { MobileAppVariant } from "../../../src/vector/mobile_guide/mobile-apps";
|
|
||||||
|
|
||||||
const variants = [MobileAppVariant.Classic, MobileAppVariant.X, MobileAppVariant.Pro];
|
|
||||||
|
|
||||||
test.describe("Mobile Guide Screenshots", { tag: "@screenshot" }, () => {
|
|
||||||
for (const variant of variants) {
|
|
||||||
test.describe(`for variant ${variant}`, () => {
|
|
||||||
test.use({
|
|
||||||
config: {
|
|
||||||
default_server_config: {
|
|
||||||
"m.homeserver": {
|
|
||||||
base_url: "https://matrix.server.invalid",
|
|
||||||
server_name: "server.invalid",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mobile_guide_app_variant: variant,
|
|
||||||
},
|
|
||||||
viewport: { width: 390, height: 844 }, // iPhone 16e
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should match the mobile_guide screenshot", async ({ page, axe }) => {
|
|
||||||
await page.goto("/mobile_guide/");
|
|
||||||
await expect(page).toMatchScreenshot(`mobile-guide-${variant}.png`);
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -6,7 +6,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Page } from "@playwright/test";
|
import { type Page } from "@playwright/test";
|
||||||
import fs from "node:fs";
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
@@ -23,9 +22,6 @@ const screenshotOptions = (page: Page) => ({
|
|||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const IMAGE_FILE = fs.readFileSync("playwright/sample-files/element.png");
|
|
||||||
|
|
||||||
test.describe("Custom Component API", () => {
|
test.describe("Custom Component API", () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Manny",
|
displayName: "Manny",
|
||||||
@@ -88,50 +84,6 @@ test.describe("Custom Component API", () => {
|
|||||||
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }),
|
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
test("should disallow downloading media when the allowDownloading hint is set to false", async ({
|
|
||||||
page,
|
|
||||||
room,
|
|
||||||
app,
|
|
||||||
}) => {
|
|
||||||
await app.viewRoomById(room.roomId);
|
|
||||||
await app.viewRoomById(room.roomId);
|
|
||||||
const upload = await app.client.uploadContent(IMAGE_FILE, { name: "bad.png", type: "image/png" });
|
|
||||||
await app.client.sendEvent(room.roomId, null, "m.room.message", {
|
|
||||||
msgtype: "m.image",
|
|
||||||
body: "bad.png",
|
|
||||||
url: upload.content_uri,
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.timeline.scrollToBottom();
|
|
||||||
const imgTile = page.locator(".mx_MImageBody").first();
|
|
||||||
await expect(imgTile).toBeVisible();
|
|
||||||
await imgTile.hover();
|
|
||||||
await expect(page.getByRole("button", { name: "Download" })).not.toBeVisible();
|
|
||||||
await imgTile.click();
|
|
||||||
await expect(page.getByLabel("Image view").getByLabel("Download")).not.toBeVisible();
|
|
||||||
});
|
|
||||||
test("should allow downloading media when the allowDownloading hint is set to true", async ({
|
|
||||||
page,
|
|
||||||
room,
|
|
||||||
app,
|
|
||||||
}) => {
|
|
||||||
await app.viewRoomById(room.roomId);
|
|
||||||
await app.viewRoomById(room.roomId);
|
|
||||||
const upload = await app.client.uploadContent(IMAGE_FILE, { name: "good.png", type: "image/png" });
|
|
||||||
await app.client.sendEvent(room.roomId, null, "m.room.message", {
|
|
||||||
msgtype: "m.image",
|
|
||||||
body: "good.png",
|
|
||||||
url: upload.content_uri,
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.timeline.scrollToBottom();
|
|
||||||
const imgTile = page.locator(".mx_MImageBody").first();
|
|
||||||
await expect(imgTile).toBeVisible();
|
|
||||||
await imgTile.hover();
|
|
||||||
await expect(page.getByRole("button", { name: "Download" })).toBeVisible();
|
|
||||||
await imgTile.click();
|
|
||||||
await expect(page.getByLabel("Image view").getByLabel("Download")).toBeVisible();
|
|
||||||
});
|
|
||||||
test(
|
test(
|
||||||
"should render the next registered component if the filter function throws",
|
"should render the next registered component if the filter function throws",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
test(
|
test(
|
||||||
"it should log out the user & wipe data when logging out via MAS",
|
"it should log out the user & wipe data when logging out via MAS",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ mas, page, mailpitClient, homeserver }, testInfo) => {
|
async ({ mas, page, mailpitClient }, testInfo) => {
|
||||||
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
|
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
@@ -99,7 +99,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByText("For security, this session has been signed out. Please sign in again."),
|
page.getByText("For security, this session has been signed out. Please sign in again."),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
//await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
|
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
|
||||||
|
|
||||||
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
|
||||||
expect(localStorageKeys).toHaveLength(0);
|
expect(localStorageKeys).toHaveLength(0);
|
||||||
@@ -129,8 +129,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// We should be in
|
// We should be in (we see an error because we have no recovery key).
|
||||||
await expect(page.getByText("Confirm your identity")).toBeVisible();
|
await expect(page.getByText("Unable to verify this device")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("with force_verification on", () => {
|
test.describe("with force_verification on", () => {
|
||||||
@@ -162,7 +162,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await page.getByRole("button", { name: "Continue" }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// We should be being warned that we need to verify (but we can't)
|
// We should be being warned that we need to verify (but we can't)
|
||||||
await expect(page.getByText("Confirm your identity")).toBeVisible();
|
await expect(page.getByText("Unable to verify this device")).toBeVisible();
|
||||||
|
|
||||||
// And there should be no way to close this prompt
|
// And there should be no way to close this prompt
|
||||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
|
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
|
||||||
@@ -210,7 +210,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
|
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
|
||||||
|
|
||||||
// When we start verifying with another device
|
// When we start verifying with another device
|
||||||
await page.getByRole("button", { name: "Use another device" }).click();
|
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||||
|
|
||||||
// And then cancel it
|
// And then cancel it
|
||||||
await page.getByRole("button", { name: "Close dialog" }).click();
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
@@ -227,8 +227,8 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
* Perform interactive emoji verification for a new device.
|
* Perform interactive emoji verification for a new device.
|
||||||
*/
|
*/
|
||||||
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
|
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
|
||||||
await deviceToVerifyPage.getByRole("button", { name: "Use another device" }).click();
|
await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click();
|
||||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start verification" }).click();
|
await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
|
||||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
|
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
|
||||||
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
|
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
|
||||||
await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
|
await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
|
||||||
|
|||||||
@@ -100,51 +100,3 @@ test.describe("permalinks", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("triple-click message selection", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Alice",
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should select entire message line when triple-clicking on message with pills", async ({
|
|
||||||
page,
|
|
||||||
app,
|
|
||||||
user,
|
|
||||||
bot,
|
|
||||||
}) => {
|
|
||||||
await bot.prepareClient();
|
|
||||||
|
|
||||||
const roomId = await app.client.createRoom({ name: "Test Room" });
|
|
||||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
|
||||||
await app.viewRoomByName("Test Room");
|
|
||||||
|
|
||||||
// Send a message with user and room pills
|
|
||||||
await app.client.sendMessage(
|
|
||||||
roomId,
|
|
||||||
`Testing triple-click message selection. ` +
|
|
||||||
`User: ${permalinkPrefix}${bot.credentials.userId}, ` +
|
|
||||||
`Room: ${permalinkPrefix}${roomId}, ` +
|
|
||||||
`Message: ${permalinkPrefix}${roomId}/$dummy-event, ` +
|
|
||||||
`and @room mention.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeline = page.locator(".mx_RoomView_timeline");
|
|
||||||
const messageTile = timeline.locator(".mx_EventTile").last();
|
|
||||||
|
|
||||||
// Triple-click on the message body to select its entire content
|
|
||||||
const messageBody = messageTile.locator(".mx_EventTile_body");
|
|
||||||
await messageBody.click({ clickCount: 3 });
|
|
||||||
|
|
||||||
// Get the expected text content of the message, including pills
|
|
||||||
const expectedText = await messageBody.innerText();
|
|
||||||
|
|
||||||
// Get the currently selected text from the page
|
|
||||||
const selectedText = await page.evaluate(() => {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
return selection ? selection.toString().trim() : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Verify that the selected text exactly matches the message content
|
|
||||||
expect(selectedText).toBe(expectedText);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { JSHandle, Locator, Page } from "@playwright/test";
|
import type { JSHandle, Page } from "@playwright/test";
|
||||||
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
|
import type { MatrixEvent, Room, IndexedDBStore, ReceiptType } from "matrix-js-sdk/src/matrix";
|
||||||
import { test as base, expect } from "../../element-web-test";
|
import { test as base, expect } from "../../element-web-test";
|
||||||
import { type Bot } from "../../pages/bot";
|
import { type Bot } from "../../pages/bot";
|
||||||
@@ -428,7 +428,7 @@ class Helpers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRoomListTile(label: string) {
|
getRoomListTile(label: string) {
|
||||||
return this.page.getByRole("option", { name: new RegExp("^Open room " + label) });
|
return this.page.getByRole("treeitem", { name: new RegExp("^" + label) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -446,8 +446,8 @@ class Helpers {
|
|||||||
*/
|
*/
|
||||||
async assertRead(room: RoomRef) {
|
async assertRead(room: RoomRef) {
|
||||||
const tile = this.getRoomListTile(room.name);
|
const tile = this.getRoomListTile(room.name);
|
||||||
await expect(tile.getByTestId("notification-decoration")).not.toBeVisible();
|
await expect(tile.locator(".mx_NotificationBadge_dot")).not.toBeVisible();
|
||||||
await expect(tile).not.toHaveAccessibleName(/with \d* unread message/);
|
await expect(tile.locator(".mx_NotificationBadge_count")).not.toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -463,18 +463,15 @@ class Helpers {
|
|||||||
/**
|
/**
|
||||||
* Assert a given room is marked as unread (via the room list tile)
|
* Assert a given room is marked as unread (via the room list tile)
|
||||||
* @param room - the name of the room to check
|
* @param room - the name of the room to check
|
||||||
* @param count - the numeric count to assert
|
* @param count - the numeric count to assert, or if "." specified then a bold/dot (no count) state is asserted
|
||||||
*/
|
*/
|
||||||
async assertUnread(room: RoomRef, count: number) {
|
async assertUnread(room: RoomRef, count: number | ".") {
|
||||||
const tile = this.getRoomListTile(room.name);
|
const tile = this.getRoomListTile(room.name);
|
||||||
await expect(tile).toBeVisible();
|
if (count === ".") {
|
||||||
await expect(tile).toHaveAccessibleName(/with \d* unread message/);
|
await expect(tile.locator(".mx_NotificationBadge_dot")).toBeVisible();
|
||||||
}
|
} else {
|
||||||
|
await expect(tile.locator(".mx_NotificationBadge_count")).toHaveText(count.toString());
|
||||||
async unreadCountForRoomTile(tile: Locator): Promise<number> {
|
}
|
||||||
const accessibleName = await tile.getAttribute("aria-label");
|
|
||||||
const match = accessibleName?.match(/(\d+)\s+unread message/);
|
|
||||||
return match ? parseInt(match[1], 10) : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -490,7 +487,7 @@ class Helpers {
|
|||||||
// .toBeLessThan doesn't have a retry mechanism, so we use .poll
|
// .toBeLessThan doesn't have a retry mechanism, so we use .poll
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
return this.unreadCountForRoomTile(tile);
|
return parseInt(await tile.locator(".mx_NotificationBadge_count").textContent(), 10);
|
||||||
})
|
})
|
||||||
.toBeLessThan(lessThan);
|
.toBeLessThan(lessThan);
|
||||||
}
|
}
|
||||||
@@ -508,7 +505,7 @@ class Helpers {
|
|||||||
// .toBeGreaterThan doesn't have a retry mechanism, so we use .poll
|
// .toBeGreaterThan doesn't have a retry mechanism, so we use .poll
|
||||||
await expect
|
await expect
|
||||||
.poll(async () => {
|
.poll(async () => {
|
||||||
return this.unreadCountForRoomTile(tile);
|
return parseInt(await tile.locator(".mx_NotificationBadge_count").textContent(), 10);
|
||||||
})
|
})
|
||||||
.toBeGreaterThan(greaterThan);
|
.toBeGreaterThan(greaterThan);
|
||||||
}
|
}
|
||||||
@@ -599,15 +596,24 @@ class Helpers {
|
|||||||
await button.click();
|
await button.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the `Show rooms with unread messages first` option for the room list
|
||||||
|
*/
|
||||||
|
async toggleRoomUnreadOrder() {
|
||||||
|
await this.toggleRoomListMenu();
|
||||||
|
await this.page.getByText("Show rooms with unread messages first").click();
|
||||||
|
// Close contextual menu
|
||||||
|
await this.page.locator(".mx_ContextualMenu_background").click();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that the room list is ordered as expected
|
* Assert that the room list is ordered as expected
|
||||||
* @param rooms
|
* @param rooms
|
||||||
*/
|
*/
|
||||||
async assertRoomListOrder(rooms: Array<{ name: string }>) {
|
async assertRoomListOrder(rooms: Array<{ name: string }>) {
|
||||||
const roomListContainer = this.page.getByTestId("room-list");
|
const roomList = this.page.locator(".mx_RoomTile_title");
|
||||||
const roomTiles = roomListContainer.getByRole("option");
|
|
||||||
for (const [i, room] of rooms.entries()) {
|
for (const [i, room] of rooms.entries()) {
|
||||||
await expect(roomTiles.nth(i)).toHaveAccessibleName(new RegExp(`${room.name}`));
|
await expect(roomList.nth(i)).toHaveText(room.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
const main3 = await sendMessage(bot);
|
const main3 = await sendMessage(bot);
|
||||||
|
|
||||||
// (So the room starts off unread)
|
// (So the room starts off unread)
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send a threaded receipt for the last event in main
|
// When we send a threaded receipt for the last event in main
|
||||||
// And an unthreaded receipt for an earlier event
|
// And an unthreaded receipt for an earlier event
|
||||||
@@ -147,13 +147,13 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
await sendMessage(bot);
|
await sendMessage(bot);
|
||||||
|
|
||||||
// (The room starts off unread)
|
// (The room starts off unread)
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send a threaded receipt for the second-last event in main
|
// When we send a threaded receipt for the second-last event in main
|
||||||
await sendThreadedReadReceipt(app, main2);
|
await sendThreadedReadReceipt(app, main2);
|
||||||
|
|
||||||
// Then the room has only one unread
|
// Then the room has only one unread
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Considers room read if there is only a main thread and we have a main receipt", async ({
|
test("Considers room read if there is only a main thread and we have a main receipt", async ({
|
||||||
@@ -166,7 +166,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
await sendMessage(bot);
|
await sendMessage(bot);
|
||||||
const main3 = await sendMessage(bot);
|
const main3 = await sendMessage(bot);
|
||||||
// (The room starts off unread)
|
// (The room starts off unread)
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 3 unread messages.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 3 unread messages.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send a threaded receipt for the last event in main
|
// When we send a threaded receipt for the last event in main
|
||||||
await sendThreadedReadReceipt(app, main3);
|
await sendThreadedReadReceipt(app, main3);
|
||||||
@@ -186,7 +186,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
const thread1a = await botSendThreadMessage(bot, main1.event_id);
|
const thread1a = await botSendThreadMessage(bot, main1.event_id);
|
||||||
await botSendThreadMessage(bot, main1.event_id);
|
await botSendThreadMessage(bot, main1.event_id);
|
||||||
// 1 unread on the main thread, 2 in the new thread that aren't shown
|
// 1 unread on the main thread, 2 in the new thread that aren't shown
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send receipts for main, and the second-last in the thread
|
// When we send receipts for main, and the second-last in the thread
|
||||||
await sendThreadedReadReceipt(app, main1);
|
await sendThreadedReadReceipt(app, main1);
|
||||||
@@ -203,7 +203,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
await botSendThreadMessage(bot, main1.event_id);
|
await botSendThreadMessage(bot, main1.event_id);
|
||||||
const thread1b = await botSendThreadMessage(bot, main1.event_id);
|
const thread1b = await botSendThreadMessage(bot, main1.event_id);
|
||||||
// 1 unread on the main thread, 2 in the new thread which don't show
|
// 1 unread on the main thread, 2 in the new thread which don't show
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send receipts for main, and the last in the thread
|
// When we send receipts for main, and the last in the thread
|
||||||
await sendThreadedReadReceipt(app, main1);
|
await sendThreadedReadReceipt(app, main1);
|
||||||
@@ -226,7 +226,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
const thread1a = await botSendThreadMessage(bot, main1.event_id);
|
const thread1a = await botSendThreadMessage(bot, main1.event_id);
|
||||||
await botSendThreadMessage(bot, main1.event_id);
|
await botSendThreadMessage(bot, main1.event_id);
|
||||||
// 1 unread on the main thread, 2 in the new thread which don't count
|
// 1 unread on the main thread, 2 in the new thread which don't count
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send an unthreaded receipt for the second-last in the thread
|
// When we send an unthreaded receipt for the second-last in the thread
|
||||||
await sendUnthreadedReadReceipt(app, thread1a);
|
await sendUnthreadedReadReceipt(app, thread1a);
|
||||||
@@ -251,7 +251,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
const thread1b = await botSendThreadMessage(bot, main1.event_id);
|
const thread1b = await botSendThreadMessage(bot, main1.event_id);
|
||||||
await sendMessage(bot);
|
await sendMessage(bot);
|
||||||
// 2 unreads on the main thread, 2 in the new thread which don't count
|
// 2 unreads on the main thread, 2 in the new thread which don't count
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 2 unread messages.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 2 unread messages.`)).toBeVisible();
|
||||||
|
|
||||||
// When we send an unthreaded receipt for the last in the thread
|
// When we send an unthreaded receipt for the last in the thread
|
||||||
await sendUnthreadedReadReceipt(app, thread1b);
|
await sendUnthreadedReadReceipt(app, thread1b);
|
||||||
@@ -259,7 +259,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
// Then the room has only one unread - the one in the
|
// Then the room has only one unread - the one in the
|
||||||
// main thread, because it is later than the unthreaded
|
// main thread, because it is later than the unthreaded
|
||||||
// receipt.
|
// receipt.
|
||||||
await expect(page.getByLabel(`${otherRoomName} with 1 unread message.`)).toBeVisible();
|
await expect(page.getByLabel(`${otherRoomName} 1 unread message.`)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,9 +291,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
|
const uriEncodedLastMessageId = encodeURIComponent(lastMessageId);
|
||||||
|
|
||||||
// wait until all messages have been received
|
// wait until all messages have been received
|
||||||
await expect(
|
await expect(page.getByLabel(`${otherRoomName} ${sendMessageResponses.length} unread messages.`)).toBeVisible();
|
||||||
page.getByLabel(`${otherRoomName} with ${sendMessageResponses.length} unread messages.`),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// switch to the room with the messages
|
// switch to the room with the messages
|
||||||
await page.goto(`/#/room/${otherRoomId}`);
|
await page.goto(`/#/room/${otherRoomId}`);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { test } from ".";
|
|||||||
|
|
||||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("Room list order", () => {
|
test.describe("Room list order", () => {
|
||||||
test("Rooms with unread messages appear at the top of room list with default 'activity' ordering", async ({
|
test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({
|
||||||
roomAlpha: room1,
|
roomAlpha: room1,
|
||||||
roomBeta: room2,
|
roomBeta: room2,
|
||||||
util,
|
util,
|
||||||
@@ -22,18 +22,15 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
await util.goTo(room2);
|
await util.goTo(room2);
|
||||||
|
|
||||||
// Display the unread first room
|
// Display the unread first room
|
||||||
|
await util.toggleRoomUnreadOrder();
|
||||||
await util.receiveMessages(room1, ["Msg1"]);
|
await util.receiveMessages(room1, ["Msg1"]);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
// switch rooms so they can re-order in the list
|
|
||||||
await util.goTo(room1);
|
|
||||||
|
|
||||||
// Room 1 has an unread message and should be displayed first
|
// Room 1 has an unread message and should be displayed first
|
||||||
// (as the default is to sort by activity)
|
|
||||||
await util.assertRoomListOrder([room1, room2]);
|
await util.assertRoomListOrder([room1, room2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Rooms with unread threads appear at the top of room list with default 'activity' order", async ({
|
test("Rooms with unread threads appear at the top of room list if 'unread first' is selected", async ({
|
||||||
roomAlpha: room1,
|
roomAlpha: room1,
|
||||||
roomBeta: room2,
|
roomBeta: room2,
|
||||||
util,
|
util,
|
||||||
@@ -45,6 +42,7 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||||||
await util.assertRead(room1);
|
await util.assertRead(room1);
|
||||||
|
|
||||||
// Display the unread first room
|
// Display the unread first room
|
||||||
|
await util.toggleRoomUnreadOrder();
|
||||||
await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]);
|
await util.receiveMessages(room1, [msg.threadedOff("Msg1", "Resp1")]);
|
||||||
await util.saveAndReload();
|
await util.saveAndReload();
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ test.describe("Pills", () => {
|
|||||||
|
|
||||||
// send a message using the built-in room mention functionality (autocomplete)
|
// send a message using the built-in room mention functionality (autocomplete)
|
||||||
await page
|
await page
|
||||||
.getByRole("textbox", { name: "Send an unencrypted message…" })
|
.getByRole("textbox", { name: "Send a message…" })
|
||||||
.pressSequentially(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
|
.pressSequentially(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
|
||||||
await page.locator(".mx_Autocomplete_Completion_title").click();
|
await page.locator(".mx_Autocomplete_Completion_title").click();
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ export class Helpers {
|
|||||||
/**
|
/**
|
||||||
* Get the release announcement with the given name.
|
* Get the release announcement with the given name.
|
||||||
* @param name
|
* @param name
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
public getReleaseAnnouncement(name: string) {
|
private getReleaseAnnouncement(name: string) {
|
||||||
return this.page.getByRole("dialog", { name });
|
return this.page.getByRole("dialog", { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,10 +42,7 @@ export class Helpers {
|
|||||||
*/
|
*/
|
||||||
async assertReleaseAnnouncementIsVisible(name: string) {
|
async assertReleaseAnnouncementIsVisible(name: string) {
|
||||||
await expect(this.getReleaseAnnouncement(name)).toBeVisible();
|
await expect(this.getReleaseAnnouncement(name)).toBeVisible();
|
||||||
await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, {
|
await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, { showTooltips: true });
|
||||||
showTooltips: true,
|
|
||||||
hideJumpToBottomButton: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +52,16 @@ export class Helpers {
|
|||||||
assertReleaseAnnouncementIsNotVisible(name: string) {
|
assertReleaseAnnouncementIsNotVisible(name: string) {
|
||||||
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
|
return expect(this.getReleaseAnnouncement(name)).not.toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the release announcement with the given name as read.
|
||||||
|
* If the release announcement is not visible, this will throw an error.
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
async markReleaseAnnouncementAsRead(name: string) {
|
||||||
|
const dialog = this.getReleaseAnnouncement(name);
|
||||||
|
await dialog.getByRole("button", { name: "Ok" }).click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { expect };
|
export { expect };
|
||||||
|
|||||||
@@ -22,36 +22,27 @@ test.describe("Release announcement", () => {
|
|||||||
await app.viewRoomById(roomId);
|
await app.viewRoomById(roomId);
|
||||||
await use({ roomId });
|
await use({ roomId });
|
||||||
},
|
},
|
||||||
labsFlags: ["feature_new_room_list"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"should display the new room list release announcement",
|
"should display the pinned messages release announcement",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, room, util }) => {
|
async ({ page, app, room, util }) => {
|
||||||
// dismiss the toast so the announcement appears
|
await app.toggleRoomInfoPanel();
|
||||||
await page.getByRole("button", { name: "Dismiss" }).click();
|
|
||||||
|
|
||||||
const newSoundsName = "We’ve refreshed your sounds";
|
const name = "All new pinned messages";
|
||||||
// The new sounds release announcement should be displayed
|
|
||||||
await util.assertReleaseAnnouncementIsVisible(newSoundsName);
|
|
||||||
// Hide the new sounds release announcement
|
|
||||||
const newSoundsDialog = util.getReleaseAnnouncement(newSoundsName);
|
|
||||||
await newSoundsDialog.getByRole("button", { name: "OK" }).click();
|
|
||||||
|
|
||||||
const newRoomListName = "Chats has a new look!";
|
// The release announcement should be displayed
|
||||||
// The new room list release announcement should be displayed
|
await util.assertReleaseAnnouncementIsVisible(name);
|
||||||
await util.assertReleaseAnnouncementIsVisible(newRoomListName);
|
// Hide the release announcement
|
||||||
// Hide the new room list release announcement
|
await util.markReleaseAnnouncementAsRead(name);
|
||||||
const dialog = util.getReleaseAnnouncement(newRoomListName);
|
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||||
await dialog.getByRole("button", { name: "Next" }).click();
|
|
||||||
|
|
||||||
await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
|
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
|
await app.toggleRoomInfoPanel();
|
||||||
// Check that once the release announcements has been marked as viewed, it does not appear again
|
await expect(page.getByRole("menuitem", { name: "Pinned messages" })).toBeVisible();
|
||||||
await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
|
// Check that once the release announcement has been marked as viewed, it does not appear again
|
||||||
|
await util.assertReleaseAnnouncementIsNotVisible(name);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type Page } from "@playwright/test";
|
import { type Download, type Page } from "@playwright/test";
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { viewRoomSummaryByName } from "./utils";
|
import { viewRoomSummaryByName } from "./utils";
|
||||||
@@ -63,7 +63,9 @@ test.describe("FilePanel", () => {
|
|||||||
await expect(roomViewBody.locator(".mx_EventTile[data-layout='group'] img[alt='riot.png']")).toBeVisible();
|
await expect(roomViewBody.locator(".mx_EventTile[data-layout='group'] img[alt='riot.png']")).toBeVisible();
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(roomViewBody.getByRole("region", { name: "Audio player" })).toBeVisible();
|
await expect(
|
||||||
|
roomViewBody.locator(".mx_EventTile[data-layout='group'] .mx_AudioPlayer_container"),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// Assert that the file button exists
|
// Assert that the file button exists
|
||||||
await expect(
|
await expect(
|
||||||
@@ -95,7 +97,9 @@ test.describe("FilePanel", () => {
|
|||||||
await expect(image.locator("img[alt='riot.png']")).toBeVisible();
|
await expect(image.locator("img[alt='riot.png']")).toBeVisible();
|
||||||
|
|
||||||
// Detect the audio file
|
// Detect the audio file
|
||||||
const audio = filePanelMessageList.getByRole("region", { name: "Audio player" });
|
const audio = filePanelMessageList.locator(
|
||||||
|
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
|
||||||
|
);
|
||||||
// Assert that the play button is rendered
|
// Assert that the play button is rendered
|
||||||
await expect(audio.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(audio.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
|
|
||||||
@@ -126,7 +130,7 @@ test.describe("FilePanel", () => {
|
|||||||
// Take a snapshot of file tiles list on FilePanel
|
// Take a snapshot of file tiles list on FilePanel
|
||||||
await expect(filePanelMessageList).toMatchScreenshot("file-tiles-list.png", {
|
await expect(filePanelMessageList).toMatchScreenshot("file-tiles-list.png", {
|
||||||
// Exclude timestamps & flaky seek bar from snapshot
|
// Exclude timestamps & flaky seek bar from snapshot
|
||||||
mask: [page.locator(".mx_MessageTimestamp"), page.getByTestId("audio-player-seek")],
|
mask: [page.locator(".mx_MessageTimestamp, .mx_AudioPlayer_seek")],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -134,19 +138,21 @@ test.describe("FilePanel", () => {
|
|||||||
// Upload an image file
|
// Upload an image file
|
||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
const audioBody = page.getByTestId("right-panel").getByRole("region", { name: "Audio player" });
|
const audioBody = page.locator(
|
||||||
|
".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
|
||||||
|
);
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
// Assert that the audio file information is rendered;
|
// Assert that the audio file information is rendered
|
||||||
await expect(audioBody.getByText("1sec.ogg")).toBeVisible(); // extension
|
const mediaInfo = audioBody.locator(".mx_AudioPlayer_mediaInfo");
|
||||||
await expect(audioBody.getByRole("time")).toHaveText("00:01"); // duration
|
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName").getByText("1sec.ogg")).toBeVisible();
|
||||||
await expect(audioBody.getByText("(3.56 KB)")).toBeVisible(); // actual size;
|
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
|
||||||
|
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
|
||||||
|
|
||||||
// Assert that the duration counter is 00:01 before clicking the play button
|
// Assert that the duration counter is 00:01 before clicking the play button
|
||||||
await expect(audioBody.getByRole("time")).toHaveText("00:01");
|
await expect(audioBody.locator(".mx_AudioPlayer_mediaInfo time", { hasText: "00:01" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the counter is zero before clicking the play button
|
// Assert that the counter is zero before clicking the play button
|
||||||
await expect(audioBody.getByRole("timer")).toHaveText("00:00");
|
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Click the play button
|
// Click the play button
|
||||||
await audioBody.getByRole("button", { name: "Play" }).click();
|
await audioBody.getByRole("button", { name: "Play" }).click();
|
||||||
@@ -155,7 +161,7 @@ test.describe("FilePanel", () => {
|
|||||||
await expect(audioBody.getByRole("button", { name: "Pause" })).toBeVisible();
|
await expect(audioBody.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the timer is reset when the audio file finished playing
|
// Assert that the timer is reset when the audio file finished playing
|
||||||
await expect(audioBody.getByRole("timer")).toHaveText("00:00");
|
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that the play button is rendered
|
// Assert that the play button is rendered
|
||||||
await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible();
|
await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible();
|
||||||
@@ -189,13 +195,23 @@ test.describe("FilePanel", () => {
|
|||||||
|
|
||||||
const link = imageBody.locator(".mx_MFileBody_download a");
|
const link = imageBody.locator(".mx_MFileBody_download a");
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent("download");
|
const newPagePromise = context.waitForEvent("page");
|
||||||
|
|
||||||
|
const downloadPromise = new Promise<Download>((resolve) => {
|
||||||
|
page.once("download", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
// Click the anchor link (not the image itself)
|
// Click the anchor link (not the image itself)
|
||||||
await link.click();
|
await link.click();
|
||||||
|
|
||||||
const download = await downloadPromise;
|
const newPage = await newPagePromise;
|
||||||
expect(download.suggestedFilename()).toBe("riot.png");
|
// XXX: Clicking the link opens the image in a new tab on some browsers rather than downloading
|
||||||
|
await expect(newPage)
|
||||||
|
.toHaveURL(/.+\/_matrix\/media\/\w+\/download\/localhost\/\w+/)
|
||||||
|
.catch(async () => {
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toBe("riot.png");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,32 +11,6 @@ import { Bot } from "../../pages/bot";
|
|||||||
const ROOM_NAME = "Test room";
|
const ROOM_NAME = "Test room";
|
||||||
const NAME = "Alice";
|
const NAME = "Alice";
|
||||||
|
|
||||||
async function setupRoomWithMembers(
|
|
||||||
app: any,
|
|
||||||
page: any,
|
|
||||||
homeserver: any,
|
|
||||||
roomName: string,
|
|
||||||
memberNames: string[],
|
|
||||||
): Promise<string> {
|
|
||||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
|
||||||
const id = await app.client.createRoom({ name: roomName, visibility });
|
|
||||||
const bots: Bot[] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < memberNames.length; i++) {
|
|
||||||
const displayName = memberNames[i];
|
|
||||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
|
||||||
if (displayName === "Susan") {
|
|
||||||
await bot.prepareClient();
|
|
||||||
await app.client.inviteUser(id, bot.credentials?.userId);
|
|
||||||
} else {
|
|
||||||
await bot.joinRoom(id);
|
|
||||||
}
|
|
||||||
bots.push(bot);
|
|
||||||
}
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
synapseConfig: {
|
synapseConfig: {
|
||||||
presence: {
|
presence: {
|
||||||
@@ -51,8 +25,17 @@ test.use({
|
|||||||
test.describe("Memberlist", () => {
|
test.describe("Memberlist", () => {
|
||||||
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
||||||
testInfo.setTimeout(testInfo.timeout + 30_000);
|
testInfo.setTimeout(testInfo.timeout + 30_000);
|
||||||
|
const id = await app.client.createRoom({ name: ROOM_NAME });
|
||||||
|
const newBots: Bot[] = [];
|
||||||
const names = ["Bob", "Bob", "Susan"];
|
const names = ["Bob", "Bob", "Susan"];
|
||||||
await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names);
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const displayName = names[i];
|
||||||
|
const autoAcceptInvites = displayName !== "Susan";
|
||||||
|
const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites });
|
||||||
|
await bot.prepareClient();
|
||||||
|
await app.client.inviteUser(id, bot.credentials?.userId);
|
||||||
|
newBots.push(bot);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
@@ -62,37 +45,4 @@ test.describe("Memberlist", () => {
|
|||||||
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
||||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should handle scroll and click to view member profile", async ({ page, app, homeserver }) => {
|
|
||||||
// Create a room with many members to enable scrolling
|
|
||||||
const memberNames = Array.from({ length: 15 }, (_, i) => `Member${i.toString()}`);
|
|
||||||
await setupRoomWithMembers(app, page, homeserver, "Large Room", memberNames);
|
|
||||||
|
|
||||||
// Navigate to the room and open member list
|
|
||||||
await app.viewRoomByName("Large Room");
|
|
||||||
|
|
||||||
const memberlist = await app.toggleMemberlistPanel();
|
|
||||||
|
|
||||||
// Get the scrollable container
|
|
||||||
const memberListContainer = memberlist.locator(".mx_AutoHideScrollbar");
|
|
||||||
|
|
||||||
// Scroll down to the bottom of the member list
|
|
||||||
await app.scrollListToBottom(memberListContainer);
|
|
||||||
|
|
||||||
// Wait for the target member to be visible after scrolling
|
|
||||||
const targetName = "Member14";
|
|
||||||
const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName });
|
|
||||||
await targetMember.waitFor({ state: "visible" });
|
|
||||||
|
|
||||||
// Verify Alice is not visible at this point
|
|
||||||
await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0);
|
|
||||||
|
|
||||||
// Click on a member near the bottom of the list
|
|
||||||
await expect(targetMember).toBeVisible();
|
|
||||||
await targetMember.click();
|
|
||||||
|
|
||||||
// Verify that the user info screen is shown and hasn't scrolled back to top
|
|
||||||
await expect(page.locator(".mx_UserInfo")).toBeVisible();
|
|
||||||
await expect(page.locator(".mx_UserInfo_profile").getByText(targetName)).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|
||||||
import { UIFeature } from "../../../src/settings/UIFeature";
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
const name = "Test room";
|
|
||||||
const topic = "A decently explanatory topic for a test room.";
|
|
||||||
|
|
||||||
test.describe("Create Room", () => {
|
|
||||||
test.use({ displayName: "Jim" });
|
|
||||||
|
|
||||||
test(
|
|
||||||
"should create a public room with name, topic & address set",
|
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, user, app, axe }) => {
|
|
||||||
const dialog = await app.openCreateRoomDialog();
|
|
||||||
// Fill name & topic
|
|
||||||
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
|
||||||
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
|
||||||
// Change room to public
|
|
||||||
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
|
||||||
await dialog.getByRole("option", { name: "Public room" }).click();
|
|
||||||
// Fill room address
|
|
||||||
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-standard");
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
// Snapshot it
|
|
||||||
await expect(dialog).toMatchScreenshot("create-room.png");
|
|
||||||
|
|
||||||
// Submit
|
|
||||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/#/room/#test-create-room-standard:${user.homeServer}`));
|
|
||||||
const header = page.locator(".mx_RoomHeader");
|
|
||||||
await expect(header).toContainText(name);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test("should allow us to start a chat and show encryption state", async ({ page, user, app }) => {
|
|
||||||
await page.getByRole("button", { name: "Add", exact: true }).click();
|
|
||||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
|
||||||
|
|
||||||
await page.getByTestId("invite-dialog-input").fill(user.userId);
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Go" }).click();
|
|
||||||
|
|
||||||
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
|
||||||
await expect(page.getByText("Send your first message to")).toBeVisible();
|
|
||||||
|
|
||||||
const composer = page.getByRole("region", { name: "Message composer" });
|
|
||||||
await expect(composer.getByRole("textbox", { name: "Send a message…" })).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
|
||||||
await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
|
||||||
|
|
||||||
const dialog = await app.openCreateRoomDialog("New video room");
|
|
||||||
// Fill name & topic
|
|
||||||
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
|
||||||
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
|
||||||
// Change room to public
|
|
||||||
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
|
||||||
await dialog.getByRole("option", { name: "Public room" }).click();
|
|
||||||
// Fill room address
|
|
||||||
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-video");
|
|
||||||
// Snapshot it
|
|
||||||
await expect(dialog).toMatchScreenshot("create-video-room.png");
|
|
||||||
|
|
||||||
// Submit
|
|
||||||
await dialog.getByRole("button", { name: "Create video room" }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/#/room/#test-create-room-video:${user.homeServer}`));
|
|
||||||
const header = page.locator(".mx_RoomHeader");
|
|
||||||
await expect(header).toContainText(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Should hide public room option if not allowed", () => {
|
|
||||||
test.use({
|
|
||||||
config: {
|
|
||||||
setting_defaults: {
|
|
||||||
[UIFeature.AllowCreatingPublicRooms]: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should disallow creating public rooms", { tag: "@screenshot" }, async ({ page, user, app, axe }) => {
|
|
||||||
const dialog = await app.openCreateRoomDialog();
|
|
||||||
// Fill name & topic
|
|
||||||
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
|
||||||
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
|
||||||
|
|
||||||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
|
||||||
await expect(axe).toHaveNoViolations();
|
|
||||||
// Snapshot it
|
|
||||||
await expect(dialog).toMatchScreenshot("create-room-no-public.png");
|
|
||||||
|
|
||||||
// Submit
|
|
||||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/#/room/!.+`));
|
|
||||||
const header = page.locator(".mx_RoomHeader");
|
|
||||||
await expect(header).toContainText(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user