Apply prettier formatting

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

View file

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

View file

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

View file

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

View file

@ -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 }}

View file

@ -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

View file

@ -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

View file

@ -3,7 +3,7 @@
name: Upload Preview Build to Netlify
on:
workflow_run:
workflows: [ "Element Web - Build" ]
workflows: ["Element Web - Build"]
types:
- completed
jobs:

View file

@ -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"

View file

@ -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:

View file

@ -1,7 +1,7 @@
name: Release Process
on:
release:
types: [ published ]
types: [published]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
npm:

View file

@ -1,7 +1,7 @@
name: SonarQube
on:
workflow_run:
workflows: [ "Tests" ]
workflows: ["Tests"]
types:
- completed
concurrency:

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -1,13 +1,8 @@
module.exports = {
"extends": [
"stylelint-config-standard",
"stylelint-config-prettier",
],
customSyntax: require('postcss-scss'),
"plugins": [
"stylelint-scss",
],
"rules": {
extends: ["stylelint-config-standard", "stylelint-config-prettier"],
customSyntax: require("postcss-scss"),
plugins: ["stylelint-scss"],
rules: {
"color-hex-case": null,
"comment-empty-line-before": null,
"declaration-empty-line-before": null,
@ -22,15 +17,18 @@ module.exports = {
"at-rule-no-unknown": null,
"no-descending-specificity": null,
"no-empty-first-line": true,
"scss/at-rule-no-unknown": [true, {
"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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -9,18 +9,18 @@
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
matrix-react-sdk
================
# matrix-react-sdk
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
This package provides the React components needed to build a Matrix web client
using React. It is not useable in isolation, and instead must be used from
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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,7 @@ interface CryptoTestContext extends Mocha.Context {
}
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
return new Promise<VerificationRequest>(resolve => {
return new Promise<VerificationRequest>((resolve) => {
const onVerificationRequestEvent = (request: VerificationRequest) => {
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
cli.off("crypto.verification.request", onVerificationRequestEvent);
@ -49,7 +49,7 @@ const checkDMRoom = () => {
cy.contains(".mx_RoomView_body .mx_cryptoEvent", "Encryption enabled").should("exist");
};
const startDMWithBob = function(this: CryptoTestContext) {
const startDMWithBob = function (this: CryptoTestContext) {
cy.get('.mx_RoomList [aria-label="Start chat"]').click();
cy.get('[data-testid="invite-dialog-input"]').type(this.bob.getUserId());
cy.contains(".mx_InviteDialog_tile_nameStack_name", "Bob").click();
@ -57,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}`);

View file

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

View file

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

View file

@ -87,8 +87,9 @@ function expectKickedMessage(shouldExist: boolean) {
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true });
// Check for the event message (or lack thereof)
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)
.should(shouldExist ? "exist" : "not.exist");
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should(
shouldExist ? "exist" : "not.exist",
);
}
describe("Integration Manager: Kick", () => {
@ -97,18 +98,18 @@ describe("Integration Manager: Kick", () => {
let integrationManagerUrl: string;
beforeEach(() => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
integrationManagerUrl = url;
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
});
}).then(user => {
}).then((user) => {
testUser = user;
});
@ -127,8 +128,8 @@ describe("Integration Manager: Kick", () => {
}).as("integrationManager");
// Succeed when checking the token is valid
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
req.continue(res => {
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, (req) => {
req.continue((res) => {
return res.send(200, {
user_id: testUser.userId,
});
@ -149,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);
});
},
);
});
});

View file

@ -31,11 +31,11 @@ describe("Lazy Loading", () => {
const charlies: Charly[] = [];
beforeEach(() => {
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Alice");
@ -44,7 +44,7 @@ describe("Lazy Loading", () => {
displayName: "Bob",
startClient: false,
autoAcceptInvites: false,
}).then(_bob => {
}).then((_bob) => {
bob = _bob;
});
@ -54,7 +54,7 @@ describe("Lazy Loading", () => {
displayName,
startClient: false,
autoAcceptInvites: false,
}).then(client => {
}).then((client) => {
charlies[i - 1] = { displayName, client };
});
}
@ -71,15 +71,22 @@ describe("Lazy Loading", () => {
const charlyMsg2 = "how's it going??";
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
cy.window({ log: false }).then(win => {
return cy.wrap(bob.createRoom({
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);
}

View file

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

View file

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

View file

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

View file

@ -33,17 +33,17 @@ describe("Polls", () => {
};
const createPoll = ({ title, options }: CreatePollOptions) => {
if (options.length < 2) {
throw new Error('Poll must have at least two options');
throw new Error("Poll must have at least two options");
}
cy.get('.mx_PollCreateDialog').within((pollCreateDialog) => {
cy.get('#poll-topic-input').type(title);
cy.get(".mx_PollCreateDialog").within((pollCreateDialog) => {
cy.get("#poll-topic-input").type(title);
options.forEach((option, index) => {
const optionId = `#pollcreate_option_${index}`;
// click 'add option' button if needed
if (pollCreateDialog.find(optionId).length === 0) {
cy.get('.mx_PollCreateDialog_addOption').scrollIntoView().click();
cy.get(".mx_PollCreateDialog_addOption").scrollIntoView().click();
}
cy.get(optionId).scrollIntoView().type(option);
});
@ -56,34 +56,32 @@ describe("Polls", () => {
};
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
return getPollTile(pollId).contains('.mx_MPollBody_option .mx_StyledRadioButton', optionText);
return getPollTile(pollId).contains(".mx_MPollBody_option .mx_StyledRadioButton", optionText);
};
const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => {
getPollOption(pollId, optionText).within(() => {
cy.get('.mx_MPollBody_optionVoteCount').should('contain', `${votes} vote`);
cy.get(".mx_MPollBody_optionVoteCount").should("contain", `${votes} vote`);
});
};
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
getPollOption(pollId, optionText).within(ref => {
cy.get('input[type="radio"]').invoke('attr', 'value').then(optionId => {
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();
});
});

View file

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

View file

@ -22,7 +22,7 @@ describe("Pills", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Sally");
@ -33,7 +33,7 @@ describe("Pills", () => {
cy.stopSynapse(synapse);
});
it('should navigate clicks internally to the app', () => {
it("should navigate clicks internally to the app", () => {
const messageRoom = "Send Messages Here";
const targetLocalpart = "aliasssssssssssss";
cy.createRoom({
@ -43,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);
});
},
);
});
});

View file

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

View file

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

View file

@ -25,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");
});
});

View file

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

View file

@ -26,18 +26,17 @@ import { ProxyInstance } from "../../plugins/sliding-sync";
describe("Sliding Sync", () => {
beforeEach(() => {
cy.startSynapse("default").as("synapse").then(synapse => {
cy.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

View file

@ -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] });

View file

@ -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");
});
});
});

View file

@ -21,7 +21,7 @@ import { MatrixClient } from "../../global";
function markWindowBeforeReload(): void {
// mark our window object to "know" when it gets reloaded
cy.window().then(w => w.beforeReload = true);
cy.window().then((w) => (w.beforeReload = true));
}
describe("Threads", () => {
@ -30,10 +30,10 @@ describe("Threads", () => {
beforeEach(() => {
// Default threads to ON for this spec
cy.enableLabsFeature("feature_thread");
cy.window().then(win => {
cy.window().then((win) => {
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
});
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Tom");
@ -78,12 +78,12 @@ describe("Threads", () => {
cy.getBot(synapse, {
displayName: "BotBob",
autoAcceptInvites: false,
}).then(_bot => {
}).then((_bot) => {
bot = _bot;
});
let roomId: string;
cy.createRoom({}).then(_roomId => {
cy.createRoom({}).then((_roomId) => {
roomId = _roomId;
cy.inviteUser(roomId, bot.getUserId());
bot.joinRoom(roomId);
@ -95,10 +95,11 @@ describe("Threads", () => {
// Wait for message to send, get its ID and save as @threadId
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
.invoke("attr", "data-scroll-tokens").as("threadId");
.invoke("attr", "data-scroll-tokens")
.as("threadId");
// Bot starts thread
cy.get<string>("@threadId").then(threadId => {
cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, {
body: "Hello there",
msgtype: "m.text",
@ -119,7 +120,8 @@ describe("Threads", () => {
// User reacts to message instead
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Hello there")
.find('[aria-label="React"]').click({ force: true }); // Cypress has no ability to hover
.find('[aria-label="React"]')
.click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_EmojiPicker").within(() => {
cy.get('input[type="text"]').type("wave");
cy.contains('[role="menuitem"]', "👋").click();
@ -127,7 +129,8 @@ describe("Threads", () => {
// User redacts their prior response
cy.contains(".mx_ThreadView .mx_EventTile .mx_EventTile_line", "Test")
.find('[aria-label="Options"]').click({ force: true }); // Cypress has no ability to hover
.find('[aria-label="Options"]')
.click({ force: true }); // Cypress has no ability to hover
cy.get(".mx_IconizedContextMenu").within(() => {
cy.contains('[role="menuitem"]', "Remove").click();
});
@ -144,7 +147,7 @@ describe("Threads", () => {
cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
// Bot responds to thread
cy.get<string>("@threadId").then(threadId => {
cy.get<string>("@threadId").then((threadId) => {
bot.sendMessage(roomId, threadId, {
body: "How are things?",
msgtype: "m.text",
@ -178,45 +181,55 @@ describe("Threads", () => {
cy.get(".mx_BasicMessageComposer_input").type(" How about yourself?{enter}");
});
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_sender").should("contain", "Tom");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content")
.should("contain", "Great! How about yourself?");
cy.get(".mx_RoomView_body .mx_ThreadSummary .mx_ThreadSummary_content").should(
"contain",
"Great! How about yourself?",
);
// User closes right panel
cy.get(".mx_ThreadView .mx_BaseCard_close").click();
// Bot responds to thread and saves the id of their message to @eventId
cy.get<string>("@threadId").then(threadId => {
cy.wrap(bot.sendMessage(roomId, threadId, {
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");

View file

@ -45,10 +45,7 @@ const expectDisplayName = (e: JQuery<HTMLElement>, displayName: string): void =>
};
const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
cy.all([
cy.window({ log: false }),
cy.getClient(),
]).then(([win, cli]) => {
cy.all([cy.window({ log: false }), cy.getClient()]).then(([win, cli]) => {
const size = AVATAR_SIZE * win.devicePixelRatio;
expect(e.find(".mx_BaseAvatar_image").attr("src")).to.equal(
// eslint-disable-next-line no-restricted-properties
@ -75,10 +72,10 @@ describe("Timeline", () => {
let newAvatarUrl: string;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, OLD_NAME).then(() =>
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
roomId = _room1Id;
}),
);
@ -154,8 +151,11 @@ describe("Timeline", () => {
it("should create and configure a room on IRC layout", () => {
cy.visit("/#/room/" + roomId);
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary[data-layout=irc] " +
".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
cy.get(".mx_Spinner").should("not.exist");
cy.percySnapshot("Configured room on IRC layout");
});
@ -165,8 +165,10 @@ describe("Timeline", () => {
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
@ -177,13 +179,13 @@ describe("Timeline", () => {
// = calc(var(--name-width) + 10px + var(--icon-width))
// = 80 + 10 + 14 = 104px
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info:first-of-type .mx_EventTile_line")
.should('have.css', "margin-inline-start", "104px")
.should('have.css', "inset-inline-start", "0px");
.should("have.css", "margin-inline-start", "104px")
.should("have.css", "inset-inline-start", "0px");
cy.get(".mx_Spinner").should("not.exist");
// Exclude timestamp from snapshot
const percyCSS = ".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp "
+ "{ visibility: hidden !important; }";
const percyCSS =
".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
cy.percySnapshot("Event line with inline start margin on IRC layout", { percyCSS });
cy.checkA11y();
});
@ -192,8 +194,10 @@ describe("Timeline", () => {
sendEvent(roomId);
cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Edit message
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
@ -206,20 +210,23 @@ describe("Timeline", () => {
cy.get(".mx_RoomView_body .mx_EventTile_info .mx_MessageTimestamp").click();
// Exclude timestamp from snapshot
const percyCSS = ".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp "
+ "{ visibility: hidden !important; }";
const percyCSS =
".mx_RoomView_body .mx_EventTile .mx_MessageTimestamp " + "{ visibility: hidden !important; }";
// should not add inline start padding to a hidden event line on IRC layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line")
.should('have.css', 'padding-inline-start', '0px');
cy.get(".mx_EventTile[data-layout=irc].mx_EventTile_info .mx_EventTile_line").should(
"have.css",
"padding-inline-start",
"0px",
);
cy.percySnapshot("Hidden event line with zero padding on IRC layout", { percyCSS });
// should add inline start padding to a hidden event line on modern layout
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.Group);
cy.get(".mx_EventTile[data-layout=group].mx_EventTile_info .mx_EventTile_line")
// calc(var(--EventTile_group_line-spacing-inline-start) + 20px) = 64 + 20 = 84px
.should('have.css', 'padding-inline-start', '84px');
.should("have.css", "padding-inline-start", "84px");
cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
});
@ -227,8 +234,10 @@ describe("Timeline", () => {
sendEvent(roomId);
cy.visit("/#/room/" + roomId);
cy.setSettingValue("showHiddenEventsInTimeline", null, SettingLevel.DEVICE, true);
cy.contains(".mx_RoomView_body .mx_GenericEventListSummary " +
".mx_GenericEventListSummary_summary", "created and configured the room.").should("exist");
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary " + ".mx_GenericEventListSummary_summary",
"created and configured the room.",
).should("exist");
// Edit message
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
@ -238,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,
);
});
});
});

View file

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

View file

@ -22,7 +22,7 @@ describe("Update", () => {
let synapse: SynapseInstance;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
});
});
@ -45,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);
});

View file

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

View file

@ -26,23 +26,23 @@ describe("User Onboarding (new user)", () => {
let bot1: MatrixClient;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Jane Doe");
cy.window({ log: false }).then(win => {
cy.window({ log: false }).then((win) => {
win.localStorage.setItem("mx_registration_time", "1656633601");
});
cy.reload().then(() => {
// wait for the app to load
return cy.get(".mx_MatrixChat", { timeout: 15000 });
});
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => {
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
bot1 = _bot1;
});
cy.get('.mx_UserOnboardingPage').should('exist');
cy.get('.mx_UserOnboardingButton').should('exist');
cy.get('.mx_UserOnboardingList')
.should('exist')
cy.get(".mx_UserOnboardingPage").should("exist");
cy.get(".mx_UserOnboardingButton").should("exist");
cy.get(".mx_UserOnboardingList")
.should("exist")
.should(($list) => {
const list = $list.get(0);
expect(getComputedStyle(list).opacity).to.be.eq("1");
@ -55,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");

View file

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

View file

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

View file

@ -19,7 +19,7 @@ import { IWidget } from "matrix-widget-api";
import { SynapseInstance } from "../../plugins/synapsedocker";
const ROOM_NAME = 'Test Room';
const ROOM_NAME = "Test Room";
const WIDGET_ID = "fake-widget";
const WIDGET_HTML = `
<html lang="en">
@ -32,18 +32,18 @@ const WIDGET_HTML = `
</html>
`;
describe('Widget Layout', () => {
describe("Widget Layout", () => {
let widgetUrl: string;
let synapse: SynapseInstance;
let roomId: string;
beforeEach(() => {
cy.startSynapse("default").then(data => {
cy.startSynapse("default").then((data) => {
synapse = data;
cy.initTestUser(synapse, "Sally");
});
cy.serveHtmlFile(WIDGET_HTML).then(url => {
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
widgetUrl = url;
});
@ -53,34 +53,38 @@ describe('Widget Layout', () => {
roomId = id;
// setup widget via state event
cy.getClient().then(async matrixClient => {
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);
});
});

View file

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

View file

@ -57,7 +57,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
return new Promise((resolve, reject) => {
function eventsInIntendedState(evList) {
const widgetPresent = evList.some((ev) => {
return ev.getContent() && ev.getContent()['id'] === widgetId;
return ev.getContent() && ev.getContent()["id"] === widgetId;
});
if (add) {
return widgetPresent;
@ -68,7 +68,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
const room = matrixClient.getRoom(roomId);
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
const startingWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
if (eventsInIntendedState(startingWidgetEvents)) {
resolve();
return;
@ -77,7 +77,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
function onRoomStateEvents(ev: MatrixEvent) {
if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return;
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
const currentWidgetEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
if (eventsInIntendedState(currentWidgetEvents)) {
matrixClient.removeListener(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
@ -95,35 +95,39 @@ describe("Widget PIP", () => {
let bot: MatrixClient;
let demoWidgetUrl: string;
function roomCreateAddWidgetPip(userRemove: 'leave' | 'kick' | 'ban') {
function roomCreateAddWidgetPip(userRemove: "leave" | "kick" | "ban") {
cy.createRoom({
name: ROOM_NAME,
invite: [bot.getUserId()],
}).then(roomId => {
}).then((roomId) => {
// sets bot to Admin and user to Moderator
cy.getClient().then(matrixClient => {
return matrixClient.sendStateEvent(roomId, 'm.room.power_levels', {
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");
});
});

View file

@ -36,4 +36,4 @@
"org.matrix.msc3886": false,
"org.matrix.msc3912": false
}
}
}

View file

@ -42,7 +42,8 @@ export function dockerRun(opts: {
const args = [
"run",
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
"--name",
`${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
"-d",
...params,
opts.image,
@ -58,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());
});
},
);
});
}

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ public_baseurl: http://localhost:8008/
listeners:
- port: 8008
tls: false
bind_addresses: ['::']
bind_addresses: ["::"]
type: http
x_forwarded: true

View file

@ -4,7 +4,7 @@ public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ['::']
bind_addresses: ["::"]
type: http
x_forwarded: true

View file

@ -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>

View file

@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test Privacy policy</title>

View file

@ -4,7 +4,7 @@ public_baseurl: "{{PUBLIC_BASEURL}}"
listeners:
- port: 8008
tls: false
bind_addresses: ['::']
bind_addresses: ["::"]
type: http
x_forwarded: true

View file

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

View file

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

View file

@ -24,10 +24,10 @@ import Chainable = Cypress.Chainable;
function terminalLog(violations: axe.Result[]): void {
cy.task(
'log',
`${violations.length} accessibility violation${
violations.length === 1 ? '' : 's'
} ${violations.length === 1 ? 'was' : 'were'} detected`,
"log",
`${violations.length} accessibility violation${violations.length === 1 ? "" : "s"} ${
violations.length === 1 ? "was" : "were"
} detected`,
);
// pluck specific keys to keep the table readable
@ -38,24 +38,32 @@ function terminalLog(violations: axe.Result[]): void {
nodes: nodes.length,
}));
cy.task('table', violationData);
cy.task("table", violationData);
}
Cypress.Commands.overwrite("checkA11y", (
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,
);
},
);

View file

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

View file

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

View file

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

View file

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

View file

@ -35,11 +35,15 @@ declare global {
// Inspired by https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
Cypress.Commands.add("accessIframe", (selector: string): Chainable<JQuery<HTMLElement>> => {
return cy.get(selector)
.its("0.contentDocument.body").should("not.be.empty")
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 {};

View file

@ -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 {};

View file

@ -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,
}));
});
});
},
);

View file

@ -35,27 +35,35 @@ declare global {
Cypress.Commands.add("goOffline", (): void => {
cy.log("Going offline");
cy.window({ log: false }).then(win => {
cy.intercept("**/_matrix/**", {
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 {};

View file

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

View file

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

View file

@ -102,26 +102,27 @@ declare global {
}
Cypress.Commands.add("getSettingsStore", (): Chainable<typeof SettingsStore> => {
return cy.window({ log: false }).then(win => win.mxSettingsStore);
return cy.window({ log: false }).then((win) => win.mxSettingsStore);
});
Cypress.Commands.add("setSettingValue", (
name: string,
roomId: string,
level: SettingLevel,
value: any,
): Chainable<void> => {
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 {};

View file

@ -16,7 +16,7 @@ limitations under the License.
/// <reference types="cypress" />
import * as crypto from 'crypto';
import * as crypto from "crypto";
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
@ -64,7 +64,7 @@ function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
if (!synapse) return;
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
return cy.window({ log: false }).then((win) => {
win.location.href = 'about:blank';
win.location.href = "about:blank";
cy.task("synapseStop", synapse.synapseId);
});
}
@ -83,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,

View file

@ -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 {};

View file

@ -29,7 +29,7 @@ declare global {
interface cy {
all<T extends Cypress.Chainable[] | []>(
commands: T
commands: T,
): Cypress.Chainable<{ [P in keyof T]: ChainableValue<T[P]> }>;
queue: any;
}
@ -59,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 {};

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,17 @@
# Cypress in Element Web
## Scope of this Document
This doc is about our Cypress tests in Element Web and how we use Cypress to write tests.
It aims to cover:
* How to run the tests yourself
* How the tests work
* How to write great Cypress tests
* Visual testing
- How to run the tests yourself
- How the tests work
- How to write great Cypress tests
- Visual testing
## Running the Tests
Our Cypress tests run automatically as part of our CI along with our other tests,
on every pull request and on every merge to develop & master.
@ -43,6 +46,7 @@ yarn run test:cypress:open
```
## How the Tests Work
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
as is typical for Cypress tests. Likewise, tests live in `cypress/e2e`.
@ -68,6 +72,7 @@ with each instance in a separate directory named after its ID. These logs are re
at the start of each test run.
## Writing Tests
Mostly this is the same advice as for writing any other Cypress test: the Cypress
docs are well worth a read if you're not already familiar with Cypress testing, eg.
https://docs.cypress.io/guides/references/best-practices. To avoid your tests being
@ -75,11 +80,12 @@ flaky it is also recommended to give https://docs.cypress.io/guides/core-concept
a read.
### Getting a Synapse
The key difference is in starting Synapse instances. Tests use this plugin via
`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.

View file

@ -1,37 +1,38 @@
# Composer Features
## Auto Complete
- Hitting tab tries to auto-complete the word before the caret as a room member
- 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.

View file

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

View file

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

View file

@ -6,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

View file

@ -36,4 +36,3 @@ mechanisms.
The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left)
is set up, where the dialog then drives through the contexts and transactions.

View file

@ -5,11 +5,12 @@ It's so complicated it needs its own README.
![](img/RoomListStore2.png)
Legend:
* Orange = External event.
* Purple = Deterministic flow.
* Green = Algorithm definition.
* Red = Exit condition/point.
* Blue = Process definition.
- Orange = External event.
- Purple = Deterministic flow.
- Green = Algorithm definition.
- Red = Exit condition/point.
- Blue = Process definition.
## Algorithms involved
@ -22,7 +23,6 @@ Behaviour of the overall room list (sticky rooms, etc) are determined by the gen
class. Here is where much of the coordination from the room list store is done to figure out which list
algorithm to call, instead of having all the logic in the room list store itself.
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
@ -68,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

View file

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

View file

@ -5,23 +5,22 @@ different values for a setting at particular levels of interest. For example, a
they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity
of dealing with the different levels and exposes easy to use getters and setters.
## Levels
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
order of priority, are:
* `device` - The current user's device
* `room-device` - The current user's device, but only when in a specific room
* `room-account` - The current user's account, but only when in a specific room
* `account` - The current user's account
* `room` - A specific room (setting for all members of the room)
* `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
* `default` - The hardcoded default for the settings
- `device` - The current user's device
- `room-device` - The current user's device, but only when in a specific room
- `room-account` - The current user's account, but only when in a specific room
- `account` - The current user's account
- `room` - A specific room (setting for all members of the room)
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
- `default` - The hardcoded default for the settings
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
that room administrators cannot force account-only settings upon participants.
## Settings
Settings are the different options a user may set or experience in the application. These are pre-defined in
@ -29,6 +28,7 @@ Settings are the different options a user may set or experience in the applicati
Settings that support the config level can be set in the config file under the `setting_defaults` key (note that some
settings, like the "theme" setting, are special cased in the config file):
```json5
{
...
@ -56,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

View file

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

View file

@ -4,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",
},
},
}
```

View file

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

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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