mirror of
https://github.com/element-hq/element-web
synced 2024-11-21 16:55:34 +03:00
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:
|
||||
|
|
4
.github/renovate.json
vendored
4
.github/renovate.json
vendored
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"]
|
||||
}
|
||||
|
|
15
.github/workflows/cypress.yaml
vendored
15
.github/workflows/cypress.yaml
vendored
|
@ -2,7 +2,7 @@
|
|||
name: Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Element Web - Build" ]
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
|
@ -75,7 +75,8 @@ jobs:
|
|||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment: Cypress
|
||||
environment:
|
||||
Cypress
|
||||
#strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
|
@ -107,11 +108,12 @@ jobs:
|
|||
# to run the tests, so use chrome.
|
||||
browser: chrome
|
||||
start: npx serve -p 8080 webapp
|
||||
wait-on: 'http://localhost:8080'
|
||||
record: true
|
||||
wait-on: "http://localhost:8080"
|
||||
record:
|
||||
true
|
||||
#parallel: true
|
||||
#command-prefix: 'yarn percy exec --parallel --'
|
||||
command-prefix: 'yarn percy exec --'
|
||||
command-prefix: "yarn percy exec --"
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
# pass the Dashboard record key as an environment variable
|
||||
|
@ -141,7 +143,8 @@ jobs:
|
|||
# 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_PULL_REQUEST:
|
||||
${{ needs.prepare.outputs.pr_id }}
|
||||
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
|
||||
|
|
8
.github/workflows/element-web.yaml
vendored
8
.github/workflows/element-web.yaml
vendored
|
@ -3,11 +3,11 @@
|
|||
# as an artifact and run integration tests.
|
||||
name: Element Web - Build
|
||||
on:
|
||||
pull_request: { }
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
types: [upstream-sdk-notify]
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
|
@ -21,7 +21,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
|
|
4
.github/workflows/i18n_check.yml
vendored
4
.github/workflows/i18n_check.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: i18n Check
|
||||
on:
|
||||
workflow_call: { }
|
||||
workflow_call: {}
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -30,7 +30,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
|
|
2
.github/workflows/netlify.yaml
vendored
2
.github/workflows/netlify.yaml
vendored
|
@ -3,7 +3,7 @@
|
|||
name: Upload Preview Build to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Element Web - Build" ]
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
|
|
4
.github/workflows/notify-element-web.yml
vendored
4
.github/workflows/notify-element-web.yml
vendored
|
@ -1,9 +1,9 @@
|
|||
name: Notify element-web
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
branches: [develop]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
types: [upstream-sdk-notify]
|
||||
jobs:
|
||||
notify-element-web:
|
||||
name: "Notify Element Web"
|
||||
|
|
2
.github/workflows/pull_request.yaml
vendored
2
.github/workflows/pull_request.yaml
vendored
|
@ -1,7 +1,7 @@
|
|||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
jobs:
|
||||
action:
|
||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -1,7 +1,7 @@
|
|||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [published]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
npm:
|
||||
|
|
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
|
@ -1,7 +1,7 @@
|
|||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
|
|
18
.github/workflows/static_analysis.yaml
vendored
18
.github/workflows/static_analysis.yaml
vendored
|
@ -1,10 +1,10 @@
|
|||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
types: [upstream-sdk-notify]
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
|
@ -18,7 +18,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
|
@ -48,8 +48,8 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
args:
|
||||
- '--strict --noImplicitAny'
|
||||
- '--noImplicitAny'
|
||||
- "--strict --noImplicitAny"
|
||||
- "--noImplicitAny"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
|
@ -103,7 +103,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
|
@ -120,7 +120,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
|
@ -137,7 +137,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
|
|
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
|
@ -1,10 +1,10 @@
|
|||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
types: [upstream-sdk-notify]
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
|
@ -20,7 +20,7 @@ jobs:
|
|||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
cache: "yarn"
|
||||
|
||||
- name: Run tests
|
||||
run: "./scripts/ci/app-tests.sh"
|
||||
|
|
2
.github/workflows/upgrade_dependencies.yml
vendored
2
.github/workflows/upgrade_dependencies.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
workflow_dispatch: {}
|
||||
jobs:
|
||||
upgrade:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
|
||||
|
|
|
@ -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, {
|
||||
"scss/at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
// https://github.com/vector-im/element-web/issues/10544
|
||||
"ignoreAtRules": ["define-mixin"],
|
||||
}],
|
||||
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,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
16850
CHANGELOG.md
16850
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
|
||||
|
|
75
README.md
75
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
|
||||
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,17 +28,17 @@ 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
|
||||
|
||||
- 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.
|
||||
|
||||
|
@ -52,16 +52,17 @@ 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:
|
||||
|
||||
- All new components:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||
* Element-specific 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
|
||||
- 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:
|
||||
- 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:
|
||||
|
@ -75,55 +76,55 @@ 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:
|
||||
- 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
|
||||
- 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.
|
||||
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
|
||||
- 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
|
||||
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.
|
||||
|
@ -135,14 +136,12 @@ 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.
|
||||
|
||||
|
|
|
@ -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": [
|
||||
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,9 +57,11 @@ 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.contains(".mx_EventTile_body", "Hey!")
|
||||
.closest(".mx_EventTile")
|
||||
.within(() => {
|
||||
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
|
||||
});
|
||||
|
||||
|
@ -72,11 +74,12 @@ 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 bobJoin = function (this: CryptoTestContext) {
|
||||
cy.window({ log: false })
|
||||
.then(async (win) => {
|
||||
const bobRooms = this.bob.getRooms();
|
||||
if (!bobRooms.length) {
|
||||
await new Promise<void>(resolve => {
|
||||
await new Promise<void>((resolve) => {
|
||||
const onMembership = (_event) => {
|
||||
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
resolve();
|
||||
|
@ -84,7 +87,8 @@ const bobJoin = function(this: CryptoTestContext) {
|
|||
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
|
||||
});
|
||||
|
||||
|
@ -93,7 +97,7 @@ const bobJoin = function(this: CryptoTestContext) {
|
|||
|
||||
/** 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,7 +107,8 @@ function autoJoin(client: MatrixClient) {
|
|||
}
|
||||
|
||||
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
||||
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
||||
return cy.wrap(
|
||||
new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
verifier.off("show_sas", onShowSas);
|
||||
event.confirm();
|
||||
|
@ -114,10 +119,11 @@ const handleVerificationRequest = (request: VerificationRequest): Chainable<Emoj
|
|||
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) => {
|
||||
cy.wrap(bobsVerificationRequestPromise)
|
||||
.then((verificationRequest: VerificationRequest) => {
|
||||
verificationRequest.accept();
|
||||
return verificationRequest;
|
||||
}).as("bobsVerificationRequest");
|
||||
})
|
||||
.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) => {
|
||||
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,96 +150,92 @@ 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]) => {
|
||||
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.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
|
||||
|
||||
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]) => {
|
||||
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', {
|
||||
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(() => {
|
||||
})
|
||||
.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]) => {
|
||||
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 () => {
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`)
|
||||
.should("exist")
|
||||
.then(async () => {
|
||||
await targetUser.leave(roomId);
|
||||
}).then(() => {
|
||||
})
|
||||
.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]) => {
|
||||
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 => {
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should("exist");
|
||||
cy.getClient()
|
||||
.then(async (client) => {
|
||||
await client.ban(roomId, targetUserId);
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should no-op if the target was never a room member", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
cy.all([cy.get<MatrixClient>("@bob"), cy.get<string>("@roomId"), cy.get<{}>("@integrationManager")]).then(
|
||||
([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
|
@ -246,6 +243,7 @@ describe("Integration Manager: Kick", () => {
|
|||
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({
|
||||
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");
|
||||
})
|
||||
.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 => {
|
||||
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,
|
||||
);
|
||||
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,16 +43,16 @@ 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.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_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();
|
||||
|
||||
|
@ -71,6 +71,7 @@ describe("Pills", () => {
|
|||
.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,15 +25,18 @@ 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 => {
|
||||
cy.initTestUser(synapse, "Alice")
|
||||
.then((credentials) => {
|
||||
user = credentials;
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
// create some extra sessions to manage
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
});
|
||||
});
|
||||
|
@ -45,50 +48,52 @@ 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(".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);
|
||||
|
@ -99,9 +104,9 @@ describe("Device manager", () => {
|
|||
cy.get(".mx_Spinner").should("not.exist");
|
||||
|
||||
// session name updated in details
|
||||
cy.get('.mx_DeviceDetailHeading h3').should('have.text', sessionName);
|
||||
cy.get(".mx_DeviceDetailHeading h3").should("have.text", sessionName);
|
||||
// and main list item
|
||||
cy.get('.mx_DeviceTile h4').should('have.text', sessionName);
|
||||
cy.get(".mx_DeviceTile h4").should("have.text", sessionName);
|
||||
|
||||
// sign out using the device details sign out
|
||||
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
|
||||
|
@ -110,7 +115,7 @@ describe("Device manager", () => {
|
|||
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.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,10 +61,15 @@ describe("Sliding Sync", () => {
|
|||
|
||||
// assert order
|
||||
const checkOrder = (wantOrder: string[]) => {
|
||||
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomTile_title").should((elements) => {
|
||||
expect(_.map(elements, (e) => {
|
||||
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);
|
||||
}),
|
||||
"rooms are sorted",
|
||||
).to.deep.equal(wantOrder);
|
||||
});
|
||||
};
|
||||
const bumpRoom = (alias: string) => {
|
||||
|
@ -80,9 +84,11 @@ describe("Sliding Sync", () => {
|
|||
const createAndJoinBob = () => {
|
||||
// create a Bob user
|
||||
cy.get<SynapseInstance>("@synapse").then((synapse) => {
|
||||
return cy.getBot(synapse, {
|
||||
return cy
|
||||
.getBot(synapse, {
|
||||
displayName: "Bob",
|
||||
}).as("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,15 +263,20 @@ 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) => {
|
||||
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]) => {
|
||||
})
|
||||
.then(([join, reject, rescind]) => {
|
||||
roomJoin = join.room_id;
|
||||
roomReject = reject.room_id;
|
||||
roomRescind = rescind.room_id;
|
||||
|
@ -283,29 +288,30 @@ describe("Sliding Sync", () => {
|
|||
});
|
||||
|
||||
// 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) => {
|
||||
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"]);
|
||||
}),
|
||||
"rooms are sorted",
|
||||
).to.deep.equal(["Rescind"]);
|
||||
});
|
||||
|
||||
// now rescind the invite
|
||||
|
@ -314,18 +320,18 @@ 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");
|
||||
|
@ -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,15 +373,18 @@ 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", {
|
||||
return cy
|
||||
.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "First message",
|
||||
msgtype: "m.text",
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Permalink me",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Reply to me",
|
||||
msgtype: "m.text",
|
||||
|
@ -384,9 +395,9 @@ describe("Sliding Sync", () => {
|
|||
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: [{
|
||||
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,46 +37,50 @@ 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 });
|
||||
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>> => {
|
||||
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", (
|
||||
Cypress.Commands.add(
|
||||
"spotlightFilter",
|
||||
(
|
||||
filter: Filter | null,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
let selector: string;
|
||||
switch (filter) {
|
||||
case Filter.People:
|
||||
|
@ -90,25 +94,29 @@ Cypress.Commands.add("spotlightFilter", (
|
|||
break;
|
||||
}
|
||||
return cy.get(selector, options).click();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("spotlightSearch", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
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>> => {
|
||||
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>> => {
|
||||
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 => {
|
||||
cy.initTestUser(synapse, "Jim")
|
||||
.then(() =>
|
||||
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
|
||||
bot1 = _bot1;
|
||||
}),
|
||||
).then(() =>
|
||||
cy.getBot(synapse, { displayName: bot2Name }).then(_bot2 => {
|
||||
)
|
||||
.then(() =>
|
||||
cy.getBot(synapse, { displayName: bot2Name }).then((_bot2) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
bot2 = _bot2;
|
||||
}),
|
||||
).then(() =>
|
||||
)
|
||||
.then(() =>
|
||||
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
|
||||
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => {
|
||||
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 }) => {
|
||||
bot2.createRoom({ name: room2Name, visibility: Visibility.Public }).then(
|
||||
({ room_id: _room2Id }) => {
|
||||
room2Id = _room2Id;
|
||||
bot2.invite(room2Id, bot1.getUserId());
|
||||
});
|
||||
},
|
||||
);
|
||||
bot2.createRoom({
|
||||
name: room3Name,
|
||||
visibility: Visibility.Public, initial_state: [{
|
||||
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'),
|
||||
);
|
||||
)
|
||||
.then(() => cy.get(".mx_RoomSublist_skeletonUI").should("not.exist"));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,20 +228,23 @@ describe("Spotlight", () => {
|
|||
});
|
||||
|
||||
it("should find joined rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
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(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find known public rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
|
@ -238,13 +253,15 @@ describe("Spotlight", () => {
|
|||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown public rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
|
@ -253,14 +270,16 @@ describe("Spotlight", () => {
|
|||
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room2Id);
|
||||
}).then(() => {
|
||||
})
|
||||
.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.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
|
@ -269,7 +288,8 @@ describe("Spotlight", () => {
|
|||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room3Id);
|
||||
}).then(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
|
||||
cy.roomHeaderName().should("contain", room3Name);
|
||||
});
|
||||
|
@ -299,27 +319,31 @@ describe("Spotlight", () => {
|
|||
});
|
||||
*/
|
||||
it("should find known people", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
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(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", bot1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown people", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
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(() => {
|
||||
})
|
||||
.then(() => {
|
||||
cy.roomHeaderName().should("contain", bot2Name);
|
||||
});
|
||||
});
|
||||
|
@ -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,7 +428,8 @@ describe("Spotlight", () => {
|
|||
});
|
||||
|
||||
it("should allow opening group chat dialog", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.openSpotlightDialog()
|
||||
.within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
|
@ -415,8 +437,9 @@ describe("Spotlight", () => {
|
|||
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");
|
||||
})
|
||||
.then(() => {
|
||||
cy.get("[role=dialog]").should("contain", "Direct Messages");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -445,36 +468,38 @@ 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", "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("{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", "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("{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, {
|
||||
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");
|
||||
})
|
||||
.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,8 +247,11 @@ 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
|
||||
|
@ -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,7 +45,9 @@ describe("Update", () => {
|
|||
cy.initTestUser(synapse, "Ursa");
|
||||
|
||||
cy.wait("@version");
|
||||
cy.url().should("contain", "updated=" + NEW_VERSION).then(href => {
|
||||
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,25 +55,23 @@ 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", {
|
||||
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) => {
|
||||
cy.get(".mx_ProgressBar")
|
||||
.invoke("val")
|
||||
.then((oldProgress) => {
|
||||
const findPeopleAction = cy.contains(".mx_UserOnboardingTask_action", "Find friends");
|
||||
expect(findPeopleAction).to.exist;
|
||||
findPeopleAction.click();
|
||||
|
@ -84,10 +82,10 @@ describe("User Onboarding (new user)", () => {
|
|||
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')
|
||||
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");
|
||||
|
|
|
@ -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 => {
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content: IWidget = {
|
||||
id: WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
type: 'widget',
|
||||
name: 'widget',
|
||||
creatorUserId: "somebody",
|
||||
type: "widget",
|
||||
name: "widget",
|
||||
url: widgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, WIDGET_ID);
|
||||
}).as('widgetEventSent');
|
||||
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, WIDGET_ID);
|
||||
})
|
||||
.as("widgetEventSent");
|
||||
|
||||
// set initial layout
|
||||
cy.getClient().then(async matrixClient => {
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 0,
|
||||
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', {
|
||||
cy.getClient()
|
||||
.then((matrixClient) => {
|
||||
return matrixClient.sendStateEvent(roomId, "m.room.power_levels", {
|
||||
users: {
|
||||
[user.userId]: 50,
|
||||
[bot.getUserId()]: 100,
|
||||
},
|
||||
});
|
||||
}).as('powerLevelsChanged');
|
||||
})
|
||||
.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 => {
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content: IWidget = {
|
||||
id: DEMO_WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
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');
|
||||
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,15 +149,17 @@ 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 () => {
|
||||
cy.get("#demo")
|
||||
.should("exist")
|
||||
.then(async () => {
|
||||
const userId = user.userId;
|
||||
if (userRemove == 'leave') {
|
||||
cy.getClient().then(async matrixClient => {
|
||||
if (userRemove == "leave") {
|
||||
cy.getClient().then(async (matrixClient) => {
|
||||
await matrixClient.leave(roomId);
|
||||
});
|
||||
} else if (userRemove == 'kick') {
|
||||
} else if (userRemove == "kick") {
|
||||
await bot.kick(roomId, userId);
|
||||
} else if (userRemove == 'ban') {
|
||||
} else if (userRemove == "ban") {
|
||||
await bot.ban(roomId, userId);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -36,4 +36,4 @@
|
|||
"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,15 +59,13 @@ 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) => {
|
||||
childProcess.execFile(
|
||||
"docker",
|
||||
["exec", args.containerId, ...args.params],
|
||||
{ encoding: "utf8" },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
|
@ -74,7 +73,8 @@ export function dockerExec(args: {
|
|||
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,
|
||||
], {
|
||||
childProcess
|
||||
.spawn("docker", ["logs", args.containerId], {
|
||||
stdio: ["ignore", stdoutFile, stderrFile],
|
||||
}).once('close', resolve);
|
||||
})
|
||||
.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) => {
|
||||
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",
|
||||
|
|
|
@ -7,7 +7,7 @@ public_baseurl: http://localhost:8008/
|
|||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ public_baseurl: "{{PUBLIC_BASEURL}}"
|
|||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
<!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>
|
||||
<p>Thank you, you've already accepted the license.</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Please accept the license!
|
||||
</p>
|
||||
<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!"/>
|
||||
<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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
|
|
|
@ -4,7 +4,7 @@ public_baseurl: "{{PUBLIC_BASEURL}}"
|
|||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
|
|
|
@ -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", (
|
||||
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, {
|
||||
): void => {
|
||||
return originalFn(
|
||||
context,
|
||||
{
|
||||
...options,
|
||||
rules: {
|
||||
// Disable contrast checking for now as we have too many issues with it
|
||||
'color-contrast': {
|
||||
"color-contrast": {
|
||||
enabled: false,
|
||||
},
|
||||
...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, {
|
||||
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 });
|
||||
});
|
||||
}),
|
||||
{ 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> => {
|
||||
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")
|
||||
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>>;
|
||||
.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 => {
|
||||
return cy
|
||||
.window({ log: false })
|
||||
.then((win) => {
|
||||
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
|
||||
}).then(() => null);
|
||||
})
|
||||
.then(() => null);
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
||||
export {};
|
||||
|
|
|
@ -49,19 +49,18 @@ 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> => {
|
||||
Cypress.Commands.add(
|
||||
"loginUser",
|
||||
(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
|
||||
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
|
||||
return cy.request<{
|
||||
return cy
|
||||
.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
|
@ -70,14 +69,15 @@ Cypress.Commands.add("loginUser", (synapse: SynapseInstance, username: string, p
|
|||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
"user": username,
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
"password": password,
|
||||
password: password,
|
||||
},
|
||||
}).then(response => ({
|
||||
})
|
||||
.then((response) => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.body.access_token,
|
||||
|
@ -85,14 +85,17 @@ Cypress.Commands.add("loginUser", (synapse: SynapseInstance, username: string, p
|
|||
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> => {
|
||||
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 => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.indexedDB.databases()?.then((databases) => {
|
||||
databases.forEach((database) => {
|
||||
win.indexedDB.deleteDatabase(database.name);
|
||||
});
|
||||
});
|
||||
|
@ -100,11 +103,14 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
|||
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
||||
return cy
|
||||
.registerUser(synapse, username, password, displayName)
|
||||
.then(() => {
|
||||
return cy.loginUser(synapse, username, password);
|
||||
}).then(response => {
|
||||
})
|
||||
.then((response) => {
|
||||
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||
cy.window({ log: false }).then(win => {
|
||||
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);
|
||||
|
@ -120,10 +126,13 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
|||
|
||||
prelaunchFn?.();
|
||||
|
||||
return cy.visit("/").then(() => {
|
||||
return cy
|
||||
.visit("/")
|
||||
.then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 30000 });
|
||||
}).then(() => ({
|
||||
})
|
||||
.then(() => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.accessToken,
|
||||
|
@ -132,4 +141,5 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
|||
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/**", {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
cy.intercept(
|
||||
"**/_matrix/**",
|
||||
{
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
Authorization: "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
}, req => {
|
||||
},
|
||||
(req) => {
|
||||
req.destroy();
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("goOnline", (): void => {
|
||||
cy.log("Going online");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
cy.intercept(
|
||||
"**/_matrix/**",
|
||||
{
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
Authorization: "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
}, req => {
|
||||
},
|
||||
(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> => {
|
||||
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> => {
|
||||
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
|
||||
.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
|
||||
.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,14 +83,17 @@ function registerUser(
|
|||
displayName?: string,
|
||||
): Chainable<Credentials> {
|
||||
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
|
||||
return cy.then(() => {
|
||||
return cy
|
||||
.then(() => {
|
||||
// get a nonce
|
||||
return cy.request<{ nonce: string }>({ url });
|
||||
}).then(response => {
|
||||
})
|
||||
.then((response) => {
|
||||
const { nonce } = response.body;
|
||||
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
|
||||
`${nonce}\0${username}\0${password}\0notadmin`,
|
||||
).digest('hex');
|
||||
const mac = crypto
|
||||
.createHmac("sha1", synapse.registrationSecret)
|
||||
.update(`${nonce}\0${username}\0${password}\0notadmin`)
|
||||
.digest("hex");
|
||||
|
||||
return cy.request<{
|
||||
access_token: string;
|
||||
|
@ -109,7 +112,8 @@ function registerUser(
|
|||
displayname: displayName,
|
||||
},
|
||||
});
|
||||
}).then(response => ({
|
||||
})
|
||||
.then((response) => ({
|
||||
homeServer: response.body.home_server,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
|
|
|
@ -38,7 +38,9 @@ export interface Message {
|
|||
}
|
||||
|
||||
Cypress.Commands.add("scrollToTop", (): void => {
|
||||
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
|
||||
cy.get(".mx_RoomView_timeline .mx_ScrollPanel")
|
||||
.scrollTo("top", { duration: 100 })
|
||||
.then((ref) => {
|
||||
if (ref.scrollTop() > 0) {
|
||||
return cy.scrollToTop();
|
||||
}
|
||||
|
@ -48,7 +50,7 @@ Cypress.Commands.add("scrollToTop", (): void => {
|
|||
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,7 +59,7 @@ 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(), {
|
||||
|
@ -68,7 +68,7 @@ cy.all = function all(commands): Cypress.Chainable {
|
|||
})
|
||||
.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
|
||||
`cy.startSynapse()` to provide a Synapse instance to log into:
|
||||
|
||||
```javascript
|
||||
cy.startSynapse("consent").then(result => {
|
||||
cy.startSynapse("consent").then((result) => {
|
||||
synapse = result;
|
||||
});
|
||||
```
|
||||
|
@ -96,21 +102,24 @@ 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`:
|
||||
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
|
@ -118,10 +127,13 @@ All other files in the template are copied recursively to `/data/`, so the file
|
|||
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
|
||||
- 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
|
||||
- @ + 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,
|
||||
- # + 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();
|
||||
|
@ -31,7 +31,7 @@ const onKeyDown = (ev: KeyboardEvent): void => {
|
|||
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,22 +6,24 @@ 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
|
||||
_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
|
||||
- `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
|
||||
_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
|
||||
- `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
|
||||
- `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
|
||||
- `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
|
||||
|
|
|
@ -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,13 +68,13 @@ 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
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
|
|
|
@ -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,6 +56,7 @@ 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) {
|
||||
|
@ -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,19 +4,20 @@ 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": {
|
||||
widgets: {
|
||||
"first-widget-id": {
|
||||
"container": "top",
|
||||
"index": 0,
|
||||
"width": 60,
|
||||
"height": 40
|
||||
container: "top",
|
||||
index: 0,
|
||||
width: 60,
|
||||
height: 40,
|
||||
},
|
||||
"second-widget-id": {
|
||||
"container": "right"
|
||||
}
|
||||
}
|
||||
container: "right",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
subprojects:
|
||||
matrix-js-sdk:
|
||||
includeByDefault: false
|
||||
|
||||
|
|
|
@ -35,9 +35,15 @@ limitations under the License.
|
|||
}
|
||||
|
||||
@keyframes mx--anim-pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
100% { opacity: 1; }
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mx_Dialog_lightbox_background_keyframes {
|
||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
|||
@import "./_spacing.pcss";
|
||||
@import url("maplibre-gl/dist/maplibre-gl.css");
|
||||
|
||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); /* quadratic */
|
||||
$hover-transition: 0.08s cubic-bezier(0.46, 0.03, 0.52, 0.96); /* quadratic */
|
||||
|
||||
$selected-message-border-width: 4px;
|
||||
|
||||
|
@ -40,8 +40,8 @@ $timeline-image-border-radius: 8px;
|
|||
:root {
|
||||
font-size: 10px;
|
||||
|
||||
--transition-short: .1s;
|
||||
--transition-standard: .3s;
|
||||
--transition-short: 0.1s;
|
||||
--transition-standard: 0.3s;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
|
@ -74,13 +74,16 @@ body {
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
pre,
|
||||
code {
|
||||
font-family: $monospace-font-family;
|
||||
font-size: 100% !important;
|
||||
}
|
||||
|
||||
.error, .warning,
|
||||
.text-error, .text-warning {
|
||||
.error,
|
||||
.warning,
|
||||
.text-error,
|
||||
.text-warning {
|
||||
color: $alert;
|
||||
}
|
||||
|
||||
|
@ -132,7 +135,7 @@ input[type="search"].mx_textinput_icon {
|
|||
/* FIXME THEME - Tint by CSS rather than referencing a duplicate asset */
|
||||
input[type="text"].mx_textinput_icon.mx_textinput_search,
|
||||
input[type="search"].mx_textinput_icon.mx_textinput_search {
|
||||
background-image: url('$(res)/img/feather-customised/search-input.svg');
|
||||
background-image: url("$(res)/img/feather-customised/search-input.svg");
|
||||
}
|
||||
|
||||
/* dont search UI as not all browsers support it, */
|
||||
|
@ -150,7 +153,9 @@ textarea::placeholder {
|
|||
opacity: initial;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"], textarea {
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
background-color: transparent;
|
||||
color: $primary-content;
|
||||
}
|
||||
|
@ -160,7 +165,9 @@ textarea {
|
|||
color: $primary-content;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, input[type="password"]:focus, textarea:focus {
|
||||
input[type="text"]:focus,
|
||||
input[type="password"]:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -197,7 +204,8 @@ legend {
|
|||
/* it has the appearance of a text box so the controls */
|
||||
/* appear to be part of the input */
|
||||
|
||||
.mx_Dialog, .mx_MatrixChat_wrapper {
|
||||
.mx_Dialog,
|
||||
.mx_MatrixChat_wrapper {
|
||||
.mx_textinput > input[type="text"],
|
||||
.mx_textinput > input[type="search"] {
|
||||
border: none;
|
||||
|
@ -213,7 +221,7 @@ legend {
|
|||
background-color: transparent;
|
||||
color: $input-darker-fg-color;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba($primary-content, .1);
|
||||
border: 1px solid rgba($primary-content, 0.1);
|
||||
/* these things should probably not be defined globally */
|
||||
margin: 9px;
|
||||
}
|
||||
|
@ -226,7 +234,7 @@ legend {
|
|||
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="text"]::placeholder,
|
||||
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) > input[type="search"]::placeholder,
|
||||
.mx_textinput input::placeholder {
|
||||
color: rgba($input-darker-fg-color, .75);
|
||||
color: rgba($input-darker-fg-color, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,7 +337,7 @@ legend {
|
|||
pre code {
|
||||
white-space: pre; /* we want code blocks to be scrollable and not wrap */
|
||||
|
||||
>* {
|
||||
> * {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
@ -457,7 +465,7 @@ legend {
|
|||
}
|
||||
|
||||
@define-mixin customisedCancelButton {
|
||||
mask: url('$(res)/img/cancel.svg');
|
||||
mask: url("$(res)/img/cancel.svg");
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
|
@ -768,7 +776,7 @@ legend {
|
|||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
display: inline-block;
|
||||
background-color: $button-fg-color;
|
||||
mask-position: center;
|
||||
|
@ -789,7 +797,7 @@ legend {
|
|||
@define-mixin ThreadSummaryIcon {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
mask-image: url('$(res)/img/element-icons/thread-summary.svg');
|
||||
mask-image: url("$(res)/img/element-icons/thread-summary.svg");
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
|
@ -806,7 +814,7 @@ legend {
|
|||
}
|
||||
}
|
||||
|
||||
@define-mixin composerButton $border-radius,$hover-color {
|
||||
@define-mixin composerButton $border-radius, $hover-color {
|
||||
--size: 26px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
@ -817,7 +825,7 @@ legend {
|
|||
border-radius: $border-radius;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
|
@ -830,7 +838,7 @@ legend {
|
|||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
|
|
@ -24,7 +24,7 @@ $font-6px: 0.6rem;
|
|||
$font-7px: 0.7rem;
|
||||
$font-8px: 0.8rem;
|
||||
$font-9px: 0.9rem;
|
||||
$font-10px: 1.0rem;
|
||||
$font-10px: 1rem;
|
||||
$font-10-4px: 1.04rem;
|
||||
$font-11px: 1.1rem;
|
||||
$font-12px: 1.2rem;
|
||||
|
@ -35,7 +35,7 @@ $font-16px: 1.6rem;
|
|||
$font-17px: 1.7rem;
|
||||
$font-18px: 1.8rem;
|
||||
$font-19px: 1.9rem;
|
||||
$font-20px: 2.0rem;
|
||||
$font-20px: 2rem;
|
||||
$font-21px: 2.1rem;
|
||||
$font-22px: 2.2rem;
|
||||
$font-23px: 2.3rem;
|
||||
|
@ -45,7 +45,7 @@ $font-26px: 2.6rem;
|
|||
$font-27px: 2.7rem;
|
||||
$font-28px: 2.8rem;
|
||||
$font-29px: 2.9rem;
|
||||
$font-30px: 3.0rem;
|
||||
$font-30px: 3rem;
|
||||
$font-31px: 3.1rem;
|
||||
$font-32px: 3.2rem;
|
||||
$font-33px: 3.3rem;
|
||||
|
@ -55,7 +55,7 @@ $font-36px: 3.6rem;
|
|||
$font-37px: 3.7rem;
|
||||
$font-38px: 3.8rem;
|
||||
$font-39px: 3.9rem;
|
||||
$font-40px: 4.0rem;
|
||||
$font-40px: 4rem;
|
||||
$font-41px: 4.1rem;
|
||||
$font-42px: 4.2rem;
|
||||
$font-43px: 4.3rem;
|
||||
|
@ -65,7 +65,7 @@ $font-46px: 4.6rem;
|
|||
$font-47px: 4.7rem;
|
||||
$font-48px: 4.8rem;
|
||||
$font-49px: 4.9rem;
|
||||
$font-50px: 5.0rem;
|
||||
$font-50px: 5rem;
|
||||
$font-51px: 5.1rem;
|
||||
$font-52px: 5.2rem;
|
||||
$font-78px: 7.8rem;
|
||||
|
|
|
@ -31,7 +31,7 @@ limitations under the License.
|
|||
|
||||
/* caret down */
|
||||
&::before {
|
||||
content: '';
|
||||
content: "";
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 5px solid currentColor;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue