Apply prettier formatting

This commit is contained in:
Michael Weimann 2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
No known key found for this signature in database
GPG key ID: 53F535A266BB9584
1576 changed files with 65385 additions and 62478 deletions

View file

@ -1,12 +1,6 @@
module.exports = {
plugins: [
"matrix-org",
],
extends: [
"plugin:matrix-org/babel",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
],
plugins: ["matrix-org"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
env: {
browser: true,
node: true,
@ -40,34 +34,47 @@ module.exports = {
],
// Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell.
"no-restricted-imports": ["error", {
"paths": [{
"name": "matrix-js-sdk",
"message": "Please use matrix-js-sdk/src/matrix instead",
}, {
"name": "matrix-js-sdk/",
"message": "Please use matrix-js-sdk/src/matrix instead",
}, {
"name": "matrix-js-sdk/src",
"message": "Please use matrix-js-sdk/src/matrix instead",
}, {
"name": "matrix-js-sdk/src/",
"message": "Please use matrix-js-sdk/src/matrix instead",
}, {
"name": "matrix-js-sdk/src/index",
"message": "Please use matrix-js-sdk/src/matrix instead",
}, {
"name": "matrix-react-sdk",
"message": "Please use matrix-react-sdk/src/index instead",
}, {
"name": "matrix-react-sdk/",
"message": "Please use matrix-react-sdk/src/index instead",
}],
"patterns": [{
"group": ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
"message": "Please use matrix-js-sdk/src/* instead",
}],
}],
"no-restricted-imports": [
"error",
{
paths: [
{
name: "matrix-js-sdk",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src/",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-js-sdk/src/index",
message: "Please use matrix-js-sdk/src/matrix instead",
},
{
name: "matrix-react-sdk",
message: "Please use matrix-react-sdk/src/index instead",
},
{
name: "matrix-react-sdk/",
message: "Please use matrix-react-sdk/src/index instead",
},
],
patterns: [
{
group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
message: "Please use matrix-js-sdk/src/* instead",
},
],
},
],
// There are too many a11y violations to fix at once
// Turn violated rules off until they are fixed
@ -90,15 +97,8 @@ module.exports = {
},
overrides: [
{
files: [
"src/**/*.{ts,tsx}",
"test/**/*.{ts,tsx}",
"cypress/**/*.ts",
],
extends: [
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
],
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts"],
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
rules: {
// temporary disabled
"@typescript-eslint/explicit-function-return-type": "off",
@ -151,12 +151,12 @@ module.exports = {
"src/components/views/rooms/MessageComposer.tsx",
"src/components/views/rooms/ReplyPreview.tsx",
"src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx",
"src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx"
"src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx",
],
rules: {
"@typescript-eslint/no-var-requires": "off",
},
}
},
],
settings: {
react: {
@ -166,7 +166,7 @@ module.exports = {
};
function buildRestrictedPropertiesOptions(properties, message) {
return properties.map(prop => {
return properties.map((prop) => {
let [object, property] = prop.split(".");
if (object === "*") {
object = undefined;

View file

@ -2,9 +2,9 @@
## Checklist
* [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md))
- [ ] Tests written for new code (and old code if feasible)
- [ ] Linter and other CI checks pass
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md))
<!--
If you would like to specify text for the changelog entry other than your PR title, add the following:

View file

@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>matrix-org/renovate-config-element-web"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>matrix-org/renovate-config-element-web"]
}

View file

@ -1,30 +1,30 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
branches:
- develop
pull_request_target:
types:
- closed
- labeled
branches:
- develop
jobs:
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
backport:
name: Backport
runs-on: ubuntu-latest
# Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: >
github.event.pull_request.merged
&& (
github.event.action == 'closed'
|| (
github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport')
)
)
steps:
- uses: tibdex/backport@v2
with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -1,172 +1,175 @@
# Triggers after the layered build has finished, taking the artifact and running cypress on it
name: Cypress End to End Tests
on:
workflow_run:
workflows: [ "Element Web - Build" ]
types:
- completed
workflow_run:
workflows: ["Element Web - Build"]
types:
- completed
jobs:
prepare:
name: Prepare
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
outputs:
uuid: ${{ steps.uuid.outputs.value }}
pr_id: ${{ steps.prdetails.outputs.pr_id }}
commit_message: ${{ steps.commit.outputs.message }}
commit_author: ${{ steps.commit.outputs.author }}
commit_email: ${{ steps.commit.outputs.email }}
percy_enable: ${{ steps.percy.outputs.value || '1' }}
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
prepare:
name: Prepare
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
outputs:
uuid: ${{ steps.uuid.outputs.value }}
pr_id: ${{ steps.prdetails.outputs.pr_id }}
commit_message: ${{ steps.commit.outputs.message }}
commit_author: ${{ steps.commit.outputs.author }}
commit_email: ${{ steps.commit.outputs.email }}
percy_enable: ${{ steps.percy.outputs.value || '1' }}
steps:
# We create the status here and then update it to success/failure in the `report` stage
# This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- id: prdetails
if: github.event.workflow_run.event == 'pull_request'
uses: matrix-org/pr-details-action@v1.2
with:
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
- id: prdetails
if: github.event.workflow_run.event == 'pull_request'
uses: matrix-org/pr-details-action@v1.2
with:
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
- name: Get commit details
id: commit
if: github.event.workflow_run.event == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const response = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: "${{ github.event.workflow_run.head_sha }}",
});
core.setOutput("message", response.data.message);
core.setOutput("author", response.data.author.name);
core.setOutput("email", response.data.author.email);
- name: Get commit details
id: commit
if: github.event.workflow_run.event == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const response = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: "${{ github.event.workflow_run.head_sha }}",
});
core.setOutput("message", response.data.message);
core.setOutput("author", response.data.author.name);
core.setOutput("email", response.data.author.email);
# Only run Percy when it is demanded or on develop
- name: Disable Percy if not needed
id: percy
if: |
github.event.workflow_run.event == 'pull_request' &&
!contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
run: echo "::set-output name=value::0"
# Only run Percy when it is demanded or on develop
- name: Disable Percy if not needed
id: percy
if: |
github.event.workflow_run.event == 'pull_request' &&
!contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
run: echo "::set-output name=value::0"
- name: Generate unique ID 💎
id: uuid
run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")"
- name: Generate unique ID 💎
id: uuid
run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")"
tests:
name: "Run Tests"
needs: prepare
runs-on: ubuntu-latest
permissions:
actions: read
issues: read
pull-requests: read
environment: Cypress
#strategy:
# fail-fast: false
# matrix:
# # Run 4 instances in Parallel
# runner: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
with:
# XXX: We're checking out untrusted code in a secure context
# We need to be careful to not trust anything this code outputs/may do
# We need to check this out to access the cypress tests which are on the head branch
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
tests:
name: "Run Tests"
needs: prepare
runs-on: ubuntu-latest
permissions:
actions: read
issues: read
pull-requests: read
environment:
Cypress
#strategy:
# fail-fast: false
# matrix:
# # Run 4 instances in Parallel
# runner: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v3
with:
# XXX: We're checking out untrusted code in a secure context
# We need to be careful to not trust anything this code outputs/may do
# We need to check this out to access the cypress tests which are on the head branch
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2
with:
run_id: ${{ github.event.workflow_run.id }}
name: previewbuild
path: webapp
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2
with:
run_id: ${{ github.event.workflow_run.id }}
name: previewbuild
path: webapp
- name: Run Cypress tests
uses: cypress-io/github-action@v4.2.2
with:
# The built-in Electron runner seems to grind to a halt trying
# to run the tests, so use chrome.
browser: chrome
start: npx serve -p 8080 webapp
wait-on: 'http://localhost:8080'
record: true
#parallel: true
#command-prefix: 'yarn percy exec --parallel --'
command-prefix: 'yarn percy exec --'
ci-build-id: ${{ needs.prepare.outputs.uuid }}
env:
# pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
- name: Run Cypress tests
uses: cypress-io/github-action@v4.2.2
with:
# The built-in Electron runner seems to grind to a halt trying
# to run the tests, so use chrome.
browser: chrome
start: npx serve -p 8080 webapp
wait-on: "http://localhost:8080"
record:
true
#parallel: true
#command-prefix: 'yarn percy exec --parallel --'
command-prefix: "yarn percy exec --"
ci-build-id: ${{ needs.prepare.outputs.uuid }}
env:
# pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# Use existing chromium rather than downloading another
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
# Use existing chromium rather than downloading another
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
# pass GitHub token to allow accurately detecting a build vs a re-run build
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# pass GitHub token to allow accurately detecting a build vs a re-run build
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# make Node's os.tmpdir() return something where we actually have permissions
TMPDIR: ${{ runner.temp }}
# make Node's os.tmpdir() return something where we actually have permissions
TMPDIR: ${{ runner.temp }}
# tell Cypress more details about the context of this run
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
# tell Cypress more details about the context of this run
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
# pass the Percy token as an environment variable
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }}
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
# tell Percy more details about the context of this run
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
# pass the Percy token as an environment variable
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }}
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
# tell Percy more details about the context of this run
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
PERCY_PULL_REQUEST:
${{ needs.prepare.outputs.pr_id }}
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
- name: Upload Artifact
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-results
path: |
cypress/screenshots
cypress/videos
cypress/synapselogs
- name: Upload Artifact
if: failure()
uses: actions/upload-artifact@v3
with:
name: cypress-results
path: |
cypress/screenshots
cypress/videos
cypress/synapselogs
report:
name: Report results
needs: tests
runs-on: ubuntu-latest
if: always()
permissions:
statuses: write
steps:
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
report:
name: Report results
needs: tests
runs-on: ubuntu-latest
if: always()
permissions:
statuses: write
steps:
- uses: Sibz/github-status-action@v1
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}

View file

@ -3,52 +3,52 @@
# as an artifact and run integration tests.
name: Element Web - Build
on:
pull_request: { }
push:
branches: [ develop, master ]
repository_dispatch:
types: [ upstream-sdk-notify ]
pull_request: {}
push:
branches: [develop, master]
repository_dispatch:
types: [upstream-sdk-notify]
env:
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
build:
name: "Build Element-Web"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
build:
name: "Build Element-Web"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Fetch layered build
id: layered_build
run: |
scripts/ci/layered.sh
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
REACT_SHA=$(git rev-parse --short=12 HEAD)
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
echo "::set-output name=VERSION::$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA"
- name: Fetch layered build
id: layered_build
run: |
scripts/ci/layered.sh
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
REACT_SHA=$(git rev-parse --short=12 HEAD)
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
echo "::set-output name=VERSION::$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA"
- name: Copy config
run: cp element.io/develop/config.json config.json
working-directory: ./element-web
- name: Copy config
run: cp element.io/develop/config.json config.json
working-directory: ./element-web
- name: Build
env:
CI_PACKAGE: true
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
run: |
yarn build
echo $VERSION > webapp/version
working-directory: ./element-web
- name: Build
env:
CI_PACKAGE: true
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
run: |
yarn build
echo $VERSION > webapp/version
working-directory: ./element-web
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: previewbuild
path: element-web/webapp
# We'll only use this in a triggered job, then we're done with it
retention-days: 1
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: previewbuild
path: element-web/webapp
# We'll only use this in a triggered job, then we're done with it
retention-days: 1

View file

@ -1,40 +1,40 @@
name: i18n Check
on:
workflow_call: { }
workflow_call: {}
jobs:
check:
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: actions/checkout@v3
check:
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: actions/checkout@v3
- name: "Get modified files"
id: changed_files
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
uses: tj-actions/changed-files@v34
with:
files: |
src/i18n/strings/*
files_ignore: |
src/i18n/strings/en_EN.json
- name: "Get modified files"
id: changed_files
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
uses: tj-actions/changed-files@v34
with:
files: |
src/i18n/strings/*
files_ignore: |
src/i18n/strings/en_EN.json
- name: "Assert only en_EN was modified"
if: |
github.event_name == 'pull_request' &&
github.event.pull_request.user.login != 'RiotTranslateBot' &&
steps.changed_files.outputs.any_modified == 'true'
run: |
echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways."
exit 1
- name: "Assert only en_EN was modified"
if: |
github.event_name == 'pull_request' &&
github.event.pull_request.user.login != 'RiotTranslateBot' &&
steps.changed_files.outputs.any_modified == 'true'
run: |
echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways."
exit 1
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install --pure-lockfile"
- name: i18n Check
run: "yarn run diff-i18n"
- name: i18n Check
run: "yarn run diff-i18n"

View file

@ -2,70 +2,70 @@
# and uploading it to netlify
name: Upload Preview Build to Netlify
on:
workflow_run:
workflows: [ "Element Web - Build" ]
types:
- completed
workflow_run:
workflows: ["Element Web - Build"]
types:
- completed
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
environment: Netlify
steps:
- name: 📝 Create Deployment
uses: bobheadxi/deployments@v1
id: deployment
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: Netlify
ref: ${{ github.event.workflow_run.head_sha }}
desc: |
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
Exercise caution. Use test accounts.
deploy:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
environment: Netlify
steps:
- name: 📝 Create Deployment
uses: bobheadxi/deployments@v1
id: deployment
with:
step: start
token: ${{ secrets.GITHUB_TOKEN }}
env: Netlify
ref: ${{ github.event.workflow_run.head_sha }}
desc: |
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
Exercise caution. Use test accounts.
- id: prdetails
uses: matrix-org/pr-details-action@v1.2
with:
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
- id: prdetails
uses: matrix-org/pr-details-action@v1.2
with:
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: element-build-and-test.yaml
run_id: ${{ github.event.workflow_run.id }}
name: previewbuild
path: webapp
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: element-build-and-test.yaml
run_id: ${{ github.event.workflow_run.id }}
name: previewbuild
path: webapp
- name: ☁️ Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@v1.2
with:
publish-dir: webapp
deploy-message: "Deploy from GitHub Actions"
# These don't work because we're in workflow_run
enable-pull-request-comment: false
enable-commit-comment: false
alias: pr${{ steps.prdetails.outputs.pr_id }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
- name: ☁️ Deploy to Netlify
id: netlify
uses: nwtgck/actions-netlify@v1.2
with:
publish-dir: webapp
deploy-message: "Deploy from GitHub Actions"
# These don't work because we're in workflow_run
enable-pull-request-comment: false
enable-commit-comment: false
alias: pr${{ steps.prdetails.outputs.pr_id }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
timeout-minutes: 1
- name: 🚦 Update deployment status
uses: bobheadxi/deployments@v1
if: always()
with:
step: finish
override: false
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
env: ${{ steps.deployment.outputs.env }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env_url: ${{ steps.netlify.outputs.deploy-url }}
desc: |
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
Exercise caution. Use test accounts.
- name: 🚦 Update deployment status
uses: bobheadxi/deployments@v1
if: always()
with:
step: finish
override: false
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status }}
env: ${{ steps.deployment.outputs.env }}
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
env_url: ${{ steps.netlify.outputs.deploy-url }}
desc: |
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
Exercise caution. Use test accounts.

View file

@ -1,19 +1,19 @@
name: Notify element-web
on:
push:
branches: [ develop ]
repository_dispatch:
types: [ upstream-sdk-notify ]
push:
branches: [develop]
repository_dispatch:
types: [upstream-sdk-notify]
jobs:
notify-element-web:
name: "Notify Element Web"
runs-on: ubuntu-latest
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-react-sdk'
steps:
- name: Notify element-web repo that a new SDK build is on develop
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: vector-im/element-web
event-type: element-web-notify
notify-element-web:
name: "Notify Element Web"
runs-on: ubuntu-latest
# Only respect triggers from our develop branch, ignore that of forks
if: github.repository == 'matrix-org/matrix-react-sdk'
steps:
- name: Notify element-web repo that a new SDK build is on develop
uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
repository: vector-im/element-web
event-type: element-web-notify

View file

@ -1,12 +1,12 @@
name: Pull Request
on:
pull_request_target:
types: [ opened, edited, labeled, unlabeled, synchronize ]
pull_request_target:
types: [opened, edited, labeled, unlabeled, synchronize]
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
jobs:
action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
with:
labels: "T-Defect,T-Enhancement,T-Task"
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
with:
labels: "T-Defect,T-Enhancement,T-Task"
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -1,11 +1,11 @@
name: Release Process
on:
release:
types: [ published ]
release:
types: [published]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View file

@ -1,15 +1,15 @@
name: SonarQube
on:
workflow_run:
workflows: [ "Tests" ]
types:
- completed
workflow_run:
workflows: ["Tests"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
sonarqube:
name: 🩻 SonarQube
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
sonarqube:
name: 🩻 SonarQube
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View file

@ -1,148 +1,148 @@
name: Static Analysis
on:
pull_request: { }
push:
branches: [ develop, master ]
repository_dispatch:
types: [ upstream-sdk-notify ]
pull_request: {}
push:
branches: [develop, master]
repository_dispatch:
types: [upstream-sdk-notify]
env:
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
ts_lint:
name: "Typescript Syntax Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Typecheck
run: "yarn run lint:types"
- name: Typecheck
run: "yarn run lint:types"
- name: Switch js-sdk to release mode
working-directory: node_modules/matrix-js-sdk
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Switch js-sdk to release mode
working-directory: node_modules/matrix-js-sdk
run: |
scripts/switch_package_to_release.js
yarn install
yarn run build:compile
yarn run build:types
- name: Typecheck (release mode)
run: "yarn run lint:types"
- name: Typecheck (release mode)
run: "yarn run lint:types"
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
strategy:
fail-fast: false
matrix:
args:
- '--strict --noImplicitAny'
- '--noImplicitAny'
steps:
- uses: actions/checkout@v3
- name: Install Deps
run: "scripts/ci/layered.sh"
tsc-strict:
name: Typescript Strict Error Checker
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: read
checks: write
strategy:
fail-fast: false
matrix:
args:
- "--strict --noImplicitAny"
- "--noImplicitAny"
steps:
- uses: actions/checkout@v3
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Install Deps
run: "scripts/ci/layered.sh"
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
- name: Get diff lines
id: diff
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: ${{ matrix.args }}
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
- name: Detecting files changed
id: files
uses: futuratrepadeira/changed-files@v4.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pattern: '^.*\.tsx?$'
i18n_lint:
name: "i18n Check"
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
- uses: t3chguy/typescript-check-action@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
use-check: false
check-fail-mode: added
output-behaviour: annotate
ts-extra-args: ${{ matrix.args }}
files-changed: ${{ steps.files.outputs.files_updated }}
files-added: ${{ steps.files.outputs.files_created }}
files-deleted: ${{ steps.files.outputs.files_deleted }}
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
rethemendex_lint:
name: "Rethemendex Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: ./res/css/rethemendex.sh
- run: git diff --exit-code
i18n_lint:
name: "i18n Check"
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
rethemendex_lint:
name: "Rethemendex Check"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- run: ./res/css/rethemendex.sh
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install"
- run: git diff --exit-code
- name: Run Linter
run: "yarn run lint:js"
js_lint:
name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
style_lint:
name: "Style Lint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- uses: actions/setup-node@v3
with:
cache: 'yarn'
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install"
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install"
- name: Run Linter
run: "yarn run lint:js"
- name: Run Linter
run: "yarn run lint:style"
style_lint:
name: "Style Lint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
analyse_dead_code:
name: "Analyse Dead Code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- uses: actions/setup-node@v3
with:
cache: 'yarn'
# Does not need branch matching as only analyses this layer
- name: Install Deps
run: "yarn install"
- name: Install Deps
run: "scripts/ci/layered.sh"
- name: Run Linter
run: "yarn run lint:style"
- name: Dead Code Analysis
run: |
cd element-web
yarn run analyse:unused-exports
analyse_dead_code:
name: "Analyse Dead Code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "scripts/ci/layered.sh"
- name: Dead Code Analysis
run: |
cd element-web
yarn run analyse:unused-exports

View file

@ -1,59 +1,59 @@
name: Tests
on:
pull_request: { }
push:
branches: [ develop, master ]
repository_dispatch:
types: [ upstream-sdk-notify ]
pull_request: {}
push:
branches: [develop, master]
repository_dispatch:
types: [upstream-sdk-notify]
env:
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
jest:
name: Jest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
jest:
name: Jest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Yarn cache
uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Yarn cache
uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage and metrics
if: github.ref == 'refs/heads/develop'
run: "yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Run tests with coverage and metrics
if: github.ref == 'refs/heads/develop'
run: "yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Run tests with coverage
if: github.ref != 'refs/heads/develop'
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Run tests with coverage
if: github.ref != 'refs/heads/develop'
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: coverage
path: |
coverage
!coverage/lcov-report
app-tests:
name: Element Web Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
app-tests:
name: Element Web Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Run tests
run: "./scripts/ci/app-tests.sh"
- name: Run tests
run: "./scripts/ci/app-tests.sh"

View file

@ -1,8 +1,8 @@
name: Upgrade Dependencies
on:
workflow_dispatch: { }
workflow_dispatch: {}
jobs:
upgrade:
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
upgrade:
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -1,7 +1,7 @@
version: 2
snapshot:
widths:
- 1024
- 1920
widths:
- 1024
- 1920
percy:
defer-uploads: true
defer-uploads: true

View file

@ -1,13 +1,8 @@
module.exports = {
"extends": [
"stylelint-config-standard",
"stylelint-config-prettier",
],
customSyntax: require('postcss-scss'),
"plugins": [
"stylelint-scss",
],
"rules": {
extends: ["stylelint-config-standard", "stylelint-config-prettier"],
customSyntax: require("postcss-scss"),
plugins: ["stylelint-scss"],
rules: {
"color-hex-case": null,
"comment-empty-line-before": null,
"declaration-empty-line-before": null,
@ -22,15 +17,18 @@ module.exports = {
"at-rule-no-unknown": null,
"no-descending-specificity": null,
"no-empty-first-line": true,
"scss/at-rule-no-unknown": [true, {
// https://github.com/vector-im/element-web/issues/10544
"ignoreAtRules": ["define-mixin"],
}],
"scss/at-rule-no-unknown": [
true,
{
// https://github.com/vector-im/element-web/issues/10544
ignoreAtRules: ["define-mixin"],
},
],
// Disable `&_kind`-style selectors while our unused CSS approach is "Find & Replace All"
// rather than a CI thing. Shorthand selectors are harder to detect when searching for a
// class name. This regex is trying to *allow* anything except `&words`, such as `&::before`,
// `&.mx_Class`, etc.
"selector-nested-pattern": "^((&[ :.\\\[,])|([^&]))",
"selector-nested-pattern": "^((&[ :.\\[,])|([^&]))",
"declaration-colon-space-after": "always-single-line",
// Disable some defaults
"selector-class-pattern": null,
@ -52,4 +50,4 @@ module.exports = {
"number-max-precision": null,
"no-invalid-double-slash-comments": true,
},
}
};

27848
CHANGELOG.md

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
Contributing code to matrix-react-sdk
=====================================
# Contributing code to matrix-react-sdk
matrix-react-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md

115
README.md
View file

@ -9,18 +9,18 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
matrix-react-sdk
================
# matrix-react-sdk
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
This package provides the React components needed to build a Matrix web client
using React. It is not useable in isolation, and instead must be used from
using React. It is not useable in isolation, and instead must be used from
a 'skin'. A skin provides:
* Customised implementations of presentation components.
* Custom CSS
* The containing application
* Zero or more 'modules' containing non-UI functionality
- Customised implementations of presentation components.
- Custom CSS
- The containing application
- Zero or more 'modules' containing non-UI functionality
As of Aug 2018, the only skin that exists is
[`vector-im/element-web`](https://github.com/vector-im/element-web/); it and
@ -28,19 +28,19 @@ As of Aug 2018, the only skin that exists is
be considered as a single project (for instance, matrix-react-sdk bugs
are currently filed against vector-im/element-web rather than this project).
Translation Status
------------------
## Translation Status
[![Translation status](https://translate.element.io/widgets/element-web/-/multi-auto.svg)](https://translate.element.io/engage/element-web/?utm_source=widget)
Developer Guide
---------------
## Developer Guide
Platform Targets:
* Chrome, Firefox and Safari.
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
* Mobile Web is not currently a target platform - instead please use the native
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
- Chrome, Firefox and Safari.
- WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
- Mobile Web is not currently a target platform - instead please use the native
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
All code lands on the `develop` branch - `master` is only used for stable releases.
**Please file PRs against `develop`!!**
@ -52,22 +52,23 @@ Our code style is also the same as Element's:
https://github.com/vector-im/element-web/blob/develop/code_style.md
Code should be committed as follows:
* All new components:
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
* Element-specific components:
https://github.com/vector-im/element-web/tree/master/src/components
* In practice, `matrix-react-sdk` is still evolving so fast that the
maintenance burden of customising and overriding these components for
Element can seriously impede development. So right now, there should be
very few (if any) customisations for Element.
* CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
* Theme specific CSS & resources:
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
- All new components:
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
- Element-specific components:
https://github.com/vector-im/element-web/tree/master/src/components
- In practice, `matrix-react-sdk` is still evolving so fast that the
maintenance burden of customising and overriding these components for
Element can seriously impede development. So right now, there should be
very few (if any) customisations for Element.
- CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
- Theme specific CSS & resources:
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
React components in matrix-react-sdk come in two different flavours:
'structures' and 'views'. Structures are stateful components which handle the
'structures' and 'views'. Structures are stateful components which handle the
more complicated business logic of the app, delegating their actual presentation
rendering to stateless 'view' components. For instance, the RoomView component
rendering to stateless 'view' components. For instance, the RoomView component
that orchestrates the act of visualising the contents of a given Matrix chat
room tracks lots of state for its child components which it passes into them for
visual rendering via props.
@ -75,74 +76,72 @@ visual rendering via props.
Good separation between the components is maintained by adopting various best
practices that anyone working with the SDK needs to be aware of and uphold:
* Components are named with upper camel case (e.g. views/rooms/EventTile.js)
- Components are named with upper camel case (e.g. views/rooms/EventTile.js)
* They are organised in a typically two-level hierarchy - first whether the
- They are organised in a typically two-level hierarchy - first whether the
component is a view or a structure, and then a broad functional grouping
(e.g. 'rooms' here)
* The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
- The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
CSS for matrix-react-sdk currently resides in
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css.
* Per-view CSS is optional - it could choose to inherit all its styling from
- Per-view CSS is optional - it could choose to inherit all its styling from
the context of the rest of the app, although this is unusual for any but
* Theme specific CSS & resources:
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
structural components (lacking presentation logic) and the simplest view
components.
- Theme specific CSS & resources:
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
structural components (lacking presentation logic) and the simplest view
components.
* The view MUST *only* refer to the CSS rules defined in its own CSS file.
- The view MUST _only_ refer to the CSS rules defined in its own CSS file.
'Stealing' styling information from other components (including parents)
is not cool, as it breaks the independence of the components.
* CSS classes are named with an app-specific name-spacing prefix to try to
avoid CSS collisions. The base skin shipped by Matrix.org with the
matrix-react-sdk uses the naming prefix "mx_". A company called Yoyodyne
Inc might use a prefix like "yy_" for its app-specific classes.
- CSS classes are named with an app-specific name-spacing prefix to try to
avoid CSS collisions. The base skin shipped by Matrix.org with the
matrix-react-sdk uses the naming prefix "mx*". A company called Yoyodyne
Inc might use a prefix like "yy*" for its app-specific classes.
* CSS classes use upper camel case when they describe React components - e.g.
- CSS classes use upper camel case when they describe React components - e.g.
.mx_MessageTile is the selector for the CSS applied to a MessageTile view.
* CSS classes for DOM elements within a view which aren't components are named
- CSS classes for DOM elements within a view which aren't components are named
by appending a lower camel case identifier to the view's class name - e.g.
.mx_MessageTile_randomDiv is how you'd name the class of an arbitrary div
within the MessageTile view.
* We deliberately use vanilla CSS 3.0 to avoid adding any more magic
dependencies into the mix than we already have. App developers are welcome
to use whatever floats their boat however. In future we'll start using
- We deliberately use vanilla CSS 3.0 to avoid adding any more magic
dependencies into the mix than we already have. App developers are welcome
to use whatever floats their boat however. In future we'll start using
css-next to pull in features like CSS variable support.
* The CSS for a component can override the rules for child components.
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override
- The CSS for a component can override the rules for child components.
For instance, .mx*RoomList .mx_RoomTile {} would be the selector to override
styles of RoomTiles when viewed in the context of a RoomList view.
Overrides *must* be scoped to the View's CSS class - i.e. don't just define
Overrides \_must* be scoped to the View's CSS class - i.e. don't just define
.mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
only to the context of RoomList views. N.B. overrides should be relatively
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
only to the context of RoomList views. N.B. overrides should be relatively
rare as in general CSS inheritance should be enough.
* Components should render only within the bounding box of their outermost DOM
- Components should render only within the bounding box of their outermost DOM
element. Page-absolute positioning and negative CSS margins and similar are
generally not cool and stop the component from being reused easily in
different places.
Originally `matrix-react-sdk` followed the Atomic design pattern as per
http://patternlab.io to try to encourage a modular architecture. However, we
http://patternlab.io to try to encourage a modular architecture. However, we
found that the grouping of components into atoms/molecules/organisms
made them harder to find relative to a functional split, and didn't emphasise
the distinction between 'structural' and 'view' components, so we backed away
from it.
Github Issues
-------------
## Github Issues
All issues should be filed under https://github.com/vector-im/element-web/issues
for now.
Development
-----------
## Development
Ensure you have the latest LTS version of Node.js installed.

View file

@ -1,10 +1,10 @@
{
"en": {
"fileName": "en_EN.json",
"label": "English"
},
"en-us": {
"fileName": "en_US.json",
"label": "English (US)"
}
"en": {
"fileName": "en_EN.json",
"label": "English"
},
"en-us": {
"fileName": "en_US.json",
"label": "English (US)"
}
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
const EventEmitter = require("events");
const { LngLat, NavigationControl, LngLatBounds, AttributionControl } = require('maplibre-gl');
const { LngLat, NavigationControl, LngLatBounds, AttributionControl } = require("maplibre-gl");
class MockMap extends EventEmitter {
addControl = jest.fn();
@ -32,7 +32,7 @@ class MockGeolocateControl extends EventEmitter {
trigger = jest.fn();
}
const MockGeolocateInstance = new MockGeolocateControl();
const MockMarker = {}
const MockMarker = {};
MockMarker.setLngLat = jest.fn().mockReturnValue(MockMarker);
MockMarker.addTo = jest.fn().mockReturnValue(MockMarker);
MockMarker.remove = jest.fn().mockReturnValue(MockMarker);

View file

@ -1,2 +1,2 @@
export const Icon = 'div';
export const Icon = "div";
export default "image-file-stub";

View file

@ -1,18 +1,21 @@
module.exports = {
"sourceMaps": "inline",
"presets": [
["@babel/preset-env", {
"targets": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions",
],
}],
sourceMaps: "inline",
presets: [
[
"@babel/preset-env",
{
targets: [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions",
],
},
],
"@babel/preset-typescript",
"@babel/preset-react",
],
"plugins": [
plugins: [
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",

View file

@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { defineConfig } from 'cypress';
import { defineConfig } from "cypress";
export default defineConfig({
videoUploadOnPasses: false,
projectId: 'ppvnzg',
projectId: "ppvnzg",
experimentalInteractiveRunEvents: true,
defaultCommandTimeout: 10000,
chromeWebSecurity: false,
e2e: {
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.ts').default(on, config);
return require("./cypress/plugins/index.ts").default(on, config);
},
baseUrl: 'http://localhost:8080',
baseUrl: "http://localhost:8080",
experimentalSessionAndOrigin: true,
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
},
env: {
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.

View file

@ -23,7 +23,7 @@ describe("Composer", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
});
});
@ -42,26 +42,26 @@ describe("Composer", () => {
it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get('div[contenteditable=true]').type('my message 0');
cy.get("div[contenteditable=true]").type("my message 0");
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
cy.contains(".mx_EventTile_body", "my message 0").should("not.exist");
// Click send
cy.get('div[aria-label="Send message"]').click();
// It has been sent
cy.contains('.mx_EventTile_body', 'my message 0');
cy.contains(".mx_EventTile_body", "my message 0");
// Type another and press Enter afterwards
cy.get('div[contenteditable=true]').type('my message 1{enter}');
cy.get("div[contenteditable=true]").type("my message 1{enter}");
// It was sent
cy.contains('.mx_EventTile_body', 'my message 1');
cy.contains(".mx_EventTile_body", "my message 1");
});
it("can write formatted text", () => {
cy.get('div[contenteditable=true]').type('my bold{ctrl+b} message');
cy.get("div[contenteditable=true]").type("my bold{ctrl+b} message");
cy.get('div[aria-label="Send message"]').click();
// Note: both "bold" and "message" are bold, which is probably surprising
cy.contains('.mx_EventTile_body strong', 'bold message');
cy.contains(".mx_EventTile_body strong", "bold message");
});
it("should allow user to input emoji via graphical picker", () => {
@ -74,7 +74,7 @@ describe("Composer", () => {
});
cy.get(".mx_ContextualMenu_background").click(); // Close emoji picker
cy.get('div[contenteditable=true]').type("{enter}"); // Send message
cy.get("div[contenteditable=true]").type("{enter}"); // Send message
cy.contains(".mx_EventTile_body", "😇");
});
@ -86,14 +86,14 @@ describe("Composer", () => {
it("only sends when you press Ctrl+Enter", () => {
// Type a message and press Enter
cy.get('div[contenteditable=true]').type('my message 3{enter}');
cy.get("div[contenteditable=true]").type("my message 3{enter}");
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
cy.contains(".mx_EventTile_body", "my message 3").should("not.exist");
// Press Ctrl+Enter
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
cy.get("div[contenteditable=true]").type("{ctrl+enter}");
// It was sent
cy.contains('.mx_EventTile_body', 'my message 3');
cy.contains(".mx_EventTile_body", "my message 3");
});
});
});
@ -109,28 +109,28 @@ describe("Composer", () => {
it("sends a message when you click send or press Enter", () => {
// Type a message
cy.get('div[contenteditable=true]').type('my message 0');
cy.get("div[contenteditable=true]").type("my message 0");
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
cy.contains(".mx_EventTile_body", "my message 0").should("not.exist");
// Click send
cy.get('div[aria-label="Send message"]').click();
// It has been sent
cy.contains('.mx_EventTile_body', 'my message 0');
cy.contains(".mx_EventTile_body", "my message 0");
// Type another
cy.get('div[contenteditable=true]').type('my message 1');
cy.get("div[contenteditable=true]").type("my message 1");
// Press enter. Would be nice to just use {enter} but we can't because Cypress
// does not trigger an insertParagraph when you do that.
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" });
// It was sent
cy.contains('.mx_EventTile_body', 'my message 1');
cy.contains(".mx_EventTile_body", "my message 1");
});
it("can write formatted text", () => {
cy.get('div[contenteditable=true]').type('my {ctrl+b}bold{ctrl+b} message');
cy.get("div[contenteditable=true]").type("my {ctrl+b}bold{ctrl+b} message");
cy.get('div[aria-label="Send message"]').click();
cy.contains('.mx_EventTile_body strong', 'bold');
cy.contains(".mx_EventTile_body strong", "bold");
});
describe("when Ctrl+Enter is required to send", () => {
@ -140,15 +140,15 @@ describe("Composer", () => {
it("only sends when you press Ctrl+Enter", () => {
// Type a message and press Enter
cy.get('div[contenteditable=true]').type('my message 3');
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
cy.get("div[contenteditable=true]").type("my message 3");
cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" });
// It has not been sent yet
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
cy.contains(".mx_EventTile_body", "my message 3").should("not.exist");
// Press Ctrl+Enter
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
cy.get("div[contenteditable=true]").type("{ctrl+enter}");
// It was sent
cy.contains('.mx_EventTile_body', 'my message 3');
cy.contains(".mx_EventTile_body", "my message 3");
});
});
});

View file

@ -29,7 +29,7 @@ describe("Create Room", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Jim");

View file

@ -28,7 +28,7 @@ interface CryptoTestContext extends Mocha.Context {
}
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
return new Promise<VerificationRequest>(resolve => {
return new Promise<VerificationRequest>((resolve) => {
const onVerificationRequestEvent = (request: VerificationRequest) => {
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
cli.off("crypto.verification.request", onVerificationRequestEvent);
@ -49,7 +49,7 @@ const checkDMRoom = () => {
cy.contains(".mx_RoomView_body .mx_cryptoEvent", "Encryption enabled").should("exist");
};
const startDMWithBob = function(this: CryptoTestContext) {
const startDMWithBob = function (this: CryptoTestContext) {
cy.get('.mx_RoomList [aria-label="Start chat"]').click();
cy.get('[data-testid="invite-dialog-input"]').type(this.bob.getUserId());
cy.contains(".mx_InviteDialog_tile_nameStack_name", "Bob").click();
@ -57,11 +57,13 @@ const startDMWithBob = function(this: CryptoTestContext) {
cy.get(".mx_InviteDialog_goButton").click();
};
const testMessages = function(this: CryptoTestContext) {
const testMessages = function (this: CryptoTestContext) {
// check the invite message
cy.contains(".mx_EventTile_body", "Hey!").closest(".mx_EventTile").within(() => {
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
});
cy.contains(".mx_EventTile_body", "Hey!")
.closest(".mx_EventTile")
.within(() => {
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
});
// Bob sends a response
cy.get<Room>("@bobsRoom").then((room) => {
@ -72,28 +74,30 @@ const testMessages = function(this: CryptoTestContext) {
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
};
const bobJoin = function(this: CryptoTestContext) {
cy.window({ log: false }).then(async win => {
const bobRooms = this.bob.getRooms();
if (!bobRooms.length) {
await new Promise<void>(resolve => {
const onMembership = (_event) => {
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
resolve();
};
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
});
}
}).then(() => {
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
});
const bobJoin = function (this: CryptoTestContext) {
cy.window({ log: false })
.then(async (win) => {
const bobRooms = this.bob.getRooms();
if (!bobRooms.length) {
await new Promise<void>((resolve) => {
const onMembership = (_event) => {
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
resolve();
};
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
});
}
})
.then(() => {
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
});
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
};
/** configure the given MatrixClient to auto-accept any invites */
function autoJoin(client: MatrixClient) {
cy.window({ log: false }).then(async win => {
cy.window({ log: false }).then(async (win) => {
client.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
if (member.membership === "invite" && member.userId === client.getUserId()) {
client.joinRoom(member.roomId);
@ -103,21 +107,23 @@ function autoJoin(client: MatrixClient) {
}
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
const onShowSas = (event: ISasEvent) => {
verifier.off("show_sas", onShowSas);
event.confirm();
verifier.done();
resolve(event.sas.emoji);
};
return cy.wrap(
new Promise<EmojiMapping[]>((resolve) => {
const onShowSas = (event: ISasEvent) => {
verifier.off("show_sas", onShowSas);
event.confirm();
verifier.done();
resolve(event.sas.emoji);
};
const verifier = request.beginKeyVerification("m.sas.v1");
verifier.on("show_sas", onShowSas);
verifier.verify();
}));
const verifier = request.beginKeyVerification("m.sas.v1");
verifier.on("show_sas", onShowSas);
verifier.verify();
}),
);
};
const verify = function(this: CryptoTestContext) {
const verify = function (this: CryptoTestContext) {
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
openRoomInfo().within(() => {
@ -125,14 +131,16 @@ const verify = function(this: CryptoTestContext) {
cy.contains(".mx_EntityTile_name", "Bob").click();
cy.contains(".mx_UserInfo_verifyButton", "Verify").click();
cy.contains(".mx_AccessibleButton", "Start Verification").click();
cy.wrap(bobsVerificationRequestPromise).then((verificationRequest: VerificationRequest) => {
verificationRequest.accept();
return verificationRequest;
}).as("bobsVerificationRequest");
cy.wrap(bobsVerificationRequestPromise)
.then((verificationRequest: VerificationRequest) => {
verificationRequest.accept();
return verificationRequest;
})
.as("bobsVerificationRequest");
cy.contains(".mx_AccessibleButton", "Verify by emoji").click();
cy.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => {
return handleVerificationRequest(request).then((emojis: EmojiMapping[]) => {
cy.get('.mx_VerificationShowSas_emojiSas_block').then((emojiBlocks) => {
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
emojis.forEach((emoji: EmojiMapping, index: number) => {
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
});
@ -145,15 +153,17 @@ const verify = function(this: CryptoTestContext) {
});
};
describe("Cryptography", function() {
beforeEach(function() {
cy.startSynapse("default").as("synapse").then((synapse: SynapseInstance) => {
cy.initTestUser(synapse, "Alice");
cy.getBot(synapse, { displayName: "Bob", autoAcceptInvites: false }).as("bob");
});
describe("Cryptography", function () {
beforeEach(function () {
cy.startSynapse("default")
.as("synapse")
.then((synapse: SynapseInstance) => {
cy.initTestUser(synapse, "Alice");
cy.getBot(synapse, { displayName: "Bob", autoAcceptInvites: false }).as("bob");
});
});
afterEach(function(this: CryptoTestContext) {
afterEach(function (this: CryptoTestContext) {
cy.stopSynapse(this.synapse);
});
@ -172,27 +182,24 @@ describe("Cryptography", function() {
return;
});
it("creating a DM should work, being e2e-encrypted / user verification", function(this: CryptoTestContext) {
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
cy.bootstrapCrossSigning();
startDMWithBob.call(this);
// send first message
cy.get(".mx_BasicMessageComposer_input")
.click()
.should("have.focus")
.type("Hey!{enter}");
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
checkDMRoom();
bobJoin.call(this);
testMessages.call(this);
verify.call(this);
});
it("should allow verification when there is no existing DM", function(this: CryptoTestContext) {
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
cy.bootstrapCrossSigning();
autoJoin(this.bob);
/* we need to have a room with the other user present, so we can open the verification panel */
let roomId: string;
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then(_room1Id => {
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => {
roomId = _room1Id;
cy.log(`Created test room ${roomId}`);
cy.visit(`/#/room/${roomId}`);

View file

@ -24,19 +24,14 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
import Chainable = Cypress.Chainable;
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
return cy.sendEvent(
roomId,
null,
"m.room.message" as EventType,
MessageEvent.from("Message").serialize().content,
);
return cy.sendEvent(roomId, null, "m.room.message" as EventType, MessageEvent.from("Message").serialize().content);
};
describe("Editing", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Edith").then(() => {
cy.injectAxe();
@ -50,7 +45,7 @@ describe("Editing", () => {
});
it("should close the composer when clicking save after making a change and undoing it", () => {
cy.get<string>("@roomId").then(roomId => {
cy.get<string>("@roomId").then((roomId) => {
sendEvent(roomId);
cy.visit("/#/room/" + roomId);
});

View file

@ -77,18 +77,18 @@ describe("Integration Manager: Get OpenID Token", () => {
let integrationManagerUrl: string;
beforeEach(() => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
integrationManagerUrl = url;
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
});
}).then(user => {
}).then((user) => {
testUser = user;
});
@ -107,8 +107,8 @@ describe("Integration Manager: Get OpenID Token", () => {
}).as("integrationManager");
// Succeed when checking the token is valid
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
req.continue(res => {
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
req.continue((res) => {
return res.send(200, {
user_id: testUser.userId,
});
@ -127,16 +127,14 @@ describe("Integration Manager: Get OpenID Token", () => {
});
it("should successfully obtain an openID token", () => {
cy.all([
cy.get<{}>("@integrationManager"),
]).then(() => {
cy.all([cy.get<{}>("@integrationManager")]).then(() => {
cy.viewRoomByName(ROOM_NAME);
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl);
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
cy.get("#message-response").should('include.text', 'access_token');
cy.get("#message-response").should("include.text", "access_token");
});
});
});

View file

@ -87,8 +87,9 @@ function expectKickedMessage(shouldExist: boolean) {
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true });
// Check for the event message (or lack thereof)
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)
.should(shouldExist ? "exist" : "not.exist");
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should(
shouldExist ? "exist" : "not.exist",
);
}
describe("Integration Manager: Kick", () => {
@ -97,18 +98,18 @@ describe("Integration Manager: Kick", () => {
let integrationManagerUrl: string;
beforeEach(() => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
integrationManagerUrl = url;
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
});
}).then(user => {
}).then((user) => {
testUser = user;
});
@ -127,8 +128,8 @@ describe("Integration Manager: Kick", () => {
}).as("integrationManager");
// Succeed when checking the token is valid
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
req.continue(res => {
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
req.continue((res) => {
return res.send(200, {
user_id: testUser.userId,
});
@ -149,103 +150,100 @@ describe("Integration Manager: Kick", () => {
});
it("should kick the target", () => {
cy.all([
cy.get<MatrixClient>("@bob"),
cy.get<string>("@roomId"),
cy.get<{}>("@integrationManager"),
]).then(([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(true);
});
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(true);
},
);
});
it("should not kick the target if lacking permissions", () => {
cy.all([
cy.get<MatrixClient>("@bob"),
cy.get<string>("@roomId"),
cy.get<{}>("@integrationManager"),
]).then(([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
cy.getClient().then(async client => {
await client.sendStateEvent(roomId, 'm.room.power_levels', {
kick: 50,
users: {
[testUser.userId]: 0,
},
});
}).then(() => {
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
});
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
cy.getClient()
.then(async (client) => {
await client.sendStateEvent(roomId, "m.room.power_levels", {
kick: 50,
users: {
[testUser.userId]: 0,
},
});
})
.then(() => {
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
},
);
});
it("should no-op if the target already left", () => {
cy.all([
cy.get<MatrixClient>("@bob"),
cy.get<string>("@roomId"),
cy.get<{}>("@integrationManager"),
]).then(([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist').then(async () => {
await targetUser.leave(roomId);
}).then(() => {
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
});
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`)
.should("exist")
.then(async () => {
await targetUser.leave(roomId);
})
.then(() => {
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
},
);
});
it("should no-op if the target was banned", () => {
cy.all([
cy.get<MatrixClient>("@bob"),
cy.get<string>("@roomId"),
cy.get<{}>("@integrationManager"),
]).then(([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
cy.getClient().then(async client => {
await client.ban(roomId, targetUserId);
}).then(() => {
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
cy.getClient()
.then(async (client) => {
await client.ban(roomId, targetUserId);
})
.then(() => {
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
},
);
});
it("should no-op if the target was never a room member", () => {
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
});
});
it("should no-op if the target was never a room member", () => {
cy.all([
cy.get<MatrixClient>("@bob"),
cy.get<string>("@roomId"),
cy.get<{}>("@integrationManager"),
]).then(([targetUser, roomId]) => {
const targetUserId = targetUser.getUserId();
cy.viewRoomByName(ROOM_NAME);
openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false);
});
},
);
});
});

View file

@ -31,11 +31,11 @@ describe("Lazy Loading", () => {
const charlies: Charly[] = [];
beforeEach(() => {
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Alice");
@ -44,7 +44,7 @@ describe("Lazy Loading", () => {
displayName: "Bob",
startClient: false,
autoAcceptInvites: false,
}).then(_bob => {
}).then((_bob) => {
bob = _bob;
});
@ -54,7 +54,7 @@ describe("Lazy Loading", () => {
displayName,
startClient: false,
autoAcceptInvites: false,
}).then(client => {
}).then((client) => {
charlies[i - 1] = { displayName, client };
});
}
@ -71,15 +71,22 @@ describe("Lazy Loading", () => {
const charlyMsg2 = "how's it going??";
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
cy.window({ log: false }).then(win => {
return cy.wrap(bob.createRoom({
name,
room_alias_name: "lltest",
visibility: win.matrixcs.Visibility.Public,
}).then(r => r.room_id), { log: false }).as("roomId");
cy.window({ log: false }).then((win) => {
return cy
.wrap(
bob
.createRoom({
name,
room_alias_name: "lltest",
visibility: win.matrixcs.Visibility.Public,
})
.then((r) => r.room_id),
{ log: false },
)
.as("roomId");
});
cy.get<string>("@roomId").then(async roomId => {
cy.get<string>("@roomId").then(async (roomId) => {
for (const charly of charlies) {
await charly.client.joinRoom(alias);
}
@ -122,13 +129,13 @@ describe("Lazy Loading", () => {
function checkMemberList(charlies: Charly[]) {
getMemberInMemberlist("Alice").should("exist");
getMemberInMemberlist("Bob").should("exist");
charlies.forEach(charly => {
charlies.forEach((charly) => {
getMemberInMemberlist(charly.displayName).should("exist");
});
}
function checkMemberListLacksCharlies(charlies: Charly[]) {
charlies.forEach(charly => {
charlies.forEach((charly) => {
getMemberInMemberlist(charly.displayName).should("not.exist");
});
}
@ -136,7 +143,7 @@ describe("Lazy Loading", () => {
function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
cy.goOffline();
cy.get<string>("@roomId").then(async roomId => {
cy.get<string>("@roomId").then(async (roomId) => {
for (const charly of charlies) {
await charly.client.joinRoom(alias);
}
@ -163,7 +170,7 @@ describe("Lazy Loading", () => {
joinCharliesWhileAliceIsOffline(charly6to10);
checkMemberList(charly6to10);
cy.get<string>("@roomId").then(async roomId => {
cy.get<string>("@roomId").then(async (roomId) => {
for (const charly of charlies) {
await charly.client.leave(roomId);
}

View file

@ -31,10 +31,10 @@ describe("Location sharing", () => {
};
beforeEach(() => {
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Tom");
@ -47,31 +47,28 @@ describe("Location sharing", () => {
it("sends and displays pin drop location message successfully", () => {
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.visit('/#/room/' + roomId);
cy.visit("/#/room/" + roomId);
});
cy.openMessageComposerOptions().within(() => {
cy.get('[aria-label="Location"]').click();
});
selectLocationShareTypeOption('Pin').click();
selectLocationShareTypeOption("Pin").click();
cy.get('#mx_LocationPicker_map').click('center');
cy.get("#mx_LocationPicker_map").click("center");
submitShareLocation();
cy.get(".mx_RoomView_body .mx_EventTile .mx_MLocationBody", { timeout: 10000 })
.should('exist')
.click();
cy.get(".mx_RoomView_body .mx_EventTile .mx_MLocationBody", { timeout: 10000 }).should("exist").click();
// clicking location tile opens maximised map
cy.get('.mx_LocationViewDialog_wrapper').should('exist');
cy.get(".mx_LocationViewDialog_wrapper").should("exist");
cy.get('[aria-label="Close dialog"]').click();
cy.get('.mx_Marker')
.should('exist');
cy.get(".mx_Marker").should("exist");
});
});

View file

@ -24,7 +24,7 @@ describe("Consent", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("consent").then(data => {
cy.startSynapse("consent").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Bob");
@ -37,7 +37,7 @@ describe("Consent", () => {
it("should prompt the user to consent to terms when server deems it necessary", () => {
// Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN`
cy.window().then(win => {
cy.window().then((win) => {
win.mxMatrixClientPeg.matrixClient.createRoom({}).catch(() => {});
// Stub `window.open` - clicking the primary button below will call it
@ -50,7 +50,7 @@ describe("Consent", () => {
cy.get(".mx_Dialog_primary").click();
});
cy.get<SinonStub>("@windowOpen").then(stub => {
cy.get<SinonStub>("@windowOpen").then((stub) => {
const url = stub.getCall(0).args[0];
// Go to Synapse's consent page and accept it

View file

@ -34,7 +34,7 @@ describe("Login", () => {
const password = "p4s5W0rD";
beforeEach(() => {
cy.startSynapse("consent").then(data => {
cy.startSynapse("consent").then((data) => {
synapse = data;
cy.registerUser(synapse, username, password);
cy.visit("/#/login");
@ -52,19 +52,19 @@ describe("Login", () => {
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
cy.get(".mx_ServerPickerDialog_continue").click();
// wait for the dialog to go away
cy.get('.mx_ServerPickerDialog').should('not.exist');
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.get("#mx_LoginForm_username").type(username);
cy.get("#mx_LoginForm_password").type(password);
cy.get(".mx_Login_submit").click();
cy.url().should('contain', '/#/home', { timeout: 30000 });
cy.url().should("contain", "/#/home", { timeout: 30000 });
});
});
describe("logout", () => {
beforeEach(() => {
cy.startSynapse("consent").then(data => {
cy.startSynapse("consent").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Erin");
});

View file

@ -33,17 +33,17 @@ describe("Polls", () => {
};
const createPoll = ({ title, options }: CreatePollOptions) => {
if (options.length < 2) {
throw new Error('Poll must have at least two options');
throw new Error("Poll must have at least two options");
}
cy.get('.mx_PollCreateDialog').within((pollCreateDialog) => {
cy.get('#poll-topic-input').type(title);
cy.get(".mx_PollCreateDialog").within((pollCreateDialog) => {
cy.get("#poll-topic-input").type(title);
options.forEach((option, index) => {
const optionId = `#pollcreate_option_${index}`;
// click 'add option' button if needed
if (pollCreateDialog.find(optionId).length === 0) {
cy.get('.mx_PollCreateDialog_addOption').scrollIntoView().click();
cy.get(".mx_PollCreateDialog_addOption").scrollIntoView().click();
}
cy.get(optionId).scrollIntoView().type(option);
});
@ -56,34 +56,32 @@ describe("Polls", () => {
};
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
return getPollTile(pollId).contains('.mx_MPollBody_option .mx_StyledRadioButton', optionText);
return getPollTile(pollId).contains(".mx_MPollBody_option .mx_StyledRadioButton", optionText);
};
const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => {
getPollOption(pollId, optionText).within(() => {
cy.get('.mx_MPollBody_optionVoteCount').should('contain', `${votes} vote`);
cy.get(".mx_MPollBody_optionVoteCount").should("contain", `${votes} vote`);
});
};
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
getPollOption(pollId, optionText).within(ref => {
cy.get('input[type="radio"]').invoke('attr', 'value').then(optionId => {
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
bot.sendEvent(
roomId,
pollVote.type,
pollVote.content,
);
});
getPollOption(pollId, optionText).within((ref) => {
cy.get('input[type="radio"]')
.invoke("attr", "value")
.then((optionId) => {
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
bot.sendEvent(roomId, pollVote.type, pollVote.content);
});
});
};
beforeEach(() => {
cy.enableLabsFeature("feature_thread");
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Tom");
@ -96,15 +94,15 @@ describe("Polls", () => {
it("should be creatable and votable", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
cy.visit('/#/room/' + roomId);
cy.visit("/#/room/" + roomId);
// wait until Bob joined
cy.contains(".mx_TextualEvent", "BotBob joined the room").should("exist");
});
@ -113,34 +111,35 @@ describe("Polls", () => {
cy.get('[aria-label="Poll"]').click();
});
cy.get('.mx_CompoundDialog').percySnapshotElement('Polls Composer');
cy.get(".mx_CompoundDialog").percySnapshotElement("Polls Composer");
const pollParams = {
title: 'Does the polls feature work?',
options: ['Yes', 'No', 'Maybe'],
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"],
};
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
.invoke("attr", "data-scroll-tokens")
.as("pollId");
cy.get<string>("@pollId").then(pollId => {
getPollTile(pollId).percySnapshotElement('Polls Timeline tile - no votes', { percyCSS: hideTimestampCSS });
cy.get<string>("@pollId").then((pollId) => {
getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hideTimestampCSS });
// Bot votes 'Maybe' in the poll
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
// no votes shown until I vote, check bots vote has arrived
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
cy.get(".mx_MPollBody_totalVotes").should("contain", "1 vote cast");
// vote 'Maybe'
getPollOption(pollId, pollParams.options[2]).click('topLeft');
getPollOption(pollId, pollParams.options[2]).click("topLeft");
// both me and bot have voted Maybe
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
// change my vote to 'Yes'
getPollOption(pollId, pollParams.options[0]).click('topLeft');
getPollOption(pollId, pollParams.options[0]).click("topLeft");
// 1 vote for yes
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
@ -161,15 +160,15 @@ describe("Polls", () => {
it("should be editable from context menu if no votes have been cast", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
cy.visit('/#/room/' + roomId);
cy.visit("/#/room/" + roomId);
});
cy.openMessageComposerOptions().within(() => {
@ -177,40 +176,42 @@ describe("Polls", () => {
});
const pollParams = {
title: 'Does the polls feature work?',
options: ['Yes', 'No', 'Maybe'],
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"],
};
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
cy.get(".mx_RoomView_body .mx_EventTile")
.contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens")
.as("pollId");
cy.get<string>("@pollId").then(pollId => {
cy.get<string>("@pollId").then((pollId) => {
// Open context menu
getPollTile(pollId).rightclick();
// Select edit item
cy.get('.mx_ContextualMenu').within(() => {
cy.get(".mx_ContextualMenu").within(() => {
cy.get('[aria-label="Edit"]').click();
});
// Expect poll editing dialog
cy.get('.mx_PollCreateDialog');
cy.get(".mx_PollCreateDialog");
});
});
it("should not be editable from context menu if votes have been cast", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
cy.visit('/#/room/' + roomId);
cy.visit("/#/room/" + roomId);
});
cy.openMessageComposerOptions().within(() => {
@ -218,51 +219,53 @@ describe("Polls", () => {
});
const pollParams = {
title: 'Does the polls feature work?',
options: ['Yes', 'No', 'Maybe'],
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"],
};
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
cy.get(".mx_RoomView_body .mx_EventTile")
.contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens")
.as("pollId");
cy.get<string>("@pollId").then(pollId => {
cy.get<string>("@pollId").then((pollId) => {
// Bot votes 'Maybe' in the poll
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
// wait for bot's vote to arrive
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
cy.get(".mx_MPollBody_totalVotes").should("contain", "1 vote cast");
// Open context menu
getPollTile(pollId).rightclick();
// Select edit item
cy.get('.mx_ContextualMenu').within(() => {
cy.get(".mx_ContextualMenu").within(() => {
cy.get('[aria-label="Edit"]').click();
});
// Expect error dialog
cy.get('.mx_ErrorDialog');
cy.get(".mx_ErrorDialog");
});
});
it("should be displayed correctly in thread panel", () => {
let botBob: MatrixClient;
let botCharlie: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
botBob = _bot;
});
cy.getBot(synapse, { displayName: "BotCharlie" }).then(_bot => {
cy.getBot(synapse, { displayName: "BotCharlie" }).then((_bot) => {
botCharlie = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.inviteUser(roomId, botBob.getUserId());
cy.inviteUser(roomId, botCharlie.getUserId());
cy.visit('/#/room/' + roomId);
cy.visit("/#/room/" + roomId);
// wait until the bots joined
cy.contains(".mx_TextualEvent", "and one other were invited and joined").should("exist");
});
@ -272,16 +275,17 @@ describe("Polls", () => {
});
const pollParams = {
title: 'Does the polls feature work?',
options: ['Yes', 'No', 'Maybe'],
title: "Does the polls feature work?",
options: ["Yes", "No", "Maybe"],
};
createPoll(pollParams);
// Wait for message to send, get its ID and save as @pollId
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
.invoke("attr", "data-scroll-tokens").as("pollId");
.invoke("attr", "data-scroll-tokens")
.as("pollId");
cy.get<string>("@pollId").then(pollId => {
cy.get<string>("@pollId").then((pollId) => {
// Bob starts thread on the poll
botBob.sendMessage(roomId, pollId, {
body: "Hello there",
@ -297,22 +301,22 @@ describe("Polls", () => {
botVoteForOption(botCharlie, roomId, pollId, pollParams.options[1]);
// no votes shown until I vote, check votes have arrived in main tl
cy.get('.mx_RoomView_body .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
cy.get(".mx_RoomView_body .mx_MPollBody_totalVotes").should("contain", "2 votes cast");
// and thread view
cy.get('.mx_ThreadView .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
cy.get(".mx_ThreadView .mx_MPollBody_totalVotes").should("contain", "2 votes cast");
cy.get('.mx_RoomView_body').within(() => {
cy.get(".mx_RoomView_body").within(() => {
// vote 'Maybe' in the main timeline poll
getPollOption(pollId, pollParams.options[2]).click('topLeft');
getPollOption(pollId, pollParams.options[2]).click("topLeft");
// both me and bob have voted Maybe
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
});
cy.get('.mx_ThreadView').within(() => {
cy.get(".mx_ThreadView").within(() => {
// votes updated in thread view too
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
// change my vote to 'Yes'
getPollOption(pollId, pollParams.options[0]).click('topLeft');
getPollOption(pollId, pollParams.options[0]).click("topLeft");
});
// Bob updates vote to 'No'
@ -329,11 +333,11 @@ describe("Polls", () => {
};
// check counts are correct in main timeline tile
cy.get('.mx_RoomView_body').within(() => {
cy.get(".mx_RoomView_body").within(() => {
expectVoteCounts();
});
// and in thread view tile
cy.get('.mx_ThreadView').within(() => {
cy.get(".mx_ThreadView").within(() => {
expectVoteCounts();
});
});

View file

@ -24,7 +24,7 @@ describe("Registration", () => {
beforeEach(() => {
cy.stubDefaultServer();
cy.visit("/#/register");
cy.startSynapse("consent").then(data => {
cy.startSynapse("consent").then((data) => {
synapse = data;
});
});
@ -45,7 +45,7 @@ describe("Registration", () => {
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
cy.get(".mx_ServerPickerDialog_continue").click();
// wait for the dialog to go away
cy.get('.mx_ServerPickerDialog').should('not.exist');
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.get("#mx_RegistrationForm_username").should("be.visible");
// Hide the server text as it contains the randomly allocated Synapse port
@ -75,12 +75,14 @@ describe("Registration", () => {
cy.checkA11y();
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
cy.url().should('contain', '/#/home');
cy.url().should("contain", "/#/home");
cy.get('[aria-label="User menu"]').click();
cy.get('[aria-label="Security & Privacy"]').click();
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon")
.should("have.class", "mx_E2EIcon_verified");
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should(
"have.class",
"mx_E2EIcon_verified",
);
});
it("should require username to fulfil requirements and be available", () => {
@ -89,7 +91,7 @@ describe("Registration", () => {
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
cy.get(".mx_ServerPickerDialog_continue").click();
// wait for the dialog to go away
cy.get('.mx_ServerPickerDialog').should('not.exist');
cy.get(".mx_ServerPickerDialog").should("not.exist");
cy.get("#mx_RegistrationForm_username").should("be.visible");

View file

@ -22,7 +22,7 @@ describe("Pills", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Sally");
@ -33,7 +33,7 @@ describe("Pills", () => {
cy.stopSynapse(synapse);
});
it('should navigate clicks internally to the app', () => {
it("should navigate clicks internally to the app", () => {
const messageRoom = "Send Messages Here";
const targetLocalpart = "aliasssssssssssss";
cy.createRoom({
@ -43,34 +43,35 @@ describe("Pills", () => {
cy.createRoom({
name: messageRoom,
}).as("messageRoomId");
cy.all([
cy.get<string>("@targetRoomId"),
cy.get<string>("@messageRoomId"),
]).then(([targetRoomId, messageRoomId]) => { // discard the target room ID - we don't need it
cy.viewRoomByName(messageRoom);
cy.url().should("contain", `/#/room/${messageRoomId}`);
cy.all([cy.get<string>("@targetRoomId"), cy.get<string>("@messageRoomId")]).then(
([targetRoomId, messageRoomId]) => {
// discard the target room ID - we don't need it
cy.viewRoomByName(messageRoom);
cy.url().should("contain", `/#/room/${messageRoomId}`);
// send a message using the built-in room mention functionality (autocomplete)
cy.get(".mx_SendMessageComposer .mx_BasicMessageComposer_input")
.type(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
cy.get(".mx_Autocomplete_Completion_title").click();
cy.get(".mx_MessageComposer_sendMessage").click();
// send a message using the built-in room mention functionality (autocomplete)
cy.get(".mx_SendMessageComposer .mx_BasicMessageComposer_input").type(
`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`,
);
cy.get(".mx_Autocomplete_Completion_title").click();
cy.get(".mx_MessageComposer_sendMessage").click();
// find the pill in the timeline and click it
cy.get(".mx_EventTile_body .mx_Pill").click();
// find the pill in the timeline and click it
cy.get(".mx_EventTile_body .mx_Pill").click();
const localUrl = `/#/room/#${targetLocalpart}:`;
// verify we landed at a sane place
cy.url().should("contain", localUrl);
const localUrl = `/#/room/#${targetLocalpart}:`;
// verify we landed at a sane place
cy.url().should("contain", localUrl);
cy.wait(250); // let the room list settle
cy.wait(250); // let the room list settle
// go back to the message room and try to click on the pill text, as a user would
cy.viewRoomByName(messageRoom);
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
.should("have.css", "pointer-events", "none")
.click({ force: true }); // force is to ensure we bypass pointer-events
cy.url().should("contain", localUrl);
});
// go back to the message room and try to click on the pill text, as a user would
cy.viewRoomByName(messageRoom);
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
.should("have.css", "pointer-events", "none")
.click({ force: true }); // force is to ensure we bypass pointer-events
cy.url().should("contain", localUrl);
},
);
});
});

View file

@ -46,7 +46,7 @@ describe("RightPanel", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, NAME).then(() =>
cy.window({ log: false }).then(() => {

View file

@ -23,7 +23,7 @@ describe("Room Directory", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Ray");
@ -36,7 +36,7 @@ describe("Room Directory", () => {
});
it("should allow admin to add alias & publish room to directory", () => {
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
cy.createRoom({
name: "Gaming",
preset: win.matrixcs.Preset.PublicChat,
@ -56,16 +56,14 @@ describe("Room Directory", () => {
// Publish into the public rooms directory
cy.contains(".mx_SettingsFieldset", "Published Addresses").within(() => {
cy.get("#canonicalAlias").find(":selected").should("contain", "#gaming:localhost");
cy.get(`[aria-label="Publish this room to the public in localhost's room directory?"]`).click()
cy.get(`[aria-label="Publish this room to the public in localhost's room directory?"]`)
.click()
.should("have.attr", "aria-checked", "true");
});
cy.closeDialog();
cy.all([
cy.get<MatrixClient>("@bot"),
cy.get<string>("@roomId"),
]).then(async ([bot, roomId]) => {
cy.all([cy.get<MatrixClient>("@bot"), cy.get<string>("@roomId")]).then(async ([bot, roomId]) => {
const resp = await bot.publicRooms({});
expect(resp.total_room_count_estimate).to.equal(1);
expect(resp.chunk).to.have.length(1);
@ -75,10 +73,7 @@ describe("Room Directory", () => {
it("should allow finding published rooms in directory", () => {
const name = "This is a public room";
cy.all([
cy.window({ log: false }),
cy.get<MatrixClient>("@bot"),
]).then(([win, bot]) => {
cy.all([cy.window({ log: false }), cy.get<MatrixClient>("@bot")]).then(([win, bot]) => {
bot.createRoom({
visibility: win.matrixcs.Visibility.Public,
name,
@ -89,16 +84,17 @@ describe("Room Directory", () => {
cy.get('[role="button"][aria-label="Explore rooms"]').click();
cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("Unknown Room");
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_otherSearches_messageSearchText")
.should("contain", "can't find the room you're looking for");
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_otherSearches_messageSearchText").should(
"contain",
"can't find the room you're looking for",
);
cy.get(".mx_SpotlightDialog_wrapper").percySnapshotElement("Room Directory - filtered no results");
cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("{selectAll}{backspace}test1234");
cy.contains(".mx_SpotlightDialog .mx_SpotlightDialog_result_publicRoomName", name)
.should("exist");
cy.contains(".mx_SpotlightDialog .mx_SpotlightDialog_result_publicRoomName", name).should("exist");
cy.get(".mx_SpotlightDialog_wrapper").percySnapshotElement("Room Directory - filtered one result");
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_option").find(".mx_AccessibleButton").contains("Join").click();
cy.url().should('contain', `/#/room/#test1234:localhost`);
cy.url().should("contain", `/#/room/#test1234:localhost`);
});
});

View file

@ -25,17 +25,20 @@ describe("Device manager", () => {
beforeEach(() => {
cy.enableLabsFeature("feature_new_device_manager");
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Alice").then(credentials => {
user = credentials;
}).then(() => {
// create some extra sessions to manage
return cy.loginUser(synapse, user.username, user.password);
}).then(() => {
return cy.loginUser(synapse, user.username, user.password);
});
cy.initTestUser(synapse, "Alice")
.then((credentials) => {
user = credentials;
})
.then(() => {
// create some extra sessions to manage
return cy.loginUser(synapse, user.username, user.password);
})
.then(() => {
return cy.loginUser(synapse, user.username, user.password);
});
});
});
@ -45,72 +48,74 @@ describe("Device manager", () => {
it("should display sessions", () => {
cy.openUserSettings("Sessions");
cy.contains('Current session').should('exist');
cy.contains("Current session").should("exist");
cy.get('[data-testid="current-session-section"]').within(() => {
cy.contains('Unverified session').should('exist');
cy.contains("Unverified session").should("exist");
});
// current session details opened
cy.get('[data-testid="current-session-toggle-details"]').click();
cy.contains('Session details').should('exist');
cy.contains("Session details").should("exist");
// close current session details
cy.get('[data-testid="current-session-toggle-details"]').click();
cy.contains('Session details').should('not.exist');
cy.contains("Session details").should("not.exist");
cy.get('[data-testid="security-recommendations-section"]').within(() => {
cy.contains('Security recommendations').should('exist');
cy.get('[data-testid="unverified-devices-cta"]').should('have.text', 'View all (3)').click();
cy.contains("Security recommendations").should("exist");
cy.get('[data-testid="unverified-devices-cta"]').should("have.text", "View all (3)").click();
});
/**
* Other sessions section
*/
cy.contains('Other sessions').should('exist');
cy.contains("Other sessions").should("exist");
// filter applied after clicking through from security recommendations
cy.get('[aria-label="Filter devices"]').should('have.text', 'Show: Unverified');
cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 3);
cy.get('[aria-label="Filter devices"]').should("have.text", "Show: Unverified");
cy.get(".mx_FilteredDeviceList_list").find(".mx_FilteredDeviceList_listItem").should("have.length", 3);
// select two sessions
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').first().click();
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').last().click();
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox").first().click();
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox").last().click();
// sign out from list selection action buttons
cy.get('[data-testid="sign-out-selection-cta"]').click();
cy.get('[data-testid="dialog-primary-button"]').click();
// list updated after sign out
cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 1);
cy.get(".mx_FilteredDeviceList_list").find(".mx_FilteredDeviceList_listItem").should("have.length", 1);
// security recommendation count updated
cy.get('[data-testid="unverified-devices-cta"]').should('have.text', 'View all (1)');
cy.get('[data-testid="unverified-devices-cta"]').should("have.text", "View all (1)");
const sessionName = `Alice's device`;
// open the first session
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem').first().within(() => {
cy.get('[aria-label="Show details"]').click();
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem")
.first()
.within(() => {
cy.get('[aria-label="Show details"]').click();
cy.contains('Session details').should('exist');
cy.contains("Session details").should("exist");
cy.get('[data-testid="device-heading-rename-cta"]').click();
cy.get('[data-testid="device-rename-input"]').type(sessionName);
cy.get('[data-testid="device-rename-submit-cta"]').click();
// there should be a spinner while device updates
cy.get(".mx_Spinner").should("exist");
// wait for spinner to complete
cy.get(".mx_Spinner").should("not.exist");
cy.get('[data-testid="device-heading-rename-cta"]').click();
cy.get('[data-testid="device-rename-input"]').type(sessionName);
cy.get('[data-testid="device-rename-submit-cta"]').click();
// there should be a spinner while device updates
cy.get(".mx_Spinner").should("exist");
// wait for spinner to complete
cy.get(".mx_Spinner").should("not.exist");
// session name updated in details
cy.get('.mx_DeviceDetailHeading h3').should('have.text', sessionName);
// and main list item
cy.get('.mx_DeviceTile h4').should('have.text', sessionName);
// session name updated in details
cy.get(".mx_DeviceDetailHeading h3").should("have.text", sessionName);
// and main list item
cy.get(".mx_DeviceTile h4").should("have.text", sessionName);
// sign out using the device details sign out
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
});
// sign out using the device details sign out
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
});
// confirm the signout
cy.get('[data-testid="dialog-primary-button"]').click();
// no other sessions or security recommendations sections when only one session
cy.contains('Other sessions').should('not.exist');
cy.get('[data-testid="security-recommendations-section"]').should('not.exist');
cy.contains("Other sessions").should("not.exist");
cy.get('[data-testid="security-recommendations-section"]').should("not.exist");
});
});

View file

@ -21,7 +21,7 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
function seedLabs(synapse: SynapseInstance, labsVal: boolean | null): void {
cy.initTestUser(synapse, "Sally", () => {
// seed labs flag
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
if (typeof labsVal === "boolean") {
// stringify boolean
win.localStorage.setItem("mx_labs_feature_feature_hidden_read_receipts", `${labsVal}`);
@ -64,7 +64,7 @@ describe("Hidden Read Receipts Setting Migration", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
});
});
@ -73,17 +73,17 @@ describe("Hidden Read Receipts Setting Migration", () => {
cy.stopSynapse(synapse);
});
it('should not migrate the lack of a labs flag', () => {
it("should not migrate the lack of a labs flag", () => {
seedLabs(synapse, null);
testForVal(null);
});
it('should migrate labsHiddenRR=false as sendRR=true', () => {
it("should migrate labsHiddenRR=false as sendRR=true", () => {
seedLabs(synapse, false);
testForVal(true);
});
it('should migrate labsHiddenRR=true as sendRR=false', () => {
it("should migrate labsHiddenRR=true as sendRR=false", () => {
seedLabs(synapse, true);
testForVal(false);
});

View file

@ -26,18 +26,17 @@ import { ProxyInstance } from "../../plugins/sliding-sync";
describe("Sliding Sync", () => {
beforeEach(() => {
cy.startSynapse("default").as("synapse").then(synapse => {
cy.startProxy(synapse).as("proxy");
});
cy.startSynapse("default")
.as("synapse")
.then((synapse) => {
cy.startProxy(synapse).as("proxy");
});
cy.all([
cy.get<SynapseInstance>("@synapse"),
cy.get<ProxyInstance>("@proxy"),
]).then(([synapse, proxy]) => {
cy.all([cy.get<SynapseInstance>("@synapse"), cy.get<ProxyInstance>("@proxy")]).then(([synapse, proxy]) => {
cy.enableLabsFeature("feature_sliding_sync");
cy.intercept("/config.json?cachebuster=*", req => {
return req.continue(res => {
cy.intercept("/config.json?cachebuster=*", (req) => {
return req.continue((res) => {
res.send(200, {
...res.body,
setting_defaults: {
@ -62,11 +61,16 @@ describe("Sliding Sync", () => {
// assert order
const checkOrder = (wantOrder: string[]) => {
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomTile_title").should((elements) => {
expect(_.map(elements, (e) => {
return e.textContent;
}), "rooms are sorted").to.deep.equal(wantOrder);
});
cy.contains(".mx_RoomSublist", "Rooms")
.find(".mx_RoomTile_title")
.should((elements) => {
expect(
_.map(elements, (e) => {
return e.textContent;
}),
"rooms are sorted",
).to.deep.equal(wantOrder);
});
};
const bumpRoom = (alias: string) => {
// Send a message into the given room, this should bump the room to the top
@ -80,9 +84,11 @@ describe("Sliding Sync", () => {
const createAndJoinBob = () => {
// create a Bob user
cy.get<SynapseInstance>("@synapse").then((synapse) => {
return cy.getBot(synapse, {
displayName: "Bob",
}).as("bob");
return cy
.getBot(synapse, {
displayName: "Bob",
})
.as("bob");
});
// invite Bob to Test Room and accept then send a message.
@ -95,7 +101,7 @@ describe("Sliding Sync", () => {
// sanity check everything works
it("should correctly render expected messages", () => {
cy.get<string>("@roomId").then(roomId => cy.visit("/#/room/" + roomId));
cy.get<string>("@roomId").then((roomId) => cy.visit("/#/room/" + roomId));
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
@ -114,54 +120,52 @@ describe("Sliding Sync", () => {
cy.createRoom({ name: "Pineapple" }).then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" }).then(() => cy.contains(".mx_RoomSublist", "Orange"));
// check the rooms are in the right order
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
checkOrder([
"Orange", "Pineapple", "Apple", "Test Room",
]);
cy.get(".mx_RoomTile").should("have.length", 4); // due to the Test Room in beforeEach
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomSublist_menuButton").click({ force: true });
cy.contains("A-Z").click();
cy.get('.mx_StyledRadioButton_checked').should("contain.text", "A-Z");
checkOrder([
"Apple", "Orange", "Pineapple", "Test Room",
]);
cy.get(".mx_StyledRadioButton_checked").should("contain.text", "A-Z");
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
});
it("should move rooms around as new events arrive", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
cy.createRoom({ name: "Apple" })
.as("roomA")
.then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" })
.as("roomP")
.then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" })
.as("roomO")
.then(() => cy.contains(".mx_RoomSublist", "Orange"));
// Select the Test Room
cy.contains(".mx_RoomTile", "Test Room").click();
checkOrder([
"Orange", "Pineapple", "Apple", "Test Room",
]);
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
bumpRoom("@roomA");
checkOrder([
"Apple", "Orange", "Pineapple", "Test Room",
]);
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
bumpRoom("@roomO");
checkOrder([
"Orange", "Apple", "Pineapple", "Test Room",
]);
checkOrder(["Orange", "Apple", "Pineapple", "Test Room"]);
bumpRoom("@roomO");
checkOrder([
"Orange", "Apple", "Pineapple", "Test Room",
]);
checkOrder(["Orange", "Apple", "Pineapple", "Test Room"]);
bumpRoom("@roomP");
checkOrder([
"Pineapple", "Orange", "Apple", "Test Room",
]);
checkOrder(["Pineapple", "Orange", "Apple", "Test Room"]);
});
it("should not move the selected room: it should be sticky", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
cy.createRoom({ name: "Apple" })
.as("roomA")
.then(() => cy.contains(".mx_RoomSublist", "Apple"));
cy.createRoom({ name: "Pineapple" })
.as("roomP")
.then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
cy.createRoom({ name: "Orange" })
.as("roomO")
.then(() => cy.contains(".mx_RoomSublist", "Orange"));
// Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
// turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically
@ -169,23 +173,17 @@ describe("Sliding Sync", () => {
// Select the Pineapple room
cy.contains(".mx_RoomTile", "Pineapple").click();
checkOrder([
"Orange", "Pineapple", "Apple", "Test Room",
]);
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
// Move Apple
bumpRoom("@roomA");
checkOrder([
"Apple", "Pineapple", "Orange", "Test Room",
]);
checkOrder(["Apple", "Pineapple", "Orange", "Test Room"]);
// Select the Test Room
cy.contains(".mx_RoomTile", "Test Room").click();
// the rooms reshuffle to match reality
checkOrder([
"Apple", "Orange", "Pineapple", "Test Room",
]);
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
});
it("should show the right unread notifications", () => {
@ -212,7 +210,8 @@ describe("Sliding Sync", () => {
cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count");
});
it("should not show unread indicators", () => { // TODO: for now. Later we should.
it("should not show unread indicators", () => {
// TODO: for now. Later we should.
createAndJoinBob();
// disable notifs in this room (TODO: CS API call?)
@ -223,17 +222,13 @@ describe("Sliding Sync", () => {
cy.createRoom({
name: "Dummy",
});
checkOrder([
"Dummy", "Test Room",
]);
checkOrder(["Dummy", "Test Room"]);
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
return bob.sendTextMessage(roomId, "Do you read me?");
});
// wait for this message to arrive, tell by the room list resorting
checkOrder([
"Test Room", "Dummy",
]);
checkOrder(["Test Room", "Dummy"]);
cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist");
});
@ -242,13 +237,18 @@ describe("Sliding Sync", () => {
cy.get(".mx_UserMenu_userAvatar").click();
cy.contains("All settings").click();
cy.contains("Preferences").click();
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
".mx_ToggleSwitch_on").should("not.exist");
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
".mx_ToggleSwitch_ball").click();
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 }).should("exist").find(
".mx_ToggleSwitch_on", { timeout: 2000 },
).should("exist");
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format")
.should("exist")
.find(".mx_ToggleSwitch_on")
.should("not.exist");
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format")
.should("exist")
.find(".mx_ToggleSwitch_ball")
.click();
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 })
.should("exist")
.find(".mx_ToggleSwitch_on", { timeout: 2000 })
.should("exist");
});
it("should show and be able to accept/reject/rescind invites", () => {
@ -263,50 +263,56 @@ describe("Sliding Sync", () => {
// - roomJoin: will join this room
// - roomReject: will reject the invite
// - roomRescind: will make Bob rescind the invite
let roomJoin; let roomReject; let roomRescind; let bobClient;
cy.get<MatrixClient>("@bob").then((bob) => {
bobClient = bob;
return Promise.all([
bob.createRoom({ name: "Join" }),
bob.createRoom({ name: "Reject" }),
bob.createRoom({ name: "Rescind" }),
]);
}).then(([join, reject, rescind]) => {
roomJoin = join.room_id;
roomReject = reject.room_id;
roomRescind = rescind.room_id;
return Promise.all([
bobClient.invite(roomJoin, clientUserId),
bobClient.invite(roomReject, clientUserId),
bobClient.invite(roomRescind, clientUserId),
]);
});
let roomJoin;
let roomReject;
let roomRescind;
let bobClient;
cy.get<MatrixClient>("@bob")
.then((bob) => {
bobClient = bob;
return Promise.all([
bob.createRoom({ name: "Join" }),
bob.createRoom({ name: "Reject" }),
bob.createRoom({ name: "Rescind" }),
]);
})
.then(([join, reject, rescind]) => {
roomJoin = join.room_id;
roomReject = reject.room_id;
roomRescind = rescind.room_id;
return Promise.all([
bobClient.invite(roomJoin, clientUserId),
bobClient.invite(roomReject, clientUserId),
bobClient.invite(roomRescind, clientUserId),
]);
});
// wait for them all to be on the UI
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
cy.get(".mx_RoomTile").should("have.length", 4); // due to the Test Room in beforeEach
cy.contains(".mx_RoomTile", "Join").click();
cy.contains(".mx_AccessibleButton", "Accept").click();
checkOrder([
"Join", "Test Room",
]);
checkOrder(["Join", "Test Room"]);
cy.contains(".mx_RoomTile", "Reject").click();
cy.contains(".mx_RoomView .mx_AccessibleButton", "Reject").click();
// wait for the rejected room to disappear
cy.get(".mx_RoomTile").should('have.length', 3);
cy.get(".mx_RoomTile").should("have.length", 3);
// check the lists are correct
checkOrder([
"Join", "Test Room",
]);
cy.contains(".mx_RoomSublist", "Invites").find(".mx_RoomTile_title").should((elements) => {
expect(_.map(elements, (e) => {
return e.textContent;
}), "rooms are sorted").to.deep.equal(["Rescind"]);
});
checkOrder(["Join", "Test Room"]);
cy.contains(".mx_RoomSublist", "Invites")
.find(".mx_RoomTile_title")
.should((elements) => {
expect(
_.map(elements, (e) => {
return e.textContent;
}),
"rooms are sorted",
).to.deep.equal(["Rescind"]);
});
// now rescind the invite
cy.get<MatrixClient>("@bob").then((bob) => {
@ -314,19 +320,19 @@ describe("Sliding Sync", () => {
});
// wait for the rescind to take effect and check the joined list once more
cy.get(".mx_RoomTile").should('have.length', 2);
checkOrder([
"Join", "Test Room",
]);
cy.get(".mx_RoomTile").should("have.length", 2);
checkOrder(["Join", "Test Room"]);
});
it("should show a favourite DM only in the favourite sublist", () => {
cy.createRoom({
name: "Favourite DM",
is_direct: true,
}).as("room").then(roomId => {
cy.getClient().then(cli => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
});
})
.as("room")
.then((roomId) => {
cy.getClient().then((cli) => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
});
cy.contains('.mx_RoomSublist[aria-label="Favourites"] .mx_RoomTile', "Favourite DM").should("exist");
cy.contains('.mx_RoomSublist[aria-label="People"] .mx_RoomTile', "Favourite DM").should("not.exist");
@ -335,7 +341,9 @@ describe("Sliding Sync", () => {
// Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.
// This ensures we are setting RoomViewStore state correctly.
it("should clear the reply to field when swapping rooms", () => {
cy.createRoom({ name: "Other Room" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Other Room"));
cy.createRoom({ name: "Other Room" })
.as("roomA")
.then(() => cy.contains(".mx_RoomSublist", "Other Room"));
cy.get<string>("@roomId").then((roomId) => {
return cy.sendEvent(roomId, null, "m.room.message", {
body: "Hello world",
@ -346,9 +354,9 @@ describe("Sliding Sync", () => {
cy.contains(".mx_RoomTile", "Test Room").click();
cy.get(".mx_ReplyPreview").should("not.exist");
// click reply-to on the Hello World message
cy.contains(".mx_EventTile", "Hello world").find('.mx_AccessibleButton[aria-label="Reply"]').click(
{ force: true },
);
cy.contains(".mx_EventTile", "Hello world")
.find('.mx_AccessibleButton[aria-label="Reply"]')
.click({ force: true });
// check it's visible
cy.get(".mx_ReplyPreview").should("exist");
// now click Other Room
@ -365,28 +373,31 @@ describe("Sliding Sync", () => {
it("should not cancel replies when permalinks are clicked ", () => {
cy.get<string>("@roomId").then((roomId) => {
// we require a first message as you cannot click the permalink text with the avatar in the way
return cy.sendEvent(roomId, null, "m.room.message", {
body: "First message",
msgtype: "m.text",
}).then(() => {
return cy.sendEvent(roomId, null, "m.room.message", {
body: "Permalink me",
return cy
.sendEvent(roomId, null, "m.room.message", {
body: "First message",
msgtype: "m.text",
})
.then(() => {
return cy.sendEvent(roomId, null, "m.room.message", {
body: "Permalink me",
msgtype: "m.text",
});
})
.then(() => {
cy.sendEvent(roomId, null, "m.room.message", {
body: "Reply to me",
msgtype: "m.text",
});
});
}).then(() => {
cy.sendEvent(roomId, null, "m.room.message", {
body: "Reply to me",
msgtype: "m.text",
});
});
});
// select the room
cy.contains(".mx_RoomTile", "Test Room").click();
cy.get(".mx_ReplyPreview").should("not.exist");
// click reply-to on the Reply to me message
cy.contains(".mx_EventTile", "Reply to me").find('.mx_AccessibleButton[aria-label="Reply"]').click(
{ force: true },
);
cy.contains(".mx_EventTile", "Reply to me")
.find('.mx_AccessibleButton[aria-label="Reply"]')
.click({ force: true });
// check it's visible
cy.get(".mx_ReplyPreview").should("exist");
// now click on the permalink for Permalink me

View file

@ -37,12 +37,14 @@ function spaceCreateOptions(spaceName: string): ICreateRoomOpts {
creation_content: {
type: "m.space",
},
initial_state: [{
type: "m.room.name",
content: {
name: spaceName,
initial_state: [
{
type: "m.room.name",
content: {
name: spaceName,
},
},
}],
],
};
}
@ -61,10 +63,10 @@ describe("Spaces", () => {
let user: UserCredentials;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Sue").then(_user => {
cy.initTestUser(synapse, "Sue").then((_user) => {
user = _user;
cy.mockClipboard();
});
@ -78,8 +80,10 @@ describe("Spaces", () => {
it("should allow user to create public space", () => {
openSpaceCreateMenu().within(() => {
cy.get(".mx_SpaceCreateMenuType_public").click();
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.selectFile("cypress/fixtures/riot.png", { force: true });
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
"cypress/fixtures/riot.png",
{ force: true },
);
cy.get('input[label="Name"]').type("Let's have a Riot");
cy.get('input[label="Address"]').should("have.value", "lets-have-a-riot");
cy.get('textarea[label="Description"]').type("This is a space to reminisce Riot.im!");
@ -108,8 +112,10 @@ describe("Spaces", () => {
it("should allow user to create private space", () => {
openSpaceCreateMenu().within(() => {
cy.get(".mx_SpaceCreateMenuType_private").click();
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.selectFile("cypress/fixtures/riot.png", { force: true });
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
"cypress/fixtures/riot.png",
{ force: true },
);
cy.get('input[label="Name"]').type("This is not a Riot");
cy.get('input[label="Address"]').should("not.exist");
cy.get('textarea[label="Description"]').type("This is a private space of mourning Riot.im...");
@ -145,8 +151,10 @@ describe("Spaces", () => {
openSpaceCreateMenu().within(() => {
cy.get(".mx_SpaceCreateMenuType_private").click();
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
.selectFile("cypress/fixtures/riot.png", { force: true });
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
"cypress/fixtures/riot.png",
{ force: true },
);
cy.get('input[label="Address"]').should("not.exist");
cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im...");
cy.get('input[label="Name"]').type("This is my Riot{enter}");
@ -163,7 +171,7 @@ describe("Spaces", () => {
it("should allow user to invite another to a space", () => {
let bot: MatrixClient;
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
bot = _bot;
});
@ -198,13 +206,17 @@ describe("Spaces", () => {
});
cy.getSpacePanelButton("My Space").should("exist");
cy.getBot(synapse, { displayName: "BotBob" }).then({ timeout: 10000 }, async bot => {
cy.getBot(synapse, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space"));
await bot.invite(roomId, user.userId);
});
// Assert that `Space Space` is above `My Space` due to it being an invite
cy.getSpacePanelButton("Space Space").should("exist")
.parent().next().find('.mx_SpaceButton[aria-label="My Space"]').should("exist");
cy.getSpacePanelButton("Space Space")
.should("exist")
.parent()
.next()
.find('.mx_SpaceButton[aria-label="My Space"]')
.should("exist");
});
it("should include rooms in space home", () => {
@ -216,16 +228,10 @@ describe("Spaces", () => {
}).as("roomId2");
const spaceName = "Spacey Mc. Space Space";
cy.all([
cy.get<string>("@roomId1"),
cy.get<string>("@roomId2"),
]).then(([roomId1, roomId2]) => {
cy.all([cy.get<string>("@roomId1"), cy.get<string>("@roomId2")]).then(([roomId1, roomId2]) => {
cy.createSpace({
name: spaceName,
initial_state: [
spaceChildInitialState(roomId1),
spaceChildInitialState(roomId2),
],
initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
}).as("spaceId");
});
@ -244,12 +250,10 @@ describe("Spaces", () => {
cy.createSpace({
name: "Child Space",
initial_state: [],
}).then(spaceId => {
}).then((spaceId) => {
cy.createSpace({
name: "Root Space",
initial_state: [
spaceChildInitialState(spaceId),
],
initial_state: [spaceChildInitialState(spaceId)],
}).as("spaceId");
});
cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Root Space"]').should("exist");
@ -258,7 +262,7 @@ describe("Spaces", () => {
const axeOptions = {
rules: {
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
'nested-interactive': {
"nested-interactive": {
enabled: false,
},
},
@ -269,8 +273,10 @@ describe("Spaces", () => {
cy.get(".mx_SpaceButton_toggleCollapse").click({ force: true });
cy.get(".mx_SpacePanel:not(.collapsed)").should("exist");
cy.contains(".mx_SpaceItem", "Root Space").should("exist")
.contains(".mx_SpaceItem", "Child Space").should("exist");
cy.contains(".mx_SpaceItem", "Root Space")
.should("exist")
.contains(".mx_SpaceItem", "Child Space")
.should("exist");
cy.checkA11y(undefined, axeOptions);
cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] });

View file

@ -26,7 +26,7 @@ import Shadow = Cypress.Shadow;
export enum Filter {
People = "people",
PublicRooms = "public_rooms"
PublicRooms = "public_rooms",
}
declare global {
@ -37,78 +37,86 @@ declare global {
* Opens the spotlight dialog
*/
openSpotlightDialog(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>;
spotlightDialog(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>;
spotlightFilter(
filter: Filter | null,
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>;
spotlightSearch(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>;
spotlightResults(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>;
roomHeaderName(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>;
startDM(name: string): Chainable<void>;
}
}
}
Cypress.Commands.add("openSpotlightDialog", (
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
cy.get('.mx_RoomSearch_spotlightTrigger', options).click({ force: true });
return cy.spotlightDialog(options);
});
Cypress.Commands.add(
"openSpotlightDialog",
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
cy.get(".mx_RoomSearch_spotlightTrigger", options).click({ force: true });
return cy.spotlightDialog(options);
},
);
Cypress.Commands.add("spotlightDialog", (
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
});
Cypress.Commands.add(
"spotlightDialog",
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
},
);
Cypress.Commands.add("spotlightFilter", (
filter: Filter | null,
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
let selector: string;
switch (filter) {
case Filter.People:
selector = "#mx_SpotlightDialog_button_startChat";
break;
case Filter.PublicRooms:
selector = "#mx_SpotlightDialog_button_explorePublicRooms";
break;
default:
selector = ".mx_SpotlightDialog_filter";
break;
}
return cy.get(selector, options).click();
});
Cypress.Commands.add(
"spotlightFilter",
(
filter: Filter | null,
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
let selector: string;
switch (filter) {
case Filter.People:
selector = "#mx_SpotlightDialog_button_startChat";
break;
case Filter.PublicRooms:
selector = "#mx_SpotlightDialog_button_explorePublicRooms";
break;
default:
selector = ".mx_SpotlightDialog_filter";
break;
}
return cy.get(selector, options).click();
},
);
Cypress.Commands.add("spotlightSearch", (
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_SpotlightDialog_searchBox input", options);
});
Cypress.Commands.add(
"spotlightSearch",
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_SpotlightDialog_searchBox input", options);
},
);
Cypress.Commands.add("spotlightResults", (
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
});
Cypress.Commands.add(
"spotlightResults",
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
},
);
Cypress.Commands.add("roomHeaderName", (
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_RoomHeader_nametext", options);
});
Cypress.Commands.add(
"roomHeaderName",
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_RoomHeader_nametext", options);
},
);
Cypress.Commands.add("startDM", (name: string) => {
cy.openSpotlightDialog().within(() => {
@ -121,9 +129,7 @@ Cypress.Commands.add("startDM", (name: string) => {
cy.spotlightResults().eq(0).click();
});
// send first message to start DM
cy.get(".mx_BasicMessageComposer_input")
.should("have.focus")
.type("Hey!{enter}");
cy.get(".mx_BasicMessageComposer_input").should("have.focus").type("Hey!{enter}");
// The DM room is created at this point, this can take a little bit of time
cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
cy.contains(".mx_RoomSublist[aria-label=People]", name);
@ -148,46 +154,52 @@ describe("Spotlight", () => {
let room3Id: string;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Jim").then(() =>
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => {
bot1 = _bot1;
}),
).then(() =>
cy.getBot(synapse, { displayName: bot2Name }).then(_bot2 => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
bot2 = _bot2;
}),
).then(() =>
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => {
room1Id = _room1Id;
bot1.joinRoom(room1Id);
cy.visit("/#/room/" + room1Id);
});
bot2.createRoom({ name: room2Name, visibility: Visibility.Public })
.then(({ room_id: _room2Id }) => {
room2Id = _room2Id;
bot2.invite(room2Id, bot1.getUserId());
cy.initTestUser(synapse, "Jim")
.then(() =>
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
bot1 = _bot1;
}),
)
.then(() =>
cy.getBot(synapse, { displayName: bot2Name }).then((_bot2) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
bot2 = _bot2;
}),
)
.then(() =>
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then((_room1Id) => {
room1Id = _room1Id;
bot1.joinRoom(room1Id);
cy.visit("/#/room/" + room1Id);
});
bot2.createRoom({
name: room3Name,
visibility: Visibility.Public, initial_state: [{
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "world_readable",
bot2.createRoom({ name: room2Name, visibility: Visibility.Public }).then(
({ room_id: _room2Id }) => {
room2Id = _room2Id;
bot2.invite(room2Id, bot1.getUserId());
},
}],
}).then(({ room_id: _room3Id }) => {
room3Id = _room3Id;
bot2.invite(room3Id, bot1.getUserId());
});
}),
).then(() =>
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
);
);
bot2.createRoom({
name: room3Name,
visibility: Visibility.Public,
initial_state: [
{
type: "m.room.history_visibility",
state_key: "",
content: {
history_visibility: "world_readable",
},
},
],
}).then(({ room_id: _room3Id }) => {
room3Id = _room3Id;
bot2.invite(room3Id, bot1.getUserId());
});
}),
)
.then(() => cy.get(".mx_RoomSublist_skeletonUI").should("not.exist"));
});
});
@ -216,63 +228,71 @@ describe("Spotlight", () => {
});
it("should find joined rooms", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightSearch().clear().type(room1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room1Id);
}).then(() => {
cy.roomHeaderName().should("contain", room1Name);
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightSearch().clear().type(room1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room1Id);
})
.then(() => {
cy.roomHeaderName().should("contain", room1Name);
});
});
it("should find known public rooms", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).should("contain", "View");
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room1Id);
}).then(() => {
cy.roomHeaderName().should("contain", room1Name);
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).should("contain", "View");
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room1Id);
})
.then(() => {
cy.roomHeaderName().should("contain", room1Name);
});
});
it("should find unknown public rooms", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room2Name);
cy.spotlightResults().eq(0).should("contain", "Join");
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room2Id);
}).then(() => {
cy.get(".mx_RoomView_MessageList").should("have.length", 1);
cy.roomHeaderName().should("contain", room2Name);
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room2Name);
cy.spotlightResults().eq(0).should("contain", "Join");
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room2Id);
})
.then(() => {
cy.get(".mx_RoomView_MessageList").should("have.length", 1);
cy.roomHeaderName().should("contain", room2Name);
});
});
it("should find unknown public world readable rooms", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room3Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).should("contain", "View");
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room3Id);
}).then(() => {
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
cy.roomHeaderName().should("contain", room3Name);
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightFilter(Filter.PublicRooms);
cy.spotlightSearch().clear().type(room3Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).should("contain", "View");
cy.spotlightResults().eq(0).click();
cy.url().should("contain", room3Id);
})
.then(() => {
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
cy.roomHeaderName().should("contain", room3Name);
});
});
// TODO: We currently cant test finding rooms on other homeservers/other protocols
@ -299,29 +319,33 @@ describe("Spotlight", () => {
});
*/
it("should find known people", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot1Name);
cy.spotlightResults().eq(0).click();
}).then(() => {
cy.roomHeaderName().should("contain", bot1Name);
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot1Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot1Name);
cy.spotlightResults().eq(0).click();
})
.then(() => {
cy.roomHeaderName().should("contain", bot1Name);
});
});
it("should find unknown people", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
}).then(() => {
cy.roomHeaderName().should("contain", bot2Name);
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.spotlightResults().eq(0).click();
})
.then(() => {
cy.roomHeaderName().should("contain", bot2Name);
});
});
it("should find group DMs by usernames or user ids", () => {
@ -340,10 +364,7 @@ describe("Spotlight", () => {
// Send first message to actually start DM
cy.roomHeaderName().should("contain", bot2Name);
cy.get(".mx_BasicMessageComposer_input")
.click()
.should("have.focus")
.type("Hey!{enter}");
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
// Assert DM exists by checking for the first message and the room being in the room list
cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
@ -352,13 +373,13 @@ describe("Spotlight", () => {
// Invite BotBob into existing DM with ByteBot
cy.getDmRooms(bot2.getUserId())
.should("have.length", 1)
.then(dmRooms => cy.getClient().then(client => client.getRoom(dmRooms[0])))
.then(groupDm => {
.then((dmRooms) => cy.getClient().then((client) => client.getRoom(dmRooms[0])))
.then((groupDm) => {
cy.inviteUser(groupDm.roomId, bot1.getUserId());
cy.roomHeaderName().should(($element) =>
expect($element.get(0).innerText).contains(groupDm.name));
cy.roomHeaderName().should(($element) => expect($element.get(0).innerText).contains(groupDm.name));
cy.get(".mx_RoomSublist[aria-label=People]").should(($element) =>
expect($element.get(0).innerText).contains(groupDm.name));
expect($element.get(0).innerText).contains(groupDm.name),
);
// Search for BotBob by id, should return group DM and user
cy.openSpotlightDialog().within(() => {
@ -407,17 +428,19 @@ describe("Spotlight", () => {
});
it("should allow opening group chat dialog", () => {
cy.openSpotlightDialog().within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
cy.get(".mx_SpotlightDialog_startGroupChat").click();
}).then(() => {
cy.get('[role=dialog]').should("contain", "Direct Messages");
});
cy.openSpotlightDialog()
.within(() => {
cy.spotlightFilter(Filter.People);
cy.spotlightSearch().clear().type(bot2Name);
cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
cy.get(".mx_SpotlightDialog_startGroupChat").click();
})
.then(() => {
cy.get("[role=dialog]").should("contain", "Direct Messages");
});
});
it("should close spotlight after starting a DM", () => {
@ -445,38 +468,40 @@ describe("Spotlight", () => {
// our debouncing logic only starts the search after a short timeout,
// so we wait a few milliseconds.
cy.wait(1000);
cy.get(".mx_Spinner").should("not.exist").then(() => {
cy.spotlightResults().should("have.length", 2).then(() => {
cy.spotlightResults().eq(0)
.should("have.attr", "aria-selected", "true");
cy.spotlightResults().eq(1)
.should("have.attr", "aria-selected", "false");
cy.get(".mx_Spinner")
.should("not.exist")
.then(() => {
cy.spotlightResults()
.should("have.length", 2)
.then(() => {
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true");
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
});
cy.spotlightSearch()
.type("{downArrow}")
.then(() => {
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true");
});
cy.spotlightSearch()
.type("{downArrow}")
.then(() => {
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
});
cy.spotlightSearch()
.type("{upArrow}")
.then(() => {
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true");
});
cy.spotlightSearch()
.type("{upArrow}")
.then(() => {
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true");
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
});
});
cy.spotlightSearch().type("{downArrow}").then(() => {
cy.spotlightResults().eq(0)
.should("have.attr", "aria-selected", "false");
cy.spotlightResults().eq(1)
.should("have.attr", "aria-selected", "true");
});
cy.spotlightSearch().type("{downArrow}").then(() => {
cy.spotlightResults().eq(0)
.should("have.attr", "aria-selected", "false");
cy.spotlightResults().eq(1)
.should("have.attr", "aria-selected", "false");
});
cy.spotlightSearch().type("{upArrow}").then(() => {
cy.spotlightResults().eq(0)
.should("have.attr", "aria-selected", "false");
cy.spotlightResults().eq(1)
.should("have.attr", "aria-selected", "true");
});
cy.spotlightSearch().type("{upArrow}").then(() => {
cy.spotlightResults().eq(0)
.should("have.attr", "aria-selected", "true");
cy.spotlightResults().eq(1)
.should("have.attr", "aria-selected", "false");
});
});
});
});
});

View file

@ -21,7 +21,7 @@ import { MatrixClient } from "../../global";
function markWindowBeforeReload(): void {
// mark our window object to "know" when it gets reloaded
cy.window().then(w => w.beforeReload = true);
cy.window().then((w) => (w.beforeReload = true));
}
describe("Threads", () => {
@ -30,10 +30,10 @@ describe("Threads", () => {
beforeEach(() => {
// Default threads to ON for this spec
cy.enableLabsFeature("feature_thread");
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Tom");
@ -78,12 +78,12 @@ describe("Threads", () => {
cy.getBot(synapse, {
displayName: "BotBob",
autoAcceptInvites: false,
}).then(_bot => {
}).then((_bot) => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
bot.joinRoom(roomId);
@ -95,10 +95,11 @@ describe("Threads", () => {
// Wait for message to send, get its ID and save as @threadId
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.invoke("attr", "data-scroll-tokens").as("threadId");
.invoke("attr", "data-scroll-tokens")
.as("threadId");
// Bot starts thread
cy.get<string>("@threadId").then(threadId => {
cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, {
body: "Hello there",
msgtype: "m.text",
@ -119,7 +120,8 @@ describe("Threads", () => {
// User reacts to message instead
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Hello there")
.find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
.find('[aria-label="React"]')
.click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_EmojiPicker").within(() => {
cy.get('input[type="text"]').type("wave");
cy.contains('[role="menuitem"]', "👋").click();
@ -127,7 +129,8 @@ describe("Threads", () => {
// User redacts their prior response
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test")
.find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
.find('[aria-label="Options"]')
.click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_IconizedContextMenu").within(() => {
cy.contains('[role="menuitem"]', "Remove").click();
});
@ -144,7 +147,7 @@ describe("Threads", () => {
cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
// Bot responds to thread
cy.get<string>("@threadId").then(threadId => {
cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, {
body: "How are things?",
msgtype: "m.text",
@ -178,45 +181,55 @@ describe("Threads", () => {
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
});
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
.should("contain", "Great! How about yourself?");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
"contain",
"Great! How about yourself?",
);
// User closes right panel
cy.get(".mx_ThreadView .mx_BaseCard_close").click();
// Bot responds to thread and saves the id of their message to @eventId
cy.get<string>("@threadId").then(threadId => {
cy.wrap(bot.sendMessage(roomId, threadId, {
body: "I'm very good thanks",
msgtype: "m.text",
}).then(res => res.event_id)).as("eventId");
cy.get<string>("@threadId").then((threadId) => {
cy.wrap(
bot
.sendMessage(roomId, threadId, {
body: "I'm very good thanks",
msgtype: "m.text",
})
.then((res) => res.event_id),
).as("eventId");
});
// User asserts
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
.should("contain", "I'm very good thanks");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
"contain",
"I'm very good thanks",
);
// Bot edits their latest event
cy.get<string>("@eventId").then(eventId => {
cy.get<string>("@eventId").then((eventId) => {
bot.sendMessage(roomId, {
"body": "* I'm very good thanks :)",
"msgtype": "m.text",
"m.new_content": {
"body": "I'm very good thanks :)",
"msgtype": "m.text",
body: "I'm very good thanks :)",
msgtype: "m.text",
},
"m.relates_to": {
"rel_type": "m.replace",
"event_id": eventId,
rel_type: "m.replace",
event_id: eventId,
},
});
});
// User asserts
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
.should("contain", "I'm very good thanks :)");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
"contain",
"I'm very good thanks :)",
);
});
it("can send voice messages", () => {
@ -227,7 +240,7 @@ describe("Threads", () => {
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.visit("/#/room/" + roomId);
});
@ -237,7 +250,9 @@ describe("Threads", () => {
// Create thread
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
.realHover()
.find(".mx_MessageActionBar_threadButton")
.click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click();
@ -250,7 +265,7 @@ describe("Threads", () => {
it("right panel behaves correctly", () => {
// Create room
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.visit("/#/room/" + roomId);
});
@ -259,7 +274,9 @@ describe("Threads", () => {
// Create thread
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
.realHover()
.find(".mx_MessageActionBar_threadButton")
.click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
// Send message to thread
@ -271,7 +288,9 @@ describe("Threads", () => {
// Open existing thread
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.realHover().find(".mx_MessageActionBar_threadButton").click();
.realHover()
.find(".mx_MessageActionBar_threadButton")
.click();
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. Bot");
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User");

View file

@ -45,10 +45,7 @@ const expectDisplayName = (e: JQuery<HTMLElement>, displayName: string): void =>
};
const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
cy.all([
cy.window({ log: false }),
cy.getClient(),
]).then(([win, cli]) => {
cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => {
const size = AVATAR_SIZE * win.devicePixelRatio;
expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
// eslint-disable-next-line no-restricted-properties
@ -75,10 +72,10 @@ describe("Timeline", () => {
let newAvatarUrl: string;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, OLD_NAME).then(() =>
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
roomId = _room1Id;
}),
);
@ -154,8 +151,11 @@ describe("Timeline", () => {
it("should create and configure a room on IRC layout", () => {
cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
cy.get(".mx_Spinner").should("not.exist");
cy.percySnapshot("Configured room on IRC layout");
});
@ -165,8 +165,10 @@ describe("Timeline", () => {
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
@ -177,13 +179,13 @@ describe("Timeline", () => {
// = calc(var(--name-width) + 10px + var(--icon-width))
// = 80 + 10 + 14 = 104px
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
.should('have.css', "margin-inline-start", "104px")
.should('have.css', "inset-inline-start", "0px");
.should("have.css", "margin-inline-start", "104px")
.should("have.css", "inset-inline-start", "0px");
cy.get(".mx_Spinner").should("not.exist");
// Exclude timestamp from snapshot
const percyCSS = ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp "
+ "{ visibility: hidden !important; }";
const percyCSS =
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS });
cy.checkA11y();
});
@ -192,8 +194,10 @@ describe("Timeline", () => {
sendEvent(roomId);
cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Edit message
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
@ -206,20 +210,23 @@ describe("Timeline", () => {
cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
// Exclude timestamp from snapshot
const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp "
+ "{ visibility: hidden !important; }";
const percyCSS =
".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
// should not add inline start padding to a hidden event line on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line")
.should('have.css', 'padding-inline-start', '0px');
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").should(
"have.css",
"padding-inline-start",
"0px",
);
cy.percySnapshot("Hidden event line with zero padding on IRC layout", { percyCSS });
// should add inline start padding to a hidden event line on modern layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line")
// calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
.should('have.css', 'padding-inline-start', '84px');
.should("have.css", "padding-inline-start", "84px");
cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
});
@ -227,8 +234,10 @@ describe("Timeline", () => {
sendEvent(roomId);
cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Edit message
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
@ -238,9 +247,12 @@ describe("Timeline", () => {
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist");
// Click top left of the event toggle, which should not be covered by MessageActionBar's safe area
cy.get(".mx_EventTile .mx_ViewSourceEvent").should("exist").realHover().within(() => {
cy.get(".mx_ViewSourceEvent_toggle").click('topLeft', { force: false });
});
cy.get(".mx_EventTile .mx_ViewSourceEvent")
.should("exist")
.realHover()
.within(() => {
cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false });
});
// Make sure the expand toggle worked
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible");
@ -249,8 +261,11 @@ describe("Timeline", () => {
it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => {
cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " +
".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
@ -340,10 +355,14 @@ describe("Timeline", () => {
cy.getComposer().type(`${reply}{enter}`);
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody")
.should("contain", MESSAGE);
cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply)
.should("have.length", 1);
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody").should(
"contain",
MESSAGE,
);
cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply).should(
"have.length",
1,
);
});
it("can reply with a voice message", () => {
@ -355,10 +374,14 @@ describe("Timeline", () => {
cy.wait(3000);
cy.get(".mx_RoomView_body .mx_MessageComposer .mx_MessageComposer_sendMessage").click();
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody")
.should("contain", MESSAGE);
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody")
.should("have.length", 1);
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody").should(
"contain",
MESSAGE,
);
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody").should(
"have.length",
1,
);
});
});
});

View file

@ -47,15 +47,15 @@ describe("Analytics Toast", () => {
});
it("should not show an analytics toast if config has nothing about posthog", () => {
cy.intercept("/config.json?cachebuster=*", req => {
req.continue(res => {
cy.intercept("/config.json?cachebuster=*", (req) => {
req.continue((res) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { posthog, ...body } = res.body;
res.send(200, body);
});
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Tod");
});
@ -66,8 +66,8 @@ describe("Analytics Toast", () => {
describe("with posthog enabled", () => {
beforeEach(() => {
cy.intercept("/config.json?cachebuster=*", req => {
req.continue(res => {
cy.intercept("/config.json?cachebuster=*", (req) => {
req.continue((res) => {
res.send(200, {
...res.body,
posthog: {
@ -78,7 +78,7 @@ describe("Analytics Toast", () => {
});
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Tod");
rejectToast("Notifications");

View file

@ -22,7 +22,7 @@ describe("Update", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
});
});
@ -45,9 +45,11 @@ describe("Update", () => {
cy.initTestUser(synapse, "Ursa");
cy.wait("@version");
cy.url().should("contain", "updated=" + NEW_VERSION).then(href => {
const url = new URL(href);
expect(url.searchParams.get("updated")).to.equal(NEW_VERSION);
});
cy.url()
.should("contain", "updated=" + NEW_VERSION)
.then((href) => {
const url = new URL(href);
expect(url.searchParams.get("updated")).to.equal(NEW_VERSION);
});
});
});

View file

@ -24,10 +24,10 @@ describe("User Menu", () => {
let user: UserCredentials;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Jeff").then(credentials => {
cy.initTestUser(synapse, "Jeff").then((credentials) => {
user = credentials;
});
});

View file

@ -26,23 +26,23 @@ describe("User Onboarding (new user)", () => {
let bot1: MatrixClient;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Jane Doe");
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("mx_registration_time", "1656633601");
});
cy.reload().then(() => {
// wait for the app to load
return cy.get(".mx_MatrixChat", { timeout: 15000 });
});
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => {
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
bot1 = _bot1;
});
cy.get('.mx_UserOnboardingPage').should('exist');
cy.get('.mx_UserOnboardingButton').should('exist');
cy.get('.mx_UserOnboardingList')
.should('exist')
cy.get(".mx_UserOnboardingPage").should("exist");
cy.get(".mx_UserOnboardingButton").should("exist");
cy.get(".mx_UserOnboardingList")
.should("exist")
.should(($list) => {
const list = $list.get(0);
expect(getComputedStyle(list).opacity).to.be.eq("1");
@ -55,44 +55,42 @@ describe("User Onboarding (new user)", () => {
});
it("page is shown and preference exists", () => {
cy.get('.mx_UserOnboardingPage')
.percySnapshotElement("User onboarding page");
cy.get(".mx_UserOnboardingPage").percySnapshotElement("User onboarding page");
cy.openUserSettings("Preferences");
cy.contains("Show shortcut to welcome checklist above the room list").should("exist");
});
it("app download dialog", () => {
cy.contains(".mx_UserOnboardingTask_action", "Download apps").click();
cy.get('[role=dialog]')
.contains("#mx_BaseDialog_title", "Download Element")
.should("exist");
cy.get('[role=dialog]')
.percySnapshotElement("App download dialog", {
widths: [640],
});
cy.get("[role=dialog]").contains("#mx_BaseDialog_title", "Download Element").should("exist");
cy.get("[role=dialog]").percySnapshotElement("App download dialog", {
widths: [640],
});
});
it("using find friends action should increase progress", () => {
cy.get(".mx_ProgressBar").invoke("val").then((oldProgress) => {
const findPeopleAction = cy.contains(".mx_UserOnboardingTask_action", "Find friends");
expect(findPeopleAction).to.exist;
findPeopleAction.click();
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
const message = "Hi!";
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
cy.visit("/#/home");
cy.get('.mx_UserOnboardingPage').should('exist');
cy.get('.mx_UserOnboardingButton').should('exist');
cy.get('.mx_UserOnboardingList')
.should('exist')
.should(($list) => {
const list = $list.get(0);
expect(getComputedStyle(list).opacity).to.be.eq("1");
});
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
});
cy.get(".mx_ProgressBar")
.invoke("val")
.then((oldProgress) => {
const findPeopleAction = cy.contains(".mx_UserOnboardingTask_action", "Find friends");
expect(findPeopleAction).to.exist;
findPeopleAction.click();
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
const message = "Hi!";
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
cy.visit("/#/home");
cy.get(".mx_UserOnboardingPage").should("exist");
cy.get(".mx_UserOnboardingButton").should("exist");
cy.get(".mx_UserOnboardingList")
.should("exist")
.should(($list) => {
const list = $list.get(0);
expect(getComputedStyle(list).opacity).to.be.eq("1");
});
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
});
});
});

View file

@ -22,10 +22,10 @@ describe("User Onboarding (old user)", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Jane Doe");
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("mx_registration_time", "2");
});
cy.reload().then(() => {
@ -41,8 +41,8 @@ describe("User Onboarding (old user)", () => {
});
it("page and preference are hidden", () => {
cy.get('.mx_UserOnboardingPage').should('not.exist');
cy.get('.mx_UserOnboardingButton').should('not.exist');
cy.get(".mx_UserOnboardingPage").should("not.exist");
cy.get(".mx_UserOnboardingButton").should("not.exist");
cy.openUserSettings("Preferences");
cy.contains("Show shortcut to welcome page above the room list").should("not.exist");
});

View file

@ -23,7 +23,7 @@ describe("UserView", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Violet");
@ -36,7 +36,7 @@ describe("UserView", () => {
});
it("should render the user view as expected", () => {
cy.get<MatrixClient>("@bot").then(bot => {
cy.get<MatrixClient>("@bot").then((bot) => {
cy.visit(`/#/user/${bot.getUserId()}`);
});

View file

@ -19,7 +19,7 @@ import { IWidget } from "matrix-widget-api";
import { SynapseInstance } from "../../plugins/synapsedocker";
const ROOM_NAME = 'Test Room';
const ROOM_NAME = "Test Room";
const WIDGET_ID = "fake-widget";
const WIDGET_HTML = `
<html lang="en">
@ -32,18 +32,18 @@ const WIDGET_HTML = `
</html>
`;
describe('Widget Layout', () => {
describe("Widget Layout", () => {
let widgetUrl: string;
let synapse: SynapseInstance;
let roomId: string;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Sally");
});
cy.serveHtmlFile(WIDGET_HTML).then(url => {
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
widgetUrl = url;
});
@ -53,34 +53,38 @@ describe('Widget Layout', () => {
roomId = id;
// setup widget via state event
cy.getClient().then(async matrixClient => {
const content: IWidget = {
id: WIDGET_ID,
creatorUserId: 'somebody',
type: 'widget',
name: 'widget',
url: widgetUrl,
};
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, WIDGET_ID);
}).as('widgetEventSent');
cy.getClient()
.then(async (matrixClient) => {
const content: IWidget = {
id: WIDGET_ID,
creatorUserId: "somebody",
type: "widget",
name: "widget",
url: widgetUrl,
};
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
})
.as("widgetEventSent");
// set initial layout
cy.getClient().then(async matrixClient => {
const content = {
widgets: {
[WIDGET_ID]: {
container: 'top', index: 1, width: 100, height: 0,
cy.getClient()
.then(async (matrixClient) => {
const content = {
widgets: {
[WIDGET_ID]: {
container: "top",
index: 1,
width: 100,
height: 0,
},
},
},
};
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
}).as('layoutEventSent');
};
await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
})
.as("layoutEventSent");
});
cy.all([
cy.get<string>("@widgetEventSent"),
cy.get<string>("@layoutEventSent"),
]).then(() => {
cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(() => {
// open the room
cy.viewRoomByName(ROOM_NAME);
});
@ -91,31 +95,34 @@ describe('Widget Layout', () => {
cy.stopWebServers();
});
it('manually resize the height of the top container layout', () => {
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250);
it("manually resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
cy.get('.mx_AppsContainer_resizerHandle')
.trigger('mousedown')
.trigger('mousemove', { clientX: 0, clientY: 550, force: true })
.trigger('mouseup', { clientX: 0, clientY: 550, force: true });
cy.get(".mx_AppsContainer_resizerHandle")
.trigger("mousedown")
.trigger("mousemove", { clientX: 0, clientY: 550, force: true })
.trigger("mouseup", { clientX: 0, clientY: 550, force: true });
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
cy.get('iframe[title="widget"]').invoke("height").should("be.greaterThan", 400);
});
it('programatically resize the height of the top container layout', () => {
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250);
it("programatically resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
cy.getClient().then(async matrixClient => {
cy.getClient().then(async (matrixClient) => {
const content = {
widgets: {
[WIDGET_ID]: {
container: 'top', index: 1, width: 100, height: 100,
container: "top",
index: 1,
width: 100,
height: 100,
},
},
};
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
});
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
cy.get('iframe[title="widget"]').invoke("height").should("be.greaterThan", 400);
});
});

View file

@ -67,8 +67,8 @@ const WIDGET_HTML = `
`;
function openStickerPicker() {
cy.get('.mx_MessageComposer_buttonMenu').click();
cy.get('#stickersButton').click();
cy.get(".mx_MessageComposer_buttonMenu").click();
cy.get("#stickersButton").click();
}
function sendStickerFromPicker() {
@ -76,18 +76,16 @@ function sendStickerFromPicker() {
// to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can
// break into the iframe for us :(
cy.accessIframe(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`).within({}, () => {
cy.get("#sendsticker").should('exist').click();
cy.get("#sendsticker").should("exist").click();
});
// Sticker picker should close itself after sending.
cy.get(".mx_AppTileFullWidth#stickers").should('not.exist');
cy.get(".mx_AppTileFullWidth#stickers").should("not.exist");
}
function expectTimelineSticker(roomId: string) {
// Make sure it's in the right room
cy.get('.mx_EventTile_sticker > a')
.should("have.attr", "href")
.and("include", `/${roomId}/`);
cy.get(".mx_EventTile_sticker > a").should("have.attr", "href").and("include", `/${roomId}/`);
// Make sure the image points at the sticker image
cy.get<HTMLImageElement>(`img[alt="${STICKER_NAME}"]`)
@ -107,12 +105,12 @@ describe("Stickers", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Sally");
});
cy.serveHtmlFile(WIDGET_HTML).then(url => {
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
stickerPickerUrl = url;
});
});
@ -122,7 +120,7 @@ describe("Stickers", () => {
cy.stopWebServers();
});
it('should send a sticker to multiple rooms', () => {
it("should send a sticker to multiple rooms", () => {
cy.createRoom({
name: ROOM_NAME_1,
}).as("roomId1");

View file

@ -57,7 +57,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
return new Promise((resolve, reject) => {
function eventsInIntendedState(evList) {
const widgetPresent = evList.some((ev) => {
return ev.getContent() && ev.getContent()['id'] === widgetId;
return ev.getContent() && ev.getContent()["id"] === widgetId;
});
if (add) {
return widgetPresent;
@ -68,7 +68,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
const room = matrixClient.getRoom(roomId);
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
if (eventsInIntendedState(startingWidgetEvents)) {
resolve();
return;
@ -77,7 +77,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
function onRoomStateEvents(ev: MatrixEvent) {
if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return;
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
if (eventsInIntendedState(currentWidgetEvents)) {
matrixClient.removeListener(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
@ -95,35 +95,39 @@ describe("Widget PIP", () => {
let bot: MatrixClient;
let demoWidgetUrl: string;
function roomCreateAddWidgetPip(userRemove: 'leave' | 'kick' | 'ban') {
function roomCreateAddWidgetPip(userRemove: "leave" | "kick" | "ban") {
cy.createRoom({
name: ROOM_NAME,
invite: [bot.getUserId()],
}).then(roomId => {
}).then((roomId) => {
// sets bot to Admin and user to Moderator
cy.getClient().then(matrixClient => {
return matrixClient.sendStateEvent(roomId, 'm.room.power_levels', {
users: {
[user.userId]: 50,
[bot.getUserId()]: 100,
},
});
}).as('powerLevelsChanged');
cy.getClient()
.then((matrixClient) => {
return matrixClient.sendStateEvent(roomId, "m.room.power_levels", {
users: {
[user.userId]: 50,
[bot.getUserId()]: 100,
},
});
})
.as("powerLevelsChanged");
// bot joins the room
cy.botJoinRoom(bot, roomId).as('botJoined');
cy.botJoinRoom(bot, roomId).as("botJoined");
// setup widget via state event
cy.getClient().then(async matrixClient => {
const content: IWidget = {
id: DEMO_WIDGET_ID,
creatorUserId: 'somebody',
type: DEMO_WIDGET_TYPE,
name: DEMO_WIDGET_NAME,
url: demoWidgetUrl,
};
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID);
}).as('widgetEventSent');
cy.getClient()
.then(async (matrixClient) => {
const content: IWidget = {
id: DEMO_WIDGET_ID,
creatorUserId: "somebody",
type: DEMO_WIDGET_TYPE,
name: DEMO_WIDGET_NAME,
url: demoWidgetUrl,
};
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID);
})
.as("widgetEventSent");
// open the room
cy.viewRoomByName(ROOM_NAME);
@ -133,7 +137,7 @@ describe("Widget PIP", () => {
cy.get<string>("@botJoined"),
cy.get<string>("@widgetEventSent"),
]).then(() => {
cy.window().then(async win => {
cy.window().then(async (win) => {
// wait for widget state event
await waitForRoomWidget(win, DEMO_WIDGET_ID, roomId, true);
@ -145,21 +149,23 @@ describe("Widget PIP", () => {
// checks that widget is opened in pip
cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => {
cy.get("#demo").should('exist').then(async () => {
const userId = user.userId;
if (userRemove == 'leave') {
cy.getClient().then(async matrixClient => {
await matrixClient.leave(roomId);
});
} else if (userRemove == 'kick') {
await bot.kick(roomId, userId);
} else if (userRemove == 'ban') {
await bot.ban(roomId, userId);
}
cy.get("#demo")
.should("exist")
.then(async () => {
const userId = user.userId;
if (userRemove == "leave") {
cy.getClient().then(async (matrixClient) => {
await matrixClient.leave(roomId);
});
} else if (userRemove == "kick") {
await bot.kick(roomId, userId);
} else if (userRemove == "ban") {
await bot.ban(roomId, userId);
}
// checks that pip window is closed
cy.get(".mx_LegacyCallView_pip").should("not.exist");
});
// checks that pip window is closed
cy.get(".mx_LegacyCallView_pip").should("not.exist");
});
});
});
});
@ -167,17 +173,17 @@ describe("Widget PIP", () => {
}
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Mike").then(_user => {
cy.initTestUser(synapse, "Mike").then((_user) => {
user = _user;
});
cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then(_bot => {
cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then((_bot) => {
bot = _bot;
});
});
cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => {
cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => {
demoWidgetUrl = url;
});
});
@ -187,15 +193,15 @@ describe("Widget PIP", () => {
cy.stopWebServers();
});
it('should be closed on leave', () => {
roomCreateAddWidgetPip('leave');
it("should be closed on leave", () => {
roomCreateAddWidgetPip("leave");
});
it('should be closed on kick', () => {
roomCreateAddWidgetPip('kick');
it("should be closed on kick", () => {
roomCreateAddWidgetPip("kick");
});
it('should be closed on ban', () => {
roomCreateAddWidgetPip('ban');
it("should be closed on ban", () => {
roomCreateAddWidgetPip("ban");
});
});

View file

@ -1,39 +1,39 @@
{
"versions": [
"r0.0.1",
"r0.1.0",
"r0.2.0",
"r0.3.0",
"r0.4.0",
"r0.5.0",
"r0.6.0",
"r0.6.1",
"v1.1",
"v1.2",
"v1.3",
"v1.4"
],
"unstable_features": {
"org.matrix.label_based_filtering": true,
"org.matrix.e2e_cross_signing": true,
"org.matrix.msc2432": true,
"uk.half-shot.msc2666.mutual_rooms": true,
"io.element.e2ee_forced.public": false,
"io.element.e2ee_forced.private": false,
"io.element.e2ee_forced.trusted_private": false,
"org.matrix.msc3026.busy_presence": false,
"org.matrix.msc2285.stable": true,
"org.matrix.msc3827.stable": true,
"org.matrix.msc2716": false,
"org.matrix.msc3030": false,
"org.matrix.msc3440.stable": true,
"org.matrix.msc3771": true,
"org.matrix.msc3773": false,
"fi.mau.msc2815": false,
"org.matrix.msc3882": false,
"org.matrix.msc3881": false,
"org.matrix.msc3874": false,
"org.matrix.msc3886": false,
"org.matrix.msc3912": false
}
}
"versions": [
"r0.0.1",
"r0.1.0",
"r0.2.0",
"r0.3.0",
"r0.4.0",
"r0.5.0",
"r0.6.0",
"r0.6.1",
"v1.1",
"v1.2",
"v1.3",
"v1.4"
],
"unstable_features": {
"org.matrix.label_based_filtering": true,
"org.matrix.e2e_cross_signing": true,
"org.matrix.msc2432": true,
"uk.half-shot.msc2666.mutual_rooms": true,
"io.element.e2ee_forced.public": false,
"io.element.e2ee_forced.private": false,
"io.element.e2ee_forced.trusted_private": false,
"org.matrix.msc3026.busy_presence": false,
"org.matrix.msc2285.stable": true,
"org.matrix.msc3827.stable": true,
"org.matrix.msc2716": false,
"org.matrix.msc3030": false,
"org.matrix.msc3440.stable": true,
"org.matrix.msc3771": true,
"org.matrix.msc3773": false,
"fi.mau.msc2815": false,
"org.matrix.msc3882": false,
"org.matrix.msc3881": false,
"org.matrix.msc3874": false,
"org.matrix.msc3886": false,
"org.matrix.msc3912": false
}
}

View file

@ -42,7 +42,8 @@ export function dockerRun(opts: {
const args = [
"run",
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
"--name",
`${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
"-d",
...params,
opts.image,
@ -58,23 +59,22 @@ export function dockerRun(opts: {
});
}
export function dockerExec(args: {
containerId: string;
params: string[];
}): Promise<void> {
export function dockerExec(args: { containerId: string; params: string[] }): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile("docker", [
"exec", args.containerId,
...args.params,
], { encoding: 'utf8' }, (err, stdout, stderr) => {
if (err) {
console.log(stdout);
console.log(stderr);
reject(err);
return;
}
resolve();
});
childProcess.execFile(
"docker",
["exec", args.containerId, ...args.params],
{ encoding: "utf8" },
(err, stdout, stderr) => {
if (err) {
console.log(stdout);
console.log(stderr);
reject(err);
return;
}
resolve();
},
);
});
}
@ -87,58 +87,45 @@ export async function dockerLogs(args: {
const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore";
await new Promise<void>((resolve) => {
childProcess.spawn("docker", [
"logs",
args.containerId,
], {
stdio: ["ignore", stdoutFile, stderrFile],
}).once('close', resolve);
childProcess
.spawn("docker", ["logs", args.containerId], {
stdio: ["ignore", stdoutFile, stderrFile],
})
.once("close", resolve);
});
if (args.stdoutFile) await fse.close(<number>stdoutFile);
if (args.stderrFile) await fse.close(<number>stderrFile);
}
export function dockerStop(args: {
containerId: string;
}): Promise<void> {
export function dockerStop(args: { containerId: string }): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile('docker', [
"stop",
args.containerId,
], err => {
childProcess.execFile("docker", ["stop", args.containerId], (err) => {
if (err) reject(err);
resolve();
});
});
}
export function dockerRm(args: {
containerId: string;
}): Promise<void> {
export function dockerRm(args: { containerId: string }): Promise<void> {
return new Promise<void>((resolve, reject) => {
childProcess.execFile('docker', [
"rm",
args.containerId,
], err => {
childProcess.execFile("docker", ["rm", args.containerId], (err) => {
if (err) reject(err);
resolve();
});
});
}
export function dockerIp(args: {
containerId: string;
}): Promise<string> {
export function dockerIp(args: { containerId: string }): Promise<string> {
return new Promise<string>((resolve, reject) => {
childProcess.execFile('docker', [
"inspect",
"-f", "{{ .NetworkSettings.IPAddress }}",
args.containerId,
], (err, stdout) => {
if (err) reject(err);
else resolve(stdout.trim());
});
childProcess.execFile(
"docker",
["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", args.containerId],
(err, stdout) => {
if (err) reject(err);
else resolve(stdout.trim());
},
);
});
}

View file

@ -27,7 +27,7 @@ import { log } from "./log";
/**
* @type {Cypress.PluginConfig}
*/
export default function(on: PluginEvents, config: PluginConfigOptions) {
export default function (on: PluginEvents, config: PluginConfigOptions) {
docker(on, config);
synapseDocker(on, config);
slidingSyncProxyDocker(on, config);

View file

@ -41,10 +41,7 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
const postgresId = await dockerRun({
image: "postgres",
containerName: "react-sdk-cypress-sliding-sync-postgres",
params: [
"--rm",
"-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`,
],
params: ["--rm", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`],
});
const postgresIp = await dockerIp({ containerId: postgresId });
@ -54,14 +51,11 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
const waitTimeMillis = 30000;
const startTime = new Date().getTime();
let lastErr: Error;
while ((new Date().getTime() - startTime) < waitTimeMillis) {
while (new Date().getTime() - startTime < waitTimeMillis) {
try {
await dockerExec({
containerId: postgresId,
params: [
"pg_isready",
"-U", "postgres",
],
params: ["pg_isready", "-U", "postgres"],
});
lastErr = null;
break;
@ -82,10 +76,14 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
containerName: "react-sdk-cypress-sliding-sync-proxy",
params: [
"--rm",
"-p", `${port}:8008/tcp`,
"-e", "SYNCV3_SECRET=bwahahaha",
"-e", `SYNCV3_SERVER=http://${synapseIp}:8008`,
"-e", `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
"-p",
`${port}:8008/tcp`,
"-e",
"SYNCV3_SECRET=bwahahaha",
"-e",
`SYNCV3_SERVER=http://${synapseIp}:8008`,
"-e",
`SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
],
});
console.log(new Date(), "started!");

View file

@ -54,11 +54,11 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
if (!stats?.isDirectory) {
throw new Error(`No such template: ${template}`);
}
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-'));
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-synapsedocker-"));
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
console.log(`Copy ${templateDir} -> ${tempDir}`);
await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' });
await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== "homeserver.yaml" });
const registrationSecret = randB64Bytes(16);
const macaroonSecret = randB64Bytes(16);
@ -102,11 +102,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
const synapseId = await dockerRun({
image: "matrixdotorg/synapse:develop",
containerName: `react-sdk-cypress-synapse`,
params: [
"--rm",
"-v", `${synCfg.configDir}:/data`,
"-p", `${synCfg.port}:8008/tcp`,
],
params: ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`],
cmd: "run",
});
@ -117,9 +113,12 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
containerId: synapseId,
params: [
"curl",
"--connect-timeout", "30",
"--retry", "30",
"--retry-delay", "1",
"--connect-timeout",
"30",
"--retry",
"30",
"--retry-delay",
"1",
"--retry-all-errors",
"--silent",
"http://localhost:8008/health",

View file

@ -5,21 +5,21 @@ pid_file: /data/homeserver.pid
public_baseurl: http://localhost:8008/
# Listener is always port 8008 (configured in the container)
listeners:
- port: 8008
tls: false
bind_addresses: ['::']
type: http
x_forwarded: true
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client, federation, consent]
compress: false
resources:
- names: [client, federation, consent]
compress: false
# An sqlite in-memory database is fast & automatically wipes each time
database:
name: "sqlite3"
args:
database: ":memory:"
name: "sqlite3"
args:
database: ":memory:"
# Needs to be configured to log to the console like a good docker process
log_config: "/data/log.config"
@ -27,19 +27,19 @@ log_config: "/data/log.config"
rc_messages_per_second: 10000
rc_message_burst_count: 10000
rc_registration:
per_second: 10000
burst_count: 10000
per_second: 10000
burst_count: 10000
rc_login:
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
media_store_path: "/data/media_store"
uploads_path: "/data/uploads"
@ -54,19 +54,19 @@ form_secret: "{{FORM_SECRET}}"
# Signing key must be here: it will be generated to this file
signing_key_path: "/data/localhost.signing.key"
email:
enable_notifs: false
smtp_host: "localhost"
smtp_port: 25
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
require_transport_security: False
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: Matrix
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
client_base_url: "http://localhost/element"
enable_notifs: false
smtp_host: "localhost"
smtp_port: 25
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
require_transport_security: False
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: Matrix
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
client_base_url: "http://localhost/element"
trusted_key_servers:
- server_name: "matrix.org"
- server_name: "matrix.org"
suppress_key_server_warning: true

View file

@ -2,39 +2,39 @@ server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ['::']
type: http
x_forwarded: true
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client, federation, consent]
compress: false
resources:
- names: [client, federation, consent]
compress: false
database:
name: "sqlite3"
args:
database: ":memory:"
name: "sqlite3"
args:
database: ":memory:"
log_config: "/data/log.config"
rc_messages_per_second: 10000
rc_message_burst_count: 10000
rc_registration:
per_second: 10000
burst_count: 10000
per_second: 10000
burst_count: 10000
rc_login:
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
media_store_path: "/data/media_store"
uploads_path: "/data/uploads"
@ -47,38 +47,38 @@ macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
form_secret: "{{FORM_SECRET}}"
signing_key_path: "/data/localhost.signing.key"
email:
enable_notifs: false
smtp_host: "localhost"
smtp_port: 25
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
require_transport_security: False
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: Matrix
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
client_base_url: "http://localhost/element"
enable_notifs: false
smtp_host: "localhost"
smtp_port: 25
smtp_user: "exampleusername"
smtp_pass: "examplepassword"
require_transport_security: False
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
app_name: Matrix
notif_template_html: notif_mail.html
notif_template_text: notif_mail.txt
notif_for_new_users: True
client_base_url: "http://localhost/element"
user_consent:
template_dir: /data/res/templates/privacy
version: 1.0
server_notice_content:
msgtype: m.text
body: >-
To continue using this homeserver you must review and agree to the
terms and conditions at %(consent_uri)s
send_server_notice_to_guests: True
block_events_error: >-
To continue using this homeserver you must review and agree to the
terms and conditions at %(consent_uri)s
require_at_registration: true
template_dir: /data/res/templates/privacy
version: 1.0
server_notice_content:
msgtype: m.text
body: >-
To continue using this homeserver you must review and agree to the
terms and conditions at %(consent_uri)s
send_server_notice_to_guests: True
block_events_error: >-
To continue using this homeserver you must review and agree to the
terms and conditions at %(consent_uri)s
require_at_registration: true
server_notices:
system_mxid_localpart: notices
system_mxid_display_name: "Server Notices"
system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ"
room_name: "Server Notices"
system_mxid_localpart: notices
system_mxid_display_name: "Server Notices"
system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ"
room_name: "Server Notices"
trusted_key_servers:
- server_name: "matrix.org"
- server_name: "matrix.org"
suppress_key_server_warning: true

View file

@ -1,23 +1,19 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Privacy policy</title>
</head>
<body>
{% if has_consented %}
<p>
Thank you, you've already accepted the license.
</p>
{% else %}
<p>
Please accept the license!
</p>
<form method="post" action="consent">
<input type="hidden" name="v" value="{{version}}"/>
<input type="hidden" name="u" value="{{user}}"/>
<input type="hidden" name="h" value="{{userhmac}}"/>
<input type="submit" value="Sure thing!"/>
</form>
{% endif %}
</body>
</html>
<head>
<title>Test Privacy policy</title>
</head>
<body>
{% if has_consented %}
<p>Thank you, you've already accepted the license.</p>
{% else %}
<p>Please accept the license!</p>
<form method="post" action="consent">
<input type="hidden" name="v" value="{{version}}" />
<input type="hidden" name="u" value="{{user}}" />
<input type="hidden" name="h" value="{{userhmac}}" />
<input type="submit" value="Sure thing!" />
</form>
{% endif %}
</body>
</html>

View file

@ -1,9 +1,9 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Privacy policy</title>
</head>
<body>
<p>Danke schon</p>
</body>
</html>
<head>
<title>Test Privacy policy</title>
</head>
<body>
<p>Danke schon</p>
</body>
</html>

View file

@ -2,60 +2,60 @@ server_name: "localhost"
pid_file: /data/homeserver.pid
public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ['::']
type: http
x_forwarded: true
- port: 8008
tls: false
bind_addresses: ["::"]
type: http
x_forwarded: true
resources:
- names: [client]
compress: false
resources:
- names: [client]
compress: false
database:
name: "sqlite3"
args:
database: ":memory:"
name: "sqlite3"
args:
database: ":memory:"
log_config: "/data/log.config"
rc_messages_per_second: 10000
rc_message_burst_count: 10000
rc_registration:
per_second: 10000
burst_count: 10000
per_second: 10000
burst_count: 10000
rc_joins:
local:
per_second: 9999
burst_count: 9999
remote:
per_second: 9999
burst_count: 9999
local:
per_second: 9999
burst_count: 9999
remote:
per_second: 9999
burst_count: 9999
rc_joins_per_room:
per_second: 9999
burst_count: 9999
per_second: 9999
burst_count: 9999
rc_3pid_validation:
per_second: 1000
burst_count: 1000
per_second: 1000
burst_count: 1000
rc_invites:
per_room:
per_second: 1000
burst_count: 1000
per_user:
per_second: 1000
burst_count: 1000
per_room:
per_second: 1000
burst_count: 1000
per_user:
per_second: 1000
burst_count: 1000
rc_login:
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
address:
per_second: 10000
burst_count: 10000
account:
per_second: 10000
burst_count: 10000
failed_attempts:
per_second: 10000
burst_count: 10000
media_store_path: "/data/media_store"
uploads_path: "/data/uploads"
@ -69,8 +69,8 @@ form_secret: "{{FORM_SECRET}}"
signing_key_path: "/data/localhost.signing.key"
trusted_key_servers:
- server_name: "matrix.org"
- server_name: "matrix.org"
suppress_key_server_warning: true
ui_auth:
session_timeout: "300s"
session_timeout: "300s"

View file

@ -17,7 +17,7 @@ limitations under the License.
import * as net from "net";
export async function getFreePort(): Promise<number> {
return new Promise<number>(resolve => {
return new Promise<number>((resolve) => {
const srv = net.createServer();
srv.listen(0, () => {
const port = (<net.AddressInfo>srv.address()).port;

View file

@ -32,7 +32,7 @@ declare global {
}
Cypress.Commands.add("tweakConfig", (tweaks: Record<string, any>): Chainable<AUTWindow> => {
return cy.window().then(win => {
return cy.window().then((win) => {
// note: we can't *set* the object because the window version is effectively a pointer.
for (const [k, v] of Object.entries(tweaks)) {
// @ts-ignore - for some reason it's not picking up on global.d.ts types.
@ -42,4 +42,4 @@ Cypress.Commands.add("tweakConfig", (tweaks: Record<string, any>): Chainable<AUT
});
// Needed to make this file a module
export { };
export {};

View file

@ -24,10 +24,10 @@ import Chainable = Cypress.Chainable;
function terminalLog(violations: axe.Result[]): void {
cy.task(
'log',
`${violations.length} accessibility violation${
violations.length === 1 ? '' : 's'
} ${violations.length === 1 ? 'was' : 'were'} detected`,
"log",
`${violations.length} accessibility violation${violations.length === 1 ? "" : "s"} ${
violations.length === 1 ? "was" : "were"
} detected`,
);
// pluck specific keys to keep the table readable
@ -38,24 +38,32 @@ function terminalLog(violations: axe.Result[]): void {
nodes: nodes.length,
}));
cy.task('table', violationData);
cy.task("table", violationData);
}
Cypress.Commands.overwrite("checkA11y", (
originalFn: Chainable["checkA11y"],
context?: string | Node | axe.ContextObject | undefined,
options: Options = {},
violationCallback?: ((violations: axe.Result[]) => void) | undefined,
skipFailures?: boolean,
): void => {
return originalFn(context, {
...options,
rules: {
// Disable contrast checking for now as we have too many issues with it
'color-contrast': {
enabled: false,
Cypress.Commands.overwrite(
"checkA11y",
(
originalFn: Chainable["checkA11y"],
context?: string | Node | axe.ContextObject | undefined,
options: Options = {},
violationCallback?: ((violations: axe.Result[]) => void) | undefined,
skipFailures?: boolean,
): void => {
return originalFn(
context,
{
...options,
rules: {
// Disable contrast checking for now as we have too many issues with it
"color-contrast": {
enabled: false,
},
...options.rules,
},
},
...options.rules,
},
}, violationCallback ?? terminalLog, skipFailures);
});
violationCallback ?? terminalLog,
skipFailures,
);
},
);

View file

@ -77,9 +77,9 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
opts = Object.assign({}, defaultCreateBotOptions, opts);
const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_");
return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => {
return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => {
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
return cy.window({ log: false }).then(win => {
return cy.window({ log: false }).then((win) => {
const cli = new win.matrixcs.MatrixClient({
baseUrl: synapse.baseUrl,
userId: credentials.userId,
@ -103,12 +103,17 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
}
return cy.wrap(
cli.initCrypto()
cli
.initCrypto()
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
.then(() => cli.startClient())
.then(() => cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); },
}))
.then(() =>
cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async (func) => {
await func({});
},
}),
)
.then(() => cli),
);
});
@ -129,13 +134,15 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
return cy.wrap(Promise.reject(`Bot room join failed. Cannot find room '${roomName}'`));
});
Cypress.Commands.add("botSendMessage", (
cli: MatrixClient,
roomId: string,
message: string,
): Chainable<ISendEventResponse> => {
return cy.wrap(cli.sendMessage(roomId, {
msgtype: "m.text",
body: message,
}), { log: false });
});
Cypress.Commands.add(
"botSendMessage",
(cli: MatrixClient, roomId: string, message: string): Chainable<ISendEventResponse> => {
return cy.wrap(
cli.sendMessage(roomId, {
msgtype: "m.text",
body: message,
}),
{ log: false },
);
},
);

View file

@ -66,7 +66,7 @@ declare global {
roomId: string,
threadId: string | null,
eventType: string,
content: IContent
content: IContent,
): Chainable<ISendEventResponse>;
/**
* @param {string} name
@ -89,10 +89,7 @@ declare global {
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a a Buffer, String or ReadStream.
*/
uploadContent(
file: FileType,
opts?: UploadOpts,
): Chainable<Awaited<Upload["promise"]>>;
uploadContent(file: FileType, opts?: UploadOpts): Chainable<Awaited<Upload["promise"]>>;
/**
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and
* may change.</strong>
@ -133,23 +130,24 @@ declare global {
}
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => {
return cy.window({ log: false }).then(win => win.mxMatrixClientPeg.matrixClient);
return cy.window({ log: false }).then((win) => win.mxMatrixClientPeg.matrixClient);
});
Cypress.Commands.add("getDmRooms", (userId: string): Chainable<string[]> => {
return cy.getClient()
.then(cli => cli.getAccountData("m.direct")?.getContent<Record<string, string[]>>())
.then(dmRoomMap => dmRoomMap[userId] ?? []);
return cy
.getClient()
.then((cli) => cli.getAccountData("m.direct")?.getContent<Record<string, string[]>>())
.then((dmRoomMap) => dmRoomMap[userId] ?? []);
});
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
return cy.window({ log: false }).then(async win => {
return cy.window({ log: false }).then(async (win) => {
const cli = win.mxMatrixClientPeg.matrixClient;
const resp = await cli.createRoom(options);
const roomId = resp.room_id;
if (!cli.getRoom(roomId)) {
await new Promise<void>(resolve => {
await new Promise<void>((resolve) => {
const onRoom = (room: Room) => {
if (room.roomId === roomId) {
cli.off(win.matrixcs.ClientEvent.Room, onRoom);
@ -168,7 +166,7 @@ Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable<string
return cy.createRoom({
...options,
creation_content: {
"type": "m.space",
type: "m.space",
},
});
});
@ -185,16 +183,14 @@ Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{
});
});
Cypress.Commands.add("sendEvent", (
roomId: string,
threadId: string | null,
eventType: string,
content: IContent,
): Chainable<ISendEventResponse> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.sendEvent(roomId, threadId, eventType, content);
});
});
Cypress.Commands.add(
"sendEvent",
(roomId: string, threadId: string | null, eventType: string, content: IContent): Chainable<ISendEventResponse> => {
return cy.getClient().then(async (cli: MatrixClient) => {
return cli.sendEvent(roomId, threadId, eventType, content);
});
},
);
Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => {
@ -215,13 +211,15 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
});
Cypress.Commands.add("bootstrapCrossSigning", () => {
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); },
authUploadDeviceSigningKeys: async (func) => {
await func({});
},
});
});
});
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => {
return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
return cy.getClient().then((cli) => cli.joinRoom(roomIdOrAlias));
});

View file

@ -41,7 +41,7 @@ declare global {
}
Cypress.Commands.add("mockClipboard", () => {
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
win.navigator.clipboard.writeText = (text) => {
copyText = text;
return Promise.resolve();
@ -54,4 +54,4 @@ Cypress.Commands.add("getClipboardText", (): Chainable<string> => {
});
// Needed to make this file a module
export { };
export {};

View file

@ -33,7 +33,7 @@ declare global {
}
Cypress.Commands.add("getComposer", (isRightPanel?: boolean): Chainable<JQuery> => {
const panelClass = isRightPanel ? '.mx_RightPanel' : '.mx_RoomView_body';
const panelClass = isRightPanel ? ".mx_RightPanel" : ".mx_RoomView_body";
return cy.get(`${panelClass} .mx_MessageComposer`);
});
@ -41,8 +41,8 @@ Cypress.Commands.add("openMessageComposerOptions", (isRightPanel?: boolean): Cha
cy.getComposer(isRightPanel).within(() => {
cy.get('[aria-label="More options"]').click();
});
return cy.get('.mx_MessageComposer_Menu');
return cy.get(".mx_MessageComposer_Menu");
});
// Needed to make this file a module
export { };
export {};

View file

@ -35,11 +35,15 @@ declare global {
// Inspired by https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
Cypress.Commands.add("accessIframe", (selector: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(selector)
.its("0.contentDocument.body").should("not.be.empty")
// Cypress loses types in the mess of wrapping, so force cast
.then(cy.wrap) as Chainable<JQuery<HTMLElement>>;
return (
cy
.get(selector)
.its("0.contentDocument.body")
.should("not.be.empty")
// Cypress loses types in the mess of wrapping, so force cast
.then(cy.wrap) as Chainable<JQuery<HTMLElement>>
);
});
// Needed to make this file a module
export { };
export {};

View file

@ -33,10 +33,13 @@ declare global {
}
Cypress.Commands.add("enableLabsFeature", (feature: string): Chainable<null> => {
return cy.window({ log: false }).then(win => {
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
}).then(() => null);
return cy
.window({ log: false })
.then((win) => {
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
})
.then(() => null);
});
// Needed to make this file a module
export { };
export {};

View file

@ -49,87 +49,97 @@ declare global {
* @param username login username
* @param password login password
*/
loginUser(
synapse: SynapseInstance,
username: string,
password: string,
): Chainable<UserCredentials>;
loginUser(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials>;
}
}
}
// eslint-disable-next-line max-len
Cypress.Commands.add("loginUser", (synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
return cy.request<{
access_token: string;
user_id: string;
device_id: string;
home_server: string;
}>({
url,
method: "POST",
body: {
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": username,
Cypress.Commands.add(
"loginUser",
(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
return cy
.request<{
access_token: string;
user_id: string;
device_id: string;
home_server: string;
}>({
url,
method: "POST",
body: {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: username,
},
password: password,
},
"password": password,
},
}).then(response => ({
password,
username,
accessToken: response.body.access_token,
userId: response.body.user_id,
deviceId: response.body.device_id,
homeServer: response.body.home_server,
}));
});
})
.then((response) => ({
password,
username,
accessToken: response.body.access_token,
userId: response.body.user_id,
deviceId: response.body.device_id,
homeServer: response.body.home_server,
}));
},
);
// eslint-disable-next-line max-len
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
// XXX: work around Cypress not clearing IDB between tests
cy.window({ log: false }).then(win => {
win.indexedDB.databases()?.then(databases => {
databases.forEach(database => {
win.indexedDB.deleteDatabase(database.name);
Cypress.Commands.add(
"initTestUser",
(synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
// XXX: work around Cypress not clearing IDB between tests
cy.window({ log: false }).then((win) => {
win.indexedDB.databases()?.then((databases) => {
databases.forEach((database) => {
win.indexedDB.deleteDatabase(database.name);
});
});
});
});
const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_");
return cy.registerUser(synapse, username, password, displayName).then(() => {
return cy.loginUser(synapse, username, password);
}).then(response => {
cy.log(`Registered test user ${username} with displayname ${displayName}`);
cy.window({ log: false }).then(win => {
// Seed the localStorage with the required credentials
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
win.localStorage.setItem("mx_user_id", response.userId);
win.localStorage.setItem("mx_access_token", response.accessToken);
win.localStorage.setItem("mx_device_id", response.deviceId);
win.localStorage.setItem("mx_is_guest", "false");
win.localStorage.setItem("mx_has_pickle_key", "false");
win.localStorage.setItem("mx_has_access_token", "true");
const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_");
return cy
.registerUser(synapse, username, password, displayName)
.then(() => {
return cy.loginUser(synapse, username, password);
})
.then((response) => {
cy.log(`Registered test user ${username} with displayname ${displayName}`);
cy.window({ log: false }).then((win) => {
// Seed the localStorage with the required credentials
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
win.localStorage.setItem("mx_user_id", response.userId);
win.localStorage.setItem("mx_access_token", response.accessToken);
win.localStorage.setItem("mx_device_id", response.deviceId);
win.localStorage.setItem("mx_is_guest", "false");
win.localStorage.setItem("mx_has_pickle_key", "false");
win.localStorage.setItem("mx_has_access_token", "true");
// Ensure the language is set to a consistent value
win.localStorage.setItem("mx_local_settings", '{"language":"en"}');
});
// Ensure the language is set to a consistent value
win.localStorage.setItem("mx_local_settings", '{"language":"en"}');
});
prelaunchFn?.();
prelaunchFn?.();
return cy.visit("/").then(() => {
// wait for the app to load
return cy.get(".mx_MatrixChat", { timeout: 30000 });
}).then(() => ({
password,
username,
accessToken: response.accessToken,
userId: response.userId,
deviceId: response.deviceId,
homeServer: response.homeServer,
}));
});
});
return cy
.visit("/")
.then(() => {
// wait for the app to load
return cy.get(".mx_MatrixChat", { timeout: 30000 });
})
.then(() => ({
password,
username,
accessToken: response.accessToken,
userId: response.userId,
deviceId: response.deviceId,
homeServer: response.homeServer,
}));
});
},
);

View file

@ -35,27 +35,35 @@ declare global {
Cypress.Commands.add("goOffline", (): void => {
cy.log("Going offline");
cy.window({ log: false }).then(win => {
cy.intercept("**/_matrix/**", {
headers: {
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
cy.window({ log: false }).then((win) => {
cy.intercept(
"**/_matrix/**",
{
headers: {
Authorization: "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
},
},
}, req => {
req.destroy();
});
(req) => {
req.destroy();
},
);
});
});
Cypress.Commands.add("goOnline", (): void => {
cy.log("Going online");
cy.window({ log: false }).then(win => {
cy.intercept("**/_matrix/**", {
headers: {
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
cy.window({ log: false }).then((win) => {
cy.intercept(
"**/_matrix/**",
{
headers: {
Authorization: "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
},
},
}, req => {
req.continue();
});
(req) => {
req.continue();
},
);
win.dispatchEvent(new Event("online"));
});
});
@ -85,4 +93,4 @@ Cypress.Commands.add("stubDefaultServer", (): void => {
});
// Needed to make this file a module
export { };
export {};

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
/// <reference types="cypress" />
import { SnapshotOptions as PercySnapshotOptions } from '@percy/core';
import { SnapshotOptions as PercySnapshotOptions } from "@percy/core";
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@ -39,16 +39,16 @@ declare global {
Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => {
cy.percySnapshot(name, {
domTransformation: documentClone => scope(documentClone, subject.selector),
domTransformation: (documentClone) => scope(documentClone, subject.selector),
...options,
});
});
function scope(documentClone: Document, selector: string): Document {
const element = documentClone.querySelector(selector);
documentClone.querySelector('body').innerHTML = element.outerHTML;
documentClone.querySelector("body").innerHTML = element.outerHTML;
return documentClone;
}
export { };
export {};

View file

@ -18,7 +18,7 @@ limitations under the License.
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
import { ProxyInstance } from '../plugins/sliding-sync';
import { ProxyInstance } from "../plugins/sliding-sync";
import { SynapseInstance } from "../plugins/synapsedocker";
declare global {
@ -49,7 +49,7 @@ function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
if (!proxy) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window({ log: false }).then((win) => {
win.location.href = 'about:blank';
win.location.href = "about:blank";
cy.task("proxyStop", proxy);
});
}

View file

@ -102,26 +102,27 @@ declare global {
}
Cypress.Commands.add("getSettingsStore", (): Chainable<typeof SettingsStore> => {
return cy.window({ log: false }).then(win => win.mxSettingsStore);
return cy.window({ log: false }).then((win) => win.mxSettingsStore);
});
Cypress.Commands.add("setSettingValue", (
name: string,
roomId: string,
level: SettingLevel,
value: any,
): Chainable<void> => {
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
return cy.wrap(store.setValue(name, roomId, level, value));
});
});
Cypress.Commands.add(
"setSettingValue",
(name: string, roomId: string, level: SettingLevel, value: any): Chainable<void> => {
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
return cy.wrap(store.setValue(name, roomId, level, value));
});
},
);
// eslint-disable-next-line max-len
Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T> => {
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
return store.getValue(name, roomId, excludeDefault);
});
});
Cypress.Commands.add(
"getSettingValue",
<T = any>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T> => {
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
return store.getValue(name, roomId, excludeDefault);
});
},
);
Cypress.Commands.add("openUserMenu", (): Chainable<JQuery<HTMLElement>> => {
cy.get('[aria-label="User menu"]').click();
@ -162,16 +163,22 @@ Cypress.Commands.add("closeDialog", (): Chainable<JQuery<HTMLElement>> => {
});
Cypress.Commands.add("joinBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
});
return cy
.contains(".mx_BetaCard_title", name)
.closest(".mx_BetaCard")
.within(() => {
return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
});
});
Cypress.Commands.add("leaveBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
});
return cy
.contains(".mx_BetaCard_title", name)
.closest(".mx_BetaCard")
.within(() => {
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
});
});
// Needed to make this file a module
export { };
export {};

View file

@ -16,7 +16,7 @@ limitations under the License.
/// <reference types="cypress" />
import * as crypto from 'crypto';
import * as crypto from "crypto";
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
@ -64,7 +64,7 @@ function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
if (!synapse) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window({ log: false }).then((win) => {
win.location.href = 'about:blank';
win.location.href = "about:blank";
cy.task("synapseStop", synapse.synapseId);
});
}
@ -83,38 +83,42 @@ function registerUser(
displayName?: string,
): Chainable<Credentials> {
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
return cy.then(() => {
// get a nonce
return cy.request<{ nonce: string }>({ url });
}).then(response => {
const { nonce } = response.body;
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
`${nonce}\0${username}\0${password}\0notadmin`,
).digest('hex');
return cy
.then(() => {
// get a nonce
return cy.request<{ nonce: string }>({ url });
})
.then((response) => {
const { nonce } = response.body;
const mac = crypto
.createHmac("sha1", synapse.registrationSecret)
.update(`${nonce}\0${username}\0${password}\0notadmin`)
.digest("hex");
return cy.request<{
access_token: string;
user_id: string;
home_server: string;
device_id: string;
}>({
url,
method: "POST",
body: {
nonce,
username,
password,
mac,
admin: false,
displayname: displayName,
},
});
}).then(response => ({
homeServer: response.body.home_server,
accessToken: response.body.access_token,
userId: response.body.user_id,
deviceId: response.body.device_id,
}));
return cy.request<{
access_token: string;
user_id: string;
home_server: string;
device_id: string;
}>({
url,
method: "POST",
body: {
nonce,
username,
password,
mac,
admin: false,
displayname: displayName,
},
});
})
.then((response) => ({
homeServer: response.body.home_server,
accessToken: response.body.access_token,
userId: response.body.user_id,
deviceId: response.body.device_id,
}));
}
Cypress.Commands.add("startSynapse", startSynapse);

View file

@ -38,17 +38,19 @@ export interface Message {
}
Cypress.Commands.add("scrollToTop", (): void => {
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
if (ref.scrollTop() > 0) {
return cy.scrollToTop();
}
});
cy.get(".mx_RoomView_timeline .mx_ScrollPanel")
.scrollTo("top", { duration: 100 })
.then((ref) => {
if (ref.scrollTop() > 0) {
return cy.scrollToTop();
}
});
});
Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<JQuery> => {
// We can't just use a bunch of `.contains` here due to continuations meaning that the events don't
// have their own rendered sender displayname so we have to walk the list to keep track of the sender.
return cy.get(".mx_RoomView_MessageList .mx_EventTile").then(refs => {
return cy.get(".mx_RoomView_MessageList .mx_EventTile").then((refs) => {
let latestSender: string;
for (let i = 0; i < refs.length; i++) {
const ref = refs.eq(i);
@ -65,4 +67,4 @@ Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<
});
// Needed to make this file a module
export { };
export {};

View file

@ -29,7 +29,7 @@ declare global {
interface cy {
all<T extends Cypress.Chainable[] | []>(
commands: T
commands: T,
): Cypress.Chainable<{ [P in keyof T]: ChainableValue<T[P]> }>;
queue: any;
}
@ -59,16 +59,16 @@ cy.all = function all(commands): Cypress.Chainable {
return cy.wrap(
// @see https://lodash.com/docs/4.17.15#lodash
Cypress._(commands)
.map(cmd => {
.map((cmd) => {
return cmd[chainStart]
? cmd[chainStart].attributes
: Cypress._.find(cy.queue.get(), {
attributes: { chainerId: cmd.chainerId },
}).attributes;
attributes: { chainerId: cmd.chainerId },
}).attributes;
})
.concat(stopCommand.attributes)
.slice(1)
.map(cmd => {
.map((cmd) => {
return cmd.prev.get("subject");
})
.value(),
@ -79,4 +79,4 @@ cy.all = function all(commands): Cypress.Chainable {
};
// Needed to make this file a module
export { };
export {};

View file

@ -70,4 +70,4 @@ Cypress.Commands.add("viewSpaceHomeByName", (name: string): Chainable<JQuery<HTM
});
// Needed to make this file a module
export { };
export {};

View file

@ -49,4 +49,4 @@ Cypress.Commands.add("serveHtmlFile", serveHtmlFile);
Cypress.Commands.add("stopWebServers", stopWebServers);
// Needed to make this file a module
export { };
export {};

View file

@ -2,22 +2,12 @@
"compilerOptions": {
"target": "es2016",
"jsx": "react",
"lib": [
"es2020",
"dom",
"dom.iterable"
],
"types": [
"cypress",
"cypress-axe",
"@percy/cypress"
],
"lib": ["es2020", "dom", "dom.iterable"],
"types": ["cypress", "cypress-axe", "@percy/cypress"],
"resolveJsonModule": true,
"esModuleInterop": true,
"moduleResolution": "node",
"module": "commonjs"
},
"include": [
"**/*.ts"
]
"include": ["**/*.ts"]
}

View file

@ -64,8 +64,8 @@ the content string, caret nodes need to be ignored, as they would confuse the mo
As part of the reconciliation, the caret position is also adjusted to any changes
the model made to the input. The caret is passed around in two formats.
The model receives the caret *offset* within the content string (which includes
The model receives the caret _offset_ within the content string (which includes
an atNodeEnd flag to make it unambiguous if it is at a part and or the next part start).
The model converts this to a caret *position* internally, which has a partIndex
The model converts this to a caret _position_ internally, which has a partIndex
and an offset within the part text, which is more natural to work with.
From there on, the caret *position* is used, also during reconciliation.
From there on, the caret _position_ is used, also during reconciliation.

View file

@ -1,14 +1,17 @@
# Cypress in Element Web
## Scope of this Document
This doc is about our Cypress tests in Element Web and how we use Cypress to write tests.
It aims to cover:
* How to run the tests yourself
* How the tests work
* How to write great Cypress tests
* Visual testing
- How to run the tests yourself
- How the tests work
- How to write great Cypress tests
- Visual testing
## Running the Tests
Our Cypress tests run automatically as part of our CI along with our other tests,
on every pull request and on every merge to develop & master.
@ -43,6 +46,7 @@ yarn run test:cypress:open
```
## How the Tests Work
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
as is typical for Cypress tests. Likewise, tests live in `cypress/e2e`.
@ -68,6 +72,7 @@ with each instance in a separate directory named after its ID. These logs are re
at the start of each test run.
## Writing Tests
Mostly this is the same advice as for writing any other Cypress test: the Cypress
docs are well worth a read if you're not already familiar with Cypress testing, eg.
https://docs.cypress.io/guides/references/best-practices. To avoid your tests being
@ -75,11 +80,12 @@ flaky it is also recommended to give https://docs.cypress.io/guides/core-concept
a read.
### Getting a Synapse
The key difference is in starting Synapse instances. Tests use this plugin via
The key difference is in starting Synapse instances. Tests use this plugin via
`cy.startSynapse()` to provide a Synapse instance to log into:
```javascript
cy.startSynapse("consent").then(result => {
cy.startSynapse("consent").then((result) => {
synapse = result;
});
```
@ -96,32 +102,38 @@ Synapse instance for each test suite, i.e. in `before()`, and then tear it down
To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
object you received when starting it.
```javascript
cy.stopSynapse(synapse);
```
### Synapse Config Templates
When a Synapse instance is started, it's given a config generated from one of the config
templates in `cypress/plugins/synapsedocker/templates`. There are a couple of special files
in these templates:
* `homeserver.yaml`:
Template substitution happens in this file. Template variables are:
* `REGISTRATION_SECRET`: The secret used to register users via the REST API.
* `MACAROON_SECRET_KEY`: Generated each time for security
* `FORM_SECRET`: Generated each time for security
* `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
* `localhost.signing.key`: A signing key is auto-generated and saved to this file.
Config templates should not contain a signing key and instead assume that one will exist
in this file.
- `homeserver.yaml`:
Template substitution happens in this file. Template variables are:
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
- `MACAROON_SECRET_KEY`: Generated each time for security
- `FORM_SECRET`: Generated each time for security
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
Config templates should not contain a signing key and instead assume that one will exist
in this file.
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
in a template can be referenced in the config as `/data/foo.html`.
### Logging In
There exists a basic utility to start the app with a random user already logged in:
```javascript
cy.initTestUser(synapse, "Jeff");
```
It takes the SynapseInstance you received from `startSynapse` and a display name for your test user.
This custom command will register a random userId using the registrationSecret with a random password
and the given display name. The returned Chainable will contain details about the credentials for if
@ -132,20 +144,24 @@ The internals of how this custom command run may be swapped out later,
but the signature can be maintained for simpler maintenance.
### Joining a Room
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
way to do this may be to get an access token for the user and use this to create a room with the REST
API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
### Convenience APIs
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
creation that can be called to set up tests.
### Using matrix-js-sdk
Due to the way we run the Cypress tests in CI, at this time you can only use the matrix-js-sdk module
exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
This may be revisited in the future.
## Good Test Hygiene
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
already familiar with Cypress.
@ -158,11 +174,11 @@ already familiar with Cypress.
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
all assertions are retired until they either pass or time out, so you should never need to
manually wait for an element.
* For example, for asserting about editing an already-edited message, you can't wait for the
- For example, for asserting about editing an already-edited message, you can't wait for the
'edited' element to appear as there was already one there, but you can assert that the body
of the message is what is should be after the second edit and this assertion will pass once
it becomes true. You can then assert that the 'edited' element is still in the DOM.
* You can also wait for other things like network requests in the
- You can also wait for other things like network requests in the
browser to complete (https://docs.cypress.io/guides/guides/network-requests#Waiting).
Needing to wait for things can also be because of race conditions in the app itself, which ideally
shouldn't be there!
@ -171,6 +187,7 @@ This is a small selection - the Cypress best practices guide, linked above, has
should generally try to adhere to them.
## Percy Visual Testing
We also support visual testing via Percy, this extracts the DOM from Cypress and renders it using custom renderers
for Safari, Firefox, Chrome & Edge, allowing us to spot visual regressions before they become release regressions.
Each `cy.percySnapshot()` call results in 8 screenshots (4 browsers, 2 sizes) this can quickly be exhausted and
@ -178,4 +195,3 @@ so we only run Percy testing on `develop` and PRs which are labelled `X-Needs-Pe
To record a snapshot use `cy.percySnapshot()`, you may have to pass `percyCSS` into the 2nd argument to hide certain
elements which contain dynamic/generated data to avoid them cause false positives in the Percy screenshot diffs.

View file

@ -1,37 +1,38 @@
# Composer Features
## Auto Complete
- 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
- @ + 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 anywhere else in composer, only a space is appended to the pill
- # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji
- 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,
- 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
- @ + 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 anywhere else in composer, only a space is appended to the pill
- # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji
- 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,
wrapping around at the end after reverting to the typed text first.
## Formatting
- When selecting text, a formatting bar appears above the selection.
- The formatting bar allows to format the selected test as:
- When selecting text, a formatting bar appears above the selection.
- 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).
- Formatting is applied as markdown syntax.
- 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+> also marks the selected text as a blockquote
- Formatting is applied as markdown syntax.
- 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+> also marks the selected text as a blockquote
## 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.
- 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
- Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break
- 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 "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.
- 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
- Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break
- 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 "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.

View file

@ -17,7 +17,7 @@ Let's say we want to close a menu when the correct keys were pressed:
```ts
const onKeyDown = (ev: KeyboardEvent): void => {
let handled = true;
const action = getKeyBindingManager().getAccessibilityAction(ev)
const action = getKeyBindingManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Escape:
closeMenu();
@ -26,12 +26,12 @@ const onKeyDown = (ev: KeyboardEvent): void => {
handled = false;
break;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
}
};
```
## Managing keyboard shortcuts

View file

@ -6,6 +6,7 @@ Each .svg exports a `ReactComponent` at the named export `Icon`.
Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component.
eg
```
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';

View file

@ -6,23 +6,25 @@ instructions on setting up Jitsi.
The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html`
which takes several parameters:
*Query string*:
* `widgetId`: The ID of the widget. This is needed for communication back to the
react-sdk.
* `parentUrl`: The URL of the parent window. This is also needed for
communication back to the react-sdk.
_Query string_:
*Hash/fragment (formatted as a query string)*:
* `conferenceDomain`: The domain 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
be present, should default to `false`.
* `displayName`: The display name of the user viewing the widget. May not
be present or could be null.
* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
not be present or could be null.
* `userId`: The MXID of the user viewing the widget. May not be present or could
be null.
- `widgetId`: The ID of the widget. This is needed for communication back to the
react-sdk.
- `parentUrl`: The URL of the parent window. This is also needed for
communication back to the react-sdk.
_Hash/fragment (formatted as a query string)_:
- `conferenceDomain`: The domain 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
be present, should default to `false`.
- `displayName`: The display name of the user viewing the widget. May not
be present or could be null.
- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
not be present or could be null.
- `userId`: The MXID of the user viewing the widget. May not be present or could
be null.
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`.

View file

@ -10,7 +10,7 @@ implementation, such as the `RoomEchoChamber` (which handles echoable details of
Anything that can be locally echoed will be provided by the `GenericEchoChamber` implementation.
The echo chamber will also need to deal with external changes, and has full control over whether
or not something has successfully been echoed.
or not something has successfully been echoed.
An `EchoContext` is provided to echo chambers (usually with a matching type: `RoomEchoContext`
gets provided to a `RoomEchoChamber` for example) with details about their intended area of
@ -21,7 +21,7 @@ The `EchoStore` manages echo chamber instances, builds contexts, and is generall
accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things
tidy, this is an intentional design decision.
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
`EventEmitter`. Whenables are ways of actioning a changing condition without having to deal
with listeners being torn down. Once the reference count of the Whenable causes garbage collection,
the Whenable's listeners will also be torn down. This is accelerated by the `IDestroyable` interface
@ -36,4 +36,3 @@ mechanisms.
The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left)
is set up, where the dialog then drives through the contexts and transactions.

View file

@ -5,11 +5,12 @@ It's so complicated it needs its own README.
![](img/RoomListStore2.png)
Legend:
* Orange = External event.
* Purple = Deterministic flow.
* Green = Algorithm definition.
* Red = Exit condition/point.
* Blue = Process definition.
- Orange = External event.
- Purple = Deterministic flow.
- Green = Algorithm definition.
- Red = Exit condition/point.
- Blue = Process definition.
## Algorithms involved
@ -22,7 +23,6 @@ Behaviour of the overall room list (sticky rooms, etc) are determined by the gen
class. Here is where much of the coordination from the room list store is done to figure out which list
algorithm to call, instead of having all the logic in the room list store itself.
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
@ -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
relative (perceived) importance to 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
messages which cause a push notification or badge count. Typically, this is the default as rooms get
set to 'All Messages'.
* **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').
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
last read it.
- **Red**: The room has unread mentions waiting for the user.
- **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
set to 'All Messages'.
- **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').
- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
last read it.
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
above bold, etc.

View file

@ -8,7 +8,6 @@ During an onscroll event, we check whether we're getting close to the top or bot
ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline.
## BACAT (Bottom-Aligned, Clipped-At-Top) scrolling
BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842.

View file

@ -5,23 +5,22 @@ different values for a setting at particular levels of interest. For example, a
they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity
of dealing with the different levels and exposes easy to use getters and setters.
## Levels
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:
* `device` - The current user's device
* `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
* `account` - The current user's account
* `room` - A specific room (setting for all members of the room)
* `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
* `default` - The hardcoded default for the settings
- `device` - The current user's device
- `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
- `account` - The current user's account
- `room` - A specific room (setting for all members of the room)
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
- `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
that room administrators cannot force account-only settings upon participants.
## Settings
Settings are the different options a user may set or experience in the application. These are pre-defined in
@ -29,6 +28,7 @@ Settings are the different options a user may set or experience in the applicati
Settings that support the config level can be set in the config file under the `setting_defaults` key (note that some
settings, like the "theme" setting, are special cased in the config file):
```json5
{
...
@ -56,13 +56,14 @@ target level.
Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a
clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue
although there are circumstances where this changes. An example of a safe call is:
```javascript
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
if (isSupported) {
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
}
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
}
}
```
@ -73,19 +74,14 @@ instance, the component which allows changing the setting may be hidden conditio
Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The
`SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
```html
<SettingsFlag name="theSettingId"
level={SettingsLevel.ROOM}
roomId="!curbf:matrix.org"
label={_td("Your label here")} // optional, if falsey then the `SettingsStore` will be used
onChange={function(newValue) { }} // optional, called after saving
isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false
manualSave={false} // if true, saving is delayed. You will need to call .save() on this component
// Options for radio buttons
group="your-radio-group" // this enables radio button support
value="yourValueHere" // the value for this particular option
/>
```html
<SettingsFlag name="theSettingId" level={SettingsLevel.ROOM} roomId="!curbf:matrix.org" label={_td("Your label here")}
// optional, if falsey then the `SettingsStore` will be used onChange={function(newValue) { }} // optional, called after
saving isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false manualSave={false}
// if true, saving is delayed. You will need to call .save() on this component // Options for radio buttons
group="your-radio-group" // this enables radio button support value="yourValueHere" // the value for this particular
option />
```
### Getting the display name for a setting
@ -93,16 +89,16 @@ Where possible, the `SettingsFlag` component should be used to set simple "flip-
Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated
for you. If a display name cannot be found, it will return `null`.
## Features
Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually
a feature flag is used when a portion of the application is under development or not ready for full release yet, such
as new functionality or experimental ideas. In these cases, the feature name *should* be named with the `feature_*`
as new functionality or experimental ideas. In these cases, the feature name _should_ be named with the `feature_*`
convention and must be tagged with `isFeature: true` in the setting definition. By doing so, the feature will automatically
appear in the "labs" section of the user's settings.
Features can be controlled at the config level using the following structure:
```json
"features": {
"feature_lazyloading": true
@ -144,7 +140,6 @@ additional steps to actually enable notifications.
For more information, see `src/settings/controllers/SettingController.ts`.
## Local echo
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a
@ -160,7 +155,6 @@ SettingsStore.setValue(...).then(() => {
SettingsStore.getValue(...); // this will return the value set in `setValue` above.
```
## Watching for changes
Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the
@ -174,12 +168,11 @@ An example of a watcher in action would be:
```javascript
class MyComponent extends React.Component {
settingWatcherRef = null;
componentWillMount() {
const callback = (settingName, roomId, level, newValAtLevel, newVal) => {
this.setState({color: newVal});
this.setState({ color: newVal });
};
this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", callback);
}
@ -190,7 +183,6 @@ class MyComponent extends React.Component {
}
```
# Maintainers Reference
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is

View file

@ -13,12 +13,12 @@ It exposes a function over a postMessage API, when sent an object with the match
```json5
{
"imgSrc": "", // the src of the image to display in the download link
"imgStyle": "", // the style to apply to the image
"style": "", // the style to apply to the download link
"download": "", // download attribute to pass to the <a/> tag
"textContent": "", // the text to put inside the download link
"blob": "", // the data blob to wrap in an object url and allow the user to download
imgSrc: "", // the src of the image to display in the download link
imgStyle: "", // the style to apply to the image
style: "", // the style to apply to the download link
download: "", // download attribute to pass to the <a/> tag
textContent: "", // the text to put inside the download link
blob: "", // the data blob to wrap in an object url and allow the user to download
}
```

View file

@ -4,24 +4,25 @@ Rooms can have a default widget layout to auto-pin certain widgets, make the con
sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key).
Full example content:
```json5
{
"widgets": {
"first-widget-id": {
"container": "top",
"index": 0,
"width": 60,
"height": 40
widgets: {
"first-widget-id": {
container: "top",
index: 0,
width: 60,
height: 40,
},
"second-widget-id": {
container: "right",
},
},
"second-widget-id": {
"container": "right"
}
}
}
```
As shown, there are two containers possible for widgets. These containers have different behaviour
and interpret the other options differently.
and interpret the other options differently.
## `top` container
@ -32,7 +33,7 @@ therefore fewer messages can be shown).
The `index` for a widget determines which order the widgets show up in from left to right. Widgets
without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined
without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
represent leftmost widgets.
The `width` is relative width within the container in percentage points. This will be clamped to a
@ -43,7 +44,7 @@ attempt to show them at 33% width each.
Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning
hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions.
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
capabilities in future containers. The top container will take the tallest `height` and use that for
the height of the whole container, and thus all widgets in that container. The `height` is relative
to the container, like with `width`, meaning that 100% will consume as much space as the client is

View file

@ -1,261 +1,261 @@
{
"name": "matrix-react-sdk",
"version": "3.62.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-react-sdk"
},
"license": "Apache-2.0",
"files": [
"lib",
"res",
"src",
"scripts",
"git-revision.txt",
"docs",
"header",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
".stylelintrc.js"
],
"main": "./src/index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_lib_main": "./lib/index.ts",
"matrix_lib_typings": "./lib/index.d.ts",
"matrix_i18n_extra_translation_funcs": [
"newTranslatableError"
],
"scripts": {
"prepublishOnly": "yarn build",
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"make-component": "node scripts/make-react-component.js",
"rethemendex": "res/css/rethemendex.sh",
"clean": "rimraf lib",
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js,.tsx\" src",
"build:types": "tsc --emitDeclarationOnly --jsx react",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all",
"start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build",
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 src test cypress && prettier --check .",
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src test cypress",
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p cypress",
"lint:style": "stylelint \"res/css/**/*.pcss\"",
"test": "jest",
"test:cypress": "cypress run",
"test:cypress:open": "cypress open",
"coverage": "yarn test --coverage"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.3.0",
"@matrix-org/matrix-wysiwyg": "^0.9.0",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"@sentry/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"@testing-library/react-hooks": "^8.0.1",
"await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6",
"commonmark": "^0.30.0",
"counterpart": "^0.18.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
"emojibase": "6.1.0",
"emojibase-data": "7.0.1",
"emojibase-regex": "6.0.1",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "10.0.5",
"flux": "4.0.3",
"focus-visible": "^5.2.0",
"gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
"html-entities": "^2.0.0",
"is-ip": "^3.1.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-element": "4.0.0-beta.4",
"linkify-string": "4.0.0-beta.4",
"linkifyjs": "4.0.0-beta.4",
"lodash": "^4.17.20",
"maplibre-gl": "^1.15.2",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.1.1",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.36.0",
"qrcode": "1.5.1",
"re-resizable": "^6.9.0",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.2.0",
"react-dom": "17.0.2",
"react-focus-lock": "^2.5.1",
"react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "^2.3.2",
"tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2",
"url": "^0.11.0",
"what-input": "^5.2.10",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/parser": "^7.12.11",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/traverse": "^7.12.12",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@peculiar/webcrypto": "^1.4.1",
"@percy/cli": "^1.11.0",
"@percy/cypress": "^3.1.2",
"@sinonjs/fake-timers": "^9.1.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3",
"@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1",
"@types/css-font-loading-module": "^0.0.7",
"@types/diff-match-patch": "^1.0.32",
"@types/enzyme": "^3.10.9",
"@types/escape-html": "^1.0.1",
"@types/file-saver": "^2.0.3",
"@types/flux": "^3.1.9",
"@types/fs-extra": "^9.0.13",
"@types/geojson": "^7946.0.8",
"@types/jest": "^29.2.1",
"@types/katex": "^0.14.0",
"@types/lodash": "^4.14.168",
"@types/modernizr": "^3.5.3",
"@types/node": "^16",
"@types/pako": "^2.0.0",
"@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "17.0.49",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "17.0.17",
"@types/react-test-renderer": "^17.0.1",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1",
"@types/ua-parser-js": "^0.7.36",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.6.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"allchange": "^1.1.0",
"axe-core": "4.4.3",
"babel-jest": "^29.0.0",
"blob-polyfill": "^7.0.0",
"chokidar": "^3.5.1",
"cypress": "^11.0.0",
"cypress-axe": "^1.0.0",
"cypress-real-events": "^1.7.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"eslint": "8.28.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "0.9.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",
"fetch-mock-jest": "^1.5.1",
"fs-extra": "^11.0.0",
"glob": "^8.0.0",
"jest": "^29.2.2",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^29.2.2",
"jest-mock": "^29.2.2",
"jest-raw-loader": "^1.0.1",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^1.3.0",
"node-fetch": "2",
"postcss-scss": "^4.0.4",
"prettier": "2.8.0",
"raw-loader": "^4.0.2",
"react-test-renderer": "^17.0.2",
"rimraf": "^3.0.2",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "4.9.3",
"walk": "^2.3.14"
},
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"testEnvironment": "jsdom",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
],
"globalSetup": "<rootDir>/test/globalSetup.js",
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"<rootDir>/test/setupTests.js"
],
"moduleNameMapper": {
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
"^!!raw-loader!.*": "jest-raw-loader",
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
"name": "matrix-react-sdk",
"version": "3.62.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-react-sdk"
},
"transformIgnorePatterns": [
"/node_modules/(?!matrix-js-sdk).+$"
"license": "Apache-2.0",
"files": [
"lib",
"res",
"src",
"scripts",
"git-revision.txt",
"docs",
"header",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
".stylelintrc.js"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts,tsx}"
"main": "./src/index.ts",
"matrix_src_main": "./src/index.ts",
"matrix_lib_main": "./lib/index.ts",
"matrix_lib_typings": "./lib/index.d.ts",
"matrix_i18n_extra_translation_funcs": [
"newTranslatableError"
],
"coverageReporters": [
"text-summary",
"lcov"
],
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
}
"scripts": {
"prepublishOnly": "yarn build",
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"make-component": "node scripts/make-react-component.js",
"rethemendex": "res/css/rethemendex.sh",
"clean": "rimraf lib",
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js,.tsx\" src",
"build:types": "tsc --emitDeclarationOnly --jsx react",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all",
"start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build",
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 src test cypress && prettier --check .",
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src test cypress",
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p cypress",
"lint:style": "stylelint \"res/css/**/*.pcss\"",
"test": "jest",
"test:cypress": "cypress run",
"test:cypress:open": "cypress open",
"coverage": "yarn test --coverage"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.3.0",
"@matrix-org/matrix-wysiwyg": "^0.9.0",
"@matrix-org/react-sdk-module-api": "^0.0.3",
"@sentry/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"@testing-library/react-hooks": "^8.0.1",
"await-lock": "^2.1.0",
"blurhash": "^1.1.3",
"cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6",
"commonmark": "^0.30.0",
"counterpart": "^0.18.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
"emojibase": "6.1.0",
"emojibase-data": "7.0.1",
"emojibase-regex": "6.0.1",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "10.0.5",
"flux": "4.0.3",
"focus-visible": "^5.2.0",
"gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
"html-entities": "^2.0.0",
"is-ip": "^3.1.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-element": "4.0.0-beta.4",
"linkify-string": "4.0.0-beta.4",
"linkifyjs": "4.0.0-beta.4",
"lodash": "^4.17.20",
"maplibre-gl": "^1.15.2",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.1.1",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.36.0",
"qrcode": "1.5.1",
"re-resizable": "^6.9.0",
"react": "17.0.2",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.2.0",
"react-dom": "17.0.2",
"react-focus-lock": "^2.5.1",
"react-transition-group": "^4.4.1",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "^2.3.2",
"tar-js": "^0.3.0",
"ua-parser-js": "^1.0.2",
"url": "^0.11.0",
"what-input": "^5.2.10",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/parser": "^7.12.11",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/traverse": "^7.12.12",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@peculiar/webcrypto": "^1.4.1",
"@percy/cli": "^1.11.0",
"@percy/cypress": "^3.1.2",
"@sinonjs/fake-timers": "^9.1.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^14.4.3",
"@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4",
"@types/counterpart": "^0.18.1",
"@types/css-font-loading-module": "^0.0.7",
"@types/diff-match-patch": "^1.0.32",
"@types/enzyme": "^3.10.9",
"@types/escape-html": "^1.0.1",
"@types/file-saver": "^2.0.3",
"@types/flux": "^3.1.9",
"@types/fs-extra": "^9.0.13",
"@types/geojson": "^7946.0.8",
"@types/jest": "^29.2.1",
"@types/katex": "^0.14.0",
"@types/lodash": "^4.14.168",
"@types/modernizr": "^3.5.3",
"@types/node": "^16",
"@types/pako": "^2.0.0",
"@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "17.0.49",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "17.0.17",
"@types/react-test-renderer": "^17.0.1",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1",
"@types/ua-parser-js": "^0.7.36",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^5.35.1",
"@typescript-eslint/parser": "^5.6.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"allchange": "^1.1.0",
"axe-core": "4.4.3",
"babel-jest": "^29.0.0",
"blob-polyfill": "^7.0.0",
"chokidar": "^3.5.1",
"cypress": "^11.0.0",
"cypress-axe": "^1.0.0",
"cypress-real-events": "^1.7.1",
"enzyme": "^3.11.0",
"enzyme-to-json": "^3.6.2",
"eslint": "8.28.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "0.9.0",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",
"fetch-mock-jest": "^1.5.1",
"fs-extra": "^11.0.0",
"glob": "^8.0.0",
"jest": "^29.2.2",
"jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom": "^29.2.2",
"jest-mock": "^29.2.2",
"jest-raw-loader": "^1.0.1",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^1.3.0",
"node-fetch": "2",
"postcss-scss": "^4.0.4",
"prettier": "2.8.0",
"raw-loader": "^4.0.2",
"react-test-renderer": "^17.0.2",
"rimraf": "^3.0.2",
"stylelint": "^14.9.1",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-standard": "^29.0.0",
"stylelint-scss": "^4.2.0",
"typescript": "4.9.3",
"walk": "^2.3.14"
},
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"testEnvironment": "jsdom",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
],
"globalSetup": "<rootDir>/test/globalSetup.js",
"setupFiles": [
"jest-canvas-mock"
],
"setupFilesAfterEnv": [
"<rootDir>/test/setupTests.js"
],
"moduleNameMapper": {
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
"^!!raw-loader!.*": "jest-raw-loader",
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
},
"transformIgnorePatterns": [
"/node_modules/(?!matrix-js-sdk).+$"
],
"collectCoverageFrom": [
"<rootDir>/src/**/*.{js,ts,tsx}"
],
"coverageReporters": [
"text-summary",
"lcov"
],
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
},
"@casualbot/jest-sonar-reporter": {
"outputDirectory": "coverage",
"outputName": "jest-sonar-report.xml",
"relativePaths": true
}
}

View file

@ -1,4 +1,3 @@
subprojects:
matrix-js-sdk:
includeByDefault: false

Some files were not shown because too many files have changed in this diff Show more