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

View file

@ -2,9 +2,9 @@
## Checklist ## Checklist
* [ ] Tests written for new code (and old code if feasible) - [ ] Tests written for new code (and old code if feasible)
* [ ] Linter and other CI checks pass - [ ] 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)) - [ ] 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: 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", "$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [ "extends": ["github>matrix-org/renovate-config-element-web"]
"github>matrix-org/renovate-config-element-web"
]
} }

View file

@ -1,30 +1,30 @@
name: Backport name: Backport
on: on:
pull_request_target: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
branches: branches:
- develop - develop
jobs: jobs:
backport: backport:
name: Backport name: Backport
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Only react to merged PRs for security reasons. # Only react to merged PRs for security reasons.
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
if: > if: >
github.event.pull_request.merged github.event.pull_request.merged
&& ( && (
github.event.action == 'closed' github.event.action == 'closed'
|| ( || (
github.event.action == 'labeled' github.event.action == 'labeled'
&& contains(github.event.label.name, 'backport') && contains(github.event.label.name, 'backport')
) )
) )
steps: steps:
- uses: tibdex/backport@v2 - uses: tibdex/backport@v2
with: with:
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>" labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
# We can't use GITHUB_TOKEN here or CI won't run on the new PR # We can't use GITHUB_TOKEN here or CI won't run on the new PR
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }} 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 # Triggers after the layered build has finished, taking the artifact and running cypress on it
name: Cypress End to End Tests name: Cypress End to End Tests
on: on:
workflow_run: workflow_run:
workflows: [ "Element Web - Build" ] workflows: ["Element Web - Build"]
types: types:
- completed - completed
jobs: jobs:
prepare: prepare:
name: Prepare name: Prepare
if: github.event.workflow_run.conclusion == 'success' if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
issues: read issues: read
statuses: write statuses: write
pull-requests: read pull-requests: read
outputs: outputs:
uuid: ${{ steps.uuid.outputs.value }} uuid: ${{ steps.uuid.outputs.value }}
pr_id: ${{ steps.prdetails.outputs.pr_id }} pr_id: ${{ steps.prdetails.outputs.pr_id }}
commit_message: ${{ steps.commit.outputs.message }} commit_message: ${{ steps.commit.outputs.message }}
commit_author: ${{ steps.commit.outputs.author }} commit_author: ${{ steps.commit.outputs.author }}
commit_email: ${{ steps.commit.outputs.email }} commit_email: ${{ steps.commit.outputs.email }}
percy_enable: ${{ steps.percy.outputs.value || '1' }} percy_enable: ${{ steps.percy.outputs.value || '1' }}
steps: steps:
# We create the status here and then update it to success/failure in the `report` stage # 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. # This provides an easy link to this workflow_run from the PR before Cypress is done.
- uses: Sibz/github-status-action@v1 - uses: Sibz/github-status-action@v1
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: pending state: pending
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }} sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- id: prdetails - id: prdetails
if: github.event.workflow_run.event == 'pull_request' if: github.event.workflow_run.event == 'pull_request'
uses: matrix-org/pr-details-action@v1.2 uses: matrix-org/pr-details-action@v1.2
with: with:
owner: ${{ github.event.workflow_run.head_repository.owner.login }} owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }} branch: ${{ github.event.workflow_run.head_branch }}
- name: Get commit details - name: Get commit details
id: commit id: commit
if: github.event.workflow_run.event == 'pull_request' if: github.event.workflow_run.event == 'pull_request'
uses: actions/github-script@v6 uses: actions/github-script@v6
with: with:
script: | script: |
const response = await github.rest.git.getCommit({ const response = await github.rest.git.getCommit({
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
commit_sha: "${{ github.event.workflow_run.head_sha }}", commit_sha: "${{ github.event.workflow_run.head_sha }}",
}); });
core.setOutput("message", response.data.message); core.setOutput("message", response.data.message);
core.setOutput("author", response.data.author.name); core.setOutput("author", response.data.author.name);
core.setOutput("email", response.data.author.email); core.setOutput("email", response.data.author.email);
# Only run Percy when it is demanded or on develop # Only run Percy when it is demanded or on develop
- name: Disable Percy if not needed - name: Disable Percy if not needed
id: percy id: percy
if: | if: |
github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.event == 'pull_request' &&
!contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy') !contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
run: echo "::set-output name=value::0" run: echo "::set-output name=value::0"
- name: Generate unique ID 💎 - name: Generate unique ID 💎
id: uuid id: uuid
run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")" run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")"
tests: tests:
name: "Run Tests" name: "Run Tests"
needs: prepare needs: prepare
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
issues: read issues: read
pull-requests: read pull-requests: read
environment: Cypress environment:
#strategy: Cypress
# fail-fast: false #strategy:
# matrix: # fail-fast: false
# # Run 4 instances in Parallel # matrix:
# runner: [1, 2, 3, 4] # # Run 4 instances in Parallel
steps: # runner: [1, 2, 3, 4]
- uses: actions/checkout@v3 steps:
with: - uses: actions/checkout@v3
# XXX: We're checking out untrusted code in a secure context with:
# We need to be careful to not trust anything this code outputs/may do # XXX: We're checking out untrusted code in a secure context
# We need to check this out to access the cypress tests which are on the head branch # We need to be careful to not trust anything this code outputs/may do
repository: ${{ github.event.workflow_run.head_repository.full_name }} # We need to check this out to access the cypress tests which are on the head branch
ref: ${{ github.event.workflow_run.head_sha }} repository: ${{ github.event.workflow_run.head_repository.full_name }}
persist-credentials: false 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 # 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: # (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
- name: 📥 Download artifact - name: 📥 Download artifact
uses: dawidd6/action-download-artifact@v2 uses: dawidd6/action-download-artifact@v2
with: with:
run_id: ${{ github.event.workflow_run.id }} run_id: ${{ github.event.workflow_run.id }}
name: previewbuild name: previewbuild
path: webapp path: webapp
- name: Run Cypress tests - name: Run Cypress tests
uses: cypress-io/github-action@v4.2.2 uses: cypress-io/github-action@v4.2.2
with: with:
# The built-in Electron runner seems to grind to a halt trying # The built-in Electron runner seems to grind to a halt trying
# to run the tests, so use chrome. # to run the tests, so use chrome.
browser: chrome browser: chrome
start: npx serve -p 8080 webapp start: npx serve -p 8080 webapp
wait-on: 'http://localhost:8080' wait-on: "http://localhost:8080"
record: true record:
#parallel: true true
#command-prefix: 'yarn percy exec --parallel --' #parallel: true
command-prefix: 'yarn percy exec --' #command-prefix: 'yarn percy exec --parallel --'
ci-build-id: ${{ needs.prepare.outputs.uuid }} command-prefix: "yarn percy exec --"
env: ci-build-id: ${{ needs.prepare.outputs.uuid }}
# pass the Dashboard record key as an environment variable env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} # pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# Use existing chromium rather than downloading another # Use existing chromium rather than downloading another
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
# pass GitHub token to allow accurately detecting a build vs a re-run build # pass GitHub token to allow accurately detecting a build vs a re-run build
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# make Node's os.tmpdir() return something where we actually have permissions # make Node's os.tmpdir() return something where we actually have permissions
TMPDIR: ${{ runner.temp }} TMPDIR: ${{ runner.temp }}
# tell Cypress more details about the context of this run # tell Cypress more details about the context of this run
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }} COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }} COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }} COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }} COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }} COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }} COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
# pass the Percy token as an environment variable # pass the Percy token as an environment variable
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }} PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }}
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
# tell Percy more details about the context of this run # tell Percy more details about the context of this run
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }} PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }} PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }} PERCY_PULL_REQUEST:
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }} ${{ needs.prepare.outputs.pr_id }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }} #PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
- name: Upload Artifact - name: Upload Artifact
if: failure() if: failure()
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: cypress-results name: cypress-results
path: | path: |
cypress/screenshots cypress/screenshots
cypress/videos cypress/videos
cypress/synapselogs cypress/synapselogs
report: report:
name: Report results name: Report results
needs: tests needs: tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: always() if: always()
permissions: permissions:
statuses: write statuses: write
steps: steps:
- uses: Sibz/github-status-action@v1 - uses: Sibz/github-status-action@v1
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }} state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }}) context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }} sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 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. # as an artifact and run integration tests.
name: Element Web - Build name: Element Web - Build
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
repository_dispatch: repository_dispatch:
types: [ upstream-sdk-notify ] types: [upstream-sdk-notify]
env: env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
build: build:
name: "Build Element-Web" name: "Build Element-Web"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Fetch layered build - name: Fetch layered build
id: layered_build id: layered_build
run: | run: |
scripts/ci/layered.sh scripts/ci/layered.sh
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD) JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
REACT_SHA=$(git rev-parse --short=12 HEAD) REACT_SHA=$(git rev-parse --short=12 HEAD)
VECTOR_SHA=$(git -C element-web 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" echo "::set-output name=VERSION::$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA"
- name: Copy config - name: Copy config
run: cp element.io/develop/config.json config.json run: cp element.io/develop/config.json config.json
working-directory: ./element-web working-directory: ./element-web
- name: Build - name: Build
env: env:
CI_PACKAGE: true CI_PACKAGE: true
VERSION: "${{ steps.layered_build.outputs.VERSION }}" VERSION: "${{ steps.layered_build.outputs.VERSION }}"
run: | run: |
yarn build yarn build
echo $VERSION > webapp/version echo $VERSION > webapp/version
working-directory: ./element-web working-directory: ./element-web
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: previewbuild name: previewbuild
path: element-web/webapp path: element-web/webapp
# We'll only use this in a triggered job, then we're done with it # We'll only use this in a triggered job, then we're done with it
retention-days: 1 retention-days: 1

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,148 +1,148 @@
name: Static Analysis name: Static Analysis
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
repository_dispatch: repository_dispatch:
types: [ upstream-sdk-notify ] types: [upstream-sdk-notify]
env: env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
ts_lint: ts_lint:
name: "Typescript Syntax Check" name: "Typescript Syntax Check"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Deps - name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts" run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Typecheck - name: Typecheck
run: "yarn run lint:types" run: "yarn run lint:types"
- name: Switch js-sdk to release mode - name: Switch js-sdk to release mode
working-directory: node_modules/matrix-js-sdk working-directory: node_modules/matrix-js-sdk
run: | run: |
scripts/switch_package_to_release.js scripts/switch_package_to_release.js
yarn install yarn install
yarn run build:compile yarn run build:compile
yarn run build:types yarn run build:types
- name: Typecheck (release mode) - name: Typecheck (release mode)
run: "yarn run lint:types" run: "yarn run lint:types"
tsc-strict: tsc-strict:
name: Typescript Strict Error Checker name: Typescript Strict Error Checker
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
pull-requests: read pull-requests: read
checks: write checks: write
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
args: args:
- '--strict --noImplicitAny' - "--strict --noImplicitAny"
- '--noImplicitAny' - "--noImplicitAny"
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install Deps
run: "scripts/ci/layered.sh"
- name: Get diff lines - name: Install Deps
id: diff run: "scripts/ci/layered.sh"
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with:
include: '["\\.tsx?$"]'
- name: Detecting files changed - name: Get diff lines
id: files id: diff
uses: futuratrepadeira/changed-files@v4.0.0 uses: Equip-Collaboration/diff-line-numbers@v1.0.0
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} include: '["\\.tsx?$"]'
pattern: '^.*\.tsx?$'
- uses: t3chguy/typescript-check-action@main - name: Detecting files changed
with: id: files
repo-token: ${{ secrets.GITHUB_TOKEN }} uses: futuratrepadeira/changed-files@v4.0.0
use-check: false with:
check-fail-mode: added repo-token: ${{ secrets.GITHUB_TOKEN }}
output-behaviour: annotate pattern: '^.*\.tsx?$'
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 }}
i18n_lint: - uses: t3chguy/typescript-check-action@main
name: "i18n Check" with:
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop 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: i18n_lint:
name: "Rethemendex Check" name: "i18n Check"
runs-on: ubuntu-latest uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
steps:
- uses: actions/checkout@v3
- run: ./res/css/rethemendex.sh
- run: git diff --exit-code
js_lint: rethemendex_lint:
name: "ESLint" name: "Rethemendex Check"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - run: ./res/css/rethemendex.sh
with:
cache: 'yarn'
# Does not need branch matching as only analyses this layer - run: git diff --exit-code
- name: Install Deps
run: "yarn install"
- name: Run Linter js_lint:
run: "yarn run lint:js" name: "ESLint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
style_lint: - uses: actions/setup-node@v3
name: "Style Lint" with:
runs-on: ubuntu-latest cache: "yarn"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3 # Does not need branch matching as only analyses this layer
with: - name: Install Deps
cache: 'yarn' run: "yarn install"
# Does not need branch matching as only analyses this layer - name: Run Linter
- name: Install Deps run: "yarn run lint:js"
run: "yarn install"
- name: Run Linter style_lint:
run: "yarn run lint:style" name: "Style Lint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
analyse_dead_code: - uses: actions/setup-node@v3
name: "Analyse Dead Code" with:
runs-on: ubuntu-latest cache: "yarn"
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3 # Does not need branch matching as only analyses this layer
with: - name: Install Deps
cache: 'yarn' run: "yarn install"
- name: Install Deps - name: Run Linter
run: "scripts/ci/layered.sh" run: "yarn run lint:style"
- name: Dead Code Analysis analyse_dead_code:
run: | name: "Analyse Dead Code"
cd element-web runs-on: ubuntu-latest
yarn run analyse:unused-exports 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 name: Tests
on: on:
pull_request: { } pull_request: {}
push: push:
branches: [ develop, master ] branches: [develop, master]
repository_dispatch: repository_dispatch:
types: [ upstream-sdk-notify ] types: [upstream-sdk-notify]
env: env:
# These must be set for fetchdep.sh to get the right branch # These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }} REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
jobs: jobs:
jest: jest:
name: Jest name: Jest
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Yarn cache - name: Yarn cache
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Install Deps - name: Install Deps
run: "./scripts/ci/install-deps.sh --ignore-scripts" run: "./scripts/ci/install-deps.sh --ignore-scripts"
- name: Get number of CPU cores - name: Get number of CPU cores
id: cpu-cores id: cpu-cores
uses: SimenB/github-actions-cpu-cores@v1 uses: SimenB/github-actions-cpu-cores@v1
- name: Run tests with coverage and metrics - name: Run tests with coverage and metrics
if: github.ref == 'refs/heads/develop' 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 }}" run: "yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Run tests with coverage - name: Run tests with coverage
if: github.ref != 'refs/heads/develop' if: github.ref != 'refs/heads/develop'
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}" run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: coverage name: coverage
path: | path: |
coverage coverage
!coverage/lcov-report !coverage/lcov-report
app-tests: app-tests:
name: Element Web Integration Tests name: Element Web Integration Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'yarn' cache: "yarn"
- name: Run tests - name: Run tests
run: "./scripts/ci/app-tests.sh" run: "./scripts/ci/app-tests.sh"

View file

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

View file

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

View file

@ -1,13 +1,8 @@
module.exports = { module.exports = {
"extends": [ extends: ["stylelint-config-standard", "stylelint-config-prettier"],
"stylelint-config-standard", customSyntax: require("postcss-scss"),
"stylelint-config-prettier", plugins: ["stylelint-scss"],
], rules: {
customSyntax: require('postcss-scss'),
"plugins": [
"stylelint-scss",
],
"rules": {
"color-hex-case": null, "color-hex-case": null,
"comment-empty-line-before": null, "comment-empty-line-before": null,
"declaration-empty-line-before": null, "declaration-empty-line-before": null,
@ -22,15 +17,18 @@ module.exports = {
"at-rule-no-unknown": null, "at-rule-no-unknown": null,
"no-descending-specificity": null, "no-descending-specificity": null,
"no-empty-first-line": true, "no-empty-first-line": true,
"scss/at-rule-no-unknown": [true, { "scss/at-rule-no-unknown": [
// https://github.com/vector-im/element-web/issues/10544 true,
"ignoreAtRules": ["define-mixin"], {
}], // 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" // 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 // 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`, // class name. This regex is trying to *allow* anything except `&words`, such as `&::before`,
// `&.mx_Class`, etc. // `&.mx_Class`, etc.
"selector-nested-pattern": "^((&[ :.\\\[,])|([^&]))", "selector-nested-pattern": "^((&[ :.\\[,])|([^&]))",
"declaration-colon-space-after": "always-single-line", "declaration-colon-space-after": "always-single-line",
// Disable some defaults // Disable some defaults
"selector-class-pattern": null, "selector-class-pattern": null,
@ -52,4 +50,4 @@ module.exports = {
"number-max-precision": null, "number-max-precision": null,
"no-invalid-double-slash-comments": true, "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 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) [![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) [![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 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 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: a 'skin'. A skin provides:
* Customised implementations of presentation components.
* Custom CSS - Customised implementations of presentation components.
* The containing application - Custom CSS
* Zero or more 'modules' containing non-UI functionality - The containing application
- Zero or more 'modules' containing non-UI functionality
As of Aug 2018, the only skin that exists is As of Aug 2018, the only skin that exists is
[`vector-im/element-web`](https://github.com/vector-im/element-web/); it and [`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 be considered as a single project (for instance, matrix-react-sdk bugs
are currently filed against vector-im/element-web rather than this project). 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) [![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: Platform Targets:
* Chrome, Firefox and Safari.
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. - Chrome, Firefox and Safari.
* Mobile Web is not currently a target platform - instead please use the native - WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - Mobile Web is not currently a target platform - instead please use the native
(https://github.com/matrix-org/matrix-android-sdk2) SDKs. 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. All code lands on the `develop` branch - `master` is only used for stable releases.
**Please file PRs against `develop`!!** **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 https://github.com/vector-im/element-web/blob/develop/code_style.md
Code should be committed as follows: Code should be committed as follows:
* All new components:
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components - All new components:
* Element-specific components: https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
https://github.com/vector-im/element-web/tree/master/src/components - Element-specific components:
* In practice, `matrix-react-sdk` is still evolving so fast that the https://github.com/vector-im/element-web/tree/master/src/components
maintenance burden of customising and overriding these components for - In practice, `matrix-react-sdk` is still evolving so fast that the
Element can seriously impede development. So right now, there should be maintenance burden of customising and overriding these components for
very few (if any) customisations for Element. Element can seriously impede development. So right now, there should be
* CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css very few (if any) customisations for Element.
* Theme specific CSS & resources: - CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes - 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: 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 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 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 room tracks lots of state for its child components which it passes into them for
visual rendering via props. visual rendering via props.
@ -75,74 +76,72 @@ visual rendering via props.
Good separation between the components is maintained by adopting various best 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: 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 component is a view or a structure, and then a broad functional grouping
(e.g. 'rooms' here) (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 CSS for matrix-react-sdk currently resides in
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css. 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 the context of the rest of the app, although this is unusual for any but
* Theme specific CSS & resources: - Theme specific CSS & resources:
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
structural components (lacking presentation logic) and the simplest view structural components (lacking presentation logic) and the simplest view
components. 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) 'Stealing' styling information from other components (including parents)
is not cool, as it breaks the independence of the components. 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 - 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 avoid CSS collisions. The base skin shipped by Matrix.org with the
matrix-react-sdk uses the naming prefix "mx_". A company called Yoyodyne matrix-react-sdk uses the naming prefix "mx*". A company called Yoyodyne
Inc might use a prefix like "yy_" for its app-specific classes. 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. .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. 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 .mx_MessageTile_randomDiv is how you'd name the class of an arbitrary div
within the MessageTile view. within the MessageTile view.
* We deliberately use vanilla CSS 3.0 to avoid adding any more magic - 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 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 to use whatever floats their boat however. In future we'll start using
css-next to pull in features like CSS variable support. css-next to pull in features like CSS variable support.
* The CSS for a component can override the rules for child components. - The CSS for a component can override the rules for child components.
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override For instance, .mx*RoomList .mx_RoomTile {} would be the selector to override
styles of RoomTiles when viewed in the context of a RoomList view. 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 .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 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 only to the context of RoomList views. N.B. overrides should be relatively
rare as in general CSS inheritance should be enough. 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 element. Page-absolute positioning and negative CSS margins and similar are
generally not cool and stop the component from being reused easily in generally not cool and stop the component from being reused easily in
different places. different places.
Originally `matrix-react-sdk` followed the Atomic design pattern as per 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 found that the grouping of components into atoms/molecules/organisms
made them harder to find relative to a functional split, and didn't emphasise 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 the distinction between 'structural' and 'view' components, so we backed away
from it. from it.
Github Issues ## Github Issues
-------------
All issues should be filed under https://github.com/vector-im/element-web/issues All issues should be filed under https://github.com/vector-im/element-web/issues
for now. for now.
Development ## Development
-----------
Ensure you have the latest LTS version of Node.js installed. Ensure you have the latest LTS version of Node.js installed.

View file

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

View file

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

View file

@ -1,18 +1,21 @@
module.exports = { module.exports = {
"sourceMaps": "inline", sourceMaps: "inline",
"presets": [ presets: [
["@babel/preset-env", { [
"targets": [ "@babel/preset-env",
"last 2 Chrome versions", {
"last 2 Firefox versions", targets: [
"last 2 Safari versions", "last 2 Chrome versions",
"last 2 Edge versions", "last 2 Firefox versions",
], "last 2 Safari versions",
}], "last 2 Edge versions",
],
},
],
"@babel/preset-typescript", "@babel/preset-typescript",
"@babel/preset-react", "@babel/preset-react",
], ],
"plugins": [ plugins: [
"@babel/plugin-proposal-export-default-from", "@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-numeric-separator", "@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties", "@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. limitations under the License.
*/ */
import { defineConfig } from 'cypress'; import { defineConfig } from "cypress";
export default defineConfig({ export default defineConfig({
videoUploadOnPasses: false, videoUploadOnPasses: false,
projectId: 'ppvnzg', projectId: "ppvnzg",
experimentalInteractiveRunEvents: true, experimentalInteractiveRunEvents: true,
defaultCommandTimeout: 10000, defaultCommandTimeout: 10000,
chromeWebSecurity: false, chromeWebSecurity: false,
e2e: { e2e: {
setupNodeEvents(on, config) { 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, experimentalSessionAndOrigin: true,
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
}, },
env: { env: {
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image. // Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.

View file

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

View file

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

View file

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

View file

@ -77,18 +77,18 @@ describe("Integration Manager: Get OpenID Token", () => {
let integrationManagerUrl: string; let integrationManagerUrl: string;
beforeEach(() => { beforeEach(() => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => { cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
integrationManagerUrl = url; integrationManagerUrl = url;
}); });
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => { 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", INTEGRATION_MANAGER_TOKEN);
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN); win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
}); });
}).then(user => { }).then((user) => {
testUser = user; testUser = user;
}); });
@ -107,8 +107,8 @@ describe("Integration Manager: Get OpenID Token", () => {
}).as("integrationManager"); }).as("integrationManager");
// Succeed when checking the token is valid // Succeed when checking the token is valid
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => { cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
req.continue(res => { req.continue((res) => {
return res.send(200, { return res.send(200, {
user_id: testUser.userId, user_id: testUser.userId,
}); });
@ -127,16 +127,14 @@ describe("Integration Manager: Get OpenID Token", () => {
}); });
it("should successfully obtain an openID token", () => { it("should successfully obtain an openID token", () => {
cy.all([ cy.all([cy.get<{}>("@integrationManager")]).then(() => {
cy.get<{}>("@integrationManager"),
]).then(() => {
cy.viewRoomByName(ROOM_NAME); cy.viewRoomByName(ROOM_NAME);
openIntegrationManager(); openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl); sendActionFromIntegrationManager(integrationManagerUrl);
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => { 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 }); cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true });
// Check for the event message (or lack thereof) // Check for the event message (or lack thereof)
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`) cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should(
.should(shouldExist ? "exist" : "not.exist"); shouldExist ? "exist" : "not.exist",
);
} }
describe("Integration Manager: Kick", () => { describe("Integration Manager: Kick", () => {
@ -97,18 +98,18 @@ describe("Integration Manager: Kick", () => {
let integrationManagerUrl: string; let integrationManagerUrl: string;
beforeEach(() => { beforeEach(() => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => { cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
integrationManagerUrl = url; integrationManagerUrl = url;
}); });
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => { 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", INTEGRATION_MANAGER_TOKEN);
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN); win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
}); });
}).then(user => { }).then((user) => {
testUser = user; testUser = user;
}); });
@ -127,8 +128,8 @@ describe("Integration Manager: Kick", () => {
}).as("integrationManager"); }).as("integrationManager");
// Succeed when checking the token is valid // Succeed when checking the token is valid
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => { cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
req.continue(res => { req.continue((res) => {
return res.send(200, { return res.send(200, {
user_id: testUser.userId, user_id: testUser.userId,
}); });
@ -149,103 +150,100 @@ describe("Integration Manager: Kick", () => {
}); });
it("should kick the target", () => { it("should kick the target", () => {
cy.all([ cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
cy.get<MatrixClient>("@bob"), ([targetUser, roomId]) => {
cy.get<string>("@roomId"), const targetUserId = targetUser.getUserId();
cy.get<{}>("@integrationManager"), cy.viewRoomByName(ROOM_NAME);
]).then(([targetUser, roomId]) => { cy.inviteUser(roomId, targetUserId);
const targetUserId = targetUser.getUserId(); cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
cy.viewRoomByName(ROOM_NAME);
cy.inviteUser(roomId, targetUserId);
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
openIntegrationManager(); openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl); closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(true); expectKickedMessage(true);
}); },
);
}); });
it("should not kick the target if lacking permissions", () => { it("should not kick the target if lacking permissions", () => {
cy.all([ cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
cy.get<MatrixClient>("@bob"), ([targetUser, roomId]) => {
cy.get<string>("@roomId"), const targetUserId = targetUser.getUserId();
cy.get<{}>("@integrationManager"), cy.viewRoomByName(ROOM_NAME);
]).then(([targetUser, roomId]) => { cy.inviteUser(roomId, targetUserId);
const targetUserId = targetUser.getUserId(); cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
cy.viewRoomByName(ROOM_NAME); cy.getClient()
cy.inviteUser(roomId, targetUserId); .then(async (client) => {
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist'); await client.sendStateEvent(roomId, "m.room.power_levels", {
cy.getClient().then(async client => { kick: 50,
await client.sendStateEvent(roomId, 'm.room.power_levels', { users: {
kick: 50, [testUser.userId]: 0,
users: { },
[testUser.userId]: 0, });
}, })
}); .then(() => {
}).then(() => { openIntegrationManager();
openIntegrationManager(); sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); closeIntegrationManager(integrationManagerUrl);
closeIntegrationManager(integrationManagerUrl); expectKickedMessage(false);
expectKickedMessage(false); });
}); },
}); );
}); });
it("should no-op if the target already left", () => { it("should no-op if the target already left", () => {
cy.all([ cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
cy.get<MatrixClient>("@bob"), ([targetUser, roomId]) => {
cy.get<string>("@roomId"), const targetUserId = targetUser.getUserId();
cy.get<{}>("@integrationManager"), cy.viewRoomByName(ROOM_NAME);
]).then(([targetUser, roomId]) => { cy.inviteUser(roomId, targetUserId);
const targetUserId = targetUser.getUserId(); cy.contains(`${BOT_DISPLAY_NAME} joined the room`)
cy.viewRoomByName(ROOM_NAME); .should("exist")
cy.inviteUser(roomId, targetUserId); .then(async () => {
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist').then(async () => { await targetUser.leave(roomId);
await targetUser.leave(roomId); })
}).then(() => { .then(() => {
openIntegrationManager(); openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl); closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false); expectKickedMessage(false);
}); });
}); },
);
}); });
it("should no-op if the target was banned", () => { it("should no-op if the target was banned", () => {
cy.all([ cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
cy.get<MatrixClient>("@bob"), ([targetUser, roomId]) => {
cy.get<string>("@roomId"), const targetUserId = targetUser.getUserId();
cy.get<{}>("@integrationManager"), cy.viewRoomByName(ROOM_NAME);
]).then(([targetUser, roomId]) => { cy.inviteUser(roomId, targetUserId);
const targetUserId = targetUser.getUserId(); cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
cy.viewRoomByName(ROOM_NAME); cy.getClient()
cy.inviteUser(roomId, targetUserId); .then(async (client) => {
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist'); await client.ban(roomId, targetUserId);
cy.getClient().then(async client => { })
await client.ban(roomId, targetUserId); .then(() => {
}).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(); openIntegrationManager();
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId); sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
closeIntegrationManager(integrationManagerUrl); closeIntegrationManager(integrationManagerUrl);
expectKickedMessage(false); 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[] = []; const charlies: Charly[] = [];
beforeEach(() => { beforeEach(() => {
cy.window().then(win => { cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests 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; synapse = data;
cy.initTestUser(synapse, "Alice"); cy.initTestUser(synapse, "Alice");
@ -44,7 +44,7 @@ describe("Lazy Loading", () => {
displayName: "Bob", displayName: "Bob",
startClient: false, startClient: false,
autoAcceptInvites: false, autoAcceptInvites: false,
}).then(_bob => { }).then((_bob) => {
bob = _bob; bob = _bob;
}); });
@ -54,7 +54,7 @@ describe("Lazy Loading", () => {
displayName, displayName,
startClient: false, startClient: false,
autoAcceptInvites: false, autoAcceptInvites: false,
}).then(client => { }).then((client) => {
charlies[i - 1] = { displayName, client }; charlies[i - 1] = { displayName, client };
}); });
} }
@ -71,15 +71,22 @@ describe("Lazy Loading", () => {
const charlyMsg2 = "how's it going??"; const charlyMsg2 = "how's it going??";
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) { function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
cy.window({ log: false }).then(win => { cy.window({ log: false }).then((win) => {
return cy.wrap(bob.createRoom({ return cy
name, .wrap(
room_alias_name: "lltest", bob
visibility: win.matrixcs.Visibility.Public, .createRoom({
}).then(r => r.room_id), { log: false }).as("roomId"); 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) { for (const charly of charlies) {
await charly.client.joinRoom(alias); await charly.client.joinRoom(alias);
} }
@ -122,13 +129,13 @@ describe("Lazy Loading", () => {
function checkMemberList(charlies: Charly[]) { function checkMemberList(charlies: Charly[]) {
getMemberInMemberlist("Alice").should("exist"); getMemberInMemberlist("Alice").should("exist");
getMemberInMemberlist("Bob").should("exist"); getMemberInMemberlist("Bob").should("exist");
charlies.forEach(charly => { charlies.forEach((charly) => {
getMemberInMemberlist(charly.displayName).should("exist"); getMemberInMemberlist(charly.displayName).should("exist");
}); });
} }
function checkMemberListLacksCharlies(charlies: Charly[]) { function checkMemberListLacksCharlies(charlies: Charly[]) {
charlies.forEach(charly => { charlies.forEach((charly) => {
getMemberInMemberlist(charly.displayName).should("not.exist"); getMemberInMemberlist(charly.displayName).should("not.exist");
}); });
} }
@ -136,7 +143,7 @@ describe("Lazy Loading", () => {
function joinCharliesWhileAliceIsOffline(charlies: Charly[]) { function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
cy.goOffline(); cy.goOffline();
cy.get<string>("@roomId").then(async roomId => { cy.get<string>("@roomId").then(async (roomId) => {
for (const charly of charlies) { for (const charly of charlies) {
await charly.client.joinRoom(alias); await charly.client.joinRoom(alias);
} }
@ -163,7 +170,7 @@ describe("Lazy Loading", () => {
joinCharliesWhileAliceIsOffline(charly6to10); joinCharliesWhileAliceIsOffline(charly6to10);
checkMemberList(charly6to10); checkMemberList(charly6to10);
cy.get<string>("@roomId").then(async roomId => { cy.get<string>("@roomId").then(async (roomId) => {
for (const charly of charlies) { for (const charly of charlies) {
await charly.client.leave(roomId); await charly.client.leave(roomId);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ describe("Room Directory", () => {
let synapse: SynapseInstance; let synapse: SynapseInstance;
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Ray"); cy.initTestUser(synapse, "Ray");
@ -36,7 +36,7 @@ describe("Room Directory", () => {
}); });
it("should allow admin to add alias & publish room to 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({ cy.createRoom({
name: "Gaming", name: "Gaming",
preset: win.matrixcs.Preset.PublicChat, preset: win.matrixcs.Preset.PublicChat,
@ -56,16 +56,14 @@ describe("Room Directory", () => {
// Publish into the public rooms directory // Publish into the public rooms directory
cy.contains(".mx_SettingsFieldset", "Published Addresses").within(() => { cy.contains(".mx_SettingsFieldset", "Published Addresses").within(() => {
cy.get("#canonicalAlias").find(":selected").should("contain", "#gaming:localhost"); 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"); .should("have.attr", "aria-checked", "true");
}); });
cy.closeDialog(); cy.closeDialog();
cy.all([ cy.all([cy.get<MatrixClient>("@bot"), cy.get<string>("@roomId")]).then(async ([bot, roomId]) => {
cy.get<MatrixClient>("@bot"),
cy.get<string>("@roomId"),
]).then(async ([bot, roomId]) => {
const resp = await bot.publicRooms({}); const resp = await bot.publicRooms({});
expect(resp.total_room_count_estimate).to.equal(1); expect(resp.total_room_count_estimate).to.equal(1);
expect(resp.chunk).to.have.length(1); expect(resp.chunk).to.have.length(1);
@ -75,10 +73,7 @@ describe("Room Directory", () => {
it("should allow finding published rooms in directory", () => { it("should allow finding published rooms in directory", () => {
const name = "This is a public room"; const name = "This is a public room";
cy.all([ cy.all([cy.window({ log: false }), cy.get<MatrixClient>("@bot")]).then(([win, bot]) => {
cy.window({ log: false }),
cy.get<MatrixClient>("@bot"),
]).then(([win, bot]) => {
bot.createRoom({ bot.createRoom({
visibility: win.matrixcs.Visibility.Public, visibility: win.matrixcs.Visibility.Public,
name, name,
@ -89,16 +84,17 @@ describe("Room Directory", () => {
cy.get('[role="button"][aria-label="Explore rooms"]').click(); cy.get('[role="button"][aria-label="Explore rooms"]').click();
cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("Unknown Room"); cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("Unknown Room");
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_otherSearches_messageSearchText") cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_otherSearches_messageSearchText").should(
.should("contain", "can't find the room you're looking for"); "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_wrapper").percySnapshotElement("Room Directory - filtered no results");
cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("{selectAll}{backspace}test1234"); cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("{selectAll}{backspace}test1234");
cy.contains(".mx_SpotlightDialog .mx_SpotlightDialog_result_publicRoomName", name) cy.contains(".mx_SpotlightDialog .mx_SpotlightDialog_result_publicRoomName", name).should("exist");
.should("exist");
cy.get(".mx_SpotlightDialog_wrapper").percySnapshotElement("Room Directory - filtered one result"); 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.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(() => { beforeEach(() => {
cy.enableLabsFeature("feature_new_device_manager"); cy.enableLabsFeature("feature_new_device_manager");
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Alice").then(credentials => { cy.initTestUser(synapse, "Alice")
user = credentials; .then((credentials) => {
}).then(() => { user = credentials;
// create some extra sessions to manage })
return cy.loginUser(synapse, user.username, user.password); .then(() => {
}).then(() => { // create some extra sessions to manage
return cy.loginUser(synapse, user.username, user.password); 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", () => { it("should display sessions", () => {
cy.openUserSettings("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.get('[data-testid="current-session-section"]').within(() => {
cy.contains('Unverified session').should('exist'); cy.contains("Unverified session").should("exist");
}); });
// current session details opened // current session details opened
cy.get('[data-testid="current-session-toggle-details"]').click(); 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 // close current session details
cy.get('[data-testid="current-session-toggle-details"]').click(); 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.get('[data-testid="security-recommendations-section"]').within(() => {
cy.contains('Security recommendations').should('exist'); cy.contains("Security recommendations").should("exist");
cy.get('[data-testid="unverified-devices-cta"]').should('have.text', 'View all (3)').click(); cy.get('[data-testid="unverified-devices-cta"]').should("have.text", "View all (3)").click();
}); });
/** /**
* Other sessions section * Other sessions section
*/ */
cy.contains('Other sessions').should('exist'); cy.contains("Other sessions").should("exist");
// filter applied after clicking through from security recommendations // filter applied after clicking through from security recommendations
cy.get('[aria-label="Filter devices"]').should('have.text', 'Show: Unverified'); 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(".mx_FilteredDeviceList_list").find(".mx_FilteredDeviceList_listItem").should("have.length", 3);
// select two sessions // 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").first().click();
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').last().click(); cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox").last().click();
// sign out from list selection action buttons // sign out from list selection action buttons
cy.get('[data-testid="sign-out-selection-cta"]').click(); cy.get('[data-testid="sign-out-selection-cta"]').click();
cy.get('[data-testid="dialog-primary-button"]').click(); cy.get('[data-testid="dialog-primary-button"]').click();
// list updated after sign out // 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 // 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`; const sessionName = `Alice's device`;
// open the first session // open the first session
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem').first().within(() => { cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem")
cy.get('[aria-label="Show details"]').click(); .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-heading-rename-cta"]').click();
cy.get('[data-testid="device-rename-input"]').type(sessionName); cy.get('[data-testid="device-rename-input"]').type(sessionName);
cy.get('[data-testid="device-rename-submit-cta"]').click(); cy.get('[data-testid="device-rename-submit-cta"]').click();
// there should be a spinner while device updates // there should be a spinner while device updates
cy.get(".mx_Spinner").should("exist"); cy.get(".mx_Spinner").should("exist");
// wait for spinner to complete // wait for spinner to complete
cy.get(".mx_Spinner").should("not.exist"); cy.get(".mx_Spinner").should("not.exist");
// session name updated in details // session name updated in details
cy.get('.mx_DeviceDetailHeading h3').should('have.text', sessionName); cy.get(".mx_DeviceDetailHeading h3").should("have.text", sessionName);
// and main list item // and main list item
cy.get('.mx_DeviceTile h4').should('have.text', sessionName); cy.get(".mx_DeviceTile h4").should("have.text", sessionName);
// sign out using the device details sign out // sign out using the device details sign out
cy.get('[data-testid="device-detail-sign-out-cta"]').click(); cy.get('[data-testid="device-detail-sign-out-cta"]').click();
}); });
// confirm the signout // confirm the signout
cy.get('[data-testid="dialog-primary-button"]').click(); cy.get('[data-testid="dialog-primary-button"]').click();
// no other sessions or security recommendations sections when only one session // no other sessions or security recommendations sections when only one session
cy.contains('Other sessions').should('not.exist'); cy.contains("Other sessions").should("not.exist");
cy.get('[data-testid="security-recommendations-section"]').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 { function seedLabs(synapse: SynapseInstance, labsVal: boolean | null): void {
cy.initTestUser(synapse, "Sally", () => { cy.initTestUser(synapse, "Sally", () => {
// seed labs flag // seed labs flag
cy.window({ log: false }).then(win => { cy.window({ log: false }).then((win) => {
if (typeof labsVal === "boolean") { if (typeof labsVal === "boolean") {
// stringify boolean // stringify boolean
win.localStorage.setItem("mx_labs_feature_feature_hidden_read_receipts", `${labsVal}`); win.localStorage.setItem("mx_labs_feature_feature_hidden_read_receipts", `${labsVal}`);
@ -64,7 +64,7 @@ describe("Hidden Read Receipts Setting Migration", () => {
let synapse: SynapseInstance; let synapse: SynapseInstance;
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
}); });
}); });
@ -73,17 +73,17 @@ describe("Hidden Read Receipts Setting Migration", () => {
cy.stopSynapse(synapse); 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); seedLabs(synapse, null);
testForVal(null); testForVal(null);
}); });
it('should migrate labsHiddenRR=false as sendRR=true', () => { it("should migrate labsHiddenRR=false as sendRR=true", () => {
seedLabs(synapse, false); seedLabs(synapse, false);
testForVal(true); testForVal(true);
}); });
it('should migrate labsHiddenRR=true as sendRR=false', () => { it("should migrate labsHiddenRR=true as sendRR=false", () => {
seedLabs(synapse, true); seedLabs(synapse, true);
testForVal(false); testForVal(false);
}); });

View file

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

View file

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

View file

@ -26,7 +26,7 @@ import Shadow = Cypress.Shadow;
export enum Filter { export enum Filter {
People = "people", People = "people",
PublicRooms = "public_rooms" PublicRooms = "public_rooms",
} }
declare global { declare global {
@ -37,78 +37,86 @@ declare global {
* Opens the spotlight dialog * Opens the spotlight dialog
*/ */
openSpotlightDialog( openSpotlightDialog(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow> options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
spotlightDialog( spotlightDialog(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow> options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
spotlightFilter( spotlightFilter(
filter: Filter | null, filter: Filter | null,
options?: Partial<Loggable & Timeoutable & Withinable & Shadow> options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
spotlightSearch( spotlightSearch(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow> options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
spotlightResults( spotlightResults(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow> options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
roomHeaderName( roomHeaderName(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow> options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
): Chainable<JQuery<HTMLElement>>; ): Chainable<JQuery<HTMLElement>>;
startDM(name: string): Chainable<void>; startDM(name: string): Chainable<void>;
} }
} }
} }
Cypress.Commands.add("openSpotlightDialog", ( Cypress.Commands.add(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>, "openSpotlightDialog",
): Chainable<JQuery<HTMLElement>> => { (options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
cy.get('.mx_RoomSearch_spotlightTrigger', options).click({ force: true }); cy.get(".mx_RoomSearch_spotlightTrigger", options).click({ force: true });
return cy.spotlightDialog(options); return cy.spotlightDialog(options);
}); },
);
Cypress.Commands.add("spotlightDialog", ( Cypress.Commands.add(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>, "spotlightDialog",
): Chainable<JQuery<HTMLElement>> => { (options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get('[role=dialog][aria-label="Search Dialog"]', options); return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
}); },
);
Cypress.Commands.add("spotlightFilter", ( Cypress.Commands.add(
filter: Filter | null, "spotlightFilter",
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>, (
): Chainable<JQuery<HTMLElement>> => { filter: Filter | null,
let selector: string; options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
switch (filter) { ): Chainable<JQuery<HTMLElement>> => {
case Filter.People: let selector: string;
selector = "#mx_SpotlightDialog_button_startChat"; switch (filter) {
break; case Filter.People:
case Filter.PublicRooms: selector = "#mx_SpotlightDialog_button_startChat";
selector = "#mx_SpotlightDialog_button_explorePublicRooms"; break;
break; case Filter.PublicRooms:
default: selector = "#mx_SpotlightDialog_button_explorePublicRooms";
selector = ".mx_SpotlightDialog_filter"; break;
break; default:
} selector = ".mx_SpotlightDialog_filter";
return cy.get(selector, options).click(); break;
}); }
return cy.get(selector, options).click();
},
);
Cypress.Commands.add("spotlightSearch", ( Cypress.Commands.add(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>, "spotlightSearch",
): Chainable<JQuery<HTMLElement>> => { (options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_SpotlightDialog_searchBox input", options); return cy.get(".mx_SpotlightDialog_searchBox input", options);
}); },
);
Cypress.Commands.add("spotlightResults", ( Cypress.Commands.add(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>, "spotlightResults",
): Chainable<JQuery<HTMLElement>> => { (options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options); return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
}); },
);
Cypress.Commands.add("roomHeaderName", ( Cypress.Commands.add(
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>, "roomHeaderName",
): Chainable<JQuery<HTMLElement>> => { (options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
return cy.get(".mx_RoomHeader_nametext", options); return cy.get(".mx_RoomHeader_nametext", options);
}); },
);
Cypress.Commands.add("startDM", (name: string) => { Cypress.Commands.add("startDM", (name: string) => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog().within(() => {
@ -121,9 +129,7 @@ Cypress.Commands.add("startDM", (name: string) => {
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).click();
}); });
// send first message to start DM // send first message to start DM
cy.get(".mx_BasicMessageComposer_input") cy.get(".mx_BasicMessageComposer_input").should("have.focus").type("Hey!{enter}");
.should("have.focus")
.type("Hey!{enter}");
// The DM room is created at this point, this can take a little bit of time // 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_EventTile_body", "Hey!", { timeout: 30000 });
cy.contains(".mx_RoomSublist[aria-label=People]", name); cy.contains(".mx_RoomSublist[aria-label=People]", name);
@ -148,46 +154,52 @@ describe("Spotlight", () => {
let room3Id: string; let room3Id: string;
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Jim").then(() => cy.initTestUser(synapse, "Jim")
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => { .then(() =>
bot1 = _bot1; 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 .then(() =>
bot2 = _bot2; cy.getBot(synapse, { displayName: bot2Name }).then((_bot2) => {
}), // eslint-disable-next-line @typescript-eslint/no-unused-vars
).then(() => bot2 = _bot2;
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => { }),
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => { )
room1Id = _room1Id; .then(() =>
bot1.joinRoom(room1Id); cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
cy.visit("/#/room/" + room1Id); cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then((_room1Id) => {
}); room1Id = _room1Id;
bot2.createRoom({ name: room2Name, visibility: Visibility.Public }) bot1.joinRoom(room1Id);
.then(({ room_id: _room2Id }) => { cy.visit("/#/room/" + room1Id);
room2Id = _room2Id;
bot2.invite(room2Id, bot1.getUserId());
}); });
bot2.createRoom({ bot2.createRoom({ name: room2Name, visibility: Visibility.Public }).then(
name: room3Name, ({ room_id: _room2Id }) => {
visibility: Visibility.Public, initial_state: [{ room2Id = _room2Id;
type: "m.room.history_visibility", bot2.invite(room2Id, bot1.getUserId());
state_key: "",
content: {
history_visibility: "world_readable",
}, },
}], );
}).then(({ room_id: _room3Id }) => { bot2.createRoom({
room3Id = _room3Id; name: room3Name,
bot2.invite(room3Id, bot1.getUserId()); visibility: Visibility.Public,
}); initial_state: [
}), {
).then(() => type: "m.room.history_visibility",
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'), 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", () => { it("should find joined rooms", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightSearch().clear().type(room1Name); .within(() => {
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(room1Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", room1Name); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).should("contain", room1Name);
cy.url().should("contain", room1Id); cy.spotlightResults().eq(0).click();
}).then(() => { cy.url().should("contain", room1Id);
cy.roomHeaderName().should("contain", room1Name); })
}); .then(() => {
cy.roomHeaderName().should("contain", room1Name);
});
}); });
it("should find known public rooms", () => { it("should find known public rooms", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightFilter(Filter.PublicRooms); .within(() => {
cy.spotlightSearch().clear().type(room1Name); cy.spotlightFilter(Filter.PublicRooms);
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(room1Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", room1Name); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", "View"); cy.spotlightResults().eq(0).should("contain", room1Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).should("contain", "View");
cy.url().should("contain", room1Id); cy.spotlightResults().eq(0).click();
}).then(() => { cy.url().should("contain", room1Id);
cy.roomHeaderName().should("contain", room1Name); })
}); .then(() => {
cy.roomHeaderName().should("contain", room1Name);
});
}); });
it("should find unknown public rooms", () => { it("should find unknown public rooms", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightFilter(Filter.PublicRooms); .within(() => {
cy.spotlightSearch().clear().type(room2Name); cy.spotlightFilter(Filter.PublicRooms);
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(room2Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", room2Name); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", "Join"); cy.spotlightResults().eq(0).should("contain", room2Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).should("contain", "Join");
cy.url().should("contain", room2Id); cy.spotlightResults().eq(0).click();
}).then(() => { cy.url().should("contain", room2Id);
cy.get(".mx_RoomView_MessageList").should("have.length", 1); })
cy.roomHeaderName().should("contain", room2Name); .then(() => {
}); cy.get(".mx_RoomView_MessageList").should("have.length", 1);
cy.roomHeaderName().should("contain", room2Name);
});
}); });
it("should find unknown public world readable rooms", () => { it("should find unknown public world readable rooms", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightFilter(Filter.PublicRooms); .within(() => {
cy.spotlightSearch().clear().type(room3Name); cy.spotlightFilter(Filter.PublicRooms);
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(room3Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", room3Name); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).should("contain", "View"); cy.spotlightResults().eq(0).should("contain", room3Name);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).should("contain", "View");
cy.url().should("contain", room3Id); cy.spotlightResults().eq(0).click();
}).then(() => { cy.url().should("contain", room3Id);
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click(); })
cy.roomHeaderName().should("contain", room3Name); .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 // TODO: We currently cant test finding rooms on other homeservers/other protocols
@ -299,29 +319,33 @@ describe("Spotlight", () => {
}); });
*/ */
it("should find known people", () => { it("should find known people", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightFilter(Filter.People); .within(() => {
cy.spotlightSearch().clear().type(bot1Name); cy.spotlightFilter(Filter.People);
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(bot1Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", bot1Name); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).should("contain", bot1Name);
}).then(() => { cy.spotlightResults().eq(0).click();
cy.roomHeaderName().should("contain", bot1Name); })
}); .then(() => {
cy.roomHeaderName().should("contain", bot1Name);
});
}); });
it("should find unknown people", () => { it("should find unknown people", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightFilter(Filter.People); .within(() => {
cy.spotlightSearch().clear().type(bot2Name); cy.spotlightFilter(Filter.People);
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(bot2Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", bot2Name); cy.spotlightResults().should("have.length", 1);
cy.spotlightResults().eq(0).click(); cy.spotlightResults().eq(0).should("contain", bot2Name);
}).then(() => { cy.spotlightResults().eq(0).click();
cy.roomHeaderName().should("contain", bot2Name); })
}); .then(() => {
cy.roomHeaderName().should("contain", bot2Name);
});
}); });
it("should find group DMs by usernames or user ids", () => { it("should find group DMs by usernames or user ids", () => {
@ -340,10 +364,7 @@ describe("Spotlight", () => {
// Send first message to actually start DM // Send first message to actually start DM
cy.roomHeaderName().should("contain", bot2Name); cy.roomHeaderName().should("contain", bot2Name);
cy.get(".mx_BasicMessageComposer_input") cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
.click()
.should("have.focus")
.type("Hey!{enter}");
// Assert DM exists by checking for the first message and the room being in the room list // 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 }); cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
@ -352,13 +373,13 @@ describe("Spotlight", () => {
// Invite BotBob into existing DM with ByteBot // Invite BotBob into existing DM with ByteBot
cy.getDmRooms(bot2.getUserId()) cy.getDmRooms(bot2.getUserId())
.should("have.length", 1) .should("have.length", 1)
.then(dmRooms => cy.getClient().then(client => client.getRoom(dmRooms[0]))) .then((dmRooms) => cy.getClient().then((client) => client.getRoom(dmRooms[0])))
.then(groupDm => { .then((groupDm) => {
cy.inviteUser(groupDm.roomId, bot1.getUserId()); cy.inviteUser(groupDm.roomId, bot1.getUserId());
cy.roomHeaderName().should(($element) => cy.roomHeaderName().should(($element) => expect($element.get(0).innerText).contains(groupDm.name));
expect($element.get(0).innerText).contains(groupDm.name));
cy.get(".mx_RoomSublist[aria-label=People]").should(($element) => 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 // Search for BotBob by id, should return group DM and user
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog().within(() => {
@ -407,17 +428,19 @@ describe("Spotlight", () => {
}); });
it("should allow opening group chat dialog", () => { it("should allow opening group chat dialog", () => {
cy.openSpotlightDialog().within(() => { cy.openSpotlightDialog()
cy.spotlightFilter(Filter.People); .within(() => {
cy.spotlightSearch().clear().type(bot2Name); cy.spotlightFilter(Filter.People);
cy.wait(3000); // wait for the dialog code to settle cy.spotlightSearch().clear().type(bot2Name);
cy.spotlightResults().should("have.length", 1); cy.wait(3000); // wait for the dialog code to settle
cy.spotlightResults().eq(0).should("contain", bot2Name); cy.spotlightResults().should("have.length", 1);
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat"); cy.spotlightResults().eq(0).should("contain", bot2Name);
cy.get(".mx_SpotlightDialog_startGroupChat").click(); cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
}).then(() => { cy.get(".mx_SpotlightDialog_startGroupChat").click();
cy.get('[role=dialog]').should("contain", "Direct Messages"); })
}); .then(() => {
cy.get("[role=dialog]").should("contain", "Direct Messages");
});
}); });
it("should close spotlight after starting a DM", () => { 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, // our debouncing logic only starts the search after a short timeout,
// so we wait a few milliseconds. // so we wait a few milliseconds.
cy.wait(1000); cy.wait(1000);
cy.get(".mx_Spinner").should("not.exist").then(() => { cy.get(".mx_Spinner")
cy.spotlightResults().should("have.length", 2).then(() => { .should("not.exist")
cy.spotlightResults().eq(0) .then(() => {
.should("have.attr", "aria-selected", "true"); cy.spotlightResults()
cy.spotlightResults().eq(1) .should("have.length", 2)
.should("have.attr", "aria-selected", "false"); .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 { function markWindowBeforeReload(): void {
// mark our window object to "know" when it gets reloaded // 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", () => { describe("Threads", () => {
@ -30,10 +30,10 @@ describe("Threads", () => {
beforeEach(() => { beforeEach(() => {
// Default threads to ON for this spec // Default threads to ON for this spec
cy.enableLabsFeature("feature_thread"); 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 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; synapse = data;
cy.initTestUser(synapse, "Tom"); cy.initTestUser(synapse, "Tom");
@ -78,12 +78,12 @@ describe("Threads", () => {
cy.getBot(synapse, { cy.getBot(synapse, {
displayName: "BotBob", displayName: "BotBob",
autoAcceptInvites: false, autoAcceptInvites: false,
}).then(_bot => { }).then((_bot) => {
bot = _bot; bot = _bot;
}); });
let roomId: string; let roomId: string;
cy.createRoom({}).then(_roomId => { cy.createRoom({}).then((_roomId) => {
roomId = _roomId; roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId()); cy.inviteUser(roomId, bot.getUserId());
bot.joinRoom(roomId); bot.joinRoom(roomId);
@ -95,10 +95,11 @@ describe("Threads", () => {
// Wait for message to send, get its ID and save as @threadId // 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") 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 // Bot starts thread
cy.get<string>("@threadId").then(threadId => { cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, { bot.sendMessage(roomId, threadId, {
body: "Hello there", body: "Hello there",
msgtype: "m.text", msgtype: "m.text",
@ -119,7 +120,8 @@ describe("Threads", () => {
// User reacts to message instead // User reacts to message instead
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Hello there") 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(".mx_EmojiPicker").within(() => {
cy.get('input[type="text"]').type("wave"); cy.get('input[type="text"]').type("wave");
cy.contains('[role="menuitem"]', "👋").click(); cy.contains('[role="menuitem"]', "👋").click();
@ -127,7 +129,8 @@ describe("Threads", () => {
// User redacts their prior response // User redacts their prior response
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test") 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.get(".mx_IconizedContextMenu").within(() => {
cy.contains('[role="menuitem"]', "Remove").click(); cy.contains('[role="menuitem"]', "Remove").click();
}); });
@ -144,7 +147,7 @@ describe("Threads", () => {
cy.get(".mx_ThreadPanel .mx_BaseCard_close").click(); cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
// Bot responds to thread // Bot responds to thread
cy.get<string>("@threadId").then(threadId => { cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, { bot.sendMessage(roomId, threadId, {
body: "How are things?", body: "How are things?",
msgtype: "m.text", msgtype: "m.text",
@ -178,45 +181,55 @@ describe("Threads", () => {
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}"); 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_sender").should("contain", "Tom");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content") cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
.should("contain", "Great! How about yourself?"); "contain",
"Great! How about yourself?",
);
// User closes right panel // User closes right panel
cy.get(".mx_ThreadView .mx_BaseCard_close").click(); cy.get(".mx_ThreadView .mx_BaseCard_close").click();
// Bot responds to thread and saves the id of their message to @eventId // Bot responds to thread and saves the id of their message to @eventId
cy.get<string>("@threadId").then(threadId => { cy.get<string>("@threadId").then((threadId) => {
cy.wrap(bot.sendMessage(roomId, threadId, { cy.wrap(
body: "I'm very good thanks", bot
msgtype: "m.text", .sendMessage(roomId, threadId, {
}).then(res => res.event_id)).as("eventId"); body: "I'm very good thanks",
msgtype: "m.text",
})
.then((res) => res.event_id),
).as("eventId");
}); });
// User asserts // User asserts
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content") cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
.should("contain", "I'm very good thanks"); "contain",
"I'm very good thanks",
);
// Bot edits their latest event // Bot edits their latest event
cy.get<string>("@eventId").then(eventId => { cy.get<string>("@eventId").then((eventId) => {
bot.sendMessage(roomId, { bot.sendMessage(roomId, {
"body": "* I'm very good thanks :)", "body": "* I'm very good thanks :)",
"msgtype": "m.text", "msgtype": "m.text",
"m.new_content": { "m.new_content": {
"body": "I'm very good thanks :)", body: "I'm very good thanks :)",
"msgtype": "m.text", msgtype: "m.text",
}, },
"m.relates_to": { "m.relates_to": {
"rel_type": "m.replace", rel_type: "m.replace",
"event_id": eventId, event_id: eventId,
}, },
}); });
}); });
// User asserts // User asserts
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob"); cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content") cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
.should("contain", "I'm very good thanks :)"); "contain",
"I'm very good thanks :)",
);
}); });
it("can send voice messages", () => { it("can send voice messages", () => {
@ -227,7 +240,7 @@ describe("Threads", () => {
}); });
let roomId: string; let roomId: string;
cy.createRoom({}).then(_roomId => { cy.createRoom({}).then((_roomId) => {
roomId = _roomId; roomId = _roomId;
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
}); });
@ -237,7 +250,9 @@ describe("Threads", () => {
// Create thread // Create thread
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") 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_ThreadView_timelinePanelWrapper").should("have.length", 1);
cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click(); cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click();
@ -250,7 +265,7 @@ describe("Threads", () => {
it("right panel behaves correctly", () => { it("right panel behaves correctly", () => {
// Create room // Create room
let roomId: string; let roomId: string;
cy.createRoom({}).then(_roomId => { cy.createRoom({}).then((_roomId) => {
roomId = _roomId; roomId = _roomId;
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
}); });
@ -259,7 +274,9 @@ describe("Threads", () => {
// Create thread // Create thread
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") 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_ThreadView_timelinePanelWrapper").should("have.length", 1);
// Send message to thread // Send message to thread
@ -271,7 +288,9 @@ describe("Threads", () => {
// Open existing thread // Open existing thread
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot") 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_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. Bot");
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User"); 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 => { const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
cy.all([ cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => {
cy.window({ log: false }),
cy.getClient(),
]).then(([win, cli]) => {
const size = AVATAR_SIZE * win.devicePixelRatio; const size = AVATAR_SIZE * win.devicePixelRatio;
expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal( expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
// eslint-disable-next-line no-restricted-properties // eslint-disable-next-line no-restricted-properties
@ -75,10 +72,10 @@ describe("Timeline", () => {
let newAvatarUrl: string; let newAvatarUrl: string;
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, OLD_NAME).then(() => cy.initTestUser(synapse, OLD_NAME).then(() =>
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => { cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
roomId = _room1Id; roomId = _room1Id;
}), }),
); );
@ -154,8 +151,11 @@ describe("Timeline", () => {
it("should create and configure a room on IRC layout", () => { it("should create and configure a room on IRC layout", () => {
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " + cy.contains(
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); ".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.get(".mx_Spinner").should("not.exist");
cy.percySnapshot("Configured room on IRC layout"); cy.percySnapshot("Configured room on IRC layout");
}); });
@ -165,8 +165,10 @@ describe("Timeline", () => {
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished // Wait until configuration is finished
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " + cy.contains(
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); ".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Click "expand" link button // Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
@ -177,13 +179,13 @@ describe("Timeline", () => {
// = calc(var(--name-width) + 10px + var(--icon-width)) // = calc(var(--name-width) + 10px + var(--icon-width))
// = 80 + 10 + 14 = 104px // = 80 + 10 + 14 = 104px
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line") 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", "margin-inline-start", "104px")
.should('have.css', "inset-inline-start", "0px"); .should("have.css", "inset-inline-start", "0px");
cy.get(".mx_Spinner").should("not.exist"); cy.get(".mx_Spinner").should("not.exist");
// Exclude timestamp from snapshot // Exclude timestamp from snapshot
const percyCSS = ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " const percyCSS =
+ "{ visibility: hidden !important; }"; ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS }); cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS });
cy.checkA11y(); cy.checkA11y();
}); });
@ -192,8 +194,10 @@ describe("Timeline", () => {
sendEvent(roomId); sendEvent(roomId);
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary", cy.contains(
"created and configured the room.").should("exist"); ".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Edit message // Edit message
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => { 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(); cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
// Exclude timestamp from snapshot // Exclude timestamp from snapshot
const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " const percyCSS =
+ "{ visibility: hidden !important; }"; ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
// should not add inline start padding to a hidden event line on IRC layout // should not add inline start padding to a hidden event line on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line") cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").should(
.should('have.css', 'padding-inline-start', '0px'); "have.css",
"padding-inline-start",
"0px",
);
cy.percySnapshot("Hidden event line with zero padding on IRC layout", { percyCSS }); 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 // should add inline start padding to a hidden event line on modern layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line") 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 // 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 }); cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
}); });
@ -227,8 +234,10 @@ describe("Timeline", () => {
sendEvent(roomId); sendEvent(roomId);
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true); cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " + cy.contains(
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); ".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Edit message // Edit message
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => { 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"); 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 // 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_EventTile .mx_ViewSourceEvent")
cy.get(".mx_ViewSourceEvent_toggle").click('topLeft', { force: false }); .should("exist")
}); .realHover()
.within(() => {
cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false });
});
// Make sure the expand toggle worked // Make sure the expand toggle worked
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible"); 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", () => { it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => {
cy.visit("/#/room/" + roomId); cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble); cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " + cy.contains(
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist"); ".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " +
".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Click "expand" link button // Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click(); cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
@ -340,10 +355,14 @@ describe("Timeline", () => {
cy.getComposer().type(`${reply}{enter}`); cy.getComposer().type(`${reply}{enter}`);
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody") cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody").should(
.should("contain", MESSAGE); "contain",
cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply) MESSAGE,
.should("have.length", 1); );
cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply).should(
"have.length",
1,
);
}); });
it("can reply with a voice message", () => { it("can reply with a voice message", () => {
@ -355,10 +374,14 @@ describe("Timeline", () => {
cy.wait(3000); cy.wait(3000);
cy.get(".mx_RoomView_body .mx_MessageComposer .mx_MessageComposer_sendMessage").click(); 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") cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody").should(
.should("contain", MESSAGE); "contain",
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody") MESSAGE,
.should("have.length", 1); );
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", () => { it("should not show an analytics toast if config has nothing about posthog", () => {
cy.intercept("/config.json?cachebuster=*", req => { cy.intercept("/config.json?cachebuster=*", (req) => {
req.continue(res => { req.continue((res) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { posthog, ...body } = res.body; const { posthog, ...body } = res.body;
res.send(200, body); res.send(200, body);
}); });
}); });
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Tod"); cy.initTestUser(synapse, "Tod");
}); });
@ -66,8 +66,8 @@ describe("Analytics Toast", () => {
describe("with posthog enabled", () => { describe("with posthog enabled", () => {
beforeEach(() => { beforeEach(() => {
cy.intercept("/config.json?cachebuster=*", req => { cy.intercept("/config.json?cachebuster=*", (req) => {
req.continue(res => { req.continue((res) => {
res.send(200, { res.send(200, {
...res.body, ...res.body,
posthog: { posthog: {
@ -78,7 +78,7 @@ describe("Analytics Toast", () => {
}); });
}); });
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Tod"); cy.initTestUser(synapse, "Tod");
rejectToast("Notifications"); rejectToast("Notifications");

View file

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

View file

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

View file

@ -19,7 +19,7 @@ import { IWidget } from "matrix-widget-api";
import { SynapseInstance } from "../../plugins/synapsedocker"; import { SynapseInstance } from "../../plugins/synapsedocker";
const ROOM_NAME = 'Test Room'; const ROOM_NAME = "Test Room";
const WIDGET_ID = "fake-widget"; const WIDGET_ID = "fake-widget";
const WIDGET_HTML = ` const WIDGET_HTML = `
<html lang="en"> <html lang="en">
@ -32,18 +32,18 @@ const WIDGET_HTML = `
</html> </html>
`; `;
describe('Widget Layout', () => { describe("Widget Layout", () => {
let widgetUrl: string; let widgetUrl: string;
let synapse: SynapseInstance; let synapse: SynapseInstance;
let roomId: string; let roomId: string;
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Sally"); cy.initTestUser(synapse, "Sally");
}); });
cy.serveHtmlFile(WIDGET_HTML).then(url => { cy.serveHtmlFile(WIDGET_HTML).then((url) => {
widgetUrl = url; widgetUrl = url;
}); });
@ -53,34 +53,38 @@ describe('Widget Layout', () => {
roomId = id; roomId = id;
// setup widget via state event // setup widget via state event
cy.getClient().then(async matrixClient => { cy.getClient()
const content: IWidget = { .then(async (matrixClient) => {
id: WIDGET_ID, const content: IWidget = {
creatorUserId: 'somebody', id: WIDGET_ID,
type: 'widget', creatorUserId: "somebody",
name: 'widget', type: "widget",
url: widgetUrl, name: "widget",
}; url: widgetUrl,
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, WIDGET_ID); };
}).as('widgetEventSent'); await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
})
.as("widgetEventSent");
// set initial layout // set initial layout
cy.getClient().then(async matrixClient => { cy.getClient()
const content = { .then(async (matrixClient) => {
widgets: { const content = {
[WIDGET_ID]: { widgets: {
container: 'top', index: 1, width: 100, height: 0, [WIDGET_ID]: {
container: "top",
index: 1,
width: 100,
height: 0,
},
}, },
}, };
}; await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, ""); })
}).as('layoutEventSent'); .as("layoutEventSent");
}); });
cy.all([ cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(() => {
cy.get<string>("@widgetEventSent"),
cy.get<string>("@layoutEventSent"),
]).then(() => {
// open the room // open the room
cy.viewRoomByName(ROOM_NAME); cy.viewRoomByName(ROOM_NAME);
}); });
@ -91,31 +95,34 @@ describe('Widget Layout', () => {
cy.stopWebServers(); cy.stopWebServers();
}); });
it('manually resize the height of the top container layout', () => { it("manually resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250); cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
cy.get('.mx_AppsContainer_resizerHandle') cy.get(".mx_AppsContainer_resizerHandle")
.trigger('mousedown') .trigger("mousedown")
.trigger('mousemove', { clientX: 0, clientY: 550, force: true }) .trigger("mousemove", { clientX: 0, clientY: 550, force: true })
.trigger('mouseup', { 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', () => { it("programatically resize the height of the top container layout", () => {
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250); cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
cy.getClient().then(async matrixClient => { cy.getClient().then(async (matrixClient) => {
const content = { const content = {
widgets: { widgets: {
[WIDGET_ID]: { [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() { function openStickerPicker() {
cy.get('.mx_MessageComposer_buttonMenu').click(); cy.get(".mx_MessageComposer_buttonMenu").click();
cy.get('#stickersButton').click(); cy.get("#stickersButton").click();
} }
function sendStickerFromPicker() { function sendStickerFromPicker() {
@ -76,18 +76,16 @@ function sendStickerFromPicker() {
// to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can // to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can
// break into the iframe for us :( // break into the iframe for us :(
cy.accessIframe(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`).within({}, () => { 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. // 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) { function expectTimelineSticker(roomId: string) {
// Make sure it's in the right room // Make sure it's in the right room
cy.get('.mx_EventTile_sticker > a') cy.get(".mx_EventTile_sticker > a").should("have.attr", "href").and("include", `/${roomId}/`);
.should("have.attr", "href")
.and("include", `/${roomId}/`);
// Make sure the image points at the sticker image // Make sure the image points at the sticker image
cy.get<HTMLImageElement>(`img[alt="${STICKER_NAME}"]`) cy.get<HTMLImageElement>(`img[alt="${STICKER_NAME}"]`)
@ -107,12 +105,12 @@ describe("Stickers", () => {
let synapse: SynapseInstance; let synapse: SynapseInstance;
beforeEach(() => { beforeEach(() => {
cy.startSynapse("default").then(data => { cy.startSynapse("default").then((data) => {
synapse = data; synapse = data;
cy.initTestUser(synapse, "Sally"); cy.initTestUser(synapse, "Sally");
}); });
cy.serveHtmlFile(WIDGET_HTML).then(url => { cy.serveHtmlFile(WIDGET_HTML).then((url) => {
stickerPickerUrl = url; stickerPickerUrl = url;
}); });
}); });
@ -122,7 +120,7 @@ describe("Stickers", () => {
cy.stopWebServers(); cy.stopWebServers();
}); });
it('should send a sticker to multiple rooms', () => { it("should send a sticker to multiple rooms", () => {
cy.createRoom({ cy.createRoom({
name: ROOM_NAME_1, name: ROOM_NAME_1,
}).as("roomId1"); }).as("roomId1");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@ limitations under the License.
import * as net from "net"; import * as net from "net";
export async function getFreePort(): Promise<number> { export async function getFreePort(): Promise<number> {
return new Promise<number>(resolve => { return new Promise<number>((resolve) => {
const srv = net.createServer(); const srv = net.createServer();
srv.listen(0, () => { srv.listen(0, () => {
const port = (<net.AddressInfo>srv.address()).port; 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> => { 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. // note: we can't *set* the object because the window version is effectively a pointer.
for (const [k, v] of Object.entries(tweaks)) { for (const [k, v] of Object.entries(tweaks)) {
// @ts-ignore - for some reason it's not picking up on global.d.ts types. // @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 // 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 { function terminalLog(violations: axe.Result[]): void {
cy.task( cy.task(
'log', "log",
`${violations.length} accessibility violation${ `${violations.length} accessibility violation${violations.length === 1 ? "" : "s"} ${
violations.length === 1 ? '' : 's' violations.length === 1 ? "was" : "were"
} ${violations.length === 1 ? 'was' : 'were'} detected`, } detected`,
); );
// pluck specific keys to keep the table readable // pluck specific keys to keep the table readable
@ -38,24 +38,32 @@ function terminalLog(violations: axe.Result[]): void {
nodes: nodes.length, nodes: nodes.length,
})); }));
cy.task('table', violationData); cy.task("table", violationData);
} }
Cypress.Commands.overwrite("checkA11y", ( Cypress.Commands.overwrite(
originalFn: Chainable["checkA11y"], "checkA11y",
context?: string | Node | axe.ContextObject | undefined, (
options: Options = {}, originalFn: Chainable["checkA11y"],
violationCallback?: ((violations: axe.Result[]) => void) | undefined, context?: string | Node | axe.ContextObject | undefined,
skipFailures?: boolean, options: Options = {},
): void => { violationCallback?: ((violations: axe.Result[]) => void) | undefined,
return originalFn(context, { skipFailures?: boolean,
...options, ): void => {
rules: { return originalFn(
// Disable contrast checking for now as we have too many issues with it context,
'color-contrast': { {
enabled: false, ...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); opts = Object.assign({}, defaultCreateBotOptions, opts);
const username = Cypress._.uniqueId("userId_"); const username = Cypress._.uniqueId("userId_");
const password = Cypress._.uniqueId("password_"); 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}`); 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({ const cli = new win.matrixcs.MatrixClient({
baseUrl: synapse.baseUrl, baseUrl: synapse.baseUrl,
userId: credentials.userId, userId: credentials.userId,
@ -103,12 +103,17 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
} }
return cy.wrap( return cy.wrap(
cli.initCrypto() cli
.initCrypto()
.then(() => cli.setGlobalErrorOnUnknownDevices(false)) .then(() => cli.setGlobalErrorOnUnknownDevices(false))
.then(() => cli.startClient()) .then(() => cli.startClient())
.then(() => cli.bootstrapCrossSigning({ .then(() =>
authUploadDeviceSigningKeys: async func => { await func({}); }, cli.bootstrapCrossSigning({
})) authUploadDeviceSigningKeys: async (func) => {
await func({});
},
}),
)
.then(() => cli), .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}'`)); return cy.wrap(Promise.reject(`Bot room join failed. Cannot find room '${roomName}'`));
}); });
Cypress.Commands.add("botSendMessage", ( Cypress.Commands.add(
cli: MatrixClient, "botSendMessage",
roomId: string, (cli: MatrixClient, roomId: string, message: string): Chainable<ISendEventResponse> => {
message: string, return cy.wrap(
): Chainable<ISendEventResponse> => { cli.sendMessage(roomId, {
return cy.wrap(cli.sendMessage(roomId, { msgtype: "m.text",
msgtype: "m.text", body: message,
body: message, }),
}), { log: false }); { log: false },
}); );
},
);

View file

@ -66,7 +66,7 @@ declare global {
roomId: string, roomId: string,
threadId: string | null, threadId: string | null,
eventType: string, eventType: string,
content: IContent content: IContent,
): Chainable<ISendEventResponse>; ): Chainable<ISendEventResponse>;
/** /**
* @param {string} name * @param {string} name
@ -89,10 +89,7 @@ declare global {
* can be sent to XMLHttpRequest.send (typically a File). Under node.js, * can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a a Buffer, String or ReadStream. * a a Buffer, String or ReadStream.
*/ */
uploadContent( uploadContent(file: FileType, opts?: UploadOpts): Chainable<Awaited<Upload["promise"]>>;
file: FileType,
opts?: UploadOpts,
): Chainable<Awaited<Upload["promise"]>>;
/** /**
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and * Turn an MXC URL into an HTTP one. <strong>This method is experimental and
* may change.</strong> * may change.</strong>
@ -133,23 +130,24 @@ declare global {
} }
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => { 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[]> => { Cypress.Commands.add("getDmRooms", (userId: string): Chainable<string[]> => {
return cy.getClient() return cy
.then(cli => cli.getAccountData("m.direct")?.getContent<Record<string, string[]>>()) .getClient()
.then(dmRoomMap => dmRoomMap[userId] ?? []); .then((cli) => cli.getAccountData("m.direct")?.getContent<Record<string, string[]>>())
.then((dmRoomMap) => dmRoomMap[userId] ?? []);
}); });
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => { 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 cli = win.mxMatrixClientPeg.matrixClient;
const resp = await cli.createRoom(options); const resp = await cli.createRoom(options);
const roomId = resp.room_id; const roomId = resp.room_id;
if (!cli.getRoom(roomId)) { if (!cli.getRoom(roomId)) {
await new Promise<void>(resolve => { await new Promise<void>((resolve) => {
const onRoom = (room: Room) => { const onRoom = (room: Room) => {
if (room.roomId === roomId) { if (room.roomId === roomId) {
cli.off(win.matrixcs.ClientEvent.Room, onRoom); cli.off(win.matrixcs.ClientEvent.Room, onRoom);
@ -168,7 +166,7 @@ Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable<string
return cy.createRoom({ return cy.createRoom({
...options, ...options,
creation_content: { 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", ( Cypress.Commands.add(
roomId: string, "sendEvent",
threadId: string | null, (roomId: string, threadId: string | null, eventType: string, content: IContent): Chainable<ISendEventResponse> => {
eventType: string, return cy.getClient().then(async (cli: MatrixClient) => {
content: IContent, return cli.sendEvent(roomId, threadId, eventType, content);
): Chainable<ISendEventResponse> => { });
return cy.getClient().then(async (cli: MatrixClient) => { },
return cli.sendEvent(roomId, threadId, eventType, content); );
});
});
Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => { Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
return cy.getClient().then(async (cli: MatrixClient) => { return cy.getClient().then(async (cli: MatrixClient) => {
@ -215,13 +211,15 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
}); });
Cypress.Commands.add("bootstrapCrossSigning", () => { Cypress.Commands.add("bootstrapCrossSigning", () => {
cy.window({ log: false }).then(win => { cy.window({ log: false }).then((win) => {
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({ win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => { await func({}); }, authUploadDeviceSigningKeys: async (func) => {
await func({});
},
}); });
}); });
}); });
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => { 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", () => { Cypress.Commands.add("mockClipboard", () => {
cy.window({ log: false }).then(win => { cy.window({ log: false }).then((win) => {
win.navigator.clipboard.writeText = (text) => { win.navigator.clipboard.writeText = (text) => {
copyText = text; copyText = text;
return Promise.resolve(); return Promise.resolve();
@ -54,4 +54,4 @@ Cypress.Commands.add("getClipboardText", (): Chainable<string> => {
}); });
// Needed to make this file a module // 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> => { 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`); return cy.get(`${panelClass} .mx_MessageComposer`);
}); });
@ -41,8 +41,8 @@ Cypress.Commands.add("openMessageComposerOptions", (isRightPanel?: boolean): Cha
cy.getComposer(isRightPanel).within(() => { cy.getComposer(isRightPanel).within(() => {
cy.get('[aria-label="More options"]').click(); 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 // 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/ // Inspired by https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
Cypress.Commands.add("accessIframe", (selector: string): Chainable<JQuery<HTMLElement>> => { Cypress.Commands.add("accessIframe", (selector: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(selector) return (
.its("0.contentDocument.body").should("not.be.empty") cy
// Cypress loses types in the mess of wrapping, so force cast .get(selector)
.then(cy.wrap) as Chainable<JQuery<HTMLElement>>; .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 // 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> => { Cypress.Commands.add("enableLabsFeature", (feature: string): Chainable<null> => {
return cy.window({ log: false }).then(win => { return cy
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true"); .window({ log: false })
}).then(() => null); .then((win) => {
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
})
.then(() => null);
}); });
// Needed to make this file a module // Needed to make this file a module
export { }; export {};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -38,17 +38,19 @@ export interface Message {
} }
Cypress.Commands.add("scrollToTop", (): void => { Cypress.Commands.add("scrollToTop", (): void => {
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => { cy.get(".mx_RoomView_timeline .mx_ScrollPanel")
if (ref.scrollTop() > 0) { .scrollTo("top", { duration: 100 })
return cy.scrollToTop(); .then((ref) => {
} if (ref.scrollTop() > 0) {
}); return cy.scrollToTop();
}
});
}); });
Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<JQuery> => { 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 // 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. // 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; let latestSender: string;
for (let i = 0; i < refs.length; i++) { for (let i = 0; i < refs.length; i++) {
const ref = refs.eq(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 // Needed to make this file a module
export { }; export {};

View file

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

View file

@ -2,22 +2,12 @@
"compilerOptions": { "compilerOptions": {
"target": "es2016", "target": "es2016",
"jsx": "react", "jsx": "react",
"lib": [ "lib": ["es2020", "dom", "dom.iterable"],
"es2020", "types": ["cypress", "cypress-axe", "@percy/cypress"],
"dom",
"dom.iterable"
],
"types": [
"cypress",
"cypress-axe",
"@percy/cypress"
],
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"module": "commonjs" "module": "commonjs"
}, },
"include": [ "include": ["**/*.ts"]
"**/*.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 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 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). 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. 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 # Cypress in Element Web
## Scope of this Document ## Scope of this Document
This doc is about our Cypress tests in Element Web and how we use Cypress to write tests. This doc is about our Cypress tests in Element Web and how we use Cypress to write tests.
It aims to cover: It aims to cover:
* How to run the tests yourself
* How the tests work - How to run the tests yourself
* How to write great Cypress tests - How the tests work
* Visual testing - How to write great Cypress tests
- Visual testing
## Running the Tests ## Running the Tests
Our Cypress tests run automatically as part of our CI along with our other 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. on every pull request and on every merge to develop & master.
@ -43,6 +46,7 @@ yarn run test:cypress:open
``` ```
## How the Tests Work ## How the Tests Work
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
as is typical for Cypress tests. Likewise, tests live in `cypress/e2e`. 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. at the start of each test run.
## Writing Tests ## Writing Tests
Mostly this is the same advice as for writing any other Cypress test: the Cypress 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. 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 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. a read.
### Getting a Synapse ### 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: `cy.startSynapse()` to provide a Synapse instance to log into:
```javascript ```javascript
cy.startSynapse("consent").then(result => { cy.startSynapse("consent").then((result) => {
synapse = 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 To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
object you received when starting it. object you received when starting it.
```javascript ```javascript
cy.stopSynapse(synapse); cy.stopSynapse(synapse);
``` ```
### Synapse Config Templates ### Synapse Config Templates
When a Synapse instance is started, it's given a config generated from one of the config 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 templates in `cypress/plugins/synapsedocker/templates`. There are a couple of special files
in these templates: in these templates:
* `homeserver.yaml`:
Template substitution happens in this file. Template variables are: - `homeserver.yaml`:
* `REGISTRATION_SECRET`: The secret used to register users via the REST API. Template substitution happens in this file. Template variables are:
* `MACAROON_SECRET_KEY`: Generated each time for security - `REGISTRATION_SECRET`: The secret used to register users via the REST API.
* `FORM_SECRET`: Generated each time for security - `MACAROON_SECRET_KEY`: Generated each time for security
* `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at - `FORM_SECRET`: Generated each time for security
* `localhost.signing.key`: A signing key is auto-generated and saved to this file. - `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
Config templates should not contain a signing key and instead assume that one will exist - `localhost.signing.key`: A signing key is auto-generated and saved to this file.
in 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` All other files in the template are copied recursively to `/data/`, so the file `foo.html`
in a template can be referenced in the config as `/data/foo.html`. in a template can be referenced in the config as `/data/foo.html`.
### Logging In ### Logging In
There exists a basic utility to start the app with a random user already logged in: There exists a basic utility to start the app with a random user already logged in:
```javascript ```javascript
cy.initTestUser(synapse, "Jeff"); cy.initTestUser(synapse, "Jeff");
``` ```
It takes the SynapseInstance you received from `startSynapse` and a display name for your test user. 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 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 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. but the signature can be maintained for simpler maintenance.
### Joining a Room ### Joining a Room
Many tests will also want to start with the client in a room, ready to send & receive messages. Best 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 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. API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
### Convenience APIs ### Convenience APIs
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room 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. creation that can be called to set up tests.
### Using matrix-js-sdk ### 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 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. exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
This may be revisited in the future. This may be revisited in the future.
## Good Test Hygiene ## Good Test Hygiene
This section mostly summarises general good Cypress testing practice, and should not be news to anyone This section mostly summarises general good Cypress testing practice, and should not be news to anyone
already familiar with Cypress. 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 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 all assertions are retired until they either pass or time out, so you should never need to
manually wait for an element. 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 '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 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. 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). 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 Needing to wait for things can also be because of race conditions in the app itself, which ideally
shouldn't be there! 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. should generally try to adhere to them.
## Percy Visual Testing ## Percy Visual Testing
We also support visual testing via Percy, this extracts the DOM from Cypress and renders it using custom renderers 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. 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 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 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. 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 # Composer Features
## Auto Complete ## Auto Complete
- Hitting tab tries to auto-complete the word before the caret as a room member - Hitting tab tries to auto-complete the word before the caret as a room member
- If no matching name is found, a visual bell is shown - If no matching name is found, a visual bell is shown
- @ + a letter opens auto complete for members starting with the given letter - @ + a letter opens auto complete for members starting with the given letter
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill - When inserting a user pill at the start in the composer, a colon and space is appended to the pill
- When inserting a user pill anywhere else in composer, only a space is appended to the pill - When inserting a user pill anywhere else in composer, only a space is appended to the pill
- # + a letter opens auto complete for rooms starting with the given letter - # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji - : open auto complete for emoji
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options - Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
- Pressing tab while the autocomplete is open goes to the next autocomplete option, - Pressing tab while the autocomplete is open goes to the next autocomplete option,
wrapping around at the end after reverting to the typed text first. wrapping around at the end after reverting to the typed text first.
## Formatting ## Formatting
- When selecting text, a formatting bar appears above the selection. - When selecting text, a formatting bar appears above the selection.
- The formatting bar allows to format the selected test as: - The formatting bar allows to format the selected test as:
bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected). bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
- Formatting is applied as markdown syntax. - Formatting is applied as markdown syntax.
- Hitting ctrl/cmd+B also marks the selected text as bold - Hitting ctrl/cmd+B also marks the selected text as bold
- Hitting ctrl/cmd+I also marks the selected text as italic - Hitting ctrl/cmd+I also marks the selected text as italic
- Hitting ctrl/cmd+> also marks the selected text as a blockquote - Hitting ctrl/cmd+> also marks the selected text as a blockquote
## Misc ## Misc
- When hitting the arrow-up button while having the caret at the start in the composer, - When hitting the arrow-up button while having the caret at the start in the composer,
the last message sent by the syncing user is edited. the last message sent by the syncing user is edited.
- Clicking a display name on an event in the timeline inserts a user pill into the composer - Clicking a display name on an event in the timeline inserts a user pill into the composer
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled - Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
- Typing in the composer sends typing notifications in the room - Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications - Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break - Pressing shift+enter inserts a line break
- Pressing enter sends the message. - Pressing enter sends the message.
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer. - Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to. - Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer. - Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.

View file

@ -17,7 +17,7 @@ Let's say we want to close a menu when the correct keys were pressed:
```ts ```ts
const onKeyDown = (ev: KeyboardEvent): void => { const onKeyDown = (ev: KeyboardEvent): void => {
let handled = true; let handled = true;
const action = getKeyBindingManager().getAccessibilityAction(ev) const action = getKeyBindingManager().getAccessibilityAction(ev);
switch (action) { switch (action) {
case KeyBindingAction.Escape: case KeyBindingAction.Escape:
closeMenu(); closeMenu();
@ -26,12 +26,12 @@ const onKeyDown = (ev: KeyboardEvent): void => {
handled = false; handled = false;
break; break;
} }
if (handled) { if (handled) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
} }
} };
``` ```
## Managing keyboard shortcuts ## 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. Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component.
eg eg
``` ```
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg'; 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` The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html`
which takes several parameters: which takes several parameters:
*Query string*: _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.
*Hash/fragment (formatted as a query string)*: - `widgetId`: The ID of the widget. This is needed for communication back to the
* `conferenceDomain`: The domain to connect Jitsi Meet to. react-sdk.
* `conferenceId`: The room or conference ID to connect Jitsi Meet to. - `parentUrl`: The URL of the parent window. This is also needed for
* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not communication back to the react-sdk.
be present, should default to `false`.
* `displayName`: The display name of the user viewing the widget. May not _Hash/fragment (formatted as a query string)_:
be present or could be null.
* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May - `conferenceDomain`: The domain to connect Jitsi Meet to.
not be present or could be null. - `conferenceId`: The room or conference ID to connect Jitsi Meet to.
* `userId`: The MXID of the user viewing the widget. May not be present or could - `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
be null. 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 The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently
being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`. being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`.

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. 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 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` 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 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 accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things
tidy, this is an intentional design decision. 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 `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, 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 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) 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. 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) ![](img/RoomListStore2.png)
Legend: Legend:
* Orange = External event.
* Purple = Deterministic flow. - Orange = External event.
* Green = Algorithm definition. - Purple = Deterministic flow.
* Red = Exit condition/point. - Green = Algorithm definition.
* Blue = Process definition. - Red = Exit condition/point.
- Blue = Process definition.
## Algorithms involved ## 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 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. 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 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, 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. 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 algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
relative (perceived) importance to the user: relative (perceived) importance to the user:
* **Red**: The room has unread mentions waiting for the user. - **Red**: The room has unread mentions waiting for the user.
* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread - **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
messages which cause a push notification or badge count. Typically, this is the default as rooms get messages which cause a push notification or badge count. Typically, this is the default as rooms get
set to 'All Messages'. set to 'All Messages'.
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without - **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
a badge/notification count (or 'Mentions Only'/'Muted'). a badge/notification count (or 'Mentions Only'/'Muted').
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user - **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
last read it. last read it.
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
above bold, etc. above bold, etc.

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. 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 (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. 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 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. of dealing with the different levels and exposes easy to use getters and setters.
## Levels ## Levels
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
order of priority, are: order of priority, are:
* `device` - The current user's device
* `room-device` - The current user's device, but only when in a specific room - `device` - The current user's device
* `room-account` - The current user's account, but only when in a specific room - `room-device` - The current user's device, but only when in a specific room
* `account` - The current user's account - `room-account` - The current user's account, but only when in a specific room
* `room` - A specific room (setting for all members of the room) - `account` - The current user's account
* `config` - Values are defined by the `setting_defaults` key (usually) in `config.json` - `room` - A specific room (setting for all members of the room)
* `default` - The hardcoded default for the settings - `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 Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
that room administrators cannot force account-only settings upon participants. that room administrators cannot force account-only settings upon participants.
## Settings ## Settings
Settings are the different options a user may set or experience in the application. These are pre-defined in 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 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): settings, like the "theme" setting, are special cased in the config file):
```json5 ```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 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 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: although there are circumstances where this changes. An example of a safe call is:
```javascript ```javascript
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM); const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
if (isSupported) { if (isSupported) {
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM); const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) { if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue); 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 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. `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 ```html
group="your-radio-group" // this enables radio button support <SettingsFlag name="theSettingId" level={SettingsLevel.ROOM} roomId="!curbf:matrix.org" label={_td("Your label here")}
value="yourValueHere" // the value for this particular option // 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 ### 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 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`. for you. If a display name cannot be found, it will return `null`.
## Features ## Features
Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually 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 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 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. appear in the "labs" section of the user's settings.
Features can be controlled at the config level using the following structure: Features can be controlled at the config level using the following structure:
```json ```json
"features": { "features": {
"feature_lazyloading": true "feature_lazyloading": true
@ -144,7 +140,6 @@ additional steps to actually enable notifications.
For more information, see `src/settings/controllers/SettingController.ts`. For more information, see `src/settings/controllers/SettingController.ts`.
## Local echo ## Local echo
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a `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. SettingsStore.getValue(...); // this will return the value set in `setValue` above.
``` ```
## Watching for changes ## 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 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 ```javascript
class MyComponent extends React.Component { class MyComponent extends React.Component {
settingWatcherRef = null; settingWatcherRef = null;
componentWillMount() { componentWillMount() {
const callback = (settingName, roomId, level, newValAtLevel, newVal) => { const callback = (settingName, roomId, level, newValAtLevel, newVal) => {
this.setState({color: newVal}); this.setState({ color: newVal });
}; };
this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", callback); this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", callback);
} }
@ -190,7 +183,6 @@ class MyComponent extends React.Component {
} }
``` ```
# Maintainers Reference # Maintainers Reference
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is 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 ```json5
{ {
"imgSrc": "", // the src of the image to display in the download link imgSrc: "", // the src of the image to display in the download link
"imgStyle": "", // the style to apply to the image imgStyle: "", // the style to apply to the image
"style": "", // the style to apply to the download link style: "", // the style to apply to the download link
"download": "", // download attribute to pass to the <a/> tag download: "", // download attribute to pass to the <a/> tag
"textContent": "", // the text to put inside the download link 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 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). sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key).
Full example content: Full example content:
```json5 ```json5
{ {
"widgets": { widgets: {
"first-widget-id": { "first-widget-id": {
"container": "top", container: "top",
"index": 0, index: 0,
"width": 60, width: 60,
"height": 40 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 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 ## `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 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` 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 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. represent leftmost widgets.
The `width` is relative width within the container in percentage points. This will be clamped to a 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 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. 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 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 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 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", "name": "matrix-react-sdk",
"version": "3.62.0", "version": "3.62.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/matrix-org/matrix-react-sdk" "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"
}, },
"transformIgnorePatterns": [ "license": "Apache-2.0",
"/node_modules/(?!matrix-js-sdk).+$" "files": [
"lib",
"res",
"src",
"scripts",
"git-revision.txt",
"docs",
"header",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"package.json",
".stylelintrc.js"
], ],
"collectCoverageFrom": [ "main": "./src/index.ts",
"<rootDir>/src/**/*.{js,ts,tsx}" "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": [ "scripts": {
"text-summary", "prepublishOnly": "yarn build",
"lcov" "i18n": "matrix-gen-i18n",
], "prunei18n": "matrix-prune-i18n",
"testResultsProcessor": "@casualbot/jest-sonar-reporter" "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",
"@casualbot/jest-sonar-reporter": { "rethemendex": "res/css/rethemendex.sh",
"outputDirectory": "coverage", "clean": "rimraf lib",
"outputName": "jest-sonar-report.xml", "build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"relativePaths": true "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: subprojects:
matrix-js-sdk: matrix-js-sdk:
includeByDefault: false includeByDefault: false

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