Compare commits
130 commits
rav/wasm_i
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
943b817194 | ||
|
2aa72bb40b | ||
|
a755e399cf | ||
|
8dff758153 | ||
|
cf3bdbdc7a | ||
|
ba98c2085d | ||
|
b330de5d6e | ||
|
b86bb5cc2f | ||
|
e835cab139 | ||
|
6b7c94905f | ||
|
a4e8bb3f9a | ||
|
2b4000d47f | ||
|
01304439ee | ||
|
c659afa8db | ||
|
9cc5564d50 | ||
|
549300726f | ||
|
319dab5920 | ||
|
5c51d179b9 | ||
|
dbdb23f6bc | ||
|
5686666ad2 | ||
|
0c4189f2ed | ||
|
450cb608ec | ||
|
7e03f38a3b | ||
|
9bf3d22439 | ||
|
5547101bcc | ||
|
085854b125 | ||
|
ee24989f49 | ||
|
5a418f3f19 | ||
|
db5b3359c6 | ||
|
188f910dc7 | ||
|
619e41e3a2 | ||
|
c1838b34b6 | ||
|
1d51323451 | ||
|
d0d0b8212d | ||
|
974d3c175a | ||
|
d0e19d3e03 | ||
|
b016cf59e9 | ||
|
cfdfc4e640 | ||
|
d0fea745bb | ||
|
f3ef9e6602 | ||
|
af0391b86a | ||
|
36108c0c22 | ||
|
d2acce1221 | ||
|
b72c053d1a | ||
|
865c5b0e9c | ||
|
ce3fa2164f | ||
|
60b0e8b237 | ||
|
823828fc17 | ||
|
8774a68a13 | ||
|
4730352092 | ||
|
0429809c00 | ||
|
06fa3481df | ||
|
e75ff818d3 | ||
|
2c3e01a31c | ||
|
84709df3c9 | ||
|
00aadf1580 | ||
|
d8ebc68aa8 | ||
|
5d72735b1f | ||
|
2099aaa663 | ||
|
6d8cbf39f5 | ||
|
b87437d439 | ||
|
8619a22f57 | ||
|
418f121f96 | ||
|
351774d3e3 | ||
|
70f898c71d | ||
|
4f276c1690 | ||
|
2b4ce627b8 | ||
|
d68c5a26af | ||
|
95175caf0c | ||
|
08418c16c9 | ||
|
6798239aa8 | ||
|
9c74110969 | ||
|
1bee3becfb | ||
|
bfac727307 | ||
|
8e213c5d34 | ||
|
c34cbd011d | ||
|
e10ecc9a4d | ||
|
3dbcb5efa3 | ||
|
2b883a8aa0 | ||
|
bb54a0e063 | ||
|
af3a9777e8 | ||
|
61542ff3a8 | ||
|
ec460326f1 | ||
|
af846f8be9 | ||
|
de5ddcf6f7 | ||
|
8d28dd3784 | ||
|
df7703a4a5 | ||
|
fe7ac68478 | ||
|
bda045fad0 | ||
|
8a7cdaa3ef | ||
|
7796200562 | ||
|
80cd93678a | ||
|
b8c178d133 | ||
|
5f4d789259 | ||
|
85711be352 | ||
|
a891dcddc2 | ||
|
61947deef5 | ||
|
c1549e6aaa | ||
|
adfc66bd1b | ||
|
dbfb84eb77 | ||
|
3d48168394 | ||
|
9ddd5d96eb | ||
|
e6dc6b93a7 | ||
|
bd0bb879ec | ||
|
903928c33c | ||
|
0525887be4 | ||
|
4259e96c90 | ||
|
20532144d2 | ||
|
d2eeb3d8af | ||
|
b4445fed53 | ||
|
9860f9320a | ||
|
c8f46a0191 | ||
|
4285b4b140 | ||
|
de820e11fc | ||
|
94130be5a7 | ||
|
0333cba258 | ||
|
2ebc1b4a89 | ||
|
756ce2c639 | ||
|
c519361b4e | ||
|
15bd59b81a | ||
|
c74f9159ad | ||
|
2ac2bae4fa | ||
|
a478463b75 | ||
|
3f221891f7 | ||
|
2dffadc92e | ||
|
f3d0fc4d68 | ||
|
0b636aec4b | ||
|
cb0d72fc2f | ||
|
931edd7419 | ||
|
044eaf7eb5 |
722 changed files with 7160 additions and 20435 deletions
|
@ -42,6 +42,10 @@ module.exports = {
|
||||||
name: "setImmediate",
|
name: "setImmediate",
|
||||||
message: "Use setTimeout instead.",
|
message: "Use setTimeout instead.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Buffer",
|
||||||
|
message: "Buffer is not available in the web.",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
"import/no-duplicates": ["error"],
|
"import/no-duplicates": ["error"],
|
||||||
|
@ -255,6 +259,9 @@ module.exports = {
|
||||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
|
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// These are fine in tests
|
||||||
|
"no-restricted-globals": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -13,6 +13,7 @@
|
||||||
|
|
||||||
# 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
|
||||||
# Ignore the synapse plugin 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/plugins/homeserver/synapse/index.ts
|
/playwright/plugins/homeserver/synapse/index.ts
|
||||||
|
|
||||||
|
|
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
8
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
|
|
||||||
- [ ] 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.
|
||||||
- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web)
|
- [ ] I have licensed the changes to Element by completing the [Contributor License Agreement (CLA)](https://cla-assistant.io/element-hq/element-web)
|
||||||
|
|
|
@ -10,24 +10,29 @@ inputs:
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Download current version for its old bundles
|
- name: Download release tarball
|
||||||
id: current_download
|
|
||||||
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
||||||
with:
|
with:
|
||||||
tag: steps.current_version.outputs.version
|
tag: ${{ inputs.tag }}
|
||||||
fileName: element-*.tar.gz*
|
fileName: element-*.tar.gz*
|
||||||
out-file-path: ${{ runner.temp }}/download-verify-element-tarball
|
out-file-path: ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
||||||
- name: Verify tarball
|
- name: Verify tarball
|
||||||
|
shell: bash
|
||||||
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
|
run: gpg --verify element-*.tar.gz.asc element-*.tar.gz
|
||||||
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
||||||
- name: Extract tarball
|
- name: Extract tarball
|
||||||
run: tar xvzf element-*.tar.gz -C webapp --strip-components=1
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir webapp
|
||||||
|
tar xvzf element-*.tar.gz -C webapp --strip-components=1
|
||||||
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
working-directory: ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
||||||
- name: Move webapp to out-file-path
|
- name: Move webapp to out-file-path
|
||||||
|
shell: bash
|
||||||
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
|
run: mv ${{ runner.temp }}/download-verify-element-tarball/webapp ${{ inputs.out-file-path }}
|
||||||
|
|
||||||
- name: Clean up temp directory
|
- name: Clean up temp directory
|
||||||
|
shell: bash
|
||||||
run: rm -R ${{ runner.temp }}/download-verify-element-tarball
|
run: rm -R ${{ runner.temp }}/download-verify-element-tarball
|
||||||
|
|
22
.github/workflows/deploy.yml
vendored
22
.github/workflows/deploy.yml
vendored
|
@ -1,7 +1,8 @@
|
||||||
# Manual deploy workflow for deploying to app.element.io & staging.element.io
|
# Manual deploy workflow for deploying to app.element.io & staging.element.io
|
||||||
# Runs automatically for staging.element.io when an RC or Release is published
|
# Runs automatically for staging.element.io when an RC or Release is published
|
||||||
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
|
# Note: Does *NOT* run automatically for app.element.io so that it gets tested on staging.element.io beforehand
|
||||||
name: Build and Deploy ${{ inputs.site || 'staging.element.io' }}
|
name: Deploy release
|
||||||
|
run-name: Deploy ${{ github.ref_name }} to ${{ inputs.site || 'staging.element.io' }}
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
@ -28,37 +29,40 @@ jobs:
|
||||||
env:
|
env:
|
||||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Load GPG key
|
- name: Load GPG key
|
||||||
run: |
|
run: |
|
||||||
curl https://packages.element.io/element-release-key.gpg | gpg --import
|
curl https://packages.element.io/element-release-key.gpg | gpg --import
|
||||||
gpg -k "$GPG_FINGERPRINT"
|
gpg -k "$GPG_FINGERPRINT"
|
||||||
env:
|
env:
|
||||||
GPG_FINGERPRINT: ${{ secrets.GPG_FINGERPRINT }}
|
GPG_FINGERPRINT: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
- name: Check current version on deployment
|
- name: Check current version on deployment
|
||||||
id: current_version
|
id: current_version
|
||||||
run: |
|
run: |
|
||||||
echo "version=$(curl -s https://$SITE/version)" >> $GITHUB_OUTPUT
|
version=$(curl -s https://$SITE/version)
|
||||||
|
echo "version=${version#v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# The current version bundle melding dance is skipped if the version we're deploying is the same
|
# The current version bundle melding dance is skipped if the version we're deploying is the same
|
||||||
# as then we're just doing a re-deploy of the same version with potentially different configs.
|
# as then we're just doing a re-deploy of the same version with potentially different configs.
|
||||||
- name: Download current version for its old bundles
|
- name: Download current version for its old bundles
|
||||||
id: current_download
|
id: current_download
|
||||||
if: steps.current_version.outputs.version != github.ref_name
|
if: steps.current_version.outputs.version != github.ref_name
|
||||||
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
|
uses: ./.github/actions/download-verify-element-tarball
|
||||||
with:
|
with:
|
||||||
tag: steps.current_version.outputs.version
|
tag: v${{ steps.current_version.outputs.version }}
|
||||||
out-file-path: current_version
|
out-file-path: _current_version
|
||||||
|
|
||||||
- name: Download target version
|
- name: Download target version
|
||||||
uses: element-hq/element-web/.github/actions/download-verify-element-tarball@${{ github.ref_name }}
|
uses: ./.github/actions/download-verify-element-tarball
|
||||||
with:
|
with:
|
||||||
tag: ${{ github.ref_name }}
|
tag: ${{ github.ref_name }}
|
||||||
out-file-path: _deploy
|
out-file-path: _deploy
|
||||||
|
|
||||||
- name: Merge current bundles into target
|
- name: Merge current bundles into target
|
||||||
if: steps.current_download.outcome == 'success'
|
if: steps.current_download.outcome == 'success'
|
||||||
run: cp -vnpr current_version/bundles/* _deploy/bundles/
|
run: cp -vnpr _current_version/bundles/* _deploy/bundles/
|
||||||
|
|
||||||
- name: Copy config
|
- name: Copy config
|
||||||
run: cp element.io/app/config.json _deploy/config.json
|
run: cp element.io/app/config.json _deploy/config.json
|
||||||
|
@ -73,7 +77,7 @@ jobs:
|
||||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
running-workflow-name: "Build and Deploy ${{ env.SITE }}"
|
running-workflow-name: "Deploy to Cloudflare Pages"
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
|
check-regexp: ^((?!SonarCloud|SonarQube|issue|board|label|Release|prepare|GitHub Pages).)*$
|
||||||
|
|
4
.github/workflows/dockerhub.yaml
vendored
4
.github/workflows/dockerhub.yaml
vendored
|
@ -39,7 +39,7 @@ jobs:
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5
|
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
vectorim/element-web
|
vectorim/element-web
|
||||||
|
@ -51,7 +51,7 @@ jobs:
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6
|
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|
14
.github/workflows/end-to-end-tests.yaml
vendored
14
.github/workflows/end-to-end-tests.yaml
vendored
|
@ -83,7 +83,7 @@ jobs:
|
||||||
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
|
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
|
||||||
needs: build
|
needs: build
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
actions: read
|
||||||
issues: read
|
issues: read
|
||||||
|
@ -124,14 +124,18 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/ms-playwright
|
~/.cache/ms-playwright
|
||||||
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
|
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}-chromium
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browser
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps --no-shell chromium
|
||||||
|
|
||||||
|
# We skip tests tagged with @mergequeue when running on PRs, but run them in MQ and everywhere else
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }}
|
run: |
|
||||||
|
yarn playwright test \
|
||||||
|
--shard "${{ matrix.runner }}/${{ strategy.job-total }}" \
|
||||||
|
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
|
||||||
|
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
if: always()
|
if: always()
|
||||||
|
|
2
.github/workflows/pull_request.yaml
vendored
2
.github/workflows/pull_request.yaml
vendored
|
@ -9,6 +9,6 @@ jobs:
|
||||||
action:
|
action:
|
||||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
secrets:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
|
@ -18,6 +18,7 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
|
pull-requests: read
|
||||||
secrets:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
|
15
.github/workflows/release_prepare.yml
vendored
15
.github/workflows/release_prepare.yml
vendored
|
@ -19,8 +19,23 @@ on:
|
||||||
default: true
|
default: true
|
||||||
permissions: {} # Uses ELEMENT_BOT_TOKEN instead
|
permissions: {} # Uses ELEMENT_BOT_TOKEN instead
|
||||||
jobs:
|
jobs:
|
||||||
|
checks:
|
||||||
|
name: Sanity checks
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
repo:
|
||||||
|
- matrix-org/matrix-js-sdk
|
||||||
|
- element-hq/element-web
|
||||||
|
- element-hq/element-desktop
|
||||||
|
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
|
||||||
|
secrets:
|
||||||
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
with:
|
||||||
|
repository: ${{ matrix.repo }}
|
||||||
|
|
||||||
prepare:
|
prepare:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
needs: checks
|
||||||
env:
|
env:
|
||||||
# The order is specified bottom-up to avoid any races for allchange
|
# The order is specified bottom-up to avoid any races for allchange
|
||||||
REPOS: matrix-js-sdk element-web element-desktop
|
REPOS: matrix-js-sdk element-web element-desktop
|
||||||
|
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
|
@ -104,7 +104,7 @@ jobs:
|
||||||
|
|
||||||
- name: Skip SonarCloud in merge queue
|
- name: Skip SonarCloud in merge queue
|
||||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||||
uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07
|
uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -1,3 +1,33 @@
|
||||||
|
Changes in [1.11.87](https://github.com/element-hq/element-web/releases/tag/v1.11.87) (2024-12-03)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Send and respect MSC4230 is\_animated flag ([#28513](https://github.com/element-hq/element-web/pull/28513)). Contributed by @t3chguy.
|
||||||
|
* Display a warning when an unverified user's identity changes ([#28211](https://github.com/element-hq/element-web/pull/28211)). Contributed by @uhoreg.
|
||||||
|
* Swap out Twitter link for Mastodon on auth footer ([#28508](https://github.com/element-hq/element-web/pull/28508)). Contributed by @t3chguy.
|
||||||
|
* Consider `org.matrix.msc3417.call` as video room in create room dialog ([#28497](https://github.com/element-hq/element-web/pull/28497)). Contributed by @t3chguy.
|
||||||
|
* Standardise icons using Compound Design Tokens ([#28217](https://github.com/element-hq/element-web/pull/28217)). Contributed by @t3chguy.
|
||||||
|
* Start sending stable `m.marked_unread` events ([#28478](https://github.com/element-hq/element-web/pull/28478)). Contributed by @tulir.
|
||||||
|
* Upgrade to compound-design-tokens v2 ([#28471](https://github.com/element-hq/element-web/pull/28471)). Contributed by @t3chguy.
|
||||||
|
* Standardise icons using Compound Design Tokens ([#28286](https://github.com/element-hq/element-web/pull/28286)). Contributed by @t3chguy.
|
||||||
|
* Remove reply fallbacks as per merged MSC2781 ([#28406](https://github.com/element-hq/element-web/pull/28406)). Contributed by @t3chguy.
|
||||||
|
* Use React Suspense when rendering async modals ([#28386](https://github.com/element-hq/element-web/pull/28386)). Contributed by @t3chguy.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Add spinner when room encryption is loading in room settings ([#28535](https://github.com/element-hq/element-web/pull/28535)). Contributed by @florianduros.
|
||||||
|
* Fix getOidcCallbackUrl for Element Desktop ([#28521](https://github.com/element-hq/element-web/pull/28521)). Contributed by @t3chguy.
|
||||||
|
* Filter out redacted poll votes to avoid crashing the Poll widget ([#28498](https://github.com/element-hq/element-web/pull/28498)). Contributed by @t3chguy.
|
||||||
|
* Fix force tab complete not working since switching to React 18 createRoot API ([#28505](https://github.com/element-hq/element-web/pull/28505)). Contributed by @t3chguy.
|
||||||
|
* Fix media captions in bubble layout ([#28480](https://github.com/element-hq/element-web/pull/28480)). Contributed by @tulir.
|
||||||
|
* Reset cross-signing before backup when resetting both ([#28402](https://github.com/element-hq/element-web/pull/28402)). Contributed by @uhoreg.
|
||||||
|
* Listen to events so that encryption icon updates when status changes ([#28407](https://github.com/element-hq/element-web/pull/28407)). Contributed by @uhoreg.
|
||||||
|
* Check that the file the user chose has a MIME type of `image/*` ([#28467](https://github.com/element-hq/element-web/pull/28467)). Contributed by @t3chguy.
|
||||||
|
* Fix download button size in message action bar ([#28472](https://github.com/element-hq/element-web/pull/28472)). Contributed by @t3chguy.
|
||||||
|
* Allow tab completing users in brackets ([#28460](https://github.com/element-hq/element-web/pull/28460)). Contributed by @t3chguy.
|
||||||
|
* Fix React 18 strict mode breaking spotlight dialog ([#28452](https://github.com/element-hq/element-web/pull/28452)). Contributed by @MidhunSureshR.
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
|
Changes in [1.11.86](https://github.com/element-hq/element-web/releases/tag/v1.11.86) (2024-11-19)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
|
@ -20,26 +20,26 @@ Definitely don't use the GitHub default of "Update file.ts".
|
||||||
|
|
||||||
As for your PR description, it should include these things:
|
As for your PR description, it should include these things:
|
||||||
|
|
||||||
- References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
- References to any bugs fixed by the change (in GitHub's `Fixes` notation)
|
||||||
- Describe the why and what is changing in the PR description so it's easy for
|
- Describe the why and what is changing in the PR description so it's easy for
|
||||||
onlookers and reviewers to onboard and context switch. This information is
|
onlookers and reviewers to onboard and context switch. This information is
|
||||||
also helpful when we come back to look at this in 6 months and ask "why did
|
also helpful when we come back to look at this in 6 months and ask "why did
|
||||||
we do it like that?" we have a chance of finding out.
|
we do it like that?" we have a chance of finding out.
|
||||||
- Why didn't it work before? Why does it work now? What use cases does it
|
- Why didn't it work before? Why does it work now? What use cases does it
|
||||||
unlock?
|
unlock?
|
||||||
- If you find yourself adding information on how the code works or why you
|
- If you find yourself adding information on how the code works or why you
|
||||||
chose to do it the way you did, make sure this information is instead
|
chose to do it the way you did, make sure this information is instead
|
||||||
written as comments in the code itself.
|
written as comments in the code itself.
|
||||||
- Sometimes a PR can change considerably as it is developed. In this case,
|
- Sometimes a PR can change considerably as it is developed. In this case,
|
||||||
the description should be updated to reflect the most recent state of
|
the description should be updated to reflect the most recent state of
|
||||||
the PR. (It can be helpful to retain the old content under a suitable
|
the PR. (It can be helpful to retain the old content under a suitable
|
||||||
heading, for additional context.)
|
heading, for additional context.)
|
||||||
- Include both **before** and **after** screenshots to easily compare and discuss
|
- Include both **before** and **after** screenshots to easily compare and discuss
|
||||||
what's changing.
|
what's changing.
|
||||||
- Include a step-by-step testing strategy so that a reviewer can check out the
|
- Include a step-by-step testing strategy so that a reviewer can check out the
|
||||||
code locally and easily get to the point of testing your change.
|
code locally and easily get to the point of testing your change.
|
||||||
- Add comments to the diff for the reviewer that might help them to understand
|
- Add comments to the diff for the reviewer that might help them to understand
|
||||||
why the change is necessary or how they might better understand and review it.
|
why the change is necessary or how they might better understand and review it.
|
||||||
|
|
||||||
### Changelogs
|
### Changelogs
|
||||||
|
|
||||||
|
@ -79,8 +79,8 @@ element-web notes: Fix a bug where the 'Herd' button only worked on Tuesdays
|
||||||
|
|
||||||
This example is for Element Web. You can specify:
|
This example is for Element Web. You can specify:
|
||||||
|
|
||||||
- element-web
|
- element-web
|
||||||
- element-desktop
|
- element-desktop
|
||||||
|
|
||||||
If your PR introduces a breaking change, use the `Notes` section in the same
|
If your PR introduces a breaking change, use the `Notes` section in the same
|
||||||
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
way, additionally adding the `X-Breaking-Change` label (see below). There's no need
|
||||||
|
@ -96,10 +96,10 @@ Notes: Remove legacy `Camelopard` class. `Giraffe` should be used instead.
|
||||||
|
|
||||||
Other metadata can be added using labels.
|
Other metadata can be added using labels.
|
||||||
|
|
||||||
- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump.
|
- `X-Breaking-Change`: A breaking change - adding this label will mean the change causes a _major_ version bump.
|
||||||
- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump.
|
- `T-Enhancement`: A new feature - adding this label will mean the change causes a _minor_ version bump.
|
||||||
- `T-Defect`: A bug fix (in either code or docs).
|
- `T-Defect`: A bug fix (in either code or docs).
|
||||||
- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
- `T-Task`: No user-facing changes, eg. code comments, CI fixes, refactors or tests. Won't have a changelog entry unless you specify one.
|
||||||
|
|
||||||
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
If you don't have permission to add labels, your PR reviewer(s) can work with you
|
||||||
to add them: ask in the PR description or comments.
|
to add them: ask in the PR description or comments.
|
||||||
|
|
64
README.md
64
README.md
|
@ -16,28 +16,28 @@ JS SDK](https://github.com/matrix-org/matrix-js-sdk).
|
||||||
|
|
||||||
Element has several tiers of support for different environments:
|
Element has several tiers of support for different environments:
|
||||||
|
|
||||||
- Supported
|
- Supported
|
||||||
- Definition:
|
- Definition:
|
||||||
- Issues **actively triaged**, regressions **block** the release
|
- Issues **actively triaged**, regressions **block** the release
|
||||||
- Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes
|
- Last 2 major versions of Chrome, Firefox, and Edge on desktop OSes
|
||||||
- Last 2 versions of Safari
|
- Last 2 versions of Safari
|
||||||
- Latest release of official Element Desktop app on desktop OSes
|
- Latest release of official Element Desktop app on desktop OSes
|
||||||
- Desktop OSes means macOS, Windows, and Linux versions for desktop devices
|
- Desktop OSes means macOS, Windows, and Linux versions for desktop devices
|
||||||
that are actively supported by the OS vendor and receive security updates
|
that are actively supported by the OS vendor and receive security updates
|
||||||
- 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
|
||||||
- Definition:
|
- Definition:
|
||||||
- Issues **accepted**, regressions **do not block** the release
|
- Issues **accepted**, regressions **do not block** the release
|
||||||
- Community contributions are welcome to support these issues
|
- Community contributions are welcome to support these issues
|
||||||
- Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS
|
- Mobile web for current stable version of Chrome, Firefox, and Safari on Android, iOS, and iPadOS
|
||||||
- Not supported
|
- Not supported
|
||||||
- Definition: Issues only affecting unsupported environments are **closed**
|
- Definition: Issues only affecting unsupported environments are **closed**
|
||||||
- Everything else
|
- Everything else
|
||||||
|
|
||||||
The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable.
|
The period of support for these tiers should last until the releases specified above, plus 1 app release cycle(2 weeks). In the case of Firefox ESR this is extended further to allow it land in Debian Stable.
|
||||||
|
|
||||||
|
@ -74,16 +74,16 @@ situation, but it's still not good practice to do it in the first place. See
|
||||||
Unless you have special requirements, you will want to add the following to
|
Unless you have special requirements, you will want to add the following to
|
||||||
your web server configuration when hosting Element Web:
|
your web server configuration when hosting Element Web:
|
||||||
|
|
||||||
- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being
|
- The `X-Frame-Options: SAMEORIGIN` header, to prevent Element Web from being
|
||||||
framed and protect from [clickjacking][owasp-clickjacking].
|
framed and protect from [clickjacking][owasp-clickjacking].
|
||||||
- The `frame-ancestors 'self'` directive to your `Content-Security-Policy`
|
- The `frame-ancestors 'self'` directive to your `Content-Security-Policy`
|
||||||
header, as the modern replacement for `X-Frame-Options` (though both should be
|
header, as the modern replacement for `X-Frame-Options` (though both should be
|
||||||
included since not all browsers support it yet, see
|
included since not all browsers support it yet, see
|
||||||
[this][owasp-clickjacking-csp]).
|
[this][owasp-clickjacking-csp]).
|
||||||
- The `X-Content-Type-Options: nosniff` header, to [disable MIME
|
- The `X-Content-Type-Options: nosniff` header, to [disable MIME
|
||||||
sniffing][mime-sniffing].
|
sniffing][mime-sniffing].
|
||||||
- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in
|
- The `X-XSS-Protection: 1; mode=block;` header, for basic XSS protection in
|
||||||
legacy browsers.
|
legacy browsers.
|
||||||
|
|
||||||
[mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing
|
[mime-sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing
|
||||||
[owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples
|
[owasp-clickjacking-csp]: https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html#content-security-policy-frame-ancestors-examples
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
// Stub out FontManager for tests as it doesn't validate anything we don't already know given
|
|
||||||
// our fixed test environment and it requires the installation of node-canvas.
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fixupColorFonts: () => Promise.resolve(),
|
|
||||||
};
|
|
|
@ -3,9 +3,9 @@
|
||||||
This code style applies to projects which the element-web team directly maintains or is reasonably
|
This code style applies to projects which the element-web team directly maintains or is reasonably
|
||||||
adjacent to. As of writing, these are:
|
adjacent to. As of writing, these are:
|
||||||
|
|
||||||
- element-desktop
|
- element-desktop
|
||||||
- element-web
|
- element-web
|
||||||
- matrix-js-sdk
|
- matrix-js-sdk
|
||||||
|
|
||||||
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
|
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
|
||||||
has stricter code organization to reduce the maintenance burden. These projects will declare their code
|
has stricter code organization to reduce the maintenance burden. These projects will declare their code
|
||||||
|
|
|
@ -1,55 +1,55 @@
|
||||||
# Summary
|
# Summary
|
||||||
|
|
||||||
- [Introduction](../README.md)
|
- [Introduction](../README.md)
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
- [Betas](betas.md)
|
- [Betas](betas.md)
|
||||||
- [Labs](labs.md)
|
- [Labs](labs.md)
|
||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
|
|
||||||
- [Install](install.md)
|
- [Install](install.md)
|
||||||
- [Config](config.md)
|
- [Config](config.md)
|
||||||
- [Custom home page](custom-home.md)
|
- [Custom home page](custom-home.md)
|
||||||
- [Kubernetes](kubernetes.md)
|
- [Kubernetes](kubernetes.md)
|
||||||
- [Jitsi](jitsi.md)
|
- [Jitsi](jitsi.md)
|
||||||
- [Encryption](e2ee.md)
|
- [Encryption](e2ee.md)
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
|
|
||||||
- [Customisations](customisations.md)
|
- [Customisations](customisations.md)
|
||||||
- [Modules](modules.md)
|
- [Modules](modules.md)
|
||||||
- [Native Node modules](native-node-modules.md)
|
- [Native Node modules](native-node-modules.md)
|
||||||
|
|
||||||
# Contribution
|
# Contribution
|
||||||
|
|
||||||
- [Choosing an issue](choosing-an-issue.md)
|
- [Choosing an issue](choosing-an-issue.md)
|
||||||
- [Translation](translating.md)
|
- [Translation](translating.md)
|
||||||
- [Netlify builds](pr-previews.md)
|
- [Netlify builds](pr-previews.md)
|
||||||
- [Code review](review.md)
|
- [Code review](review.md)
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
- [App load order](app-load.md)
|
- [App load order](app-load.md)
|
||||||
- [Translation](translating-dev.md)
|
- [Translation](translating-dev.md)
|
||||||
- [Theming](theming.md)
|
- [Theming](theming.md)
|
||||||
- [Playwright end to end tests](playwright.md)
|
- [Playwright end to end tests](playwright.md)
|
||||||
- [Memory profiling](memory-profiles-and-leaks.md)
|
- [Memory profiling](memory-profiles-and-leaks.md)
|
||||||
- [Jitsi](jitsi-dev.md)
|
- [Jitsi](jitsi-dev.md)
|
||||||
- [Feature flags](feature-flags.md)
|
- [Feature flags](feature-flags.md)
|
||||||
- [OIDC and delegated authentication](oidc.md)
|
- [OIDC and delegated authentication](oidc.md)
|
||||||
- [Release Process](release.md)
|
- [Release Process](release.md)
|
||||||
|
|
||||||
# Deep dive
|
# Deep dive
|
||||||
|
|
||||||
- [Skinning](skinning.md)
|
- [Skinning](skinning.md)
|
||||||
- [Cider editor](ciderEditor.md)
|
- [Cider editor](ciderEditor.md)
|
||||||
- [Iconography](icons.md)
|
- [Iconography](icons.md)
|
||||||
- [Jitsi](jitsi.md)
|
- [Jitsi](jitsi.md)
|
||||||
- [Local echo](local-echo-dev.md)
|
- [Local echo](local-echo-dev.md)
|
||||||
- [Media](media-handling.md)
|
- [Media](media-handling.md)
|
||||||
- [Room List Store](room-list-store.md)
|
- [Room List Store](room-list-store.md)
|
||||||
- [Scrolling](scrolling.md)
|
- [Scrolling](scrolling.md)
|
||||||
- [Usercontent](usercontent.md)
|
- [Usercontent](usercontent.md)
|
||||||
- [Widget layouts](widget-layouts.md)
|
- [Widget layouts](widget-layouts.md)
|
||||||
|
|
|
@ -61,18 +61,18 @@ flowchart TD
|
||||||
|
|
||||||
Key:
|
Key:
|
||||||
|
|
||||||
- Parallelogram: async/await task
|
- Parallelogram: async/await task
|
||||||
- Box: sync task
|
- Box: sync task
|
||||||
- Diamond: conditional branch
|
- Diamond: conditional branch
|
||||||
- Circle: user interaction
|
- Circle: user interaction
|
||||||
- Blue arrow: async task is allowed to settle but allowed to fail
|
- Blue arrow: async task is allowed to settle but allowed to fail
|
||||||
- Red arrow: async task success is asserted
|
- Red arrow: async task success is asserted
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- A task begins when all its dependencies (arrows going into it) are fulfilled.
|
- A task begins when all its dependencies (arrows going into it) are fulfilled.
|
||||||
- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake).
|
- The success of setting up rageshake is never asserted, element-web has a fallback path for running without IDB (and thus rageshake).
|
||||||
- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful.
|
- Everything is awaited to be settled before the Modernizr check, to allow it to make use of things like i18n if they are successful.
|
||||||
|
|
||||||
Underlying dependencies:
|
Underlying dependencies:
|
||||||
|
|
||||||
|
|
|
@ -32,19 +32,19 @@ someone to add something.
|
||||||
When you're looking through the list, here are some things that might make an
|
When you're looking through the list, here are some things that might make an
|
||||||
issue a **GOOD** choice:
|
issue a **GOOD** choice:
|
||||||
|
|
||||||
- It is a problem or feature you care about.
|
- It is a problem or feature you care about.
|
||||||
- It concerns a type of code you know a little about.
|
- It concerns a type of code you know a little about.
|
||||||
- You think you can understand what's needed.
|
- You think you can understand what's needed.
|
||||||
- It already has approval from Element Web's designers (look for comments from
|
- It already has approval from Element Web's designers (look for comments from
|
||||||
members of the
|
members of the
|
||||||
[Product](https://github.com/orgs/element-hq/teams/product/members) or
|
[Product](https://github.com/orgs/element-hq/teams/product/members) or
|
||||||
[Design](https://github.com/orgs/element-hq/teams/design/members) teams).
|
[Design](https://github.com/orgs/element-hq/teams/design/members) teams).
|
||||||
|
|
||||||
Here are some things that might make it a **BAD** choice:
|
Here are some things that might make it a **BAD** choice:
|
||||||
|
|
||||||
- You don't understand it (maybe add a comment asking a clarifying question).
|
- You don't understand it (maybe add a comment asking a clarifying question).
|
||||||
- It sounds difficult, or is part of a larger change you don't know about.
|
- It sounds difficult, or is part of a larger change you don't know about.
|
||||||
- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.**
|
- **It is tagged with `X-Needs-Design` or `X-Needs-Product`.**
|
||||||
|
|
||||||
**Element Web's Design and Product teams tend to be very busy**, so if you make
|
**Element Web's Design and Product teams tend to be very busy**, so if you make
|
||||||
changes that require approval from one of those teams, you will probably have
|
changes that require approval from one of those teams, you will probably have
|
||||||
|
|
|
@ -455,7 +455,7 @@ If you would like to use Scalar, the integration manager maintained by Element,
|
||||||
|
|
||||||
For widgets in general (from an integration manager or not) there is also:
|
For widgets in general (from an integration manager or not) there is also:
|
||||||
|
|
||||||
- `default_widget_container_height`
|
- `default_widget_container_height`
|
||||||
|
|
||||||
This controls the height that the top widget panel initially appears as and is the height in pixels, default 280.
|
This controls the height that the top widget panel initially appears as and is the height in pixels, default 280.
|
||||||
|
|
||||||
|
@ -551,38 +551,38 @@ preferences.
|
||||||
|
|
||||||
Currently, the following UI feature flags are supported:
|
Currently, the following UI feature flags are supported:
|
||||||
|
|
||||||
- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application.
|
- `UIFeature.urlPreviews` - Whether URL previews are enabled across the entire application.
|
||||||
- `UIFeature.feedback` - Whether prompts to supply feedback are shown.
|
- `UIFeature.feedback` - Whether prompts to supply feedback are shown.
|
||||||
- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled,
|
- `UIFeature.voip` - Whether or not VoIP is shown readily to the user. When disabled,
|
||||||
Jitsi widgets will still work though they cannot easily be added.
|
Jitsi widgets will still work though they cannot easily be added.
|
||||||
- `UIFeature.widgets` - Whether or not widgets will be shown.
|
- `UIFeature.widgets` - Whether or not widgets will be shown.
|
||||||
- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and
|
- `UIFeature.advancedSettings` - Whether or not sections titled "advanced" in room and
|
||||||
user settings are shown to the user.
|
user settings are shown to the user.
|
||||||
- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog
|
- `UIFeature.shareQrCode` - Whether or not the QR code on the share room/event dialog
|
||||||
is shown.
|
is shown.
|
||||||
- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog
|
- `UIFeature.shareSocial` - Whether or not the social icons on the share room/event dialog
|
||||||
are shown.
|
are shown.
|
||||||
- `UIFeature.identityServer` - Whether or not functionality requiring an identity server
|
- `UIFeature.identityServer` - Whether or not functionality requiring an identity server
|
||||||
is shown. When disabled, the user will not be able to interact with the identity
|
is shown. When disabled, the user will not be able to interact with the identity
|
||||||
server (sharing email addresses, 3PID invites, etc).
|
server (sharing email addresses, 3PID invites, etc).
|
||||||
- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs)
|
- `UIFeature.thirdPartyId` - Whether or not UI relating to third party identifiers (3PIDs)
|
||||||
is shown. Typically this is considered "contact information" on the homeserver, and is
|
is shown. Typically this is considered "contact information" on the homeserver, and is
|
||||||
not directly related to the identity server.
|
not directly related to the identity server.
|
||||||
- `UIFeature.registration` - Whether or not the registration page is accessible. Typically
|
- `UIFeature.registration` - Whether or not the registration page is accessible. Typically
|
||||||
useful if accounts are managed externally.
|
useful if accounts are managed externally.
|
||||||
- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically
|
- `UIFeature.passwordReset` - Whether or not the password reset page is accessible. Typically
|
||||||
useful if accounts are managed externally.
|
useful if accounts are managed externally.
|
||||||
- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically
|
- `UIFeature.deactivate` - Whether or not the deactivate account button is accessible. Typically
|
||||||
useful if accounts are managed externally.
|
useful if accounts are managed externally.
|
||||||
- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the
|
- `UIFeature.advancedEncryption` - Whether or not advanced encryption options are shown to the
|
||||||
user.
|
user.
|
||||||
- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user.
|
- `UIFeature.roomHistorySettings` - Whether or not the room history settings are shown to the user.
|
||||||
This should only be used if the room history visibility options are managed by the server.
|
This should only be used if the room history visibility options are managed by the server.
|
||||||
- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the
|
- `UIFeature.TimelineEnableRelativeDates` - Display relative date separators (eg: 'Today', 'Yesterday') in the
|
||||||
timeline for recent messages. When false day dates will be used.
|
timeline for recent messages. When false day dates will be used.
|
||||||
- `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.
|
||||||
|
|
||||||
## Undocumented / developer options
|
## Undocumented / developer options
|
||||||
|
|
||||||
|
@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
|
||||||
2. `sync_timeline_limit`
|
2. `sync_timeline_limit`
|
||||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||||
5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development.
|
|
||||||
|
|
|
@ -50,9 +50,9 @@ that properties/state machines won't change.
|
||||||
|
|
||||||
UI for some actions can be hidden via the ComponentVisibility customisation:
|
UI for some actions can be hidden via the ComponentVisibility customisation:
|
||||||
|
|
||||||
- inviting users to rooms and spaces,
|
- inviting users to rooms and spaces,
|
||||||
- creating rooms,
|
- creating rooms,
|
||||||
- creating spaces,
|
- creating spaces,
|
||||||
|
|
||||||
To customise visibility create a customisation module from [ComponentVisibility](https://github.com/element-hq/element-web/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above.
|
To customise visibility create a customisation module from [ComponentVisibility](https://github.com/element-hq/element-web/blob/master/src/customisations/ComponentVisibility.ts) following the instructions above.
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,9 @@ Set the following on your homeserver's
|
||||||
|
|
||||||
When `force_disable` is true:
|
When `force_disable` is true:
|
||||||
|
|
||||||
- all rooms will be created with encryption disabled, and it will not be possible to enable
|
- all rooms will be created with encryption disabled, and it will not be possible to enable
|
||||||
encryption from room settings.
|
encryption from room settings.
|
||||||
- any `io.element.e2ee.default` value will be disregarded.
|
- any `io.element.e2ee.default` value will be disregarded.
|
||||||
|
|
||||||
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.
|
||||||
|
|
|
@ -5,10 +5,10 @@ flexibility and control over when and where those features are enabled.
|
||||||
|
|
||||||
For example, flags make the following things possible:
|
For example, flags make the following things possible:
|
||||||
|
|
||||||
- Extended testing of a feature via labs on develop
|
- Extended testing of a feature via labs on develop
|
||||||
- Enabling features when ready instead of the first moment the code is released
|
- Enabling features when ready instead of the first moment the code is released
|
||||||
- Testing a feature with a specific set of users (by enabling only on a specific
|
- Testing a feature with a specific set of users (by enabling only on a specific
|
||||||
Element instance)
|
Element instance)
|
||||||
|
|
||||||
The size of the feature controlled by a feature flag may vary widely: it could
|
The size of the feature controlled by a feature flag may vary widely: it could
|
||||||
be a large project like reactions or a smaller change to an existing algorithm.
|
be a large project like reactions or a smaller change to an existing algorithm.
|
||||||
|
|
|
@ -2,37 +2,37 @@
|
||||||
|
|
||||||
## Auto Complete
|
## Auto Complete
|
||||||
|
|
||||||
- Hitting tab tries to auto-complete the word before the caret as a room member
|
- Hitting tab tries to auto-complete the word before the caret as a room member
|
||||||
- If no matching name is found, a visual bell is shown
|
- If no matching name is found, a visual bell is shown
|
||||||
- @ + a letter opens auto complete for members starting with the given letter
|
- @ + a letter opens auto complete for members starting with the given letter
|
||||||
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
|
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
|
||||||
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
|
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
|
||||||
- # + a letter opens auto complete for rooms starting with the given letter
|
- # + a letter opens auto complete for rooms starting with the given letter
|
||||||
- : open auto complete for emoji
|
- : open auto complete for emoji
|
||||||
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
|
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
|
||||||
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
|
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
|
||||||
wrapping around at the end after reverting to the typed text first.
|
wrapping around at the end after reverting to the typed text first.
|
||||||
|
|
||||||
## Formatting
|
## Formatting
|
||||||
|
|
||||||
- When selecting text, a formatting bar appears above the selection.
|
- When selecting text, a formatting bar appears above the selection.
|
||||||
- The formatting bar allows to format the selected test as:
|
- The formatting bar allows to format the selected test as:
|
||||||
bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
|
bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
|
||||||
- Formatting is applied as markdown syntax.
|
- Formatting is applied as markdown syntax.
|
||||||
- Hitting ctrl/cmd+B also marks the selected text as bold
|
- Hitting ctrl/cmd+B also marks the selected text as bold
|
||||||
- Hitting ctrl/cmd+I also marks the selected text as italic
|
- Hitting ctrl/cmd+I also marks the selected text as italic
|
||||||
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
|
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
|
|
||||||
- When hitting the arrow-up button while having the caret at the start in the composer,
|
- When hitting the arrow-up button while having the caret at the start in the composer,
|
||||||
the last message sent by the syncing user is edited.
|
the last message sent by the syncing user is edited.
|
||||||
- Clicking a display name on an event in the timeline inserts a user pill into the composer
|
- Clicking a display name on an event in the timeline inserts a user pill into the composer
|
||||||
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
|
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
|
||||||
- Typing in the composer sends typing notifications in the room
|
- Typing in the composer sends typing notifications in the room
|
||||||
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
|
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
|
||||||
- Pressing shift+enter inserts a line break
|
- Pressing shift+enter inserts a line break
|
||||||
- Pressing enter sends the message.
|
- Pressing enter sends the message.
|
||||||
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
|
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
|
||||||
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
|
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
|
||||||
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.
|
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.
|
||||||
|
|
|
@ -8,9 +8,9 @@ Icons have `role="presentation"` and `aria-hidden` automatically applied. These
|
||||||
|
|
||||||
SVG file recommendations:
|
SVG file recommendations:
|
||||||
|
|
||||||
- Colours should not be defined absolutely. Use `currentColor` instead.
|
- Colours should not be defined absolutely. Use `currentColor` instead.
|
||||||
- SVG files should be taken from the design compound as they are. Some icons contain special padding.
|
- SVG files should be taken from the design compound as they are. Some icons contain special padding.
|
||||||
This means that there should be icons for each size, e.g. warning-16px and warning-32px.
|
This means that there should be icons for each size, e.g. warning-16px and warning-32px.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
|
|
||||||
|
|
|
@ -81,27 +81,27 @@ which takes several parameters:
|
||||||
|
|
||||||
_Query string_:
|
_Query string_:
|
||||||
|
|
||||||
- `widgetId`: The ID of the widget. This is needed for communication back to the
|
- `widgetId`: The ID of the widget. This is needed for communication back to the
|
||||||
react-sdk.
|
react-sdk.
|
||||||
- `parentUrl`: The URL of the parent window. This is also needed for
|
- `parentUrl`: The URL of the parent window. This is also needed for
|
||||||
communication back to the react-sdk.
|
communication back to the react-sdk.
|
||||||
|
|
||||||
_Hash/fragment (formatted as a query string)_:
|
_Hash/fragment (formatted as a query string)_:
|
||||||
|
|
||||||
- `conferenceDomain`: The domain to connect Jitsi Meet to.
|
- `conferenceDomain`: The domain to connect Jitsi Meet to.
|
||||||
- `conferenceId`: The room or conference ID to connect Jitsi Meet to.
|
- `conferenceId`: The room or conference ID to connect Jitsi Meet to.
|
||||||
- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
|
- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
|
||||||
be present, should default to `false`.
|
be present, should default to `false`.
|
||||||
- `startWithAudioMuted`: Boolean for whether the calls start with audio
|
- `startWithAudioMuted`: Boolean for whether the calls start with audio
|
||||||
muted. May not be present.
|
muted. May not be present.
|
||||||
- `startWithVideoMuted`: Boolean for whether the calls start with video
|
- `startWithVideoMuted`: Boolean for whether the calls start with video
|
||||||
muted. May not be present.
|
muted. May not be present.
|
||||||
- `displayName`: The display name of the user viewing the widget. May not
|
- `displayName`: The display name of the user viewing the widget. May not
|
||||||
be present or could be null.
|
be present or could be null.
|
||||||
- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
|
- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
|
||||||
not be present or could be null.
|
not be present or could be null.
|
||||||
- `userId`: The MXID of the user viewing the widget. May not be present or could
|
- `userId`: The MXID of the user viewing the widget. May not be present or could
|
||||||
be null.
|
be null.
|
||||||
|
|
||||||
The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently
|
The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently
|
||||||
being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`.
|
being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`.
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
- How to run the tests
|
- How to run the tests
|
||||||
- How the tests work
|
- How the tests work
|
||||||
- How to write great Playwright tests
|
- How to write great Playwright tests
|
||||||
- Visual testing
|
- Visual testing
|
||||||
|
|
||||||
## Running the Tests
|
## Running the Tests
|
||||||
|
|
||||||
|
@ -123,15 +123,15 @@ When a Synapse instance is started, it's given a config generated from one of th
|
||||||
templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files
|
templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files
|
||||||
in these templates:
|
in these templates:
|
||||||
|
|
||||||
- `homeserver.yaml`:
|
- `homeserver.yaml`:
|
||||||
Template substitution happens in this file. Template variables are:
|
Template substitution happens in this file. Template variables are:
|
||||||
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
||||||
- `MACAROON_SECRET_KEY`: Generated each time for security
|
- `MACAROON_SECRET_KEY`: Generated each time for security
|
||||||
- `FORM_SECRET`: Generated each time for security
|
- `FORM_SECRET`: Generated each time for security
|
||||||
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
||||||
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
||||||
Config templates should not contain a signing key and instead assume that one will exist
|
Config templates should not contain a signing key and instead assume that one will exist
|
||||||
in this file.
|
in this file.
|
||||||
|
|
||||||
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
|
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
|
||||||
in a template can be referenced in the config as `/data/foo.html`.
|
in a template can be referenced in the config as `/data/foo.html`.
|
||||||
|
@ -217,3 +217,10 @@ instead of the native `toHaveScreenshot`.
|
||||||
|
|
||||||
If you are running Linux and are unfortunate that the screenshots are not rendering identically,
|
If you are running Linux and are unfortunate that the screenshots are not rendering identically,
|
||||||
you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you.
|
you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you.
|
||||||
|
|
||||||
|
## Test Tags
|
||||||
|
|
||||||
|
We use test tags to categorise tests for running subsets more efficiently.
|
||||||
|
|
||||||
|
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
|
||||||
|
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.
|
||||||
|
|
|
@ -82,28 +82,28 @@ This label will automagically convert to `X-Release-Blocker` at the conclusion o
|
||||||
|
|
||||||
This release process revolves around our main repositories:
|
This release process revolves around our main repositories:
|
||||||
|
|
||||||
- [Element Desktop](https://github.com/element-hq/element-desktop/)
|
- [Element Desktop](https://github.com/element-hq/element-desktop/)
|
||||||
- [Element Web](https://github.com/element-hq/element-web/)
|
- [Element Web](https://github.com/element-hq/element-web/)
|
||||||
- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/)
|
- [Matrix JS SDK](https://github.com/matrix-org/matrix-js-sdk/)
|
||||||
|
|
||||||
We own other repositories, but they have more ad-hoc releases and are not part of the bi-weekly cycle:
|
We own other repositories, but they have more ad-hoc releases and are not part of the bi-weekly cycle:
|
||||||
|
|
||||||
- https://github.com/matrix-org/matrix-web-i18n/
|
- https://github.com/matrix-org/matrix-web-i18n/
|
||||||
- https://github.com/matrix-org/matrix-react-sdk-module-api
|
- https://github.com/matrix-org/matrix-react-sdk-module-api
|
||||||
|
|
||||||
</blockquote></details>
|
</blockquote></details>
|
||||||
|
|
||||||
<details><summary><h1>Prerequisites</h1></summary><blockquote>
|
<details><summary><h1>Prerequisites</h1></summary><blockquote>
|
||||||
|
|
||||||
- You must be part of the 2 Releasers GitHub groups:
|
- You must be part of the 2 Releasers GitHub groups:
|
||||||
- <https://github.com/orgs/element-hq/teams/element-web-releasers>
|
- <https://github.com/orgs/element-hq/teams/element-web-releasers>
|
||||||
- <https://github.com/orgs/matrix-org/teams/element-web-releasers>
|
- <https://github.com/orgs/matrix-org/teams/element-web-releasers>
|
||||||
- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below.
|
- You will need access to the **VPN** ([docs](https://gitlab.matrix.org/new-vector/internal/-/wikis/SRE/Tailscale)) to be able to follow the instructions under Deploy below.
|
||||||
- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for:
|
- You will need the ability to **SSH** in to the production machines to be able to follow the instructions under Deploy below. Ensure that your SSH key has a non-empty passphrase, and you registered your SSH key with Ops. Log a ticket at https://github.com/matrix-org/matrix-ansible-private and ask for:
|
||||||
- Two-factor authentication to be set up on your SSH key. (This is needed to get access to production).
|
- Two-factor authentication to be set up on your SSH key. (This is needed to get access to production).
|
||||||
- SSH access to `horme` (staging.element.io and app.element.io)
|
- SSH access to `horme` (staging.element.io and app.element.io)
|
||||||
- Permission to sudo on horme as the user `element`
|
- Permission to sudo on horme as the user `element`
|
||||||
- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding.
|
- You need "**jumphost**" configuration in your local `~/.ssh/config`. This should have been set up as part of your onboarding.
|
||||||
|
|
||||||
</blockquote></details>
|
</blockquote></details>
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ For security, you may wish to merge the security advisory private fork or apply
|
||||||
It is worth noting that at the end of the Final/Hotfix/Security release `staging` is merged to `master` which is merged back into `develop` -
|
It is worth noting that at the end of the Final/Hotfix/Security release `staging` is merged to `master` which is merged back into `develop` -
|
||||||
this means that any commit which goes to `staging` will eventually make its way back to the default branch.
|
this means that any commit which goes to `staging` will eventually make its way back to the default branch.
|
||||||
|
|
||||||
- [ ] The staging branch is prepared
|
- [ ] The staging branch is prepared
|
||||||
|
|
||||||
# Releasing
|
# Releasing
|
||||||
|
|
||||||
|
@ -192,21 +192,21 @@ switched back to the version of the dependency from the master branch to not lea
|
||||||
|
|
||||||
### Matrix JS SDK
|
### Matrix JS SDK
|
||||||
|
|
||||||
- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml)
|
- [ ] Check the draft release which has been generated by [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release-drafter.yml)
|
||||||
- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft**
|
- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft**
|
||||||
- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.
|
- [ ] Kick off a release using [the automation](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.
|
||||||
|
|
||||||
### Element Web
|
### Element Web
|
||||||
|
|
||||||
- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml)
|
- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-web/actions/workflows/release-drafter.yml)
|
||||||
- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft**
|
- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft**
|
||||||
- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.
|
- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-web/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.
|
||||||
|
|
||||||
### Element Desktop
|
### Element Desktop
|
||||||
|
|
||||||
- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml)
|
- [ ] Check the draft release which has been generated by [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release-drafter.yml)
|
||||||
- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft**
|
- [ ] Make any changes to the release notes in the draft release as are necessary - **Do not click publish, only save draft**
|
||||||
- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.
|
- [ ] Kick off a release using [the automation](https://github.com/element-hq/element-desktop/actions/workflows/release.yml) - making sure to select the right type of release. For anything other than an RC: choose final. You should not need to ever switch off either of the Publishing options.
|
||||||
|
|
||||||
# Deploying
|
# Deploying
|
||||||
|
|
||||||
|
@ -214,23 +214,23 @@ We ship the SDKs to npm, this happens as part of the release process.
|
||||||
We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
||||||
We ship Element Desktop to packages.element.io.
|
We ship Element Desktop to packages.element.io.
|
||||||
|
|
||||||
- [ ] Check that element-web has shipped to dockerhub
|
- [ ] Check that element-web has shipped to dockerhub
|
||||||
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
||||||
- [ ] Test staging.element.io
|
- [ ] Test staging.element.io
|
||||||
|
|
||||||
For final releases additionally do these steps:
|
For final releases additionally do these steps:
|
||||||
|
|
||||||
- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
- [ ] Deploy app.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
||||||
- [ ] Test app.element.io
|
- [ ] Test app.element.io
|
||||||
- [ ] Ensure Element Web package has shipped to packages.element.io
|
- [ ] Ensure Element Web package has shipped to packages.element.io
|
||||||
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
||||||
|
|
||||||
# Housekeeping
|
# Housekeeping
|
||||||
|
|
||||||
We have some manual housekeeping to do in order to prepare for the next release.
|
We have some manual housekeeping to do in order to prepare for the next release.
|
||||||
|
|
||||||
- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work.
|
- [ ] Update topics using [the automation](https://github.com/element-hq/element-web/actions/workflows/update-topics.yaml). It will autodetect the current latest version. Don't forget the date you supply should be e.g. September 5th (including the "th") for the script to work.
|
||||||
- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org)
|
- [ ] Announce the release in [#element-web-announcements:matrix.org](https://matrix.to/#/#element-web-announcements:matrix.org)
|
||||||
|
|
||||||
<details><summary>(show)</summary>
|
<details><summary>(show)</summary>
|
||||||
|
|
||||||
|
@ -246,15 +246,15 @@ With wording like:
|
||||||
|
|
||||||
For the first RC of a given release cycle do these steps:
|
For the first RC of a given release cycle do these steps:
|
||||||
|
|
||||||
- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs.
|
- [ ] Go to the [matrix-js-sdk Renovate dashboard](https://github.com/matrix-org/matrix-js-sdk/issues/2406) and click the checkbox to create/update its PRs.
|
||||||
|
|
||||||
- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs.
|
- [ ] Go to the [element-web Renovate dashboard](https://github.com/element-hq/element-web/issues/22941) and click the checkbox to create/update its PRs.
|
||||||
|
|
||||||
- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs.
|
- [ ] Go to the [element-desktop Renovate dashboard](https://github.com/element-hq/element-desktop/issues/465) and click the checkbox to create/update its PRs.
|
||||||
|
|
||||||
- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#).
|
- [ ] Later, check back and merge the PRs that succeeded to build. The ones that failed will get picked up by the [maintainer](https://docs.google.com/document/d/1V5VINWXATMpz9UBw4IKmVVB8aw3CxM0Jt7igtHnDfSk/edit#).
|
||||||
|
|
||||||
For final releases additionally do these steps:
|
For final releases additionally do these steps:
|
||||||
|
|
||||||
- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_
|
- [ ] Archive done column on the [team board](https://github.com/orgs/element-hq/projects/67/views/34) _Note: this should be automated_
|
||||||
- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case.
|
- [ ] Add entry to the [milestones diary](https://docs.google.com/document/d/1cpRFJdfNCo2Ps6jqzQmatzbYEToSrQpyBug0aP_iwZE/edit#heading=h.6y55fw4t283z). The document says only to add significant releases, but we add all of them just in case.
|
||||||
|
|
|
@ -10,53 +10,53 @@ When reviewing code, here are some things we look for and also things we avoid:
|
||||||
|
|
||||||
### We review for
|
### We review for
|
||||||
|
|
||||||
- Correctness
|
- Correctness
|
||||||
- Performance
|
- Performance
|
||||||
- Accessibility
|
- Accessibility
|
||||||
- Security
|
- Security
|
||||||
- Quality via automated and manual testing
|
- Quality via automated and manual testing
|
||||||
- Comments and documentation where needed
|
- Comments and documentation where needed
|
||||||
- Sharing knowledge of different areas among the team
|
- Sharing knowledge of different areas among the team
|
||||||
- Ensuring it's something we're comfortable maintaining for the long term
|
- Ensuring it's something we're comfortable maintaining for the long term
|
||||||
- Progress indicators and local echo where appropriate with network activity
|
- Progress indicators and local echo where appropriate with network activity
|
||||||
|
|
||||||
### We should avoid
|
### We should avoid
|
||||||
|
|
||||||
- Style nits that are already handled by the linter
|
- Style nits that are already handled by the linter
|
||||||
- Dramatically increasing scope
|
- Dramatically increasing scope
|
||||||
|
|
||||||
### Good practices
|
### Good practices
|
||||||
|
|
||||||
- Use empathetic language
|
- Use empathetic language
|
||||||
- See also [Mindful Communication in Code
|
- See also [Mindful Communication in Code
|
||||||
Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e)
|
Reviews](https://kickstarter.engineering/a-guide-to-mindful-communication-in-code-reviews-48aab5282e5e)
|
||||||
and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/)
|
and [How to Do Code Reviews Like a Human](https://mtlynch.io/human-code-reviews-1/)
|
||||||
- Authors should prefer smaller commits for easier reviewing and bisection
|
- Authors should prefer smaller commits for easier reviewing and bisection
|
||||||
- Reviewers should be explicit about required versus optional changes
|
- Reviewers should be explicit about required versus optional changes
|
||||||
- Reviews are conversations and the PR author should feel comfortable
|
- Reviews are conversations and the PR author should feel comfortable
|
||||||
discussing and pushing back on changes before making them
|
discussing and pushing back on changes before making them
|
||||||
- Reviewers are encouraged to ask for tests where they believe it is reasonable
|
- Reviewers are encouraged to ask for tests where they believe it is reasonable
|
||||||
- Core team should lead by example through their tone and language
|
- Core team should lead by example through their tone and language
|
||||||
- Take the time to thank and point out good code changes
|
- Take the time to thank and point out good code changes
|
||||||
- Using softer language like "please" and "what do you think?" goes a long way
|
- Using softer language like "please" and "what do you think?" goes a long way
|
||||||
towards making others feel like colleagues working towards a common goal
|
towards making others feel like colleagues working towards a common goal
|
||||||
|
|
||||||
### Workflow
|
### Workflow
|
||||||
|
|
||||||
- Authors should request review from the element-web team by default (if someone on
|
- Authors should request review from the element-web team by default (if someone on
|
||||||
the team is clearly the expert in an area, a direct review request to them may
|
the team is clearly the expert in an area, a direct review request to them may
|
||||||
be more appropriate)
|
be more appropriate)
|
||||||
- Reviewers should remove the team review request and request review from
|
- Reviewers should remove the team review request and request review from
|
||||||
themselves when starting a review to avoid double review
|
themselves when starting a review to avoid double review
|
||||||
- If there are multiple related PRs authors should reference each of the PRs in
|
- If there are multiple related PRs authors should reference each of the PRs in
|
||||||
the others before requesting review. Reviewers might start reviewing from
|
the others before requesting review. Reviewers might start reviewing from
|
||||||
different places and could miss other required PRs.
|
different places and could miss other required PRs.
|
||||||
- Avoid force pushing to a PR after the first round of review
|
- Avoid force pushing to a PR after the first round of review
|
||||||
- Use the GitHub default of merge commits when landing (avoid alternate options
|
- Use the GitHub default of merge commits when landing (avoid alternate options
|
||||||
like squash or rebase)
|
like squash or rebase)
|
||||||
- PR author merges after review (assuming they have write access)
|
- PR author merges after review (assuming they have write access)
|
||||||
- Assign issues only when in progress to indicate to others what can be picked
|
- Assign issues only when in progress to indicate to others what can be picked
|
||||||
up
|
up
|
||||||
|
|
||||||
## Code Quality
|
## Code Quality
|
||||||
|
|
||||||
|
@ -64,10 +64,10 @@ In the past, we have occasionally written different kinds of tests for
|
||||||
Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd
|
Element and the SDKs, but it hasn't been a consistent focus. Going forward, we'd
|
||||||
like to change that.
|
like to change that.
|
||||||
|
|
||||||
- For new features, code reviewers will expect some form of automated testing to
|
- For new features, code reviewers will expect some form of automated testing to
|
||||||
be included by default
|
be included by default
|
||||||
- For bug fixes, regression tests are of course great to have, but we don't want
|
- For bug fixes, regression tests are of course great to have, but we don't want
|
||||||
to block fixes on this, so we won't require them at this time
|
to block fixes on this, so we won't require them at this time
|
||||||
|
|
||||||
The above policy is not a strict rule, but instead it's meant to be a
|
The above policy is not a strict rule, but instead it's meant to be a
|
||||||
conversation between the author and reviewer. As an author, try to think about
|
conversation between the author and reviewer. As an author, try to think about
|
||||||
|
@ -104,10 +104,10 @@ perspective.
|
||||||
In more detail, our usual process for changes that affect the UI or alter user
|
In more detail, our usual process for changes that affect the UI or alter user
|
||||||
functionality is:
|
functionality is:
|
||||||
|
|
||||||
- For changes that will go live when merged, always flag Design and Product
|
- For changes that will go live when merged, always flag Design and Product
|
||||||
teams as appropriate
|
teams as appropriate
|
||||||
- For changes guarded by a feature flag, Design and Product review is not
|
- For changes guarded by a feature flag, Design and Product review is not
|
||||||
required (though may still be useful) since we can continue tweaking
|
required (though may still be useful) since we can continue tweaking
|
||||||
|
|
||||||
As it can be difficult to review design work from looking at just the changed
|
As it can be difficult to review design work from looking at just the changed
|
||||||
files in a PR, a [preview site](./pr-previews.md) that includes your changes
|
files in a PR, a [preview site](./pr-previews.md) that includes your changes
|
||||||
|
|
|
@ -6,11 +6,11 @@ It's so complicated it needs its own README.
|
||||||
|
|
||||||
Legend:
|
Legend:
|
||||||
|
|
||||||
- Orange = External event.
|
- Orange = External event.
|
||||||
- Purple = Deterministic flow.
|
- Purple = Deterministic flow.
|
||||||
- Green = Algorithm definition.
|
- Green = Algorithm definition.
|
||||||
- Red = Exit condition/point.
|
- Red = Exit condition/point.
|
||||||
- Blue = Process definition.
|
- Blue = Process definition.
|
||||||
|
|
||||||
## Algorithms involved
|
## Algorithms involved
|
||||||
|
|
||||||
|
@ -68,14 +68,14 @@ simply get the manual sorting algorithm applied to them with no further involvem
|
||||||
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
|
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
|
||||||
relative (perceived) importance to the user:
|
relative (perceived) importance to the user:
|
||||||
|
|
||||||
- **Red**: The room has unread mentions waiting for the user.
|
- **Red**: The room has unread mentions waiting for the user.
|
||||||
- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
|
- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
|
||||||
messages which cause a push notification or badge count. Typically, this is the default as rooms get
|
messages which cause a push notification or badge count. Typically, this is the default as rooms get
|
||||||
set to 'All Messages'.
|
set to 'All Messages'.
|
||||||
- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
||||||
a badge/notification count (or 'Mentions Only'/'Muted').
|
a badge/notification count (or 'Mentions Only'/'Muted').
|
||||||
- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
||||||
last read it.
|
last read it.
|
||||||
|
|
||||||
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
||||||
above bold, etc.
|
above bold, etc.
|
||||||
|
|
|
@ -10,13 +10,13 @@ of dealing with the different levels and exposes easy to use getters and setters
|
||||||
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
|
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
|
||||||
order of priority, are:
|
order of priority, are:
|
||||||
|
|
||||||
- `device` - The current user's device
|
- `device` - The current user's device
|
||||||
- `room-device` - The current user's device, but only when in a specific room
|
- `room-device` - The current user's device, but only when in a specific room
|
||||||
- `room-account` - The current user's account, but only when in a specific room
|
- `room-account` - The current user's account, but only when in a specific room
|
||||||
- `account` - The current user's account
|
- `account` - The current user's account
|
||||||
- `room` - A specific room (setting for all members of the room)
|
- `room` - A specific room (setting for all members of the room)
|
||||||
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
|
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
|
||||||
- `default` - The hardcoded default for the settings
|
- `default` - The hardcoded default for the settings
|
||||||
|
|
||||||
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
||||||
that room administrators cannot force account-only settings upon participants.
|
that room administrators cannot force account-only settings upon participants.
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- A working [Development Setup](../README.md#setting-up-a-dev-environment)
|
- A working [Development Setup](../README.md#setting-up-a-dev-environment)
|
||||||
- Latest LTS version of Node.js installed
|
- Latest LTS version of Node.js installed
|
||||||
- Be able to understand English
|
- Be able to understand English
|
||||||
|
|
||||||
## Translating strings vs. marking strings for translation
|
## Translating strings vs. marking strings for translation
|
||||||
|
|
||||||
|
@ -65,17 +65,17 @@ There you can also require all translations to be redone if the meaning of the s
|
||||||
1. Add it to the array in `_t` for example `_t(TKEY, {variable: this.variable})`
|
1. Add it to the array in `_t` for example `_t(TKEY, {variable: this.variable})`
|
||||||
1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name.
|
1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name.
|
||||||
|
|
||||||
- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two).
|
- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two).
|
||||||
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink.
|
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink.
|
||||||
- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`.
|
- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`.
|
||||||
|
|
||||||
## Things to know/Style Guides
|
## Things to know/Style Guides
|
||||||
|
|
||||||
- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later.
|
- Do not use `_t()` inside `getDefaultProps`: the translations aren't loaded when `getDefaultProps` is called, leading to missing translations. Use `_td()` to indicate that `_t()` will be called on the string later.
|
||||||
- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later.
|
- If using translated strings as constants, translated strings can't be in constants loaded at class-load time since the translations won't be loaded. Mark the strings using `_td()` instead and perform the actual translation later.
|
||||||
- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too.
|
- If a string is presented in the UI with punctuation like a full stop, include this in the translation strings, since punctuation varies between languages too.
|
||||||
- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible.
|
- Avoid "translation in parts", i.e. concatenating translated strings or using translated strings in variable substitutions. Context is important for translations, and translating partial strings this way is simply not always possible.
|
||||||
- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages.
|
- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages.
|
||||||
- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence.
|
- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence.
|
||||||
- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments.
|
- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments.
|
||||||
- Don't forget curly braces when you assign an expression to JSX attributes in the render method)
|
- Don't forget curly braces when you assign an expression to JSX attributes in the render method)
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Web Browser
|
- Web Browser
|
||||||
- Be able to understand English
|
- Be able to understand English
|
||||||
- Be able to understand the language you want to translate Element into
|
- Be able to understand the language you want to translate Element into
|
||||||
|
|
||||||
## Join #element-translations:matrix.org
|
## Join #element-translations:matrix.org
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,6 @@ const config: Config = {
|
||||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||||
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
|
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
|
||||||
"FontManager.ts": "<rootDir>/__mocks__/FontManager.js",
|
|
||||||
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
|
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
|
||||||
"^!!raw-loader!.*": "jest-raw-loader",
|
"^!!raw-loader!.*": "jest-raw-loader",
|
||||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||||
|
|
22
package.json
22
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.86",
|
"version": "1.11.87",
|
||||||
"description": "A feature-rich client for Matrix.org",
|
"description": "A feature-rich client for Matrix.org",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
"test:playwright:open": "yarn test:playwright --ui",
|
"test:playwright:open": "yarn test:playwright --ui",
|
||||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||||
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
|
||||||
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright",
|
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot",
|
||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
|
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
|
||||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||||
|
@ -73,12 +73,14 @@
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"oidc-client-ts": "3.1.0",
|
"oidc-client-ts": "3.1.0",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001679",
|
"caniuse-lite": "1.0.30001684",
|
||||||
"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",
|
||||||
|
"@fontsource/inconsolata": "^5",
|
||||||
|
"@fontsource/inter": "^5",
|
||||||
"@formatjs/intl-segmenter": "^11.5.7",
|
"@formatjs/intl-segmenter": "^11.5.7",
|
||||||
"@matrix-org/analytics-events": "^0.29.0",
|
"@matrix-org/analytics-events": "^0.29.0",
|
||||||
"@matrix-org/emojibase-bindings": "^1.3.3",
|
"@matrix-org/emojibase-bindings": "^1.3.3",
|
||||||
|
@ -86,7 +88,7 @@
|
||||||
"@matrix-org/spec": "^1.7.0",
|
"@matrix-org/spec": "^1.7.0",
|
||||||
"@sentry/browser": "^8.0.0",
|
"@sentry/browser": "^8.0.0",
|
||||||
"@vector-im/compound-design-tokens": "^2.0.1",
|
"@vector-im/compound-design-tokens": "^2.0.1",
|
||||||
"@vector-im/compound-web": "^7.4.0",
|
"@vector-im/compound-web": "^7.5.0",
|
||||||
"@vector-im/matrix-wysiwyg": "2.37.13",
|
"@vector-im/matrix-wysiwyg": "2.37.13",
|
||||||
"@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",
|
||||||
|
@ -114,10 +116,10 @@
|
||||||
"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-element": "4.1.3",
|
"linkify-element": "4.2.0",
|
||||||
"linkify-react": "4.1.3",
|
"linkify-react": "4.2.0",
|
||||||
"linkify-string": "4.1.3",
|
"linkify-string": "4.2.0",
|
||||||
"linkifyjs": "4.1.3",
|
"linkifyjs": "4.2.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"maplibre-gl": "^4.0.0",
|
"maplibre-gl": "^4.0.0",
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
|
@ -214,7 +216,6 @@
|
||||||
"babel-loader": "^9.0.0",
|
"babel-loader": "^9.0.0",
|
||||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||||
"blob-polyfill": "^9.0.0",
|
"blob-polyfill": "^9.0.0",
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"concurrently": "^9.0.0",
|
"concurrently": "^9.0.0",
|
||||||
"copy-webpack-plugin": "^12.0.0",
|
"copy-webpack-plugin": "^12.0.0",
|
||||||
|
@ -268,11 +269,12 @@
|
||||||
"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.3.3",
|
"prettier": "3.4.2",
|
||||||
"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",
|
||||||
"stylelint": "^16.1.0",
|
"stylelint": "^16.1.0",
|
||||||
"stylelint-config-standard": "^36.0.0",
|
"stylelint-config-standard": "^36.0.0",
|
||||||
"stylelint-scss": "^6.0.0",
|
"stylelint-scss": "^6.0.0",
|
||||||
|
|
|
@ -6,11 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineConfig } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
projects: [{ name: "Chrome", use: { ...devices["Desktop Chrome"], channel: "chromium" } }],
|
||||||
use: {
|
use: {
|
||||||
viewport: { width: 1280, height: 720 },
|
viewport: { width: 1280, height: 720 },
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mcr.microsoft.com/playwright:v1.48.2-jammy
|
FROM mcr.microsoft.com/playwright:v1.49.0-noble
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
test(`shows error page if browser lacks Intl support`, async ({ page }) => {
|
test(`shows error page if browser lacks Intl support`, { tag: "@screenshot" }, async ({ page }) => {
|
||||||
await page.addInitScript({ content: `delete window.Intl;` });
|
await page.addInitScript({ content: `delete window.Intl;` });
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ test(`shows error page if browser lacks Intl support`, async ({ page }) => {
|
||||||
await expect(page).toMatchScreenshot("unsupported-browser.png");
|
await expect(page).toMatchScreenshot("unsupported-browser.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`shows error page if browser lacks WebAssembly support`, async ({ page }) => {
|
test(`shows error page if browser lacks WebAssembly support`, { tag: "@screenshot" }, async ({ page }) => {
|
||||||
await page.addInitScript({ content: `delete window.WebAssembly;` });
|
await page.addInitScript({ content: `delete window.WebAssembly;` });
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
|
|
|
@ -134,18 +134,22 @@ test.describe("Audio player", () => {
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be correctly rendered - light theme", async ({ page, app }) => {
|
test("should be correctly rendered - light theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
|
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => {
|
test(
|
||||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
"should be correctly rendered - light theme with monospace font",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app }) => {
|
||||||
|
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||||
|
|
||||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
|
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should be correctly rendered - high contrast theme", async ({ page, app }) => {
|
test("should be correctly rendered - high contrast theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
// Disable system theme in case ThemeWatcher enables the theme automatically,
|
// Disable system theme in case ThemeWatcher enables the theme automatically,
|
||||||
// so that the high contrast theme can be enabled
|
// so that the high contrast theme can be enabled
|
||||||
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
await app.settings.setValue("use_system_theme", null, SettingLevel.DEVICE, false);
|
||||||
|
@ -161,7 +165,7 @@ test.describe("Audio player", () => {
|
||||||
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
|
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be correctly rendered - dark theme", async ({ page, app }) => {
|
test("should be correctly rendered - dark theme", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
// Enable dark theme
|
// Enable dark theme
|
||||||
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
|
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
|
||||||
|
|
||||||
|
@ -207,93 +211,101 @@ test.describe("Audio player", () => {
|
||||||
expect(download.suggestedFilename()).toBe("1sec.ogg");
|
expect(download.suggestedFilename()).toBe("1sec.ogg");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should support replying to audio file with another audio file", async ({ page, app }) => {
|
test(
|
||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
"should support replying to audio file with another audio file",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app }) => {
|
||||||
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert the audio player is rendered
|
// Assert the audio player is rendered
|
||||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).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");
|
||||||
await tile.hover();
|
|
||||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
|
||||||
|
|
||||||
// Reply to the player with another audio file
|
|
||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
|
||||||
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that replied audio file is rendered as file button inside ReplyChain
|
|
||||||
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
|
||||||
// Assert that the file button has file name
|
|
||||||
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
|
|
||||||
|
|
||||||
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should support creating a reply chain with multiple audio files", async ({ page, app, user }) => {
|
|
||||||
// Note: "mx_ReplyChain" element is used not only for replies which
|
|
||||||
// create a reply chain, but also for a single reply without a replied
|
|
||||||
// message. This test checks whether a reply chain which consists of
|
|
||||||
// multiple audio file replies is rendered properly.
|
|
||||||
|
|
||||||
const tile = page.locator(".mx_EventTile_last");
|
|
||||||
|
|
||||||
// Find and click "Reply" button
|
|
||||||
const clickButtonReply = async () => {
|
|
||||||
await tile.scrollIntoViewIfNeeded();
|
|
||||||
await tile.hover();
|
await tile.hover();
|
||||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||||
};
|
|
||||||
|
|
||||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
// Reply to the player with another audio file
|
||||||
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
await clickButtonReply();
|
// Assert that replied audio file is rendered as file button inside ReplyChain
|
||||||
|
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
||||||
|
// Assert that the file button has file name
|
||||||
|
await expect(button.locator(".mx_MFileBody_info_filename")).toBeVisible();
|
||||||
|
|
||||||
// Reply to the player with another audio file
|
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply");
|
||||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
test(
|
||||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
"should support creating a reply chain with multiple audio files",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
// Note: "mx_ReplyChain" element is used not only for replies which
|
||||||
|
// create a reply chain, but also for a single reply without a replied
|
||||||
|
// message. This test checks whether a reply chain which consists of
|
||||||
|
// multiple audio file replies is rendered properly.
|
||||||
|
|
||||||
await clickButtonReply();
|
const tile = page.locator(".mx_EventTile_last");
|
||||||
|
|
||||||
// Reply to the player with yet another audio file to create a reply chain
|
// Find and click "Reply" button
|
||||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
const clickButtonReply = async () => {
|
||||||
|
await tile.scrollIntoViewIfNeeded();
|
||||||
|
await tile.hover();
|
||||||
|
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||||
|
};
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||||
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that there are two "mx_ReplyChain" elements
|
// Assert that the audio player is rendered
|
||||||
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
// Assert that one line contains the user name
|
await clickButtonReply();
|
||||||
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the other line contains the file button
|
// Reply to the player with another audio file
|
||||||
await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible();
|
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||||
|
|
||||||
// Click "In reply to"
|
// Assert that the audio player is rendered
|
||||||
await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click();
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
const replyChain = tile.locator(".mx_ReplyChain:first-of-type");
|
await clickButtonReply();
|
||||||
// Assert that "In reply to" has disappeared
|
|
||||||
await expect(replyChain.getByText("In reply to")).not.toBeVisible();
|
|
||||||
|
|
||||||
// Assert that the file button contains the name of the file sent at first
|
// Reply to the player with yet another audio file to create a reply chain
|
||||||
await expect(
|
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||||
replyChain
|
|
||||||
.locator(".mx_MFileBody_info[role='button']")
|
|
||||||
.locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Take snapshots
|
// Assert that the audio player is rendered
|
||||||
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
|
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||||
});
|
|
||||||
|
// Assert that there are two "mx_ReplyChain" elements
|
||||||
|
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
||||||
|
|
||||||
|
// Assert that one line contains the user name
|
||||||
|
await expect(tile.locator(".mx_ReplyChain .mx_ReplyTile_sender").getByText(user.displayName)).toBeVisible();
|
||||||
|
|
||||||
|
// Assert that the other line contains the file button
|
||||||
|
await expect(tile.locator(".mx_ReplyChain .mx_MFileBody")).toBeVisible();
|
||||||
|
|
||||||
|
// Click "In reply to"
|
||||||
|
await tile.locator(".mx_ReplyChain .mx_ReplyChain_show", { hasText: "In reply to" }).click();
|
||||||
|
|
||||||
|
const replyChain = tile.locator(".mx_ReplyChain:first-of-type");
|
||||||
|
// Assert that "In reply to" has disappeared
|
||||||
|
await expect(replyChain.getByText("In reply to")).not.toBeVisible();
|
||||||
|
|
||||||
|
// Assert that the file button contains the name of the file sent at first
|
||||||
|
await expect(
|
||||||
|
replyChain
|
||||||
|
.locator(".mx_MFileBody_info[role='button']")
|
||||||
|
.locator(".mx_MFileBody_info_filename", { hasText: "upload-first.ogg" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Take snapshots
|
||||||
|
await takeSnapshots(page, app, "Selected EventTile of audio player with a reply chain");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
|
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
|
||||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||||
|
|
|
@ -89,43 +89,47 @@ test.describe("HTML Export", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should export html successfully and match screenshot", async ({ page, app, room }) => {
|
test(
|
||||||
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
|
"should export html successfully and match screenshot",
|
||||||
// about the width changing and we can actually test this line looks correct.
|
{ tag: "@screenshot" },
|
||||||
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
async ({ page, app, room }) => {
|
||||||
|
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
|
||||||
|
// about the width changing and we can actually test this line looks correct.
|
||||||
|
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
||||||
|
|
||||||
// Send a bunch of messages to populate the room
|
// Send a bunch of messages to populate the room
|
||||||
for (let i = 1; i < 10; i++) {
|
for (let i = 1; i < 10; i++) {
|
||||||
const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" });
|
const respone = await app.client.sendMessage(room.roomId, { body: `Testing ${i}`, msgtype: "m.text" });
|
||||||
if (i == 1) {
|
if (i == 1) {
|
||||||
await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃");
|
await app.client.reactToMessage(room.roomId, null, respone.event_id, "🙃");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all the messages to be displayed
|
// Wait for all the messages to be displayed
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"),
|
page.locator(".mx_EventTile_last .mx_MTextBody .mx_EventTile_body").getByText("Testing 9"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await app.toggleRoomInfoPanel();
|
await app.toggleRoomInfoPanel();
|
||||||
await page.getByRole("menuitem", { name: "Export Chat" }).click();
|
await page.getByRole("menuitem", { name: "Export Chat" }).click();
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent("download");
|
const downloadPromise = page.waitForEvent("download");
|
||||||
await page.getByRole("button", { name: "Export", exact: true }).click();
|
await page.getByRole("button", { name: "Export", exact: true }).click();
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
|
|
||||||
const dirPath = path.join(os.tmpdir(), "html-export-test");
|
const dirPath = path.join(os.tmpdir(), "html-export-test");
|
||||||
const zipPath = `${dirPath}.zip`;
|
const zipPath = `${dirPath}.zip`;
|
||||||
await download.saveAs(zipPath);
|
await download.saveAs(zipPath);
|
||||||
|
|
||||||
const zip = await extractZipFileToPath(zipPath, dirPath);
|
const zip = await extractZipFileToPath(zipPath, dirPath);
|
||||||
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
|
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
|
||||||
await expect(page).toMatchScreenshot("html-export.png", {
|
await expect(page).toMatchScreenshot("html-export.png", {
|
||||||
mask: [
|
mask: [
|
||||||
// We need to mask the whole thing because the width of the time part changes
|
// We need to mask the whole thing because the width of the time part changes
|
||||||
page.locator(".mx_TimelineSeparator"),
|
page.locator(".mx_TimelineSeparator"),
|
||||||
page.locator(".mx_MessageTimestamp"),
|
page.locator(".mx_MessageTimestamp"),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -204,30 +204,29 @@ test.describe("Cryptography", function () {
|
||||||
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("creating a DM should work, being e2e-encrypted / user verification", async ({
|
test(
|
||||||
page,
|
"creating a DM should work, being e2e-encrypted / user verification",
|
||||||
app,
|
{ tag: "@screenshot" },
|
||||||
bot: bob,
|
async ({ page, app, bot: bob, user: aliceCredentials }) => {
|
||||||
user: aliceCredentials,
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
}) => {
|
await startDMWithBob(page, bob);
|
||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
// send first message
|
||||||
await startDMWithBob(page, bob);
|
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
|
||||||
// send first message
|
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
|
await checkDMRoom(page);
|
||||||
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
const bobRoomId = await bobJoin(page, bob);
|
||||||
await checkDMRoom(page);
|
await testMessages(page, bob, bobRoomId);
|
||||||
const bobRoomId = await bobJoin(page, bob);
|
await verify(app, bob);
|
||||||
await testMessages(page, bob, bobRoomId);
|
|
||||||
await verify(app, bob);
|
|
||||||
|
|
||||||
// Assert that verified icon is rendered
|
// Assert that verified icon is rendered
|
||||||
await page.getByTestId("base-card-back-button").click();
|
await page.getByTestId("base-card-back-button").click();
|
||||||
await page.getByLabel("Room info").nth(1).click();
|
await page.getByLabel("Room info").nth(1).click();
|
||||||
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted");
|
await expect(page.locator('.mx_RoomSummaryCard_badges [data-kind="green"]')).toContainText("Encrypted");
|
||||||
|
|
||||||
// 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");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should allow verification when there is no existing DM", async ({
|
test("should allow verification when there is no existing DM", async ({
|
||||||
page,
|
page,
|
||||||
|
|
|
@ -67,6 +67,9 @@ test.describe("Cryptography", function () {
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
||||||
await app.viewRoomByName("Test room");
|
await app.viewRoomByName("Test room");
|
||||||
|
|
||||||
|
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
// There should be two historical events in the timeline
|
// There should be two historical events in the timeline
|
||||||
const tiles = await page.locator(".mx_EventTile").all();
|
const tiles = await page.locator(".mx_EventTile").all();
|
||||||
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
|
@ -102,7 +102,7 @@ test.describe("Device verification", () => {
|
||||||
// feed the QR code into the verification request.
|
// feed the QR code into the verification request.
|
||||||
const qrData = await readQrCode(infoDialog);
|
const qrData = await readQrCode(infoDialog);
|
||||||
const verifier = await verificationRequest.evaluateHandle(
|
const verifier = await verificationRequest.evaluateHandle(
|
||||||
(request, qrData) => request.scanQRCode(new Uint8Array(qrData)),
|
(request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)),
|
||||||
[...qrData],
|
[...qrData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Locator } from "@playwright/test";
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import {
|
import {
|
||||||
autoJoin,
|
autoJoin,
|
||||||
|
@ -16,6 +18,8 @@ import {
|
||||||
logOutOfElement,
|
logOutOfElement,
|
||||||
verify,
|
verify,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||||
|
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
|
|
||||||
test.describe("Cryptography", function () {
|
test.describe("Cryptography", function () {
|
||||||
test.use({
|
test.use({
|
||||||
|
@ -276,6 +280,15 @@ test.describe("Cryptography", function () {
|
||||||
bot: bob,
|
bot: bob,
|
||||||
homeserver,
|
homeserver,
|
||||||
}) => {
|
}) => {
|
||||||
|
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
||||||
|
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
||||||
|
// his user info.
|
||||||
|
await app.toggleRoomInfoPanel();
|
||||||
|
const rightPanel = page.locator(".mx_RightPanel");
|
||||||
|
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||||
|
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
|
||||||
|
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
|
||||||
|
|
||||||
// Our app is blocked from syncing while Bob sends his messages.
|
// Our app is blocked from syncing while Bob sends his messages.
|
||||||
await app.client.network.goOffline();
|
await app.client.network.goOffline();
|
||||||
|
|
||||||
|
@ -305,7 +318,50 @@ test.describe("Cryptography", function () {
|
||||||
);
|
);
|
||||||
|
|
||||||
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
||||||
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
await assertNoE2EIcon(penultimate, app);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show correct shields on events sent by users with changed identity", async ({
|
||||||
|
page,
|
||||||
|
app,
|
||||||
|
bot: bob,
|
||||||
|
homeserver,
|
||||||
|
}) => {
|
||||||
|
// Verify Bob
|
||||||
|
await verify(app, bob);
|
||||||
|
|
||||||
|
// Bob logs in a new device and resets cross-signing
|
||||||
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
|
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
|
||||||
|
|
||||||
|
/* should show an error for a message from a previously verified device */
|
||||||
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||||
|
const last = page.locator(".mx_EventTile_last");
|
||||||
|
await expect(last).toContainText("test encrypted from user that was previously verified");
|
||||||
|
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||||
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
|
await lastE2eIcon.focus();
|
||||||
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
|
"Sender's verified identity has changed",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the given message doesn't have an E2E warning icon.
|
||||||
|
*
|
||||||
|
* If it does, throw an error.
|
||||||
|
*/
|
||||||
|
async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
|
||||||
|
// Make sure the message itself exists, before we check if it has any icons
|
||||||
|
await messageLocator.waitFor();
|
||||||
|
|
||||||
|
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
|
||||||
|
if ((await e2eIcon.count()) > 0) {
|
||||||
|
// uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error.
|
||||||
|
await e2eIcon.focus();
|
||||||
|
const tooltip = await app.getTooltipForElement(e2eIcon);
|
||||||
|
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
|
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
|
||||||
import { Client } from "../../pages/client";
|
import { Client } from "../../pages/client";
|
||||||
|
@ -38,6 +39,8 @@ test.describe("User verification", () => {
|
||||||
toasts,
|
toasts,
|
||||||
room: { roomId: dmRoomId },
|
room: { roomId: dmRoomId },
|
||||||
}) => {
|
}) => {
|
||||||
|
await waitForDeviceKeys(page);
|
||||||
|
|
||||||
// once Alice has joined, Bob starts the verification
|
// once Alice has joined, Bob starts the verification
|
||||||
const bobVerificationRequest = await bob.evaluateHandle(
|
const bobVerificationRequest = await bob.evaluateHandle(
|
||||||
async (client, { dmRoomId, aliceCredentials }) => {
|
async (client, { dmRoomId, aliceCredentials }) => {
|
||||||
|
@ -87,6 +90,8 @@ test.describe("User verification", () => {
|
||||||
toasts,
|
toasts,
|
||||||
room: { roomId: dmRoomId },
|
room: { roomId: dmRoomId },
|
||||||
}) => {
|
}) => {
|
||||||
|
await waitForDeviceKeys(page);
|
||||||
|
|
||||||
// once Alice has joined, Bob starts the verification
|
// once Alice has joined, Bob starts the verification
|
||||||
const bobVerificationRequest = await bob.evaluateHandle(
|
const bobVerificationRequest = await bob.evaluateHandle(
|
||||||
async (client, { dmRoomId, aliceCredentials }) => {
|
async (client, { dmRoomId, aliceCredentials }) => {
|
||||||
|
@ -149,3 +154,15 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until we get the other user's device keys.
|
||||||
|
* In newer rust-crypto versions, the verification request will be ignored if we
|
||||||
|
* don't have the sender's device keys.
|
||||||
|
*/
|
||||||
|
async function waitForDeviceKeys(page: Page): Promise<void> {
|
||||||
|
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||||
|
const avatar = await page.getByRole("button", { name: "Avatar" });
|
||||||
|
await avatar.click();
|
||||||
|
await expect(page.getByText("1 session")).toBeVisible();
|
||||||
|
}
|
||||||
|
|
|
@ -66,126 +66,130 @@ test.describe("Editing", () => {
|
||||||
botCreateOpts: { displayName: "Bob" },
|
botCreateOpts: { displayName: "Bob" },
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render and interact with the message edit history dialog", async ({ page, user, app, room }) => {
|
test(
|
||||||
// Click the "Remove" button on the message edit history dialog
|
"should render and interact with the message edit history dialog",
|
||||||
const clickButtonRemove = async (locator: Locator) => {
|
{ tag: "@screenshot" },
|
||||||
const eventTileLine = locator.locator(".mx_EventTile_line");
|
async ({ page, user, app, room }) => {
|
||||||
await eventTileLine.hover();
|
// Click the "Remove" button on the message edit history dialog
|
||||||
await eventTileLine.getByRole("button", { name: "Remove" }).click();
|
const clickButtonRemove = async (locator: Locator) => {
|
||||||
};
|
const eventTileLine = locator.locator(".mx_EventTile_line");
|
||||||
|
await eventTileLine.hover();
|
||||||
|
await eventTileLine.getByRole("button", { name: "Remove" }).click();
|
||||||
|
};
|
||||||
|
|
||||||
await page.goto(`#/room/${room.roomId}`);
|
await page.goto(`#/room/${room.roomId}`);
|
||||||
|
|
||||||
// Send "Message"
|
// Send "Message"
|
||||||
await sendEvent(app, room.roomId);
|
await sendEvent(app, room.roomId);
|
||||||
|
|
||||||
// Edit "Message" to "Massage"
|
// Edit "Message" to "Massage"
|
||||||
await editLastMessage(page, "Massage");
|
await editLastMessage(page, "Massage");
|
||||||
|
|
||||||
// Assert that the edit label is visible
|
// Assert that the edit label is visible
|
||||||
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
|
await expect(page.locator(".mx_EventTile_edited")).toBeVisible();
|
||||||
|
|
||||||
await clickEditedMessage(page, "Massage");
|
await clickEditedMessage(page, "Massage");
|
||||||
|
|
||||||
// Assert that the message edit history dialog is rendered
|
// Assert that the message edit history dialog is rendered
|
||||||
const dialog = page.getByRole("dialog");
|
const dialog = page.getByRole("dialog");
|
||||||
const li = dialog.getByRole("listitem").last();
|
const li = dialog.getByRole("listitem").last();
|
||||||
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
|
// Assert CSS styles which are difficult or cannot be detected with snapshots are applied as expected
|
||||||
await expect(li).toHaveCSS("clear", "both");
|
await expect(li).toHaveCSS("clear", "both");
|
||||||
|
|
||||||
const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp");
|
const timestamp = li.locator(".mx_EventTile .mx_MessageTimestamp");
|
||||||
await expect(timestamp).toHaveCSS("position", "absolute");
|
await expect(timestamp).toHaveCSS("position", "absolute");
|
||||||
await expect(timestamp).toHaveCSS("inset-inline-start", "0px");
|
await expect(timestamp).toHaveCSS("inset-inline-start", "0px");
|
||||||
await expect(timestamp).toHaveCSS("text-align", "center");
|
await expect(timestamp).toHaveCSS("text-align", "center");
|
||||||
|
|
||||||
// Assert that monospace characters can fill the content line as expected
|
// Assert that monospace characters can fill the content line as expected
|
||||||
await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px");
|
await expect(li.locator(".mx_EventTile .mx_EventTile_content")).toHaveCSS("margin-inline-end", "0px");
|
||||||
|
|
||||||
// Assert that zero block start padding is applied to mx_EventTile as expected
|
// Assert that zero block start padding is applied to mx_EventTile as expected
|
||||||
// See: .mx_EventTile on _EventTile.pcss
|
// See: .mx_EventTile on _EventTile.pcss
|
||||||
await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px");
|
await expect(li.locator(".mx_EventTile")).toHaveCSS("padding-block-start", "0px");
|
||||||
|
|
||||||
// Assert that the date separator is rendered at the top
|
// Assert that the date separator is rendered at the top
|
||||||
await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS(
|
await expect(dialog.getByRole("listitem").first().locator("h2", { hasText: "today" })).toHaveCSS(
|
||||||
"text-transform",
|
"text-transform",
|
||||||
"capitalize",
|
"capitalize",
|
||||||
);
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
// Assert that the edited message is rendered under the date separator
|
// Assert that the edited message is rendered under the date separator
|
||||||
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
|
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
|
||||||
// Assert that the edited message body consists of both deleted character and inserted character
|
// Assert that the edited message body consists of both deleted character and inserted character
|
||||||
// Above the first "e" of "Message" was replaced with "a"
|
// Above the first "e" of "Message" was replaced with "a"
|
||||||
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
|
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
|
||||||
|
|
||||||
const body = tile.locator(".mx_EventTile_content .mx_EventTile_body");
|
const body = tile.locator(".mx_EventTile_content .mx_EventTile_body");
|
||||||
await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible();
|
await expect(body.locator(".mx_EditHistoryMessage_deletion").getByText("e")).toBeVisible();
|
||||||
await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible();
|
await expect(body.locator(".mx_EditHistoryMessage_insertion").getByText("a")).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert that the original message is rendered at the bottom
|
// Assert that the original message is rendered at the bottom
|
||||||
await expect(
|
await expect(
|
||||||
dialog
|
dialog
|
||||||
.locator("li:nth-child(3) .mx_EventTile")
|
.locator("li:nth-child(3) .mx_EventTile")
|
||||||
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
|
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Take a snapshot of the dialog
|
// Take a snapshot of the dialog
|
||||||
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
|
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
|
||||||
mask: [page.locator(".mx_MessageTimestamp")],
|
mask: [page.locator(".mx_MessageTimestamp")],
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
|
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
|
||||||
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
|
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
|
||||||
// Click the "Remove" button again
|
// Click the "Remove" button again
|
||||||
await clickButtonRemove(tile);
|
await clickButtonRemove(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
|
// Do nothing and close the dialog to confirm that the message edit history dialog is rendered
|
||||||
await app.closeDialog();
|
await app.closeDialog();
|
||||||
|
|
||||||
{
|
{
|
||||||
// Assert that the message edit history dialog is rendered again after it was closed
|
// Assert that the message edit history dialog is rendered again after it was closed
|
||||||
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
|
const tile = dialog.locator("li:nth-child(2) .mx_EventTile");
|
||||||
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
|
await expect(tile.locator(".mx_EventTile_body")).toHaveText("Meassage");
|
||||||
// Click the "Remove" button again
|
// Click the "Remove" button again
|
||||||
await clickButtonRemove(tile);
|
await clickButtonRemove(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This time remove the message really
|
// This time remove the message really
|
||||||
const textInputDialog = page.locator(".mx_TextInputDialog");
|
const textInputDialog = page.locator(".mx_TextInputDialog");
|
||||||
await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason
|
await textInputDialog.getByRole("textbox", { name: "Reason (optional)" }).fill("This is a test."); // Reason
|
||||||
await textInputDialog.getByRole("button", { name: "Remove" }).click();
|
await textInputDialog.getByRole("button", { name: "Remove" }).click();
|
||||||
|
|
||||||
// Assert that the message edit history dialog is rendered again
|
// Assert that the message edit history dialog is rendered again
|
||||||
const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog");
|
const messageEditHistoryDialog = page.locator(".mx_MessageEditHistoryDialog");
|
||||||
// Assert that the date is rendered
|
// Assert that the date is rendered
|
||||||
await expect(
|
await expect(
|
||||||
messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }),
|
messageEditHistoryDialog.getByRole("listitem").first().locator("h2", { hasText: "today" }),
|
||||||
).toHaveCSS("text-transform", "capitalize");
|
).toHaveCSS("text-transform", "capitalize");
|
||||||
|
|
||||||
// Assert that the original message is rendered under the date on the dialog
|
// Assert that the original message is rendered under the date on the dialog
|
||||||
await expect(
|
await expect(
|
||||||
messageEditHistoryDialog
|
messageEditHistoryDialog
|
||||||
.locator("li:nth-child(2) .mx_EventTile")
|
.locator("li:nth-child(2) .mx_EventTile")
|
||||||
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
|
.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Message" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Assert that the edited message is gone
|
// Assert that the edited message is gone
|
||||||
await expect(
|
await expect(
|
||||||
messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }),
|
messageEditHistoryDialog.locator(".mx_EventTile_content .mx_EventTile_body", { hasText: "Meassage" }),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
await app.closeDialog();
|
await app.closeDialog();
|
||||||
|
|
||||||
// Assert that the redaction placeholder is rendered
|
// Assert that the redaction placeholder is rendered
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.locator(".mx_RoomView_MessageList")
|
.locator(".mx_RoomView_MessageList")
|
||||||
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
|
.locator(".mx_EventTile_last .mx_RedactedBody", { hasText: "Message deleted" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
|
test("should render 'View Source' button in developer mode on the message edit history dialog", async ({
|
||||||
page,
|
page,
|
||||||
|
|
|
@ -25,7 +25,7 @@ test.describe("Image Upload", () => {
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show image preview when uploading an image", async ({ page, app }) => {
|
test("should show image preview when uploading an image", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
await page
|
await page
|
||||||
.locator(".mx_MessageComposer_actions input[type='file']")
|
.locator(".mx_MessageComposer_actions input[type='file']")
|
||||||
.setInputFiles("playwright/sample-files/riot.png");
|
.setInputFiles("playwright/sample-files/riot.png");
|
||||||
|
|
|
@ -26,7 +26,7 @@ test.describe("Forgot Password", () => {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders properly", async ({ page, homeserver }) => {
|
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Sign in" }).click();
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
@ -39,7 +39,7 @@ test.describe("Forgot Password", () => {
|
||||||
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders email verification dialog properly", async ({ page, homeserver }) => {
|
test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||||
const user = await homeserver.registerUser(username, password);
|
const user = await homeserver.registerUser(username, password);
|
||||||
|
|
||||||
await homeserver.setThreepid(user.userId, "email", email);
|
await homeserver.setThreepid(user.userId, "email", email);
|
||||||
|
|
|
@ -19,7 +19,7 @@ test.describe("Invite dialog", function () {
|
||||||
|
|
||||||
const botName = "BotAlice";
|
const botName = "BotAlice";
|
||||||
|
|
||||||
test("should support inviting a user to a room", async ({ page, app, user, bot }) => {
|
test("should support inviting a user to a room", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
// Create and view a room
|
// Create and view a room
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
await app.client.createRoom({ name: "Test Room" });
|
||||||
await app.viewRoomByName("Test Room");
|
await app.viewRoomByName("Test Room");
|
||||||
|
@ -73,52 +73,63 @@ test.describe("Invite dialog", function () {
|
||||||
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
|
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should support inviting a user to Direct Messages", async ({ page, app, user, bot }) => {
|
test(
|
||||||
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
"should support inviting a user to Direct Messages",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user, bot }) => {
|
||||||
|
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
|
||||||
|
|
||||||
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
|
||||||
await expect(other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages")).toBeVisible();
|
await expect(
|
||||||
|
other.locator(".mx_Dialog_header .mx_Dialog_title").getByText("Direct Messages"),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// Assert that the bar is rendered
|
// Assert that the bar is rendered
|
||||||
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
|
await expect(other.locator(".mx_InviteDialog_addressBar")).toBeVisible();
|
||||||
|
|
||||||
// Take a snapshot of the invite dialog
|
// Take a snapshot of the invite dialog
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png");
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-without-user.png");
|
||||||
|
|
||||||
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
||||||
|
|
||||||
await expect(other.locator(".mx_InviteDialog_tile_nameStack").getByText(bot.credentials.userId)).toBeVisible();
|
await expect(
|
||||||
await other.locator(".mx_InviteDialog_tile_nameStack").getByText(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),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// Take a snapshot of the invite dialog with a user pill
|
// Take a snapshot of the invite dialog with a user pill
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
|
||||||
|
|
||||||
// Open a direct message UI
|
// Open a direct message UI
|
||||||
await other.getByRole("button", { name: "Go" }).click();
|
await other.getByRole("button", { name: "Go" }).click();
|
||||||
|
|
||||||
// Assert that the invite dialog disappears
|
// Assert that the invite dialog disappears
|
||||||
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
|
await expect(page.locator(".mx_InviteDialog_other")).not.toBeVisible();
|
||||||
|
|
||||||
// Assert that the hovered user name on invitation UI does not have background color
|
// Assert that the hovered user name on invitation UI does not have background color
|
||||||
// TODO: implement the test on room-header.spec.ts
|
// TODO: implement the test on room-header.spec.ts
|
||||||
const roomHeader = page.locator(".mx_RoomHeader");
|
const roomHeader = page.locator(".mx_RoomHeader");
|
||||||
await roomHeader.locator(".mx_RoomHeader_heading").hover();
|
await roomHeader.locator(".mx_RoomHeader_heading").hover();
|
||||||
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
await expect(roomHeader.locator(".mx_RoomHeader_heading")).toHaveCSS(
|
||||||
|
"background-color",
|
||||||
|
"rgba(0, 0, 0, 0)",
|
||||||
|
);
|
||||||
|
|
||||||
// Send a message to invite the bots
|
// Send a message to invite the bots
|
||||||
const composer = app.getComposer().locator("[contenteditable]");
|
const composer = app.getComposer().locator("[contenteditable]");
|
||||||
await composer.fill("Hello}");
|
await composer.fill("Hello}");
|
||||||
await composer.press("Enter");
|
await composer.press("Enter");
|
||||||
|
|
||||||
// Assert that they were invited and joined
|
// Assert that they were invited and joined
|
||||||
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
|
await expect(page.getByText(`${botName} joined the room`)).toBeVisible();
|
||||||
|
|
||||||
// Assert that the message is displayed at the bottom
|
// Assert that the message is displayed at the bottom
|
||||||
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
|
await expect(page.locator(".mx_EventTile_last").getByText("Hello")).toBeVisible();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -63,7 +63,7 @@ test.describe("Message rendering", () => {
|
||||||
{ direction: "ltr", displayName: "Quentin" },
|
{ direction: "ltr", displayName: "Quentin" },
|
||||||
{ direction: "rtl", displayName: "كوينتين" },
|
{ direction: "rtl", displayName: "كوينتين" },
|
||||||
].forEach(({ direction, displayName }) => {
|
].forEach(({ direction, displayName }) => {
|
||||||
test.describe(`with ${direction} display name`, () => {
|
test.describe(`with ${direction} display name`, { tag: "@screenshot" }, () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName,
|
displayName,
|
||||||
room: async ({ user, app }, use) => {
|
room: async ({ user, app }, use) => {
|
||||||
|
@ -72,14 +72,18 @@ test.describe("Message rendering", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render a basic LTR text message", async ({ page, user, app, room }) => {
|
test(
|
||||||
await page.goto(`#/room/${room.roomId}`);
|
"should render a basic LTR text message",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, user, app, room }) => {
|
||||||
|
await page.goto(`#/room/${room.roomId}`);
|
||||||
|
|
||||||
const msgTile = await sendMessage(page, "Hello, world!");
|
const msgTile = await sendMessage(page, "Hello, world!");
|
||||||
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
|
await expect(msgTile).toMatchScreenshot(`basic-message-ltr-${direction}displayname.png`, {
|
||||||
mask: [page.locator(".mx_MessageTimestamp")],
|
mask: [page.locator(".mx_MessageTimestamp")],
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should render an LTR emote", async ({ page, user, app, room }) => {
|
test("should render an LTR emote", async ({ page, user, app, room }) => {
|
||||||
await page.goto(`#/room/${room.roomId}`);
|
await page.goto(`#/room/${room.roomId}`);
|
||||||
|
|
|
@ -24,7 +24,7 @@ test.describe("permalinks", () => {
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shoud render permalinks as expected", async ({ page, app, user, homeserver }) => {
|
test("shoud render permalinks as expected", { tag: "@screenshot" }, async ({ page, app, user, homeserver }) => {
|
||||||
const bob = new Bot(page, homeserver, { displayName: "Bob" });
|
const bob = new Bot(page, homeserver, { displayName: "Bob" });
|
||||||
const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" });
|
const charlotte = new Bot(page, homeserver, { displayName: "Charlotte" });
|
||||||
await bob.prepareClient();
|
await bob.prepareClient();
|
||||||
|
|
|
@ -129,6 +129,7 @@ export class Helpers {
|
||||||
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
|
const timelineMessage = this.page.locator(".mx_MTextBody", { hasText: message });
|
||||||
await timelineMessage.click({ button: "right" });
|
await timelineMessage.click({ button: "right" });
|
||||||
await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click();
|
await this.page.getByRole("menuitem", { name: "Pin", exact: true }).click();
|
||||||
|
await this.assertMessageInBanner(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,35 +10,38 @@ import { test } from "./index";
|
||||||
import { expect } from "../../element-web-test";
|
import { expect } from "../../element-web-test";
|
||||||
|
|
||||||
test.describe("Pinned messages", () => {
|
test.describe("Pinned messages", () => {
|
||||||
test("should show the empty state when there are no pinned messages", async ({ page, app, room1, util }) => {
|
test(
|
||||||
await util.goTo(room1);
|
"should show the empty state when there are no pinned messages",
|
||||||
await util.openRoomInfo();
|
{ tag: "@screenshot" },
|
||||||
await util.assertPinnedCountInRoomInfo(0);
|
async ({ page, app, room1, util }) => {
|
||||||
await util.openPinnedMessagesList();
|
await util.goTo(room1);
|
||||||
await util.assertEmptyPinnedMessagesList();
|
await util.openRoomInfo();
|
||||||
});
|
await util.assertPinnedCountInRoomInfo(0);
|
||||||
|
await util.openPinnedMessagesList();
|
||||||
|
await util.assertEmptyPinnedMessagesList();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should pin one message and to have the pinned message badge in the timeline", async ({
|
test(
|
||||||
page,
|
"should pin one message and to have the pinned message badge in the timeline",
|
||||||
app,
|
{ tag: "@screenshot" },
|
||||||
room1,
|
async ({ page, app, room1, util }) => {
|
||||||
util,
|
await util.goTo(room1);
|
||||||
}) => {
|
await util.receiveMessages(room1, ["Msg1"]);
|
||||||
await util.goTo(room1);
|
await util.pinMessages(["Msg1"]);
|
||||||
await util.receiveMessages(room1, ["Msg1"]);
|
|
||||||
await util.pinMessages(["Msg1"]);
|
|
||||||
|
|
||||||
const tile = util.getEventTile("Msg1");
|
const tile = util.getEventTile("Msg1");
|
||||||
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
|
await expect(tile).toMatchScreenshot("pinned-message-Msg1.png", {
|
||||||
mask: [tile.locator(".mx_MessageTimestamp")],
|
mask: [tile.locator(".mx_MessageTimestamp")],
|
||||||
// Hide the jump to bottom button in the timeline to avoid flakiness
|
// Hide the jump to bottom button in the timeline to avoid flakiness
|
||||||
css: `
|
css: `
|
||||||
.mx_JumpToBottomButton {
|
.mx_JumpToBottomButton {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
|
test("should pin messages and show them in the room info panel", async ({ page, app, room1, util }) => {
|
||||||
await util.goTo(room1);
|
await util.goTo(room1);
|
||||||
|
@ -73,7 +76,7 @@ test.describe("Pinned messages", () => {
|
||||||
await util.assertPinnedCountInRoomInfo(2);
|
await util.assertPinnedCountInRoomInfo(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should unpin all messages", async ({ page, app, room1, util }) => {
|
test("should unpin all messages", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||||
await util.goTo(room1);
|
await util.goTo(room1);
|
||||||
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||||
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
|
await util.pinMessages(["Msg1", "Msg2", "Msg4"]);
|
||||||
|
@ -98,7 +101,7 @@ test.describe("Pinned messages", () => {
|
||||||
await util.assertPinnedCountInRoomInfo(0);
|
await util.assertPinnedCountInRoomInfo(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should display one message in the banner", async ({ page, app, room1, util }) => {
|
test("should display one message in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||||
await util.goTo(room1);
|
await util.goTo(room1);
|
||||||
await util.receiveMessages(room1, ["Msg1"]);
|
await util.receiveMessages(room1, ["Msg1"]);
|
||||||
await util.pinMessages(["Msg1"]);
|
await util.pinMessages(["Msg1"]);
|
||||||
|
@ -106,7 +109,7 @@ test.describe("Pinned messages", () => {
|
||||||
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png");
|
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-1-Msg1.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should display 2 messages in the banner", async ({ page, app, room1, util }) => {
|
test("should display 2 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||||
await util.goTo(room1);
|
await util.goTo(room1);
|
||||||
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
|
await util.receiveMessages(room1, ["Msg1", "Msg2"]);
|
||||||
await util.pinMessages(["Msg1", "Msg2"]);
|
await util.pinMessages(["Msg1", "Msg2"]);
|
||||||
|
@ -123,7 +126,7 @@ test.describe("Pinned messages", () => {
|
||||||
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png");
|
await expect(util.getBanner()).toMatchScreenshot("pinned-message-banner-2-Msg2.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should display 4 messages in the banner", async ({ page, app, room1, util }) => {
|
test("should display 4 messages in the banner", { tag: "@screenshot" }, async ({ page, app, room1, util }) => {
|
||||||
await util.goTo(room1);
|
await util.goTo(room1);
|
||||||
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
await util.receiveMessages(room1, ["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||||
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);
|
await util.pinMessages(["Msg1", "Msg2", "Msg3", "Msg4"]);
|
||||||
|
|
|
@ -93,7 +93,7 @@ test.describe("Polls", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be creatable and votable", async ({ page, app, bot, user }) => {
|
test("should be creatable and votable", { tag: "@screenshot" }, async ({ page, app, bot, user }) => {
|
||||||
const roomId: string = await app.client.createRoom({});
|
const roomId: string = await app.client.createRoom({});
|
||||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
await page.goto("/#/room/" + roomId);
|
await page.goto("/#/room/" + roomId);
|
||||||
|
@ -219,107 +219,121 @@ test.describe("Polls", () => {
|
||||||
await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
|
await expect(page.locator(".mx_ErrorDialog")).toBeAttached();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be displayed correctly in thread panel", async ({ page, app, user, bot, homeserver }) => {
|
test(
|
||||||
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
|
"should be displayed correctly in thread panel",
|
||||||
await botCharlie.prepareClient();
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user, bot, homeserver }) => {
|
||||||
|
const botCharlie = new Bot(page, homeserver, { displayName: "BotCharlie" });
|
||||||
|
await botCharlie.prepareClient();
|
||||||
|
|
||||||
const roomId: string = await app.client.createRoom({});
|
const roomId: string = await app.client.createRoom({});
|
||||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
await app.client.inviteUser(roomId, botCharlie.credentials.userId);
|
await app.client.inviteUser(roomId, botCharlie.credentials.userId);
|
||||||
await page.goto("/#/room/" + roomId);
|
await page.goto("/#/room/" + roomId);
|
||||||
|
|
||||||
// wait until the bots joined
|
// wait until the bots joined
|
||||||
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({ timeout: 10000 });
|
await expect(page.getByText("BotBob and one other were invited and joined")).toBeAttached({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const locator = await app.openMessageComposerOptions();
|
const locator = await app.openMessageComposerOptions();
|
||||||
await locator.getByRole("menuitem", { name: "Poll" }).click();
|
await locator.getByRole("menuitem", { name: "Poll" }).click();
|
||||||
|
|
||||||
const pollParams = {
|
const pollParams = {
|
||||||
title: "Does the polls feature work?",
|
title: "Does the polls feature work?",
|
||||||
options: ["Yes", "No", "Maybe"],
|
options: ["Yes", "No", "Maybe"],
|
||||||
};
|
};
|
||||||
await createPoll(page, pollParams);
|
await createPoll(page, pollParams);
|
||||||
|
|
||||||
// Wait for message to send, get its ID and save as @pollId
|
// Wait for message to send, get its ID and save as @pollId
|
||||||
const pollId = await page
|
const pollId = await page
|
||||||
.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
|
.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]")
|
||||||
.filter({ hasText: pollParams.title })
|
.filter({ hasText: pollParams.title })
|
||||||
.getAttribute("data-scroll-tokens");
|
.getAttribute("data-scroll-tokens");
|
||||||
|
|
||||||
// Bob starts thread on the poll
|
// Bob starts thread on the poll
|
||||||
await bot.sendMessage(
|
await bot.sendMessage(
|
||||||
roomId,
|
roomId,
|
||||||
{
|
{
|
||||||
body: "Hello there",
|
body: "Hello there",
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
},
|
},
|
||||||
pollId,
|
pollId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// open the thread summary
|
// open the thread summary
|
||||||
await page.getByRole("button", { name: "Open thread" }).click();
|
await page.getByRole("button", { name: "Open thread" }).click();
|
||||||
|
|
||||||
// Bob votes 'Maybe' in the poll
|
// Bob votes 'Maybe' in the poll
|
||||||
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
|
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[2]);
|
||||||
|
|
||||||
// Charlie votes 'No'
|
// Charlie votes 'No'
|
||||||
await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]);
|
await botVoteForOption(page, botCharlie, roomId, pollId, pollParams.options[1]);
|
||||||
|
|
||||||
// no votes shown until I vote, check votes have arrived in main tl
|
// no votes shown until I vote, check votes have arrived in main tl
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
.locator(".mx_RoomView_body .mx_MPollBody_totalVotes")
|
.locator(".mx_RoomView_body .mx_MPollBody_totalVotes")
|
||||||
.getByText("2 votes cast. Vote to see the results"),
|
.getByText("2 votes cast. Vote to see the results"),
|
||||||
).toBeAttached();
|
).toBeAttached();
|
||||||
|
|
||||||
// and thread view
|
// and thread view
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".mx_ThreadView .mx_MPollBody_totalVotes").getByText("2 votes cast. Vote to see the results"),
|
page
|
||||||
).toBeAttached();
|
.locator(".mx_ThreadView .mx_MPollBody_totalVotes")
|
||||||
|
.getByText("2 votes cast. Vote to see the results"),
|
||||||
|
).toBeAttached();
|
||||||
|
|
||||||
// Take snapshots of poll on ThreadView
|
// Take snapshots of poll on ThreadView
|
||||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
|
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']").first()).toBeVisible();
|
||||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_bubble_layout.png", {
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||||
mask: [page.locator(".mx_MessageTimestamp")],
|
"ThreadView_with_a_poll_on_bubble_layout.png",
|
||||||
});
|
{
|
||||||
|
mask: [page.locator(".mx_MessageTimestamp")],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||||
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
|
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='group']").first()).toBeVisible();
|
||||||
|
|
||||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("ThreadView_with_a_poll_on_group_layout.png", {
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||||
mask: [page.locator(".mx_MessageTimestamp")],
|
"ThreadView_with_a_poll_on_group_layout.png",
|
||||||
});
|
{
|
||||||
|
mask: [page.locator(".mx_MessageTimestamp")],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const roomViewLocator = page.locator(".mx_RoomView_body");
|
const roomViewLocator = page.locator(".mx_RoomView_body");
|
||||||
// vote 'Maybe' in the main timeline poll
|
// vote 'Maybe' in the main timeline poll
|
||||||
await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click();
|
await getPollOption(page, pollId, pollParams.options[2], roomViewLocator).click();
|
||||||
// both me and bob have voted Maybe
|
// both me and bob have voted Maybe
|
||||||
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator);
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, roomViewLocator);
|
||||||
|
|
||||||
const threadViewLocator = page.locator(".mx_ThreadView");
|
const threadViewLocator = page.locator(".mx_ThreadView");
|
||||||
// votes updated in thread view too
|
// votes updated in thread view too
|
||||||
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator);
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 2, threadViewLocator);
|
||||||
// change my vote to 'Yes'
|
// change my vote to 'Yes'
|
||||||
await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click();
|
await getPollOption(page, pollId, pollParams.options[0], threadViewLocator).click();
|
||||||
|
|
||||||
// Bob updates vote to 'No'
|
// Bob updates vote to 'No'
|
||||||
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
|
await botVoteForOption(page, bot, roomId, pollId, pollParams.options[1]);
|
||||||
|
|
||||||
// me: yes, bob: no, charlie: no
|
// me: yes, bob: no, charlie: no
|
||||||
const expectVoteCounts = async (optLocator: Locator) => {
|
const expectVoteCounts = async (optLocator: Locator) => {
|
||||||
// I voted yes
|
// I voted yes
|
||||||
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator);
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[0], 1, optLocator);
|
||||||
// Bob and Charlie voted no
|
// Bob and Charlie voted no
|
||||||
await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator);
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[1], 2, optLocator);
|
||||||
// 0 for maybe
|
// 0 for maybe
|
||||||
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator);
|
await expectPollOptionVoteCount(page, pollId, pollParams.options[2], 0, optLocator);
|
||||||
};
|
};
|
||||||
|
|
||||||
// check counts are correct in main timeline tile
|
// check counts are correct in main timeline tile
|
||||||
await expectVoteCounts(page.locator(".mx_RoomView_body"));
|
await expectVoteCounts(page.locator(".mx_RoomView_body"));
|
||||||
|
|
||||||
// and in thread view tile
|
// and in thread view tile
|
||||||
await expectVoteCounts(page.locator(".mx_ThreadView"));
|
await expectVoteCounts(page.locator(".mx_ThreadView"));
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("editing messages", () => {
|
test.describe("editing messages", () => {
|
||||||
test.describe("in threads", () => {
|
test.describe("in threads", () => {
|
||||||
test("An edit of a threaded message makes the room unread", async ({
|
test("An edit of a threaded message makes the room unread", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("editing messages", () => {
|
test.describe("editing messages", () => {
|
||||||
test.describe("in the main timeline", () => {
|
test.describe("in the main timeline", () => {
|
||||||
test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
|
test("Editing a message leaves a room read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("editing messages", () => {
|
test.describe("editing messages", () => {
|
||||||
test.describe("thread roots", () => {
|
test.describe("thread roots", () => {
|
||||||
test("An edit of a thread root leaves the room read", async ({
|
test("An edit of a thread root leaves the room read", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { customEvent, many, test } from ".";
|
import { customEvent, many, test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("Ignored events", () => {
|
test.describe("Ignored events", () => {
|
||||||
test("If all events after receipt are unimportant, the room is read", async ({
|
test("If all events after receipt are unimportant, the room is read", async ({
|
||||||
roomAlpha: room1,
|
roomAlpha: room1,
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("Message ordering", () => {
|
test.describe("Message ordering", () => {
|
||||||
test.describe("in the main timeline", () => {
|
test.describe("in the main timeline", () => {
|
||||||
test.fixme(
|
test.fixme(
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("messages with missing referents", () => {
|
test.describe("messages with missing referents", () => {
|
||||||
test.fixme(
|
test.fixme(
|
||||||
"A message in an unknown thread is not visible and the room is read",
|
"A message in an unknown thread is not visible and the room is read",
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { many, test } from ".";
|
import { many, test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("new messages", () => {
|
test.describe("new messages", () => {
|
||||||
test.describe("in threads", () => {
|
test.describe("in threads", () => {
|
||||||
test("Receiving a message makes a room unread", async ({
|
test("Receiving a message makes a room unread", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { many, test } from ".";
|
import { many, test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("new messages", () => {
|
test.describe("new messages", () => {
|
||||||
test.describe("in the main timeline", () => {
|
test.describe("in the main timeline", () => {
|
||||||
test("Receiving a message makes a room unread", async ({
|
test("Receiving a message makes a room unread", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { many, test } from ".";
|
import { many, test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("new messages", () => {
|
test.describe("new messages", () => {
|
||||||
test.describe("thread roots", () => {
|
test.describe("thread roots", () => {
|
||||||
test("Reading a thread root does not mark the thread as read", async ({
|
test("Reading a thread root does not mark the thread as read", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("Notifications", () => {
|
test.describe("Notifications", () => {
|
||||||
test.describe("in the main timeline", () => {
|
test.describe("in the main timeline", () => {
|
||||||
test.fixme("A new message that mentions me shows a notification", () => {});
|
test.fixme("A new message that mentions me shows a notification", () => {});
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test, expect } from ".";
|
import { test, expect } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("reactions", () => {
|
test.describe("reactions", () => {
|
||||||
test.describe("in threads", () => {
|
test.describe("in threads", () => {
|
||||||
test("A reaction to a threaded message does not make the room unread", async ({
|
test("A reaction to a threaded message does not make the room unread", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("reactions", () => {
|
test.describe("reactions", () => {
|
||||||
test.describe("in the main timeline", () => {
|
test.describe("in the main timeline", () => {
|
||||||
test("Receiving a reaction to a message does not make a room unread", async ({
|
test("Receiving a reaction to a message does not make a room unread", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("reactions", () => {
|
test.describe("reactions", () => {
|
||||||
test.describe("thread roots", () => {
|
test.describe("thread roots", () => {
|
||||||
test("A reaction to a thread root does not make the room unread", async ({
|
test("A reaction to a thread root does not make the room unread", async ({
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Mae",
|
displayName: "Mae",
|
||||||
botCreateOpts: { displayName: "Other User" },
|
botCreateOpts: { displayName: "Other User" },
|
||||||
|
|
|
@ -2,19 +2,19 @@
|
||||||
|
|
||||||
Tips for writing these tests:
|
Tips for writing these tests:
|
||||||
|
|
||||||
- Break up your tests into the smallest test case possible. The purpose of
|
- Break up your tests into the smallest test case possible. The purpose of
|
||||||
these tests is to understand hard-to-find bugs, so small tests are necessary.
|
these tests is to understand hard-to-find bugs, so small tests are necessary.
|
||||||
We know that Playwright recommends combining tests together for performance, but
|
We know that Playwright recommends combining tests together for performance, but
|
||||||
that will frustrate our goals here. (We will need to find a different way to
|
that will frustrate our goals here. (We will need to find a different way to
|
||||||
reduce CI time.)
|
reduce CI time.)
|
||||||
|
|
||||||
- Try to assert something after every action, to make sure it has completed.
|
- Try to assert something after every action, to make sure it has completed.
|
||||||
E.g.:
|
E.g.:
|
||||||
markAsRead(room2);
|
markAsRead(room2);
|
||||||
assertRead(room2);
|
assertRead(room2);
|
||||||
You should especially follow this rule if you are jumping to a different
|
You should especially follow this rule if you are jumping to a different
|
||||||
room or similar straight afterward.
|
room or similar straight afterward.
|
||||||
|
|
||||||
- Use assertStillRead() if you are asserting something is read when it was
|
- Use assertStillRead() if you are asserting something is read when it was
|
||||||
also read before. This waits a little while to make sure you're not getting a
|
also read before. This waits a little while to make sure you're not getting a
|
||||||
false positive.
|
false positive.
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("redactions", () => {
|
test.describe("redactions", () => {
|
||||||
test.describe("in threads", () => {
|
test.describe("in threads", () => {
|
||||||
test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({
|
test("Redacting the threaded message pointed to by my receipt leaves the room read", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("redactions", () => {
|
test.describe("redactions", () => {
|
||||||
test.describe("in the main timeline", () => {
|
test.describe("in the main timeline", () => {
|
||||||
test("Redacting the message pointed to by my receipt leaves the room read", async ({
|
test("Redacting the message pointed to by my receipt leaves the room read", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||||
test.describe("redactions", () => {
|
test.describe("redactions", () => {
|
||||||
test.describe("thread roots", () => {
|
test.describe("thread roots", () => {
|
||||||
test("Redacting a thread root after it was read leaves the room read", async ({
|
test("Redacting a thread root after it was read leaves the room read", async ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ Please see LICENSE files in the repository root for full details.
|
||||||
|
|
||||||
import { test } from ".";
|
import { test } from ".";
|
||||||
|
|
||||||
test.describe("Read receipts", () => {
|
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 if 'unread first' is selected", async ({
|
test("Rooms with unread messages appear at the top of room list if 'unread first' is selected", async ({
|
||||||
roomAlpha: room1,
|
roomAlpha: room1,
|
||||||
|
|
|
@ -38,34 +38,33 @@ test.describe("Email Registration", async () => {
|
||||||
await page.goto("/#/register");
|
await page.goto("/#/register");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("registers an account and lands on the use case selection screen", async ({
|
test(
|
||||||
page,
|
"registers an account and lands on the use case selection screen",
|
||||||
mailhog,
|
{ tag: "@screenshot" },
|
||||||
request,
|
async ({ page, mailhog, request, checkA11y }) => {
|
||||||
checkA11y,
|
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||||
}) => {
|
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
||||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
|
||||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
|
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username" }).fill("alice");
|
await page.getByRole("textbox", { name: "Username" }).fill("alice");
|
||||||
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
|
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
|
||||||
await page.getByPlaceholder("Confirm password").fill("totally a great password");
|
await page.getByPlaceholder("Confirm password").fill("totally a great password");
|
||||||
await page.getByPlaceholder("Email").fill("alice@email.com");
|
await page.getByPlaceholder("Email").fill("alice@email.com");
|
||||||
await page.getByRole("button", { name: "Register" }).click();
|
await page.getByRole("button", { name: "Register" }).click();
|
||||||
|
|
||||||
await expect(page.getByText("Check your email to continue")).toBeVisible();
|
await expect(page.getByText("Check your email to continue")).toBeVisible();
|
||||||
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions);
|
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions);
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
|
|
||||||
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();
|
||||||
|
|
||||||
const messages = await mailhog.api.messages();
|
const messages = await mailhog.api.messages();
|
||||||
expect(messages.items).toHaveLength(1);
|
expect(messages.items).toHaveLength(1);
|
||||||
expect(messages.items[0].to).toEqual("alice@email.com");
|
expect(messages.items[0].to).toEqual("alice@email.com");
|
||||||
const [emailLink] = messages.items[0].text.match(/http.+/);
|
const [emailLink] = messages.items[0].text.match(/http.+/);
|
||||||
await request.get(emailLink); // "Click" the link in the email
|
await request.get(emailLink); // "Click" the link in the email
|
||||||
|
|
||||||
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,66 +15,73 @@ test.describe("Registration", () => {
|
||||||
await page.goto("/#/register");
|
await page.goto("/#/register");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("registers an account and lands on the home screen", async ({ homeserver, page, checkA11y, crypto }) => {
|
test(
|
||||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
"registers an account and lands on the home screen",
|
||||||
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
|
{ tag: "@screenshot" },
|
||||||
|
async ({ homeserver, page, checkA11y, crypto }) => {
|
||||||
|
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||||
|
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
|
||||||
|
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
// wait for the dialog to go away
|
// wait for the dialog to go away
|
||||||
await expect(page.getByRole("dialog")).not.toBeVisible();
|
await expect(page.getByRole("dialog")).not.toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
|
await expect(page.getByRole("textbox", { name: "Username", exact: true })).toBeVisible();
|
||||||
// Hide the server text as it contains the randomly allocated Homeserver port
|
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||||
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")], includeDialogBackground: true };
|
const screenshotOptions = {
|
||||||
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
|
mask: [page.locator(".mx_ServerPicker_server")],
|
||||||
await checkA11y();
|
includeDialogBackground: true,
|
||||||
|
};
|
||||||
|
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
|
||||||
|
await checkA11y();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
|
await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
|
||||||
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
|
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
|
||||||
await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password");
|
await page.getByPlaceholder("Confirm password", { exact: true }).fill("totally a great password");
|
||||||
await page.getByRole("button", { name: "Register", exact: true }).click();
|
await page.getByRole("button", { name: "Register", exact: true }).click();
|
||||||
|
|
||||||
const dialog = page.getByRole("dialog");
|
const dialog = page.getByRole("dialog");
|
||||||
await expect(dialog).toBeVisible();
|
await expect(dialog).toBeVisible();
|
||||||
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
|
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
|
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
|
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
|
||||||
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
|
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
|
|
||||||
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
|
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
|
||||||
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link
|
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link
|
||||||
await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible();
|
await expect(termsPolicy.getByLabel("Privacy Policy")).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Accept", exact: true }).click();
|
await page.getByRole("button", { name: "Accept", exact: true }).click();
|
||||||
|
|
||||||
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
await expect(page.locator(".mx_UseCaseSelection_skip")).toBeVisible();
|
||||||
await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions);
|
await expect(page).toMatchScreenshot("use-case-selection.png", screenshotOptions);
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
await page.getByRole("button", { name: "Skip", exact: true }).click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/#\/home$/);
|
await expect(page).toHaveURL(/\/#\/home$/);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Cross-signing checks
|
* Cross-signing checks
|
||||||
*/
|
*/
|
||||||
// check that the device considers itself verified
|
// check that the device considers itself verified
|
||||||
await page.getByRole("button", { name: "User menu", exact: true }).click();
|
await page.getByRole("button", { name: "User menu", exact: true }).click();
|
||||||
await page.getByRole("menuitem", { name: "All settings", exact: true }).click();
|
await page.getByRole("menuitem", { name: "All settings", exact: true }).click();
|
||||||
await page.getByRole("tab", { name: "Sessions", exact: true }).click();
|
await page.getByRole("tab", { name: "Sessions", exact: true }).click();
|
||||||
await expect(page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified")).toHaveText(
|
await expect(
|
||||||
"Verified",
|
page.getByTestId("current-session-section").getByTestId("device-metadata-isVerified"),
|
||||||
);
|
).toHaveText("Verified");
|
||||||
|
|
||||||
// check that cross-signing keys have been uploaded.
|
// check that cross-signing keys have been uploaded.
|
||||||
await crypto.assertDeviceIsCrossSigned();
|
await crypto.assertDeviceIsCrossSigned();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
|
test("should require username to fulfil requirements and be available", async ({ homeserver, page }) => {
|
||||||
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
await page.getByRole("button", { name: "Edit", exact: true }).click();
|
||||||
|
|
|
@ -18,7 +18,7 @@ test.describe("Release announcement", () => {
|
||||||
labsFlags: ["threadsActivityCentre"],
|
labsFlags: ["threadsActivityCentre"],
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should display the release announcement process", async ({ page, app, util }) => {
|
test("should display the release announcement process", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||||
// The TAC release announcement should be displayed
|
// The TAC release announcement should be displayed
|
||||||
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre");
|
await util.assertReleaseAnnouncementIsVisible("Threads Activity Centre");
|
||||||
// Hide the release announcement
|
// Hide the release announcement
|
||||||
|
|
|
@ -40,7 +40,7 @@ test.describe("FilePanel", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("render", () => {
|
test.describe("render", () => {
|
||||||
test("should render empty state", async ({ page }) => {
|
test("should render empty state", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
// Wait until the information about the empty state is rendered
|
// Wait until the information about the empty state is rendered
|
||||||
await expect(page.locator(".mx_EmptyState")).toBeVisible();
|
await expect(page.locator(".mx_EmptyState")).toBeVisible();
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ test.describe("FilePanel", () => {
|
||||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("empty.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should list tiles on the panel", async ({ page }) => {
|
test("should list tiles on the panel", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
// Upload multiple files
|
// Upload multiple files
|
||||||
await uploadFile(page, "playwright/sample-files/riot.png"); // Image
|
await uploadFile(page, "playwright/sample-files/riot.png"); // Image
|
||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio
|
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio
|
||||||
|
|
|
@ -21,7 +21,7 @@ test.describe("NotificationPanel", () => {
|
||||||
await app.client.createRoom({ name: ROOM_NAME });
|
await app.client.createRoom({ name: ROOM_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render empty state", async ({ page, app }) => {
|
test("should render empty state", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
await app.viewRoomByName(ROOM_NAME);
|
await app.viewRoomByName(ROOM_NAME);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Notifications" }).click();
|
await page.getByRole("button", { name: "Notifications" }).click();
|
||||||
|
|
|
@ -38,7 +38,7 @@ test.describe("RightPanel", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("in rooms", () => {
|
test.describe("in rooms", () => {
|
||||||
test("should handle long room address and long room name", async ({ page, app }) => {
|
test("should handle long room address and long room name", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
await app.client.createRoom({ name: ROOM_NAME_LONG });
|
await app.client.createRoom({ name: ROOM_NAME_LONG });
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);
|
await viewRoomSummaryByName(page, app, ROOM_NAME_LONG);
|
||||||
|
|
||||||
|
|
|
@ -47,34 +47,40 @@ test.describe("Room Directory", () => {
|
||||||
expect(resp.chunk[0].room_id).toEqual(roomId);
|
expect(resp.chunk[0].room_id).toEqual(roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should allow finding published rooms in directory", async ({ page, app, user, bot }) => {
|
test(
|
||||||
const name = "This is a public room";
|
"should allow finding published rooms in directory",
|
||||||
await bot.createRoom({
|
{ tag: "@screenshot" },
|
||||||
visibility: "public" as Visibility,
|
async ({ page, app, user, bot }) => {
|
||||||
name,
|
const name = "This is a public room";
|
||||||
room_alias_name: "test1234",
|
await bot.createRoom({
|
||||||
});
|
visibility: "public" as Visibility,
|
||||||
|
name,
|
||||||
|
room_alias_name: "test1234",
|
||||||
|
});
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Explore rooms" }).click();
|
await page.getByRole("button", { name: "Explore rooms" }).click();
|
||||||
|
|
||||||
const dialog = page.locator(".mx_SpotlightDialog");
|
const dialog = page.locator(".mx_SpotlightDialog");
|
||||||
await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room");
|
await dialog.getByRole("textbox", { name: "Search" }).fill("Unknown Room");
|
||||||
await expect(
|
await expect(
|
||||||
dialog.getByText("If you can't find the room you're looking for, ask for an invite or create a new room."),
|
dialog.getByText(
|
||||||
).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText");
|
"If you can't find the room you're looking for, ask for an invite or create a new room.",
|
||||||
|
),
|
||||||
|
).toHaveClass("mx_SpotlightDialog_otherSearches_messageSearchText");
|
||||||
|
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png");
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-no-results.png");
|
||||||
|
|
||||||
await dialog.getByRole("textbox", { name: "Search" }).fill("test1234");
|
await dialog.getByRole("textbox", { name: "Search" }).fill("test1234");
|
||||||
await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName");
|
await expect(dialog.getByText(name)).toHaveClass("mx_SpotlightDialog_result_publicRoomName");
|
||||||
|
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png");
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("filtered-one-result.png");
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator(".mx_SpotlightDialog .mx_SpotlightDialog_option")
|
.locator(".mx_SpotlightDialog .mx_SpotlightDialog_option")
|
||||||
.getByRole("button", { name: "Join" })
|
.getByRole("button", { name: "Join" })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(page).toHaveURL("/#/room/#test1234:localhost");
|
await expect(page).toHaveURL("/#/room/#test1234:localhost");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ test.describe("Room Header", () => {
|
||||||
test.use({
|
test.use({
|
||||||
labsFlags: ["feature_notifications"],
|
labsFlags: ["feature_notifications"],
|
||||||
});
|
});
|
||||||
test("should render default buttons properly", async ({ page, app, user }) => {
|
test("should render default buttons properly", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
await app.client.createRoom({ name: "Test Room" });
|
await app.client.createRoom({ name: "Test Room" });
|
||||||
await app.viewRoomByName("Test Room");
|
await app.viewRoomByName("Test Room");
|
||||||
|
|
||||||
|
@ -51,34 +51,38 @@ test.describe("Room Header", () => {
|
||||||
await expect(header).toMatchScreenshot("room-header.png");
|
await expect(header).toMatchScreenshot("room-header.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render a very long room name without collapsing the buttons", async ({ page, app, user }) => {
|
test(
|
||||||
const LONG_ROOM_NAME =
|
"should render a very long room name without collapsing the buttons",
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
|
{ tag: "@screenshot" },
|
||||||
"et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
|
async ({ page, app, user }) => {
|
||||||
"aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
|
const LONG_ROOM_NAME =
|
||||||
"dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
|
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore " +
|
||||||
"officia deserunt mollit anim id est laborum.";
|
"et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut " +
|
||||||
|
"aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum " +
|
||||||
|
"dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
|
||||||
|
"officia deserunt mollit anim id est laborum.";
|
||||||
|
|
||||||
await app.client.createRoom({ name: LONG_ROOM_NAME });
|
await app.client.createRoom({ name: LONG_ROOM_NAME });
|
||||||
await app.viewRoomByName(LONG_ROOM_NAME);
|
await app.viewRoomByName(LONG_ROOM_NAME);
|
||||||
|
|
||||||
const header = page.locator(".mx_RoomHeader");
|
const header = page.locator(".mx_RoomHeader");
|
||||||
// Wait until the room name is set
|
// Wait until the room name is set
|
||||||
await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible();
|
await expect(page.locator(".mx_RoomHeader_heading").getByText(LONG_ROOM_NAME)).toBeVisible();
|
||||||
|
|
||||||
// Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
|
// Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
|
||||||
// Note these assertions do not check the size of mx_LegacyRoomHeader_name button
|
// Note these assertions do not check the size of mx_LegacyRoomHeader_name button
|
||||||
const buttons = header.locator(".mx_Flex").getByRole("button");
|
const buttons = header.locator(".mx_Flex").getByRole("button");
|
||||||
await expect(buttons).toHaveCount(5);
|
await expect(buttons).toHaveCount(5);
|
||||||
|
|
||||||
for (const button of await buttons.all()) {
|
for (const button of await buttons.all()) {
|
||||||
await expect(button).toBeVisible();
|
await expect(button).toBeVisible();
|
||||||
await expect(button).toHaveCSS("height", "32px");
|
await expect(button).toHaveCSS("height", "32px");
|
||||||
await expect(button).toHaveCSS("width", "32px");
|
await expect(button).toHaveCSS("width", "32px");
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(header).toMatchScreenshot("room-header-long-name.png");
|
await expect(header).toMatchScreenshot("room-header-long-name.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("with a video room", () => {
|
test.describe("with a video room", () => {
|
||||||
|
@ -99,30 +103,34 @@ test.describe("Room Header", () => {
|
||||||
test.describe("and with feature_notifications enabled", () => {
|
test.describe("and with feature_notifications enabled", () => {
|
||||||
test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
|
test.use({ labsFlags: ["feature_video_rooms", "feature_notifications"] });
|
||||||
|
|
||||||
test("should render buttons for chat, room info, threads and facepile", async ({ page, app, user }) => {
|
test(
|
||||||
await createVideoRoom(page, app);
|
"should render buttons for chat, room info, threads and facepile",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
await createVideoRoom(page, app);
|
||||||
|
|
||||||
const header = page.locator(".mx_RoomHeader");
|
const header = page.locator(".mx_RoomHeader");
|
||||||
|
|
||||||
// There's two room info button - the header itself and the i button
|
// There's two room info button - the header itself and the i button
|
||||||
const infoButtons = header.getByRole("button", { name: "Room info" });
|
const infoButtons = header.getByRole("button", { name: "Room info" });
|
||||||
await expect(infoButtons).toHaveCount(2);
|
await expect(infoButtons).toHaveCount(2);
|
||||||
await expect(infoButtons.first()).toBeVisible();
|
await expect(infoButtons.first()).toBeVisible();
|
||||||
await expect(infoButtons.last()).toBeVisible();
|
await expect(infoButtons.last()).toBeVisible();
|
||||||
|
|
||||||
// Facepile
|
// Facepile
|
||||||
await expect(header.locator(".mx_FacePile")).toBeVisible();
|
await expect(header.locator(".mx_FacePile")).toBeVisible();
|
||||||
|
|
||||||
// Chat, Threads and Notification buttons
|
// Chat, Threads and Notification buttons
|
||||||
await expect(header.getByRole("button", { name: "Chat" })).toBeVisible();
|
await expect(header.getByRole("button", { name: "Chat" })).toBeVisible();
|
||||||
await expect(header.getByRole("button", { name: "Threads" })).toBeVisible();
|
await expect(header.getByRole("button", { name: "Threads" })).toBeVisible();
|
||||||
await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible();
|
await expect(header.getByRole("button", { name: "Notifications" })).toBeVisible();
|
||||||
|
|
||||||
// Assert that there is not a button except those buttons
|
// Assert that there is not a button except those buttons
|
||||||
await expect(header.getByRole("button")).toHaveCount(7);
|
await expect(header.getByRole("button")).toHaveCount(7);
|
||||||
|
|
||||||
await expect(header).toMatchScreenshot("room-header-video-room.png");
|
await expect(header).toMatchScreenshot("room-header-video-room.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render a working chat button which opens the timeline on a right panel", async ({
|
test("should render a working chat button which opens the timeline on a right panel", async ({
|
||||||
|
|
|
@ -23,7 +23,7 @@ test.describe("Account user settings tab", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered properly", async ({ uut, user }) => {
|
test("should be rendered properly", { tag: "@screenshot" }, async ({ uut, user }) => {
|
||||||
await expect(uut).toMatchScreenshot("account.png");
|
await expect(uut).toMatchScreenshot("account.png");
|
||||||
|
|
||||||
// Assert that the top heading is rendered
|
// Assert that the top heading is rendered
|
||||||
|
@ -71,7 +71,7 @@ test.describe("Account user settings tab", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should respond to small screen sizes", async ({ page, uut }) => {
|
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page, uut }) => {
|
||||||
await page.setViewportSize({ width: 700, height: 600 });
|
await page.setViewportSize({ width: 700, height: 600 });
|
||||||
await expect(uut).toMatchScreenshot("account-smallscreen.png");
|
await expect(uut).toMatchScreenshot("account-smallscreen.png");
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ test.describe("Appearance user settings tab", () => {
|
||||||
displayName: "Hanako",
|
displayName: "Hanako",
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered properly", async ({ page, user, app }) => {
|
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
||||||
const tab = await app.settings.openUserSettings("Appearance");
|
const tab = await app.settings.openUserSettings("Appearance");
|
||||||
|
|
||||||
// Click "Show advanced" link button
|
// Click "Show advanced" link button
|
||||||
|
@ -25,19 +25,23 @@ test.describe("Appearance user settings tab", () => {
|
||||||
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
await expect(tab).toMatchScreenshot("appearance-tab.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should support changing font size by using the font size dropdown", async ({ page, app, user }) => {
|
test(
|
||||||
await app.settings.openUserSettings("Appearance");
|
"should support changing font size by using the font size dropdown",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
await app.settings.openUserSettings("Appearance");
|
||||||
|
|
||||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||||
const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
|
const fontDropdown = tab.locator(".mx_FontScalingPanel_Dropdown");
|
||||||
await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
|
await expect(fontDropdown.getByLabel("Font size")).toBeVisible();
|
||||||
|
|
||||||
// Default browser font size is 16px and the select value is 0
|
// Default browser font size is 16px and the select value is 0
|
||||||
// -4 value is 12px
|
// -4 value is 12px
|
||||||
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
|
await fontDropdown.getByLabel("Font size").selectOption({ value: "-4" });
|
||||||
|
|
||||||
await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true });
|
await expect(page).toMatchScreenshot("window-12px.png", { includeDialogBackground: true });
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should support enabling system font", async ({ page, app, user }) => {
|
test("should support enabling system font", async ({ page, app, user }) => {
|
||||||
await app.settings.openUserSettings("Appearance");
|
await app.settings.openUserSettings("Appearance");
|
||||||
|
|
|
@ -20,20 +20,24 @@ test.describe("Appearance user settings tab", () => {
|
||||||
await util.openAppearanceTab();
|
await util.openAppearanceTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should change the message layout from modern to bubble", async ({ page, app, user, util }) => {
|
test(
|
||||||
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png");
|
"should change the message layout from modern to bubble",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user, util }) => {
|
||||||
|
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-modern.png");
|
||||||
|
|
||||||
await util.getBubbleLayout().click();
|
await util.getBubbleLayout().click();
|
||||||
|
|
||||||
// Assert that modern are irc layout are not selected
|
// Assert that modern are irc layout are not selected
|
||||||
await expect(util.getBubbleLayout()).toBeChecked();
|
await expect(util.getBubbleLayout()).toBeChecked();
|
||||||
await expect(util.getModernLayout()).not.toBeChecked();
|
await expect(util.getModernLayout()).not.toBeChecked();
|
||||||
await expect(util.getIRCLayout()).not.toBeChecked();
|
await expect(util.getIRCLayout()).not.toBeChecked();
|
||||||
|
|
||||||
// Assert that the room layout is set to bubble layout
|
// Assert that the room layout is set to bubble layout
|
||||||
await util.assertBubbleLayout();
|
await util.assertBubbleLayout();
|
||||||
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png");
|
await util.assertScreenshot(util.getMessageLayoutPanel(), "message-layout-panel-bubble.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => {
|
test("should enable compact layout when the modern layout is selected", async ({ page, app, user, util }) => {
|
||||||
await expect(util.getCompactLayoutCheckbox()).not.toBeChecked();
|
await expect(util.getCompactLayoutCheckbox()).not.toBeChecked();
|
||||||
|
|
|
@ -20,31 +20,39 @@ test.describe("Appearance user settings tab", () => {
|
||||||
await util.openAppearanceTab();
|
await util.openAppearanceTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered with the light theme selected", async ({ page, app, util }) => {
|
test(
|
||||||
// Assert that 'Match system theme' is not checked
|
"should be rendered with the light theme selected",
|
||||||
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked();
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, util }) => {
|
||||||
|
// Assert that 'Match system theme' is not checked
|
||||||
|
await expect(util.getMatchSystemThemeCheckbox()).not.toBeChecked();
|
||||||
|
|
||||||
// Assert that the light theme is selected
|
// Assert that the light theme is selected
|
||||||
await expect(util.getLightTheme()).toBeChecked();
|
await expect(util.getLightTheme()).toBeChecked();
|
||||||
// Assert that the dark and high contrast themes are not selected
|
// Assert that the dark and high contrast themes are not selected
|
||||||
await expect(util.getDarkTheme()).not.toBeChecked();
|
await expect(util.getDarkTheme()).not.toBeChecked();
|
||||||
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
await expect(util.getHighContrastTheme()).not.toBeChecked();
|
||||||
|
|
||||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-light.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should disable the themes when the system theme is clicked", async ({ page, app, util }) => {
|
test(
|
||||||
await util.getMatchSystemThemeCheckbox().click();
|
"should disable the themes when the system theme is clicked",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, util }) => {
|
||||||
|
await util.getMatchSystemThemeCheckbox().click();
|
||||||
|
|
||||||
// Assert that the themes are disabled
|
// Assert that the themes are disabled
|
||||||
await expect(util.getLightTheme()).toBeDisabled();
|
await expect(util.getLightTheme()).toBeDisabled();
|
||||||
await expect(util.getDarkTheme()).toBeDisabled();
|
await expect(util.getDarkTheme()).toBeDisabled();
|
||||||
await expect(util.getHighContrastTheme()).toBeDisabled();
|
await expect(util.getHighContrastTheme()).toBeDisabled();
|
||||||
|
|
||||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-match-system-enabled.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should change the theme to dark", async ({ page, app, util }) => {
|
test("should change the theme to dark", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||||
// Assert that the light theme is selected
|
// Assert that the light theme is selected
|
||||||
await expect(util.getLightTheme()).toBeChecked();
|
await expect(util.getLightTheme()).toBeChecked();
|
||||||
|
|
||||||
|
@ -63,19 +71,23 @@ test.describe("Appearance user settings tab", () => {
|
||||||
labsFlags: ["feature_custom_themes"],
|
labsFlags: ["feature_custom_themes"],
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render the custom theme section", async ({ page, app, util }) => {
|
test("should render the custom theme section", { tag: "@screenshot" }, async ({ page, app, util }) => {
|
||||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to add and remove a custom theme", async ({ page, app, util }) => {
|
test(
|
||||||
await util.addCustomTheme();
|
"should be able to add and remove a custom theme",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, util }) => {
|
||||||
|
await util.addCustomTheme();
|
||||||
|
|
||||||
await expect(util.getCustomTheme()).not.toBeChecked();
|
await expect(util.getCustomTheme()).not.toBeChecked();
|
||||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png");
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-added.png");
|
||||||
|
|
||||||
await util.removeCustomTheme();
|
await util.removeCustomTheme();
|
||||||
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png");
|
await expect(util.getThemePanel()).toMatchScreenshot("theme-panel-custom-theme-removed.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ test.describe("General room settings tab", () => {
|
||||||
await app.viewRoomByName(roomName);
|
await app.viewRoomByName(roomName);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered properly", async ({ page, app }) => {
|
test("should be rendered properly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
const settings = await app.settings.openRoomSettings("General");
|
const settings = await app.settings.openRoomSettings("General");
|
||||||
|
|
||||||
// Assert that "Show less" details element is rendered
|
// Assert that "Show less" details element is rendered
|
||||||
|
|
|
@ -23,7 +23,7 @@ test.describe("Preferences user settings tab", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be rendered properly", async ({ app, page, user }) => {
|
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||||
page.setViewportSize({ width: 1024, height: 3300 });
|
page.setViewportSize({ width: 1024, height: 3300 });
|
||||||
const tab = await app.settings.openUserSettings("Preferences");
|
const tab = await app.settings.openUserSettings("Preferences");
|
||||||
// Assert that the top heading is rendered
|
// Assert that the top heading is rendered
|
||||||
|
|
|
@ -36,7 +36,7 @@ test.describe("Security user settings tab", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("AnalyticsLearnMoreDialog", () => {
|
test.describe("AnalyticsLearnMoreDialog", () => {
|
||||||
test("should be rendered properly", async ({ app, page }) => {
|
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => {
|
||||||
const tab = await app.settings.openUserSettings("Security");
|
const tab = await app.settings.openUserSettings("Security");
|
||||||
await tab.getByRole("button", { name: "Learn more" }).click();
|
await tab.getByRole("button", { name: "Learn more" }).click();
|
||||||
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
|
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
|
||||||
|
|
67
playwright/e2e/share-dialog/share-dialog.spec.ts
Normal file
67
playwright/e2e/share-dialog/share-dialog.spec.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Share dialog", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
room: async ({ app, user, bot }, use) => {
|
||||||
|
const roomId = await app.client.createRoom({ name: "Alice room" });
|
||||||
|
await use({ roomId });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should share a room", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await app.toggleRoomInfoPanel();
|
||||||
|
await page.getByRole("menuitem", { name: "Copy link" }).click();
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Share room" });
|
||||||
|
await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible();
|
||||||
|
expect(dialog).toMatchScreenshot("share-dialog-room.png", {
|
||||||
|
// QRCode and url changes at every run
|
||||||
|
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should share a room member", { tag: "@screenshot" }, async ({ page, app, room, user }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
|
||||||
|
|
||||||
|
const rightPanel = await app.toggleRoomInfoPanel();
|
||||||
|
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||||
|
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
|
||||||
|
await rightPanel.getByRole("button", { name: "Share profile" }).click();
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Share User" });
|
||||||
|
await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible();
|
||||||
|
expect(dialog).toMatchScreenshot("share-dialog-user.png", {
|
||||||
|
// QRCode changes at every run
|
||||||
|
mask: [page.locator(".mx_QRCode")],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should share an event", { tag: "@screenshot" }, async ({ page, app, room }) => {
|
||||||
|
await app.viewRoomById(room.roomId);
|
||||||
|
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
|
||||||
|
|
||||||
|
const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" });
|
||||||
|
await timelineMessage.hover();
|
||||||
|
await page.getByRole("button", { name: "Options", exact: true }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "Share" }).click();
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Share Room Message" });
|
||||||
|
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
|
||||||
|
expect(dialog).toMatchScreenshot("share-dialog-event.png", {
|
||||||
|
// QRCode and url changes at every run
|
||||||
|
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
|
||||||
|
});
|
||||||
|
await dialog.getByRole("checkbox", { name: "Link to selected message" }).click();
|
||||||
|
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
|
@ -55,7 +55,7 @@ test.describe("Spaces", () => {
|
||||||
botCreateOpts: { displayName: "BotBob" },
|
botCreateOpts: { displayName: "BotBob" },
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should allow user to create public space", async ({ page, app, user }) => {
|
test("should allow user to create public space", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
const contextMenu = await openSpaceCreateMenu(page);
|
const contextMenu = await openSpaceCreateMenu(page);
|
||||||
await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
|
await expect(contextMenu).toMatchScreenshot("space-create-menu.png");
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ test.describe("Spaces", () => {
|
||||||
await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
|
await expect(page.getByRole("treeitem", { name: "Jokes" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should allow user to create private space", async ({ page, app, user }) => {
|
test("should allow user to create private space", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
const menu = await openSpaceCreateMenu(page);
|
const menu = await openSpaceCreateMenu(page);
|
||||||
await menu.getByRole("button", { name: "Private" }).click();
|
await menu.getByRole("button", { name: "Private" }).click();
|
||||||
|
|
||||||
|
@ -216,49 +216,47 @@ test.describe("Spaces", () => {
|
||||||
await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible();
|
await expect(hierarchyList.getByRole("treeitem", { name: "Gaming" }).getByRole("button")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render subspaces in the space panel only when expanded", async ({
|
test(
|
||||||
page,
|
"should render subspaces in the space panel only when expanded",
|
||||||
app,
|
{ tag: "@screenshot" },
|
||||||
user,
|
async ({ page, app, user, axe, checkA11y }) => {
|
||||||
axe,
|
axe.disableRules([
|
||||||
checkA11y,
|
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||||
}) => {
|
"nested-interactive",
|
||||||
axe.disableRules([
|
// XXX: We have some known contrast issues here
|
||||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
"color-contrast",
|
||||||
"nested-interactive",
|
]);
|
||||||
// XXX: We have some known contrast issues here
|
|
||||||
"color-contrast",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const childSpaceId = await app.client.createSpace({
|
const childSpaceId = await app.client.createSpace({
|
||||||
name: "Child Space",
|
name: "Child Space",
|
||||||
initial_state: [],
|
initial_state: [],
|
||||||
});
|
});
|
||||||
await app.client.createSpace({
|
await app.client.createSpace({
|
||||||
name: "Root Space",
|
name: "Root Space",
|
||||||
initial_state: [spaceChildInitialState(childSpaceId)],
|
initial_state: [spaceChildInitialState(childSpaceId)],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find collapsed Space panel
|
// Find collapsed Space panel
|
||||||
const spaceTree = page.getByRole("tree", { name: "Spaces" });
|
const spaceTree = page.getByRole("tree", { name: "Spaces" });
|
||||||
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
|
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
|
||||||
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
|
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
|
||||||
|
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png");
|
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png");
|
||||||
|
|
||||||
// This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
|
// This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
|
||||||
// button with the same name with different class name "mx_SpacePanel_toggleCollapse".
|
// button with the same name with different class name "mx_SpacePanel_toggleCollapse".
|
||||||
await spaceTree.getByRole("button", { name: "Expand" }).click();
|
await spaceTree.getByRole("button", { name: "Expand" }).click();
|
||||||
await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector
|
await expect(page.locator(".mx_SpacePanel:not(.collapsed)")).toBeVisible(); // TODO: replace :not() selector
|
||||||
|
|
||||||
const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" });
|
const item = page.locator(".mx_SpaceItem", { hasText: "Root Space" });
|
||||||
await expect(item).toBeVisible();
|
await expect(item).toBeVisible();
|
||||||
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
|
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
|
||||||
|
|
||||||
await checkA11y();
|
await checkA11y();
|
||||||
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
|
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({
|
test("should not soft crash when joining a room from space hierarchy which has a link in its topic", async ({
|
||||||
page,
|
page,
|
||||||
|
|
|
@ -276,7 +276,7 @@ export class Helpers {
|
||||||
* Assert that the threads activity centre button has no indicator
|
* Assert that the threads activity centre button has no indicator
|
||||||
*/
|
*/
|
||||||
async assertNoTacIndicator() {
|
async assertNoTacIndicator() {
|
||||||
// Assert by checkng neither of the known indicators are visible first. This will wait
|
// Assert by checking neither of the known indicators are visible first. This will wait
|
||||||
// if it takes a little time to disappear, but the screenshot comparison won't.
|
// if it takes a little time to disappear, but the screenshot comparison won't.
|
||||||
await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible();
|
await expect(this.getTacButton().locator("[data-indicator='success']")).not.toBeVisible();
|
||||||
await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible();
|
await expect(this.getTacButton().locator("[data-indicator='critical']")).not.toBeVisible();
|
||||||
|
@ -376,7 +376,7 @@ export class Helpers {
|
||||||
* Clicks the button to mark all threads as read in the current room
|
* Clicks the button to mark all threads as read in the current room
|
||||||
*/
|
*/
|
||||||
clickMarkAllThreadsRead() {
|
clickMarkAllThreadsRead() {
|
||||||
return this.page.getByLabel("Mark all as read").click();
|
return this.page.locator("#thread-panel").getByRole("button", { name: "Mark all as read" }).click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,16 +16,18 @@ test.describe("Threads Activity Centre", () => {
|
||||||
labsFlags: ["threadsActivityCentre"],
|
labsFlags: ["threadsActivityCentre"],
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should have the button correctly aligned and displayed in the space panel when expanded", async ({
|
test(
|
||||||
util,
|
"should have the button correctly aligned and displayed in the space panel when expanded",
|
||||||
}) => {
|
{ tag: "@screenshot" },
|
||||||
// Open the space panel
|
async ({ util }) => {
|
||||||
await util.expandSpacePanel();
|
// Open the space panel
|
||||||
// The buttons in the space panel should be aligned when expanded
|
await util.expandSpacePanel();
|
||||||
await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png");
|
// The buttons in the space panel should be aligned when expanded
|
||||||
});
|
await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("should not show indicator when there is no thread", async ({ room1, util }) => {
|
test("should not show indicator when there is no thread", { tag: "@screenshot" }, async ({ room1, util }) => {
|
||||||
// No indicator should be shown
|
// No indicator should be shown
|
||||||
await util.assertNoTacIndicator();
|
await util.assertNoTacIndicator();
|
||||||
|
|
||||||
|
@ -62,7 +64,7 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await util.assertHighlightIndicator();
|
await util.assertHighlightIndicator();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => {
|
test("should show the rooms with unread threads", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
|
||||||
await util.goTo(room2);
|
await util.goTo(room2);
|
||||||
await util.populateThreads(room1, room2, msg);
|
await util.populateThreads(room1, room2, msg);
|
||||||
// The indicator should be shown
|
// The indicator should be shown
|
||||||
|
@ -79,7 +81,7 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
|
await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should update with a thread is read", async ({ room1, room2, util, msg }) => {
|
test("should update with a thread is read", { tag: "@screenshot" }, async ({ room1, room2, util, msg }) => {
|
||||||
await util.goTo(room2);
|
await util.goTo(room2);
|
||||||
await util.populateThreads(room1, room2, msg);
|
await util.populateThreads(room1, room2, msg);
|
||||||
|
|
||||||
|
@ -128,7 +130,7 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible();
|
await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should have the correct hover state", async ({ util, page }) => {
|
test("should have the correct hover state", { tag: "@screenshot" }, async ({ util, page }) => {
|
||||||
await util.hoverTacButton();
|
await util.hoverTacButton();
|
||||||
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png");
|
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png");
|
||||||
|
|
||||||
|
@ -138,7 +140,7 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
|
await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should mark all threads as read", async ({ room1, room2, util, msg, page }) => {
|
test("should mark all threads as read", { tag: "@screenshot" }, async ({ room1, room2, util, msg, page }) => {
|
||||||
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
|
||||||
|
|
||||||
await util.assertNotificationTac();
|
await util.assertNotificationTac();
|
||||||
|
@ -146,7 +148,7 @@ test.describe("Threads Activity Centre", () => {
|
||||||
await util.openTac();
|
await util.openTac();
|
||||||
await util.clickRoomInTac(room1.name);
|
await util.clickRoomInTac(room1.name);
|
||||||
|
|
||||||
util.clickMarkAllThreadsRead();
|
await util.clickMarkAllThreadsRead();
|
||||||
|
|
||||||
await util.assertNoTacIndicator();
|
await util.assertNoTacIndicator();
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,7 +25,7 @@ test.describe("Threads", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Flaky: https://github.com/vector-im/element-web/issues/26452
|
// Flaky: https://github.com/vector-im/element-web/issues/26452
|
||||||
test.skip("should be usable for a conversation", async ({ page, app, bot }) => {
|
test.skip("should be usable for a conversation", { tag: "@screenshot" }, async ({ page, app, bot }) => {
|
||||||
const roomId = await app.client.createRoom({});
|
const roomId = await app.client.createRoom({});
|
||||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
await bot.joinRoom(roomId);
|
await bot.joinRoom(roomId);
|
||||||
|
@ -150,7 +150,7 @@ test.describe("Threads", () => {
|
||||||
).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart);
|
).toHaveCSS("padding-inline-start", ThreadViewGroupSpacingStart);
|
||||||
|
|
||||||
// Take snapshot of group layout (IRC layout is not available on ThreadView)
|
// Take snapshot of group layout (IRC layout is not available on ThreadView)
|
||||||
expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||||
"ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
|
"ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
|
||||||
{
|
{
|
||||||
mask: mask,
|
mask: mask,
|
||||||
|
@ -174,7 +174,7 @@ test.describe("Threads", () => {
|
||||||
.toHaveCSS("margin-inline-start", "0px");
|
.toHaveCSS("margin-inline-start", "0px");
|
||||||
|
|
||||||
// Take snapshot of bubble layout
|
// Take snapshot of bubble layout
|
||||||
expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
|
||||||
"ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
|
"ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
|
||||||
{
|
{
|
||||||
mask: mask,
|
mask: mask,
|
||||||
|
@ -351,57 +351,61 @@ test.describe("Threads", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should send location and reply to the location on ThreadView", async ({ page, app, bot }) => {
|
test(
|
||||||
const roomId = await app.client.createRoom({});
|
"should send location and reply to the location on ThreadView",
|
||||||
await app.client.inviteUser(roomId, bot.credentials.userId);
|
{ tag: "@screenshot" },
|
||||||
await bot.joinRoom(roomId);
|
async ({ page, app, bot }) => {
|
||||||
await page.goto("/#/room/" + roomId);
|
const roomId = await app.client.createRoom({});
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
await page.goto("/#/room/" + roomId);
|
||||||
|
|
||||||
// Exclude timestamp, read marker, and maplibregl-map from snapshots
|
// Exclude timestamp, read marker, and maplibregl-map from snapshots
|
||||||
const css =
|
const css =
|
||||||
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }";
|
".mx_MessageTimestamp, .mx_MessagePanel_myReadMarker, .maplibregl-map { visibility: hidden !important; }";
|
||||||
|
|
||||||
let locator = page.locator(".mx_RoomView_body");
|
let locator = page.locator(".mx_RoomView_body");
|
||||||
// User sends message
|
// User sends message
|
||||||
let textbox = locator.getByRole("textbox", { name: "Send a message…" });
|
let textbox = locator.getByRole("textbox", { name: "Send a message…" });
|
||||||
await textbox.fill("Hello Mr. Bot");
|
await textbox.fill("Hello Mr. Bot");
|
||||||
await textbox.press("Enter");
|
await textbox.press("Enter");
|
||||||
// Wait for message to send, get its ID and save as @threadId
|
// Wait for message to send, get its ID and save as @threadId
|
||||||
const threadId = await locator
|
const threadId = await locator
|
||||||
.locator(".mx_EventTile[data-scroll-tokens]")
|
.locator(".mx_EventTile[data-scroll-tokens]")
|
||||||
.filter({ hasText: "Hello Mr. Bot" })
|
.filter({ hasText: "Hello Mr. Bot" })
|
||||||
.getAttribute("data-scroll-tokens");
|
.getAttribute("data-scroll-tokens");
|
||||||
|
|
||||||
// Bot starts thread
|
// Bot starts thread
|
||||||
await bot.sendMessage(roomId, "Hello there", threadId);
|
await bot.sendMessage(roomId, "Hello there", threadId);
|
||||||
|
|
||||||
// User clicks thread summary
|
// User clicks thread summary
|
||||||
await page.locator(".mx_RoomView_body .mx_ThreadSummary").click();
|
await page.locator(".mx_RoomView_body .mx_ThreadSummary").click();
|
||||||
|
|
||||||
// User sends location on ThreadView
|
// User sends location on ThreadView
|
||||||
await expect(page.locator(".mx_ThreadView")).toBeAttached();
|
await expect(page.locator(".mx_ThreadView")).toBeAttached();
|
||||||
await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click();
|
await (await app.openMessageComposerOptions(true)).getByRole("menuitem", { name: "Location" }).click();
|
||||||
await page.getByTestId(`share-location-option-Pin`).click();
|
await page.getByTestId(`share-location-option-Pin`).click();
|
||||||
await page.locator("#mx_LocationPicker_map").click();
|
await page.locator("#mx_LocationPicker_map").click();
|
||||||
await page.getByRole("button", { name: "Share location" }).click();
|
await page.getByRole("button", { name: "Share location" }).click();
|
||||||
await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({
|
await expect(page.locator(".mx_ThreadView .mx_EventTile_last .mx_MLocationBody")).toBeAttached({
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// User replies to the location
|
// User replies to the location
|
||||||
locator = page.locator(".mx_ThreadView");
|
locator = page.locator(".mx_ThreadView");
|
||||||
await locator.locator(".mx_EventTile_last").hover();
|
await locator.locator(".mx_EventTile_last").hover();
|
||||||
await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click();
|
await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click();
|
||||||
textbox = locator.getByRole("textbox", { name: "Reply to thread…" });
|
textbox = locator.getByRole("textbox", { name: "Reply to thread…" });
|
||||||
await textbox.fill("Please come here");
|
await textbox.fill("Please come here");
|
||||||
await textbox.press("Enter");
|
await textbox.press("Enter");
|
||||||
// Wait until the reply is sent
|
// Wait until the reply is sent
|
||||||
await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible();
|
await expect(locator.locator(".mx_EventTile_last .mx_EventTile_receiptSent")).toBeVisible();
|
||||||
|
|
||||||
// Take a snapshot of reply to the shared location
|
// Take a snapshot of reply to the shared location
|
||||||
await page.addStyleTag({ content: css });
|
await page.addStyleTag({ content: css });
|
||||||
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png");
|
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Reply_to_the_location_on_ThreadView.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("right panel behaves correctly", async ({ page, app, user }) => {
|
test("right panel behaves correctly", async ({ page, app, user }) => {
|
||||||
// Create room
|
// Create room
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,7 +11,7 @@ import { test, expect } from "../../element-web-test";
|
||||||
test.describe("User Menu", () => {
|
test.describe("User Menu", () => {
|
||||||
test.use({ displayName: "Jeff" });
|
test.use({ displayName: "Jeff" });
|
||||||
|
|
||||||
test("should contain our name & userId", async ({ page, user }) => {
|
test("should contain our name & userId", { tag: "@screenshot" }, async ({ page, user }) => {
|
||||||
await page.getByRole("button", { name: "User menu", exact: true }).click();
|
await page.getByRole("button", { name: "User menu", exact: true }).click();
|
||||||
const menu = page.getByRole("menu");
|
const menu = page.getByRole("menu");
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ test.describe("User Onboarding (new user)", () => {
|
||||||
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
|
await expect(page.locator(".mx_UserOnboardingList")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("page is shown and preference exists", async ({ page, app }) => {
|
test("page is shown and preference exists", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||||
await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
|
await expect(page.locator(".mx_UserOnboardingPage")).toMatchScreenshot(
|
||||||
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png",
|
"User-Onboarding-new-user-page-is-shown-and-preference-exists-1.png",
|
||||||
);
|
);
|
||||||
|
@ -34,7 +34,7 @@ test.describe("User Onboarding (new user)", () => {
|
||||||
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
|
await expect(page.getByText("Show shortcut to welcome checklist above the room list")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("app download dialog", async ({ page }) => {
|
test("app download dialog", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
await page.getByRole("button", { name: "Download apps" }).click();
|
await page.getByRole("button", { name: "Download apps" }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),
|
page.getByRole("dialog").getByRole("heading", { level: 1, name: "Download Element" }),
|
||||||
|
|
|
@ -14,7 +14,7 @@ test.describe("UserView", () => {
|
||||||
botCreateOpts: { displayName: "Usman" },
|
botCreateOpts: { displayName: "Usman" },
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should render the user view as expected", async ({ page, homeserver, user, bot }) => {
|
test("should render the user view as expected", { tag: "@screenshot" }, async ({ page, homeserver, user, bot }) => {
|
||||||
await page.goto(`/#/user/${bot.credentials.userId}`);
|
await page.goto(`/#/user/${bot.credentials.userId}`);
|
||||||
|
|
||||||
const rightPanel = page.locator("#mx_RightPanel");
|
const rightPanel = page.locator("#mx_RightPanel");
|
||||||
|
|
|
@ -70,7 +70,7 @@ test.describe("Widget Layout", () => {
|
||||||
await app.viewRoomByName(ROOM_NAME);
|
await app.viewRoomByName(ROOM_NAME);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be set properly", async ({ page }) => {
|
test("should be set properly", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png");
|
await expect(page.locator(".mx_AppsDrawer")).toMatchScreenshot("apps-drawer.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -314,6 +314,10 @@ export const expect = baseExpect.extend({
|
||||||
const testInfo = test.info();
|
const testInfo = test.info();
|
||||||
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
|
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
|
||||||
|
|
||||||
|
if (!testInfo.tags.includes("@screenshot")) {
|
||||||
|
throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot");
|
||||||
|
}
|
||||||
|
|
||||||
const page = "page" in receiver ? receiver.page() : receiver;
|
const page = "page" in receiver ? receiver.page() : receiver;
|
||||||
|
|
||||||
let css = `
|
let css = `
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
|
||||||
// Docker tag to use for synapse docker image.
|
// Docker tag to use for synapse docker image.
|
||||||
// We target a specific digest as every now and then a Synapse update will break our CI.
|
// We target a specific digest as every now and then a Synapse update will break our CI.
|
||||||
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
// This digest is updated by the playwright-image-updates.yaml workflow periodically.
|
||||||
const DOCKER_TAG = "develop@sha256:34da08a44994e0ad2def7ed5f28c3cc7a2f7ead9722f4ae87b23030f59384ea5";
|
const DOCKER_TAG = "develop@sha256:6b82dba715fa7ae641010b4cc5e71edaeb9cc05a50ac5b9e4ff09afa9cd2a80d";
|
||||||
|
|
||||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
|
||||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue