Apply prettier formatting
This commit is contained in:
parent
1cac306093
commit
526645c791
1576 changed files with 65385 additions and 62478 deletions
96
.eslintrc.js
96
.eslintrc.js
|
@ -1,12 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
"plugin:matrix-org/react",
|
||||
"plugin:matrix-org/a11y",
|
||||
],
|
||||
plugins: ["matrix-org"],
|
||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
@ -40,34 +34,47 @@ module.exports = {
|
|||
],
|
||||
|
||||
// Ban matrix-js-sdk/src imports in favour of matrix-js-sdk/src/matrix imports to prevent unleashing hell.
|
||||
"no-restricted-imports": ["error", {
|
||||
"paths": [{
|
||||
"name": "matrix-js-sdk",
|
||||
"message": "Please use matrix-js-sdk/src/matrix instead",
|
||||
}, {
|
||||
"name": "matrix-js-sdk/",
|
||||
"message": "Please use matrix-js-sdk/src/matrix instead",
|
||||
}, {
|
||||
"name": "matrix-js-sdk/src",
|
||||
"message": "Please use matrix-js-sdk/src/matrix instead",
|
||||
}, {
|
||||
"name": "matrix-js-sdk/src/",
|
||||
"message": "Please use matrix-js-sdk/src/matrix instead",
|
||||
}, {
|
||||
"name": "matrix-js-sdk/src/index",
|
||||
"message": "Please use matrix-js-sdk/src/matrix instead",
|
||||
}, {
|
||||
"name": "matrix-react-sdk",
|
||||
"message": "Please use matrix-react-sdk/src/index instead",
|
||||
}, {
|
||||
"name": "matrix-react-sdk/",
|
||||
"message": "Please use matrix-react-sdk/src/index instead",
|
||||
}],
|
||||
"patterns": [{
|
||||
"group": ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
|
||||
"message": "Please use matrix-js-sdk/src/* instead",
|
||||
}],
|
||||
}],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: "matrix-js-sdk",
|
||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||
},
|
||||
{
|
||||
name: "matrix-js-sdk/",
|
||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||
},
|
||||
{
|
||||
name: "matrix-js-sdk/src",
|
||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||
},
|
||||
{
|
||||
name: "matrix-js-sdk/src/",
|
||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||
},
|
||||
{
|
||||
name: "matrix-js-sdk/src/index",
|
||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||
},
|
||||
{
|
||||
name: "matrix-react-sdk",
|
||||
message: "Please use matrix-react-sdk/src/index instead",
|
||||
},
|
||||
{
|
||||
name: "matrix-react-sdk/",
|
||||
message: "Please use matrix-react-sdk/src/index instead",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ["matrix-js-sdk/lib", "matrix-js-sdk/lib/", "matrix-js-sdk/lib/**"],
|
||||
message: "Please use matrix-js-sdk/src/* instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// There are too many a11y violations to fix at once
|
||||
// Turn violated rules off until they are fixed
|
||||
|
@ -90,15 +97,8 @@ module.exports = {
|
|||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"src/**/*.{ts,tsx}",
|
||||
"test/**/*.{ts,tsx}",
|
||||
"cypress/**/*.ts",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
"plugin:matrix-org/react",
|
||||
],
|
||||
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts"],
|
||||
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
|
||||
rules: {
|
||||
// temporary disabled
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
|
@ -151,12 +151,12 @@ module.exports = {
|
|||
"src/components/views/rooms/MessageComposer.tsx",
|
||||
"src/components/views/rooms/ReplyPreview.tsx",
|
||||
"src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx",
|
||||
"src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx"
|
||||
"src/components/views/settings/tabs/user/GeneralUserSettingsTab.tsx",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
|
@ -166,7 +166,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
function buildRestrictedPropertiesOptions(properties, message) {
|
||||
return properties.map(prop => {
|
||||
return properties.map((prop) => {
|
||||
let [object, property] = prop.split(".");
|
||||
if (object === "*") {
|
||||
object = undefined;
|
||||
|
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -2,9 +2,9 @@
|
|||
|
||||
## Checklist
|
||||
|
||||
* [ ] Tests written for new code (and old code if feasible)
|
||||
* [ ] Linter and other CI checks pass
|
||||
* [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md))
|
||||
- [ ] Tests written for new code (and old code if feasible)
|
||||
- [ ] Linter and other CI checks pass
|
||||
- [ ] Sign-off given on the changes (see [CONTRIBUTING.md](https://github.com/matrix-org/matrix-react-sdk/blob/develop/CONTRIBUTING.md))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
|
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"]
|
||||
}
|
||||
|
|
52
.github/workflows/backport.yml
vendored
52
.github/workflows/backport.yml
vendored
|
@ -1,30 +1,30 @@
|
|||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
311
.github/workflows/cypress.yaml
vendored
311
.github/workflows/cypress.yaml
vendored
|
@ -1,172 +1,175 @@
|
|||
# Triggers after the layered build has finished, taking the artifact and running cypress on it
|
||||
name: Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Element Web - Build" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
outputs:
|
||||
uuid: ${{ steps.uuid.outputs.value }}
|
||||
pr_id: ${{ steps.prdetails.outputs.pr_id }}
|
||||
commit_message: ${{ steps.commit.outputs.message }}
|
||||
commit_author: ${{ steps.commit.outputs.author }}
|
||||
commit_email: ${{ steps.commit.outputs.email }}
|
||||
percy_enable: ${{ steps.percy.outputs.value || '1' }}
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
outputs:
|
||||
uuid: ${{ steps.uuid.outputs.value }}
|
||||
pr_id: ${{ steps.prdetails.outputs.pr_id }}
|
||||
commit_message: ${{ steps.commit.outputs.message }}
|
||||
commit_author: ${{ steps.commit.outputs.author }}
|
||||
commit_email: ${{ steps.commit.outputs.email }}
|
||||
percy_enable: ${{ steps.percy.outputs.value || '1' }}
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- id: prdetails
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
- id: prdetails
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
- name: Get commit details
|
||||
id: commit
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const response = await github.rest.git.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: "${{ github.event.workflow_run.head_sha }}",
|
||||
});
|
||||
core.setOutput("message", response.data.message);
|
||||
core.setOutput("author", response.data.author.name);
|
||||
core.setOutput("email", response.data.author.email);
|
||||
- name: Get commit details
|
||||
id: commit
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const response = await github.rest.git.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: "${{ github.event.workflow_run.head_sha }}",
|
||||
});
|
||||
core.setOutput("message", response.data.message);
|
||||
core.setOutput("author", response.data.author.name);
|
||||
core.setOutput("email", response.data.author.email);
|
||||
|
||||
# Only run Percy when it is demanded or on develop
|
||||
- name: Disable Percy if not needed
|
||||
id: percy
|
||||
if: |
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
!contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
|
||||
run: echo "::set-output name=value::0"
|
||||
# Only run Percy when it is demanded or on develop
|
||||
- name: Disable Percy if not needed
|
||||
id: percy
|
||||
if: |
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
!contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
|
||||
run: echo "::set-output name=value::0"
|
||||
|
||||
- name: Generate unique ID 💎
|
||||
id: uuid
|
||||
run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")"
|
||||
- name: Generate unique ID 💎
|
||||
id: uuid
|
||||
run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")"
|
||||
|
||||
tests:
|
||||
name: "Run Tests"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment: Cypress
|
||||
#strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# # Run 4 instances in Parallel
|
||||
# runner: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# XXX: We're checking out untrusted code in a secure context
|
||||
# We need to be careful to not trust anything this code outputs/may do
|
||||
# We need to check this out to access the cypress tests which are on the head branch
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
tests:
|
||||
name: "Run Tests"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment:
|
||||
Cypress
|
||||
#strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# # Run 4 instances in Parallel
|
||||
# runner: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# XXX: We're checking out untrusted code in a secure context
|
||||
# We need to be careful to not trust anything this code outputs/may do
|
||||
# We need to check this out to access the cypress tests which are on the head branch
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4.2.2
|
||||
with:
|
||||
# The built-in Electron runner seems to grind to a halt trying
|
||||
# to run the tests, so use chrome.
|
||||
browser: chrome
|
||||
start: npx serve -p 8080 webapp
|
||||
wait-on: 'http://localhost:8080'
|
||||
record: true
|
||||
#parallel: true
|
||||
#command-prefix: 'yarn percy exec --parallel --'
|
||||
command-prefix: 'yarn percy exec --'
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
# pass the Dashboard record key as an environment variable
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4.2.2
|
||||
with:
|
||||
# The built-in Electron runner seems to grind to a halt trying
|
||||
# to run the tests, so use chrome.
|
||||
browser: chrome
|
||||
start: npx serve -p 8080 webapp
|
||||
wait-on: "http://localhost:8080"
|
||||
record:
|
||||
true
|
||||
#parallel: true
|
||||
#command-prefix: 'yarn percy exec --parallel --'
|
||||
command-prefix: "yarn percy exec --"
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
# pass the Dashboard record key as an environment variable
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
# Use existing chromium rather than downloading another
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||
# Use existing chromium rather than downloading another
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||
|
||||
# pass GitHub token to allow accurately detecting a build vs a re-run build
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# pass GitHub token to allow accurately detecting a build vs a re-run build
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# make Node's os.tmpdir() return something where we actually have permissions
|
||||
TMPDIR: ${{ runner.temp }}
|
||||
# make Node's os.tmpdir() return something where we actually have permissions
|
||||
TMPDIR: ${{ runner.temp }}
|
||||
|
||||
# tell Cypress more details about the context of this run
|
||||
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
|
||||
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
|
||||
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
|
||||
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
|
||||
# tell Cypress more details about the context of this run
|
||||
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
|
||||
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
|
||||
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
|
||||
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
|
||||
|
||||
# pass the Percy token as an environment variable
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }}
|
||||
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
|
||||
# tell Percy more details about the context of this run
|
||||
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
|
||||
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
# pass the Percy token as an environment variable
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }}
|
||||
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
|
||||
# tell Percy more details about the context of this run
|
||||
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PERCY_PULL_REQUEST:
|
||||
${{ needs.prepare.outputs.pr_id }}
|
||||
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-results
|
||||
path: |
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/synapselogs
|
||||
- name: Upload Artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-results
|
||||
path: |
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/synapselogs
|
||||
|
||||
report:
|
||||
name: Report results
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
report:
|
||||
name: Report results
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
|
84
.github/workflows/element-web.yaml
vendored
84
.github/workflows/element-web.yaml
vendored
|
@ -3,52 +3,52 @@
|
|||
# as an artifact and run integration tests.
|
||||
name: Element Web - Build
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
run: |
|
||||
scripts/ci/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
REACT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
|
||||
echo "::set-output name=VERSION::$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA"
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
run: |
|
||||
scripts/ci/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
REACT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
|
||||
echo "::set-output name=VERSION::$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA"
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
working-directory: ./element-web
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
echo $VERSION > webapp/version
|
||||
working-directory: ./element-web
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
echo $VERSION > webapp/version
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: previewbuild
|
||||
path: element-web/webapp
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: previewbuild
|
||||
path: element-web/webapp
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
|
|
64
.github/workflows/i18n_check.yml
vendored
64
.github/workflows/i18n_check.yml
vendored
|
@ -1,40 +1,40 @@
|
|||
name: i18n Check
|
||||
on:
|
||||
workflow_call: { }
|
||||
workflow_call: {}
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: "Get modified files"
|
||||
id: changed_files
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
src/i18n/strings/*
|
||||
files_ignore: |
|
||||
src/i18n/strings/en_EN.json
|
||||
- name: "Get modified files"
|
||||
id: changed_files
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: |
|
||||
src/i18n/strings/*
|
||||
files_ignore: |
|
||||
src/i18n/strings/en_EN.json
|
||||
|
||||
- name: "Assert only en_EN was modified"
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.user.login != 'RiotTranslateBot' &&
|
||||
steps.changed_files.outputs.any_modified == 'true'
|
||||
run: |
|
||||
echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways."
|
||||
exit 1
|
||||
- name: "Assert only en_EN was modified"
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.user.login != 'RiotTranslateBot' &&
|
||||
steps.changed_files.outputs.any_modified == 'true'
|
||||
run: |
|
||||
echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways."
|
||||
exit 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install --pure-lockfile"
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: i18n Check
|
||||
run: "yarn run diff-i18n"
|
||||
- name: i18n Check
|
||||
run: "yarn run diff-i18n"
|
||||
|
|
124
.github/workflows/netlify.yaml
vendored
124
.github/workflows/netlify.yaml
vendored
|
@ -2,70 +2,70 @@
|
|||
# and uploading it to netlify
|
||||
name: Upload Preview Build to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Element Web - Build" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
steps:
|
||||
- name: 📝 Create Deployment
|
||||
uses: bobheadxi/deployments@v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: Netlify
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
steps:
|
||||
- name: 📝 Create Deployment
|
||||
uses: bobheadxi/deployments@v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: Netlify
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
|
||||
- id: prdetails
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
- id: prdetails
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: element-build-and-test.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: element-build-and-test.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
- name: ☁️ Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2
|
||||
with:
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
enable-commit-comment: false
|
||||
alias: pr${{ steps.prdetails.outputs.pr_id }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
- name: ☁️ Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2
|
||||
with:
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
enable-commit-comment: false
|
||||
alias: pr${{ steps.prdetails.outputs.pr_id }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
|
||||
- name: 🚦 Update deployment status
|
||||
uses: bobheadxi/deployments@v1
|
||||
if: always()
|
||||
with:
|
||||
step: finish
|
||||
override: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
status: ${{ job.status }}
|
||||
env: ${{ steps.deployment.outputs.env }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
env_url: ${{ steps.netlify.outputs.deploy-url }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
- name: 🚦 Update deployment status
|
||||
uses: bobheadxi/deployments@v1
|
||||
if: always()
|
||||
with:
|
||||
step: finish
|
||||
override: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
status: ${{ job.status }}
|
||||
env: ${{ steps.deployment.outputs.env }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
env_url: ${{ steps.netlify.outputs.deploy-url }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
|
|
32
.github/workflows/notify-element-web.yml
vendored
32
.github/workflows/notify-element-web.yml
vendored
|
@ -1,19 +1,19 @@
|
|||
name: Notify element-web
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
push:
|
||||
branches: [develop]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
jobs:
|
||||
notify-element-web:
|
||||
name: "Notify Element Web"
|
||||
runs-on: ubuntu-latest
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-react-sdk'
|
||||
steps:
|
||||
- name: Notify element-web repo that a new SDK build is on develop
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: vector-im/element-web
|
||||
event-type: element-web-notify
|
||||
notify-element-web:
|
||||
name: "Notify Element Web"
|
||||
runs-on: ubuntu-latest
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-react-sdk'
|
||||
steps:
|
||||
- name: Notify element-web repo that a new SDK build is on develop
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: vector-im/element-web
|
||||
event-type: element-web-notify
|
||||
|
|
16
.github/workflows/pull_request.yaml
vendored
16
.github/workflows/pull_request.yaml
vendored
|
@ -1,12 +1,12 @@
|
|||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
action:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||
with:
|
||||
labels: "T-Defect,T-Enhancement,T-Task"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
action:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||
with:
|
||||
labels: "T-Defect,T-Enhancement,T-Task"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
|
@ -1,11 +1,11 @@
|
|||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
release:
|
||||
types: [published]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
22
.github/workflows/sonarqube.yml
vendored
22
.github/workflows/sonarqube.yml
vendored
|
@ -1,15 +1,15 @@
|
|||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
|
242
.github/workflows/static_analysis.yaml
vendored
242
.github/workflows/static_analysis.yaml
vendored
|
@ -1,148 +1,148 @@
|
|||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
working-directory: node_modules/matrix-js-sdk
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
- name: Switch js-sdk to release mode
|
||||
working-directory: node_modules/matrix-js-sdk
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
tsc-strict:
|
||||
name: Typescript Strict Error Checker
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
checks: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
args:
|
||||
- '--strict --noImplicitAny'
|
||||
- '--noImplicitAny'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
tsc-strict:
|
||||
name: Typescript Strict Error Checker
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
checks: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
args:
|
||||
- "--strict --noImplicitAny"
|
||||
- "--noImplicitAny"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get diff lines
|
||||
id: diff
|
||||
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
|
||||
with:
|
||||
include: '["\\.tsx?$"]'
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
|
||||
- name: Detecting files changed
|
||||
id: files
|
||||
uses: futuratrepadeira/changed-files@v4.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: '^.*\.tsx?$'
|
||||
- name: Get diff lines
|
||||
id: diff
|
||||
uses: Equip-Collaboration/diff-line-numbers@v1.0.0
|
||||
with:
|
||||
include: '["\\.tsx?$"]'
|
||||
|
||||
- uses: t3chguy/typescript-check-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
ts-extra-args: ${{ matrix.args }}
|
||||
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||
files-added: ${{ steps.files.outputs.files_created }}
|
||||
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
|
||||
- name: Detecting files changed
|
||||
id: files
|
||||
uses: futuratrepadeira/changed-files@v4.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pattern: '^.*\.tsx?$'
|
||||
|
||||
i18n_lint:
|
||||
name: "i18n Check"
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
|
||||
- uses: t3chguy/typescript-check-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-check: false
|
||||
check-fail-mode: added
|
||||
output-behaviour: annotate
|
||||
ts-extra-args: ${{ matrix.args }}
|
||||
files-changed: ${{ steps.files.outputs.files_updated }}
|
||||
files-added: ${{ steps.files.outputs.files_created }}
|
||||
files-deleted: ${{ steps.files.outputs.files_deleted }}
|
||||
line-numbers: ${{ steps.diff.outputs.lineNumbers }}
|
||||
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
- run: git diff --exit-code
|
||||
i18n_lint:
|
||||
name: "i18n Check"
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- run: git diff --exit-code
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
style_lint:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:style"
|
||||
style_lint:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:style"
|
||||
|
||||
- name: Dead Code Analysis
|
||||
run: |
|
||||
cd element-web
|
||||
yarn run analyse:unused-exports
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
|
||||
- name: Dead Code Analysis
|
||||
run: |
|
||||
cd element-web
|
||||
yarn run analyse:unused-exports
|
||||
|
|
92
.github/workflows/tests.yml
vendored
92
.github/workflows/tests.yml
vendored
|
@ -1,59 +1,59 @@
|
|||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@v1
|
||||
|
||||
- name: Run tests with coverage and metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: "yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||
- name: Run tests with coverage and metrics
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: "yarn coverage --ci --reporters github-actions '--reporters=<rootDir>/test/slowReporter.js' --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||
- name: Run tests with coverage
|
||||
if: github.ref != 'refs/heads/develop'
|
||||
run: "yarn coverage --ci --reporters github-actions --max-workers ${{ steps.cpu-cores.outputs.count }}"
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
app-tests:
|
||||
name: Element Web Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
app-tests:
|
||||
name: Element Web Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Run tests
|
||||
run: "./scripts/ci/app-tests.sh"
|
||||
- name: Run tests
|
||||
run: "./scripts/ci/app-tests.sh"
|
||||
|
|
10
.github/workflows/upgrade_dependencies.yml
vendored
10
.github/workflows/upgrade_dependencies.yml
vendored
|
@ -1,8 +1,8 @@
|
|||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
upgrade:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
upgrade:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
version: 2
|
||||
snapshot:
|
||||
widths:
|
||||
- 1024
|
||||
- 1920
|
||||
widths:
|
||||
- 1024
|
||||
- 1920
|
||||
percy:
|
||||
defer-uploads: true
|
||||
defer-uploads: true
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
module.exports = {
|
||||
"extends": [
|
||||
"stylelint-config-standard",
|
||||
"stylelint-config-prettier",
|
||||
],
|
||||
customSyntax: require('postcss-scss'),
|
||||
"plugins": [
|
||||
"stylelint-scss",
|
||||
],
|
||||
"rules": {
|
||||
extends: ["stylelint-config-standard", "stylelint-config-prettier"],
|
||||
customSyntax: require("postcss-scss"),
|
||||
plugins: ["stylelint-scss"],
|
||||
rules: {
|
||||
"color-hex-case": null,
|
||||
"comment-empty-line-before": null,
|
||||
"declaration-empty-line-before": null,
|
||||
|
@ -22,15 +17,18 @@ module.exports = {
|
|||
"at-rule-no-unknown": null,
|
||||
"no-descending-specificity": null,
|
||||
"no-empty-first-line": true,
|
||||
"scss/at-rule-no-unknown": [true, {
|
||||
// https://github.com/vector-im/element-web/issues/10544
|
||||
"ignoreAtRules": ["define-mixin"],
|
||||
}],
|
||||
"scss/at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
// https://github.com/vector-im/element-web/issues/10544
|
||||
ignoreAtRules: ["define-mixin"],
|
||||
},
|
||||
],
|
||||
// Disable `&_kind`-style selectors while our unused CSS approach is "Find & Replace All"
|
||||
// rather than a CI thing. Shorthand selectors are harder to detect when searching for a
|
||||
// class name. This regex is trying to *allow* anything except `&words`, such as `&::before`,
|
||||
// `&.mx_Class`, etc.
|
||||
"selector-nested-pattern": "^((&[ :.\\\[,])|([^&]))",
|
||||
"selector-nested-pattern": "^((&[ :.\\[,])|([^&]))",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
// Disable some defaults
|
||||
"selector-class-pattern": null,
|
||||
|
@ -52,4 +50,4 @@ module.exports = {
|
|||
"number-max-precision": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
27848
CHANGELOG.md
27848
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
|||
Contributing code to matrix-react-sdk
|
||||
=====================================
|
||||
# Contributing code to matrix-react-sdk
|
||||
|
||||
matrix-react-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
|
115
README.md
115
README.md
|
@ -9,18 +9,18 @@
|
|||
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
|
||||
matrix-react-sdk
|
||||
================
|
||||
# matrix-react-sdk
|
||||
|
||||
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
|
||||
|
||||
This package provides the React components needed to build a Matrix web client
|
||||
using React. It is not useable in isolation, and instead must be used from
|
||||
using React. It is not useable in isolation, and instead must be used from
|
||||
a 'skin'. A skin provides:
|
||||
* Customised implementations of presentation components.
|
||||
* Custom CSS
|
||||
* The containing application
|
||||
* Zero or more 'modules' containing non-UI functionality
|
||||
|
||||
- Customised implementations of presentation components.
|
||||
- Custom CSS
|
||||
- The containing application
|
||||
- Zero or more 'modules' containing non-UI functionality
|
||||
|
||||
As of Aug 2018, the only skin that exists is
|
||||
[`vector-im/element-web`](https://github.com/vector-im/element-web/); it and
|
||||
|
@ -28,19 +28,19 @@ As of Aug 2018, the only skin that exists is
|
|||
be considered as a single project (for instance, matrix-react-sdk bugs
|
||||
are currently filed against vector-im/element-web rather than this project).
|
||||
|
||||
Translation Status
|
||||
------------------
|
||||
## Translation Status
|
||||
|
||||
[![Translation status](https://translate.element.io/widgets/element-web/-/multi-auto.svg)](https://translate.element.io/engage/element-web/?utm_source=widget)
|
||||
|
||||
Developer Guide
|
||||
---------------
|
||||
## Developer Guide
|
||||
|
||||
Platform Targets:
|
||||
* Chrome, Firefox and Safari.
|
||||
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||
* Mobile Web is not currently a target platform - instead please use the native
|
||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||
|
||||
- Chrome, Firefox and Safari.
|
||||
- WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||
- Mobile Web is not currently a target platform - instead please use the native
|
||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||
|
||||
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||
**Please file PRs against `develop`!!**
|
||||
|
@ -52,22 +52,23 @@ Our code style is also the same as Element's:
|
|||
https://github.com/vector-im/element-web/blob/develop/code_style.md
|
||||
|
||||
Code should be committed as follows:
|
||||
* All new components:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||
* Element-specific components:
|
||||
https://github.com/vector-im/element-web/tree/master/src/components
|
||||
* In practice, `matrix-react-sdk` is still evolving so fast that the
|
||||
maintenance burden of customising and overriding these components for
|
||||
Element can seriously impede development. So right now, there should be
|
||||
very few (if any) customisations for Element.
|
||||
* CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
|
||||
* Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
|
||||
- All new components:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||
- Element-specific components:
|
||||
https://github.com/vector-im/element-web/tree/master/src/components
|
||||
- In practice, `matrix-react-sdk` is still evolving so fast that the
|
||||
maintenance burden of customising and overriding these components for
|
||||
Element can seriously impede development. So right now, there should be
|
||||
very few (if any) customisations for Element.
|
||||
- CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
|
||||
- Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
|
||||
React components in matrix-react-sdk come in two different flavours:
|
||||
'structures' and 'views'. Structures are stateful components which handle the
|
||||
'structures' and 'views'. Structures are stateful components which handle the
|
||||
more complicated business logic of the app, delegating their actual presentation
|
||||
rendering to stateless 'view' components. For instance, the RoomView component
|
||||
rendering to stateless 'view' components. For instance, the RoomView component
|
||||
that orchestrates the act of visualising the contents of a given Matrix chat
|
||||
room tracks lots of state for its child components which it passes into them for
|
||||
visual rendering via props.
|
||||
|
@ -75,74 +76,72 @@ visual rendering via props.
|
|||
Good separation between the components is maintained by adopting various best
|
||||
practices that anyone working with the SDK needs to be aware of and uphold:
|
||||
|
||||
* Components are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||
- Components are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||
|
||||
* They are organised in a typically two-level hierarchy - first whether the
|
||||
- They are organised in a typically two-level hierarchy - first whether the
|
||||
component is a view or a structure, and then a broad functional grouping
|
||||
(e.g. 'rooms' here)
|
||||
|
||||
* The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
||||
- The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
||||
CSS for matrix-react-sdk currently resides in
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css.
|
||||
|
||||
* Per-view CSS is optional - it could choose to inherit all its styling from
|
||||
- Per-view CSS is optional - it could choose to inherit all its styling from
|
||||
the context of the rest of the app, although this is unusual for any but
|
||||
* Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
structural components (lacking presentation logic) and the simplest view
|
||||
components.
|
||||
- Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
structural components (lacking presentation logic) and the simplest view
|
||||
components.
|
||||
|
||||
* The view MUST *only* refer to the CSS rules defined in its own CSS file.
|
||||
- The view MUST _only_ refer to the CSS rules defined in its own CSS file.
|
||||
'Stealing' styling information from other components (including parents)
|
||||
is not cool, as it breaks the independence of the components.
|
||||
|
||||
* CSS classes are named with an app-specific name-spacing prefix to try to
|
||||
avoid CSS collisions. The base skin shipped by Matrix.org with the
|
||||
matrix-react-sdk uses the naming prefix "mx_". A company called Yoyodyne
|
||||
Inc might use a prefix like "yy_" for its app-specific classes.
|
||||
- CSS classes are named with an app-specific name-spacing prefix to try to
|
||||
avoid CSS collisions. The base skin shipped by Matrix.org with the
|
||||
matrix-react-sdk uses the naming prefix "mx*". A company called Yoyodyne
|
||||
Inc might use a prefix like "yy*" for its app-specific classes.
|
||||
|
||||
* CSS classes use upper camel case when they describe React components - e.g.
|
||||
- CSS classes use upper camel case when they describe React components - e.g.
|
||||
.mx_MessageTile is the selector for the CSS applied to a MessageTile view.
|
||||
|
||||
* CSS classes for DOM elements within a view which aren't components are named
|
||||
- CSS classes for DOM elements within a view which aren't components are named
|
||||
by appending a lower camel case identifier to the view's class name - e.g.
|
||||
.mx_MessageTile_randomDiv is how you'd name the class of an arbitrary div
|
||||
within the MessageTile view.
|
||||
|
||||
* We deliberately use vanilla CSS 3.0 to avoid adding any more magic
|
||||
dependencies into the mix than we already have. App developers are welcome
|
||||
to use whatever floats their boat however. In future we'll start using
|
||||
- We deliberately use vanilla CSS 3.0 to avoid adding any more magic
|
||||
dependencies into the mix than we already have. App developers are welcome
|
||||
to use whatever floats their boat however. In future we'll start using
|
||||
css-next to pull in features like CSS variable support.
|
||||
|
||||
* The CSS for a component can override the rules for child components.
|
||||
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override
|
||||
- The CSS for a component can override the rules for child components.
|
||||
For instance, .mx*RoomList .mx_RoomTile {} would be the selector to override
|
||||
styles of RoomTiles when viewed in the context of a RoomList view.
|
||||
Overrides *must* be scoped to the View's CSS class - i.e. don't just define
|
||||
Overrides \_must* be scoped to the View's CSS class - i.e. don't just define
|
||||
.mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its
|
||||
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
|
||||
only to the context of RoomList views. N.B. overrides should be relatively
|
||||
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
|
||||
only to the context of RoomList views. N.B. overrides should be relatively
|
||||
rare as in general CSS inheritance should be enough.
|
||||
|
||||
* Components should render only within the bounding box of their outermost DOM
|
||||
- Components should render only within the bounding box of their outermost DOM
|
||||
element. Page-absolute positioning and negative CSS margins and similar are
|
||||
generally not cool and stop the component from being reused easily in
|
||||
different places.
|
||||
|
||||
Originally `matrix-react-sdk` followed the Atomic design pattern as per
|
||||
http://patternlab.io to try to encourage a modular architecture. However, we
|
||||
http://patternlab.io to try to encourage a modular architecture. However, we
|
||||
found that the grouping of components into atoms/molecules/organisms
|
||||
made them harder to find relative to a functional split, and didn't emphasise
|
||||
the distinction between 'structural' and 'view' components, so we backed away
|
||||
from it.
|
||||
|
||||
Github Issues
|
||||
-------------
|
||||
## Github Issues
|
||||
|
||||
All issues should be filed under https://github.com/vector-im/element-web/issues
|
||||
for now.
|
||||
|
||||
Development
|
||||
-----------
|
||||
## Development
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"en": {
|
||||
"fileName": "en_EN.json",
|
||||
"label": "English"
|
||||
},
|
||||
"en-us": {
|
||||
"fileName": "en_US.json",
|
||||
"label": "English (US)"
|
||||
}
|
||||
"en": {
|
||||
"fileName": "en_EN.json",
|
||||
"label": "English"
|
||||
},
|
||||
"en-us": {
|
||||
"fileName": "en_US.json",
|
||||
"label": "English (US)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const EventEmitter = require("events");
|
||||
const { LngLat, NavigationControl, LngLatBounds, AttributionControl } = require('maplibre-gl');
|
||||
const { LngLat, NavigationControl, LngLatBounds, AttributionControl } = require("maplibre-gl");
|
||||
|
||||
class MockMap extends EventEmitter {
|
||||
addControl = jest.fn();
|
||||
|
@ -32,7 +32,7 @@ class MockGeolocateControl extends EventEmitter {
|
|||
trigger = jest.fn();
|
||||
}
|
||||
const MockGeolocateInstance = new MockGeolocateControl();
|
||||
const MockMarker = {}
|
||||
const MockMarker = {};
|
||||
MockMarker.setLngLat = jest.fn().mockReturnValue(MockMarker);
|
||||
MockMarker.addTo = jest.fn().mockReturnValue(MockMarker);
|
||||
MockMarker.remove = jest.fn().mockReturnValue(MockMarker);
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export const Icon = 'div';
|
||||
export const Icon = "div";
|
||||
export default "image-file-stub";
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
module.exports = {
|
||||
"sourceMaps": "inline",
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Safari versions",
|
||||
"last 2 Edge versions",
|
||||
],
|
||||
}],
|
||||
sourceMaps: "inline",
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Safari versions",
|
||||
"last 2 Edge versions",
|
||||
],
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react",
|
||||
],
|
||||
"plugins": [
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-export-default-from",
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
|
|
|
@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'cypress';
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
videoUploadOnPasses: false,
|
||||
projectId: 'ppvnzg',
|
||||
projectId: "ppvnzg",
|
||||
experimentalInteractiveRunEvents: true,
|
||||
defaultCommandTimeout: 10000,
|
||||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.ts').default(on, config);
|
||||
return require("./cypress/plugins/index.ts").default(on, config);
|
||||
},
|
||||
baseUrl: 'http://localhost:8080',
|
||||
baseUrl: "http://localhost:8080",
|
||||
experimentalSessionAndOrigin: true,
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
|
||||
},
|
||||
env: {
|
||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
||||
|
|
|
@ -23,7 +23,7 @@ describe("Composer", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
@ -42,26 +42,26 @@ describe("Composer", () => {
|
|||
|
||||
it("sends a message when you click send or press Enter", () => {
|
||||
// Type a message
|
||||
cy.get('div[contenteditable=true]').type('my message 0');
|
||||
cy.get("div[contenteditable=true]").type("my message 0");
|
||||
// It has not been sent yet
|
||||
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
|
||||
cy.contains(".mx_EventTile_body", "my message 0").should("not.exist");
|
||||
|
||||
// Click send
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
// It has been sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 0');
|
||||
cy.contains(".mx_EventTile_body", "my message 0");
|
||||
|
||||
// Type another and press Enter afterwards
|
||||
cy.get('div[contenteditable=true]').type('my message 1{enter}');
|
||||
cy.get("div[contenteditable=true]").type("my message 1{enter}");
|
||||
// It was sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 1');
|
||||
cy.contains(".mx_EventTile_body", "my message 1");
|
||||
});
|
||||
|
||||
it("can write formatted text", () => {
|
||||
cy.get('div[contenteditable=true]').type('my bold{ctrl+b} message');
|
||||
cy.get("div[contenteditable=true]").type("my bold{ctrl+b} message");
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
// Note: both "bold" and "message" are bold, which is probably surprising
|
||||
cy.contains('.mx_EventTile_body strong', 'bold message');
|
||||
cy.contains(".mx_EventTile_body strong", "bold message");
|
||||
});
|
||||
|
||||
it("should allow user to input emoji via graphical picker", () => {
|
||||
|
@ -74,7 +74,7 @@ describe("Composer", () => {
|
|||
});
|
||||
|
||||
cy.get(".mx_ContextualMenu_background").click(); // Close emoji picker
|
||||
cy.get('div[contenteditable=true]').type("{enter}"); // Send message
|
||||
cy.get("div[contenteditable=true]").type("{enter}"); // Send message
|
||||
|
||||
cy.contains(".mx_EventTile_body", "😇");
|
||||
});
|
||||
|
@ -86,14 +86,14 @@ describe("Composer", () => {
|
|||
|
||||
it("only sends when you press Ctrl+Enter", () => {
|
||||
// Type a message and press Enter
|
||||
cy.get('div[contenteditable=true]').type('my message 3{enter}');
|
||||
cy.get("div[contenteditable=true]").type("my message 3{enter}");
|
||||
// It has not been sent yet
|
||||
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
|
||||
cy.contains(".mx_EventTile_body", "my message 3").should("not.exist");
|
||||
|
||||
// Press Ctrl+Enter
|
||||
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
|
||||
cy.get("div[contenteditable=true]").type("{ctrl+enter}");
|
||||
// It was sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 3');
|
||||
cy.contains(".mx_EventTile_body", "my message 3");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -109,28 +109,28 @@ describe("Composer", () => {
|
|||
|
||||
it("sends a message when you click send or press Enter", () => {
|
||||
// Type a message
|
||||
cy.get('div[contenteditable=true]').type('my message 0');
|
||||
cy.get("div[contenteditable=true]").type("my message 0");
|
||||
// It has not been sent yet
|
||||
cy.contains('.mx_EventTile_body', 'my message 0').should('not.exist');
|
||||
cy.contains(".mx_EventTile_body", "my message 0").should("not.exist");
|
||||
|
||||
// Click send
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
// It has been sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 0');
|
||||
cy.contains(".mx_EventTile_body", "my message 0");
|
||||
|
||||
// Type another
|
||||
cy.get('div[contenteditable=true]').type('my message 1');
|
||||
cy.get("div[contenteditable=true]").type("my message 1");
|
||||
// Press enter. Would be nice to just use {enter} but we can't because Cypress
|
||||
// does not trigger an insertParagraph when you do that.
|
||||
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
|
||||
cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" });
|
||||
// It was sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 1');
|
||||
cy.contains(".mx_EventTile_body", "my message 1");
|
||||
});
|
||||
|
||||
it("can write formatted text", () => {
|
||||
cy.get('div[contenteditable=true]').type('my {ctrl+b}bold{ctrl+b} message');
|
||||
cy.get("div[contenteditable=true]").type("my {ctrl+b}bold{ctrl+b} message");
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
cy.contains('.mx_EventTile_body strong', 'bold');
|
||||
cy.contains(".mx_EventTile_body strong", "bold");
|
||||
});
|
||||
|
||||
describe("when Ctrl+Enter is required to send", () => {
|
||||
|
@ -140,15 +140,15 @@ describe("Composer", () => {
|
|||
|
||||
it("only sends when you press Ctrl+Enter", () => {
|
||||
// Type a message and press Enter
|
||||
cy.get('div[contenteditable=true]').type('my message 3');
|
||||
cy.get('div[contenteditable=true]').trigger('input', { inputType: "insertParagraph" });
|
||||
cy.get("div[contenteditable=true]").type("my message 3");
|
||||
cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" });
|
||||
// It has not been sent yet
|
||||
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
|
||||
cy.contains(".mx_EventTile_body", "my message 3").should("not.exist");
|
||||
|
||||
// Press Ctrl+Enter
|
||||
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
|
||||
cy.get("div[contenteditable=true]").type("{ctrl+enter}");
|
||||
// It was sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 3');
|
||||
cy.contains(".mx_EventTile_body", "my message 3");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,7 @@ describe("Create Room", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Jim");
|
||||
|
|
|
@ -28,7 +28,7 @@ interface CryptoTestContext extends Mocha.Context {
|
|||
}
|
||||
|
||||
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
|
||||
return new Promise<VerificationRequest>(resolve => {
|
||||
return new Promise<VerificationRequest>((resolve) => {
|
||||
const onVerificationRequestEvent = (request: VerificationRequest) => {
|
||||
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
|
||||
cli.off("crypto.verification.request", onVerificationRequestEvent);
|
||||
|
@ -49,7 +49,7 @@ const checkDMRoom = () => {
|
|||
cy.contains(".mx_RoomView_body .mx_cryptoEvent", "Encryption enabled").should("exist");
|
||||
};
|
||||
|
||||
const startDMWithBob = function(this: CryptoTestContext) {
|
||||
const startDMWithBob = function (this: CryptoTestContext) {
|
||||
cy.get('.mx_RoomList [aria-label="Start chat"]').click();
|
||||
cy.get('[data-testid="invite-dialog-input"]').type(this.bob.getUserId());
|
||||
cy.contains(".mx_InviteDialog_tile_nameStack_name", "Bob").click();
|
||||
|
@ -57,11 +57,13 @@ const startDMWithBob = function(this: CryptoTestContext) {
|
|||
cy.get(".mx_InviteDialog_goButton").click();
|
||||
};
|
||||
|
||||
const testMessages = function(this: CryptoTestContext) {
|
||||
const testMessages = function (this: CryptoTestContext) {
|
||||
// check the invite message
|
||||
cy.contains(".mx_EventTile_body", "Hey!").closest(".mx_EventTile").within(() => {
|
||||
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
|
||||
});
|
||||
cy.contains(".mx_EventTile_body", "Hey!")
|
||||
.closest(".mx_EventTile")
|
||||
.within(() => {
|
||||
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
|
||||
});
|
||||
|
||||
// Bob sends a response
|
||||
cy.get<Room>("@bobsRoom").then((room) => {
|
||||
|
@ -72,28 +74,30 @@ const testMessages = function(this: CryptoTestContext) {
|
|||
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
|
||||
};
|
||||
|
||||
const bobJoin = function(this: CryptoTestContext) {
|
||||
cy.window({ log: false }).then(async win => {
|
||||
const bobRooms = this.bob.getRooms();
|
||||
if (!bobRooms.length) {
|
||||
await new Promise<void>(resolve => {
|
||||
const onMembership = (_event) => {
|
||||
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
resolve();
|
||||
};
|
||||
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
|
||||
});
|
||||
const bobJoin = function (this: CryptoTestContext) {
|
||||
cy.window({ log: false })
|
||||
.then(async (win) => {
|
||||
const bobRooms = this.bob.getRooms();
|
||||
if (!bobRooms.length) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const onMembership = (_event) => {
|
||||
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
resolve();
|
||||
};
|
||||
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
|
||||
});
|
||||
|
||||
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||
};
|
||||
|
||||
/** configure the given MatrixClient to auto-accept any invites */
|
||||
function autoJoin(client: MatrixClient) {
|
||||
cy.window({ log: false }).then(async win => {
|
||||
cy.window({ log: false }).then(async (win) => {
|
||||
client.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||
if (member.membership === "invite" && member.userId === client.getUserId()) {
|
||||
client.joinRoom(member.roomId);
|
||||
|
@ -103,21 +107,23 @@ function autoJoin(client: MatrixClient) {
|
|||
}
|
||||
|
||||
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
||||
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
verifier.off("show_sas", onShowSas);
|
||||
event.confirm();
|
||||
verifier.done();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
return cy.wrap(
|
||||
new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
verifier.off("show_sas", onShowSas);
|
||||
event.confirm();
|
||||
verifier.done();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
const verifier = request.beginKeyVerification("m.sas.v1");
|
||||
verifier.on("show_sas", onShowSas);
|
||||
verifier.verify();
|
||||
}));
|
||||
const verifier = request.beginKeyVerification("m.sas.v1");
|
||||
verifier.on("show_sas", onShowSas);
|
||||
verifier.verify();
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const verify = function(this: CryptoTestContext) {
|
||||
const verify = function (this: CryptoTestContext) {
|
||||
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
|
||||
|
||||
openRoomInfo().within(() => {
|
||||
|
@ -125,14 +131,16 @@ const verify = function(this: CryptoTestContext) {
|
|||
cy.contains(".mx_EntityTile_name", "Bob").click();
|
||||
cy.contains(".mx_UserInfo_verifyButton", "Verify").click();
|
||||
cy.contains(".mx_AccessibleButton", "Start Verification").click();
|
||||
cy.wrap(bobsVerificationRequestPromise).then((verificationRequest: VerificationRequest) => {
|
||||
verificationRequest.accept();
|
||||
return verificationRequest;
|
||||
}).as("bobsVerificationRequest");
|
||||
cy.wrap(bobsVerificationRequestPromise)
|
||||
.then((verificationRequest: VerificationRequest) => {
|
||||
verificationRequest.accept();
|
||||
return verificationRequest;
|
||||
})
|
||||
.as("bobsVerificationRequest");
|
||||
cy.contains(".mx_AccessibleButton", "Verify by emoji").click();
|
||||
cy.get<VerificationRequest>("@bobsVerificationRequest").then((request: VerificationRequest) => {
|
||||
return handleVerificationRequest(request).then((emojis: EmojiMapping[]) => {
|
||||
cy.get('.mx_VerificationShowSas_emojiSas_block').then((emojiBlocks) => {
|
||||
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
|
||||
emojis.forEach((emoji: EmojiMapping, index: number) => {
|
||||
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
|
||||
});
|
||||
|
@ -145,15 +153,17 @@ const verify = function(this: CryptoTestContext) {
|
|||
});
|
||||
};
|
||||
|
||||
describe("Cryptography", function() {
|
||||
beforeEach(function() {
|
||||
cy.startSynapse("default").as("synapse").then((synapse: SynapseInstance) => {
|
||||
cy.initTestUser(synapse, "Alice");
|
||||
cy.getBot(synapse, { displayName: "Bob", autoAcceptInvites: false }).as("bob");
|
||||
});
|
||||
describe("Cryptography", function () {
|
||||
beforeEach(function () {
|
||||
cy.startSynapse("default")
|
||||
.as("synapse")
|
||||
.then((synapse: SynapseInstance) => {
|
||||
cy.initTestUser(synapse, "Alice");
|
||||
cy.getBot(synapse, { displayName: "Bob", autoAcceptInvites: false }).as("bob");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function(this: CryptoTestContext) {
|
||||
afterEach(function (this: CryptoTestContext) {
|
||||
cy.stopSynapse(this.synapse);
|
||||
});
|
||||
|
||||
|
@ -172,27 +182,24 @@ describe("Cryptography", function() {
|
|||
return;
|
||||
});
|
||||
|
||||
it("creating a DM should work, being e2e-encrypted / user verification", function(this: CryptoTestContext) {
|
||||
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
startDMWithBob.call(this);
|
||||
// send first message
|
||||
cy.get(".mx_BasicMessageComposer_input")
|
||||
.click()
|
||||
.should("have.focus")
|
||||
.type("Hey!{enter}");
|
||||
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
|
||||
checkDMRoom();
|
||||
bobJoin.call(this);
|
||||
testMessages.call(this);
|
||||
verify.call(this);
|
||||
});
|
||||
|
||||
it("should allow verification when there is no existing DM", function(this: CryptoTestContext) {
|
||||
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
autoJoin(this.bob);
|
||||
|
||||
/* we need to have a room with the other user present, so we can open the verification panel */
|
||||
let roomId: string;
|
||||
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then(_room1Id => {
|
||||
cy.createRoom({ name: "TestRoom", invite: [this.bob.getUserId()] }).then((_room1Id) => {
|
||||
roomId = _room1Id;
|
||||
cy.log(`Created test room ${roomId}`);
|
||||
cy.visit(`/#/room/${roomId}`);
|
||||
|
|
|
@ -24,19 +24,14 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
|
|||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
|
||||
return cy.sendEvent(
|
||||
roomId,
|
||||
null,
|
||||
"m.room.message" as EventType,
|
||||
MessageEvent.from("Message").serialize().content,
|
||||
);
|
||||
return cy.sendEvent(roomId, null, "m.room.message" as EventType, MessageEvent.from("Message").serialize().content);
|
||||
};
|
||||
|
||||
describe("Editing", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Edith").then(() => {
|
||||
cy.injectAxe();
|
||||
|
@ -50,7 +45,7 @@ describe("Editing", () => {
|
|||
});
|
||||
|
||||
it("should close the composer when clicking save after making a change and undoing it", () => {
|
||||
cy.get<string>("@roomId").then(roomId => {
|
||||
cy.get<string>("@roomId").then((roomId) => {
|
||||
sendEvent(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
|
|
@ -77,18 +77,18 @@ describe("Integration Manager: Get OpenID Token", () => {
|
|||
let integrationManagerUrl: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
|
||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||
integrationManagerUrl = url;
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||
});
|
||||
}).then(user => {
|
||||
}).then((user) => {
|
||||
testUser = user;
|
||||
});
|
||||
|
||||
|
@ -107,8 +107,8 @@ describe("Integration Manager: Get OpenID Token", () => {
|
|||
}).as("integrationManager");
|
||||
|
||||
// Succeed when checking the token is valid
|
||||
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
|
||||
req.continue(res => {
|
||||
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
|
||||
req.continue((res) => {
|
||||
return res.send(200, {
|
||||
user_id: testUser.userId,
|
||||
});
|
||||
|
@ -127,16 +127,14 @@ describe("Integration Manager: Get OpenID Token", () => {
|
|||
});
|
||||
|
||||
it("should successfully obtain an openID token", () => {
|
||||
cy.all([
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(() => {
|
||||
cy.all([cy.get<{}>("@integrationManager")]).then(() => {
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl);
|
||||
|
||||
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||
cy.get("#message-response").should('include.text', 'access_token');
|
||||
cy.get("#message-response").should("include.text", "access_token");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -87,8 +87,9 @@ function expectKickedMessage(shouldExist: boolean) {
|
|||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true });
|
||||
|
||||
// Check for the event message (or lack thereof)
|
||||
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)
|
||||
.should(shouldExist ? "exist" : "not.exist");
|
||||
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should(
|
||||
shouldExist ? "exist" : "not.exist",
|
||||
);
|
||||
}
|
||||
|
||||
describe("Integration Manager: Kick", () => {
|
||||
|
@ -97,18 +98,18 @@ describe("Integration Manager: Kick", () => {
|
|||
let integrationManagerUrl: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
|
||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||
integrationManagerUrl = url;
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||
});
|
||||
}).then(user => {
|
||||
}).then((user) => {
|
||||
testUser = user;
|
||||
});
|
||||
|
||||
|
@ -127,8 +128,8 @@ describe("Integration Manager: Kick", () => {
|
|||
}).as("integrationManager");
|
||||
|
||||
// Succeed when checking the token is valid
|
||||
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
|
||||
req.continue(res => {
|
||||
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
|
||||
req.continue((res) => {
|
||||
return res.send(200, {
|
||||
user_id: testUser.userId,
|
||||
});
|
||||
|
@ -149,103 +150,100 @@ describe("Integration Manager: Kick", () => {
|
|||
});
|
||||
|
||||
it("should kick the target", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
|
||||
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
|
||||
([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
|
||||
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(true);
|
||||
});
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should not kick the target if lacking permissions", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
|
||||
cy.getClient().then(async client => {
|
||||
await client.sendStateEvent(roomId, 'm.room.power_levels', {
|
||||
kick: 50,
|
||||
users: {
|
||||
[testUser.userId]: 0,
|
||||
},
|
||||
});
|
||||
}).then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
|
||||
([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
|
||||
cy.getClient()
|
||||
.then(async (client) => {
|
||||
await client.sendStateEvent(roomId, "m.room.power_levels", {
|
||||
kick: 50,
|
||||
users: {
|
||||
[testUser.userId]: 0,
|
||||
},
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should no-op if the target already left", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist').then(async () => {
|
||||
await targetUser.leave(roomId);
|
||||
}).then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
|
||||
([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`)
|
||||
.should("exist")
|
||||
.then(async () => {
|
||||
await targetUser.leave(roomId);
|
||||
})
|
||||
.then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should no-op if the target was banned", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
|
||||
cy.getClient().then(async client => {
|
||||
await client.ban(roomId, targetUserId);
|
||||
}).then(() => {
|
||||
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
|
||||
([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
|
||||
cy.getClient()
|
||||
.then(async (client) => {
|
||||
await client.ban(roomId, targetUserId);
|
||||
})
|
||||
.then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should no-op if the target was never a room member", () => {
|
||||
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
|
||||
([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if the target was never a room member", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,11 +31,11 @@ describe("Lazy Loading", () => {
|
|||
const charlies: Charly[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Alice");
|
||||
|
@ -44,7 +44,7 @@ describe("Lazy Loading", () => {
|
|||
displayName: "Bob",
|
||||
startClient: false,
|
||||
autoAcceptInvites: false,
|
||||
}).then(_bob => {
|
||||
}).then((_bob) => {
|
||||
bob = _bob;
|
||||
});
|
||||
|
||||
|
@ -54,7 +54,7 @@ describe("Lazy Loading", () => {
|
|||
displayName,
|
||||
startClient: false,
|
||||
autoAcceptInvites: false,
|
||||
}).then(client => {
|
||||
}).then((client) => {
|
||||
charlies[i - 1] = { displayName, client };
|
||||
});
|
||||
}
|
||||
|
@ -71,15 +71,22 @@ describe("Lazy Loading", () => {
|
|||
const charlyMsg2 = "how's it going??";
|
||||
|
||||
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
|
||||
cy.window({ log: false }).then(win => {
|
||||
return cy.wrap(bob.createRoom({
|
||||
name,
|
||||
room_alias_name: "lltest",
|
||||
visibility: win.matrixcs.Visibility.Public,
|
||||
}).then(r => r.room_id), { log: false }).as("roomId");
|
||||
cy.window({ log: false }).then((win) => {
|
||||
return cy
|
||||
.wrap(
|
||||
bob
|
||||
.createRoom({
|
||||
name,
|
||||
room_alias_name: "lltest",
|
||||
visibility: win.matrixcs.Visibility.Public,
|
||||
})
|
||||
.then((r) => r.room_id),
|
||||
{ log: false },
|
||||
)
|
||||
.as("roomId");
|
||||
});
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
cy.get<string>("@roomId").then(async (roomId) => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.joinRoom(alias);
|
||||
}
|
||||
|
@ -122,13 +129,13 @@ describe("Lazy Loading", () => {
|
|||
function checkMemberList(charlies: Charly[]) {
|
||||
getMemberInMemberlist("Alice").should("exist");
|
||||
getMemberInMemberlist("Bob").should("exist");
|
||||
charlies.forEach(charly => {
|
||||
charlies.forEach((charly) => {
|
||||
getMemberInMemberlist(charly.displayName).should("exist");
|
||||
});
|
||||
}
|
||||
|
||||
function checkMemberListLacksCharlies(charlies: Charly[]) {
|
||||
charlies.forEach(charly => {
|
||||
charlies.forEach((charly) => {
|
||||
getMemberInMemberlist(charly.displayName).should("not.exist");
|
||||
});
|
||||
}
|
||||
|
@ -136,7 +143,7 @@ describe("Lazy Loading", () => {
|
|||
function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
|
||||
cy.goOffline();
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
cy.get<string>("@roomId").then(async (roomId) => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.joinRoom(alias);
|
||||
}
|
||||
|
@ -163,7 +170,7 @@ describe("Lazy Loading", () => {
|
|||
joinCharliesWhileAliceIsOffline(charly6to10);
|
||||
checkMemberList(charly6to10);
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
cy.get<string>("@roomId").then(async (roomId) => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.leave(roomId);
|
||||
}
|
||||
|
|
|
@ -31,10 +31,10 @@ describe("Location sharing", () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
|
@ -47,31 +47,28 @@ describe("Location sharing", () => {
|
|||
|
||||
it("sends and displays pin drop location message successfully", () => {
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.visit('/#/room/' + roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Location"]').click();
|
||||
});
|
||||
|
||||
selectLocationShareTypeOption('Pin').click();
|
||||
selectLocationShareTypeOption("Pin").click();
|
||||
|
||||
cy.get('#mx_LocationPicker_map').click('center');
|
||||
cy.get("#mx_LocationPicker_map").click("center");
|
||||
|
||||
submitShareLocation();
|
||||
|
||||
cy.get(".mx_RoomView_body .mx_EventTile .mx_MLocationBody", { timeout: 10000 })
|
||||
.should('exist')
|
||||
.click();
|
||||
cy.get(".mx_RoomView_body .mx_EventTile .mx_MLocationBody", { timeout: 10000 }).should("exist").click();
|
||||
|
||||
// clicking location tile opens maximised map
|
||||
cy.get('.mx_LocationViewDialog_wrapper').should('exist');
|
||||
cy.get(".mx_LocationViewDialog_wrapper").should("exist");
|
||||
|
||||
cy.get('[aria-label="Close dialog"]').click();
|
||||
|
||||
cy.get('.mx_Marker')
|
||||
.should('exist');
|
||||
cy.get(".mx_Marker").should("exist");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("Consent", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("consent").then(data => {
|
||||
cy.startSynapse("consent").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Bob");
|
||||
|
@ -37,7 +37,7 @@ describe("Consent", () => {
|
|||
|
||||
it("should prompt the user to consent to terms when server deems it necessary", () => {
|
||||
// Attempt to create a room using the js-sdk which should return an error with `M_CONSENT_NOT_GIVEN`
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.mxMatrixClientPeg.matrixClient.createRoom({}).catch(() => {});
|
||||
|
||||
// Stub `window.open` - clicking the primary button below will call it
|
||||
|
@ -50,7 +50,7 @@ describe("Consent", () => {
|
|||
cy.get(".mx_Dialog_primary").click();
|
||||
});
|
||||
|
||||
cy.get<SinonStub>("@windowOpen").then(stub => {
|
||||
cy.get<SinonStub>("@windowOpen").then((stub) => {
|
||||
const url = stub.getCall(0).args[0];
|
||||
|
||||
// Go to Synapse's consent page and accept it
|
||||
|
|
|
@ -34,7 +34,7 @@ describe("Login", () => {
|
|||
const password = "p4s5W0rD";
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("consent").then(data => {
|
||||
cy.startSynapse("consent").then((data) => {
|
||||
synapse = data;
|
||||
cy.registerUser(synapse, username, password);
|
||||
cy.visit("/#/login");
|
||||
|
@ -52,19 +52,19 @@ describe("Login", () => {
|
|||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||
// wait for the dialog to go away
|
||||
cy.get('.mx_ServerPickerDialog').should('not.exist');
|
||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||
|
||||
cy.get("#mx_LoginForm_username").type(username);
|
||||
cy.get("#mx_LoginForm_password").type(password);
|
||||
cy.get(".mx_Login_submit").click();
|
||||
|
||||
cy.url().should('contain', '/#/home', { timeout: 30000 });
|
||||
cy.url().should("contain", "/#/home", { timeout: 30000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("logout", () => {
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("consent").then(data => {
|
||||
cy.startSynapse("consent").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Erin");
|
||||
});
|
||||
|
|
|
@ -33,17 +33,17 @@ describe("Polls", () => {
|
|||
};
|
||||
const createPoll = ({ title, options }: CreatePollOptions) => {
|
||||
if (options.length < 2) {
|
||||
throw new Error('Poll must have at least two options');
|
||||
throw new Error("Poll must have at least two options");
|
||||
}
|
||||
cy.get('.mx_PollCreateDialog').within((pollCreateDialog) => {
|
||||
cy.get('#poll-topic-input').type(title);
|
||||
cy.get(".mx_PollCreateDialog").within((pollCreateDialog) => {
|
||||
cy.get("#poll-topic-input").type(title);
|
||||
|
||||
options.forEach((option, index) => {
|
||||
const optionId = `#pollcreate_option_${index}`;
|
||||
|
||||
// click 'add option' button if needed
|
||||
if (pollCreateDialog.find(optionId).length === 0) {
|
||||
cy.get('.mx_PollCreateDialog_addOption').scrollIntoView().click();
|
||||
cy.get(".mx_PollCreateDialog_addOption").scrollIntoView().click();
|
||||
}
|
||||
cy.get(optionId).scrollIntoView().type(option);
|
||||
});
|
||||
|
@ -56,34 +56,32 @@ describe("Polls", () => {
|
|||
};
|
||||
|
||||
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
|
||||
return getPollTile(pollId).contains('.mx_MPollBody_option .mx_StyledRadioButton', optionText);
|
||||
return getPollTile(pollId).contains(".mx_MPollBody_option .mx_StyledRadioButton", optionText);
|
||||
};
|
||||
|
||||
const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => {
|
||||
getPollOption(pollId, optionText).within(() => {
|
||||
cy.get('.mx_MPollBody_optionVoteCount').should('contain', `${votes} vote`);
|
||||
cy.get(".mx_MPollBody_optionVoteCount").should("contain", `${votes} vote`);
|
||||
});
|
||||
};
|
||||
|
||||
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
|
||||
getPollOption(pollId, optionText).within(ref => {
|
||||
cy.get('input[type="radio"]').invoke('attr', 'value').then(optionId => {
|
||||
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
|
||||
bot.sendEvent(
|
||||
roomId,
|
||||
pollVote.type,
|
||||
pollVote.content,
|
||||
);
|
||||
});
|
||||
getPollOption(pollId, optionText).within((ref) => {
|
||||
cy.get('input[type="radio"]')
|
||||
.invoke("attr", "value")
|
||||
.then((optionId) => {
|
||||
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
|
||||
bot.sendEvent(roomId, pollVote.type, pollVote.content);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_thread");
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
|
@ -96,15 +94,15 @@ describe("Polls", () => {
|
|||
|
||||
it("should be creatable and votable", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
// wait until Bob joined
|
||||
cy.contains(".mx_TextualEvent", "BotBob joined the room").should("exist");
|
||||
});
|
||||
|
@ -113,34 +111,35 @@ describe("Polls", () => {
|
|||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
cy.get('.mx_CompoundDialog').percySnapshotElement('Polls Composer');
|
||||
cy.get(".mx_CompoundDialog").percySnapshotElement("Polls Composer");
|
||||
|
||||
const pollParams = {
|
||||
title: 'Does the polls feature work?',
|
||||
options: ['Yes', 'No', 'Maybe'],
|
||||
title: "Does the polls feature work?",
|
||||
options: ["Yes", "No", "Maybe"],
|
||||
};
|
||||
createPoll(pollParams);
|
||||
|
||||
// Wait for message to send, get its ID and save as @pollId
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||
.invoke("attr", "data-scroll-tokens")
|
||||
.as("pollId");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
getPollTile(pollId).percySnapshotElement('Polls Timeline tile - no votes', { percyCSS: hideTimestampCSS });
|
||||
cy.get<string>("@pollId").then((pollId) => {
|
||||
getPollTile(pollId).percySnapshotElement("Polls Timeline tile - no votes", { percyCSS: hideTimestampCSS });
|
||||
|
||||
// Bot votes 'Maybe' in the poll
|
||||
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
|
||||
|
||||
// no votes shown until I vote, check bots vote has arrived
|
||||
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
|
||||
cy.get(".mx_MPollBody_totalVotes").should("contain", "1 vote cast");
|
||||
|
||||
// vote 'Maybe'
|
||||
getPollOption(pollId, pollParams.options[2]).click('topLeft');
|
||||
getPollOption(pollId, pollParams.options[2]).click("topLeft");
|
||||
// both me and bot have voted Maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
|
||||
// change my vote to 'Yes'
|
||||
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||
getPollOption(pollId, pollParams.options[0]).click("topLeft");
|
||||
|
||||
// 1 vote for yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
|
@ -161,15 +160,15 @@ describe("Polls", () => {
|
|||
|
||||
it("should be editable from context menu if no votes have been cast", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
|
@ -177,40 +176,42 @@ describe("Polls", () => {
|
|||
});
|
||||
|
||||
const pollParams = {
|
||||
title: 'Does the polls feature work?',
|
||||
options: ['Yes', 'No', 'Maybe'],
|
||||
title: "Does the polls feature work?",
|
||||
options: ["Yes", "No", "Maybe"],
|
||||
};
|
||||
createPoll(pollParams);
|
||||
|
||||
// Wait for message to send, get its ID and save as @pollId
|
||||
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||
cy.get(".mx_RoomView_body .mx_EventTile")
|
||||
.contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens")
|
||||
.as("pollId");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
cy.get<string>("@pollId").then((pollId) => {
|
||||
// Open context menu
|
||||
getPollTile(pollId).rightclick();
|
||||
|
||||
// Select edit item
|
||||
cy.get('.mx_ContextualMenu').within(() => {
|
||||
cy.get(".mx_ContextualMenu").within(() => {
|
||||
cy.get('[aria-label="Edit"]').click();
|
||||
});
|
||||
|
||||
// Expect poll editing dialog
|
||||
cy.get('.mx_PollCreateDialog');
|
||||
cy.get(".mx_PollCreateDialog");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be editable from context menu if votes have been cast", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
|
@ -218,51 +219,53 @@ describe("Polls", () => {
|
|||
});
|
||||
|
||||
const pollParams = {
|
||||
title: 'Does the polls feature work?',
|
||||
options: ['Yes', 'No', 'Maybe'],
|
||||
title: "Does the polls feature work?",
|
||||
options: ["Yes", "No", "Maybe"],
|
||||
};
|
||||
createPoll(pollParams);
|
||||
|
||||
// Wait for message to send, get its ID and save as @pollId
|
||||
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||
cy.get(".mx_RoomView_body .mx_EventTile")
|
||||
.contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens")
|
||||
.as("pollId");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
cy.get<string>("@pollId").then((pollId) => {
|
||||
// Bot votes 'Maybe' in the poll
|
||||
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
|
||||
|
||||
// wait for bot's vote to arrive
|
||||
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
|
||||
cy.get(".mx_MPollBody_totalVotes").should("contain", "1 vote cast");
|
||||
|
||||
// Open context menu
|
||||
getPollTile(pollId).rightclick();
|
||||
|
||||
// Select edit item
|
||||
cy.get('.mx_ContextualMenu').within(() => {
|
||||
cy.get(".mx_ContextualMenu").within(() => {
|
||||
cy.get('[aria-label="Edit"]').click();
|
||||
});
|
||||
|
||||
// Expect error dialog
|
||||
cy.get('.mx_ErrorDialog');
|
||||
cy.get(".mx_ErrorDialog");
|
||||
});
|
||||
});
|
||||
|
||||
it("should be displayed correctly in thread panel", () => {
|
||||
let botBob: MatrixClient;
|
||||
let botCharlie: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
||||
botBob = _bot;
|
||||
});
|
||||
cy.getBot(synapse, { displayName: "BotCharlie" }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "BotCharlie" }).then((_bot) => {
|
||||
botCharlie = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, botBob.getUserId());
|
||||
cy.inviteUser(roomId, botCharlie.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
// wait until the bots joined
|
||||
cy.contains(".mx_TextualEvent", "and one other were invited and joined").should("exist");
|
||||
});
|
||||
|
@ -272,16 +275,17 @@ describe("Polls", () => {
|
|||
});
|
||||
|
||||
const pollParams = {
|
||||
title: 'Does the polls feature work?',
|
||||
options: ['Yes', 'No', 'Maybe'],
|
||||
title: "Does the polls feature work?",
|
||||
options: ["Yes", "No", "Maybe"],
|
||||
};
|
||||
createPoll(pollParams);
|
||||
|
||||
// Wait for message to send, get its ID and save as @pollId
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||
.invoke("attr", "data-scroll-tokens")
|
||||
.as("pollId");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
cy.get<string>("@pollId").then((pollId) => {
|
||||
// Bob starts thread on the poll
|
||||
botBob.sendMessage(roomId, pollId, {
|
||||
body: "Hello there",
|
||||
|
@ -297,22 +301,22 @@ describe("Polls", () => {
|
|||
botVoteForOption(botCharlie, roomId, pollId, pollParams.options[1]);
|
||||
|
||||
// no votes shown until I vote, check votes have arrived in main tl
|
||||
cy.get('.mx_RoomView_body .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||
cy.get(".mx_RoomView_body .mx_MPollBody_totalVotes").should("contain", "2 votes cast");
|
||||
// and thread view
|
||||
cy.get('.mx_ThreadView .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||
cy.get(".mx_ThreadView .mx_MPollBody_totalVotes").should("contain", "2 votes cast");
|
||||
|
||||
cy.get('.mx_RoomView_body').within(() => {
|
||||
cy.get(".mx_RoomView_body").within(() => {
|
||||
// vote 'Maybe' in the main timeline poll
|
||||
getPollOption(pollId, pollParams.options[2]).click('topLeft');
|
||||
getPollOption(pollId, pollParams.options[2]).click("topLeft");
|
||||
// both me and bob have voted Maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
});
|
||||
|
||||
cy.get('.mx_ThreadView').within(() => {
|
||||
cy.get(".mx_ThreadView").within(() => {
|
||||
// votes updated in thread view too
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
// change my vote to 'Yes'
|
||||
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||
getPollOption(pollId, pollParams.options[0]).click("topLeft");
|
||||
});
|
||||
|
||||
// Bob updates vote to 'No'
|
||||
|
@ -329,11 +333,11 @@ describe("Polls", () => {
|
|||
};
|
||||
|
||||
// check counts are correct in main timeline tile
|
||||
cy.get('.mx_RoomView_body').within(() => {
|
||||
cy.get(".mx_RoomView_body").within(() => {
|
||||
expectVoteCounts();
|
||||
});
|
||||
// and in thread view tile
|
||||
cy.get('.mx_ThreadView').within(() => {
|
||||
cy.get(".mx_ThreadView").within(() => {
|
||||
expectVoteCounts();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("Registration", () => {
|
|||
beforeEach(() => {
|
||||
cy.stubDefaultServer();
|
||||
cy.visit("/#/register");
|
||||
cy.startSynapse("consent").then(data => {
|
||||
cy.startSynapse("consent").then((data) => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
@ -45,7 +45,7 @@ describe("Registration", () => {
|
|||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||
// wait for the dialog to go away
|
||||
cy.get('.mx_ServerPickerDialog').should('not.exist');
|
||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||
|
||||
cy.get("#mx_RegistrationForm_username").should("be.visible");
|
||||
// Hide the server text as it contains the randomly allocated Synapse port
|
||||
|
@ -75,12 +75,14 @@ describe("Registration", () => {
|
|||
cy.checkA11y();
|
||||
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
|
||||
|
||||
cy.url().should('contain', '/#/home');
|
||||
cy.url().should("contain", "/#/home");
|
||||
|
||||
cy.get('[aria-label="User menu"]').click();
|
||||
cy.get('[aria-label="Security & Privacy"]').click();
|
||||
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon")
|
||||
.should("have.class", "mx_E2EIcon_verified");
|
||||
cy.get(".mx_DevicesPanel_myDevice .mx_DevicesPanel_deviceTrust .mx_E2EIcon").should(
|
||||
"have.class",
|
||||
"mx_E2EIcon_verified",
|
||||
);
|
||||
});
|
||||
|
||||
it("should require username to fulfil requirements and be available", () => {
|
||||
|
@ -89,7 +91,7 @@ describe("Registration", () => {
|
|||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||
// wait for the dialog to go away
|
||||
cy.get('.mx_ServerPickerDialog').should('not.exist');
|
||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||
|
||||
cy.get("#mx_RegistrationForm_username").should("be.visible");
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ describe("Pills", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
|
@ -33,7 +33,7 @@ describe("Pills", () => {
|
|||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it('should navigate clicks internally to the app', () => {
|
||||
it("should navigate clicks internally to the app", () => {
|
||||
const messageRoom = "Send Messages Here";
|
||||
const targetLocalpart = "aliasssssssssssss";
|
||||
cy.createRoom({
|
||||
|
@ -43,34 +43,35 @@ describe("Pills", () => {
|
|||
cy.createRoom({
|
||||
name: messageRoom,
|
||||
}).as("messageRoomId");
|
||||
cy.all([
|
||||
cy.get<string>("@targetRoomId"),
|
||||
cy.get<string>("@messageRoomId"),
|
||||
]).then(([targetRoomId, messageRoomId]) => { // discard the target room ID - we don't need it
|
||||
cy.viewRoomByName(messageRoom);
|
||||
cy.url().should("contain", `/#/room/${messageRoomId}`);
|
||||
cy.all([cy.get<string>("@targetRoomId"), cy.get<string>("@messageRoomId")]).then(
|
||||
([targetRoomId, messageRoomId]) => {
|
||||
// discard the target room ID - we don't need it
|
||||
cy.viewRoomByName(messageRoom);
|
||||
cy.url().should("contain", `/#/room/${messageRoomId}`);
|
||||
|
||||
// send a message using the built-in room mention functionality (autocomplete)
|
||||
cy.get(".mx_SendMessageComposer .mx_BasicMessageComposer_input")
|
||||
.type(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
|
||||
cy.get(".mx_Autocomplete_Completion_title").click();
|
||||
cy.get(".mx_MessageComposer_sendMessage").click();
|
||||
// send a message using the built-in room mention functionality (autocomplete)
|
||||
cy.get(".mx_SendMessageComposer .mx_BasicMessageComposer_input").type(
|
||||
`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`,
|
||||
);
|
||||
cy.get(".mx_Autocomplete_Completion_title").click();
|
||||
cy.get(".mx_MessageComposer_sendMessage").click();
|
||||
|
||||
// find the pill in the timeline and click it
|
||||
cy.get(".mx_EventTile_body .mx_Pill").click();
|
||||
// find the pill in the timeline and click it
|
||||
cy.get(".mx_EventTile_body .mx_Pill").click();
|
||||
|
||||
const localUrl = `/#/room/#${targetLocalpart}:`;
|
||||
// verify we landed at a sane place
|
||||
cy.url().should("contain", localUrl);
|
||||
const localUrl = `/#/room/#${targetLocalpart}:`;
|
||||
// verify we landed at a sane place
|
||||
cy.url().should("contain", localUrl);
|
||||
|
||||
cy.wait(250); // let the room list settle
|
||||
cy.wait(250); // let the room list settle
|
||||
|
||||
// go back to the message room and try to click on the pill text, as a user would
|
||||
cy.viewRoomByName(messageRoom);
|
||||
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
|
||||
.should("have.css", "pointer-events", "none")
|
||||
.click({ force: true }); // force is to ensure we bypass pointer-events
|
||||
cy.url().should("contain", localUrl);
|
||||
});
|
||||
// go back to the message room and try to click on the pill text, as a user would
|
||||
cy.viewRoomByName(messageRoom);
|
||||
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
|
||||
.should("have.css", "pointer-events", "none")
|
||||
.click({ force: true }); // force is to ensure we bypass pointer-events
|
||||
cy.url().should("contain", localUrl);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,7 +46,7 @@ describe("RightPanel", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, NAME).then(() =>
|
||||
cy.window({ log: false }).then(() => {
|
||||
|
|
|
@ -23,7 +23,7 @@ describe("Room Directory", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Ray");
|
||||
|
@ -36,7 +36,7 @@ describe("Room Directory", () => {
|
|||
});
|
||||
|
||||
it("should allow admin to add alias & publish room to directory", () => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
cy.createRoom({
|
||||
name: "Gaming",
|
||||
preset: win.matrixcs.Preset.PublicChat,
|
||||
|
@ -56,16 +56,14 @@ describe("Room Directory", () => {
|
|||
// Publish into the public rooms directory
|
||||
cy.contains(".mx_SettingsFieldset", "Published Addresses").within(() => {
|
||||
cy.get("#canonicalAlias").find(":selected").should("contain", "#gaming:localhost");
|
||||
cy.get(`[aria-label="Publish this room to the public in localhost's room directory?"]`).click()
|
||||
cy.get(`[aria-label="Publish this room to the public in localhost's room directory?"]`)
|
||||
.click()
|
||||
.should("have.attr", "aria-checked", "true");
|
||||
});
|
||||
|
||||
cy.closeDialog();
|
||||
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bot"),
|
||||
cy.get<string>("@roomId"),
|
||||
]).then(async ([bot, roomId]) => {
|
||||
cy.all([cy.get<MatrixClient>("@bot"), cy.get<string>("@roomId")]).then(async ([bot, roomId]) => {
|
||||
const resp = await bot.publicRooms({});
|
||||
expect(resp.total_room_count_estimate).to.equal(1);
|
||||
expect(resp.chunk).to.have.length(1);
|
||||
|
@ -75,10 +73,7 @@ describe("Room Directory", () => {
|
|||
|
||||
it("should allow finding published rooms in directory", () => {
|
||||
const name = "This is a public room";
|
||||
cy.all([
|
||||
cy.window({ log: false }),
|
||||
cy.get<MatrixClient>("@bot"),
|
||||
]).then(([win, bot]) => {
|
||||
cy.all([cy.window({ log: false }), cy.get<MatrixClient>("@bot")]).then(([win, bot]) => {
|
||||
bot.createRoom({
|
||||
visibility: win.matrixcs.Visibility.Public,
|
||||
name,
|
||||
|
@ -89,16 +84,17 @@ describe("Room Directory", () => {
|
|||
cy.get('[role="button"][aria-label="Explore rooms"]').click();
|
||||
|
||||
cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("Unknown Room");
|
||||
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_otherSearches_messageSearchText")
|
||||
.should("contain", "can't find the room you're looking for");
|
||||
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_otherSearches_messageSearchText").should(
|
||||
"contain",
|
||||
"can't find the room you're looking for",
|
||||
);
|
||||
cy.get(".mx_SpotlightDialog_wrapper").percySnapshotElement("Room Directory - filtered no results");
|
||||
|
||||
cy.get('.mx_SpotlightDialog [aria-label="Search"]').type("{selectAll}{backspace}test1234");
|
||||
cy.contains(".mx_SpotlightDialog .mx_SpotlightDialog_result_publicRoomName", name)
|
||||
.should("exist");
|
||||
cy.contains(".mx_SpotlightDialog .mx_SpotlightDialog_result_publicRoomName", name).should("exist");
|
||||
cy.get(".mx_SpotlightDialog_wrapper").percySnapshotElement("Room Directory - filtered one result");
|
||||
cy.get(".mx_SpotlightDialog .mx_SpotlightDialog_option").find(".mx_AccessibleButton").contains("Join").click();
|
||||
|
||||
cy.url().should('contain', `/#/room/#test1234:localhost`);
|
||||
cy.url().should("contain", `/#/room/#test1234:localhost`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,17 +25,20 @@ describe("Device manager", () => {
|
|||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_new_device_manager");
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Alice").then(credentials => {
|
||||
user = credentials;
|
||||
}).then(() => {
|
||||
// create some extra sessions to manage
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
}).then(() => {
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
});
|
||||
cy.initTestUser(synapse, "Alice")
|
||||
.then((credentials) => {
|
||||
user = credentials;
|
||||
})
|
||||
.then(() => {
|
||||
// create some extra sessions to manage
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
})
|
||||
.then(() => {
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -45,72 +48,74 @@ describe("Device manager", () => {
|
|||
|
||||
it("should display sessions", () => {
|
||||
cy.openUserSettings("Sessions");
|
||||
cy.contains('Current session').should('exist');
|
||||
cy.contains("Current session").should("exist");
|
||||
|
||||
cy.get('[data-testid="current-session-section"]').within(() => {
|
||||
cy.contains('Unverified session').should('exist');
|
||||
cy.contains("Unverified session").should("exist");
|
||||
});
|
||||
|
||||
// current session details opened
|
||||
cy.get('[data-testid="current-session-toggle-details"]').click();
|
||||
cy.contains('Session details').should('exist');
|
||||
cy.contains("Session details").should("exist");
|
||||
|
||||
// close current session details
|
||||
cy.get('[data-testid="current-session-toggle-details"]').click();
|
||||
cy.contains('Session details').should('not.exist');
|
||||
cy.contains("Session details").should("not.exist");
|
||||
|
||||
cy.get('[data-testid="security-recommendations-section"]').within(() => {
|
||||
cy.contains('Security recommendations').should('exist');
|
||||
cy.get('[data-testid="unverified-devices-cta"]').should('have.text', 'View all (3)').click();
|
||||
cy.contains("Security recommendations").should("exist");
|
||||
cy.get('[data-testid="unverified-devices-cta"]').should("have.text", "View all (3)").click();
|
||||
});
|
||||
|
||||
/**
|
||||
* Other sessions section
|
||||
*/
|
||||
cy.contains('Other sessions').should('exist');
|
||||
cy.contains("Other sessions").should("exist");
|
||||
// filter applied after clicking through from security recommendations
|
||||
cy.get('[aria-label="Filter devices"]').should('have.text', 'Show: Unverified');
|
||||
cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 3);
|
||||
cy.get('[aria-label="Filter devices"]').should("have.text", "Show: Unverified");
|
||||
cy.get(".mx_FilteredDeviceList_list").find(".mx_FilteredDeviceList_listItem").should("have.length", 3);
|
||||
|
||||
// select two sessions
|
||||
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').first().click();
|
||||
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox').last().click();
|
||||
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox").first().click();
|
||||
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem .mx_Checkbox").last().click();
|
||||
// sign out from list selection action buttons
|
||||
cy.get('[data-testid="sign-out-selection-cta"]').click();
|
||||
cy.get('[data-testid="dialog-primary-button"]').click();
|
||||
// list updated after sign out
|
||||
cy.get('.mx_FilteredDeviceList_list').find('.mx_FilteredDeviceList_listItem').should('have.length', 1);
|
||||
cy.get(".mx_FilteredDeviceList_list").find(".mx_FilteredDeviceList_listItem").should("have.length", 1);
|
||||
// security recommendation count updated
|
||||
cy.get('[data-testid="unverified-devices-cta"]').should('have.text', 'View all (1)');
|
||||
cy.get('[data-testid="unverified-devices-cta"]').should("have.text", "View all (1)");
|
||||
|
||||
const sessionName = `Alice's device`;
|
||||
// open the first session
|
||||
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem').first().within(() => {
|
||||
cy.get('[aria-label="Show details"]').click();
|
||||
cy.get(".mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem")
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get('[aria-label="Show details"]').click();
|
||||
|
||||
cy.contains('Session details').should('exist');
|
||||
cy.contains("Session details").should("exist");
|
||||
|
||||
cy.get('[data-testid="device-heading-rename-cta"]').click();
|
||||
cy.get('[data-testid="device-rename-input"]').type(sessionName);
|
||||
cy.get('[data-testid="device-rename-submit-cta"]').click();
|
||||
// there should be a spinner while device updates
|
||||
cy.get(".mx_Spinner").should("exist");
|
||||
// wait for spinner to complete
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
cy.get('[data-testid="device-heading-rename-cta"]').click();
|
||||
cy.get('[data-testid="device-rename-input"]').type(sessionName);
|
||||
cy.get('[data-testid="device-rename-submit-cta"]').click();
|
||||
// there should be a spinner while device updates
|
||||
cy.get(".mx_Spinner").should("exist");
|
||||
// wait for spinner to complete
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
|
||||
// session name updated in details
|
||||
cy.get('.mx_DeviceDetailHeading h3').should('have.text', sessionName);
|
||||
// and main list item
|
||||
cy.get('.mx_DeviceTile h4').should('have.text', sessionName);
|
||||
// session name updated in details
|
||||
cy.get(".mx_DeviceDetailHeading h3").should("have.text", sessionName);
|
||||
// and main list item
|
||||
cy.get(".mx_DeviceTile h4").should("have.text", sessionName);
|
||||
|
||||
// sign out using the device details sign out
|
||||
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
|
||||
});
|
||||
// sign out using the device details sign out
|
||||
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
|
||||
});
|
||||
// confirm the signout
|
||||
cy.get('[data-testid="dialog-primary-button"]').click();
|
||||
|
||||
// no other sessions or security recommendations sections when only one session
|
||||
cy.contains('Other sessions').should('not.exist');
|
||||
cy.get('[data-testid="security-recommendations-section"]').should('not.exist');
|
||||
cy.contains("Other sessions").should("not.exist");
|
||||
cy.get('[data-testid="security-recommendations-section"]').should("not.exist");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { SynapseInstance } from "../../plugins/synapsedocker";
|
|||
function seedLabs(synapse: SynapseInstance, labsVal: boolean | null): void {
|
||||
cy.initTestUser(synapse, "Sally", () => {
|
||||
// seed labs flag
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
if (typeof labsVal === "boolean") {
|
||||
// stringify boolean
|
||||
win.localStorage.setItem("mx_labs_feature_feature_hidden_read_receipts", `${labsVal}`);
|
||||
|
@ -64,7 +64,7 @@ describe("Hidden Read Receipts Setting Migration", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
@ -73,17 +73,17 @@ describe("Hidden Read Receipts Setting Migration", () => {
|
|||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it('should not migrate the lack of a labs flag', () => {
|
||||
it("should not migrate the lack of a labs flag", () => {
|
||||
seedLabs(synapse, null);
|
||||
testForVal(null);
|
||||
});
|
||||
|
||||
it('should migrate labsHiddenRR=false as sendRR=true', () => {
|
||||
it("should migrate labsHiddenRR=false as sendRR=true", () => {
|
||||
seedLabs(synapse, false);
|
||||
testForVal(true);
|
||||
});
|
||||
|
||||
it('should migrate labsHiddenRR=true as sendRR=false', () => {
|
||||
it("should migrate labsHiddenRR=true as sendRR=false", () => {
|
||||
seedLabs(synapse, true);
|
||||
testForVal(false);
|
||||
});
|
||||
|
|
|
@ -26,18 +26,17 @@ import { ProxyInstance } from "../../plugins/sliding-sync";
|
|||
|
||||
describe("Sliding Sync", () => {
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").as("synapse").then(synapse => {
|
||||
cy.startProxy(synapse).as("proxy");
|
||||
});
|
||||
cy.startSynapse("default")
|
||||
.as("synapse")
|
||||
.then((synapse) => {
|
||||
cy.startProxy(synapse).as("proxy");
|
||||
});
|
||||
|
||||
cy.all([
|
||||
cy.get<SynapseInstance>("@synapse"),
|
||||
cy.get<ProxyInstance>("@proxy"),
|
||||
]).then(([synapse, proxy]) => {
|
||||
cy.all([cy.get<SynapseInstance>("@synapse"), cy.get<ProxyInstance>("@proxy")]).then(([synapse, proxy]) => {
|
||||
cy.enableLabsFeature("feature_sliding_sync");
|
||||
|
||||
cy.intercept("/config.json?cachebuster=*", req => {
|
||||
return req.continue(res => {
|
||||
cy.intercept("/config.json?cachebuster=*", (req) => {
|
||||
return req.continue((res) => {
|
||||
res.send(200, {
|
||||
...res.body,
|
||||
setting_defaults: {
|
||||
|
@ -62,11 +61,16 @@ describe("Sliding Sync", () => {
|
|||
|
||||
// assert order
|
||||
const checkOrder = (wantOrder: string[]) => {
|
||||
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomTile_title").should((elements) => {
|
||||
expect(_.map(elements, (e) => {
|
||||
return e.textContent;
|
||||
}), "rooms are sorted").to.deep.equal(wantOrder);
|
||||
});
|
||||
cy.contains(".mx_RoomSublist", "Rooms")
|
||||
.find(".mx_RoomTile_title")
|
||||
.should((elements) => {
|
||||
expect(
|
||||
_.map(elements, (e) => {
|
||||
return e.textContent;
|
||||
}),
|
||||
"rooms are sorted",
|
||||
).to.deep.equal(wantOrder);
|
||||
});
|
||||
};
|
||||
const bumpRoom = (alias: string) => {
|
||||
// Send a message into the given room, this should bump the room to the top
|
||||
|
@ -80,9 +84,11 @@ describe("Sliding Sync", () => {
|
|||
const createAndJoinBob = () => {
|
||||
// create a Bob user
|
||||
cy.get<SynapseInstance>("@synapse").then((synapse) => {
|
||||
return cy.getBot(synapse, {
|
||||
displayName: "Bob",
|
||||
}).as("bob");
|
||||
return cy
|
||||
.getBot(synapse, {
|
||||
displayName: "Bob",
|
||||
})
|
||||
.as("bob");
|
||||
});
|
||||
|
||||
// invite Bob to Test Room and accept then send a message.
|
||||
|
@ -95,7 +101,7 @@ describe("Sliding Sync", () => {
|
|||
|
||||
// sanity check everything works
|
||||
it("should correctly render expected messages", () => {
|
||||
cy.get<string>("@roomId").then(roomId => cy.visit("/#/room/" + roomId));
|
||||
cy.get<string>("@roomId").then((roomId) => cy.visit("/#/room/" + roomId));
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
// Wait until configuration is finished
|
||||
|
@ -114,54 +120,52 @@ describe("Sliding Sync", () => {
|
|||
cy.createRoom({ name: "Pineapple" }).then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||
cy.createRoom({ name: "Orange" }).then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||
// check the rooms are in the right order
|
||||
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
|
||||
checkOrder([
|
||||
"Orange", "Pineapple", "Apple", "Test Room",
|
||||
]);
|
||||
cy.get(".mx_RoomTile").should("have.length", 4); // due to the Test Room in beforeEach
|
||||
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
|
||||
|
||||
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomSublist_menuButton").click({ force: true });
|
||||
cy.contains("A-Z").click();
|
||||
cy.get('.mx_StyledRadioButton_checked').should("contain.text", "A-Z");
|
||||
checkOrder([
|
||||
"Apple", "Orange", "Pineapple", "Test Room",
|
||||
]);
|
||||
cy.get(".mx_StyledRadioButton_checked").should("contain.text", "A-Z");
|
||||
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
|
||||
});
|
||||
|
||||
it("should move rooms around as new events arrive", () => {
|
||||
// create rooms and check room names are correct
|
||||
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||
cy.createRoom({ name: "Apple" })
|
||||
.as("roomA")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
cy.createRoom({ name: "Pineapple" })
|
||||
.as("roomP")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||
cy.createRoom({ name: "Orange" })
|
||||
.as("roomO")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||
|
||||
// Select the Test Room
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
|
||||
checkOrder([
|
||||
"Orange", "Pineapple", "Apple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
|
||||
bumpRoom("@roomA");
|
||||
checkOrder([
|
||||
"Apple", "Orange", "Pineapple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
|
||||
bumpRoom("@roomO");
|
||||
checkOrder([
|
||||
"Orange", "Apple", "Pineapple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Orange", "Apple", "Pineapple", "Test Room"]);
|
||||
bumpRoom("@roomO");
|
||||
checkOrder([
|
||||
"Orange", "Apple", "Pineapple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Orange", "Apple", "Pineapple", "Test Room"]);
|
||||
bumpRoom("@roomP");
|
||||
checkOrder([
|
||||
"Pineapple", "Orange", "Apple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Pineapple", "Orange", "Apple", "Test Room"]);
|
||||
});
|
||||
|
||||
it("should not move the selected room: it should be sticky", () => {
|
||||
// create rooms and check room names are correct
|
||||
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||
cy.createRoom({ name: "Apple" })
|
||||
.as("roomA")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
cy.createRoom({ name: "Pineapple" })
|
||||
.as("roomP")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||
cy.createRoom({ name: "Orange" })
|
||||
.as("roomO")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||
|
||||
// Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
|
||||
// turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically
|
||||
|
@ -169,23 +173,17 @@ describe("Sliding Sync", () => {
|
|||
|
||||
// Select the Pineapple room
|
||||
cy.contains(".mx_RoomTile", "Pineapple").click();
|
||||
checkOrder([
|
||||
"Orange", "Pineapple", "Apple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Orange", "Pineapple", "Apple", "Test Room"]);
|
||||
|
||||
// Move Apple
|
||||
bumpRoom("@roomA");
|
||||
checkOrder([
|
||||
"Apple", "Pineapple", "Orange", "Test Room",
|
||||
]);
|
||||
checkOrder(["Apple", "Pineapple", "Orange", "Test Room"]);
|
||||
|
||||
// Select the Test Room
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
|
||||
// the rooms reshuffle to match reality
|
||||
checkOrder([
|
||||
"Apple", "Orange", "Pineapple", "Test Room",
|
||||
]);
|
||||
checkOrder(["Apple", "Orange", "Pineapple", "Test Room"]);
|
||||
});
|
||||
|
||||
it("should show the right unread notifications", () => {
|
||||
|
@ -212,7 +210,8 @@ describe("Sliding Sync", () => {
|
|||
cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count");
|
||||
});
|
||||
|
||||
it("should not show unread indicators", () => { // TODO: for now. Later we should.
|
||||
it("should not show unread indicators", () => {
|
||||
// TODO: for now. Later we should.
|
||||
createAndJoinBob();
|
||||
|
||||
// disable notifs in this room (TODO: CS API call?)
|
||||
|
@ -223,17 +222,13 @@ describe("Sliding Sync", () => {
|
|||
cy.createRoom({
|
||||
name: "Dummy",
|
||||
});
|
||||
checkOrder([
|
||||
"Dummy", "Test Room",
|
||||
]);
|
||||
checkOrder(["Dummy", "Test Room"]);
|
||||
|
||||
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||
return bob.sendTextMessage(roomId, "Do you read me?");
|
||||
});
|
||||
// wait for this message to arrive, tell by the room list resorting
|
||||
checkOrder([
|
||||
"Test Room", "Dummy",
|
||||
]);
|
||||
checkOrder(["Test Room", "Dummy"]);
|
||||
|
||||
cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist");
|
||||
});
|
||||
|
@ -242,13 +237,18 @@ describe("Sliding Sync", () => {
|
|||
cy.get(".mx_UserMenu_userAvatar").click();
|
||||
cy.contains("All settings").click();
|
||||
cy.contains("Preferences").click();
|
||||
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
|
||||
".mx_ToggleSwitch_on").should("not.exist");
|
||||
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
|
||||
".mx_ToggleSwitch_ball").click();
|
||||
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 }).should("exist").find(
|
||||
".mx_ToggleSwitch_on", { timeout: 2000 },
|
||||
).should("exist");
|
||||
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format")
|
||||
.should("exist")
|
||||
.find(".mx_ToggleSwitch_on")
|
||||
.should("not.exist");
|
||||
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format")
|
||||
.should("exist")
|
||||
.find(".mx_ToggleSwitch_ball")
|
||||
.click();
|
||||
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 })
|
||||
.should("exist")
|
||||
.find(".mx_ToggleSwitch_on", { timeout: 2000 })
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("should show and be able to accept/reject/rescind invites", () => {
|
||||
|
@ -263,50 +263,56 @@ describe("Sliding Sync", () => {
|
|||
// - roomJoin: will join this room
|
||||
// - roomReject: will reject the invite
|
||||
// - roomRescind: will make Bob rescind the invite
|
||||
let roomJoin; let roomReject; let roomRescind; let bobClient;
|
||||
cy.get<MatrixClient>("@bob").then((bob) => {
|
||||
bobClient = bob;
|
||||
return Promise.all([
|
||||
bob.createRoom({ name: "Join" }),
|
||||
bob.createRoom({ name: "Reject" }),
|
||||
bob.createRoom({ name: "Rescind" }),
|
||||
]);
|
||||
}).then(([join, reject, rescind]) => {
|
||||
roomJoin = join.room_id;
|
||||
roomReject = reject.room_id;
|
||||
roomRescind = rescind.room_id;
|
||||
return Promise.all([
|
||||
bobClient.invite(roomJoin, clientUserId),
|
||||
bobClient.invite(roomReject, clientUserId),
|
||||
bobClient.invite(roomRescind, clientUserId),
|
||||
]);
|
||||
});
|
||||
let roomJoin;
|
||||
let roomReject;
|
||||
let roomRescind;
|
||||
let bobClient;
|
||||
cy.get<MatrixClient>("@bob")
|
||||
.then((bob) => {
|
||||
bobClient = bob;
|
||||
return Promise.all([
|
||||
bob.createRoom({ name: "Join" }),
|
||||
bob.createRoom({ name: "Reject" }),
|
||||
bob.createRoom({ name: "Rescind" }),
|
||||
]);
|
||||
})
|
||||
.then(([join, reject, rescind]) => {
|
||||
roomJoin = join.room_id;
|
||||
roomReject = reject.room_id;
|
||||
roomRescind = rescind.room_id;
|
||||
return Promise.all([
|
||||
bobClient.invite(roomJoin, clientUserId),
|
||||
bobClient.invite(roomReject, clientUserId),
|
||||
bobClient.invite(roomRescind, clientUserId),
|
||||
]);
|
||||
});
|
||||
|
||||
// wait for them all to be on the UI
|
||||
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
|
||||
cy.get(".mx_RoomTile").should("have.length", 4); // due to the Test Room in beforeEach
|
||||
|
||||
cy.contains(".mx_RoomTile", "Join").click();
|
||||
cy.contains(".mx_AccessibleButton", "Accept").click();
|
||||
|
||||
checkOrder([
|
||||
"Join", "Test Room",
|
||||
]);
|
||||
checkOrder(["Join", "Test Room"]);
|
||||
|
||||
cy.contains(".mx_RoomTile", "Reject").click();
|
||||
cy.contains(".mx_RoomView .mx_AccessibleButton", "Reject").click();
|
||||
|
||||
// wait for the rejected room to disappear
|
||||
cy.get(".mx_RoomTile").should('have.length', 3);
|
||||
cy.get(".mx_RoomTile").should("have.length", 3);
|
||||
|
||||
// check the lists are correct
|
||||
checkOrder([
|
||||
"Join", "Test Room",
|
||||
]);
|
||||
cy.contains(".mx_RoomSublist", "Invites").find(".mx_RoomTile_title").should((elements) => {
|
||||
expect(_.map(elements, (e) => {
|
||||
return e.textContent;
|
||||
}), "rooms are sorted").to.deep.equal(["Rescind"]);
|
||||
});
|
||||
checkOrder(["Join", "Test Room"]);
|
||||
cy.contains(".mx_RoomSublist", "Invites")
|
||||
.find(".mx_RoomTile_title")
|
||||
.should((elements) => {
|
||||
expect(
|
||||
_.map(elements, (e) => {
|
||||
return e.textContent;
|
||||
}),
|
||||
"rooms are sorted",
|
||||
).to.deep.equal(["Rescind"]);
|
||||
});
|
||||
|
||||
// now rescind the invite
|
||||
cy.get<MatrixClient>("@bob").then((bob) => {
|
||||
|
@ -314,19 +320,19 @@ describe("Sliding Sync", () => {
|
|||
});
|
||||
|
||||
// wait for the rescind to take effect and check the joined list once more
|
||||
cy.get(".mx_RoomTile").should('have.length', 2);
|
||||
checkOrder([
|
||||
"Join", "Test Room",
|
||||
]);
|
||||
cy.get(".mx_RoomTile").should("have.length", 2);
|
||||
checkOrder(["Join", "Test Room"]);
|
||||
});
|
||||
|
||||
it("should show a favourite DM only in the favourite sublist", () => {
|
||||
cy.createRoom({
|
||||
name: "Favourite DM",
|
||||
is_direct: true,
|
||||
}).as("room").then(roomId => {
|
||||
cy.getClient().then(cli => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
|
||||
});
|
||||
})
|
||||
.as("room")
|
||||
.then((roomId) => {
|
||||
cy.getClient().then((cli) => cli.setRoomTag(roomId, "m.favourite", { order: 0.5 }));
|
||||
});
|
||||
|
||||
cy.contains('.mx_RoomSublist[aria-label="Favourites"] .mx_RoomTile', "Favourite DM").should("exist");
|
||||
cy.contains('.mx_RoomSublist[aria-label="People"] .mx_RoomTile', "Favourite DM").should("not.exist");
|
||||
|
@ -335,7 +341,9 @@ describe("Sliding Sync", () => {
|
|||
// Regression test for a bug in SS mode, but would be useful to have in non-SS mode too.
|
||||
// This ensures we are setting RoomViewStore state correctly.
|
||||
it("should clear the reply to field when swapping rooms", () => {
|
||||
cy.createRoom({ name: "Other Room" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Other Room"));
|
||||
cy.createRoom({ name: "Other Room" })
|
||||
.as("roomA")
|
||||
.then(() => cy.contains(".mx_RoomSublist", "Other Room"));
|
||||
cy.get<string>("@roomId").then((roomId) => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Hello world",
|
||||
|
@ -346,9 +354,9 @@ describe("Sliding Sync", () => {
|
|||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
cy.get(".mx_ReplyPreview").should("not.exist");
|
||||
// click reply-to on the Hello World message
|
||||
cy.contains(".mx_EventTile", "Hello world").find('.mx_AccessibleButton[aria-label="Reply"]').click(
|
||||
{ force: true },
|
||||
);
|
||||
cy.contains(".mx_EventTile", "Hello world")
|
||||
.find('.mx_AccessibleButton[aria-label="Reply"]')
|
||||
.click({ force: true });
|
||||
// check it's visible
|
||||
cy.get(".mx_ReplyPreview").should("exist");
|
||||
// now click Other Room
|
||||
|
@ -365,28 +373,31 @@ describe("Sliding Sync", () => {
|
|||
it("should not cancel replies when permalinks are clicked ", () => {
|
||||
cy.get<string>("@roomId").then((roomId) => {
|
||||
// we require a first message as you cannot click the permalink text with the avatar in the way
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "First message",
|
||||
msgtype: "m.text",
|
||||
}).then(() => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Permalink me",
|
||||
return cy
|
||||
.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "First message",
|
||||
msgtype: "m.text",
|
||||
})
|
||||
.then(() => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Permalink me",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Reply to me",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
}).then(() => {
|
||||
cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Reply to me",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
});
|
||||
// select the room
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
cy.get(".mx_ReplyPreview").should("not.exist");
|
||||
// click reply-to on the Reply to me message
|
||||
cy.contains(".mx_EventTile", "Reply to me").find('.mx_AccessibleButton[aria-label="Reply"]').click(
|
||||
{ force: true },
|
||||
);
|
||||
cy.contains(".mx_EventTile", "Reply to me")
|
||||
.find('.mx_AccessibleButton[aria-label="Reply"]')
|
||||
.click({ force: true });
|
||||
// check it's visible
|
||||
cy.get(".mx_ReplyPreview").should("exist");
|
||||
// now click on the permalink for Permalink me
|
||||
|
|
|
@ -37,12 +37,14 @@ function spaceCreateOptions(spaceName: string): ICreateRoomOpts {
|
|||
creation_content: {
|
||||
type: "m.space",
|
||||
},
|
||||
initial_state: [{
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: spaceName,
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: spaceName,
|
||||
},
|
||||
},
|
||||
}],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -61,10 +63,10 @@ describe("Spaces", () => {
|
|||
let user: UserCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sue").then(_user => {
|
||||
cy.initTestUser(synapse, "Sue").then((_user) => {
|
||||
user = _user;
|
||||
cy.mockClipboard();
|
||||
});
|
||||
|
@ -78,8 +80,10 @@ describe("Spaces", () => {
|
|||
it("should allow user to create public space", () => {
|
||||
openSpaceCreateMenu().within(() => {
|
||||
cy.get(".mx_SpaceCreateMenuType_public").click();
|
||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.selectFile("cypress/fixtures/riot.png", { force: true });
|
||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
|
||||
"cypress/fixtures/riot.png",
|
||||
{ force: true },
|
||||
);
|
||||
cy.get('input[label="Name"]').type("Let's have a Riot");
|
||||
cy.get('input[label="Address"]').should("have.value", "lets-have-a-riot");
|
||||
cy.get('textarea[label="Description"]').type("This is a space to reminisce Riot.im!");
|
||||
|
@ -108,8 +112,10 @@ describe("Spaces", () => {
|
|||
it("should allow user to create private space", () => {
|
||||
openSpaceCreateMenu().within(() => {
|
||||
cy.get(".mx_SpaceCreateMenuType_private").click();
|
||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.selectFile("cypress/fixtures/riot.png", { force: true });
|
||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
|
||||
"cypress/fixtures/riot.png",
|
||||
{ force: true },
|
||||
);
|
||||
cy.get('input[label="Name"]').type("This is not a Riot");
|
||||
cy.get('input[label="Address"]').should("not.exist");
|
||||
cy.get('textarea[label="Description"]').type("This is a private space of mourning Riot.im...");
|
||||
|
@ -145,8 +151,10 @@ describe("Spaces", () => {
|
|||
|
||||
openSpaceCreateMenu().within(() => {
|
||||
cy.get(".mx_SpaceCreateMenuType_private").click();
|
||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.selectFile("cypress/fixtures/riot.png", { force: true });
|
||||
cy.get('.mx_SpaceBasicSettings_avatarContainer input[type="file"]').selectFile(
|
||||
"cypress/fixtures/riot.png",
|
||||
{ force: true },
|
||||
);
|
||||
cy.get('input[label="Address"]').should("not.exist");
|
||||
cy.get('textarea[label="Description"]').type("This is a personal space to mourn Riot.im...");
|
||||
cy.get('input[label="Name"]').type("This is my Riot{enter}");
|
||||
|
@ -163,7 +171,7 @@ describe("Spaces", () => {
|
|||
|
||||
it("should allow user to invite another to a space", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
|
@ -198,13 +206,17 @@ describe("Spaces", () => {
|
|||
});
|
||||
cy.getSpacePanelButton("My Space").should("exist");
|
||||
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then({ timeout: 10000 }, async bot => {
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
|
||||
const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space"));
|
||||
await bot.invite(roomId, user.userId);
|
||||
});
|
||||
// Assert that `Space Space` is above `My Space` due to it being an invite
|
||||
cy.getSpacePanelButton("Space Space").should("exist")
|
||||
.parent().next().find('.mx_SpaceButton[aria-label="My Space"]').should("exist");
|
||||
cy.getSpacePanelButton("Space Space")
|
||||
.should("exist")
|
||||
.parent()
|
||||
.next()
|
||||
.find('.mx_SpaceButton[aria-label="My Space"]')
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("should include rooms in space home", () => {
|
||||
|
@ -216,16 +228,10 @@ describe("Spaces", () => {
|
|||
}).as("roomId2");
|
||||
|
||||
const spaceName = "Spacey Mc. Space Space";
|
||||
cy.all([
|
||||
cy.get<string>("@roomId1"),
|
||||
cy.get<string>("@roomId2"),
|
||||
]).then(([roomId1, roomId2]) => {
|
||||
cy.all([cy.get<string>("@roomId1"), cy.get<string>("@roomId2")]).then(([roomId1, roomId2]) => {
|
||||
cy.createSpace({
|
||||
name: spaceName,
|
||||
initial_state: [
|
||||
spaceChildInitialState(roomId1),
|
||||
spaceChildInitialState(roomId2),
|
||||
],
|
||||
initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
|
||||
}).as("spaceId");
|
||||
});
|
||||
|
||||
|
@ -244,12 +250,10 @@ describe("Spaces", () => {
|
|||
cy.createSpace({
|
||||
name: "Child Space",
|
||||
initial_state: [],
|
||||
}).then(spaceId => {
|
||||
}).then((spaceId) => {
|
||||
cy.createSpace({
|
||||
name: "Root Space",
|
||||
initial_state: [
|
||||
spaceChildInitialState(spaceId),
|
||||
],
|
||||
initial_state: [spaceChildInitialState(spaceId)],
|
||||
}).as("spaceId");
|
||||
});
|
||||
cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Root Space"]').should("exist");
|
||||
|
@ -258,7 +262,7 @@ describe("Spaces", () => {
|
|||
const axeOptions = {
|
||||
rules: {
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
'nested-interactive': {
|
||||
"nested-interactive": {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
|
@ -269,8 +273,10 @@ describe("Spaces", () => {
|
|||
cy.get(".mx_SpaceButton_toggleCollapse").click({ force: true });
|
||||
cy.get(".mx_SpacePanel:not(.collapsed)").should("exist");
|
||||
|
||||
cy.contains(".mx_SpaceItem", "Root Space").should("exist")
|
||||
.contains(".mx_SpaceItem", "Child Space").should("exist");
|
||||
cy.contains(".mx_SpaceItem", "Root Space")
|
||||
.should("exist")
|
||||
.contains(".mx_SpaceItem", "Child Space")
|
||||
.should("exist");
|
||||
|
||||
cy.checkA11y(undefined, axeOptions);
|
||||
cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] });
|
||||
|
|
|
@ -26,7 +26,7 @@ import Shadow = Cypress.Shadow;
|
|||
|
||||
export enum Filter {
|
||||
People = "people",
|
||||
PublicRooms = "public_rooms"
|
||||
PublicRooms = "public_rooms",
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -37,78 +37,86 @@ declare global {
|
|||
* Opens the spotlight dialog
|
||||
*/
|
||||
openSpotlightDialog(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightDialog(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightFilter(
|
||||
filter: Filter | null,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightSearch(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightResults(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
roomHeaderName(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
startDM(name: string): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("openSpotlightDialog", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get('.mx_RoomSearch_spotlightTrigger', options).click({ force: true });
|
||||
return cy.spotlightDialog(options);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"openSpotlightDialog",
|
||||
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get(".mx_RoomSearch_spotlightTrigger", options).click({ force: true });
|
||||
return cy.spotlightDialog(options);
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("spotlightDialog", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"spotlightDialog",
|
||||
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("spotlightFilter", (
|
||||
filter: Filter | null,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
let selector: string;
|
||||
switch (filter) {
|
||||
case Filter.People:
|
||||
selector = "#mx_SpotlightDialog_button_startChat";
|
||||
break;
|
||||
case Filter.PublicRooms:
|
||||
selector = "#mx_SpotlightDialog_button_explorePublicRooms";
|
||||
break;
|
||||
default:
|
||||
selector = ".mx_SpotlightDialog_filter";
|
||||
break;
|
||||
}
|
||||
return cy.get(selector, options).click();
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"spotlightFilter",
|
||||
(
|
||||
filter: Filter | null,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
let selector: string;
|
||||
switch (filter) {
|
||||
case Filter.People:
|
||||
selector = "#mx_SpotlightDialog_button_startChat";
|
||||
break;
|
||||
case Filter.PublicRooms:
|
||||
selector = "#mx_SpotlightDialog_button_explorePublicRooms";
|
||||
break;
|
||||
default:
|
||||
selector = ".mx_SpotlightDialog_filter";
|
||||
break;
|
||||
}
|
||||
return cy.get(selector, options).click();
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("spotlightSearch", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_SpotlightDialog_searchBox input", options);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"spotlightSearch",
|
||||
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_SpotlightDialog_searchBox input", options);
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("spotlightResults", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"spotlightResults",
|
||||
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("roomHeaderName", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_RoomHeader_nametext", options);
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"roomHeaderName",
|
||||
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_RoomHeader_nametext", options);
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("startDM", (name: string) => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
|
@ -121,9 +129,7 @@ Cypress.Commands.add("startDM", (name: string) => {
|
|||
cy.spotlightResults().eq(0).click();
|
||||
});
|
||||
// send first message to start DM
|
||||
cy.get(".mx_BasicMessageComposer_input")
|
||||
.should("have.focus")
|
||||
.type("Hey!{enter}");
|
||||
cy.get(".mx_BasicMessageComposer_input").should("have.focus").type("Hey!{enter}");
|
||||
// The DM room is created at this point, this can take a little bit of time
|
||||
cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
|
||||
cy.contains(".mx_RoomSublist[aria-label=People]", name);
|
||||
|
@ -148,46 +154,52 @@ describe("Spotlight", () => {
|
|||
let room3Id: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Jim").then(() =>
|
||||
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => {
|
||||
bot1 = _bot1;
|
||||
}),
|
||||
).then(() =>
|
||||
cy.getBot(synapse, { displayName: bot2Name }).then(_bot2 => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
bot2 = _bot2;
|
||||
}),
|
||||
).then(() =>
|
||||
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
|
||||
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => {
|
||||
room1Id = _room1Id;
|
||||
bot1.joinRoom(room1Id);
|
||||
cy.visit("/#/room/" + room1Id);
|
||||
});
|
||||
bot2.createRoom({ name: room2Name, visibility: Visibility.Public })
|
||||
.then(({ room_id: _room2Id }) => {
|
||||
room2Id = _room2Id;
|
||||
bot2.invite(room2Id, bot1.getUserId());
|
||||
cy.initTestUser(synapse, "Jim")
|
||||
.then(() =>
|
||||
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
|
||||
bot1 = _bot1;
|
||||
}),
|
||||
)
|
||||
.then(() =>
|
||||
cy.getBot(synapse, { displayName: bot2Name }).then((_bot2) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
bot2 = _bot2;
|
||||
}),
|
||||
)
|
||||
.then(() =>
|
||||
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
|
||||
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then((_room1Id) => {
|
||||
room1Id = _room1Id;
|
||||
bot1.joinRoom(room1Id);
|
||||
cy.visit("/#/room/" + room1Id);
|
||||
});
|
||||
bot2.createRoom({
|
||||
name: room3Name,
|
||||
visibility: Visibility.Public, initial_state: [{
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "world_readable",
|
||||
bot2.createRoom({ name: room2Name, visibility: Visibility.Public }).then(
|
||||
({ room_id: _room2Id }) => {
|
||||
room2Id = _room2Id;
|
||||
bot2.invite(room2Id, bot1.getUserId());
|
||||
},
|
||||
}],
|
||||
}).then(({ room_id: _room3Id }) => {
|
||||
room3Id = _room3Id;
|
||||
bot2.invite(room3Id, bot1.getUserId());
|
||||
});
|
||||
}),
|
||||
).then(() =>
|
||||
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
|
||||
);
|
||||
);
|
||||
bot2.createRoom({
|
||||
name: room3Name,
|
||||
visibility: Visibility.Public,
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "world_readable",
|
||||
},
|
||||
},
|
||||
],
|
||||
}).then(({ room_id: _room3Id }) => {
|
||||
room3Id = _room3Id;
|
||||
bot2.invite(room3Id, bot1.getUserId());
|
||||
});
|
||||
}),
|
||||
)
|
||||
.then(() => cy.get(".mx_RoomSublist_skeletonUI").should("not.exist"));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,63 +228,71 @@ describe("Spotlight", () => {
|
|||
});
|
||||
|
||||
it("should find joined rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find known public rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown public rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room2Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room2Id);
|
||||
}).then(() => {
|
||||
cy.get(".mx_RoomView_MessageList").should("have.length", 1);
|
||||
cy.roomHeaderName().should("contain", room2Name);
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room2Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room2Id);
|
||||
})
|
||||
.then(() => {
|
||||
cy.get(".mx_RoomView_MessageList").should("have.length", 1);
|
||||
cy.roomHeaderName().should("contain", room2Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown public world readable rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room3Id);
|
||||
}).then(() => {
|
||||
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
|
||||
cy.roomHeaderName().should("contain", room3Name);
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room3Id);
|
||||
})
|
||||
.then(() => {
|
||||
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
|
||||
cy.roomHeaderName().should("contain", room3Name);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
||||
|
@ -299,29 +319,33 @@ describe("Spotlight", () => {
|
|||
});
|
||||
*/
|
||||
it("should find known people", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", bot1Name);
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", bot1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown people", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", bot2Name);
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", bot2Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find group DMs by usernames or user ids", () => {
|
||||
|
@ -340,10 +364,7 @@ describe("Spotlight", () => {
|
|||
|
||||
// Send first message to actually start DM
|
||||
cy.roomHeaderName().should("contain", bot2Name);
|
||||
cy.get(".mx_BasicMessageComposer_input")
|
||||
.click()
|
||||
.should("have.focus")
|
||||
.type("Hey!{enter}");
|
||||
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
|
||||
|
||||
// Assert DM exists by checking for the first message and the room being in the room list
|
||||
cy.contains(".mx_EventTile_body", "Hey!", { timeout: 30000 });
|
||||
|
@ -352,13 +373,13 @@ describe("Spotlight", () => {
|
|||
// Invite BotBob into existing DM with ByteBot
|
||||
cy.getDmRooms(bot2.getUserId())
|
||||
.should("have.length", 1)
|
||||
.then(dmRooms => cy.getClient().then(client => client.getRoom(dmRooms[0])))
|
||||
.then(groupDm => {
|
||||
.then((dmRooms) => cy.getClient().then((client) => client.getRoom(dmRooms[0])))
|
||||
.then((groupDm) => {
|
||||
cy.inviteUser(groupDm.roomId, bot1.getUserId());
|
||||
cy.roomHeaderName().should(($element) =>
|
||||
expect($element.get(0).innerText).contains(groupDm.name));
|
||||
cy.roomHeaderName().should(($element) => expect($element.get(0).innerText).contains(groupDm.name));
|
||||
cy.get(".mx_RoomSublist[aria-label=People]").should(($element) =>
|
||||
expect($element.get(0).innerText).contains(groupDm.name));
|
||||
expect($element.get(0).innerText).contains(groupDm.name),
|
||||
);
|
||||
|
||||
// Search for BotBob by id, should return group DM and user
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
|
@ -407,17 +428,19 @@ describe("Spotlight", () => {
|
|||
});
|
||||
|
||||
it("should allow opening group chat dialog", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
|
||||
cy.get(".mx_SpotlightDialog_startGroupChat").click();
|
||||
}).then(() => {
|
||||
cy.get('[role=dialog]').should("contain", "Direct Messages");
|
||||
});
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
|
||||
cy.get(".mx_SpotlightDialog_startGroupChat").click();
|
||||
})
|
||||
.then(() => {
|
||||
cy.get("[role=dialog]").should("contain", "Direct Messages");
|
||||
});
|
||||
});
|
||||
|
||||
it("should close spotlight after starting a DM", () => {
|
||||
|
@ -445,38 +468,40 @@ describe("Spotlight", () => {
|
|||
// our debouncing logic only starts the search after a short timeout,
|
||||
// so we wait a few milliseconds.
|
||||
cy.wait(1000);
|
||||
cy.get(".mx_Spinner").should("not.exist").then(() => {
|
||||
cy.spotlightResults().should("have.length", 2).then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.get(".mx_Spinner")
|
||||
.should("not.exist")
|
||||
.then(() => {
|
||||
cy.spotlightResults()
|
||||
.should("have.length", 2)
|
||||
.then(() => {
|
||||
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
cy.spotlightSearch()
|
||||
.type("{downArrow}")
|
||||
.then(() => {
|
||||
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true");
|
||||
});
|
||||
cy.spotlightSearch()
|
||||
.type("{downArrow}")
|
||||
.then(() => {
|
||||
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
cy.spotlightSearch()
|
||||
.type("{upArrow}")
|
||||
.then(() => {
|
||||
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "true");
|
||||
});
|
||||
cy.spotlightSearch()
|
||||
.type("{upArrow}")
|
||||
.then(() => {
|
||||
cy.spotlightResults().eq(0).should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightResults().eq(1).should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
});
|
||||
cy.spotlightSearch().type("{downArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
});
|
||||
cy.spotlightSearch().type("{downArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
cy.spotlightSearch().type("{upArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
});
|
||||
cy.spotlightSearch().type("{upArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,7 +21,7 @@ import { MatrixClient } from "../../global";
|
|||
|
||||
function markWindowBeforeReload(): void {
|
||||
// mark our window object to "know" when it gets reloaded
|
||||
cy.window().then(w => w.beforeReload = true);
|
||||
cy.window().then((w) => (w.beforeReload = true));
|
||||
}
|
||||
|
||||
describe("Threads", () => {
|
||||
|
@ -30,10 +30,10 @@ describe("Threads", () => {
|
|||
beforeEach(() => {
|
||||
// Default threads to ON for this spec
|
||||
cy.enableLabsFeature("feature_thread");
|
||||
cy.window().then(win => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
|
@ -78,12 +78,12 @@ describe("Threads", () => {
|
|||
cy.getBot(synapse, {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: false,
|
||||
}).then(_bot => {
|
||||
}).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
bot.joinRoom(roomId);
|
||||
|
@ -95,10 +95,11 @@ describe("Threads", () => {
|
|||
|
||||
// Wait for message to send, get its ID and save as @threadId
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.invoke("attr", "data-scroll-tokens").as("threadId");
|
||||
.invoke("attr", "data-scroll-tokens")
|
||||
.as("threadId");
|
||||
|
||||
// Bot starts thread
|
||||
cy.get<string>("@threadId").then(threadId => {
|
||||
cy.get<string>("@threadId").then((threadId) => {
|
||||
bot.sendMessage(roomId, threadId, {
|
||||
body: "Hello there",
|
||||
msgtype: "m.text",
|
||||
|
@ -119,7 +120,8 @@ describe("Threads", () => {
|
|||
|
||||
// User reacts to message instead
|
||||
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Hello there")
|
||||
.find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
|
||||
.find('[aria-label="React"]')
|
||||
.click({ force: true }); // Cypress has no ability to hover
|
||||
cy.get(".mx_EmojiPicker").within(() => {
|
||||
cy.get('input[type="text"]').type("wave");
|
||||
cy.contains('[role="menuitem"]', "👋").click();
|
||||
|
@ -127,7 +129,8 @@ describe("Threads", () => {
|
|||
|
||||
// User redacts their prior response
|
||||
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test")
|
||||
.find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
|
||||
.find('[aria-label="Options"]')
|
||||
.click({ force: true }); // Cypress has no ability to hover
|
||||
cy.get(".mx_IconizedContextMenu").within(() => {
|
||||
cy.contains('[role="menuitem"]', "Remove").click();
|
||||
});
|
||||
|
@ -144,7 +147,7 @@ describe("Threads", () => {
|
|||
cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
|
||||
|
||||
// Bot responds to thread
|
||||
cy.get<string>("@threadId").then(threadId => {
|
||||
cy.get<string>("@threadId").then((threadId) => {
|
||||
bot.sendMessage(roomId, threadId, {
|
||||
body: "How are things?",
|
||||
msgtype: "m.text",
|
||||
|
@ -178,45 +181,55 @@ describe("Threads", () => {
|
|||
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
|
||||
});
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
|
||||
.should("contain", "Great! How about yourself?");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
|
||||
"contain",
|
||||
"Great! How about yourself?",
|
||||
);
|
||||
|
||||
// User closes right panel
|
||||
cy.get(".mx_ThreadView .mx_BaseCard_close").click();
|
||||
|
||||
// Bot responds to thread and saves the id of their message to @eventId
|
||||
cy.get<string>("@threadId").then(threadId => {
|
||||
cy.wrap(bot.sendMessage(roomId, threadId, {
|
||||
body: "I'm very good thanks",
|
||||
msgtype: "m.text",
|
||||
}).then(res => res.event_id)).as("eventId");
|
||||
cy.get<string>("@threadId").then((threadId) => {
|
||||
cy.wrap(
|
||||
bot
|
||||
.sendMessage(roomId, threadId, {
|
||||
body: "I'm very good thanks",
|
||||
msgtype: "m.text",
|
||||
})
|
||||
.then((res) => res.event_id),
|
||||
).as("eventId");
|
||||
});
|
||||
|
||||
// User asserts
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
|
||||
.should("contain", "I'm very good thanks");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
|
||||
"contain",
|
||||
"I'm very good thanks",
|
||||
);
|
||||
|
||||
// Bot edits their latest event
|
||||
cy.get<string>("@eventId").then(eventId => {
|
||||
cy.get<string>("@eventId").then((eventId) => {
|
||||
bot.sendMessage(roomId, {
|
||||
"body": "* I'm very good thanks :)",
|
||||
"msgtype": "m.text",
|
||||
"m.new_content": {
|
||||
"body": "I'm very good thanks :)",
|
||||
"msgtype": "m.text",
|
||||
body: "I'm very good thanks :)",
|
||||
msgtype: "m.text",
|
||||
},
|
||||
"m.relates_to": {
|
||||
"rel_type": "m.replace",
|
||||
"event_id": eventId,
|
||||
rel_type: "m.replace",
|
||||
event_id: eventId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// User asserts
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "BotBob");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
|
||||
.should("contain", "I'm very good thanks :)");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
|
||||
"contain",
|
||||
"I'm very good thanks :)",
|
||||
);
|
||||
});
|
||||
|
||||
it("can send voice messages", () => {
|
||||
|
@ -227,7 +240,7 @@ describe("Threads", () => {
|
|||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
@ -237,7 +250,9 @@ describe("Threads", () => {
|
|||
|
||||
// Create thread
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.realHover().find(".mx_MessageActionBar_threadButton").click();
|
||||
.realHover()
|
||||
.find(".mx_MessageActionBar_threadButton")
|
||||
.click();
|
||||
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
|
||||
|
||||
cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click();
|
||||
|
@ -250,7 +265,7 @@ describe("Threads", () => {
|
|||
it("right panel behaves correctly", () => {
|
||||
// Create room
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
cy.createRoom({}).then((_roomId) => {
|
||||
roomId = _roomId;
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
@ -259,7 +274,9 @@ describe("Threads", () => {
|
|||
|
||||
// Create thread
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.realHover().find(".mx_MessageActionBar_threadButton").click();
|
||||
.realHover()
|
||||
.find(".mx_MessageActionBar_threadButton")
|
||||
.click();
|
||||
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
|
||||
|
||||
// Send message to thread
|
||||
|
@ -271,7 +288,9 @@ describe("Threads", () => {
|
|||
|
||||
// Open existing thread
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.realHover().find(".mx_MessageActionBar_threadButton").click();
|
||||
.realHover()
|
||||
.find(".mx_MessageActionBar_threadButton")
|
||||
.click();
|
||||
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
|
||||
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. Bot");
|
||||
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User");
|
||||
|
|
|
@ -45,10 +45,7 @@ const expectDisplayName = (e: JQuery<HTMLElement>, displayName: string): void =>
|
|||
};
|
||||
|
||||
const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
|
||||
cy.all([
|
||||
cy.window({ log: false }),
|
||||
cy.getClient(),
|
||||
]).then(([win, cli]) => {
|
||||
cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => {
|
||||
const size = AVATAR_SIZE * win.devicePixelRatio;
|
||||
expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
|
@ -75,10 +72,10 @@ describe("Timeline", () => {
|
|||
let newAvatarUrl: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, OLD_NAME).then(() =>
|
||||
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
|
||||
cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
|
||||
roomId = _room1Id;
|
||||
}),
|
||||
);
|
||||
|
@ -154,8 +151,11 @@ describe("Timeline", () => {
|
|||
it("should create and configure a room on IRC layout", () => {
|
||||
cy.visit("/#/room/" + roomId);
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
|
||||
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
|
||||
".mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
).should("exist");
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
cy.percySnapshot("Configured room on IRC layout");
|
||||
});
|
||||
|
@ -165,8 +165,10 @@ describe("Timeline", () => {
|
|||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
// Wait until configuration is finished
|
||||
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " +
|
||||
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
).should("exist");
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
|
@ -177,13 +179,13 @@ describe("Timeline", () => {
|
|||
// = calc(var(--name-width) + 10px + var(--icon-width))
|
||||
// = 80 + 10 + 14 = 104px
|
||||
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
|
||||
.should('have.css', "margin-inline-start", "104px")
|
||||
.should('have.css', "inset-inline-start", "0px");
|
||||
.should("have.css", "margin-inline-start", "104px")
|
||||
.should("have.css", "inset-inline-start", "0px");
|
||||
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
// Exclude timestamp from snapshot
|
||||
const percyCSS = ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp "
|
||||
+ "{ visibility: hidden !important; }";
|
||||
const percyCSS =
|
||||
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
|
||||
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS });
|
||||
cy.checkA11y();
|
||||
});
|
||||
|
@ -192,8 +194,10 @@ describe("Timeline", () => {
|
|||
sendEvent(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
|
||||
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.").should("exist");
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
).should("exist");
|
||||
|
||||
// Edit message
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
|
||||
|
@ -206,20 +210,23 @@ describe("Timeline", () => {
|
|||
cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
|
||||
|
||||
// Exclude timestamp from snapshot
|
||||
const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp "
|
||||
+ "{ visibility: hidden !important; }";
|
||||
const percyCSS =
|
||||
".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
|
||||
|
||||
// should not add inline start padding to a hidden event line on IRC layout
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line")
|
||||
.should('have.css', 'padding-inline-start', '0px');
|
||||
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").should(
|
||||
"have.css",
|
||||
"padding-inline-start",
|
||||
"0px",
|
||||
);
|
||||
cy.percySnapshot("Hidden event line with zero padding on IRC layout", { percyCSS });
|
||||
|
||||
// should add inline start padding to a hidden event line on modern layout
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line")
|
||||
// calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
|
||||
.should('have.css', 'padding-inline-start', '84px');
|
||||
.should("have.css", "padding-inline-start", "84px");
|
||||
cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
|
||||
});
|
||||
|
||||
|
@ -227,8 +234,10 @@ describe("Timeline", () => {
|
|||
sendEvent(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
|
||||
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " +
|
||||
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
).should("exist");
|
||||
|
||||
// Edit message
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
|
||||
|
@ -238,9 +247,12 @@ describe("Timeline", () => {
|
|||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist");
|
||||
|
||||
// Click top left of the event toggle, which should not be covered by MessageActionBar's safe area
|
||||
cy.get(".mx_EventTile .mx_ViewSourceEvent").should("exist").realHover().within(() => {
|
||||
cy.get(".mx_ViewSourceEvent_toggle").click('topLeft', { force: false });
|
||||
});
|
||||
cy.get(".mx_EventTile .mx_ViewSourceEvent")
|
||||
.should("exist")
|
||||
.realHover()
|
||||
.within(() => {
|
||||
cy.get(".mx_ViewSourceEvent_toggle").click("topLeft", { force: false });
|
||||
});
|
||||
|
||||
// Make sure the expand toggle worked
|
||||
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible");
|
||||
|
@ -249,8 +261,11 @@ describe("Timeline", () => {
|
|||
it("should click 'collapse' link button on the first hovered info event line on bubble layout", () => {
|
||||
cy.visit("/#/room/" + roomId);
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
|
||||
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " +
|
||||
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary[data-layout=bubble] " +
|
||||
".mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
).should("exist");
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
|
@ -340,10 +355,14 @@ describe("Timeline", () => {
|
|||
|
||||
cy.getComposer().type(`${reply}{enter}`);
|
||||
|
||||
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody")
|
||||
.should("contain", MESSAGE);
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply)
|
||||
.should("have.length", 1);
|
||||
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody").should(
|
||||
"contain",
|
||||
MESSAGE,
|
||||
);
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MTextBody", reply).should(
|
||||
"have.length",
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("can reply with a voice message", () => {
|
||||
|
@ -355,10 +374,14 @@ describe("Timeline", () => {
|
|||
cy.wait(3000);
|
||||
cy.get(".mx_RoomView_body .mx_MessageComposer .mx_MessageComposer_sendMessage").click();
|
||||
|
||||
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody")
|
||||
.should("contain", MESSAGE);
|
||||
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody")
|
||||
.should("have.length", 1);
|
||||
cy.get(".mx_RoomView_body .mx_EventTile .mx_EventTile_line .mx_ReplyTile .mx_MTextBody").should(
|
||||
"contain",
|
||||
MESSAGE,
|
||||
);
|
||||
cy.get(".mx_RoomView_body .mx_EventTile > .mx_EventTile_line > .mx_MVoiceMessageBody").should(
|
||||
"have.length",
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -47,15 +47,15 @@ describe("Analytics Toast", () => {
|
|||
});
|
||||
|
||||
it("should not show an analytics toast if config has nothing about posthog", () => {
|
||||
cy.intercept("/config.json?cachebuster=*", req => {
|
||||
req.continue(res => {
|
||||
cy.intercept("/config.json?cachebuster=*", (req) => {
|
||||
req.continue((res) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { posthog, ...body } = res.body;
|
||||
res.send(200, body);
|
||||
});
|
||||
});
|
||||
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Tod");
|
||||
});
|
||||
|
@ -66,8 +66,8 @@ describe("Analytics Toast", () => {
|
|||
|
||||
describe("with posthog enabled", () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("/config.json?cachebuster=*", req => {
|
||||
req.continue(res => {
|
||||
cy.intercept("/config.json?cachebuster=*", (req) => {
|
||||
req.continue((res) => {
|
||||
res.send(200, {
|
||||
...res.body,
|
||||
posthog: {
|
||||
|
@ -78,7 +78,7 @@ describe("Analytics Toast", () => {
|
|||
});
|
||||
});
|
||||
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Tod");
|
||||
rejectToast("Notifications");
|
||||
|
|
|
@ -22,7 +22,7 @@ describe("Update", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
@ -45,9 +45,11 @@ describe("Update", () => {
|
|||
cy.initTestUser(synapse, "Ursa");
|
||||
|
||||
cy.wait("@version");
|
||||
cy.url().should("contain", "updated=" + NEW_VERSION).then(href => {
|
||||
const url = new URL(href);
|
||||
expect(url.searchParams.get("updated")).to.equal(NEW_VERSION);
|
||||
});
|
||||
cy.url()
|
||||
.should("contain", "updated=" + NEW_VERSION)
|
||||
.then((href) => {
|
||||
const url = new URL(href);
|
||||
expect(url.searchParams.get("updated")).to.equal(NEW_VERSION);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,10 +24,10 @@ describe("User Menu", () => {
|
|||
let user: UserCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Jeff").then(credentials => {
|
||||
cy.initTestUser(synapse, "Jeff").then((credentials) => {
|
||||
user = credentials;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,23 +26,23 @@ describe("User Onboarding (new user)", () => {
|
|||
let bot1: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Jane Doe");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.localStorage.setItem("mx_registration_time", "1656633601");
|
||||
});
|
||||
cy.reload().then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 15000 });
|
||||
});
|
||||
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => {
|
||||
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
|
||||
bot1 = _bot1;
|
||||
});
|
||||
cy.get('.mx_UserOnboardingPage').should('exist');
|
||||
cy.get('.mx_UserOnboardingButton').should('exist');
|
||||
cy.get('.mx_UserOnboardingList')
|
||||
.should('exist')
|
||||
cy.get(".mx_UserOnboardingPage").should("exist");
|
||||
cy.get(".mx_UserOnboardingButton").should("exist");
|
||||
cy.get(".mx_UserOnboardingList")
|
||||
.should("exist")
|
||||
.should(($list) => {
|
||||
const list = $list.get(0);
|
||||
expect(getComputedStyle(list).opacity).to.be.eq("1");
|
||||
|
@ -55,44 +55,42 @@ describe("User Onboarding (new user)", () => {
|
|||
});
|
||||
|
||||
it("page is shown and preference exists", () => {
|
||||
cy.get('.mx_UserOnboardingPage')
|
||||
.percySnapshotElement("User onboarding page");
|
||||
cy.get(".mx_UserOnboardingPage").percySnapshotElement("User onboarding page");
|
||||
cy.openUserSettings("Preferences");
|
||||
cy.contains("Show shortcut to welcome checklist above the room list").should("exist");
|
||||
});
|
||||
|
||||
it("app download dialog", () => {
|
||||
cy.contains(".mx_UserOnboardingTask_action", "Download apps").click();
|
||||
cy.get('[role=dialog]')
|
||||
.contains("#mx_BaseDialog_title", "Download Element")
|
||||
.should("exist");
|
||||
cy.get('[role=dialog]')
|
||||
.percySnapshotElement("App download dialog", {
|
||||
widths: [640],
|
||||
});
|
||||
cy.get("[role=dialog]").contains("#mx_BaseDialog_title", "Download Element").should("exist");
|
||||
cy.get("[role=dialog]").percySnapshotElement("App download dialog", {
|
||||
widths: [640],
|
||||
});
|
||||
});
|
||||
|
||||
it("using find friends action should increase progress", () => {
|
||||
cy.get(".mx_ProgressBar").invoke("val").then((oldProgress) => {
|
||||
const findPeopleAction = cy.contains(".mx_UserOnboardingTask_action", "Find friends");
|
||||
expect(findPeopleAction).to.exist;
|
||||
findPeopleAction.click();
|
||||
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
|
||||
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
|
||||
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
|
||||
const message = "Hi!";
|
||||
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
|
||||
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
|
||||
cy.visit("/#/home");
|
||||
cy.get('.mx_UserOnboardingPage').should('exist');
|
||||
cy.get('.mx_UserOnboardingButton').should('exist');
|
||||
cy.get('.mx_UserOnboardingList')
|
||||
.should('exist')
|
||||
.should(($list) => {
|
||||
const list = $list.get(0);
|
||||
expect(getComputedStyle(list).opacity).to.be.eq("1");
|
||||
});
|
||||
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
|
||||
});
|
||||
cy.get(".mx_ProgressBar")
|
||||
.invoke("val")
|
||||
.then((oldProgress) => {
|
||||
const findPeopleAction = cy.contains(".mx_UserOnboardingTask_action", "Find friends");
|
||||
expect(findPeopleAction).to.exist;
|
||||
findPeopleAction.click();
|
||||
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
|
||||
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
|
||||
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
|
||||
const message = "Hi!";
|
||||
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
|
||||
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
|
||||
cy.visit("/#/home");
|
||||
cy.get(".mx_UserOnboardingPage").should("exist");
|
||||
cy.get(".mx_UserOnboardingButton").should("exist");
|
||||
cy.get(".mx_UserOnboardingList")
|
||||
.should("exist")
|
||||
.should(($list) => {
|
||||
const list = $list.get(0);
|
||||
expect(getComputedStyle(list).opacity).to.be.eq("1");
|
||||
});
|
||||
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,10 +22,10 @@ describe("User Onboarding (old user)", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Jane Doe");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.localStorage.setItem("mx_registration_time", "2");
|
||||
});
|
||||
cy.reload().then(() => {
|
||||
|
@ -41,8 +41,8 @@ describe("User Onboarding (old user)", () => {
|
|||
});
|
||||
|
||||
it("page and preference are hidden", () => {
|
||||
cy.get('.mx_UserOnboardingPage').should('not.exist');
|
||||
cy.get('.mx_UserOnboardingButton').should('not.exist');
|
||||
cy.get(".mx_UserOnboardingPage").should("not.exist");
|
||||
cy.get(".mx_UserOnboardingButton").should("not.exist");
|
||||
cy.openUserSettings("Preferences");
|
||||
cy.contains("Show shortcut to welcome page above the room list").should("not.exist");
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ describe("UserView", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Violet");
|
||||
|
@ -36,7 +36,7 @@ describe("UserView", () => {
|
|||
});
|
||||
|
||||
it("should render the user view as expected", () => {
|
||||
cy.get<MatrixClient>("@bot").then(bot => {
|
||||
cy.get<MatrixClient>("@bot").then((bot) => {
|
||||
cy.visit(`/#/user/${bot.getUserId()}`);
|
||||
});
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { IWidget } from "matrix-widget-api";
|
|||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
const ROOM_NAME = 'Test Room';
|
||||
const ROOM_NAME = "Test Room";
|
||||
const WIDGET_ID = "fake-widget";
|
||||
const WIDGET_HTML = `
|
||||
<html lang="en">
|
||||
|
@ -32,18 +32,18 @@ const WIDGET_HTML = `
|
|||
</html>
|
||||
`;
|
||||
|
||||
describe('Widget Layout', () => {
|
||||
describe("Widget Layout", () => {
|
||||
let widgetUrl: string;
|
||||
let synapse: SynapseInstance;
|
||||
let roomId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
});
|
||||
cy.serveHtmlFile(WIDGET_HTML).then(url => {
|
||||
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
|
||||
widgetUrl = url;
|
||||
});
|
||||
|
||||
|
@ -53,34 +53,38 @@ describe('Widget Layout', () => {
|
|||
roomId = id;
|
||||
|
||||
// setup widget via state event
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content: IWidget = {
|
||||
id: WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
type: 'widget',
|
||||
name: 'widget',
|
||||
url: widgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, WIDGET_ID);
|
||||
}).as('widgetEventSent');
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content: IWidget = {
|
||||
id: WIDGET_ID,
|
||||
creatorUserId: "somebody",
|
||||
type: "widget",
|
||||
name: "widget",
|
||||
url: widgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
|
||||
})
|
||||
.as("widgetEventSent");
|
||||
|
||||
// set initial layout
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 0,
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: "top",
|
||||
index: 1,
|
||||
width: 100,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
|
||||
}).as('layoutEventSent');
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
|
||||
})
|
||||
.as("layoutEventSent");
|
||||
});
|
||||
|
||||
cy.all([
|
||||
cy.get<string>("@widgetEventSent"),
|
||||
cy.get<string>("@layoutEventSent"),
|
||||
]).then(() => {
|
||||
cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(() => {
|
||||
// open the room
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
});
|
||||
|
@ -91,31 +95,34 @@ describe('Widget Layout', () => {
|
|||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it('manually resize the height of the top container layout', () => {
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250);
|
||||
it("manually resize the height of the top container layout", () => {
|
||||
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
|
||||
|
||||
cy.get('.mx_AppsContainer_resizerHandle')
|
||||
.trigger('mousedown')
|
||||
.trigger('mousemove', { clientX: 0, clientY: 550, force: true })
|
||||
.trigger('mouseup', { clientX: 0, clientY: 550, force: true });
|
||||
cy.get(".mx_AppsContainer_resizerHandle")
|
||||
.trigger("mousedown")
|
||||
.trigger("mousemove", { clientX: 0, clientY: 550, force: true })
|
||||
.trigger("mouseup", { clientX: 0, clientY: 550, force: true });
|
||||
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
|
||||
cy.get('iframe[title="widget"]').invoke("height").should("be.greaterThan", 400);
|
||||
});
|
||||
|
||||
it('programatically resize the height of the top container layout', () => {
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250);
|
||||
it("programatically resize the height of the top container layout", () => {
|
||||
cy.get('iframe[title="widget"]').invoke("height").should("be.lessThan", 250);
|
||||
|
||||
cy.getClient().then(async matrixClient => {
|
||||
cy.getClient().then(async (matrixClient) => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 100,
|
||||
container: "top",
|
||||
index: 1,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
|
||||
await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
|
||||
});
|
||||
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
|
||||
cy.get('iframe[title="widget"]').invoke("height").should("be.greaterThan", 400);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,8 +67,8 @@ const WIDGET_HTML = `
|
|||
`;
|
||||
|
||||
function openStickerPicker() {
|
||||
cy.get('.mx_MessageComposer_buttonMenu').click();
|
||||
cy.get('#stickersButton').click();
|
||||
cy.get(".mx_MessageComposer_buttonMenu").click();
|
||||
cy.get("#stickersButton").click();
|
||||
}
|
||||
|
||||
function sendStickerFromPicker() {
|
||||
|
@ -76,18 +76,16 @@ function sendStickerFromPicker() {
|
|||
// to use `chromeWebSecurity: false` in our cypress config. Not even cy.origin() can
|
||||
// break into the iframe for us :(
|
||||
cy.accessIframe(`iframe[title="${STICKER_PICKER_WIDGET_NAME}"]`).within({}, () => {
|
||||
cy.get("#sendsticker").should('exist').click();
|
||||
cy.get("#sendsticker").should("exist").click();
|
||||
});
|
||||
|
||||
// Sticker picker should close itself after sending.
|
||||
cy.get(".mx_AppTileFullWidth#stickers").should('not.exist');
|
||||
cy.get(".mx_AppTileFullWidth#stickers").should("not.exist");
|
||||
}
|
||||
|
||||
function expectTimelineSticker(roomId: string) {
|
||||
// Make sure it's in the right room
|
||||
cy.get('.mx_EventTile_sticker > a')
|
||||
.should("have.attr", "href")
|
||||
.and("include", `/${roomId}/`);
|
||||
cy.get(".mx_EventTile_sticker > a").should("have.attr", "href").and("include", `/${roomId}/`);
|
||||
|
||||
// Make sure the image points at the sticker image
|
||||
cy.get<HTMLImageElement>(`img[alt="${STICKER_NAME}"]`)
|
||||
|
@ -107,12 +105,12 @@ describe("Stickers", () => {
|
|||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
});
|
||||
cy.serveHtmlFile(WIDGET_HTML).then(url => {
|
||||
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
|
||||
stickerPickerUrl = url;
|
||||
});
|
||||
});
|
||||
|
@ -122,7 +120,7 @@ describe("Stickers", () => {
|
|||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it('should send a sticker to multiple rooms', () => {
|
||||
it("should send a sticker to multiple rooms", () => {
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME_1,
|
||||
}).as("roomId1");
|
||||
|
|
|
@ -57,7 +57,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
|
|||
return new Promise((resolve, reject) => {
|
||||
function eventsInIntendedState(evList) {
|
||||
const widgetPresent = evList.some((ev) => {
|
||||
return ev.getContent() && ev.getContent()['id'] === widgetId;
|
||||
return ev.getContent() && ev.getContent()["id"] === widgetId;
|
||||
});
|
||||
if (add) {
|
||||
return widgetPresent;
|
||||
|
@ -68,7 +68,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
|
|||
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
||||
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
if (eventsInIntendedState(startingWidgetEvents)) {
|
||||
resolve();
|
||||
return;
|
||||
|
@ -77,7 +77,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
|
|||
function onRoomStateEvents(ev: MatrixEvent) {
|
||||
if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return;
|
||||
|
||||
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
|
||||
if (eventsInIntendedState(currentWidgetEvents)) {
|
||||
matrixClient.removeListener(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
|
||||
|
@ -95,35 +95,39 @@ describe("Widget PIP", () => {
|
|||
let bot: MatrixClient;
|
||||
let demoWidgetUrl: string;
|
||||
|
||||
function roomCreateAddWidgetPip(userRemove: 'leave' | 'kick' | 'ban') {
|
||||
function roomCreateAddWidgetPip(userRemove: "leave" | "kick" | "ban") {
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME,
|
||||
invite: [bot.getUserId()],
|
||||
}).then(roomId => {
|
||||
}).then((roomId) => {
|
||||
// sets bot to Admin and user to Moderator
|
||||
cy.getClient().then(matrixClient => {
|
||||
return matrixClient.sendStateEvent(roomId, 'm.room.power_levels', {
|
||||
users: {
|
||||
[user.userId]: 50,
|
||||
[bot.getUserId()]: 100,
|
||||
},
|
||||
});
|
||||
}).as('powerLevelsChanged');
|
||||
cy.getClient()
|
||||
.then((matrixClient) => {
|
||||
return matrixClient.sendStateEvent(roomId, "m.room.power_levels", {
|
||||
users: {
|
||||
[user.userId]: 50,
|
||||
[bot.getUserId()]: 100,
|
||||
},
|
||||
});
|
||||
})
|
||||
.as("powerLevelsChanged");
|
||||
|
||||
// bot joins the room
|
||||
cy.botJoinRoom(bot, roomId).as('botJoined');
|
||||
cy.botJoinRoom(bot, roomId).as("botJoined");
|
||||
|
||||
// setup widget via state event
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content: IWidget = {
|
||||
id: DEMO_WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
type: DEMO_WIDGET_TYPE,
|
||||
name: DEMO_WIDGET_NAME,
|
||||
url: demoWidgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID);
|
||||
}).as('widgetEventSent');
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content: IWidget = {
|
||||
id: DEMO_WIDGET_ID,
|
||||
creatorUserId: "somebody",
|
||||
type: DEMO_WIDGET_TYPE,
|
||||
name: DEMO_WIDGET_NAME,
|
||||
url: demoWidgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID);
|
||||
})
|
||||
.as("widgetEventSent");
|
||||
|
||||
// open the room
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
@ -133,7 +137,7 @@ describe("Widget PIP", () => {
|
|||
cy.get<string>("@botJoined"),
|
||||
cy.get<string>("@widgetEventSent"),
|
||||
]).then(() => {
|
||||
cy.window().then(async win => {
|
||||
cy.window().then(async (win) => {
|
||||
// wait for widget state event
|
||||
await waitForRoomWidget(win, DEMO_WIDGET_ID, roomId, true);
|
||||
|
||||
|
@ -145,21 +149,23 @@ describe("Widget PIP", () => {
|
|||
|
||||
// checks that widget is opened in pip
|
||||
cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => {
|
||||
cy.get("#demo").should('exist').then(async () => {
|
||||
const userId = user.userId;
|
||||
if (userRemove == 'leave') {
|
||||
cy.getClient().then(async matrixClient => {
|
||||
await matrixClient.leave(roomId);
|
||||
});
|
||||
} else if (userRemove == 'kick') {
|
||||
await bot.kick(roomId, userId);
|
||||
} else if (userRemove == 'ban') {
|
||||
await bot.ban(roomId, userId);
|
||||
}
|
||||
cy.get("#demo")
|
||||
.should("exist")
|
||||
.then(async () => {
|
||||
const userId = user.userId;
|
||||
if (userRemove == "leave") {
|
||||
cy.getClient().then(async (matrixClient) => {
|
||||
await matrixClient.leave(roomId);
|
||||
});
|
||||
} else if (userRemove == "kick") {
|
||||
await bot.kick(roomId, userId);
|
||||
} else if (userRemove == "ban") {
|
||||
await bot.ban(roomId, userId);
|
||||
}
|
||||
|
||||
// checks that pip window is closed
|
||||
cy.get(".mx_LegacyCallView_pip").should("not.exist");
|
||||
});
|
||||
// checks that pip window is closed
|
||||
cy.get(".mx_LegacyCallView_pip").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -167,17 +173,17 @@ describe("Widget PIP", () => {
|
|||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
cy.startSynapse("default").then((data) => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Mike").then(_user => {
|
||||
cy.initTestUser(synapse, "Mike").then((_user) => {
|
||||
user = _user;
|
||||
});
|
||||
cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then(_bot => {
|
||||
cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
});
|
||||
cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => {
|
||||
cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => {
|
||||
demoWidgetUrl = url;
|
||||
});
|
||||
});
|
||||
|
@ -187,15 +193,15 @@ describe("Widget PIP", () => {
|
|||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it('should be closed on leave', () => {
|
||||
roomCreateAddWidgetPip('leave');
|
||||
it("should be closed on leave", () => {
|
||||
roomCreateAddWidgetPip("leave");
|
||||
});
|
||||
|
||||
it('should be closed on kick', () => {
|
||||
roomCreateAddWidgetPip('kick');
|
||||
it("should be closed on kick", () => {
|
||||
roomCreateAddWidgetPip("kick");
|
||||
});
|
||||
|
||||
it('should be closed on ban', () => {
|
||||
roomCreateAddWidgetPip('ban');
|
||||
it("should be closed on ban", () => {
|
||||
roomCreateAddWidgetPip("ban");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
{
|
||||
"versions": [
|
||||
"r0.0.1",
|
||||
"r0.1.0",
|
||||
"r0.2.0",
|
||||
"r0.3.0",
|
||||
"r0.4.0",
|
||||
"r0.5.0",
|
||||
"r0.6.0",
|
||||
"r0.6.1",
|
||||
"v1.1",
|
||||
"v1.2",
|
||||
"v1.3",
|
||||
"v1.4"
|
||||
],
|
||||
"unstable_features": {
|
||||
"org.matrix.label_based_filtering": true,
|
||||
"org.matrix.e2e_cross_signing": true,
|
||||
"org.matrix.msc2432": true,
|
||||
"uk.half-shot.msc2666.mutual_rooms": true,
|
||||
"io.element.e2ee_forced.public": false,
|
||||
"io.element.e2ee_forced.private": false,
|
||||
"io.element.e2ee_forced.trusted_private": false,
|
||||
"org.matrix.msc3026.busy_presence": false,
|
||||
"org.matrix.msc2285.stable": true,
|
||||
"org.matrix.msc3827.stable": true,
|
||||
"org.matrix.msc2716": false,
|
||||
"org.matrix.msc3030": false,
|
||||
"org.matrix.msc3440.stable": true,
|
||||
"org.matrix.msc3771": true,
|
||||
"org.matrix.msc3773": false,
|
||||
"fi.mau.msc2815": false,
|
||||
"org.matrix.msc3882": false,
|
||||
"org.matrix.msc3881": false,
|
||||
"org.matrix.msc3874": false,
|
||||
"org.matrix.msc3886": false,
|
||||
"org.matrix.msc3912": false
|
||||
}
|
||||
}
|
||||
"versions": [
|
||||
"r0.0.1",
|
||||
"r0.1.0",
|
||||
"r0.2.0",
|
||||
"r0.3.0",
|
||||
"r0.4.0",
|
||||
"r0.5.0",
|
||||
"r0.6.0",
|
||||
"r0.6.1",
|
||||
"v1.1",
|
||||
"v1.2",
|
||||
"v1.3",
|
||||
"v1.4"
|
||||
],
|
||||
"unstable_features": {
|
||||
"org.matrix.label_based_filtering": true,
|
||||
"org.matrix.e2e_cross_signing": true,
|
||||
"org.matrix.msc2432": true,
|
||||
"uk.half-shot.msc2666.mutual_rooms": true,
|
||||
"io.element.e2ee_forced.public": false,
|
||||
"io.element.e2ee_forced.private": false,
|
||||
"io.element.e2ee_forced.trusted_private": false,
|
||||
"org.matrix.msc3026.busy_presence": false,
|
||||
"org.matrix.msc2285.stable": true,
|
||||
"org.matrix.msc3827.stable": true,
|
||||
"org.matrix.msc2716": false,
|
||||
"org.matrix.msc3030": false,
|
||||
"org.matrix.msc3440.stable": true,
|
||||
"org.matrix.msc3771": true,
|
||||
"org.matrix.msc3773": false,
|
||||
"fi.mau.msc2815": false,
|
||||
"org.matrix.msc3882": false,
|
||||
"org.matrix.msc3881": false,
|
||||
"org.matrix.msc3874": false,
|
||||
"org.matrix.msc3886": false,
|
||||
"org.matrix.msc3912": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@ export function dockerRun(opts: {
|
|||
|
||||
const args = [
|
||||
"run",
|
||||
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
|
||||
"--name",
|
||||
`${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
|
||||
"-d",
|
||||
...params,
|
||||
opts.image,
|
||||
|
@ -58,23 +59,22 @@ export function dockerRun(opts: {
|
|||
});
|
||||
}
|
||||
|
||||
export function dockerExec(args: {
|
||||
containerId: string;
|
||||
params: string[];
|
||||
}): Promise<void> {
|
||||
export function dockerExec(args: { containerId: string; params: string[] }): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile("docker", [
|
||||
"exec", args.containerId,
|
||||
...args.params,
|
||||
], { encoding: 'utf8' }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
childProcess.execFile(
|
||||
"docker",
|
||||
["exec", args.containerId, ...args.params],
|
||||
{ encoding: "utf8" },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -87,58 +87,45 @@ export async function dockerLogs(args: {
|
|||
const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore";
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
childProcess.spawn("docker", [
|
||||
"logs",
|
||||
args.containerId,
|
||||
], {
|
||||
stdio: ["ignore", stdoutFile, stderrFile],
|
||||
}).once('close', resolve);
|
||||
childProcess
|
||||
.spawn("docker", ["logs", args.containerId], {
|
||||
stdio: ["ignore", stdoutFile, stderrFile],
|
||||
})
|
||||
.once("close", resolve);
|
||||
});
|
||||
|
||||
if (args.stdoutFile) await fse.close(<number>stdoutFile);
|
||||
if (args.stderrFile) await fse.close(<number>stderrFile);
|
||||
}
|
||||
|
||||
export function dockerStop(args: {
|
||||
containerId: string;
|
||||
}): Promise<void> {
|
||||
export function dockerStop(args: { containerId: string }): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"stop",
|
||||
args.containerId,
|
||||
], err => {
|
||||
childProcess.execFile("docker", ["stop", args.containerId], (err) => {
|
||||
if (err) reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dockerRm(args: {
|
||||
containerId: string;
|
||||
}): Promise<void> {
|
||||
export function dockerRm(args: { containerId: string }): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"rm",
|
||||
args.containerId,
|
||||
], err => {
|
||||
childProcess.execFile("docker", ["rm", args.containerId], (err) => {
|
||||
if (err) reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dockerIp(args: {
|
||||
containerId: string;
|
||||
}): Promise<string> {
|
||||
export function dockerIp(args: { containerId: string }): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"inspect",
|
||||
"-f", "{{ .NetworkSettings.IPAddress }}",
|
||||
args.containerId,
|
||||
], (err, stdout) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stdout.trim());
|
||||
});
|
||||
childProcess.execFile(
|
||||
"docker",
|
||||
["inspect", "-f", "{{ .NetworkSettings.IPAddress }}", args.containerId],
|
||||
(err, stdout) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stdout.trim());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ import { log } from "./log";
|
|||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export default function(on: PluginEvents, config: PluginConfigOptions) {
|
||||
export default function (on: PluginEvents, config: PluginConfigOptions) {
|
||||
docker(on, config);
|
||||
synapseDocker(on, config);
|
||||
slidingSyncProxyDocker(on, config);
|
||||
|
|
|
@ -41,10 +41,7 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
|
|||
const postgresId = await dockerRun({
|
||||
image: "postgres",
|
||||
containerName: "react-sdk-cypress-sliding-sync-postgres",
|
||||
params: [
|
||||
"--rm",
|
||||
"-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`,
|
||||
],
|
||||
params: ["--rm", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`],
|
||||
});
|
||||
|
||||
const postgresIp = await dockerIp({ containerId: postgresId });
|
||||
|
@ -54,14 +51,11 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
|
|||
const waitTimeMillis = 30000;
|
||||
const startTime = new Date().getTime();
|
||||
let lastErr: Error;
|
||||
while ((new Date().getTime() - startTime) < waitTimeMillis) {
|
||||
while (new Date().getTime() - startTime < waitTimeMillis) {
|
||||
try {
|
||||
await dockerExec({
|
||||
containerId: postgresId,
|
||||
params: [
|
||||
"pg_isready",
|
||||
"-U", "postgres",
|
||||
],
|
||||
params: ["pg_isready", "-U", "postgres"],
|
||||
});
|
||||
lastErr = null;
|
||||
break;
|
||||
|
@ -82,10 +76,14 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
|
|||
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
||||
params: [
|
||||
"--rm",
|
||||
"-p", `${port}:8008/tcp`,
|
||||
"-e", "SYNCV3_SECRET=bwahahaha",
|
||||
"-e", `SYNCV3_SERVER=http://${synapseIp}:8008`,
|
||||
"-e", `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
|
||||
"-p",
|
||||
`${port}:8008/tcp`,
|
||||
"-e",
|
||||
"SYNCV3_SECRET=bwahahaha",
|
||||
"-e",
|
||||
`SYNCV3_SERVER=http://${synapseIp}:8008`,
|
||||
"-e",
|
||||
`SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
|
||||
],
|
||||
});
|
||||
console.log(new Date(), "started!");
|
||||
|
|
|
@ -54,11 +54,11 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
|||
if (!stats?.isDirectory) {
|
||||
throw new Error(`No such template: ${template}`);
|
||||
}
|
||||
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-'));
|
||||
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-synapsedocker-"));
|
||||
|
||||
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
|
||||
console.log(`Copy ${templateDir} -> ${tempDir}`);
|
||||
await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' });
|
||||
await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== "homeserver.yaml" });
|
||||
|
||||
const registrationSecret = randB64Bytes(16);
|
||||
const macaroonSecret = randB64Bytes(16);
|
||||
|
@ -102,11 +102,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
|||
const synapseId = await dockerRun({
|
||||
image: "matrixdotorg/synapse:develop",
|
||||
containerName: `react-sdk-cypress-synapse`,
|
||||
params: [
|
||||
"--rm",
|
||||
"-v", `${synCfg.configDir}:/data`,
|
||||
"-p", `${synCfg.port}:8008/tcp`,
|
||||
],
|
||||
params: ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`],
|
||||
cmd: "run",
|
||||
});
|
||||
|
||||
|
@ -117,9 +113,12 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
|||
containerId: synapseId,
|
||||
params: [
|
||||
"curl",
|
||||
"--connect-timeout", "30",
|
||||
"--retry", "30",
|
||||
"--retry-delay", "1",
|
||||
"--connect-timeout",
|
||||
"30",
|
||||
"--retry",
|
||||
"30",
|
||||
"--retry-delay",
|
||||
"1",
|
||||
"--retry-all-errors",
|
||||
"--silent",
|
||||
"http://localhost:8008/health",
|
||||
|
|
|
@ -5,21 +5,21 @@ pid_file: /data/homeserver.pid
|
|||
public_baseurl: http://localhost:8008/
|
||||
# Listener is always port 8008 (configured in the container)
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
type: http
|
||||
x_forwarded: true
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
resources:
|
||||
- names: [client, federation, consent]
|
||||
compress: false
|
||||
resources:
|
||||
- names: [client, federation, consent]
|
||||
compress: false
|
||||
|
||||
# An sqlite in-memory database is fast & automatically wipes each time
|
||||
database:
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
|
||||
# Needs to be configured to log to the console like a good docker process
|
||||
log_config: "/data/log.config"
|
||||
|
@ -27,19 +27,19 @@ log_config: "/data/log.config"
|
|||
rc_messages_per_second: 10000
|
||||
rc_message_burst_count: 10000
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
media_store_path: "/data/media_store"
|
||||
uploads_path: "/data/uploads"
|
||||
|
@ -54,19 +54,19 @@ form_secret: "{{FORM_SECRET}}"
|
|||
# Signing key must be here: it will be generated to this file
|
||||
signing_key_path: "/data/localhost.signing.key"
|
||||
email:
|
||||
enable_notifs: false
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 25
|
||||
smtp_user: "exampleusername"
|
||||
smtp_pass: "examplepassword"
|
||||
require_transport_security: False
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: Matrix
|
||||
notif_template_html: notif_mail.html
|
||||
notif_template_text: notif_mail.txt
|
||||
notif_for_new_users: True
|
||||
client_base_url: "http://localhost/element"
|
||||
enable_notifs: false
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 25
|
||||
smtp_user: "exampleusername"
|
||||
smtp_pass: "examplepassword"
|
||||
require_transport_security: False
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: Matrix
|
||||
notif_template_html: notif_mail.html
|
||||
notif_template_text: notif_mail.txt
|
||||
notif_for_new_users: True
|
||||
client_base_url: "http://localhost/element"
|
||||
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
||||
|
|
|
@ -2,39 +2,39 @@ server_name: "localhost"
|
|||
pid_file: /data/homeserver.pid
|
||||
public_baseurl: "{{PUBLIC_BASEURL}}"
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
type: http
|
||||
x_forwarded: true
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
resources:
|
||||
- names: [client, federation, consent]
|
||||
compress: false
|
||||
resources:
|
||||
- names: [client, federation, consent]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
|
||||
log_config: "/data/log.config"
|
||||
|
||||
rc_messages_per_second: 10000
|
||||
rc_message_burst_count: 10000
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
media_store_path: "/data/media_store"
|
||||
uploads_path: "/data/uploads"
|
||||
|
@ -47,38 +47,38 @@ macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
|
|||
form_secret: "{{FORM_SECRET}}"
|
||||
signing_key_path: "/data/localhost.signing.key"
|
||||
email:
|
||||
enable_notifs: false
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 25
|
||||
smtp_user: "exampleusername"
|
||||
smtp_pass: "examplepassword"
|
||||
require_transport_security: False
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: Matrix
|
||||
notif_template_html: notif_mail.html
|
||||
notif_template_text: notif_mail.txt
|
||||
notif_for_new_users: True
|
||||
client_base_url: "http://localhost/element"
|
||||
enable_notifs: false
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 25
|
||||
smtp_user: "exampleusername"
|
||||
smtp_pass: "examplepassword"
|
||||
require_transport_security: False
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: Matrix
|
||||
notif_template_html: notif_mail.html
|
||||
notif_template_text: notif_mail.txt
|
||||
notif_for_new_users: True
|
||||
client_base_url: "http://localhost/element"
|
||||
|
||||
user_consent:
|
||||
template_dir: /data/res/templates/privacy
|
||||
version: 1.0
|
||||
server_notice_content:
|
||||
msgtype: m.text
|
||||
body: >-
|
||||
To continue using this homeserver you must review and agree to the
|
||||
terms and conditions at %(consent_uri)s
|
||||
send_server_notice_to_guests: True
|
||||
block_events_error: >-
|
||||
To continue using this homeserver you must review and agree to the
|
||||
terms and conditions at %(consent_uri)s
|
||||
require_at_registration: true
|
||||
template_dir: /data/res/templates/privacy
|
||||
version: 1.0
|
||||
server_notice_content:
|
||||
msgtype: m.text
|
||||
body: >-
|
||||
To continue using this homeserver you must review and agree to the
|
||||
terms and conditions at %(consent_uri)s
|
||||
send_server_notice_to_guests: True
|
||||
block_events_error: >-
|
||||
To continue using this homeserver you must review and agree to the
|
||||
terms and conditions at %(consent_uri)s
|
||||
require_at_registration: true
|
||||
|
||||
server_notices:
|
||||
system_mxid_localpart: notices
|
||||
system_mxid_display_name: "Server Notices"
|
||||
system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ"
|
||||
room_name: "Server Notices"
|
||||
system_mxid_localpart: notices
|
||||
system_mxid_display_name: "Server Notices"
|
||||
system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ"
|
||||
room_name: "Server Notices"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if has_consented %}
|
||||
<p>
|
||||
Thank you, you've already accepted the license.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Please accept the license!
|
||||
</p>
|
||||
<form method="post" action="consent">
|
||||
<input type="hidden" name="v" value="{{version}}"/>
|
||||
<input type="hidden" name="u" value="{{user}}"/>
|
||||
<input type="hidden" name="h" value="{{userhmac}}"/>
|
||||
<input type="submit" value="Sure thing!"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if has_consented %}
|
||||
<p>Thank you, you've already accepted the license.</p>
|
||||
{% else %}
|
||||
<p>Please accept the license!</p>
|
||||
<form method="post" action="consent">
|
||||
<input type="hidden" name="v" value="{{version}}" />
|
||||
<input type="hidden" name="u" value="{{user}}" />
|
||||
<input type="hidden" name="h" value="{{userhmac}}" />
|
||||
<input type="submit" value="Sure thing!" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Danke schon</p>
|
||||
</body>
|
||||
</html>
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Danke schon</p>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -2,60 +2,60 @@ server_name: "localhost"
|
|||
pid_file: /data/homeserver.pid
|
||||
public_baseurl: "{{PUBLIC_BASEURL}}"
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
type: http
|
||||
x_forwarded: true
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
resources:
|
||||
- names: [client]
|
||||
compress: false
|
||||
resources:
|
||||
- names: [client]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
|
||||
log_config: "/data/log.config"
|
||||
|
||||
rc_messages_per_second: 10000
|
||||
rc_message_burst_count: 10000
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
rc_joins:
|
||||
local:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
remote:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
local:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
remote:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
rc_joins_per_room:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
rc_3pid_validation:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
rc_invites:
|
||||
per_room:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_user:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_room:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_user:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
media_store_path: "/data/media_store"
|
||||
uploads_path: "/data/uploads"
|
||||
|
@ -69,8 +69,8 @@ form_secret: "{{FORM_SECRET}}"
|
|||
signing_key_path: "/data/localhost.signing.key"
|
||||
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
||||
|
||||
ui_auth:
|
||||
session_timeout: "300s"
|
||||
session_timeout: "300s"
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import * as net from "net";
|
||||
|
||||
export async function getFreePort(): Promise<number> {
|
||||
return new Promise<number>(resolve => {
|
||||
return new Promise<number>((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = (<net.AddressInfo>srv.address()).port;
|
||||
|
|
|
@ -32,7 +32,7 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("tweakConfig", (tweaks: Record<string, any>): Chainable<AUTWindow> => {
|
||||
return cy.window().then(win => {
|
||||
return cy.window().then((win) => {
|
||||
// note: we can't *set* the object because the window version is effectively a pointer.
|
||||
for (const [k, v] of Object.entries(tweaks)) {
|
||||
// @ts-ignore - for some reason it's not picking up on global.d.ts types.
|
||||
|
@ -42,4 +42,4 @@ Cypress.Commands.add("tweakConfig", (tweaks: Record<string, any>): Chainable<AUT
|
|||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -24,10 +24,10 @@ import Chainable = Cypress.Chainable;
|
|||
|
||||
function terminalLog(violations: axe.Result[]): void {
|
||||
cy.task(
|
||||
'log',
|
||||
`${violations.length} accessibility violation${
|
||||
violations.length === 1 ? '' : 's'
|
||||
} ${violations.length === 1 ? 'was' : 'were'} detected`,
|
||||
"log",
|
||||
`${violations.length} accessibility violation${violations.length === 1 ? "" : "s"} ${
|
||||
violations.length === 1 ? "was" : "were"
|
||||
} detected`,
|
||||
);
|
||||
|
||||
// pluck specific keys to keep the table readable
|
||||
|
@ -38,24 +38,32 @@ function terminalLog(violations: axe.Result[]): void {
|
|||
nodes: nodes.length,
|
||||
}));
|
||||
|
||||
cy.task('table', violationData);
|
||||
cy.task("table", violationData);
|
||||
}
|
||||
|
||||
Cypress.Commands.overwrite("checkA11y", (
|
||||
originalFn: Chainable["checkA11y"],
|
||||
context?: string | Node | axe.ContextObject | undefined,
|
||||
options: Options = {},
|
||||
violationCallback?: ((violations: axe.Result[]) => void) | undefined,
|
||||
skipFailures?: boolean,
|
||||
): void => {
|
||||
return originalFn(context, {
|
||||
...options,
|
||||
rules: {
|
||||
// Disable contrast checking for now as we have too many issues with it
|
||||
'color-contrast': {
|
||||
enabled: false,
|
||||
Cypress.Commands.overwrite(
|
||||
"checkA11y",
|
||||
(
|
||||
originalFn: Chainable["checkA11y"],
|
||||
context?: string | Node | axe.ContextObject | undefined,
|
||||
options: Options = {},
|
||||
violationCallback?: ((violations: axe.Result[]) => void) | undefined,
|
||||
skipFailures?: boolean,
|
||||
): void => {
|
||||
return originalFn(
|
||||
context,
|
||||
{
|
||||
...options,
|
||||
rules: {
|
||||
// Disable contrast checking for now as we have too many issues with it
|
||||
"color-contrast": {
|
||||
enabled: false,
|
||||
},
|
||||
...options.rules,
|
||||
},
|
||||
},
|
||||
...options.rules,
|
||||
},
|
||||
}, violationCallback ?? terminalLog, skipFailures);
|
||||
});
|
||||
violationCallback ?? terminalLog,
|
||||
skipFailures,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -77,9 +77,9 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
|||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy.registerUser(synapse, username, password, opts.displayName).then(credentials => {
|
||||
return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => {
|
||||
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
||||
return cy.window({ log: false }).then(win => {
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
const cli = new win.matrixcs.MatrixClient({
|
||||
baseUrl: synapse.baseUrl,
|
||||
userId: credentials.userId,
|
||||
|
@ -103,12 +103,17 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
|||
}
|
||||
|
||||
return cy.wrap(
|
||||
cli.initCrypto()
|
||||
cli
|
||||
.initCrypto()
|
||||
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
||||
.then(() => cli.startClient())
|
||||
.then(() => cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
}))
|
||||
.then(() =>
|
||||
cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
}),
|
||||
)
|
||||
.then(() => cli),
|
||||
);
|
||||
});
|
||||
|
@ -129,13 +134,15 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
|
|||
return cy.wrap(Promise.reject(`Bot room join failed. Cannot find room '${roomName}'`));
|
||||
});
|
||||
|
||||
Cypress.Commands.add("botSendMessage", (
|
||||
cli: MatrixClient,
|
||||
roomId: string,
|
||||
message: string,
|
||||
): Chainable<ISendEventResponse> => {
|
||||
return cy.wrap(cli.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
body: message,
|
||||
}), { log: false });
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"botSendMessage",
|
||||
(cli: MatrixClient, roomId: string, message: string): Chainable<ISendEventResponse> => {
|
||||
return cy.wrap(
|
||||
cli.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
body: message,
|
||||
}),
|
||||
{ log: false },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -66,7 +66,7 @@ declare global {
|
|||
roomId: string,
|
||||
threadId: string | null,
|
||||
eventType: string,
|
||||
content: IContent
|
||||
content: IContent,
|
||||
): Chainable<ISendEventResponse>;
|
||||
/**
|
||||
* @param {string} name
|
||||
|
@ -89,10 +89,7 @@ declare global {
|
|||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
* a a Buffer, String or ReadStream.
|
||||
*/
|
||||
uploadContent(
|
||||
file: FileType,
|
||||
opts?: UploadOpts,
|
||||
): Chainable<Awaited<Upload["promise"]>>;
|
||||
uploadContent(file: FileType, opts?: UploadOpts): Chainable<Awaited<Upload["promise"]>>;
|
||||
/**
|
||||
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and
|
||||
* may change.</strong>
|
||||
|
@ -133,23 +130,24 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => {
|
||||
return cy.window({ log: false }).then(win => win.mxMatrixClientPeg.matrixClient);
|
||||
return cy.window({ log: false }).then((win) => win.mxMatrixClientPeg.matrixClient);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("getDmRooms", (userId: string): Chainable<string[]> => {
|
||||
return cy.getClient()
|
||||
.then(cli => cli.getAccountData("m.direct")?.getContent<Record<string, string[]>>())
|
||||
.then(dmRoomMap => dmRoomMap[userId] ?? []);
|
||||
return cy
|
||||
.getClient()
|
||||
.then((cli) => cli.getAccountData("m.direct")?.getContent<Record<string, string[]>>())
|
||||
.then((dmRoomMap) => dmRoomMap[userId] ?? []);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
|
||||
return cy.window({ log: false }).then(async win => {
|
||||
return cy.window({ log: false }).then(async (win) => {
|
||||
const cli = win.mxMatrixClientPeg.matrixClient;
|
||||
const resp = await cli.createRoom(options);
|
||||
const roomId = resp.room_id;
|
||||
|
||||
if (!cli.getRoom(roomId)) {
|
||||
await new Promise<void>(resolve => {
|
||||
await new Promise<void>((resolve) => {
|
||||
const onRoom = (room: Room) => {
|
||||
if (room.roomId === roomId) {
|
||||
cli.off(win.matrixcs.ClientEvent.Room, onRoom);
|
||||
|
@ -168,7 +166,7 @@ Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable<string
|
|||
return cy.createRoom({
|
||||
...options,
|
||||
creation_content: {
|
||||
"type": "m.space",
|
||||
type: "m.space",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -185,16 +183,14 @@ Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("sendEvent", (
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
eventType: string,
|
||||
content: IContent,
|
||||
): Chainable<ISendEventResponse> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
return cli.sendEvent(roomId, threadId, eventType, content);
|
||||
});
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"sendEvent",
|
||||
(roomId: string, threadId: string | null, eventType: string, content: IContent): Chainable<ISendEventResponse> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
return cli.sendEvent(roomId, threadId, eventType, content);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("setDisplayName", (name: string): Chainable<{}> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
|
@ -215,13 +211,15 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add("bootstrapCrossSigning", () => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => {
|
||||
return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
|
||||
return cy.getClient().then((cli) => cli.joinRoom(roomIdOrAlias));
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("mockClipboard", () => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.navigator.clipboard.writeText = (text) => {
|
||||
copyText = text;
|
||||
return Promise.resolve();
|
||||
|
@ -54,4 +54,4 @@ Cypress.Commands.add("getClipboardText", (): Chainable<string> => {
|
|||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -33,7 +33,7 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("getComposer", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||
const panelClass = isRightPanel ? '.mx_RightPanel' : '.mx_RoomView_body';
|
||||
const panelClass = isRightPanel ? ".mx_RightPanel" : ".mx_RoomView_body";
|
||||
return cy.get(`${panelClass} .mx_MessageComposer`);
|
||||
});
|
||||
|
||||
|
@ -41,8 +41,8 @@ Cypress.Commands.add("openMessageComposerOptions", (isRightPanel?: boolean): Cha
|
|||
cy.getComposer(isRightPanel).within(() => {
|
||||
cy.get('[aria-label="More options"]').click();
|
||||
});
|
||||
return cy.get('.mx_MessageComposer_Menu');
|
||||
return cy.get(".mx_MessageComposer_Menu");
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -35,11 +35,15 @@ declare global {
|
|||
|
||||
// Inspired by https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
|
||||
Cypress.Commands.add("accessIframe", (selector: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(selector)
|
||||
.its("0.contentDocument.body").should("not.be.empty")
|
||||
// Cypress loses types in the mess of wrapping, so force cast
|
||||
.then(cy.wrap) as Chainable<JQuery<HTMLElement>>;
|
||||
return (
|
||||
cy
|
||||
.get(selector)
|
||||
.its("0.contentDocument.body")
|
||||
.should("not.be.empty")
|
||||
// Cypress loses types in the mess of wrapping, so force cast
|
||||
.then(cy.wrap) as Chainable<JQuery<HTMLElement>>
|
||||
);
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -33,10 +33,13 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("enableLabsFeature", (feature: string): Chainable<null> => {
|
||||
return cy.window({ log: false }).then(win => {
|
||||
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
|
||||
}).then(() => null);
|
||||
return cy
|
||||
.window({ log: false })
|
||||
.then((win) => {
|
||||
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
|
||||
})
|
||||
.then(() => null);
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -49,87 +49,97 @@ declare global {
|
|||
* @param username login username
|
||||
* @param password login password
|
||||
*/
|
||||
loginUser(
|
||||
synapse: SynapseInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
): Chainable<UserCredentials>;
|
||||
loginUser(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add("loginUser", (synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
|
||||
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
|
||||
return cy.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}>({
|
||||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
"user": username,
|
||||
Cypress.Commands.add(
|
||||
"loginUser",
|
||||
(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
|
||||
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
|
||||
return cy
|
||||
.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}>({
|
||||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
password: password,
|
||||
},
|
||||
"password": password,
|
||||
},
|
||||
}).then(response => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
homeServer: response.body.home_server,
|
||||
}));
|
||||
});
|
||||
})
|
||||
.then((response) => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
homeServer: response.body.home_server,
|
||||
}));
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
|
||||
// XXX: work around Cypress not clearing IDB between tests
|
||||
cy.window({ log: false }).then(win => {
|
||||
win.indexedDB.databases()?.then(databases => {
|
||||
databases.forEach(database => {
|
||||
win.indexedDB.deleteDatabase(database.name);
|
||||
Cypress.Commands.add(
|
||||
"initTestUser",
|
||||
(synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
|
||||
// XXX: work around Cypress not clearing IDB between tests
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.indexedDB.databases()?.then((databases) => {
|
||||
databases.forEach((database) => {
|
||||
win.indexedDB.deleteDatabase(database.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
||||
return cy.loginUser(synapse, username, password);
|
||||
}).then(response => {
|
||||
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||
cy.window({ log: false }).then(win => {
|
||||
// Seed the localStorage with the required credentials
|
||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||
win.localStorage.setItem("mx_user_id", response.userId);
|
||||
win.localStorage.setItem("mx_access_token", response.accessToken);
|
||||
win.localStorage.setItem("mx_device_id", response.deviceId);
|
||||
win.localStorage.setItem("mx_is_guest", "false");
|
||||
win.localStorage.setItem("mx_has_pickle_key", "false");
|
||||
win.localStorage.setItem("mx_has_access_token", "true");
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy
|
||||
.registerUser(synapse, username, password, displayName)
|
||||
.then(() => {
|
||||
return cy.loginUser(synapse, username, password);
|
||||
})
|
||||
.then((response) => {
|
||||
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||
cy.window({ log: false }).then((win) => {
|
||||
// Seed the localStorage with the required credentials
|
||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||
win.localStorage.setItem("mx_user_id", response.userId);
|
||||
win.localStorage.setItem("mx_access_token", response.accessToken);
|
||||
win.localStorage.setItem("mx_device_id", response.deviceId);
|
||||
win.localStorage.setItem("mx_is_guest", "false");
|
||||
win.localStorage.setItem("mx_has_pickle_key", "false");
|
||||
win.localStorage.setItem("mx_has_access_token", "true");
|
||||
|
||||
// Ensure the language is set to a consistent value
|
||||
win.localStorage.setItem("mx_local_settings", '{"language":"en"}');
|
||||
});
|
||||
// Ensure the language is set to a consistent value
|
||||
win.localStorage.setItem("mx_local_settings", '{"language":"en"}');
|
||||
});
|
||||
|
||||
prelaunchFn?.();
|
||||
prelaunchFn?.();
|
||||
|
||||
return cy.visit("/").then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 30000 });
|
||||
}).then(() => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.accessToken,
|
||||
userId: response.userId,
|
||||
deviceId: response.deviceId,
|
||||
homeServer: response.homeServer,
|
||||
}));
|
||||
});
|
||||
});
|
||||
return cy
|
||||
.visit("/")
|
||||
.then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 30000 });
|
||||
})
|
||||
.then(() => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.accessToken,
|
||||
userId: response.userId,
|
||||
deviceId: response.deviceId,
|
||||
homeServer: response.homeServer,
|
||||
}));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
|
@ -35,27 +35,35 @@ declare global {
|
|||
|
||||
Cypress.Commands.add("goOffline", (): void => {
|
||||
cy.log("Going offline");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
cy.window({ log: false }).then((win) => {
|
||||
cy.intercept(
|
||||
"**/_matrix/**",
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
},
|
||||
}, req => {
|
||||
req.destroy();
|
||||
});
|
||||
(req) => {
|
||||
req.destroy();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("goOnline", (): void => {
|
||||
cy.log("Going online");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
cy.window({ log: false }).then((win) => {
|
||||
cy.intercept(
|
||||
"**/_matrix/**",
|
||||
{
|
||||
headers: {
|
||||
Authorization: "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
},
|
||||
}, req => {
|
||||
req.continue();
|
||||
});
|
||||
(req) => {
|
||||
req.continue();
|
||||
},
|
||||
);
|
||||
win.dispatchEvent(new Event("online"));
|
||||
});
|
||||
});
|
||||
|
@ -85,4 +93,4 @@ Cypress.Commands.add("stubDefaultServer", (): void => {
|
|||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
import { SnapshotOptions as PercySnapshotOptions } from '@percy/core';
|
||||
import { SnapshotOptions as PercySnapshotOptions } from "@percy/core";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
@ -39,16 +39,16 @@ declare global {
|
|||
|
||||
Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => {
|
||||
cy.percySnapshot(name, {
|
||||
domTransformation: documentClone => scope(documentClone, subject.selector),
|
||||
domTransformation: (documentClone) => scope(documentClone, subject.selector),
|
||||
...options,
|
||||
});
|
||||
});
|
||||
|
||||
function scope(documentClone: Document, selector: string): Document {
|
||||
const element = documentClone.querySelector(selector);
|
||||
documentClone.querySelector('body').innerHTML = element.outerHTML;
|
||||
documentClone.querySelector("body").innerHTML = element.outerHTML;
|
||||
|
||||
return documentClone;
|
||||
}
|
||||
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
|||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
import { ProxyInstance } from '../plugins/sliding-sync';
|
||||
import { ProxyInstance } from "../plugins/sliding-sync";
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
|
||||
declare global {
|
||||
|
@ -49,7 +49,7 @@ function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
|
|||
if (!proxy) return;
|
||||
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
win.location.href = 'about:blank';
|
||||
win.location.href = "about:blank";
|
||||
cy.task("proxyStop", proxy);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -102,26 +102,27 @@ declare global {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("getSettingsStore", (): Chainable<typeof SettingsStore> => {
|
||||
return cy.window({ log: false }).then(win => win.mxSettingsStore);
|
||||
return cy.window({ log: false }).then((win) => win.mxSettingsStore);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("setSettingValue", (
|
||||
name: string,
|
||||
roomId: string,
|
||||
level: SettingLevel,
|
||||
value: any,
|
||||
): Chainable<void> => {
|
||||
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
|
||||
return cy.wrap(store.setValue(name, roomId, level, value));
|
||||
});
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"setSettingValue",
|
||||
(name: string, roomId: string, level: SettingLevel, value: any): Chainable<void> => {
|
||||
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
|
||||
return cy.wrap(store.setValue(name, roomId, level, value));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T> => {
|
||||
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
|
||||
return store.getValue(name, roomId, excludeDefault);
|
||||
});
|
||||
});
|
||||
Cypress.Commands.add(
|
||||
"getSettingValue",
|
||||
<T = any>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T> => {
|
||||
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
|
||||
return store.getValue(name, roomId, excludeDefault);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("openUserMenu", (): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get('[aria-label="User menu"]').click();
|
||||
|
@ -162,16 +163,22 @@ Cypress.Commands.add("closeDialog", (): Chainable<JQuery<HTMLElement>> => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add("joinBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
|
||||
return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
|
||||
});
|
||||
return cy
|
||||
.contains(".mx_BetaCard_title", name)
|
||||
.closest(".mx_BetaCard")
|
||||
.within(() => {
|
||||
return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("leaveBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
|
||||
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
|
||||
});
|
||||
return cy
|
||||
.contains(".mx_BetaCard_title", name)
|
||||
.closest(".mx_BetaCard")
|
||||
.within(() => {
|
||||
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import * as crypto from "crypto";
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
|
@ -64,7 +64,7 @@ function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
|
|||
if (!synapse) return;
|
||||
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
win.location.href = 'about:blank';
|
||||
win.location.href = "about:blank";
|
||||
cy.task("synapseStop", synapse.synapseId);
|
||||
});
|
||||
}
|
||||
|
@ -83,38 +83,42 @@ function registerUser(
|
|||
displayName?: string,
|
||||
): Chainable<Credentials> {
|
||||
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
|
||||
return cy.then(() => {
|
||||
// get a nonce
|
||||
return cy.request<{ nonce: string }>({ url });
|
||||
}).then(response => {
|
||||
const { nonce } = response.body;
|
||||
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
|
||||
`${nonce}\0${username}\0${password}\0notadmin`,
|
||||
).digest('hex');
|
||||
return cy
|
||||
.then(() => {
|
||||
// get a nonce
|
||||
return cy.request<{ nonce: string }>({ url });
|
||||
})
|
||||
.then((response) => {
|
||||
const { nonce } = response.body;
|
||||
const mac = crypto
|
||||
.createHmac("sha1", synapse.registrationSecret)
|
||||
.update(`${nonce}\0${username}\0${password}\0notadmin`)
|
||||
.digest("hex");
|
||||
|
||||
return cy.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
home_server: string;
|
||||
device_id: string;
|
||||
}>({
|
||||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
nonce,
|
||||
username,
|
||||
password,
|
||||
mac,
|
||||
admin: false,
|
||||
displayname: displayName,
|
||||
},
|
||||
});
|
||||
}).then(response => ({
|
||||
homeServer: response.body.home_server,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
}));
|
||||
return cy.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
home_server: string;
|
||||
device_id: string;
|
||||
}>({
|
||||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
nonce,
|
||||
username,
|
||||
password,
|
||||
mac,
|
||||
admin: false,
|
||||
displayname: displayName,
|
||||
},
|
||||
});
|
||||
})
|
||||
.then((response) => ({
|
||||
homeServer: response.body.home_server,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
}));
|
||||
}
|
||||
|
||||
Cypress.Commands.add("startSynapse", startSynapse);
|
||||
|
|
|
@ -38,17 +38,19 @@ export interface Message {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("scrollToTop", (): void => {
|
||||
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
|
||||
if (ref.scrollTop() > 0) {
|
||||
return cy.scrollToTop();
|
||||
}
|
||||
});
|
||||
cy.get(".mx_RoomView_timeline .mx_ScrollPanel")
|
||||
.scrollTo("top", { duration: 100 })
|
||||
.then((ref) => {
|
||||
if (ref.scrollTop() > 0) {
|
||||
return cy.scrollToTop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<JQuery> => {
|
||||
// We can't just use a bunch of `.contains` here due to continuations meaning that the events don't
|
||||
// have their own rendered sender displayname so we have to walk the list to keep track of the sender.
|
||||
return cy.get(".mx_RoomView_MessageList .mx_EventTile").then(refs => {
|
||||
return cy.get(".mx_RoomView_MessageList .mx_EventTile").then((refs) => {
|
||||
let latestSender: string;
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
const ref = refs.eq(i);
|
||||
|
@ -65,4 +67,4 @@ Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<
|
|||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -29,7 +29,7 @@ declare global {
|
|||
|
||||
interface cy {
|
||||
all<T extends Cypress.Chainable[] | []>(
|
||||
commands: T
|
||||
commands: T,
|
||||
): Cypress.Chainable<{ [P in keyof T]: ChainableValue<T[P]> }>;
|
||||
queue: any;
|
||||
}
|
||||
|
@ -59,16 +59,16 @@ cy.all = function all(commands): Cypress.Chainable {
|
|||
return cy.wrap(
|
||||
// @see https://lodash.com/docs/4.17.15#lodash
|
||||
Cypress._(commands)
|
||||
.map(cmd => {
|
||||
.map((cmd) => {
|
||||
return cmd[chainStart]
|
||||
? cmd[chainStart].attributes
|
||||
: Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: cmd.chainerId },
|
||||
}).attributes;
|
||||
attributes: { chainerId: cmd.chainerId },
|
||||
}).attributes;
|
||||
})
|
||||
.concat(stopCommand.attributes)
|
||||
.slice(1)
|
||||
.map(cmd => {
|
||||
.map((cmd) => {
|
||||
return cmd.prev.get("subject");
|
||||
})
|
||||
.value(),
|
||||
|
@ -79,4 +79,4 @@ cy.all = function all(commands): Cypress.Chainable {
|
|||
};
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -70,4 +70,4 @@ Cypress.Commands.add("viewSpaceHomeByName", (name: string): Chainable<JQuery<HTM
|
|||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -49,4 +49,4 @@ Cypress.Commands.add("serveHtmlFile", serveHtmlFile);
|
|||
Cypress.Commands.add("stopWebServers", stopWebServers);
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -2,22 +2,12 @@
|
|||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-axe",
|
||||
"@percy/cypress"
|
||||
],
|
||||
"lib": ["es2020", "dom", "dom.iterable"],
|
||||
"types": ["cypress", "cypress-axe", "@percy/cypress"],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
|
|
@ -64,8 +64,8 @@ the content string, caret nodes need to be ignored, as they would confuse the mo
|
|||
|
||||
As part of the reconciliation, the caret position is also adjusted to any changes
|
||||
the model made to the input. The caret is passed around in two formats.
|
||||
The model receives the caret *offset* within the content string (which includes
|
||||
The model receives the caret _offset_ within the content string (which includes
|
||||
an atNodeEnd flag to make it unambiguous if it is at a part and or the next part start).
|
||||
The model converts this to a caret *position* internally, which has a partIndex
|
||||
The model converts this to a caret _position_ internally, which has a partIndex
|
||||
and an offset within the part text, which is more natural to work with.
|
||||
From there on, the caret *position* is used, also during reconciliation.
|
||||
From there on, the caret _position_ is used, also during reconciliation.
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
# Cypress in Element Web
|
||||
|
||||
## Scope of this Document
|
||||
|
||||
This doc is about our Cypress tests in Element Web and how we use Cypress to write tests.
|
||||
It aims to cover:
|
||||
* How to run the tests yourself
|
||||
* How the tests work
|
||||
* How to write great Cypress tests
|
||||
* Visual testing
|
||||
|
||||
- How to run the tests yourself
|
||||
- How the tests work
|
||||
- How to write great Cypress tests
|
||||
- Visual testing
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Our Cypress tests run automatically as part of our CI along with our other tests,
|
||||
on every pull request and on every merge to develop & master.
|
||||
|
||||
|
@ -43,6 +46,7 @@ yarn run test:cypress:open
|
|||
```
|
||||
|
||||
## How the Tests Work
|
||||
|
||||
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
|
||||
as is typical for Cypress tests. Likewise, tests live in `cypress/e2e`.
|
||||
|
||||
|
@ -68,6 +72,7 @@ with each instance in a separate directory named after its ID. These logs are re
|
|||
at the start of each test run.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Mostly this is the same advice as for writing any other Cypress test: the Cypress
|
||||
docs are well worth a read if you're not already familiar with Cypress testing, eg.
|
||||
https://docs.cypress.io/guides/references/best-practices. To avoid your tests being
|
||||
|
@ -75,11 +80,12 @@ flaky it is also recommended to give https://docs.cypress.io/guides/core-concept
|
|||
a read.
|
||||
|
||||
### Getting a Synapse
|
||||
The key difference is in starting Synapse instances. Tests use this plugin via
|
||||
|
||||
The key difference is in starting Synapse instances. Tests use this plugin via
|
||||
`cy.startSynapse()` to provide a Synapse instance to log into:
|
||||
|
||||
```javascript
|
||||
cy.startSynapse("consent").then(result => {
|
||||
cy.startSynapse("consent").then((result) => {
|
||||
synapse = result;
|
||||
});
|
||||
```
|
||||
|
@ -96,32 +102,38 @@ Synapse instance for each test suite, i.e. in `before()`, and then tear it down
|
|||
|
||||
To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
|
||||
object you received when starting it.
|
||||
|
||||
```javascript
|
||||
cy.stopSynapse(synapse);
|
||||
```
|
||||
|
||||
### Synapse Config Templates
|
||||
|
||||
When a Synapse instance is started, it's given a config generated from one of the config
|
||||
templates in `cypress/plugins/synapsedocker/templates`. There are a couple of special files
|
||||
in these templates:
|
||||
* `homeserver.yaml`:
|
||||
Template substitution happens in this file. Template variables are:
|
||||
* `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
||||
* `MACAROON_SECRET_KEY`: Generated each time for security
|
||||
* `FORM_SECRET`: Generated each time for security
|
||||
* `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
||||
* `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
||||
Config templates should not contain a signing key and instead assume that one will exist
|
||||
in this file.
|
||||
|
||||
- `homeserver.yaml`:
|
||||
Template substitution happens in this file. Template variables are:
|
||||
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
||||
- `MACAROON_SECRET_KEY`: Generated each time for security
|
||||
- `FORM_SECRET`: Generated each time for security
|
||||
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
||||
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
||||
Config templates should not contain a signing key and instead assume that one will exist
|
||||
in this file.
|
||||
|
||||
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
|
||||
in a template can be referenced in the config as `/data/foo.html`.
|
||||
|
||||
### Logging In
|
||||
|
||||
There exists a basic utility to start the app with a random user already logged in:
|
||||
|
||||
```javascript
|
||||
cy.initTestUser(synapse, "Jeff");
|
||||
```
|
||||
|
||||
It takes the SynapseInstance you received from `startSynapse` and a display name for your test user.
|
||||
This custom command will register a random userId using the registrationSecret with a random password
|
||||
and the given display name. The returned Chainable will contain details about the credentials for if
|
||||
|
@ -132,20 +144,24 @@ The internals of how this custom command run may be swapped out later,
|
|||
but the signature can be maintained for simpler maintenance.
|
||||
|
||||
### Joining a Room
|
||||
|
||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
||||
way to do this may be to get an access token for the user and use this to create a room with the REST
|
||||
API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
|
||||
|
||||
### Convenience APIs
|
||||
|
||||
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
|
||||
creation that can be called to set up tests.
|
||||
|
||||
### Using matrix-js-sdk
|
||||
|
||||
Due to the way we run the Cypress tests in CI, at this time you can only use the matrix-js-sdk module
|
||||
exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
|
||||
This may be revisited in the future.
|
||||
|
||||
## Good Test Hygiene
|
||||
|
||||
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
|
||||
already familiar with Cypress.
|
||||
|
||||
|
@ -158,11 +174,11 @@ already familiar with Cypress.
|
|||
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
|
||||
all assertions are retired until they either pass or time out, so you should never need to
|
||||
manually wait for an element.
|
||||
* For example, for asserting about editing an already-edited message, you can't wait for the
|
||||
- For example, for asserting about editing an already-edited message, you can't wait for the
|
||||
'edited' element to appear as there was already one there, but you can assert that the body
|
||||
of the message is what is should be after the second edit and this assertion will pass once
|
||||
it becomes true. You can then assert that the 'edited' element is still in the DOM.
|
||||
* You can also wait for other things like network requests in the
|
||||
- You can also wait for other things like network requests in the
|
||||
browser to complete (https://docs.cypress.io/guides/guides/network-requests#Waiting).
|
||||
Needing to wait for things can also be because of race conditions in the app itself, which ideally
|
||||
shouldn't be there!
|
||||
|
@ -171,6 +187,7 @@ This is a small selection - the Cypress best practices guide, linked above, has
|
|||
should generally try to adhere to them.
|
||||
|
||||
## Percy Visual Testing
|
||||
|
||||
We also support visual testing via Percy, this extracts the DOM from Cypress and renders it using custom renderers
|
||||
for Safari, Firefox, Chrome & Edge, allowing us to spot visual regressions before they become release regressions.
|
||||
Each `cy.percySnapshot()` call results in 8 screenshots (4 browsers, 2 sizes) this can quickly be exhausted and
|
||||
|
@ -178,4 +195,3 @@ so we only run Percy testing on `develop` and PRs which are labelled `X-Needs-Pe
|
|||
|
||||
To record a snapshot use `cy.percySnapshot()`, you may have to pass `percyCSS` into the 2nd argument to hide certain
|
||||
elements which contain dynamic/generated data to avoid them cause false positives in the Percy screenshot diffs.
|
||||
|
||||
|
|
|
@ -1,37 +1,38 @@
|
|||
# Composer Features
|
||||
|
||||
## Auto Complete
|
||||
|
||||
- Hitting tab tries to auto-complete the word before the caret as a room member
|
||||
- If no matching name is found, a visual bell is shown
|
||||
- @ + a letter opens auto complete for members starting with the given letter
|
||||
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
|
||||
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
|
||||
- # + a letter opens auto complete for rooms starting with the given letter
|
||||
- : open auto complete for emoji
|
||||
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
|
||||
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
|
||||
- Hitting tab tries to auto-complete the word before the caret as a room member
|
||||
- If no matching name is found, a visual bell is shown
|
||||
- @ + a letter opens auto complete for members starting with the given letter
|
||||
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
|
||||
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
|
||||
- # + a letter opens auto complete for rooms starting with the given letter
|
||||
- : open auto complete for emoji
|
||||
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
|
||||
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
|
||||
wrapping around at the end after reverting to the typed text first.
|
||||
|
||||
## Formatting
|
||||
|
||||
- When selecting text, a formatting bar appears above the selection.
|
||||
- The formatting bar allows to format the selected test as:
|
||||
- When selecting text, a formatting bar appears above the selection.
|
||||
- The formatting bar allows to format the selected test as:
|
||||
bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
|
||||
- Formatting is applied as markdown syntax.
|
||||
- Hitting ctrl/cmd+B also marks the selected text as bold
|
||||
- Hitting ctrl/cmd+I also marks the selected text as italic
|
||||
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
|
||||
- Formatting is applied as markdown syntax.
|
||||
- Hitting ctrl/cmd+B also marks the selected text as bold
|
||||
- Hitting ctrl/cmd+I also marks the selected text as italic
|
||||
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
|
||||
|
||||
## Misc
|
||||
|
||||
- When hitting the arrow-up button while having the caret at the start in the composer,
|
||||
- When hitting the arrow-up button while having the caret at the start in the composer,
|
||||
the last message sent by the syncing user is edited.
|
||||
- Clicking a display name on an event in the timeline inserts a user pill into the composer
|
||||
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
|
||||
- Typing in the composer sends typing notifications in the room
|
||||
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
|
||||
- Pressing shift+enter inserts a line break
|
||||
- Pressing enter sends the message.
|
||||
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
|
||||
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
|
||||
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.
|
||||
- Clicking a display name on an event in the timeline inserts a user pill into the composer
|
||||
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
|
||||
- Typing in the composer sends typing notifications in the room
|
||||
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
|
||||
- Pressing shift+enter inserts a line break
|
||||
- Pressing enter sends the message.
|
||||
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
|
||||
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
|
||||
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.
|
||||
|
|
|
@ -17,7 +17,7 @@ Let's say we want to close a menu when the correct keys were pressed:
|
|||
```ts
|
||||
const onKeyDown = (ev: KeyboardEvent): void => {
|
||||
let handled = true;
|
||||
const action = getKeyBindingManager().getAccessibilityAction(ev)
|
||||
const action = getKeyBindingManager().getAccessibilityAction(ev);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Escape:
|
||||
closeMenu();
|
||||
|
@ -26,12 +26,12 @@ const onKeyDown = (ev: KeyboardEvent): void => {
|
|||
handled = false;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (handled) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Managing keyboard shortcuts
|
||||
|
|
|
@ -6,6 +6,7 @@ Each .svg exports a `ReactComponent` at the named export `Icon`.
|
|||
Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component.
|
||||
|
||||
eg
|
||||
|
||||
```
|
||||
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';
|
||||
|
||||
|
|
|
@ -6,23 +6,25 @@ instructions on setting up Jitsi.
|
|||
The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html`
|
||||
which takes several parameters:
|
||||
|
||||
*Query string*:
|
||||
* `widgetId`: The ID of the widget. This is needed for communication back to the
|
||||
react-sdk.
|
||||
* `parentUrl`: The URL of the parent window. This is also needed for
|
||||
communication back to the react-sdk.
|
||||
_Query string_:
|
||||
|
||||
*Hash/fragment (formatted as a query string)*:
|
||||
* `conferenceDomain`: The domain to connect Jitsi Meet to.
|
||||
* `conferenceId`: The room or conference ID to connect Jitsi Meet to.
|
||||
* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
|
||||
be present, should default to `false`.
|
||||
* `displayName`: The display name of the user viewing the widget. May not
|
||||
be present or could be null.
|
||||
* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
|
||||
not be present or could be null.
|
||||
* `userId`: The MXID of the user viewing the widget. May not be present or could
|
||||
be null.
|
||||
- `widgetId`: The ID of the widget. This is needed for communication back to the
|
||||
react-sdk.
|
||||
- `parentUrl`: The URL of the parent window. This is also needed for
|
||||
communication back to the react-sdk.
|
||||
|
||||
_Hash/fragment (formatted as a query string)_:
|
||||
|
||||
- `conferenceDomain`: The domain to connect Jitsi Meet to.
|
||||
- `conferenceId`: The room or conference ID to connect Jitsi Meet to.
|
||||
- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
|
||||
be present, should default to `false`.
|
||||
- `displayName`: The display name of the user viewing the widget. May not
|
||||
be present or could be null.
|
||||
- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
|
||||
not be present or could be null.
|
||||
- `userId`: The MXID of the user viewing the widget. May not be present or could
|
||||
be null.
|
||||
|
||||
The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently
|
||||
being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`.
|
||||
|
|
|
@ -10,7 +10,7 @@ implementation, such as the `RoomEchoChamber` (which handles echoable details of
|
|||
|
||||
Anything that can be locally echoed will be provided by the `GenericEchoChamber` implementation.
|
||||
The echo chamber will also need to deal with external changes, and has full control over whether
|
||||
or not something has successfully been echoed.
|
||||
or not something has successfully been echoed.
|
||||
|
||||
An `EchoContext` is provided to echo chambers (usually with a matching type: `RoomEchoContext`
|
||||
gets provided to a `RoomEchoChamber` for example) with details about their intended area of
|
||||
|
@ -21,7 +21,7 @@ The `EchoStore` manages echo chamber instances, builds contexts, and is generall
|
|||
accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things
|
||||
tidy, this is an intentional design decision.
|
||||
|
||||
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
|
||||
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
|
||||
`EventEmitter`. Whenables are ways of actioning a changing condition without having to deal
|
||||
with listeners being torn down. Once the reference count of the Whenable causes garbage collection,
|
||||
the Whenable's listeners will also be torn down. This is accelerated by the `IDestroyable` interface
|
||||
|
@ -36,4 +36,3 @@ mechanisms.
|
|||
|
||||
The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left)
|
||||
is set up, where the dialog then drives through the contexts and transactions.
|
||||
|
||||
|
|
|
@ -5,11 +5,12 @@ It's so complicated it needs its own README.
|
|||
![](img/RoomListStore2.png)
|
||||
|
||||
Legend:
|
||||
* Orange = External event.
|
||||
* Purple = Deterministic flow.
|
||||
* Green = Algorithm definition.
|
||||
* Red = Exit condition/point.
|
||||
* Blue = Process definition.
|
||||
|
||||
- Orange = External event.
|
||||
- Purple = Deterministic flow.
|
||||
- Green = Algorithm definition.
|
||||
- Red = Exit condition/point.
|
||||
- Blue = Process definition.
|
||||
|
||||
## Algorithms involved
|
||||
|
||||
|
@ -22,7 +23,6 @@ Behaviour of the overall room list (sticky rooms, etc) are determined by the gen
|
|||
class. Here is where much of the coordination from the room list store is done to figure out which list
|
||||
algorithm to call, instead of having all the logic in the room list store itself.
|
||||
|
||||
|
||||
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
|
||||
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
|
||||
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
|
||||
|
@ -68,14 +68,14 @@ simply get the manual sorting algorithm applied to them with no further involvem
|
|||
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
|
||||
relative (perceived) importance to the user:
|
||||
|
||||
* **Red**: The room has unread mentions waiting for the user.
|
||||
* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
|
||||
messages which cause a push notification or badge count. Typically, this is the default as rooms get
|
||||
set to 'All Messages'.
|
||||
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
||||
a badge/notification count (or 'Mentions Only'/'Muted').
|
||||
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
||||
last read it.
|
||||
- **Red**: The room has unread mentions waiting for the user.
|
||||
- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
|
||||
messages which cause a push notification or badge count. Typically, this is the default as rooms get
|
||||
set to 'All Messages'.
|
||||
- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
||||
a badge/notification count (or 'Mentions Only'/'Muted').
|
||||
- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
|
||||
last read it.
|
||||
|
||||
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
||||
above bold, etc.
|
||||
|
|
|
@ -8,7 +8,6 @@ During an onscroll event, we check whether we're getting close to the top or bot
|
|||
|
||||
ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline.
|
||||
|
||||
|
||||
## BACAT (Bottom-Aligned, Clipped-At-Top) scrolling
|
||||
|
||||
BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842.
|
||||
|
|
|
@ -5,23 +5,22 @@ different values for a setting at particular levels of interest. For example, a
|
|||
they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity
|
||||
of dealing with the different levels and exposes easy to use getters and setters.
|
||||
|
||||
|
||||
## Levels
|
||||
|
||||
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
|
||||
order of priority, are:
|
||||
* `device` - The current user's device
|
||||
* `room-device` - The current user's device, but only when in a specific room
|
||||
* `room-account` - The current user's account, but only when in a specific room
|
||||
* `account` - The current user's account
|
||||
* `room` - A specific room (setting for all members of the room)
|
||||
* `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
|
||||
* `default` - The hardcoded default for the settings
|
||||
|
||||
- `device` - The current user's device
|
||||
- `room-device` - The current user's device, but only when in a specific room
|
||||
- `room-account` - The current user's account, but only when in a specific room
|
||||
- `account` - The current user's account
|
||||
- `room` - A specific room (setting for all members of the room)
|
||||
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
|
||||
- `default` - The hardcoded default for the settings
|
||||
|
||||
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
||||
that room administrators cannot force account-only settings upon participants.
|
||||
|
||||
|
||||
## Settings
|
||||
|
||||
Settings are the different options a user may set or experience in the application. These are pre-defined in
|
||||
|
@ -29,6 +28,7 @@ Settings are the different options a user may set or experience in the applicati
|
|||
|
||||
Settings that support the config level can be set in the config file under the `setting_defaults` key (note that some
|
||||
settings, like the "theme" setting, are special cased in the config file):
|
||||
|
||||
```json5
|
||||
{
|
||||
...
|
||||
|
@ -56,13 +56,14 @@ target level.
|
|||
Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a
|
||||
clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue
|
||||
although there are circumstances where this changes. An example of a safe call is:
|
||||
|
||||
```javascript
|
||||
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
|
||||
if (isSupported) {
|
||||
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
|
||||
if (canSetValue) {
|
||||
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
|
||||
}
|
||||
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
|
||||
if (canSetValue) {
|
||||
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -73,19 +74,14 @@ instance, the component which allows changing the setting may be hidden conditio
|
|||
|
||||
Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The
|
||||
`SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
|
||||
```html
|
||||
<SettingsFlag name="theSettingId"
|
||||
level={SettingsLevel.ROOM}
|
||||
roomId="!curbf:matrix.org"
|
||||
label={_td("Your label here")} // optional, if falsey then the `SettingsStore` will be used
|
||||
onChange={function(newValue) { }} // optional, called after saving
|
||||
isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false
|
||||
manualSave={false} // if true, saving is delayed. You will need to call .save() on this component
|
||||
|
||||
// Options for radio buttons
|
||||
group="your-radio-group" // this enables radio button support
|
||||
value="yourValueHere" // the value for this particular option
|
||||
/>
|
||||
```html
|
||||
<SettingsFlag name="theSettingId" level={SettingsLevel.ROOM} roomId="!curbf:matrix.org" label={_td("Your label here")}
|
||||
// optional, if falsey then the `SettingsStore` will be used onChange={function(newValue) { }} // optional, called after
|
||||
saving isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false manualSave={false}
|
||||
// if true, saving is delayed. You will need to call .save() on this component // Options for radio buttons
|
||||
group="your-radio-group" // this enables radio button support value="yourValueHere" // the value for this particular
|
||||
option />
|
||||
```
|
||||
|
||||
### Getting the display name for a setting
|
||||
|
@ -93,16 +89,16 @@ Where possible, the `SettingsFlag` component should be used to set simple "flip-
|
|||
Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated
|
||||
for you. If a display name cannot be found, it will return `null`.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually
|
||||
a feature flag is used when a portion of the application is under development or not ready for full release yet, such
|
||||
as new functionality or experimental ideas. In these cases, the feature name *should* be named with the `feature_*`
|
||||
as new functionality or experimental ideas. In these cases, the feature name _should_ be named with the `feature_*`
|
||||
convention and must be tagged with `isFeature: true` in the setting definition. By doing so, the feature will automatically
|
||||
appear in the "labs" section of the user's settings.
|
||||
|
||||
Features can be controlled at the config level using the following structure:
|
||||
|
||||
```json
|
||||
"features": {
|
||||
"feature_lazyloading": true
|
||||
|
@ -144,7 +140,6 @@ additional steps to actually enable notifications.
|
|||
|
||||
For more information, see `src/settings/controllers/SettingController.ts`.
|
||||
|
||||
|
||||
## Local echo
|
||||
|
||||
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a
|
||||
|
@ -160,7 +155,6 @@ SettingsStore.setValue(...).then(() => {
|
|||
SettingsStore.getValue(...); // this will return the value set in `setValue` above.
|
||||
```
|
||||
|
||||
|
||||
## Watching for changes
|
||||
|
||||
Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the
|
||||
|
@ -174,12 +168,11 @@ An example of a watcher in action would be:
|
|||
|
||||
```javascript
|
||||
class MyComponent extends React.Component {
|
||||
|
||||
settingWatcherRef = null;
|
||||
|
||||
componentWillMount() {
|
||||
const callback = (settingName, roomId, level, newValAtLevel, newVal) => {
|
||||
this.setState({color: newVal});
|
||||
this.setState({ color: newVal });
|
||||
};
|
||||
this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", callback);
|
||||
}
|
||||
|
@ -190,7 +183,6 @@ class MyComponent extends React.Component {
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
# Maintainers Reference
|
||||
|
||||
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is
|
||||
|
|
|
@ -13,12 +13,12 @@ It exposes a function over a postMessage API, when sent an object with the match
|
|||
|
||||
```json5
|
||||
{
|
||||
"imgSrc": "", // the src of the image to display in the download link
|
||||
"imgStyle": "", // the style to apply to the image
|
||||
"style": "", // the style to apply to the download link
|
||||
"download": "", // download attribute to pass to the <a/> tag
|
||||
"textContent": "", // the text to put inside the download link
|
||||
"blob": "", // the data blob to wrap in an object url and allow the user to download
|
||||
imgSrc: "", // the src of the image to display in the download link
|
||||
imgStyle: "", // the style to apply to the image
|
||||
style: "", // the style to apply to the download link
|
||||
download: "", // download attribute to pass to the <a/> tag
|
||||
textContent: "", // the text to put inside the download link
|
||||
blob: "", // the data blob to wrap in an object url and allow the user to download
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -4,24 +4,25 @@ Rooms can have a default widget layout to auto-pin certain widgets, make the con
|
|||
sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key).
|
||||
|
||||
Full example content:
|
||||
|
||||
```json5
|
||||
{
|
||||
"widgets": {
|
||||
"first-widget-id": {
|
||||
"container": "top",
|
||||
"index": 0,
|
||||
"width": 60,
|
||||
"height": 40
|
||||
widgets: {
|
||||
"first-widget-id": {
|
||||
container: "top",
|
||||
index: 0,
|
||||
width: 60,
|
||||
height: 40,
|
||||
},
|
||||
"second-widget-id": {
|
||||
container: "right",
|
||||
},
|
||||
},
|
||||
"second-widget-id": {
|
||||
"container": "right"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As shown, there are two containers possible for widgets. These containers have different behaviour
|
||||
and interpret the other options differently.
|
||||
and interpret the other options differently.
|
||||
|
||||
## `top` container
|
||||
|
||||
|
@ -32,7 +33,7 @@ therefore fewer messages can be shown).
|
|||
The `index` for a widget determines which order the widgets show up in from left to right. Widgets
|
||||
without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined
|
||||
without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top
|
||||
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
|
||||
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
|
||||
represent leftmost widgets.
|
||||
|
||||
The `width` is relative width within the container in percentage points. This will be clamped to a
|
||||
|
@ -43,7 +44,7 @@ attempt to show them at 33% width each.
|
|||
Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning
|
||||
hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions.
|
||||
|
||||
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
|
||||
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
|
||||
capabilities in future containers. The top container will take the tallest `height` and use that for
|
||||
the height of the whole container, and thus all widgets in that container. The `height` is relative
|
||||
to the container, like with `width`, meaning that 100% will consume as much space as the client is
|
||||
|
|
512
package.json
512
package.json
|
@ -1,261 +1,261 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.62.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-react-sdk"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"res",
|
||||
"src",
|
||||
"scripts",
|
||||
"git-revision.txt",
|
||||
"docs",
|
||||
"header",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
".stylelintrc.js"
|
||||
],
|
||||
"main": "./src/index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_lib_main": "./lib/index.ts",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"matrix_i18n_extra_translation_funcs": [
|
||||
"newTranslatableError"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"i18n": "matrix-gen-i18n",
|
||||
"prunei18n": "matrix-prune-i18n",
|
||||
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||
"make-component": "node scripts/make-react-component.js",
|
||||
"rethemendex": "res/css/rethemendex.sh",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js,.tsx\" src",
|
||||
"build:types": "tsc --emitDeclarationOnly --jsx react",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all",
|
||||
"start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build",
|
||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --max-warnings 0 src test cypress && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src test cypress",
|
||||
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p cypress",
|
||||
"lint:style": "stylelint \"res/css/**/*.pcss\"",
|
||||
"test": "jest",
|
||||
"test:cypress": "cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/analytics-events": "^0.3.0",
|
||||
"@matrix-org/matrix-wysiwyg": "^0.9.0",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"await-lock": "^2.1.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
"classnames": "^2.2.6",
|
||||
"commonmark": "^0.30.0",
|
||||
"counterpart": "^0.18.6",
|
||||
"diff-dom": "^4.2.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"emojibase": "6.1.0",
|
||||
"emojibase-data": "7.0.1",
|
||||
"emojibase-regex": "6.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "10.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-visible": "^5.2.0",
|
||||
"gfm.css": "^1.1.2",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"is-ip": "^3.1.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-element": "4.0.0-beta.4",
|
||||
"linkify-string": "4.0.0-beta.4",
|
||||
"linkifyjs": "4.0.0-beta.4",
|
||||
"lodash": "^4.17.20",
|
||||
"maplibre-gl": "^1.15.2",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.1.1",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"parse5": "^6.0.1",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.36.0",
|
||||
"qrcode": "1.5.1",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.2.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.3.2",
|
||||
"tar-js": "^0.3.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"what-input": "^5.2.10",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/parser": "^7.12.11",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.12",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@peculiar/webcrypto": "^1.4.1",
|
||||
"@percy/cli": "^1.11.0",
|
||||
"@percy/cypress": "^3.1.2",
|
||||
"@sinonjs/fake-timers": "^9.1.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
"@types/counterpart": "^0.18.1",
|
||||
"@types/css-font-loading-module": "^0.0.7",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
"@types/enzyme": "^3.10.9",
|
||||
"@types/escape-html": "^1.0.1",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/geojson": "^7946.0.8",
|
||||
"@types/jest": "^29.2.1",
|
||||
"@types/katex": "^0.14.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/modernizr": "^3.5.3",
|
||||
"@types/node": "^16",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/parse5": "^6.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "17.0.49",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "^2.3.1",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"allchange": "^1.1.0",
|
||||
"axe-core": "4.4.3",
|
||||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^7.0.0",
|
||||
"chokidar": "^3.5.1",
|
||||
"cypress": "^11.0.0",
|
||||
"cypress-axe": "^1.0.0",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-deprecate": "^0.7.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "0.9.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"fs-extra": "^11.0.0",
|
||||
"glob": "^8.0.0",
|
||||
"jest": "^29.2.2",
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"jest-mock": "^29.2.2",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"matrix-web-i18n": "^1.3.0",
|
||||
"node-fetch": "2",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"prettier": "2.8.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"typescript": "4.9.3",
|
||||
"walk": "^2.3.14"
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||
],
|
||||
"globalSetup": "<rootDir>/test/globalSetup.js",
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/test/setupTests.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
|
||||
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.62.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/matrix-org/matrix-react-sdk"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!matrix-js-sdk).+$"
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"lib",
|
||||
"res",
|
||||
"src",
|
||||
"scripts",
|
||||
"git-revision.txt",
|
||||
"docs",
|
||||
"header",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"package.json",
|
||||
".stylelintrc.js"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}"
|
||||
"main": "./src/index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_lib_main": "./lib/index.ts",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"matrix_i18n_extra_translation_funcs": [
|
||||
"newTranslatableError"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
}
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
"i18n": "matrix-gen-i18n",
|
||||
"prunei18n": "matrix-prune-i18n",
|
||||
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
|
||||
"make-component": "node scripts/make-react-component.js",
|
||||
"rethemendex": "res/css/rethemendex.sh",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js,.tsx\" src",
|
||||
"build:types": "tsc --emitDeclarationOnly --jsx react",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all",
|
||||
"start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build",
|
||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --max-warnings 0 src test cypress && prettier --check .",
|
||||
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src test cypress",
|
||||
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p cypress",
|
||||
"lint:style": "stylelint \"res/css/**/*.pcss\"",
|
||||
"test": "jest",
|
||||
"test:cypress": "cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/analytics-events": "^0.3.0",
|
||||
"@matrix-org/matrix-wysiwyg": "^0.9.0",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"await-lock": "^2.1.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"cheerio": "^1.0.0-rc.9",
|
||||
"classnames": "^2.2.6",
|
||||
"commonmark": "^0.30.0",
|
||||
"counterpart": "^0.18.6",
|
||||
"diff-dom": "^4.2.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"emojibase": "6.1.0",
|
||||
"emojibase-data": "7.0.1",
|
||||
"emojibase-regex": "6.0.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "10.0.5",
|
||||
"flux": "4.0.3",
|
||||
"focus-visible": "^5.2.0",
|
||||
"gfm.css": "^1.1.2",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
"html-entities": "^2.0.0",
|
||||
"is-ip": "^3.1.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-element": "4.0.0-beta.4",
|
||||
"linkify-string": "4.0.0-beta.4",
|
||||
"linkifyjs": "4.0.0-beta.4",
|
||||
"lodash": "^4.17.20",
|
||||
"maplibre-gl": "^1.15.2",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-events-sdk": "0.0.1",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.1.1",
|
||||
"minimist": "^1.2.5",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"parse5": "^6.0.1",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.36.0",
|
||||
"qrcode": "1.5.1",
|
||||
"re-resizable": "^6.9.0",
|
||||
"react": "17.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-blurhash": "^0.2.0",
|
||||
"react-dom": "17.0.2",
|
||||
"react-focus-lock": "^2.5.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.3.2",
|
||||
"tar-js": "^0.3.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"what-input": "^5.2.10",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/parser": "^7.12.11",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-react": "^7.12.10",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@babel/traverse": "^7.12.12",
|
||||
"@casualbot/jest-sonar-reporter": "^2.2.5",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
|
||||
"@peculiar/webcrypto": "^1.4.1",
|
||||
"@percy/cli": "^1.11.0",
|
||||
"@percy/cypress": "^3.1.2",
|
||||
"@sinonjs/fake-timers": "^9.1.2",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/commonmark": "^0.27.4",
|
||||
"@types/counterpart": "^0.18.1",
|
||||
"@types/css-font-loading-module": "^0.0.7",
|
||||
"@types/diff-match-patch": "^1.0.32",
|
||||
"@types/enzyme": "^3.10.9",
|
||||
"@types/escape-html": "^1.0.1",
|
||||
"@types/file-saver": "^2.0.3",
|
||||
"@types/flux": "^3.1.9",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/geojson": "^7946.0.8",
|
||||
"@types/jest": "^29.2.1",
|
||||
"@types/katex": "^0.14.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/modernizr": "^3.5.3",
|
||||
"@types/node": "^16",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/parse5": "^6.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "17.0.49",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/react-test-renderer": "^17.0.1",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "^2.3.1",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"allchange": "^1.1.0",
|
||||
"axe-core": "4.4.3",
|
||||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^7.0.0",
|
||||
"chokidar": "^3.5.1",
|
||||
"cypress": "^11.0.0",
|
||||
"cypress-axe": "^1.0.0",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-deprecate": "^0.7.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-matrix-org": "0.9.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"fetch-mock-jest": "^1.5.1",
|
||||
"fs-extra": "^11.0.0",
|
||||
"glob": "^8.0.0",
|
||||
"jest": "^29.2.2",
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"jest-mock": "^29.2.2",
|
||||
"jest-raw-loader": "^1.0.1",
|
||||
"matrix-mock-request": "^2.5.0",
|
||||
"matrix-web-i18n": "^1.3.0",
|
||||
"node-fetch": "2",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"prettier": "2.8.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"stylelint": "^14.9.1",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"typescript": "4.9.3",
|
||||
"walk": "^2.3.14"
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
],
|
||||
"testEnvironment": "jsdom",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||
],
|
||||
"globalSetup": "<rootDir>/test/globalSetup.js",
|
||||
"setupFiles": [
|
||||
"jest-canvas-mock"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/test/setupTests.js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||
"\\$webapp/i18n/languages.json": "<rootDir>/__mocks__/languages.json",
|
||||
"decoderWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"decoderWorker\\.min\\.wasm": "<rootDir>/__mocks__/empty.js",
|
||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"workers/(.+)\\.worker\\.ts": "<rootDir>/__mocks__/workerMock.js",
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"RecorderWorklet": "<rootDir>/__mocks__/empty.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"/node_modules/(?!matrix-js-sdk).+$"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text-summary",
|
||||
"lcov"
|
||||
],
|
||||
"testResultsProcessor": "@casualbot/jest-sonar-reporter"
|
||||
},
|
||||
"@casualbot/jest-sonar-reporter": {
|
||||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
subprojects:
|
||||
matrix-js-sdk:
|
||||
includeByDefault: false
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue