mirror of
https://github.com/element-hq/element-web
synced 2024-11-23 01:35:49 +03:00
Merge remote-tracking branch 'origin/develop' into last-admin-leave-room-warning
This commit is contained in:
commit
0fdb300858
2886 changed files with 393845 additions and 234022 deletions
|
@ -23,4 +23,7 @@ indent_size = 4
|
|||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_size = 4
|
||||
|
||||
[*.tsx.snap]
|
||||
trim_trailing_whitespace = false
|
||||
|
|
215
.eslintrc.js
215
.eslintrc.js
|
@ -1,12 +1,9 @@
|
|||
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"],
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
@ -19,7 +16,6 @@ module.exports = {
|
|||
"no-constant-condition": "off",
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"quotes": "off",
|
||||
"no-extra-boolean-cast": "off",
|
||||
|
||||
// Bind or arrow functions in props causes performance issues (but we
|
||||
|
@ -40,43 +36,125 @@ module.exports = {
|
|||
),
|
||||
],
|
||||
|
||||
"import/no-duplicates": ["error"],
|
||||
// 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/src/**",
|
||||
"!matrix-js-sdk/src/matrix",
|
||||
"!matrix-js-sdk/src/types",
|
||||
"matrix-js-sdk/lib",
|
||||
"matrix-js-sdk/lib/",
|
||||
"matrix-js-sdk/lib/**",
|
||||
// XXX: Temporarily allow these as they are not available via the main export
|
||||
"!matrix-js-sdk/src/logger",
|
||||
"!matrix-js-sdk/src/errors",
|
||||
"!matrix-js-sdk/src/utils",
|
||||
"!matrix-js-sdk/src/version-support",
|
||||
"!matrix-js-sdk/src/randomstring",
|
||||
"!matrix-js-sdk/src/sliding-sync",
|
||||
"!matrix-js-sdk/src/browser-index",
|
||||
"!matrix-js-sdk/src/feature",
|
||||
"!matrix-js-sdk/src/NamespacedValue",
|
||||
"!matrix-js-sdk/src/ReEmitter",
|
||||
"!matrix-js-sdk/src/event-mapper",
|
||||
"!matrix-js-sdk/src/interactive-auth",
|
||||
"!matrix-js-sdk/src/secret-storage",
|
||||
"!matrix-js-sdk/src/room-hierarchy",
|
||||
"!matrix-js-sdk/src/rendezvous",
|
||||
"!matrix-js-sdk/src/rendezvous/transports",
|
||||
"!matrix-js-sdk/src/rendezvous/channels",
|
||||
"!matrix-js-sdk/src/indexeddb-worker",
|
||||
"!matrix-js-sdk/src/pushprocessor",
|
||||
"!matrix-js-sdk/src/extensible_events_v1",
|
||||
"!matrix-js-sdk/src/extensible_events_v1/PollStartEvent",
|
||||
"!matrix-js-sdk/src/extensible_events_v1/PollResponseEvent",
|
||||
"!matrix-js-sdk/src/extensible_events_v1/PollEndEvent",
|
||||
"!matrix-js-sdk/src/extensible_events_v1/InvalidEventError",
|
||||
"!matrix-js-sdk/src/crypto-api",
|
||||
"!matrix-js-sdk/src/crypto-api/verification",
|
||||
"!matrix-js-sdk/src/crypto",
|
||||
"!matrix-js-sdk/src/crypto/algorithms",
|
||||
"!matrix-js-sdk/src/crypto/api",
|
||||
"!matrix-js-sdk/src/crypto/aes",
|
||||
"!matrix-js-sdk/src/crypto/backup",
|
||||
"!matrix-js-sdk/src/crypto/olmlib",
|
||||
"!matrix-js-sdk/src/crypto/crypto",
|
||||
"!matrix-js-sdk/src/crypto/keybackup",
|
||||
"!matrix-js-sdk/src/crypto/RoomList",
|
||||
"!matrix-js-sdk/src/crypto/deviceinfo",
|
||||
"!matrix-js-sdk/src/crypto/key_passphrase",
|
||||
"!matrix-js-sdk/src/crypto/CrossSigning",
|
||||
"!matrix-js-sdk/src/crypto/recoverykey",
|
||||
"!matrix-js-sdk/src/crypto/dehydration",
|
||||
"!matrix-js-sdk/src/crypto/verification",
|
||||
"!matrix-js-sdk/src/crypto/verification/SAS",
|
||||
"!matrix-js-sdk/src/crypto/verification/QRCode",
|
||||
"!matrix-js-sdk/src/crypto/verification/request",
|
||||
"!matrix-js-sdk/src/crypto/verification/request/VerificationRequest",
|
||||
"!matrix-js-sdk/src/oidc",
|
||||
"!matrix-js-sdk/src/oidc/discovery",
|
||||
"!matrix-js-sdk/src/oidc/authorize",
|
||||
"!matrix-js-sdk/src/oidc/validate",
|
||||
"!matrix-js-sdk/src/oidc/error",
|
||||
"!matrix-js-sdk/src/oidc/register",
|
||||
"!matrix-js-sdk/src/webrtc",
|
||||
"!matrix-js-sdk/src/webrtc/call",
|
||||
"!matrix-js-sdk/src/webrtc/callFeed",
|
||||
"!matrix-js-sdk/src/webrtc/mediaHandler",
|
||||
"!matrix-js-sdk/src/webrtc/callEventTypes",
|
||||
"!matrix-js-sdk/src/webrtc/callEventHandler",
|
||||
"!matrix-js-sdk/src/webrtc/groupCallEventHandler",
|
||||
"!matrix-js-sdk/src/models",
|
||||
"!matrix-js-sdk/src/models/read-receipt",
|
||||
"!matrix-js-sdk/src/models/relations-container",
|
||||
"!matrix-js-sdk/src/models/related-relations",
|
||||
],
|
||||
message: "Please use matrix-js-sdk/src/matrix instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
// There are too many a11y violations to fix at once
|
||||
// Turn violated rules off until they are fixed
|
||||
"jsx-a11y/alt-text": "off",
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/interactive-supports-focus": "off",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"jsx-a11y/mouse-events-have-key-events": "off",
|
||||
"jsx-a11y/no-autofocus": "off",
|
||||
|
@ -85,25 +163,23 @@ module.exports = {
|
|||
"jsx-a11y/no-noninteractive-tabindex": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"jsx-a11y/role-supports-aria-props": "off",
|
||||
"jsx-a11y/tabindex-no-positive": "off",
|
||||
|
||||
"matrix-org/require-copyright-header": "error",
|
||||
},
|
||||
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}", "playwright/**/*.ts"],
|
||||
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error",
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Things we do that break the ideal style
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"no-extra-boolean-cast": "off",
|
||||
|
||||
// Remove Babel things manually due to override limitations
|
||||
|
@ -117,10 +193,6 @@ module.exports = {
|
|||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
// We're okay with assertion errors when we ask for them
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
|
||||
// The non-TypeScript rule produces false positives
|
||||
"func-call-spacing": "off",
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
},
|
||||
},
|
||||
// temporary override for offending icon require files
|
||||
|
@ -153,12 +225,41 @@ 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",
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["test/**/*.{ts,tsx}", "playwright/**/*.ts"],
|
||||
extends: ["plugin:matrix-org/jest"],
|
||||
rules: {
|
||||
// We don't need super strict typing in test utilities
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||
|
||||
// Jest/Playwright specific
|
||||
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
"jest/no-disabled-tests": "off",
|
||||
// Also treat "oldBackendOnly" as a test function.
|
||||
// Used in some crypto tests.
|
||||
"jest/no-standalone-expect": [
|
||||
"error",
|
||||
{
|
||||
additionalTestBlockFunctions: ["beforeAll", "beforeEach", "oldBackendOnly"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["playwright/**/*.ts"],
|
||||
parserOptions: {
|
||||
project: ["./playwright/tsconfig.json"],
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
|
@ -168,7 +269,7 @@ module.exports = {
|
|||
};
|
||||
|
||||
function buildRestrictedPropertiesOptions(properties, message) {
|
||||
return properties.map(prop => {
|
||||
return properties.map((prop) => {
|
||||
let [object, property] = prop.split(".");
|
||||
if (object === "*") {
|
||||
object = undefined;
|
||||
|
|
2
.git-blame-ignore-revs
Normal file
2
.git-blame-ignore-revs
Normal file
|
@ -0,0 +1,2 @@
|
|||
# prettier
|
||||
526645c79160ab1ad4b4c3845de27d51263a405e
|
15
.github/CODEOWNERS
vendored
15
.github/CODEOWNERS
vendored
|
@ -1 +1,14 @@
|
|||
* @matrix-org/element-web
|
||||
* @matrix-org/element-web-reviewers
|
||||
/.github/workflows/** @matrix-org/element-web-team
|
||||
/package.json @matrix-org/element-web-team
|
||||
/yarn.lock @matrix-org/element-web-team
|
||||
|
||||
/src/SecurityManager.ts @matrix-org/element-crypto-web-reviewers
|
||||
/test/SecurityManager-test.ts @matrix-org/element-crypto-web-reviewers
|
||||
/src/async-components/views/dialogs/security/ @matrix-org/element-crypto-web-reviewers
|
||||
/src/components/views/dialogs/security/ @matrix-org/element-crypto-web-reviewers
|
||||
/test/components/views/dialogs/security/ @matrix-org/element-crypto-web-reviewers
|
||||
/src/stores/SetupEncryptionStore.ts @matrix-org/element-crypto-web-reviewers
|
||||
/test/stores/SetupEncryptionStore-test.ts @matrix-org/element-crypto-web-reviewers
|
||||
|
||||
/src/i18n/strings
|
||||
|
|
21
.github/PULL_REQUEST_TEMPLATE.md
vendored
21
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -2,20 +2,7 @@
|
|||
|
||||
## 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))
|
||||
|
||||
<!--
|
||||
If you would like to specify text for the changelog entry other than your PR title, add the following:
|
||||
|
||||
Notes: Add super cool feature
|
||||
|
||||
Changes in this project also generate changelogs in Element Web. To disable this, use the following:
|
||||
|
||||
element-web notes: none
|
||||
|
||||
or specify alternative text:
|
||||
|
||||
element-web notes: Add super cool feature
|
||||
-->
|
||||
- [ ] Tests written for new code (and old code if feasible).
|
||||
- [ ] New or updated `public`/`exported` symbols have accurate [TSDoc](https://tsdoc.org/) documentation.
|
||||
- [ ] 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)).
|
||||
|
|
1
.github/release-drafter.yml
vendored
Normal file
1
.github/release-drafter.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
_extends: matrix-org/matrix-js-sdk
|
6
.github/renovate.json
vendored
6
.github/renovate.json
vendored
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"github>matrix-org/renovate-config-element-web"
|
||||
]
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["github>matrix-org/renovate-config-element-web"]
|
||||
}
|
||||
|
|
52
.github/workflows/backport.yml
vendored
52
.github/workflows/backport.yml
vendored
|
@ -1,30 +1,30 @@
|
|||
name: Backport
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
types:
|
||||
- closed
|
||||
- labeled
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-latest
|
||||
# Only react to merged PRs for security reasons.
|
||||
# See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target.
|
||||
if: >
|
||||
github.event.pull_request.merged
|
||||
&& (
|
||||
github.event.action == 'closed'
|
||||
|| (
|
||||
github.event.action == 'labeled'
|
||||
&& contains(github.event.label.name, 'backport')
|
||||
)
|
||||
)
|
||||
steps:
|
||||
- uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e # v2
|
||||
with:
|
||||
labels_template: "<%= JSON.stringify([...labels, 'X-Release-Blocker']) %>"
|
||||
# We can't use GITHUB_TOKEN here or CI won't run on the new PR
|
||||
github_token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
172
.github/workflows/cypress.yaml
vendored
172
.github/workflows/cypress.yaml
vendored
|
@ -1,172 +0,0 @@
|
|||
# Triggers after the layered build has finished, taking the artifact and running cypress on it
|
||||
name: Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Element Web - Build" ]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
outputs:
|
||||
uuid: ${{ steps.uuid.outputs.value }}
|
||||
pr_id: ${{ steps.prdetails.outputs.pr_id }}
|
||||
commit_message: ${{ steps.commit.outputs.message }}
|
||||
commit_author: ${{ steps.commit.outputs.author }}
|
||||
commit_email: ${{ steps.commit.outputs.email }}
|
||||
percy_enable: ${{ steps.percy.outputs.value || '1' }}
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before Cypress is done.
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- id: prdetails
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
- name: Get commit details
|
||||
id: commit
|
||||
if: github.event.workflow_run.event == 'pull_request'
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
script: |
|
||||
const response = await github.rest.git.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: "${{ github.event.workflow_run.head_sha }}",
|
||||
});
|
||||
core.setOutput("message", response.data.message);
|
||||
core.setOutput("author", response.data.author.name);
|
||||
core.setOutput("email", response.data.author.email);
|
||||
|
||||
# Only run Percy when it is demanded or on develop
|
||||
- name: Disable Percy if not needed
|
||||
id: percy
|
||||
if: |
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
!contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
|
||||
run: echo "::set-output name=value::0"
|
||||
|
||||
- name: Generate unique ID 💎
|
||||
id: uuid
|
||||
run: echo "::set-output name=value::sha-$GITHUB_SHA-time-$(date +"%s")"
|
||||
|
||||
tests:
|
||||
name: "Run Tests"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment: Cypress
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Run 4 instances in Parallel
|
||||
runner: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
# XXX: We're checking out untrusted code in a secure context
|
||||
# We need to be careful to not trust anything this code outputs/may do
|
||||
# We need to check this out to access the cypress tests which are on the head branch
|
||||
repository: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: element-build-and-test.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4.1.1
|
||||
with:
|
||||
# The built-in Electron runner seems to grind to a halt trying
|
||||
# to run the tests, so use chrome.
|
||||
browser: chrome
|
||||
start: npx serve -p 8080 webapp
|
||||
wait-on: 'http://localhost:8080'
|
||||
record: true
|
||||
parallel: true
|
||||
command-prefix: 'yarn percy exec --parallel --'
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
# pass the Dashboard record key as an environment variable
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
|
||||
# Use existing chromium rather than downloading another
|
||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
||||
|
||||
# pass GitHub token to allow accurately detecting a build vs a re-run build
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# make Node's os.tmpdir() return something where we actually have permissions
|
||||
TMPDIR: ${{ runner.temp }}
|
||||
|
||||
# tell Cypress more details about the context of this run
|
||||
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
|
||||
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
|
||||
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
|
||||
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
|
||||
|
||||
# pass the Percy token as an environment variable
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
PERCY_ENABLE: ${{ needs.prepare.outputs.percy_enable }}
|
||||
PERCY_BROWSER_EXECUTABLE: /usr/bin/chromium-browser
|
||||
# tell Percy more details about the context of this run
|
||||
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
|
||||
PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
|
||||
- name: Upload Artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: cypress-results
|
||||
path: |
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/synapselogs
|
||||
|
||||
report:
|
||||
name: Report results
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- uses: Sibz/github-status-action@v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
118
.github/workflows/element-web.yaml
vendored
118
.github/workflows/element-web.yaml
vendored
|
@ -3,50 +3,88 @@
|
|||
# as an artifact and run integration tests.
|
||||
name: Element Web - Build
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
|
||||
# support triggering from other workflows
|
||||
workflow_call:
|
||||
inputs:
|
||||
react-sdk-repository:
|
||||
type: string
|
||||
required: true
|
||||
description: "The name of the github repository to check out and build."
|
||||
|
||||
matrix-js-sdk-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The Git SHA of matrix-js-sdk to build against. By default, will use a matching branch name if it exists, or develop."
|
||||
element-web-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The Git SHA of element-web to build against. By default, will use a matching branch name if it exists, or develop."
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
build:
|
||||
name: "Build Element-Web"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
run: |
|
||||
scripts/ci/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
REACT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
|
||||
echo "::set-output name=VERSION::$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA"
|
||||
- name: Fetch layered build
|
||||
id: layered_build
|
||||
env:
|
||||
# tell layered.sh to check out the right sha of the JS-SDK & EW, if they were given one
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
ELEMENT_WEB_GITHUB_BASE_REF: ${{ inputs.element-web-sha }}
|
||||
run: |
|
||||
scripts/ci/layered.sh
|
||||
JSSDK_SHA=$(git -C matrix-js-sdk rev-parse --short=12 HEAD)
|
||||
REACT_SHA=$(git rev-parse --short=12 HEAD)
|
||||
VECTOR_SHA=$(git -C element-web rev-parse --short=12 HEAD)
|
||||
echo "VERSION=$VECTOR_SHA-react-$REACT_SHA-js-$JSSDK_SHA" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
working-directory: ./element-web
|
||||
- name: Copy config
|
||||
run: cp element.io/develop/config.json config.json
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: yarn build
|
||||
working-directory: ./element-web
|
||||
- name: Build
|
||||
env:
|
||||
CI_PACKAGE: true
|
||||
VERSION: "${{ steps.layered_build.outputs.VERSION }}"
|
||||
run: |
|
||||
yarn build
|
||||
echo $VERSION > webapp/version
|
||||
working-directory: ./element-web
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: previewbuild
|
||||
path: element-web/webapp
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
# Record the react-sdk sha so our Playwright tests are from the same sha
|
||||
- name: Record react-sdk SHA
|
||||
run: |
|
||||
git rev-parse HEAD > element-web/webapp/sha
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: previewbuild
|
||||
path: element-web/webapp
|
||||
# We'll only use this in a triggered job, then we're done with it
|
||||
retention-days: 1
|
||||
|
|
191
.github/workflows/end-to-end-tests.yaml
vendored
Normal file
191
.github/workflows/end-to-end-tests.yaml
vendored
Normal file
|
@ -0,0 +1,191 @@
|
|||
# Triggers after the layered build has finished, taking the artifact and running Playwright on it
|
||||
name: End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
# support calls from other workflows for downstream testing
|
||||
workflow_call:
|
||||
inputs:
|
||||
react-sdk-repository:
|
||||
type: string
|
||||
required: true
|
||||
description: "The name of the github repository to check out and build."
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
# We create the status here and then update it to success/failure in the `report` stage
|
||||
# This provides an easy link to this workflow_run from the PR before the tests are done.
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / end-to-end-tests
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
tests:
|
||||
name: "Run Tests ${{ matrix.runner }}/${{ strategy.job-total }}"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment: EndToEndTests
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Run multiple instances in parallel to speed up the tests
|
||||
runner: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
steps:
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
# The workflow_run.head_sha is the sha of the head commit but the element-web was built using a simulated
|
||||
# merge commit - https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
# so use the sha from the tarball for the checkout of the tests
|
||||
# to make sure we get a matching set of code and tests.
|
||||
- name: Grab sha from webapp
|
||||
id: sha
|
||||
run: |
|
||||
echo "sha=$(cat webapp/sha)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# XXX: We're checking out untrusted code in a secure context
|
||||
# We need to be careful to not trust anything this code outputs/may do
|
||||
#
|
||||
# Note that (in the absence of a `react-sdk-repository` input),
|
||||
# we check out from the default repository, which is (for this workflow) the
|
||||
# *target* repository for the pull request.
|
||||
#
|
||||
ref: ${{ steps.sha.outputs.sha }}
|
||||
persist-credentials: false
|
||||
path: matrix-react-sdk
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: matrix-react-sdk/yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: matrix-react-sdk
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Get installed Playwright version
|
||||
id: playwright
|
||||
working-directory: matrix-react-sdk
|
||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v4
|
||||
id: playwright-cache
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
|
||||
|
||||
- name: Install Playwright browsers
|
||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||
working-directory: matrix-react-sdk
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
|
||||
with:
|
||||
run: yarn playwright test --shard ${{ matrix.runner }}/${{ strategy.job-total }}
|
||||
working-directory: matrix-react-sdk
|
||||
|
||||
- name: Upload blob report to GitHub Actions Artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: all-blob-reports-${{ matrix.runner }}
|
||||
path: matrix-react-sdk/blob-report
|
||||
retention-days: 1
|
||||
|
||||
report:
|
||||
name: Report results
|
||||
needs: tests
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
deployments: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ inputs.react-sdk-repository || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Download blob reports from GitHub Actions Artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: all-blob-reports-*
|
||||
path: all-blob-reports
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: yarn playwright merge-reports --reporter=html,github,./playwright/flaky-reporter.ts ./all-blob-reports
|
||||
env:
|
||||
# Only pass creds to the flaky-reporter on main branch runs
|
||||
GITHUB_TOKEN: ${{ github.event.workflow_run.head_branch == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: html-report--attempt-${{ github.run_attempt }}
|
||||
path: playwright-report
|
||||
retention-days: 14
|
||||
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
if: always()
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / end-to-end-tests
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v3
|
||||
with:
|
||||
path: playwright-report
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
desc: Playwright Report
|
||||
deployment_env: EndToEndTests
|
||||
prefix: "e2e-"
|
40
.github/workflows/i18n_check.yml
vendored
40
.github/workflows/i18n_check.yml
vendored
|
@ -1,40 +0,0 @@
|
|||
name: i18n Check
|
||||
on:
|
||||
workflow_call: { }
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: "Get modified files"
|
||||
id: changed_files
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
|
||||
uses: tj-actions/changed-files@v19
|
||||
with:
|
||||
files: |
|
||||
src/i18n/strings/*
|
||||
files_ignore: |
|
||||
src/i18n/strings/en_EN.json
|
||||
|
||||
- name: "Assert only en_EN was modified"
|
||||
if: |
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.user.login != 'RiotTranslateBot' &&
|
||||
steps.changed_files.outputs.any_modified == 'true'
|
||||
run: |
|
||||
echo "Only translation files modified by `yarn i18n` can be committed - other translation files will confuse weblate in unrecoverable ways."
|
||||
exit 1
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install --pure-lockfile"
|
||||
|
||||
- name: i18n Check
|
||||
run: "yarn run diff-i18n"
|
10
.github/workflows/localazy_download.yaml
vendored
Normal file
10
.github/workflows/localazy_download.yaml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
name: Localazy Download
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "0 6 * * 1,3,5" # Every Monday, Wednesday and Friday at 6am UTC
|
||||
jobs:
|
||||
download:
|
||||
uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_download.yaml@main
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
11
.github/workflows/localazy_upload.yaml
vendored
Normal file
11
.github/workflows/localazy_upload.yaml
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
name: Localazy Upload
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "src/i18n/strings/en_EN.json"
|
||||
jobs:
|
||||
upload:
|
||||
uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_upload.yaml@main
|
||||
secrets:
|
||||
LOCALAZY_WRITE_KEY: ${{ secrets.LOCALAZY_WRITE_KEY }}
|
105
.github/workflows/netlify.yaml
vendored
105
.github/workflows/netlify.yaml
vendored
|
@ -2,70 +2,47 @@
|
|||
# and uploading it to netlify
|
||||
name: Upload Preview Build to Netlify
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Element Web - Build" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
steps:
|
||||
- name: 📝 Create Deployment
|
||||
uses: bobheadxi/deployments@v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: Netlify
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
deploy:
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
environment: Netlify
|
||||
steps:
|
||||
- name: 📝 Create Deployment
|
||||
uses: bobheadxi/deployments@88ce5600046c82542f8246ac287d0a53c461bca3 # v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
env: Netlify
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
|
||||
- id: prdetails
|
||||
uses: matrix-org/pr-details-action@v1.2
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
# There's a 'download artifact' action, but it hasn't been updated for the workflow_run action
|
||||
# (https://github.com/actions/download-artifact/issues/60) so instead we get this mess:
|
||||
- name: 📥 Download artifact
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
workflow: element-build-and-test.yaml
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
name: previewbuild
|
||||
path: webapp
|
||||
|
||||
- name: ☁️ Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v1.2
|
||||
with:
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
# These don't work because we're in workflow_run
|
||||
enable-pull-request-comment: false
|
||||
enable-commit-comment: false
|
||||
alias: pr${{ steps.prdetails.outputs.pr_id }}
|
||||
env:
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
timeout-minutes: 1
|
||||
|
||||
- name: 🚦 Update deployment status
|
||||
uses: bobheadxi/deployments@v1
|
||||
if: always()
|
||||
with:
|
||||
step: finish
|
||||
override: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
status: ${{ job.status }}
|
||||
env: ${{ steps.deployment.outputs.env }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
env_url: ${{ steps.netlify.outputs.deploy-url }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
- name: 📤 Deploy to Netlify
|
||||
uses: matrix-org/netlify-pr-preview@v3
|
||||
with:
|
||||
path: webapp
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
revision: ${{ github.event.workflow_run.head_sha }}
|
||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
site_id: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
deployment_env: ${{ steps.deployment.outputs.env }}
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
desc: |
|
||||
Do you trust the author of this PR? Maybe this build will steal your keys or give you malware.
|
||||
Exercise caution. Use test accounts.
|
||||
|
|
32
.github/workflows/notify-element-web.yml
vendored
32
.github/workflows/notify-element-web.yml
vendored
|
@ -1,19 +1,19 @@
|
|||
name: Notify element-web
|
||||
on:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
push:
|
||||
branches: [develop]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
jobs:
|
||||
notify-element-web:
|
||||
name: "Notify Element Web"
|
||||
runs-on: ubuntu-latest
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-react-sdk'
|
||||
steps:
|
||||
- name: Notify element-web repo that a new SDK build is on develop
|
||||
uses: peter-evans/repository-dispatch@v1
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: vector-im/element-web
|
||||
event-type: element-web-notify
|
||||
notify-element-web:
|
||||
name: "Notify Element Web"
|
||||
runs-on: ubuntu-latest
|
||||
# Only respect triggers from our develop branch, ignore that of forks
|
||||
if: github.repository == 'matrix-org/matrix-react-sdk'
|
||||
steps:
|
||||
- name: Notify element-web repo that a new SDK build is on develop
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
repository: vector-im/element-web
|
||||
event-type: element-web-notify
|
||||
|
|
17
.github/workflows/pull_request.yaml
vendored
17
.github/workflows/pull_request.yaml
vendored
|
@ -1,12 +1,11 @@
|
|||
name: Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ opened, edited, labeled, unlabeled, synchronize ]
|
||||
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.head.ref }}
|
||||
pull_request_target:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
jobs:
|
||||
action:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||
with:
|
||||
labels: "T-Defect,T-Enhancement,T-Task"
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
action:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
|
9
.github/workflows/release-drafter.yml
vendored
Normal file
9
.github/workflows/release-drafter.yml
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
name: Release Drafter
|
||||
on:
|
||||
push:
|
||||
branches: [staging]
|
||||
workflow_dispatch: {}
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
draft:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
|
13
.github/workflows/release-gitflow.yml
vendored
Normal file
13
.github/workflows/release-gitflow.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Gitflow merge-back master->develop
|
||||
name: Merge master -> develop
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
concurrency: ${{ github.repository }}-${{ github.workflow }}
|
||||
jobs:
|
||||
merge:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-gitflow.yml@develop
|
||||
secrets: inherit
|
||||
with:
|
||||
dependencies: |
|
||||
matrix-js-sdk
|
31
.github/workflows/release.yml
vendored
31
.github/workflows/release.yml
vendored
|
@ -1,11 +1,26 @@
|
|||
name: Release Process
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: What type of release
|
||||
required: true
|
||||
default: rc
|
||||
type: choice
|
||||
options:
|
||||
- rc
|
||||
- final
|
||||
npm:
|
||||
description: Publish to npm
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
concurrency: ${{ github.workflow }}
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
|
||||
secrets: inherit
|
||||
with:
|
||||
final: ${{ inputs.mode == 'final' }}
|
||||
npm: ${{ inputs.npm }}
|
||||
downstreams: '["element-hq/element-web"]'
|
||||
|
|
26
.github/workflows/sonarqube.yml
vendored
26
.github/workflows/sonarqube.yml
vendored
|
@ -1,15 +1,19 @@
|
|||
name: SonarQube
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ "Tests" ]
|
||||
types:
|
||||
- completed
|
||||
workflow_run:
|
||||
workflows: ["Tests"]
|
||||
types:
|
||||
- completed
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
sonarqube:
|
||||
name: 🩻 SonarQube
|
||||
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'merge_group'
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
with:
|
||||
sharded: true
|
||||
|
|
192
.github/workflows/static_analysis.yaml
vendored
192
.github/workflows/static_analysis.yaml
vendored
|
@ -1,94 +1,144 @@
|
|||
name: Static Analysis
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
ts_lint:
|
||||
name: "Typescript Syntax Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
working-directory: node_modules/matrix-js-sdk
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
|
||||
# Temporary while we directly import matrix-js-sdk/src/* which means we need
|
||||
# certain @types/* packages to make sense of matrix-js-sdk types.
|
||||
#- name: Typecheck (release mode; no yarn link)
|
||||
# if: github.event_name != 'pull_request' && github.ref_name != 'master'
|
||||
# run: |
|
||||
# yarn unlink matrix-js-sdk
|
||||
# yarn add github:matrix-org/matrix-js-sdk#develop
|
||||
# yarn install --force
|
||||
# yarn run lint:types
|
||||
|
||||
i18n_lint:
|
||||
name: "i18n Check"
|
||||
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
|
||||
with:
|
||||
cache: 'yarn'
|
||||
hardcoded-words: "Element"
|
||||
allowed-hardcoded-keys: |
|
||||
console_dev_note
|
||||
labs|element_call_video_rooms
|
||||
labs|feature_disable_call_per_sender_encryption
|
||||
voip|element_call
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
rethemendex_lint:
|
||||
name: "Rethemendex Check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
- run: ./res/css/rethemendex.sh
|
||||
|
||||
- name: Switch js-sdk to release mode
|
||||
working-directory: node_modules/matrix-js-sdk
|
||||
run: |
|
||||
scripts/switch_package_to_release.js
|
||||
yarn install
|
||||
yarn run build:compile
|
||||
yarn run build:types
|
||||
- run: git diff --exit-code
|
||||
|
||||
- name: Typecheck (release mode)
|
||||
run: "yarn run lint:types"
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
i18n_lint:
|
||||
name: "i18n Check"
|
||||
uses: matrix-org/matrix-react-sdk/.github/workflows/i18n_check.yml@develop
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
js_lint:
|
||||
name: "ESLint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
style_lint:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
style_lint:
|
||||
name: "Style Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:style"
|
||||
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install"
|
||||
workflow_lint:
|
||||
name: "Workflow Lint"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:style"
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# Does not need branch matching as only analyses this layer
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- name: Run Linter
|
||||
run: "yarn lint:workflows"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
analyse_dead_code:
|
||||
name: "Analyse Dead Code"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Dead Code Analysis
|
||||
run: |
|
||||
cd element-web
|
||||
yarn run analyse:unused-exports
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "scripts/ci/layered.sh"
|
||||
|
||||
- name: Dead Code Analysis
|
||||
run: |
|
||||
cd element-web
|
||||
yarn run analyse:unused-exports
|
||||
|
|
150
.github/workflows/tests.yml
vendored
150
.github/workflows/tests.yml
vendored
|
@ -1,50 +1,122 @@
|
|||
name: Tests
|
||||
on:
|
||||
pull_request: { }
|
||||
push:
|
||||
branches: [ develop, master ]
|
||||
repository_dispatch:
|
||||
types: [ upstream-sdk-notify ]
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
push:
|
||||
branches: [develop, master]
|
||||
repository_dispatch:
|
||||
types: [upstream-sdk-notify]
|
||||
workflow_call:
|
||||
inputs:
|
||||
disable_coverage:
|
||||
type: boolean
|
||||
required: false
|
||||
description: "Specify true to skip generating and uploading coverage for tests"
|
||||
matrix-js-sdk-sha:
|
||||
type: string
|
||||
required: false
|
||||
description: "The matrix-js-sdk SHA to use"
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# These must be set for fetchdep.sh to get the right branch
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' && inputs.disable_coverage != 'true' }}
|
||||
# fetchdep.sh needs to know our PR number
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Run multiple instances in parallel to speed up the tests
|
||||
runner: [1, 2]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'matrix-org/matrix-react-sdk' || github.repository }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- name: Install Deps
|
||||
run: "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
env:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: "yarn coverage --ci"
|
||||
- name: Jest Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/jest_cache
|
||||
key: ${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
|
||||
|
||||
app-tests:
|
||||
name: Element Web Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
--shard ${{ matrix.runner }}/${{ strategy.job-total }} \
|
||||
--cacheDirectory /tmp/jest_cache
|
||||
env:
|
||||
JEST_SONAR_UNIQUE_OUTPUT_NAME: true
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: 'yarn'
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
|
||||
- name: Run tests
|
||||
run: "./scripts/ci/app-tests.sh"
|
||||
- name: Move coverage files into place
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.runner }}.lcov.info
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-${{ matrix.runner }}
|
||||
path: |
|
||||
coverage
|
||||
!coverage/lcov-report
|
||||
|
||||
skip_sonar:
|
||||
name: Skip SonarCloud in merge queue
|
||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
needs: jest
|
||||
steps:
|
||||
- name: Skip SonarCloud
|
||||
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: SonarCloud skipped
|
||||
context: SonarCloud Code Analysis
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
app-tests:
|
||||
name: Element Web Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'matrix-org/matrix-react-sdk' || github.repository }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
|
||||
- name: Run tests
|
||||
run: "./scripts/ci/app-tests.sh"
|
||||
env:
|
||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||
|
|
8
.github/workflows/upgrade_dependencies.yml
vendored
8
.github/workflows/upgrade_dependencies.yml
vendored
|
@ -1,8 +0,0 @@
|
|||
name: Upgrade Dependencies
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
jobs:
|
||||
upgrade:
|
||||
uses: matrix-org/matrix-js-sdk/.github/workflows/upgrade_dependencies.yml@develop
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -19,11 +19,3 @@ package-lock.json
|
|||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
/cypress/videos
|
||||
/cypress/downloads
|
||||
/cypress/screenshots
|
||||
/cypress/synapselogs
|
||||
# These could have files in them but don't currently
|
||||
# Cypress will still auto-create them though...
|
||||
/cypress/performance
|
||||
|
|
|
@ -1 +1 @@
|
|||
16
|
||||
20
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
version: 2
|
||||
snapshot:
|
||||
widths:
|
||||
- 1024
|
||||
- 1920
|
||||
percy:
|
||||
defer-uploads: true
|
22
.prettierignore
Normal file
22
.prettierignore
Normal file
|
@ -0,0 +1,22 @@
|
|||
/coverage
|
||||
/lib
|
||||
|
||||
/.idea
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
# Legacy skinning file that some people might still have
|
||||
/src/component-index.js
|
||||
|
||||
/.npmrc
|
||||
/*.log
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
/src/i18n/strings
|
||||
|
||||
# This file is owned, parsed, and generated by allchange, which doesn't comply with prettier
|
||||
/CHANGELOG.md
|
||||
|
||||
# This file is also machine-generated
|
||||
/playwright/e2e/crypto/test_indexeddb_cryptostore_dump/dump.json
|
1
.prettierrc.js
Normal file
1
.prettierrc.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require("eslint-plugin-matrix-org/.prettierrc.js");
|
|
@ -1,47 +1,43 @@
|
|||
module.exports = {
|
||||
"extends": "stylelint-config-standard",
|
||||
customSyntax: require('postcss-scss'),
|
||||
"plugins": [
|
||||
"stylelint-scss",
|
||||
],
|
||||
"rules": {
|
||||
"color-hex-case": null,
|
||||
"indentation": 4,
|
||||
extends: ["stylelint-config-standard"],
|
||||
customSyntax: require("postcss-scss"),
|
||||
plugins: ["stylelint-scss"],
|
||||
rules: {
|
||||
"comment-empty-line-before": null,
|
||||
"declaration-empty-line-before": null,
|
||||
"length-zero-no-unit": null,
|
||||
"rule-empty-line-before": null,
|
||||
"color-hex-length": null,
|
||||
"max-empty-lines": 1,
|
||||
"no-eol-whitespace": true,
|
||||
"number-no-trailing-zeros": null,
|
||||
"number-leading-zero": null,
|
||||
"selector-list-comma-newline-after": null,
|
||||
"at-rule-no-unknown": null,
|
||||
"no-descending-specificity": null,
|
||||
"no-empty-first-line": true,
|
||||
"scss/at-rule-no-unknown": [true, {
|
||||
// https://github.com/vector-im/element-web/issues/10544
|
||||
"ignoreAtRules": ["define-mixin"],
|
||||
}],
|
||||
"scss/at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
// https://github.com/vector-im/element-web/issues/10544
|
||||
ignoreAtRules: ["define-mixin"],
|
||||
},
|
||||
],
|
||||
// Disable `&_kind`-style selectors while our unused CSS approach is "Find & Replace All"
|
||||
// rather than a CI thing. Shorthand selectors are harder to detect when searching for a
|
||||
// class name. This regex is trying to *allow* anything except `&words`, such as `&::before`,
|
||||
// `&.mx_Class`, etc.
|
||||
"selector-nested-pattern": "^((&[ :.\\\[,])|([^&]))",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"selector-nested-pattern": "^((&[ :.\\[,])|([^&]))",
|
||||
// Disable some defaults
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"selector-id-pattern": null,
|
||||
"keyframes-name-pattern": null,
|
||||
"string-quotes": null,
|
||||
"alpha-value-notation": null,
|
||||
"color-function-notation": null,
|
||||
"selector-not-notation": null,
|
||||
"import-notation": null,
|
||||
"value-keyword-case": null,
|
||||
"declaration-block-no-redundant-longhand-properties": null,
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
// useful for fallbacks
|
||||
{ ignore: ["consecutive-duplicates-with-different-values"] },
|
||||
],
|
||||
"shorthand-property-no-redundant-values": null,
|
||||
"property-no-vendor-prefix": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
|
@ -49,5 +45,6 @@ module.exports = {
|
|||
"media-feature-name-no-vendor-prefix": null,
|
||||
"number-max-precision": null,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"media-feature-range-notation": null,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
1237
CHANGELOG.md
1237
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
|||
Contributing code to matrix-react-sdk
|
||||
=====================================
|
||||
# Contributing code to matrix-react-sdk
|
||||
|
||||
matrix-react-sdk follows the same pattern as https://github.com/vector-im/element-web/blob/develop/CONTRIBUTING.md
|
||||
|
|
126
README.md
126
README.md
|
@ -1,26 +1,25 @@
|
|||
[![npm](https://img.shields.io/npm/v/matrix-react-sdk)](https://www.npmjs.com/package/matrix-react-sdk)
|
||||
![Tests](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/tests.yml/badge.svg)
|
||||
[![Playwright](https://img.shields.io/badge/Playwright-end_to_end_tests-blue)](https://e2e-develop--matrix-react-sdk.netlify.app/)
|
||||
![Static Analysis](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/static_analysis.yaml/badge.svg)
|
||||
[![matrix-react-sdk](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ppvnzg/develop&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/ppvnzg/runs)
|
||||
[![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/dfde73bd/matrix-react-sdk)
|
||||
[![Weblate](https://translate.element.io/widgets/element-web/-/matrix-react-sdk/svg-badge.svg)](https://translate.element.io/engage/element-web/)
|
||||
[![Localazy](https://img.shields.io/endpoint?url=https%3A%2F%2Fconnect.localazy.com%2Fstatus%2Felement-web%2Fdata%3Fcontent%3Dall%26title%3Dlocalazy%26logo%3Dtrue)](https://localazy.com/p/element-web)
|
||||
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=coverage)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=matrix-react-sdk&metric=bugs)](https://sonarcloud.io/summary/new_code?id=matrix-react-sdk)
|
||||
|
||||
matrix-react-sdk
|
||||
================
|
||||
# matrix-react-sdk
|
||||
|
||||
This is a react-based SDK for inserting a Matrix chat/voip client into a web page.
|
||||
|
||||
This package provides the React components needed to build a Matrix web client
|
||||
using React. It is not useable in isolation, and instead must be used from
|
||||
using React. It is not useable in isolation, and instead must be used from
|
||||
a 'skin'. A skin provides:
|
||||
* Customised implementations of presentation components.
|
||||
* Custom CSS
|
||||
* The containing application
|
||||
* Zero or more 'modules' containing non-UI functionality
|
||||
|
||||
- Customised implementations of presentation components.
|
||||
- Custom CSS
|
||||
- The containing application
|
||||
- Zero or more 'modules' containing non-UI functionality
|
||||
|
||||
As of Aug 2018, the only skin that exists is
|
||||
[`vector-im/element-web`](https://github.com/vector-im/element-web/); it and
|
||||
|
@ -28,19 +27,15 @@ 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](https://translate.element.io/widgets/element-web/-/multi-auto.svg)](https://translate.element.io/engage/element-web/?utm_source=widget)
|
||||
|
||||
Developer Guide
|
||||
---------------
|
||||
## Developer Guide
|
||||
|
||||
Platform Targets:
|
||||
* Chrome, Firefox and Safari.
|
||||
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||
* Mobile Web is not currently a target platform - instead please use the native
|
||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||
|
||||
- Chrome, Firefox and Safari.
|
||||
- WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||
- Mobile Web is not currently a target platform - instead please use the native
|
||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||
|
||||
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||
**Please file PRs against `develop`!!**
|
||||
|
@ -52,22 +47,23 @@ Our code style is also the same as Element's:
|
|||
https://github.com/vector-im/element-web/blob/develop/code_style.md
|
||||
|
||||
Code should be committed as follows:
|
||||
* All new components:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||
* Element-specific components:
|
||||
https://github.com/vector-im/element-web/tree/master/src/components
|
||||
* In practice, `matrix-react-sdk` is still evolving so fast that the
|
||||
maintenance burden of customising and overriding these components for
|
||||
Element can seriously impede development. So right now, there should be
|
||||
very few (if any) customisations for Element.
|
||||
* CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
|
||||
* Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
|
||||
- All new components:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/src/components
|
||||
- Element-specific components:
|
||||
https://github.com/vector-im/element-web/tree/master/src/components
|
||||
- In practice, `matrix-react-sdk` is still evolving so fast that the
|
||||
maintenance burden of customising and overriding these components for
|
||||
Element can seriously impede development. So right now, there should be
|
||||
very few (if any) customisations for Element.
|
||||
- CSS: https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css
|
||||
- Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
|
||||
React components in matrix-react-sdk come in two different flavours:
|
||||
'structures' and 'views'. Structures are stateful components which handle the
|
||||
'structures' and 'views'. Structures are stateful components which handle the
|
||||
more complicated business logic of the app, delegating their actual presentation
|
||||
rendering to stateless 'view' components. For instance, the RoomView component
|
||||
rendering to stateless 'view' components. For instance, the RoomView component
|
||||
that orchestrates the act of visualising the contents of a given Matrix chat
|
||||
room tracks lots of state for its child components which it passes into them for
|
||||
visual rendering via props.
|
||||
|
@ -75,74 +71,72 @@ visual rendering via props.
|
|||
Good separation between the components is maintained by adopting various best
|
||||
practices that anyone working with the SDK needs to be aware of and uphold:
|
||||
|
||||
* Components are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||
- Components are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||
|
||||
* They are organised in a typically two-level hierarchy - first whether the
|
||||
- They are organised in a typically two-level hierarchy - first whether the
|
||||
component is a view or a structure, and then a broad functional grouping
|
||||
(e.g. 'rooms' here)
|
||||
|
||||
* The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
||||
- The view's CSS file MUST have the same name (e.g. view/rooms/MessageTile.css).
|
||||
CSS for matrix-react-sdk currently resides in
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/css.
|
||||
|
||||
* Per-view CSS is optional - it could choose to inherit all its styling from
|
||||
- Per-view CSS is optional - it could choose to inherit all its styling from
|
||||
the context of the rest of the app, although this is unusual for any but
|
||||
* Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
structural components (lacking presentation logic) and the simplest view
|
||||
components.
|
||||
- Theme specific CSS & resources:
|
||||
https://github.com/matrix-org/matrix-react-sdk/tree/master/res/themes
|
||||
structural components (lacking presentation logic) and the simplest view
|
||||
components.
|
||||
|
||||
* The view MUST *only* refer to the CSS rules defined in its own CSS file.
|
||||
- The view MUST _only_ refer to the CSS rules defined in its own CSS file.
|
||||
'Stealing' styling information from other components (including parents)
|
||||
is not cool, as it breaks the independence of the components.
|
||||
|
||||
* CSS classes are named with an app-specific name-spacing prefix to try to
|
||||
avoid CSS collisions. The base skin shipped by Matrix.org with the
|
||||
matrix-react-sdk uses the naming prefix "mx_". A company called Yoyodyne
|
||||
Inc might use a prefix like "yy_" for its app-specific classes.
|
||||
- CSS classes are named with an app-specific name-spacing prefix to try to
|
||||
avoid CSS collisions. The base skin shipped by Matrix.org with the
|
||||
matrix-react-sdk uses the naming prefix "mx*". A company called Yoyodyne
|
||||
Inc might use a prefix like "yy*" for its app-specific classes.
|
||||
|
||||
* CSS classes use upper camel case when they describe React components - e.g.
|
||||
- CSS classes use upper camel case when they describe React components - e.g.
|
||||
.mx_MessageTile is the selector for the CSS applied to a MessageTile view.
|
||||
|
||||
* CSS classes for DOM elements within a view which aren't components are named
|
||||
- CSS classes for DOM elements within a view which aren't components are named
|
||||
by appending a lower camel case identifier to the view's class name - e.g.
|
||||
.mx_MessageTile_randomDiv is how you'd name the class of an arbitrary div
|
||||
within the MessageTile view.
|
||||
|
||||
* We deliberately use vanilla CSS 3.0 to avoid adding any more magic
|
||||
dependencies into the mix than we already have. App developers are welcome
|
||||
to use whatever floats their boat however. In future we'll start using
|
||||
- We deliberately use vanilla CSS 3.0 to avoid adding any more magic
|
||||
dependencies into the mix than we already have. App developers are welcome
|
||||
to use whatever floats their boat however. In future we'll start using
|
||||
css-next to pull in features like CSS variable support.
|
||||
|
||||
* The CSS for a component can override the rules for child components.
|
||||
For instance, .mx_RoomList .mx_RoomTile {} would be the selector to override
|
||||
- The CSS for a component can override the rules for child components.
|
||||
For instance, .mx*RoomList .mx_RoomTile {} would be the selector to override
|
||||
styles of RoomTiles when viewed in the context of a RoomList view.
|
||||
Overrides *must* be scoped to the View's CSS class - i.e. don't just define
|
||||
Overrides \_must* be scoped to the View's CSS class - i.e. don't just define
|
||||
.mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its
|
||||
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
|
||||
only to the context of RoomList views. N.B. overrides should be relatively
|
||||
own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override
|
||||
only to the context of RoomList views. N.B. overrides should be relatively
|
||||
rare as in general CSS inheritance should be enough.
|
||||
|
||||
* Components should render only within the bounding box of their outermost DOM
|
||||
- Components should render only within the bounding box of their outermost DOM
|
||||
element. Page-absolute positioning and negative CSS margins and similar are
|
||||
generally not cool and stop the component from being reused easily in
|
||||
different places.
|
||||
|
||||
Originally `matrix-react-sdk` followed the Atomic design pattern as per
|
||||
http://patternlab.io to try to encourage a modular architecture. However, we
|
||||
http://patternlab.io to try to encourage a modular architecture. However, we
|
||||
found that the grouping of components into atoms/molecules/organisms
|
||||
made them harder to find relative to a functional split, and didn't emphasise
|
||||
the distinction between 'structural' and 'view' components, so we backed away
|
||||
from it.
|
||||
|
||||
Github Issues
|
||||
-------------
|
||||
## Github Issues
|
||||
|
||||
All issues should be filed under https://github.com/vector-im/element-web/issues
|
||||
for now.
|
||||
|
||||
Development
|
||||
-----------
|
||||
## Development
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
|
@ -210,7 +204,5 @@ Now the yarn commands should work as normal.
|
|||
|
||||
### End-to-End tests
|
||||
|
||||
Make sure you've got your Element development server running (by doing `yarn
|
||||
start` in element-web), and then in this project, run `yarn run test:cypress`. See
|
||||
[`docs/cypress.md`](https://github.com/matrix-org/matrix-react-sdk/blob/develop/docs/cypress.md)
|
||||
for more information.
|
||||
We use Playwright and Element Web for end-to-end tests. See
|
||||
[`docs/playwright.md`](docs/playwright.md) for more information.
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
{
|
||||
"en": {
|
||||
"fileName": "en_EN.json",
|
||||
"label": "English"
|
||||
},
|
||||
"en-us": {
|
||||
"fileName": "en_US.json",
|
||||
"label": "English (US)"
|
||||
}
|
||||
"en": "en_EN.json",
|
||||
"en-us": "en_US.json"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
const EventEmitter = require("events");
|
||||
const { LngLat, NavigationControl, LngLatBounds, AttributionControl } = require('maplibre-gl');
|
||||
const { LngLat, NavigationControl, LngLatBounds } = require("maplibre-gl");
|
||||
|
||||
class MockMap extends EventEmitter {
|
||||
addControl = jest.fn();
|
||||
|
@ -28,11 +28,12 @@ class MockMap extends EventEmitter {
|
|||
}
|
||||
const MockMapInstance = new MockMap();
|
||||
|
||||
class MockAttributionControl {}
|
||||
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);
|
||||
|
@ -43,5 +44,5 @@ module.exports = {
|
|||
LngLat,
|
||||
LngLatBounds,
|
||||
NavigationControl,
|
||||
AttributionControl,
|
||||
AttributionControl: MockAttributionControl,
|
||||
};
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export const Icon = 'div';
|
||||
export const Icon = "div";
|
||||
export default "image-file-stub";
|
||||
|
|
19
__mocks__/workerFactoryMock.js
Normal file
19
__mocks__/workerFactoryMock.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export default function workerFactory(options) {
|
||||
return jest.fn;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
module.exports = jest.fn();
|
|
@ -1,18 +1,21 @@
|
|||
module.exports = {
|
||||
"sourceMaps": "inline",
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Safari versions",
|
||||
"last 2 Edge versions",
|
||||
],
|
||||
}],
|
||||
sourceMaps: "inline",
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
targets: [
|
||||
"last 2 Chrome versions",
|
||||
"last 2 Firefox versions",
|
||||
"last 2 Safari versions",
|
||||
"last 2 Edge versions",
|
||||
],
|
||||
},
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react",
|
||||
],
|
||||
"plugins": [
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-export-default-from",
|
||||
"@babel/plugin-proposal-numeric-separator",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
videoUploadOnPasses: false,
|
||||
projectId: 'ppvnzg',
|
||||
experimentalInteractiveRunEvents: true,
|
||||
defaultCommandTimeout: 10000,
|
||||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.ts').default(on, config);
|
||||
},
|
||||
baseUrl: 'http://localhost:8080',
|
||||
experimentalSessionAndOrigin: true,
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
retries: {
|
||||
runMode: 4,
|
||||
openMode: 0,
|
||||
},
|
||||
});
|
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
|
||||
describe("Composer", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
describe("CIDER", () => {
|
||||
beforeEach(() => {
|
||||
cy.initTestUser(synapse, "Janet").then(() => {
|
||||
cy.createRoom({ name: "Composing Room" });
|
||||
});
|
||||
cy.viewRoomByName("Composing Room");
|
||||
});
|
||||
|
||||
it("sends a message when you click send or press Enter", () => {
|
||||
// Type a message
|
||||
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');
|
||||
|
||||
// Click send
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
// It has been sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 0');
|
||||
|
||||
// Type another and press Enter afterwards
|
||||
cy.get('div[contenteditable=true]').type('my message 1{enter}');
|
||||
// It was sent
|
||||
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[aria-label="Send message"]').click();
|
||||
// Note: both "bold" and "message" are bold, which is probably surprising
|
||||
cy.contains('.mx_EventTile_body strong', 'bold message');
|
||||
});
|
||||
|
||||
it("should allow user to input emoji via graphical picker", () => {
|
||||
cy.getComposer(false).within(() => {
|
||||
cy.get('[aria-label="Emoji"]').click();
|
||||
});
|
||||
|
||||
cy.get('[data-testid="mx_EmojiPicker"]').within(() => {
|
||||
cy.contains(".mx_EmojiPicker_item", "😇").click();
|
||||
});
|
||||
|
||||
cy.get(".mx_ContextualMenu_background").click(); // Close emoji picker
|
||||
cy.get('div[contenteditable=true]').type("{enter}"); // Send message
|
||||
|
||||
cy.contains(".mx_EventTile_body", "😇");
|
||||
});
|
||||
|
||||
describe("when Ctrl+Enter is required to send", () => {
|
||||
beforeEach(() => {
|
||||
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
||||
});
|
||||
|
||||
it("only sends when you press Ctrl+Enter", () => {
|
||||
// Type a message and press 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');
|
||||
|
||||
// Press Ctrl+Enter
|
||||
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
|
||||
// It was sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 3');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("WYSIWYG", () => {
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_wysiwyg_composer");
|
||||
cy.initTestUser(synapse, "Janet").then(() => {
|
||||
cy.createRoom({ name: "Composing Room" });
|
||||
});
|
||||
cy.viewRoomByName("Composing Room");
|
||||
});
|
||||
|
||||
it("sends a message when you click send or press Enter", () => {
|
||||
// Type a message
|
||||
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');
|
||||
|
||||
// Click send
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
// It has been sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 0');
|
||||
|
||||
// Type another
|
||||
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" });
|
||||
// It was sent
|
||||
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[aria-label="Send message"]').click();
|
||||
cy.contains('.mx_EventTile_body strong', 'bold');
|
||||
});
|
||||
|
||||
describe("when Ctrl+Enter is required to send", () => {
|
||||
beforeEach(() => {
|
||||
cy.setSettingValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
||||
});
|
||||
|
||||
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" });
|
||||
// It has not been sent yet
|
||||
cy.contains('.mx_EventTile_body', 'my message 3').should('not.exist');
|
||||
|
||||
// Press Ctrl+Enter
|
||||
cy.get('div[contenteditable=true]').type('{ctrl+enter}');
|
||||
// It was sent
|
||||
cy.contains('.mx_EventTile_body', 'my message 3');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
function openCreateRoomDialog(): Chainable<JQuery<HTMLElement>> {
|
||||
cy.get('[aria-label="Add room"]').click();
|
||||
cy.get('.mx_ContextualMenu [aria-label="New room"]').click();
|
||||
return cy.get(".mx_CreateRoomDialog");
|
||||
}
|
||||
|
||||
describe("Create Room", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Jim");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should allow us to create a public room with name, topic & address set", () => {
|
||||
const name = "Test room 1";
|
||||
const topic = "This room is dedicated to this test and this test only!";
|
||||
|
||||
openCreateRoomDialog().within(() => {
|
||||
// Fill name & topic
|
||||
cy.get('[label="Name"]').type(name);
|
||||
cy.get('[label="Topic (optional)"]').type(topic);
|
||||
// Change room to public
|
||||
cy.get('[aria-label="Room visibility"]').click();
|
||||
cy.get("#mx_JoinRuleDropdown__public").click();
|
||||
// Fill room address
|
||||
cy.get('[label="Room address"]').type("test-room-1");
|
||||
// Submit
|
||||
cy.get(".mx_Dialog_primary").click();
|
||||
});
|
||||
|
||||
cy.url().should("contain", "/#/room/#test-room-1:localhost");
|
||||
cy.contains(".mx_RoomHeader_nametext", name);
|
||||
cy.contains(".mx_RoomHeader_topic", topic);
|
||||
});
|
||||
});
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
interface CryptoTestContext extends Mocha.Context {
|
||||
synapse: SynapseInstance;
|
||||
bob: MatrixClient;
|
||||
}
|
||||
|
||||
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
|
||||
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);
|
||||
resolve(request);
|
||||
};
|
||||
// @ts-ignore
|
||||
cli.on("crypto.verification.request", onVerificationRequestEvent);
|
||||
});
|
||||
};
|
||||
|
||||
const openRoomInfo = () => {
|
||||
cy.get(".mx_RightPanel_roomSummaryButton").click();
|
||||
return cy.get(".mx_RightPanel");
|
||||
};
|
||||
|
||||
const checkDMRoom = () => {
|
||||
cy.contains(".mx_TextualEvent", "Alice invited Bob").should("exist");
|
||||
cy.contains(".mx_RoomView_body .mx_cryptoEvent", "Encryption enabled").should("exist");
|
||||
};
|
||||
|
||||
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();
|
||||
cy.contains(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name", "Bob").should("exist");
|
||||
cy.get(".mx_InviteDialog_goButton").click();
|
||||
};
|
||||
|
||||
const testMessages = function(this: CryptoTestContext) {
|
||||
// check the invite message
|
||||
cy.contains(".mx_EventTile_body", "Hey!").closest(".mx_EventTile").within(() => {
|
||||
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
|
||||
});
|
||||
|
||||
// Bob sends a response
|
||||
cy.get<Room>("@bobsRoom").then((room) => {
|
||||
this.bob.sendTextMessage(room.roomId, "Hoo!");
|
||||
});
|
||||
cy.contains(".mx_EventTile_body", "Hoo!")
|
||||
.closest(".mx_EventTile")
|
||||
.should("not.have.descendants", ".mx_EventTile_e2eIcon_warning");
|
||||
};
|
||||
|
||||
const bobJoin = function(this: CryptoTestContext) {
|
||||
cy.window({ log: false }).then(async win => {
|
||||
const bobRooms = this.bob.getRooms();
|
||||
if (!bobRooms.length) {
|
||||
await new Promise<void>(resolve => {
|
||||
const onMembership = (_event) => {
|
||||
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
resolve();
|
||||
};
|
||||
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
|
||||
});
|
||||
|
||||
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||
};
|
||||
|
||||
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
||||
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
verifier.off("show_sas", onShowSas);
|
||||
event.confirm();
|
||||
verifier.done();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
const verifier = request.beginKeyVerification("m.sas.v1");
|
||||
verifier.on("show_sas", onShowSas);
|
||||
verifier.verify();
|
||||
}));
|
||||
};
|
||||
|
||||
const verify = function(this: CryptoTestContext) {
|
||||
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
|
||||
|
||||
openRoomInfo().within(() => {
|
||||
cy.get(".mx_RoomSummaryCard_icon_people").click();
|
||||
cy.contains(".mx_EntityTile_name", "Bob").click();
|
||||
cy.contains(".mx_UserInfo_verifyButton", "Verify").click();
|
||||
cy.contains(".mx_AccessibleButton", "Start Verification").click();
|
||||
cy.wrap(bobsVerificationRequestPromise).then((verificationRequest: VerificationRequest) => {
|
||||
verificationRequest.accept();
|
||||
return verificationRequest;
|
||||
}).as("bobsVerificationRequest");
|
||||
cy.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) => {
|
||||
emojis.forEach((emoji: EmojiMapping, index: number) => {
|
||||
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
cy.contains(".mx_AccessibleButton", "They match").click();
|
||||
cy.contains("You've successfully verified Bob!").should("exist");
|
||||
cy.contains(".mx_AccessibleButton", "Got it").click();
|
||||
});
|
||||
};
|
||||
|
||||
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) {
|
||||
cy.stopSynapse(this.synapse);
|
||||
});
|
||||
|
||||
it("setting up secure key backup should work", () => {
|
||||
cy.openUserSettings("Security & Privacy");
|
||||
cy.contains(".mx_AccessibleButton", "Set up Secure Backup").click();
|
||||
cy.get(".mx_Dialog").within(() => {
|
||||
cy.contains(".mx_Dialog_primary", "Continue").click();
|
||||
cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey");
|
||||
// Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
|
||||
cy.contains(".mx_AccessibleButton", "Download").click();
|
||||
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
|
||||
cy.contains(".mx_Dialog_title", "Setting up keys").should("exist");
|
||||
cy.contains(".mx_Dialog_title", "Setting up keys").should("not.exist");
|
||||
});
|
||||
return;
|
||||
});
|
||||
|
||||
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}");
|
||||
checkDMRoom();
|
||||
bobJoin.call(this);
|
||||
testMessages.call(this);
|
||||
verify.call(this);
|
||||
});
|
||||
});
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { MessageEvent } from "matrix-events-sdk";
|
||||
|
||||
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
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,
|
||||
);
|
||||
};
|
||||
|
||||
describe("Editing", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Edith").then(() => {
|
||||
cy.injectAxe();
|
||||
return cy.createRoom({ name: "Test room" }).as("roomId");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should close the composer when clicking save after making a change and undoing it", () => {
|
||||
cy.get<string>("@roomId").then(roomId => {
|
||||
sendEvent(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
// Edit message
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
|
||||
cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_BasicMessageComposer_input").type("Foo{backspace}{backspace}{backspace}{enter}");
|
||||
cy.checkA11y();
|
||||
});
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Message");
|
||||
|
||||
// Assert that the edit composer has gone away
|
||||
cy.get(".mx_EditMessageComposer").should("not.exist");
|
||||
});
|
||||
});
|
|
@ -1,251 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
import { UserCredentials } from "../../support/login";
|
||||
|
||||
const ROOM_NAME = "Integration Manager Test";
|
||||
const USER_DISPLAY_NAME = "Alice";
|
||||
const BOT_DISPLAY_NAME = "Bob";
|
||||
const KICK_REASON = "Goodbye";
|
||||
|
||||
const INTEGRATION_MANAGER_TOKEN = "DefinitelySecret_DoNotUseThisForReal";
|
||||
const INTEGRATION_MANAGER_HTML = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Fake Integration Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
<input type="text" id="target-room-id"/>
|
||||
<input type="text" id="target-user-id"/>
|
||||
<button name="Send" id="send-action">Press to send action</button>
|
||||
<button name="Close" id="close">Press to close</button>
|
||||
<script>
|
||||
document.getElementById("send-action").onclick = () => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: "kick",
|
||||
room_id: document.getElementById("target-room-id").value,
|
||||
user_id: document.getElementById("target-user-id").value,
|
||||
reason: "${KICK_REASON}",
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
document.getElementById("close").onclick = () => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
action: "close_scalar",
|
||||
},
|
||||
'*',
|
||||
);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
function openIntegrationManager() {
|
||||
cy.get(".mx_RightPanel_roomSummaryButton").click();
|
||||
cy.get(".mx_RoomSummaryCard_appsGroup").within(() => {
|
||||
cy.contains("Add widgets, bridges & bots").click();
|
||||
});
|
||||
}
|
||||
|
||||
function closeIntegrationManager(integrationManagerUrl: string) {
|
||||
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||
cy.get("#close").should("exist").click();
|
||||
});
|
||||
}
|
||||
|
||||
function sendActionFromIntegrationManager(integrationManagerUrl: string, targetRoomId: string, targetUserId: string) {
|
||||
cy.accessIframe(`iframe[src*="${integrationManagerUrl}"]`).within(() => {
|
||||
cy.get("#target-room-id").should("exist").type(targetRoomId);
|
||||
cy.get("#target-user-id").should("exist").type(targetUserId);
|
||||
cy.get("#send-action").should("exist").click();
|
||||
});
|
||||
}
|
||||
|
||||
function expectKickedMessage(shouldExist: boolean) {
|
||||
// Expand any event summaries
|
||||
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");
|
||||
}
|
||||
|
||||
describe("Integration Manager: Kick", () => {
|
||||
let testUser: UserCredentials;
|
||||
let synapse: SynapseInstance;
|
||||
let integrationManagerUrl: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then(url => {
|
||||
integrationManagerUrl = url;
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
||||
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 => {
|
||||
testUser = user;
|
||||
});
|
||||
|
||||
cy.setAccountData("m.widgets", {
|
||||
"m.integration_manager": {
|
||||
content: {
|
||||
type: "m.integration_manager",
|
||||
name: "Integration Manager",
|
||||
url: integrationManagerUrl,
|
||||
data: {
|
||||
api_url: integrationManagerUrl,
|
||||
},
|
||||
},
|
||||
id: "integration-manager",
|
||||
},
|
||||
}).as("integrationManager");
|
||||
|
||||
// Succeed when checking the token is valid
|
||||
cy.intercept(`${integrationManagerUrl}/account?scalar_token=${INTEGRATION_MANAGER_TOKEN}*`, req => {
|
||||
req.continue(res => {
|
||||
return res.send(200, {
|
||||
user_id: testUser.userId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME,
|
||||
}).as("roomId");
|
||||
|
||||
cy.getBot(synapse, { displayName: BOT_DISPLAY_NAME, autoAcceptInvites: true }).as("bob");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it("should kick the target", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
|
||||
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not kick the target if lacking permissions", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
|
||||
cy.getClient().then(async client => {
|
||||
await client.sendStateEvent(roomId, 'm.room.power_levels', {
|
||||
kick: 50,
|
||||
users: {
|
||||
[testUser.userId]: 0,
|
||||
},
|
||||
});
|
||||
}).then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if the target already left", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist').then(async () => {
|
||||
await targetUser.leave(roomId);
|
||||
}).then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if the target was banned", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
cy.inviteUser(roomId, targetUserId);
|
||||
cy.contains(`${BOT_DISPLAY_NAME} joined the room`).should('exist');
|
||||
cy.getClient().then(async client => {
|
||||
await client.ban(roomId, targetUserId);
|
||||
}).then(() => {
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should no-op if the target was never a room member", () => {
|
||||
cy.all([
|
||||
cy.get<MatrixClient>("@bob"),
|
||||
cy.get<string>("@roomId"),
|
||||
cy.get<{}>("@integrationManager"),
|
||||
]).then(([targetUser, roomId]) => {
|
||||
const targetUserId = targetUser.getUserId();
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
openIntegrationManager();
|
||||
sendActionFromIntegrationManager(integrationManagerUrl, roomId, targetUserId);
|
||||
closeIntegrationManager(integrationManagerUrl);
|
||||
expectKickedMessage(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,174 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
interface Charly {
|
||||
client: MatrixClient;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
describe("Lazy Loading", () => {
|
||||
let synapse: SynapseInstance;
|
||||
let bob: MatrixClient;
|
||||
const charlies: Charly[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Alice");
|
||||
|
||||
cy.getBot(synapse, {
|
||||
displayName: "Bob",
|
||||
startClient: false,
|
||||
autoAcceptInvites: false,
|
||||
}).then(_bob => {
|
||||
bob = _bob;
|
||||
});
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const displayName = `Charly #${i}`;
|
||||
cy.getBot(synapse, {
|
||||
displayName,
|
||||
startClient: false,
|
||||
autoAcceptInvites: false,
|
||||
}).then(client => {
|
||||
charlies[i - 1] = { displayName, client };
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
const name = "Lazy Loading Test";
|
||||
const alias = "#lltest:localhost";
|
||||
const charlyMsg1 = "hi bob!";
|
||||
const charlyMsg2 = "how's it going??";
|
||||
|
||||
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
|
||||
cy.window({ log: false }).then(win => {
|
||||
return cy.wrap(bob.createRoom({
|
||||
name,
|
||||
room_alias_name: "lltest",
|
||||
visibility: win.matrixcs.Visibility.Public,
|
||||
}).then(r => r.room_id), { log: false }).as("roomId");
|
||||
});
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.joinRoom(alias);
|
||||
}
|
||||
|
||||
for (const charly of charlies) {
|
||||
cy.botSendMessage(charly.client, roomId, charlyMsg1);
|
||||
}
|
||||
for (const charly of charlies) {
|
||||
cy.botSendMessage(charly.client, roomId, charlyMsg2);
|
||||
}
|
||||
|
||||
for (let i = 20; i >= 1; --i) {
|
||||
cy.botSendMessage(bob, roomId, `I will only say this ${i} time(s)!`);
|
||||
}
|
||||
});
|
||||
|
||||
cy.joinRoom(alias);
|
||||
cy.viewRoomByName(name);
|
||||
}
|
||||
|
||||
function checkPaginatedDisplayNames(charlies: Charly[]) {
|
||||
cy.scrollToTop();
|
||||
for (const charly of charlies) {
|
||||
cy.findEventTile(charly.displayName, charlyMsg1).should("exist");
|
||||
cy.findEventTile(charly.displayName, charlyMsg2).should("exist");
|
||||
}
|
||||
}
|
||||
|
||||
function openMemberlist(): void {
|
||||
cy.get('.mx_HeaderButtons [aria-label="Room info"]').click();
|
||||
cy.get(".mx_RoomSummaryCard").within(() => {
|
||||
cy.get(".mx_RoomSummaryCard_icon_people").click();
|
||||
});
|
||||
}
|
||||
|
||||
function getMemberInMemberlist(name: string): Chainable<JQuery> {
|
||||
return cy.contains(".mx_MemberList .mx_EntityTile_name", name);
|
||||
}
|
||||
|
||||
function checkMemberList(charlies: Charly[]) {
|
||||
getMemberInMemberlist("Alice").should("exist");
|
||||
getMemberInMemberlist("Bob").should("exist");
|
||||
charlies.forEach(charly => {
|
||||
getMemberInMemberlist(charly.displayName).should("exist");
|
||||
});
|
||||
}
|
||||
|
||||
function checkMemberListLacksCharlies(charlies: Charly[]) {
|
||||
charlies.forEach(charly => {
|
||||
getMemberInMemberlist(charly.displayName).should("not.exist");
|
||||
});
|
||||
}
|
||||
|
||||
function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
|
||||
cy.goOffline();
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.joinRoom(alias);
|
||||
}
|
||||
for (let i = 20; i >= 1; --i) {
|
||||
cy.botSendMessage(charlies[0].client, roomId, "where is charly?");
|
||||
}
|
||||
});
|
||||
|
||||
cy.goOnline();
|
||||
cy.wait(1000); // Ideally we'd await a /sync here but intercepts step on each other from going offline/online
|
||||
}
|
||||
|
||||
it("should handle lazy loading properly even when offline", () => {
|
||||
const charly1to5 = charlies.slice(0, 5);
|
||||
const charly6to10 = charlies.slice(5);
|
||||
|
||||
// Set up room with alice, bob & charlies 1-5
|
||||
setupRoomWithBobAliceAndCharlies(charly1to5);
|
||||
// Alice should see 2 messages from every charly with the correct display name
|
||||
checkPaginatedDisplayNames(charly1to5);
|
||||
|
||||
openMemberlist();
|
||||
checkMemberList(charly1to5);
|
||||
joinCharliesWhileAliceIsOffline(charly6to10);
|
||||
checkMemberList(charly6to10);
|
||||
|
||||
cy.get<string>("@roomId").then(async roomId => {
|
||||
for (const charly of charlies) {
|
||||
await charly.client.leave(roomId);
|
||||
}
|
||||
});
|
||||
|
||||
checkMemberListLacksCharlies(charlies);
|
||||
});
|
||||
});
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
describe("Location sharing", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
const selectLocationShareTypeOption = (shareType: string): Chainable<JQuery> => {
|
||||
return cy.get(`[data-test-id="share-location-option-${shareType}"]`);
|
||||
};
|
||||
|
||||
const submitShareLocation = (): void => {
|
||||
cy.get('[data-test-id="location-picker-submit-button"]').click();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("sends and displays pin drop location message successfully", () => {
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.visit('/#/room/' + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Location"]').click();
|
||||
});
|
||||
|
||||
selectLocationShareTypeOption('Pin').click();
|
||||
|
||||
cy.get('#mx_LocationPicker_map').click('center');
|
||||
|
||||
submitShareLocation();
|
||||
|
||||
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('[aria-label="Close dialog"]').click();
|
||||
|
||||
cy.get('.mx_Marker')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SinonStub } from "cypress/types/sinon";
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("Consent", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("consent").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Bob");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
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 => {
|
||||
win.mxMatrixClientPeg.matrixClient.createRoom({}).catch(() => {});
|
||||
|
||||
// Stub `window.open` - clicking the primary button below will call it
|
||||
cy.stub(win, "open").as("windowOpen").returns({});
|
||||
});
|
||||
|
||||
// Accept terms & conditions
|
||||
cy.get(".mx_QuestionDialog").within(() => {
|
||||
cy.contains("#mx_BaseDialog_title", "Terms and Conditions");
|
||||
cy.get(".mx_Dialog_primary").click();
|
||||
});
|
||||
|
||||
cy.get<SinonStub>("@windowOpen").then(stub => {
|
||||
const url = stub.getCall(0).args[0];
|
||||
|
||||
// Go to Synapse's consent page and accept it
|
||||
cy.origin(synapse.baseUrl, { args: { url } }, ({ url }) => {
|
||||
cy.visit(url);
|
||||
|
||||
cy.get('[type="submit"]').click();
|
||||
cy.contains("p", "Danke schon");
|
||||
});
|
||||
});
|
||||
|
||||
// go back to the app
|
||||
cy.visit("/");
|
||||
// wait for the app to re-load
|
||||
cy.get(".mx_MatrixChat", { timeout: 15000 });
|
||||
|
||||
// attempt to perform the same action again and expect it to not fail
|
||||
cy.createRoom({});
|
||||
});
|
||||
});
|
|
@ -1,104 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("Login", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
describe("m.login.password", () => {
|
||||
const username = "user1234";
|
||||
const password = "p4s5W0rD";
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("consent").then(data => {
|
||||
synapse = data;
|
||||
cy.registerUser(synapse, username, password);
|
||||
cy.visit("/#/login");
|
||||
});
|
||||
});
|
||||
|
||||
it("logs in with an existing account and lands on the home screen", () => {
|
||||
cy.injectAxe();
|
||||
|
||||
cy.get("#mx_LoginForm_username", { timeout: 15000 }).should("be.visible");
|
||||
cy.percySnapshot("Login");
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get(".mx_ServerPicker_change").click();
|
||||
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_LoginForm_username").type(username);
|
||||
cy.get("#mx_LoginForm_password").type(password);
|
||||
cy.get(".mx_Login_submit").click();
|
||||
|
||||
cy.url().should('contain', '/#/home', { timeout: 30000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("logout", () => {
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("consent").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Erin");
|
||||
});
|
||||
});
|
||||
|
||||
it("should go to login page on logout", () => {
|
||||
cy.get('[aria-label="User menu"]').click();
|
||||
|
||||
// give a change for the outstanding requests queue to settle before logging out
|
||||
cy.wait(2000);
|
||||
|
||||
cy.get(".mx_UserMenu_contextMenu").within(() => {
|
||||
cy.get(".mx_UserMenu_iconSignOut").click();
|
||||
});
|
||||
|
||||
cy.url().should("contain", "/#/login");
|
||||
});
|
||||
|
||||
it("should respect logout_redirect_url", () => {
|
||||
cy.tweakConfig({
|
||||
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
|
||||
// We could use example.org, matrix.org, or something else, however this puts dependency of external
|
||||
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
|
||||
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
|
||||
// Using the decoder-ring is just as fine, and we can search for strategic names.
|
||||
logout_redirect_url: "/decoder-ring/",
|
||||
});
|
||||
|
||||
cy.get('[aria-label="User menu"]').click();
|
||||
|
||||
// give a change for the outstanding requests queue to settle before logging out
|
||||
cy.wait(2000);
|
||||
|
||||
cy.get(".mx_UserMenu_contextMenu").within(() => {
|
||||
cy.get(".mx_UserMenu_iconSignOut").click();
|
||||
});
|
||||
|
||||
cy.url().should("contains", "decoder-ring");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,341 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { PollResponseEvent } from "matrix-events-sdk";
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
|
||||
|
||||
describe("Polls", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
type CreatePollOptions = {
|
||||
title: string;
|
||||
options: string[];
|
||||
};
|
||||
const createPoll = ({ title, options }: CreatePollOptions) => {
|
||||
if (options.length < 2) {
|
||||
throw new Error('Poll must have at least two options');
|
||||
}
|
||||
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(optionId).scrollIntoView().type(option);
|
||||
});
|
||||
});
|
||||
cy.get('.mx_Dialog button[type="submit"]').click();
|
||||
};
|
||||
|
||||
const getPollTile = (pollId: string): Chainable<JQuery> => {
|
||||
return cy.get(`.mx_EventTile[data-scroll-tokens="${pollId}"]`);
|
||||
};
|
||||
|
||||
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
|
||||
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`);
|
||||
});
|
||||
};
|
||||
|
||||
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
|
||||
getPollOption(pollId, optionText).within(ref => {
|
||||
cy.get('input[type="radio"]').invoke('attr', 'value').then(optionId => {
|
||||
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
|
||||
bot.sendEvent(
|
||||
roomId,
|
||||
pollVote.type,
|
||||
pollVote.content,
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_thread");
|
||||
cy.window().then(win => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should be creatable and votable", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
// wait until Bob joined
|
||||
cy.contains(".mx_TextualEvent", "BotBob joined the room").should("exist");
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
cy.get('.mx_CompoundDialog').percySnapshotElement('Polls Composer');
|
||||
|
||||
const pollParams = {
|
||||
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");
|
||||
|
||||
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');
|
||||
|
||||
// vote 'Maybe'
|
||||
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');
|
||||
|
||||
// 1 vote for yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// 1 vote for maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 1);
|
||||
|
||||
// Bot updates vote to 'No'
|
||||
botVoteForOption(bot, roomId, pollId, pollParams.options[1]);
|
||||
|
||||
// 1 vote for yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// 1 vote for no
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// 0 for maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be editable from context menu if no votes have been cast", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
const pollParams = {
|
||||
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<string>("@pollId").then(pollId => {
|
||||
// Open context menu
|
||||
getPollTile(pollId).rightclick();
|
||||
|
||||
// Select edit item
|
||||
cy.get('.mx_ContextualMenu').within(() => {
|
||||
cy.get('[aria-label="Edit"]').click();
|
||||
});
|
||||
|
||||
// Expect poll editing dialog
|
||||
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 => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
const pollParams = {
|
||||
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<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');
|
||||
|
||||
// Open context menu
|
||||
getPollTile(pollId).rightclick();
|
||||
|
||||
// Select edit item
|
||||
cy.get('.mx_ContextualMenu').within(() => {
|
||||
cy.get('[aria-label="Edit"]').click();
|
||||
});
|
||||
|
||||
// Expect error dialog
|
||||
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 => {
|
||||
botBob = _bot;
|
||||
});
|
||||
cy.getBot(synapse, { displayName: "BotCharlie" }).then(_bot => {
|
||||
botCharlie = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, botBob.getUserId());
|
||||
cy.inviteUser(roomId, botCharlie.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
// wait until the bots joined
|
||||
cy.contains(".mx_TextualEvent", "and one other were invited and joined").should("exist");
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get('[aria-label="Poll"]').click();
|
||||
});
|
||||
|
||||
const pollParams = {
|
||||
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");
|
||||
|
||||
cy.get<string>("@pollId").then(pollId => {
|
||||
// Bob starts thread on the poll
|
||||
botBob.sendMessage(roomId, pollId, {
|
||||
body: "Hello there",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
|
||||
// open the thread summary
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
|
||||
|
||||
// Bob votes 'Maybe' in the poll
|
||||
botVoteForOption(botBob, roomId, pollId, pollParams.options[2]);
|
||||
// Charlie votes 'No'
|
||||
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');
|
||||
// and thread view
|
||||
cy.get('.mx_ThreadView .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||
|
||||
cy.get('.mx_RoomView_body').within(() => {
|
||||
// vote 'Maybe' in the main timeline poll
|
||||
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(() => {
|
||||
// votes updated in thread view too
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||
// change my vote to 'Yes'
|
||||
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||
});
|
||||
|
||||
// Bob updates vote to 'No'
|
||||
botVoteForOption(botBob, roomId, pollId, pollParams.options[1]);
|
||||
|
||||
// me: yes, bob: no, charlie: no
|
||||
const expectVoteCounts = () => {
|
||||
// I voted yes
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||
// Bob and Charlie voted no
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[1], 2);
|
||||
// 0 for maybe
|
||||
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
||||
};
|
||||
|
||||
// check counts are correct in main timeline tile
|
||||
cy.get('.mx_RoomView_body').within(() => {
|
||||
expectVoteCounts();
|
||||
});
|
||||
// and in thread view tile
|
||||
cy.get('.mx_ThreadView').within(() => {
|
||||
expectVoteCounts();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("Registration", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("/#/register");
|
||||
cy.startSynapse("consent").then(data => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("registers an account and lands on the home screen", () => {
|
||||
cy.injectAxe();
|
||||
|
||||
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
|
||||
cy.get(".mx_ServerPickerDialog_continue").should("be.visible");
|
||||
// Only snapshot the server picker otherwise in the background `matrix.org` may or may not be available
|
||||
cy.get(".mx_Dialog").percySnapshotElement("Server Picker", { widths: [516] });
|
||||
cy.checkA11y();
|
||||
|
||||
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_RegistrationForm_username").should("be.visible");
|
||||
// Hide the server text as it contains the randomly allocated Synapse port
|
||||
const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }";
|
||||
cy.percySnapshot("Registration", { percyCSS });
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get("#mx_RegistrationForm_username").type("alice");
|
||||
cy.get("#mx_RegistrationForm_password").type("totally a great password");
|
||||
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
|
||||
cy.get(".mx_Login_submit").click();
|
||||
|
||||
cy.get(".mx_RegistrationEmailPromptDialog").should("be.visible");
|
||||
cy.percySnapshot("Registration email prompt", { percyCSS });
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
|
||||
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").should("be.visible");
|
||||
cy.percySnapshot("Registration terms prompt", { percyCSS });
|
||||
cy.checkA11y();
|
||||
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
||||
|
||||
cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist");
|
||||
cy.percySnapshot("Use-case selection screen");
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("should require username to fulfil requirements and be available", () => {
|
||||
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
|
||||
cy.get(".mx_ServerPickerDialog_continue").should("be.visible");
|
||||
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_RegistrationForm_username").should("be.visible");
|
||||
|
||||
cy.intercept("**/_matrix/client/*/register/available?username=_alice", {
|
||||
statusCode: 400,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
errcode: "M_INVALID_USERNAME",
|
||||
error: "User ID may not begin with _",
|
||||
},
|
||||
});
|
||||
cy.get("#mx_RegistrationForm_username").type("_alice");
|
||||
cy.get(".mx_Field_tooltip")
|
||||
.should("have.class", "mx_Tooltip_visible")
|
||||
.should("contain.text", "Some characters not allowed");
|
||||
|
||||
cy.intercept("**/_matrix/client/*/register/available?username=bob", {
|
||||
statusCode: 400,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: {
|
||||
errcode: "M_USER_IN_USE",
|
||||
error: "The desired username is already taken",
|
||||
},
|
||||
});
|
||||
cy.get("#mx_RegistrationForm_username").type("{selectAll}{backspace}bob");
|
||||
cy.get(".mx_Field_tooltip")
|
||||
.should("have.class", "mx_Tooltip_visible")
|
||||
.should("contain.text", "Someone already has that username");
|
||||
|
||||
cy.get("#mx_RegistrationForm_username").type("{selectAll}{backspace}foobar");
|
||||
cy.get(".mx_Field_tooltip").should("not.have.class", "mx_Tooltip_visible");
|
||||
});
|
||||
});
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("Pills", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it('should navigate clicks internally to the app', () => {
|
||||
const messageRoom = "Send Messages Here";
|
||||
const targetLocalpart = "aliasssssssssssss";
|
||||
cy.createRoom({
|
||||
name: "Target",
|
||||
room_alias_name: targetLocalpart,
|
||||
}).as("targetRoomId");
|
||||
cy.createRoom({
|
||||
name: messageRoom,
|
||||
}).as("messageRoomId");
|
||||
cy.all([
|
||||
cy.get<string>("@targetRoomId"),
|
||||
cy.get<string>("@messageRoomId"),
|
||||
]).then(([targetRoomId, messageRoomId]) => { // discard the target room ID - we don't need it
|
||||
cy.viewRoomByName(messageRoom);
|
||||
cy.url().should("contain", `/#/room/${messageRoomId}`);
|
||||
|
||||
// send a message using the built-in room mention functionality (autocomplete)
|
||||
cy.get(".mx_SendMessageComposer .mx_BasicMessageComposer_input")
|
||||
.type(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
|
||||
cy.get(".mx_Autocomplete_Completion_title").click();
|
||||
cy.get(".mx_MessageComposer_sendMessage").click();
|
||||
|
||||
// find the pill in the timeline and click it
|
||||
cy.get(".mx_EventTile_body .mx_Pill").click();
|
||||
|
||||
const localUrl = `/#/room/#${targetLocalpart}:`;
|
||||
// verify we landed at a sane place
|
||||
cy.url().should("contain", localUrl);
|
||||
|
||||
cy.wait(250); // let the room list settle
|
||||
|
||||
// go back to the message room and try to click on the pill text, as a user would
|
||||
cy.viewRoomByName(messageRoom);
|
||||
cy.get(".mx_EventTile_body .mx_Pill .mx_Pill_linkText")
|
||||
.should("have.css", "pointer-events", "none")
|
||||
.click({ force: true }); // force is to ensure we bypass pointer-events
|
||||
cy.url().should("contain", localUrl);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const SPACE_NAME = "Test space";
|
||||
const NAME = "Alice";
|
||||
|
||||
const getMemberTileByName = (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(`.mx_EntityTile, [title="${name}"]`);
|
||||
};
|
||||
|
||||
const goBack = (): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_BaseCard_back").click();
|
||||
};
|
||||
|
||||
const viewRoomSummaryByName = (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.viewRoomByName(name);
|
||||
cy.get(".mx_RightPanel_roomSummaryButton").click();
|
||||
return checkRoomSummaryCard(name);
|
||||
};
|
||||
|
||||
const checkRoomSummaryCard = (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get(".mx_RoomSummaryCard").should("have.length", 1);
|
||||
return cy.get(".mx_BaseCard_header").should("contain", name);
|
||||
};
|
||||
|
||||
describe("RightPanel", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, NAME).then(() =>
|
||||
cy.window({ log: false }).then(() => {
|
||||
cy.createRoom({ name: ROOM_NAME });
|
||||
cy.createSpace({ name: SPACE_NAME });
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
describe("in rooms", () => {
|
||||
it("should handle clicking add widgets", () => {
|
||||
viewRoomSummaryByName(ROOM_NAME);
|
||||
|
||||
cy.get(".mx_RoomSummaryCard_appsGroup .mx_AccessibleButton").click();
|
||||
cy.get(".mx_IntegrationManager").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("should handle viewing export chat", () => {
|
||||
viewRoomSummaryByName(ROOM_NAME);
|
||||
|
||||
cy.get(".mx_RoomSummaryCard_icon_export").click();
|
||||
cy.get(".mx_ExportDialog").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("should handle viewing share room", () => {
|
||||
viewRoomSummaryByName(ROOM_NAME);
|
||||
|
||||
cy.get(".mx_RoomSummaryCard_icon_share").click();
|
||||
cy.get(".mx_ShareDialog").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("should handle viewing room settings", () => {
|
||||
viewRoomSummaryByName(ROOM_NAME);
|
||||
|
||||
cy.get(".mx_RoomSummaryCard_icon_settings").click();
|
||||
cy.get(".mx_RoomSettingsDialog").should("have.length", 1);
|
||||
cy.get(".mx_Dialog_title").should("contain", ROOM_NAME);
|
||||
});
|
||||
|
||||
it("should handle viewing files", () => {
|
||||
viewRoomSummaryByName(ROOM_NAME);
|
||||
|
||||
cy.get(".mx_RoomSummaryCard_icon_files").click();
|
||||
cy.get(".mx_FilePanel").should("have.length", 1);
|
||||
cy.get(".mx_FilePanel_empty").should("have.length", 1);
|
||||
|
||||
goBack();
|
||||
checkRoomSummaryCard(ROOM_NAME);
|
||||
});
|
||||
|
||||
it("should handle viewing room member", () => {
|
||||
viewRoomSummaryByName(ROOM_NAME);
|
||||
|
||||
cy.get(".mx_RoomSummaryCard_icon_people").click();
|
||||
cy.get(".mx_MemberList").should("have.length", 1);
|
||||
|
||||
getMemberTileByName(NAME).click();
|
||||
cy.get(".mx_UserInfo").should("have.length", 1);
|
||||
cy.get(".mx_UserInfo_profile").should("contain", NAME);
|
||||
|
||||
goBack();
|
||||
cy.get(".mx_MemberList").should("have.length", 1);
|
||||
|
||||
goBack();
|
||||
checkRoomSummaryCard(ROOM_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("in spaces", () => {
|
||||
it("should handle viewing space member", () => {
|
||||
cy.viewSpaceHomeByName(SPACE_NAME);
|
||||
cy.get(".mx_RoomInfoLine_members").click();
|
||||
cy.get(".mx_MemberList").should("have.length", 1);
|
||||
cy.get(".mx_RightPanel_scopeHeader").should("contain", SPACE_NAME);
|
||||
|
||||
getMemberTileByName(NAME).click();
|
||||
cy.get(".mx_UserInfo").should("have.length", 1);
|
||||
cy.get(".mx_UserInfo_profile").should("contain", NAME);
|
||||
cy.get(".mx_RightPanel_scopeHeader").should("contain", SPACE_NAME);
|
||||
|
||||
goBack();
|
||||
cy.get(".mx_MemberList").should("have.length", 1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,103 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
|
||||
describe("Room Directory", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Ray");
|
||||
cy.getBot(synapse, { displayName: "Paul" }).as("bot");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should allow admin to add alias & publish room to directory", () => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.createRoom({
|
||||
name: "Gaming",
|
||||
preset: win.matrixcs.Preset.PublicChat,
|
||||
}).as("roomId");
|
||||
});
|
||||
|
||||
cy.viewRoomByName("Gaming");
|
||||
cy.openRoomSettings();
|
||||
|
||||
// First add a local address `gaming`
|
||||
cy.contains(".mx_SettingsFieldset", "Local Addresses").within(() => {
|
||||
cy.get(".mx_Field input").type("gaming");
|
||||
cy.contains(".mx_AccessibleButton", "Add").click();
|
||||
cy.get(".mx_EditableItem_item").should("contain", "#gaming:localhost");
|
||||
});
|
||||
|
||||
// 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()
|
||||
.should("have.attr", "aria-checked", "true");
|
||||
});
|
||||
|
||||
cy.closeDialog();
|
||||
|
||||
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);
|
||||
expect(resp.chunk[0].room_id).to.equal(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
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]) => {
|
||||
bot.createRoom({
|
||||
visibility: win.matrixcs.Visibility.Public,
|
||||
name,
|
||||
room_alias_name: "test1234",
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('[role="button"][aria-label="Explore rooms"]').click();
|
||||
|
||||
cy.get('.mx_RoomDirectory_dialogWrapper [name="dirsearch"]').type("Unknown Room");
|
||||
cy.get(".mx_RoomDirectory_dialogWrapper h5").should("contain", 'No results for "Unknown Room"');
|
||||
cy.get(".mx_RoomDirectory_dialogWrapper").percySnapshotElement("Room Directory - filtered no results");
|
||||
|
||||
cy.get('.mx_RoomDirectory_dialogWrapper [name="dirsearch"]').type("{selectAll}{backspace}test1234");
|
||||
cy.contains(".mx_RoomDirectory_dialogWrapper .mx_RoomDirectory_listItem", name)
|
||||
.should("exist").as("resultRow");
|
||||
cy.get(".mx_RoomDirectory_dialogWrapper").percySnapshotElement("Room Directory - filtered one result");
|
||||
cy.get("@resultRow").find(".mx_AccessibleButton").contains("Join").click();
|
||||
|
||||
cy.url().should('contain', `/#/room/#test1234:localhost`);
|
||||
});
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import type { UserCredentials } from "../../support/login";
|
||||
|
||||
describe("Device manager", () => {
|
||||
let synapse: SynapseInstance | undefined;
|
||||
let user: UserCredentials | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.enableLabsFeature("feature_new_device_manager");
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Alice").then(credentials => {
|
||||
user = credentials;
|
||||
}).then(() => {
|
||||
// create some extra sessions to manage
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
}).then(() => {
|
||||
return cy.loginUser(synapse, user.username, user.password);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse!);
|
||||
});
|
||||
|
||||
it("should display sessions", () => {
|
||||
cy.openUserSettings("Sessions");
|
||||
cy.contains('Current session').should('exist');
|
||||
|
||||
cy.get('[data-testid="current-session-section"]').within(() => {
|
||||
cy.contains('Unverified session').should('exist');
|
||||
cy.get('.mx_DeviceSecurityCard_actions [role="button"]').should('exist');
|
||||
});
|
||||
|
||||
// current session details opened
|
||||
cy.get('[data-testid="current-session-toggle-details"]').click();
|
||||
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.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();
|
||||
});
|
||||
|
||||
/**
|
||||
* Other sessions section
|
||||
*/
|
||||
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);
|
||||
|
||||
// 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();
|
||||
// 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);
|
||||
// security recommendation count updated
|
||||
cy.get('[data-testid="unverified-devices-cta"]').should('have.text', 'View all (1)');
|
||||
|
||||
const sessionName = `Alice's device`;
|
||||
// open the first session
|
||||
cy.get('.mx_FilteredDeviceList_list .mx_FilteredDeviceList_listItem').first().within(() => {
|
||||
cy.get('[aria-label="Toggle device details"]').click();
|
||||
|
||||
cy.contains('Session details').should('exist');
|
||||
|
||||
cy.get('[data-testid="device-heading-rename-cta"]').click();
|
||||
cy.get('[data-testid="device-rename-input"]').type(sessionName);
|
||||
cy.get('[data-testid="device-rename-submit-cta"]').click();
|
||||
// there should be a spinner while device updates
|
||||
cy.get(".mx_Spinner").should("exist");
|
||||
// wait for spinner to complete
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
|
||||
// session name updated in details
|
||||
cy.get('.mx_DeviceDetailHeading h3').should('have.text', sessionName);
|
||||
// and main list item
|
||||
cy.get('.mx_DeviceTile h4').should('have.text', sessionName);
|
||||
|
||||
// sign out using the device details sign out
|
||||
cy.get('[data-testid="device-detail-sign-out-cta"]').click();
|
||||
});
|
||||
// confirm the signout
|
||||
cy.get('[data-testid="dialog-primary-button"]').click();
|
||||
|
||||
// no other sessions or security recommendations sections when only one session
|
||||
cy.contains('Other sessions').should('not.exist');
|
||||
cy.get('[data-testid="security-recommendations-section"]').should('not.exist');
|
||||
});
|
||||
});
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
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 => {
|
||||
if (typeof labsVal === "boolean") {
|
||||
// stringify boolean
|
||||
win.localStorage.setItem("mx_labs_feature_feature_hidden_read_receipts", `${labsVal}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function testForVal(settingVal: boolean | null): void {
|
||||
const testRoomName = "READ RECEIPTS";
|
||||
cy.createRoom({ name: testRoomName }).as("roomId");
|
||||
cy.all([cy.get<string>("@roomId")]).then(() => {
|
||||
cy.viewRoomByName(testRoomName).then(() => {
|
||||
// if we can see the room, then sync is working for us. It's time to see if the
|
||||
// migration even ran.
|
||||
|
||||
cy.getSettingValue("sendReadReceipts", null, true).should("satisfy", (val) => {
|
||||
if (typeof settingVal === "boolean") {
|
||||
return val === settingVal;
|
||||
} else {
|
||||
return !val; // falsy - we don't actually care if it's undefined, null, or a literal false
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("Hidden Read Receipts Setting Migration", () => {
|
||||
// We run this as a full-blown end-to-end test to ensure it works in an integration
|
||||
// sense. If we unit tested it, we'd be testing that the code works but not that the
|
||||
// migration actually runs.
|
||||
//
|
||||
// Here, we get to test that not only the code works but also that it gets run. Most
|
||||
// of our interactions are with the JS console as we're honestly just checking that
|
||||
// things got set correctly.
|
||||
//
|
||||
// For a security-sensitive feature like hidden read receipts, it's absolutely vital
|
||||
// that we migrate the setting appropriately.
|
||||
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it('should not migrate the lack of a labs flag', () => {
|
||||
seedLabs(synapse, null);
|
||||
testForVal(null);
|
||||
});
|
||||
|
||||
it('should migrate labsHiddenRR=false as sendRR=true', () => {
|
||||
seedLabs(synapse, false);
|
||||
testForVal(true);
|
||||
});
|
||||
|
||||
it('should migrate labsHiddenRR=true as sendRR=false', () => {
|
||||
seedLabs(synapse, true);
|
||||
testForVal(false);
|
||||
});
|
||||
});
|
|
@ -1,399 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import _ from "lodash";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { ProxyInstance } from "../../plugins/sliding-sync";
|
||||
|
||||
describe("Sliding Sync", () => {
|
||||
beforeEach(() => {
|
||||
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.enableLabsFeature("feature_sliding_sync");
|
||||
|
||||
cy.intercept("/config.json?cachebuster=*", req => {
|
||||
return req.continue(res => {
|
||||
res.send(200, {
|
||||
...res.body,
|
||||
setting_defaults: {
|
||||
feature_sliding_sync_proxy_url: `http://localhost:${proxy.port}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.initTestUser(synapse, "Sloth").then(() => {
|
||||
return cy.window({ log: false }).then(() => {
|
||||
cy.createRoom({ name: "Test Room" }).as("roomId");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.get<SynapseInstance>("@synapse").then(cy.stopSynapse);
|
||||
cy.get<ProxyInstance>("@proxy").then(cy.stopProxy);
|
||||
});
|
||||
|
||||
// assert order
|
||||
const checkOrder = (wantOrder: string[]) => {
|
||||
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomTile_title").should((elements) => {
|
||||
expect(_.map(elements, (e) => {
|
||||
return e.textContent;
|
||||
}), "rooms are sorted").to.deep.equal(wantOrder);
|
||||
});
|
||||
};
|
||||
const bumpRoom = (alias: string) => {
|
||||
// Send a message into the given room, this should bump the room to the top
|
||||
cy.get<string>(alias).then((roomId) => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Hello world",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
};
|
||||
const createAndJoinBob = () => {
|
||||
// create a Bob user
|
||||
cy.get<SynapseInstance>("@synapse").then((synapse) => {
|
||||
return cy.getBot(synapse, {
|
||||
displayName: "Bob",
|
||||
}).as("bob");
|
||||
});
|
||||
|
||||
// invite Bob to Test Room and accept then send a message.
|
||||
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||
return cy.inviteUser(roomId, bob.getUserId()).then(() => {
|
||||
return bob.joinRoom(roomId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// sanity check everything works
|
||||
it("should correctly render expected messages", () => {
|
||||
cy.get<string>("@roomId").then(roomId => cy.visit("/#/room/" + roomId));
|
||||
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.",
|
||||
);
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
});
|
||||
|
||||
it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => {
|
||||
// create rooms and check room names are correct
|
||||
cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
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.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",
|
||||
]);
|
||||
});
|
||||
|
||||
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"));
|
||||
|
||||
// Select the Test Room
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
|
||||
checkOrder([
|
||||
"Orange", "Pineapple", "Apple", "Test Room",
|
||||
]);
|
||||
bumpRoom("@roomA");
|
||||
checkOrder([
|
||||
"Apple", "Orange", "Pineapple", "Test Room",
|
||||
]);
|
||||
bumpRoom("@roomO");
|
||||
checkOrder([
|
||||
"Orange", "Apple", "Pineapple", "Test Room",
|
||||
]);
|
||||
bumpRoom("@roomO");
|
||||
checkOrder([
|
||||
"Orange", "Apple", "Pineapple", "Test Room",
|
||||
]);
|
||||
bumpRoom("@roomP");
|
||||
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"));
|
||||
|
||||
// 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
|
||||
// be Apple, Orange Pineapple - only when you click on a different room do things reshuffle.
|
||||
|
||||
// Select the Pineapple room
|
||||
cy.contains(".mx_RoomTile", "Pineapple").click();
|
||||
checkOrder([
|
||||
"Orange", "Pineapple", "Apple", "Test Room",
|
||||
]);
|
||||
|
||||
// Move Apple
|
||||
bumpRoom("@roomA");
|
||||
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",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should show the right unread notifications", () => {
|
||||
createAndJoinBob();
|
||||
|
||||
// send a message in the test room: unread notif count shoould increment
|
||||
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||
return bob.sendTextMessage(roomId, "Hello World");
|
||||
});
|
||||
|
||||
// check that there is an unread notification (grey) as 1
|
||||
cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "1");
|
||||
cy.get(".mx_NotificationBadge").should("not.have.class", "mx_NotificationBadge_highlighted");
|
||||
|
||||
// send an @mention: highlight count (red) should be 2.
|
||||
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||
return bob.sendTextMessage(roomId, "Hello Sloth");
|
||||
});
|
||||
cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "2");
|
||||
cy.get(".mx_NotificationBadge").should("have.class", "mx_NotificationBadge_highlighted");
|
||||
|
||||
// click on the room, the notif counts should disappear
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
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.
|
||||
createAndJoinBob();
|
||||
|
||||
// disable notifs in this room (TODO: CS API call?)
|
||||
cy.contains(".mx_RoomTile", "Test Room").find(".mx_RoomTile_notificationsButton").click({ force: true });
|
||||
cy.contains("None").click();
|
||||
|
||||
// create a new room so we know when the message has been received as it'll re-shuffle the room list
|
||||
cy.createRoom({
|
||||
name: "Dummy",
|
||||
});
|
||||
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",
|
||||
]);
|
||||
|
||||
cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist");
|
||||
});
|
||||
|
||||
it("should update user settings promptly", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
it("should show and be able to accept/reject/rescind invites", () => {
|
||||
createAndJoinBob();
|
||||
|
||||
let clientUserId;
|
||||
cy.getClient().then((cli) => {
|
||||
clientUserId = cli.getUserId();
|
||||
});
|
||||
|
||||
// invite Sloth into 3 rooms:
|
||||
// - roomJoin: will join this room
|
||||
// - roomReject: will reject the invite
|
||||
// - roomRescind: will make Bob rescind the invite
|
||||
let roomJoin; let roomReject; let roomRescind; let bobClient;
|
||||
cy.get<MatrixClient>("@bob").then((bob) => {
|
||||
bobClient = bob;
|
||||
return Promise.all([
|
||||
bob.createRoom({ name: "Join" }),
|
||||
bob.createRoom({ name: "Reject" }),
|
||||
bob.createRoom({ name: "Rescind" }),
|
||||
]);
|
||||
}).then(([join, reject, rescind]) => {
|
||||
roomJoin = join.room_id;
|
||||
roomReject = reject.room_id;
|
||||
roomRescind = rescind.room_id;
|
||||
return Promise.all([
|
||||
bobClient.invite(roomJoin, clientUserId),
|
||||
bobClient.invite(roomReject, clientUserId),
|
||||
bobClient.invite(roomRescind, clientUserId),
|
||||
]);
|
||||
});
|
||||
|
||||
// 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.contains(".mx_RoomTile", "Join").click();
|
||||
cy.contains(".mx_AccessibleButton", "Accept").click();
|
||||
|
||||
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);
|
||||
|
||||
// check the lists are correct
|
||||
checkOrder([
|
||||
"Join", "Test Room",
|
||||
]);
|
||||
cy.contains(".mx_RoomSublist", "Invites").find(".mx_RoomTile_title").should((elements) => {
|
||||
expect(_.map(elements, (e) => {
|
||||
return e.textContent;
|
||||
}), "rooms are sorted").to.deep.equal(["Rescind"]);
|
||||
});
|
||||
|
||||
// now rescind the invite
|
||||
cy.get<MatrixClient>("@bob").then((bob) => {
|
||||
return bob.kick(roomRescind, clientUserId);
|
||||
});
|
||||
|
||||
// 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",
|
||||
]);
|
||||
});
|
||||
|
||||
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 }));
|
||||
});
|
||||
|
||||
cy.contains('.mx_RoomSublist[aria-label="Favourites"] .mx_RoomTile', "Favourite DM").should("exist");
|
||||
cy.contains('.mx_RoomSublist[aria-label="People"] .mx_RoomTile', "Favourite DM").should("not.exist");
|
||||
});
|
||||
|
||||
// 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.get<string>("@roomId").then((roomId) => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Hello world",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
// select the room
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
cy.get(".mx_ReplyPreview").should("not.exist");
|
||||
// click reply-to on the Hello World message
|
||||
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
|
||||
cy.contains(".mx_RoomTile", "Other Room").click();
|
||||
// ensure the reply-to disappears
|
||||
cy.get(".mx_ReplyPreview").should("not.exist");
|
||||
// click back
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
// ensure the reply-to reappears
|
||||
cy.get(".mx_ReplyPreview").should("exist");
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/vector-im/element-web/issues/21462
|
||||
it("should not cancel replies when permalinks are clicked ", () => {
|
||||
cy.get<string>("@roomId").then((roomId) => {
|
||||
// we require a first message as you cannot click the permalink text with the avatar in the way
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "First message",
|
||||
msgtype: "m.text",
|
||||
}).then(() => {
|
||||
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Permalink me",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
}).then(() => {
|
||||
cy.sendEvent(roomId, null, "m.room.message", {
|
||||
body: "Reply to me",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
});
|
||||
// select the room
|
||||
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||
cy.get(".mx_ReplyPreview").should("not.exist");
|
||||
// click reply-to on the Reply to me message
|
||||
cy.contains(".mx_EventTile", "Reply to me").find('.mx_AccessibleButton[aria-label="Reply"]').click(
|
||||
{ force: true },
|
||||
);
|
||||
// check it's visible
|
||||
cy.get(".mx_ReplyPreview").should("exist");
|
||||
// now click on the permalink for Permalink me
|
||||
cy.contains(".mx_EventTile", "Permalink me").find("a").click({ force: true });
|
||||
// make sure it is now selected with the little green |
|
||||
cy.contains(".mx_EventTile_selected", "Permalink me").should("exist");
|
||||
// ensure the reply-to does not disappear
|
||||
cy.get(".mx_ReplyPreview").should("exist");
|
||||
});
|
||||
});
|
|
@ -1,278 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { UserCredentials } from "../../support/login";
|
||||
|
||||
function openSpaceCreateMenu(): Chainable<JQuery> {
|
||||
cy.get(".mx_SpaceButton_new").click();
|
||||
return cy.get(".mx_SpaceCreateMenu_wrapper .mx_ContextualMenu");
|
||||
}
|
||||
|
||||
function openSpaceContextMenu(spaceName: string): Chainable<JQuery> {
|
||||
cy.getSpacePanelButton(spaceName).rightclick();
|
||||
return cy.get(".mx_SpacePanel_contextMenu");
|
||||
}
|
||||
|
||||
function spaceCreateOptions(spaceName: string): ICreateRoomOpts {
|
||||
return {
|
||||
creation_content: {
|
||||
type: "m.space",
|
||||
},
|
||||
initial_state: [{
|
||||
type: "m.room.name",
|
||||
content: {
|
||||
name: spaceName,
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||
return {
|
||||
type: "m.space.child",
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: [roomId.split(":")[1]],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("Spaces", () => {
|
||||
let synapse: SynapseInstance;
|
||||
let user: UserCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sue").then(_user => {
|
||||
user = _user;
|
||||
cy.mockClipboard();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
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('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!");
|
||||
cy.contains(".mx_AccessibleButton", "Create").click();
|
||||
});
|
||||
|
||||
// Create the default General & Random rooms, as well as a custom "Jokes" room
|
||||
cy.get('input[label="Room name"][value="General"]').should("exist");
|
||||
cy.get('input[label="Room name"][value="Random"]').should("exist");
|
||||
cy.get('input[placeholder="Support"]').type("Jokes");
|
||||
cy.contains(".mx_AccessibleButton", "Continue").click();
|
||||
|
||||
// Copy matrix.to link
|
||||
cy.get(".mx_SpacePublicShare_shareButton").focus().realClick();
|
||||
cy.getClipboardText().should("eq", "https://matrix.to/#/#lets-have-a-riot:localhost");
|
||||
|
||||
// Go to space home
|
||||
cy.contains(".mx_AccessibleButton", "Go to my first room").click();
|
||||
|
||||
// Assert rooms exist in the room list
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "General").should("exist");
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "Random").should("exist");
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "Jokes").should("exist");
|
||||
});
|
||||
|
||||
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('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...");
|
||||
cy.contains(".mx_AccessibleButton", "Create").click();
|
||||
});
|
||||
|
||||
cy.get(".mx_SpaceRoomView_privateScope_meAndMyTeammatesButton").click();
|
||||
|
||||
// Create the default General & Random rooms, as well as a custom "Projects" room
|
||||
cy.get('input[label="Room name"][value="General"]').should("exist");
|
||||
cy.get('input[label="Room name"][value="Random"]').should("exist");
|
||||
cy.get('input[placeholder="Support"]').type("Projects");
|
||||
cy.contains(".mx_AccessibleButton", "Continue").click();
|
||||
|
||||
cy.get(".mx_SpaceRoomView").should("contain", "Invite your teammates");
|
||||
cy.contains(".mx_AccessibleButton", "Skip for now").click();
|
||||
|
||||
// Assert rooms exist in the room list
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "General").should("exist");
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "Random").should("exist");
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "Projects").should("exist");
|
||||
|
||||
// Assert rooms exist in the space explorer
|
||||
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "General").should("exist");
|
||||
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Random").should("exist");
|
||||
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Projects").should("exist");
|
||||
});
|
||||
|
||||
it("should allow user to create just-me space", () => {
|
||||
cy.createRoom({
|
||||
name: "Sample Room",
|
||||
});
|
||||
|
||||
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('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}");
|
||||
});
|
||||
|
||||
cy.get(".mx_SpaceRoomView_privateScope_justMeButton").click();
|
||||
|
||||
cy.get(".mx_AddExistingToSpace_entry").click();
|
||||
cy.contains(".mx_AccessibleButton", "Add").click();
|
||||
|
||||
cy.contains(".mx_RoomList .mx_RoomTile", "Sample Room").should("exist");
|
||||
cy.contains(".mx_SpaceHierarchy_list .mx_SpaceHierarchy_roomTile", "Sample Room").should("exist");
|
||||
});
|
||||
|
||||
it("should allow user to invite another to a space", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
cy.createSpace({
|
||||
visibility: "public" as any,
|
||||
room_alias_name: "space",
|
||||
}).as("spaceId");
|
||||
|
||||
openSpaceContextMenu("#space:localhost").within(() => {
|
||||
cy.get('.mx_SpacePanel_contextMenu_inviteButton[aria-label="Invite"]').click();
|
||||
});
|
||||
|
||||
cy.get(".mx_SpacePublicShare").within(() => {
|
||||
// Copy link first
|
||||
cy.get(".mx_SpacePublicShare_shareButton").focus().realClick();
|
||||
cy.getClipboardText().should("eq", "https://matrix.to/#/#space:localhost");
|
||||
// Start Matrix invite flow
|
||||
cy.get(".mx_SpacePublicShare_inviteButton").click();
|
||||
});
|
||||
|
||||
cy.get(".mx_InviteDialog_other").within(() => {
|
||||
cy.get('input[type="text"]').type(bot.getUserId());
|
||||
cy.contains(".mx_AccessibleButton", "Invite").click();
|
||||
});
|
||||
|
||||
cy.get(".mx_InviteDialog_other").should("not.exist");
|
||||
});
|
||||
|
||||
it("should show space invites at the top of the space panel", () => {
|
||||
cy.createSpace({
|
||||
name: "My Space",
|
||||
});
|
||||
cy.getSpacePanelButton("My Space").should("exist");
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("should include rooms in space home", () => {
|
||||
cy.createRoom({
|
||||
name: "Music",
|
||||
}).as("roomId1");
|
||||
cy.createRoom({
|
||||
name: "Gaming",
|
||||
}).as("roomId2");
|
||||
|
||||
const spaceName = "Spacey Mc. Space Space";
|
||||
cy.all([
|
||||
cy.get<string>("@roomId1"),
|
||||
cy.get<string>("@roomId2"),
|
||||
]).then(([roomId1, roomId2]) => {
|
||||
cy.createSpace({
|
||||
name: spaceName,
|
||||
initial_state: [
|
||||
spaceChildInitialState(roomId1),
|
||||
spaceChildInitialState(roomId2),
|
||||
],
|
||||
}).as("spaceId");
|
||||
});
|
||||
|
||||
cy.get("@spaceId").then(() => {
|
||||
cy.viewSpaceHomeByName(spaceName);
|
||||
});
|
||||
cy.get(".mx_SpaceRoomView .mx_SpaceHierarchy_list").within(() => {
|
||||
cy.contains(".mx_SpaceHierarchy_roomTile", "Music").should("exist");
|
||||
cy.contains(".mx_SpaceHierarchy_roomTile", "Gaming").should("exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render subspaces in the space panel only when expanded", () => {
|
||||
cy.injectAxe();
|
||||
|
||||
cy.createSpace({
|
||||
name: "Child Space",
|
||||
initial_state: [],
|
||||
}).then(spaceId => {
|
||||
cy.createSpace({
|
||||
name: "Root Space",
|
||||
initial_state: [
|
||||
spaceChildInitialState(spaceId),
|
||||
],
|
||||
}).as("spaceId");
|
||||
});
|
||||
cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Root Space"]').should("exist");
|
||||
cy.get('.mx_SpacePanel .mx_SpaceButton[aria-label="Child Space"]').should("not.exist");
|
||||
|
||||
const axeOptions = {
|
||||
rules: {
|
||||
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
|
||||
'nested-interactive': {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
cy.checkA11y(undefined, axeOptions);
|
||||
cy.get(".mx_SpacePanel").percySnapshotElement("Space panel collapsed", { widths: [68] });
|
||||
|
||||
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.checkA11y(undefined, axeOptions);
|
||||
cy.get(".mx_SpacePanel").percySnapshotElement("Space panel expanded", { widths: [258] });
|
||||
});
|
||||
});
|
|
@ -1,482 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { MatrixClient } from "../../global";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import Loggable = Cypress.Loggable;
|
||||
import Timeoutable = Cypress.Timeoutable;
|
||||
import Withinable = Cypress.Withinable;
|
||||
import Shadow = Cypress.Shadow;
|
||||
|
||||
export enum Filter {
|
||||
People = "people",
|
||||
PublicRooms = "public_rooms"
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Opens the spotlight dialog
|
||||
*/
|
||||
openSpotlightDialog(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightDialog(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightFilter(
|
||||
filter: Filter | null,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightSearch(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
spotlightResults(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
roomHeaderName(
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
startDM(name: string): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("openSpotlightDialog", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get('.mx_RoomSearch_spotlightTrigger', options).click({ force: true });
|
||||
return cy.spotlightDialog(options);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("spotlightDialog", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get('[role=dialog][aria-label="Search Dialog"]', options);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("spotlightFilter", (
|
||||
filter: Filter | null,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
let selector: string;
|
||||
switch (filter) {
|
||||
case Filter.People:
|
||||
selector = "#mx_SpotlightDialog_button_startChat";
|
||||
break;
|
||||
case Filter.PublicRooms:
|
||||
selector = "#mx_SpotlightDialog_button_explorePublicRooms";
|
||||
break;
|
||||
default:
|
||||
selector = ".mx_SpotlightDialog_filter";
|
||||
break;
|
||||
}
|
||||
return cy.get(selector, options).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("spotlightSearch", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_SpotlightDialog_searchBox input", options);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("spotlightResults", (
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>,
|
||||
): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option", options);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("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(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(name);
|
||||
cy.wait(1000); // wait for the dialog code to settle
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
});
|
||||
// send first message to start DM
|
||||
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);
|
||||
});
|
||||
|
||||
describe("Spotlight", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
const bot1Name = "BotBob";
|
||||
let bot1: MatrixClient;
|
||||
|
||||
const bot2Name = "ByteBot";
|
||||
let bot2: MatrixClient;
|
||||
|
||||
const room1Name = "247";
|
||||
let room1Id: string;
|
||||
|
||||
const room2Name = "Lounge";
|
||||
let room2Id: string;
|
||||
|
||||
const room3Name = "Public";
|
||||
let room3Id: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Jim").then(() =>
|
||||
cy.getBot(synapse, { displayName: bot1Name }).then(_bot1 => {
|
||||
bot1 = _bot1;
|
||||
}),
|
||||
).then(() =>
|
||||
cy.getBot(synapse, { displayName: bot2Name }).then(_bot2 => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
bot2 = _bot2;
|
||||
}),
|
||||
).then(() =>
|
||||
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
|
||||
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => {
|
||||
room1Id = _room1Id;
|
||||
bot1.joinRoom(room1Id);
|
||||
cy.visit("/#/room/" + room1Id);
|
||||
});
|
||||
bot2.createRoom({ name: room2Name, visibility: Visibility.Public })
|
||||
.then(({ room_id: _room2Id }) => {
|
||||
room2Id = _room2Id;
|
||||
bot2.invite(room2Id, bot1.getUserId());
|
||||
});
|
||||
bot2.createRoom({
|
||||
name: room3Name,
|
||||
visibility: Visibility.Public, initial_state: [{
|
||||
type: "m.room.history_visibility",
|
||||
state_key: "",
|
||||
content: {
|
||||
history_visibility: "world_readable",
|
||||
},
|
||||
}],
|
||||
}).then(({ room_id: _room3Id }) => {
|
||||
room3Id = _room3Id;
|
||||
bot2.invite(room3Id, bot1.getUserId());
|
||||
});
|
||||
}),
|
||||
).then(() =>
|
||||
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.visit("/#/home");
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should be able to add and remove filters via keyboard", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightSearch().type("{downArrow}");
|
||||
cy.get("#mx_SpotlightDialog_button_explorePublicRooms").should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightSearch().type("{enter}");
|
||||
cy.get(".mx_SpotlightDialog_filter").should("contain", "Public rooms");
|
||||
cy.spotlightSearch().type("{backspace}");
|
||||
cy.get(".mx_SpotlightDialog_filter").should("not.exist");
|
||||
|
||||
cy.spotlightSearch().type("{downArrow}");
|
||||
cy.spotlightSearch().type("{downArrow}");
|
||||
cy.get("#mx_SpotlightDialog_button_startChat").should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightSearch().type("{enter}");
|
||||
cy.get(".mx_SpotlightDialog_filter").should("contain", "People");
|
||||
cy.spotlightSearch().type("{backspace}");
|
||||
cy.get(".mx_SpotlightDialog_filter").should("not.exist");
|
||||
});
|
||||
});
|
||||
|
||||
it("should find joined rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find known public rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room1Id);
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", room1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown public rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room2Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room2Id);
|
||||
}).then(() => {
|
||||
cy.get(".mx_RoomView_MessageList").should("have.length", 1);
|
||||
cy.roomHeaderName().should("contain", room2Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown public world readable rooms", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||
cy.spotlightResults().eq(0).should("contain", "View");
|
||||
cy.spotlightResults().eq(0).click();
|
||||
cy.url().should("contain", room3Id);
|
||||
}).then(() => {
|
||||
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
|
||||
cy.roomHeaderName().should("contain", room3Name);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
||||
// We obviously don’t have federation or bridges in cypress tests
|
||||
/*
|
||||
const room3Name = "Matrix HQ";
|
||||
const room3Id = "#matrix:matrix.org";
|
||||
|
||||
it("should find unknown public rooms on other homeservers", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.PublicRooms);
|
||||
cy.spotlightSearch().clear().type(room3Name);
|
||||
cy.get("[aria-haspopup=true][role=button]").click();
|
||||
}).then(() => {
|
||||
cy.contains(".mx_GenericDropdownMenu_Option--header", "matrix.org")
|
||||
.next("[role=menuitemradio]")
|
||||
.click();
|
||||
cy.wait(3_600_000);
|
||||
}).then(() => cy.spotlightDialog().within(() => {
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||
cy.spotlightResults().eq(0).should("contain", room3Id);
|
||||
}));
|
||||
});
|
||||
*/
|
||||
it("should find known people", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot1Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", bot1Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find unknown people", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.spotlightResults().eq(0).click();
|
||||
}).then(() => {
|
||||
cy.roomHeaderName().should("contain", bot2Name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should find group DMs by usernames or user ids", () => {
|
||||
// First we want to share a room with both bots to ensure we’ve got their usernames cached
|
||||
cy.inviteUser(room1Id, bot2.getUserId());
|
||||
|
||||
// Starting a DM with ByteBot (will be turned into a group dm later)
|
||||
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();
|
||||
});
|
||||
|
||||
// Send first message to actually start DM
|
||||
cy.roomHeaderName().should("contain", bot2Name);
|
||||
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 });
|
||||
cy.get(".mx_RoomSublist[aria-label=People]").should("contain", bot2Name);
|
||||
|
||||
// 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 => {
|
||||
cy.inviteUser(groupDm.roomId, bot1.getUserId());
|
||||
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));
|
||||
|
||||
// Search for BotBob by id, should return group DM and user
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot1.getUserId());
|
||||
cy.wait(1000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 2);
|
||||
cy.contains(
|
||||
".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option",
|
||||
groupDm.name,
|
||||
);
|
||||
});
|
||||
|
||||
// Search for ByteBot by id, should return group DM and user
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2.getUserId());
|
||||
cy.wait(1000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 2);
|
||||
cy.contains(
|
||||
".mx_SpotlightDialog_section.mx_SpotlightDialog_results .mx_SpotlightDialog_option",
|
||||
groupDm.name,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Test against https://github.com/vector-im/element-web/issues/22851
|
||||
it("should show each person result only once", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
|
||||
// 2 rounds of search to simulate the bug conditions. Specifically, the first search
|
||||
// should have 1 result (not 2) and the second search should also have 1 result (instead
|
||||
// of the super buggy 3 described by https://github.com/vector-im/element-web/issues/22851)
|
||||
//
|
||||
// We search for user ID to trigger the profile lookup within the dialog.
|
||||
for (let i = 0; i < 2; i++) {
|
||||
cy.log("Iteration: " + i);
|
||||
cy.spotlightSearch().clear().type(bot1.getUserId());
|
||||
cy.wait(1000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot1.getUserId());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow opening group chat dialog", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot2Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
cy.spotlightResults().eq(0).should("contain", bot2Name);
|
||||
cy.get(".mx_SpotlightDialog_startGroupChat").should("contain", "Start a group chat");
|
||||
cy.get(".mx_SpotlightDialog_startGroupChat").click();
|
||||
}).then(() => {
|
||||
cy.get('[role=dialog]').should("contain", "Direct Messages");
|
||||
});
|
||||
});
|
||||
|
||||
it("should close spotlight after starting a DM", () => {
|
||||
cy.startDM(bot1Name);
|
||||
cy.get(".mx_SpotlightDialog").should("have.length", 0);
|
||||
});
|
||||
|
||||
it("should show the same user only once", () => {
|
||||
cy.startDM(bot1Name);
|
||||
cy.visit("/#/home");
|
||||
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type(bot1Name);
|
||||
cy.wait(3000); // wait for the dialog code to settle
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
cy.spotlightResults().should("have.length", 1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to navigate results via keyboard", () => {
|
||||
cy.openSpotlightDialog().within(() => {
|
||||
cy.spotlightFilter(Filter.People);
|
||||
cy.spotlightSearch().clear().type("b");
|
||||
// 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.spotlightSearch().type("{downArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
});
|
||||
cy.spotlightSearch().type("{downArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
cy.spotlightSearch().type("{upArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
});
|
||||
cy.spotlightSearch().type("{upArrow}").then(() => {
|
||||
cy.spotlightResults().eq(0)
|
||||
.should("have.attr", "aria-selected", "true");
|
||||
cy.spotlightResults().eq(1)
|
||||
.should("have.attr", "aria-selected", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,279 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
|
||||
function markWindowBeforeReload(): void {
|
||||
// mark our window object to "know" when it gets reloaded
|
||||
cy.window().then(w => w.beforeReload = true);
|
||||
}
|
||||
|
||||
describe("Threads", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Default threads to ON for this spec
|
||||
cy.enableLabsFeature("feature_thread");
|
||||
cy.window().then(win => {
|
||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||
});
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Tom");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should reload when enabling threads beta", () => {
|
||||
markWindowBeforeReload();
|
||||
|
||||
// Turn off
|
||||
cy.openUserSettings("Labs").within(() => {
|
||||
// initially the new property is there
|
||||
cy.window().should("have.prop", "beforeReload", true);
|
||||
|
||||
cy.leaveBeta("Threads");
|
||||
cy.wait(1000);
|
||||
// after reload the property should be gone
|
||||
cy.window().should("not.have.prop", "beforeReload");
|
||||
});
|
||||
|
||||
cy.get(".mx_MatrixChat", { timeout: 15000 }); // wait for the app
|
||||
markWindowBeforeReload();
|
||||
|
||||
// Turn on
|
||||
cy.openUserSettings("Labs").within(() => {
|
||||
// initially the new property is there
|
||||
cy.window().should("have.prop", "beforeReload", true);
|
||||
|
||||
cy.joinBeta("Threads");
|
||||
cy.wait(1000);
|
||||
// after reload the property should be gone
|
||||
cy.window().should("not.have.prop", "beforeReload");
|
||||
});
|
||||
});
|
||||
|
||||
it("should be usable for a conversation", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: false,
|
||||
}).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
bot.joinRoom(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
// User sends message
|
||||
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
|
||||
|
||||
// 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");
|
||||
|
||||
// Bot starts thread
|
||||
cy.get<string>("@threadId").then(threadId => {
|
||||
bot.sendMessage(roomId, threadId, {
|
||||
body: "Hello there",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
|
||||
// User asserts timeline thread summary visible & clicks it
|
||||
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", "Hello there");
|
||||
cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
|
||||
|
||||
// User responds in thread
|
||||
cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Test{enter}");
|
||||
|
||||
// User asserts summary was updated correctly
|
||||
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", "Test");
|
||||
|
||||
// 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
|
||||
cy.get(".mx_EmojiPicker").within(() => {
|
||||
cy.get('input[type="text"]').type("wave");
|
||||
cy.contains('[role="menuitem"]', "👋").click();
|
||||
});
|
||||
|
||||
// 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
|
||||
cy.get(".mx_IconizedContextMenu").within(() => {
|
||||
cy.contains('[role="menuitem"]', "Remove").click();
|
||||
});
|
||||
cy.get(".mx_TextInputDialog").within(() => {
|
||||
cy.contains(".mx_Dialog_primary", "Remove").click();
|
||||
});
|
||||
|
||||
// User asserts summary was updated correctly
|
||||
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", "Hello there");
|
||||
|
||||
// User closes right panel after clicking back to thread list
|
||||
cy.get(".mx_ThreadView .mx_BaseCard_back").click();
|
||||
cy.get(".mx_ThreadPanel .mx_BaseCard_close").click();
|
||||
|
||||
// Bot responds to thread
|
||||
cy.get<string>("@threadId").then(threadId => {
|
||||
bot.sendMessage(roomId, threadId, {
|
||||
body: "How are things?",
|
||||
msgtype: "m.text",
|
||||
});
|
||||
});
|
||||
|
||||
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", "How are things?");
|
||||
// User asserts thread list unread indicator
|
||||
cy.get('.mx_HeaderButtons [aria-label="Threads"]').should("have.class", "mx_RightPanel_headerButton_unread");
|
||||
|
||||
// User opens thread list
|
||||
cy.get('.mx_HeaderButtons [aria-label="Threads"]').click();
|
||||
|
||||
// User asserts thread with correct root & latest events & unread dot
|
||||
cy.get(".mx_ThreadPanel .mx_EventTile_last").within(() => {
|
||||
cy.get(".mx_EventTile_body").should("contain", "Hello Mr. Bot");
|
||||
cy.get(".mx_ThreadSummary_content").should("contain", "How are things?");
|
||||
// User opens thread via threads list
|
||||
cy.get(".mx_EventTile_line").click();
|
||||
});
|
||||
|
||||
// User responds & asserts
|
||||
cy.get(".mx_ThreadView .mx_BasicMessageComposer_input").type("Great!{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!");
|
||||
|
||||
// User edits & asserts
|
||||
cy.contains(".mx_ThreadView .mx_EventTile_last .mx_EventTile_line", "Great!").within(() => {
|
||||
cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
|
||||
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?");
|
||||
|
||||
// User closes right panel
|
||||
cy.get(".mx_ThreadView .mx_BaseCard_close").click();
|
||||
|
||||
// Bot responds to thread and saves the id of their message to @eventId
|
||||
cy.get<string>("@threadId").then(threadId => {
|
||||
cy.wrap(bot.sendMessage(roomId, threadId, {
|
||||
body: "I'm very good thanks",
|
||||
msgtype: "m.text",
|
||||
}).then(res => res.event_id)).as("eventId");
|
||||
});
|
||||
|
||||
// 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");
|
||||
|
||||
// Bot edits their latest event
|
||||
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",
|
||||
},
|
||||
"m.relates_to": {
|
||||
"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 :)");
|
||||
});
|
||||
|
||||
it("can send voice messages", () => {
|
||||
// Increase viewport size and right-panel size, so that voice messages fit
|
||||
cy.viewport(1280, 720);
|
||||
cy.window().then((window) => {
|
||||
window.localStorage.setItem("mx_rhs_size", "600");
|
||||
});
|
||||
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
// Send message
|
||||
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
|
||||
|
||||
// Create thread
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.realHover().find(".mx_MessageActionBar_threadButton").click();
|
||||
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
|
||||
|
||||
cy.openMessageComposerOptions(true).find(`[aria-label="Voice Message"]`).click();
|
||||
cy.wait(3000);
|
||||
cy.getComposer(true).find(".mx_MessageComposer_sendMessage").click();
|
||||
|
||||
cy.get(".mx_ThreadView .mx_MVoiceMessageBody").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("right panel behaves correctly", () => {
|
||||
// Create room
|
||||
let roomId: string;
|
||||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
// Send message
|
||||
cy.get(".mx_RoomView_body .mx_BasicMessageComposer_input").type("Hello Mr. Bot{enter}");
|
||||
|
||||
// Create thread
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.realHover().find(".mx_MessageActionBar_threadButton").click();
|
||||
cy.get(".mx_ThreadView_timelinePanelWrapper").should("have.length", 1);
|
||||
|
||||
// Send message to thread
|
||||
cy.get(".mx_BaseCard .mx_BasicMessageComposer_input").type("Hello Mr. User{enter}");
|
||||
cy.get(".mx_BaseCard .mx_EventTile").should("contain", "Hello Mr. User");
|
||||
|
||||
// Close thread
|
||||
cy.get(".mx_BaseCard_close").click();
|
||||
|
||||
// Open existing thread
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", "Hello Mr. Bot")
|
||||
.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");
|
||||
});
|
||||
});
|
|
@ -1,364 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { MessageEvent } from "matrix-events-sdk";
|
||||
|
||||
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
// The avatar size used in the timeline
|
||||
const AVATAR_SIZE = 30;
|
||||
// The resize method used in the timeline
|
||||
const AVATAR_RESIZE_METHOD = "crop";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const OLD_AVATAR = "avatar_image1";
|
||||
const NEW_AVATAR = "avatar_image2";
|
||||
const OLD_NAME = "Alan";
|
||||
const NEW_NAME = "Alan (away)";
|
||||
|
||||
const getEventTilesWithBodies = (): Chainable<JQuery> => {
|
||||
return cy.get(".mx_EventTile").filter((_i, e) => e.getElementsByClassName("mx_EventTile_body").length > 0);
|
||||
};
|
||||
|
||||
const expectDisplayName = (e: JQuery<HTMLElement>, displayName: string): void => {
|
||||
expect(e.find(".mx_DisambiguatedProfile_displayName").text()).to.equal(displayName);
|
||||
};
|
||||
|
||||
const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
|
||||
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
|
||||
cli.mxcUrlToHttp(avatarUrl, size, size, AVATAR_RESIZE_METHOD),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const sendEvent = (roomId: string, html = false): Chainable<ISendEventResponse> => {
|
||||
return cy.sendEvent(
|
||||
roomId,
|
||||
null,
|
||||
"m.room.message" as EventType,
|
||||
MessageEvent.from("Message", html ? "<b>Message</b>" : undefined).serialize().content,
|
||||
);
|
||||
};
|
||||
|
||||
describe("Timeline", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
let roomId: string;
|
||||
|
||||
let oldAvatarUrl: string;
|
||||
let newAvatarUrl: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, OLD_NAME).then(() =>
|
||||
cy.createRoom({ name: ROOM_NAME }).then(_room1Id => {
|
||||
roomId = _room1Id;
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
describe("useOnlyCurrentProfiles", () => {
|
||||
beforeEach(() => {
|
||||
cy.uploadContent(OLD_AVATAR).then(({ content_uri: url }) => {
|
||||
oldAvatarUrl = url;
|
||||
cy.setAvatarUrl(url);
|
||||
});
|
||||
cy.uploadContent(NEW_AVATAR).then(({ content_uri: url }) => {
|
||||
newAvatarUrl = url;
|
||||
});
|
||||
});
|
||||
|
||||
it("should show historical profiles if disabled", () => {
|
||||
cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, false);
|
||||
sendEvent(roomId);
|
||||
cy.setDisplayName("Alan (away)");
|
||||
cy.setAvatarUrl(newAvatarUrl);
|
||||
// XXX: If we send the second event too quickly, there won't be
|
||||
// enough time for the client to register the profile change
|
||||
cy.wait(500);
|
||||
sendEvent(roomId);
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
const events = getEventTilesWithBodies();
|
||||
|
||||
events.should("have.length", 2);
|
||||
events.each((e, i) => {
|
||||
if (i === 0) {
|
||||
expectDisplayName(e, OLD_NAME);
|
||||
expectAvatar(e, oldAvatarUrl);
|
||||
} else if (i === 1) {
|
||||
expectDisplayName(e, NEW_NAME);
|
||||
expectAvatar(e, newAvatarUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not show historical profiles if enabled", () => {
|
||||
cy.setSettingValue("useOnlyCurrentProfiles", null, SettingLevel.ACCOUNT, true);
|
||||
sendEvent(roomId);
|
||||
cy.setDisplayName(NEW_NAME);
|
||||
cy.setAvatarUrl(newAvatarUrl);
|
||||
// XXX: If we send the second event too quickly, there won't be
|
||||
// enough time for the client to register the profile change
|
||||
cy.wait(500);
|
||||
sendEvent(roomId);
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
const events = getEventTilesWithBodies();
|
||||
|
||||
events.should("have.length", 2);
|
||||
events.each((e) => {
|
||||
expectDisplayName(e, NEW_NAME);
|
||||
expectAvatar(e, newAvatarUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("message displaying", () => {
|
||||
beforeEach(() => {
|
||||
cy.injectAxe();
|
||||
});
|
||||
|
||||
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.get(".mx_Spinner").should("not.exist");
|
||||
cy.percySnapshot("Configured room on IRC layout");
|
||||
});
|
||||
|
||||
it("should add inline start margin to an event line on IRC layout", () => {
|
||||
cy.visit("/#/room/" + roomId);
|
||||
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");
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
|
||||
// Check the event line has margin instead of inset property
|
||||
// cf. _EventTile.pcss
|
||||
// --EventTile_irc_line_info-margin-inline-start
|
||||
// = 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");
|
||||
|
||||
cy.get(".mx_Spinner").should("not.exist");
|
||||
// Exclude timestamp from snapshot
|
||||
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();
|
||||
});
|
||||
|
||||
it("should set inline start padding to a hidden event line", () => {
|
||||
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");
|
||||
|
||||
// Edit message
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
|
||||
cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
|
||||
cy.get(".mx_BasicMessageComposer_input").type("Edit{enter}");
|
||||
});
|
||||
cy.contains(".mx_EventTile[data-scroll-tokens]", "MessageEdit").should("exist");
|
||||
|
||||
// Click timestamp to highlight hidden event line
|
||||
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; }";
|
||||
|
||||
// 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.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');
|
||||
cy.percySnapshot("Hidden event line with padding on modern layout", { percyCSS });
|
||||
});
|
||||
|
||||
it("should click top left of view source event toggle", () => {
|
||||
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");
|
||||
|
||||
// Edit message
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile .mx_EventTile_line", "Message").within(() => {
|
||||
cy.get('[aria-label="Edit"]').click({ force: true }); // Cypress has no ability to hover
|
||||
cy.get(".mx_BasicMessageComposer_input").type("Edit{enter}");
|
||||
});
|
||||
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 });
|
||||
});
|
||||
|
||||
// Make sure the expand toggle worked
|
||||
cy.get(".mx_EventTile .mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle").should("be.visible");
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
|
||||
// Click "collapse" link button on the first hovered info event line
|
||||
cy.get(".mx_GenericEventListSummary_unstyledList .mx_EventTile_info:first-of-type").realHover();
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=true]").click({ force: false });
|
||||
|
||||
// Make sure "collapse" link button worked
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").should("exist");
|
||||
});
|
||||
|
||||
it("should highlight search result words regardless of formatting", () => {
|
||||
sendEvent(roomId);
|
||||
sendEvent(roomId, true);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
|
||||
cy.get(".mx_RoomHeader_searchButton").click();
|
||||
cy.get(".mx_SearchBar_input input").type("Message{enter}");
|
||||
|
||||
cy.get(".mx_EventTile:not(.mx_EventTile_contextual) .mx_EventTile_searchHighlight").should("exist");
|
||||
cy.get(".mx_RoomView_searchResultsPanel").percySnapshotElement("Highlighted search results");
|
||||
});
|
||||
|
||||
it("should render url previews", () => {
|
||||
cy.intercept("**/_matrix/media/r0/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*", {
|
||||
statusCode: 200,
|
||||
fixture: "riot.png",
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
},
|
||||
}).as("mxc");
|
||||
cy.intercept("**/_matrix/media/r0/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*", {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
"og:title": "Element Call",
|
||||
"og:description": null,
|
||||
"og:image:width": 48,
|
||||
"og:image:height": 48,
|
||||
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
|
||||
"og:image:type": "image/png",
|
||||
"matrix:image:size": 2121,
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).as("preview_url");
|
||||
|
||||
cy.sendEvent(
|
||||
roomId,
|
||||
null,
|
||||
"m.room.message" as EventType,
|
||||
MessageEvent.from("https://call.element.io/").serialize().content,
|
||||
);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
|
||||
cy.get(".mx_LinkPreviewWidget").should("exist").should("contain.text", "Element Call");
|
||||
|
||||
cy.wait("@preview_url");
|
||||
cy.wait("@mxc");
|
||||
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_EventTile_last").percySnapshotElement("URL Preview", {
|
||||
widths: [800, 400],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("message sending", () => {
|
||||
const MESSAGE = "Hello world";
|
||||
const viewRoomSendMessageAndSetupReply = () => {
|
||||
// View room
|
||||
cy.visit("/#/room/" + roomId);
|
||||
|
||||
// Send a message
|
||||
cy.getComposer().type(`${MESSAGE}{enter}`);
|
||||
|
||||
// Reply to the message
|
||||
cy.contains(".mx_RoomView_body .mx_EventTile_line", "Hello world").within(() => {
|
||||
cy.get('[aria-label="Reply"]').click({ force: true }); // Cypress has no ability to hover
|
||||
});
|
||||
};
|
||||
|
||||
it("can reply with a text message", () => {
|
||||
const reply = "Reply";
|
||||
viewRoomSendMessageAndSetupReply();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it("can reply with a voice message", () => {
|
||||
viewRoomSendMessageAndSetupReply();
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
cy.get(`[aria-label="Voice Message"]`).click();
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
function assertNoToasts(): void {
|
||||
cy.get(".mx_Toast_toast").should("not.exist");
|
||||
}
|
||||
|
||||
function getToast(expectedTitle: string): Chainable<JQuery> {
|
||||
return cy.contains(".mx_Toast_toast h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
|
||||
}
|
||||
|
||||
function acceptToast(expectedTitle: string): void {
|
||||
getToast(expectedTitle).within(() => {
|
||||
cy.get(".mx_Toast_buttons .mx_AccessibleButton_kind_primary").click();
|
||||
});
|
||||
}
|
||||
|
||||
function rejectToast(expectedTitle: string): void {
|
||||
getToast(expectedTitle).within(() => {
|
||||
cy.get(".mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline").click();
|
||||
});
|
||||
}
|
||||
|
||||
describe("Analytics Toast", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should not show an analytics toast if config has nothing about posthog", () => {
|
||||
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 => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Tod");
|
||||
});
|
||||
|
||||
rejectToast("Notifications");
|
||||
assertNoToasts();
|
||||
});
|
||||
|
||||
describe("with posthog enabled", () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept("/config.json?cachebuster=*", req => {
|
||||
req.continue(res => {
|
||||
res.send(200, {
|
||||
...res.body,
|
||||
posthog: {
|
||||
project_api_key: "foo",
|
||||
api_host: "bar",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Tod");
|
||||
rejectToast("Notifications");
|
||||
});
|
||||
});
|
||||
|
||||
it("should show an analytics toast which can be accepted", () => {
|
||||
acceptToast("Help improve Element");
|
||||
assertNoToasts();
|
||||
});
|
||||
|
||||
it("should show an analytics toast which can be rejected", () => {
|
||||
rejectToast("Help improve Element");
|
||||
assertNoToasts();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("Update", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should navigate to ?updated=$VERSION if realises it is immediately out of date on load", () => {
|
||||
const NEW_VERSION = "some-new-version";
|
||||
|
||||
cy.intercept("/version*", {
|
||||
statusCode: 200,
|
||||
body: NEW_VERSION,
|
||||
headers: {
|
||||
"Content-Type": "test/plain",
|
||||
},
|
||||
}).as("version");
|
||||
|
||||
cy.initTestUser(synapse, "Ursa");
|
||||
|
||||
cy.wait("@version");
|
||||
cy.url().should("contain", "updated=" + NEW_VERSION).then(href => {
|
||||
const url = new URL(href);
|
||||
expect(url.searchParams.get("updated")).to.equal(NEW_VERSION);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,47 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import type { UserCredentials } from "../../support/login";
|
||||
|
||||
describe("User Menu", () => {
|
||||
let synapse: SynapseInstance;
|
||||
let user: UserCredentials;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Jeff").then(credentials => {
|
||||
user = credentials;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should contain our name & userId", () => {
|
||||
cy.get('[aria-label="User menu"]').click();
|
||||
cy.get(".mx_UserMenu_contextMenu").within(() => {
|
||||
cy.get(".mx_UserMenu_contextMenu_displayName").should("contain", "Jeff");
|
||||
cy.get(".mx_UserMenu_contextMenu_userId").should("contain", user.userId);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { MatrixClient } from "../../global";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("User Onboarding (new user)", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
const bot1Name = "BotBob";
|
||||
let bot1: MatrixClient;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Jane Doe");
|
||||
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 => {
|
||||
bot1 = _bot1;
|
||||
});
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("page is shown and preference exists", () => {
|
||||
cy.get('.mx_UserOnboardingPage')
|
||||
.percySnapshotElement("User onboarding page");
|
||||
cy.openUserSettings("Preferences");
|
||||
cy.contains("Show shortcut to welcome checklist above the room list").should("exist");
|
||||
});
|
||||
|
||||
it("app download dialog", () => {
|
||||
cy.contains(".mx_UserOnboardingTask_action", "Download apps").click();
|
||||
cy.get('[role=dialog]')
|
||||
.contains("#mx_BaseDialog_title", "Download Element")
|
||||
.should("exist");
|
||||
cy.get('[role=dialog]')
|
||||
.percySnapshotElement("App download dialog", {
|
||||
widths: [640],
|
||||
});
|
||||
});
|
||||
|
||||
it("using find friends action should increase progress", () => {
|
||||
cy.get(".mx_ProgressBar").invoke("val").then((oldProgress) => {
|
||||
const findPeopleAction = cy.contains(".mx_UserOnboardingTask_action", "Find friends");
|
||||
expect(findPeopleAction).to.exist;
|
||||
findPeopleAction.click();
|
||||
cy.get(".mx_InviteDialog_editor input").type(bot1.getUserId());
|
||||
cy.get(".mx_InviteDialog_buttonAndSpinner").click();
|
||||
cy.get(".mx_InviteDialog_buttonAndSpinner").should("not.exist");
|
||||
const message = "Hi!";
|
||||
cy.get(".mx_SendMessageComposer").type(`${message}!{enter}`);
|
||||
cy.contains(".mx_MTextBody.mx_EventTile_content", message);
|
||||
cy.visit("/#/home");
|
||||
cy.get('.mx_UserOnboardingPage').should('exist');
|
||||
cy.get('.mx_UserOnboardingButton').should('exist');
|
||||
cy.get('.mx_UserOnboardingList')
|
||||
.should('exist')
|
||||
.should(($list) => {
|
||||
const list = $list.get(0);
|
||||
expect(getComputedStyle(list).opacity).to.be.eq("1");
|
||||
});
|
||||
cy.get(".mx_ProgressBar").invoke("val").should("be.greaterThan", oldProgress);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
describe("User Onboarding (old user)", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
cy.initTestUser(synapse, "Jane Doe");
|
||||
cy.window({ log: false }).then(win => {
|
||||
win.localStorage.setItem("mx_registration_time", "2");
|
||||
});
|
||||
cy.reload().then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 15000 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.visit("/#/home");
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("page and preference are hidden", () => {
|
||||
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");
|
||||
});
|
||||
});
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { MatrixClient } from "../../global";
|
||||
|
||||
describe("UserView", () => {
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Violet");
|
||||
cy.getBot(synapse, { displayName: "Usman" }).as("bot");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it("should render the user view as expected", () => {
|
||||
cy.get<MatrixClient>("@bot").then(bot => {
|
||||
cy.visit(`/#/user/${bot.getUserId()}`);
|
||||
});
|
||||
|
||||
cy.get("#mx_RightPanel .mx_UserInfo_profile h2").should("contain", "Usman");
|
||||
cy.get(".mx_RightPanel .mx_Spinner").should("not.exist"); // wait for spinners to finish
|
||||
cy.get(".mx_RightPanel").percySnapshotElement("User View", {
|
||||
// Hide the MXID field as it'll vary on each test
|
||||
percyCSS: ".mx_UserInfo_profile_mxid { visibility: hidden !important; }",
|
||||
widths: [260, 500],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 Oliver Sand
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IWidget } from "matrix-widget-api";
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
const ROOM_NAME = 'Test Room';
|
||||
const WIDGET_ID = "fake-widget";
|
||||
const WIDGET_HTML = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Fake Widget</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello World
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
describe('Widget Layout', () => {
|
||||
let widgetUrl: string;
|
||||
let synapse: SynapseInstance;
|
||||
let roomId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
});
|
||||
cy.serveHtmlFile(WIDGET_HTML).then(url => {
|
||||
widgetUrl = url;
|
||||
});
|
||||
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME,
|
||||
}).then((id) => {
|
||||
roomId = id;
|
||||
|
||||
// setup widget via state event
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content: IWidget = {
|
||||
id: WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
type: 'widget',
|
||||
name: 'widget',
|
||||
url: widgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, WIDGET_ID);
|
||||
}).as('widgetEventSent');
|
||||
|
||||
// set initial layout
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
|
||||
}).as('layoutEventSent');
|
||||
});
|
||||
|
||||
cy.all([
|
||||
cy.get<string>("@widgetEventSent"),
|
||||
cy.get<string>("@layoutEventSent"),
|
||||
]).then(() => {
|
||||
// open the room
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
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('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);
|
||||
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
|
||||
});
|
||||
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
|
||||
});
|
||||
});
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
|
||||
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
|
||||
const STICKER_NAME = "Test Sticker";
|
||||
const ROOM_NAME_1 = "Sticker Test";
|
||||
const ROOM_NAME_2 = "Sticker Test Two";
|
||||
const STICKER_MESSAGE = JSON.stringify({
|
||||
action: "m.sticker",
|
||||
api: "fromWidget",
|
||||
data: {
|
||||
name: "teststicker",
|
||||
description: STICKER_NAME,
|
||||
file: "test.png",
|
||||
content: {
|
||||
body: STICKER_NAME,
|
||||
msgtype: "m.sticker",
|
||||
url: "mxc://somewhere",
|
||||
},
|
||||
},
|
||||
requestId: "1",
|
||||
widgetId: STICKER_PICKER_WIDGET_ID,
|
||||
});
|
||||
const WIDGET_HTML = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Fake Sticker Picker</title>
|
||||
<script>
|
||||
window.onmessage = ev => {
|
||||
if (ev.data.action === 'capabilities') {
|
||||
window.parent.postMessage(Object.assign({
|
||||
response: {
|
||||
capabilities: ["m.sticker"]
|
||||
},
|
||||
}, ev.data), '*');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<button name="Send" id="sendsticker">Press for sticker</button>
|
||||
<script>
|
||||
document.getElementById('sendsticker').onclick = () => {
|
||||
window.parent.postMessage(${STICKER_MESSAGE}, '*')
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
function openStickerPicker() {
|
||||
cy.get('.mx_MessageComposer_buttonMenu').click();
|
||||
cy.get('#stickersButton').click();
|
||||
}
|
||||
|
||||
function sendStickerFromPicker() {
|
||||
// Note: Until https://github.com/cypress-io/cypress/issues/136 is fixed we will need
|
||||
// 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();
|
||||
});
|
||||
|
||||
// Sticker picker should close itself after sending.
|
||||
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}/`);
|
||||
|
||||
// Make sure the image points at the sticker image
|
||||
cy.get<HTMLImageElement>(`img[alt="${STICKER_NAME}"]`)
|
||||
.should("have.attr", "src")
|
||||
.and("match", /thumbnail\/somewhere\?/);
|
||||
}
|
||||
|
||||
describe("Stickers", () => {
|
||||
// We spin up a web server for the sticker picker so that we're not testing to see if
|
||||
// sysadmins can deploy sticker pickers on the same Element domain - we actually want
|
||||
// to make sure that cross-origin postMessage works properly. This makes it difficult
|
||||
// to write the test though, as we have to juggle iframe logistics.
|
||||
//
|
||||
// See sendStickerFromPicker() for more detail on iframe comms.
|
||||
|
||||
let stickerPickerUrl: string;
|
||||
let synapse: SynapseInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
});
|
||||
cy.serveHtmlFile(WIDGET_HTML).then(url => {
|
||||
stickerPickerUrl = url;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it('should send a sticker to multiple rooms', () => {
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME_1,
|
||||
}).as("roomId1");
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME_2,
|
||||
}).as("roomId2");
|
||||
cy.setAccountData("m.widgets", {
|
||||
[STICKER_PICKER_WIDGET_ID]: {
|
||||
content: {
|
||||
type: "m.stickerpicker",
|
||||
name: STICKER_PICKER_WIDGET_NAME,
|
||||
url: stickerPickerUrl,
|
||||
},
|
||||
id: STICKER_PICKER_WIDGET_ID,
|
||||
},
|
||||
}).as("stickers");
|
||||
|
||||
cy.all([
|
||||
cy.get<string>("@roomId1"),
|
||||
cy.get<string>("@roomId2"),
|
||||
cy.get<{}>("@stickers"), // just want to wait for it to be set up
|
||||
]).then(([roomId1, roomId2]) => {
|
||||
cy.viewRoomByName(ROOM_NAME_1);
|
||||
cy.url().should("contain", `/#/room/${roomId1}`);
|
||||
openStickerPicker();
|
||||
sendStickerFromPicker();
|
||||
expectTimelineSticker(roomId1);
|
||||
|
||||
// Ensure that when we switch to a different room that the sticker
|
||||
// goes to the right place
|
||||
cy.viewRoomByName(ROOM_NAME_2);
|
||||
cy.url().should("contain", `/#/room/${roomId2}`);
|
||||
openStickerPicker();
|
||||
sendStickerFromPicker();
|
||||
expectTimelineSticker(roomId2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 Mikhail Aheichyk
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
|
||||
|
||||
import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { UserCredentials } from "../../support/login";
|
||||
|
||||
const DEMO_WIDGET_ID = "demo-widget-id";
|
||||
const DEMO_WIDGET_NAME = "Demo Widget";
|
||||
const DEMO_WIDGET_TYPE = "demo";
|
||||
const ROOM_NAME = "Demo";
|
||||
|
||||
const DEMO_WIDGET_HTML = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Demo Widget</title>
|
||||
<script>
|
||||
window.onmessage = ev => {
|
||||
if (ev.data.action === 'capabilities') {
|
||||
window.parent.postMessage(Object.assign({
|
||||
response: {
|
||||
capabilities: []
|
||||
},
|
||||
}, ev.data), '*');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="demo">Demo</button>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// mostly copied from src/utils/WidgetUtils.waitForRoomWidget with small modifications
|
||||
function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: string, add: boolean): Promise<void> {
|
||||
const matrixClient = win.mxMatrixClientPeg.get();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
function eventsInIntendedState(evList) {
|
||||
const widgetPresent = evList.some((ev) => {
|
||||
return ev.getContent() && ev.getContent()['id'] === widgetId;
|
||||
});
|
||||
if (add) {
|
||||
return widgetPresent;
|
||||
} else {
|
||||
return !widgetPresent;
|
||||
}
|
||||
}
|
||||
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
||||
const startingWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (eventsInIntendedState(startingWidgetEvents)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function onRoomStateEvents(ev: MatrixEvent) {
|
||||
if (ev.getRoomId() !== roomId || ev.getType() !== "im.vector.modular.widgets") return;
|
||||
|
||||
const currentWidgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
|
||||
if (eventsInIntendedState(currentWidgetEvents)) {
|
||||
matrixClient.removeListener(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
matrixClient.on(win.matrixcs.RoomStateEvent.Events, onRoomStateEvents);
|
||||
});
|
||||
}
|
||||
|
||||
describe("Widget PIP", () => {
|
||||
let synapse: SynapseInstance;
|
||||
let user: UserCredentials;
|
||||
let bot: MatrixClient;
|
||||
let demoWidgetUrl: string;
|
||||
|
||||
function roomCreateAddWidgetPip(userRemove: 'leave' | 'kick' | 'ban') {
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME,
|
||||
invite: [bot.getUserId()],
|
||||
}).then(roomId => {
|
||||
// sets bot to Admin and user to Moderator
|
||||
cy.getClient().then(matrixClient => {
|
||||
return matrixClient.sendStateEvent(roomId, 'm.room.power_levels', {
|
||||
users: {
|
||||
[user.userId]: 50,
|
||||
[bot.getUserId()]: 100,
|
||||
},
|
||||
});
|
||||
}).as('powerLevelsChanged');
|
||||
|
||||
// bot joins the room
|
||||
cy.botJoinRoom(bot, roomId).as('botJoined');
|
||||
|
||||
// setup widget via state event
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content: IWidget = {
|
||||
id: DEMO_WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
type: DEMO_WIDGET_TYPE,
|
||||
name: DEMO_WIDGET_NAME,
|
||||
url: demoWidgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, DEMO_WIDGET_ID);
|
||||
}).as('widgetEventSent');
|
||||
|
||||
// open the room
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
cy.all([
|
||||
cy.get<string>("@powerLevelsChanged"),
|
||||
cy.get<string>("@botJoined"),
|
||||
cy.get<string>("@widgetEventSent"),
|
||||
]).then(() => {
|
||||
cy.window().then(async win => {
|
||||
// wait for widget state event
|
||||
await waitForRoomWidget(win, DEMO_WIDGET_ID, roomId, true);
|
||||
|
||||
// activate widget in pip mode
|
||||
win.mxActiveWidgetStore.setWidgetPersistence(DEMO_WIDGET_ID, roomId, true);
|
||||
|
||||
// checks that pip window is opened
|
||||
cy.get(".mx_LegacyCallView_pip").should("exist");
|
||||
|
||||
// checks that widget is opened in pip
|
||||
cy.accessIframe(`iframe[title="${DEMO_WIDGET_NAME}"]`).within({}, () => {
|
||||
cy.get("#demo").should('exist').then(async () => {
|
||||
const userId = user.userId;
|
||||
if (userRemove == 'leave') {
|
||||
cy.getClient().then(async matrixClient => {
|
||||
await matrixClient.leave(roomId);
|
||||
});
|
||||
} else if (userRemove == 'kick') {
|
||||
await bot.kick(roomId, userId);
|
||||
} else if (userRemove == 'ban') {
|
||||
await bot.ban(roomId, userId);
|
||||
}
|
||||
|
||||
// checks that pip window is closed
|
||||
cy.get(".mx_LegacyCallView_pip").should("not.exist");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Mike").then(_user => {
|
||||
user = _user;
|
||||
});
|
||||
cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
});
|
||||
cy.serveHtmlFile(DEMO_WIDGET_HTML).then(url => {
|
||||
demoWidgetUrl = url;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it('should be closed on leave', () => {
|
||||
roomCreateAddWidgetPip('leave');
|
||||
});
|
||||
|
||||
it('should be closed on kick', () => {
|
||||
roomCreateAddWidgetPip('kick');
|
||||
});
|
||||
|
||||
it('should be closed on ban', () => {
|
||||
roomCreateAddWidgetPip('ban');
|
||||
});
|
||||
});
|
73
cypress/global.d.ts
vendored
73
cypress/global.d.ts
vendored
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "../src/@types/global";
|
||||
import "../src/@types/svg";
|
||||
import "../src/@types/raw-loader";
|
||||
import "matrix-js-sdk/src/@types/global";
|
||||
import type {
|
||||
MatrixClient,
|
||||
ClientEvent,
|
||||
MatrixScheduler,
|
||||
MemoryCryptoStore,
|
||||
MemoryStore,
|
||||
Preset,
|
||||
RoomStateEvent,
|
||||
Visibility,
|
||||
RoomMemberEvent,
|
||||
ICreateClientOpts,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
|
||||
import type PerformanceMonitor from "../src/performance";
|
||||
import type SettingsStore from "../src/settings/SettingsStore";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface ApplicationWindow {
|
||||
mxSettingsStore: typeof SettingsStore;
|
||||
mxMatrixClientPeg: {
|
||||
matrixClient?: MatrixClient;
|
||||
};
|
||||
mxDispatcher: MatrixDispatcher;
|
||||
mxPerformanceMonitor: PerformanceMonitor;
|
||||
beforeReload?: boolean; // for detecting reloads
|
||||
// Partial type for the matrix-js-sdk module, exported by browser-matrix
|
||||
matrixcs: {
|
||||
MatrixClient: typeof MatrixClient;
|
||||
ClientEvent: typeof ClientEvent;
|
||||
RoomMemberEvent: typeof RoomMemberEvent;
|
||||
RoomStateEvent: typeof RoomStateEvent;
|
||||
MatrixScheduler: typeof MatrixScheduler;
|
||||
MemoryStore: typeof MemoryStore;
|
||||
MemoryCryptoStore: typeof MemoryCryptoStore;
|
||||
Visibility: typeof Visibility;
|
||||
Preset: typeof Preset;
|
||||
createClient(opts: ICreateClientOpts | string);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
// to appease the MatrixDispatcher import
|
||||
mxDispatcher: MatrixDispatcher;
|
||||
// to appease the PerformanceMonitor import
|
||||
mxPerformanceMonitor: PerformanceMonitor;
|
||||
mxPerformanceEntryNames: any;
|
||||
}
|
||||
}
|
||||
|
||||
export { MatrixClient };
|
|
@ -1,157 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import * as os from "os";
|
||||
import * as crypto from "crypto";
|
||||
import * as childProcess from "child_process";
|
||||
import * as fse from "fs-extra";
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
|
||||
// A cypress plugin to run docker commands
|
||||
|
||||
export function dockerRun(opts: {
|
||||
image: string;
|
||||
containerName: string;
|
||||
params?: string[];
|
||||
cmd?: string;
|
||||
}): Promise<string> {
|
||||
const userInfo = os.userInfo();
|
||||
const params = opts.params ?? [];
|
||||
|
||||
if (params?.includes("-v") && userInfo.uid >= 0) {
|
||||
// On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult
|
||||
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
|
||||
}
|
||||
|
||||
const args = [
|
||||
"run",
|
||||
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
|
||||
"-d",
|
||||
...params,
|
||||
opts.image,
|
||||
];
|
||||
|
||||
if (opts.cmd) args.push(opts.cmd);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
childProcess.execFile("docker", args, (err, stdout) => {
|
||||
if (err) reject(err);
|
||||
resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dockerExec(args: {
|
||||
containerId: string;
|
||||
params: string[];
|
||||
}): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile("docker", [
|
||||
"exec", args.containerId,
|
||||
...args.params,
|
||||
], { encoding: 'utf8' }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.log(stdout);
|
||||
console.log(stderr);
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function dockerLogs(args: {
|
||||
containerId: string;
|
||||
stdoutFile?: string;
|
||||
stderrFile?: string;
|
||||
}): Promise<void> {
|
||||
const stdoutFile = args.stdoutFile ? await fse.open(args.stdoutFile, "w") : "ignore";
|
||||
const stderrFile = args.stderrFile ? await fse.open(args.stderrFile, "w") : "ignore";
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
childProcess.spawn("docker", [
|
||||
"logs",
|
||||
args.containerId,
|
||||
], {
|
||||
stdio: ["ignore", stdoutFile, stderrFile],
|
||||
}).once('close', resolve);
|
||||
});
|
||||
|
||||
if (args.stdoutFile) await fse.close(<number>stdoutFile);
|
||||
if (args.stderrFile) await fse.close(<number>stderrFile);
|
||||
}
|
||||
|
||||
export function dockerStop(args: {
|
||||
containerId: string;
|
||||
}): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"stop",
|
||||
args.containerId,
|
||||
], err => {
|
||||
if (err) reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dockerRm(args: {
|
||||
containerId: string;
|
||||
}): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"rm",
|
||||
args.containerId,
|
||||
], err => {
|
||||
if (err) reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function dockerIp(args: {
|
||||
containerId: string;
|
||||
}): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"inspect",
|
||||
"-f", "{{ .NetworkSettings.IPAddress }}",
|
||||
args.containerId,
|
||||
], (err, stdout) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export function docker(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
dockerRun,
|
||||
dockerExec,
|
||||
dockerLogs,
|
||||
dockerStop,
|
||||
dockerRm,
|
||||
dockerIp,
|
||||
});
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
import { synapseDocker } from "./synapsedocker";
|
||||
import { slidingSyncProxyDocker } from "./sliding-sync";
|
||||
import { webserver } from "./webserver";
|
||||
import { docker } from "./docker";
|
||||
import { log } from "./log";
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export default function(on: PluginEvents, config: PluginConfigOptions) {
|
||||
docker(on, config);
|
||||
synapseDocker(on, config);
|
||||
slidingSyncProxyDocker(on, config);
|
||||
webserver(on, config);
|
||||
log(on, config);
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
import { dockerExec, dockerIp, dockerRun, dockerStop } from "../docker";
|
||||
import { getFreePort } from "../utils/port";
|
||||
import { SynapseInstance } from "../synapsedocker";
|
||||
|
||||
// A cypress plugins to add command to start & stop https://github.com/matrix-org/sliding-sync
|
||||
|
||||
export interface ProxyInstance {
|
||||
containerId: string;
|
||||
postgresId: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
const instances = new Map<string, ProxyInstance>();
|
||||
|
||||
const PG_PASSWORD = "p4S5w0rD";
|
||||
|
||||
async function proxyStart(synapse: SynapseInstance): Promise<ProxyInstance> {
|
||||
console.log(new Date(), "Starting sliding sync proxy...");
|
||||
|
||||
const postgresId = await dockerRun({
|
||||
image: "postgres",
|
||||
containerName: "react-sdk-cypress-sliding-sync-postgres",
|
||||
params: [
|
||||
"--rm",
|
||||
"-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`,
|
||||
],
|
||||
});
|
||||
|
||||
const postgresIp = await dockerIp({ containerId: postgresId });
|
||||
const synapseIp = await dockerIp({ containerId: synapse.synapseId });
|
||||
console.log(new Date(), "postgres container up");
|
||||
|
||||
const waitTimeMillis = 30000;
|
||||
const startTime = new Date().getTime();
|
||||
let lastErr: Error;
|
||||
while ((new Date().getTime() - startTime) < waitTimeMillis) {
|
||||
try {
|
||||
await dockerExec({
|
||||
containerId: postgresId,
|
||||
params: [
|
||||
"pg_isready",
|
||||
"-U", "postgres",
|
||||
],
|
||||
});
|
||||
lastErr = null;
|
||||
break;
|
||||
} catch (err) {
|
||||
console.log("pg_isready: failed");
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
if (lastErr) {
|
||||
console.log("rethrowing");
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
const port = await getFreePort();
|
||||
console.log(new Date(), "starting proxy container...");
|
||||
const containerId = await dockerRun({
|
||||
image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.6.0",
|
||||
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`,
|
||||
],
|
||||
});
|
||||
console.log(new Date(), "started!");
|
||||
|
||||
const instance: ProxyInstance = { containerId, postgresId, port };
|
||||
instances.set(containerId, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
async function proxyStop(instance: ProxyInstance): Promise<void> {
|
||||
await dockerStop({
|
||||
containerId: instance.containerId,
|
||||
});
|
||||
await dockerStop({
|
||||
containerId: instance.postgresId,
|
||||
});
|
||||
|
||||
instances.delete(instance.containerId);
|
||||
|
||||
console.log(new Date(), "Stopped sliding sync proxy.");
|
||||
// cypress deliberately fails if you return 'undefined', so
|
||||
// return null to signal all is well, and we've handled the task.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export function slidingSyncProxyDocker(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
proxyStart,
|
||||
proxyStop,
|
||||
});
|
||||
|
||||
on("after:spec", async (spec) => {
|
||||
for (const instance of instances.values()) {
|
||||
console.warn(`Cleaning up proxy on port ${instance.port} after ${spec.name}`);
|
||||
await proxyStop(instance);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as crypto from "crypto";
|
||||
import * as fse from "fs-extra";
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
import { getFreePort } from "../utils/port";
|
||||
import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
|
||||
|
||||
// A cypress plugins to add command to start & stop synapses in
|
||||
// docker with preset templates.
|
||||
|
||||
interface SynapseConfig {
|
||||
configDir: string;
|
||||
registrationSecret: string;
|
||||
// Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage
|
||||
baseUrl: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface SynapseInstance extends SynapseConfig {
|
||||
synapseId: string;
|
||||
}
|
||||
|
||||
const synapses = new Map<string, SynapseInstance>();
|
||||
|
||||
function randB64Bytes(numBytes: number): string {
|
||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||
}
|
||||
|
||||
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||
const templateDir = path.join(__dirname, "templates", template);
|
||||
|
||||
const stats = await fse.stat(templateDir);
|
||||
if (!stats?.isDirectory) {
|
||||
throw new Error(`No such template: ${template}`);
|
||||
}
|
||||
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' });
|
||||
|
||||
const registrationSecret = randB64Bytes(16);
|
||||
const macaroonSecret = randB64Bytes(16);
|
||||
const formSecret = randB64Bytes(16);
|
||||
|
||||
const port = await getFreePort();
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
|
||||
// now copy homeserver.yaml, applying substitutions
|
||||
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
|
||||
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8");
|
||||
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
|
||||
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
|
||||
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
|
||||
hsYaml = hsYaml.replace(/{{PUBLIC_BASEURL}}/g, baseUrl);
|
||||
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
|
||||
|
||||
// now generate a signing key (we could use synapse's config generation for
|
||||
// this, or we could just do this...)
|
||||
// NB. This assumes the homeserver.yaml specifies the key in this location
|
||||
const signingKey = randB64Bytes(32);
|
||||
console.log(`Gen ${path.join(templateDir, "localhost.signing.key")}`);
|
||||
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
|
||||
|
||||
return {
|
||||
port,
|
||||
baseUrl,
|
||||
configDir: tempDir,
|
||||
registrationSecret,
|
||||
};
|
||||
}
|
||||
|
||||
// Start a synapse instance: the template must be the name of
|
||||
// one of the templates in the cypress/plugins/synapsedocker/templates
|
||||
// directory
|
||||
async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||
const synCfg = await cfgDirFromTemplate(template);
|
||||
|
||||
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||
|
||||
const synapseId = await dockerRun({
|
||||
image: "matrixdotorg/synapse:develop",
|
||||
containerName: `react-sdk-cypress-synapse`,
|
||||
params: [
|
||||
"--rm",
|
||||
"-v", `${synCfg.configDir}:/data`,
|
||||
"-p", `${synCfg.port}:8008/tcp`,
|
||||
],
|
||||
cmd: "run",
|
||||
});
|
||||
|
||||
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||
|
||||
// Await Synapse healthcheck
|
||||
await dockerExec({
|
||||
containerId: synapseId,
|
||||
params: [
|
||||
"curl",
|
||||
"--connect-timeout", "30",
|
||||
"--retry", "30",
|
||||
"--retry-delay", "1",
|
||||
"--retry-all-errors",
|
||||
"--silent",
|
||||
"http://localhost:8008/health",
|
||||
],
|
||||
});
|
||||
|
||||
const synapse: SynapseInstance = { synapseId, ...synCfg };
|
||||
synapses.set(synapseId, synapse);
|
||||
return synapse;
|
||||
}
|
||||
|
||||
async function synapseStop(id: string): Promise<void> {
|
||||
const synCfg = synapses.get(id);
|
||||
|
||||
if (!synCfg) throw new Error("Unknown synapse ID");
|
||||
|
||||
const synapseLogsPath = path.join("cypress", "synapselogs", id);
|
||||
await fse.ensureDir(synapseLogsPath);
|
||||
|
||||
await dockerLogs({
|
||||
containerId: id,
|
||||
stdoutFile: path.join(synapseLogsPath, "stdout.log"),
|
||||
stderrFile: path.join(synapseLogsPath, "stderr.log"),
|
||||
});
|
||||
|
||||
await dockerStop({
|
||||
containerId: id,
|
||||
});
|
||||
|
||||
await fse.remove(synCfg.configDir);
|
||||
|
||||
synapses.delete(id);
|
||||
|
||||
console.log(`Stopped synapse id ${id}.`);
|
||||
// cypress deliberately fails if you return 'undefined', so
|
||||
// return null to signal all is well, and we've handled the task.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export function synapseDocker(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
synapseStart,
|
||||
synapseStop,
|
||||
});
|
||||
|
||||
on("after:spec", async (spec) => {
|
||||
// Cleans up any remaining synapse instances after a spec run
|
||||
// This is on the theory that we should avoid re-using synapse
|
||||
// instances between spec runs: they should be cheap enough to
|
||||
// start that we can have a separate one for each spec run or even
|
||||
// test. If we accidentally re-use synapses, we could inadvertently
|
||||
// make our tests depend on each other.
|
||||
for (const synId of synapses.keys()) {
|
||||
console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`);
|
||||
await synapseStop(synId);
|
||||
}
|
||||
});
|
||||
|
||||
on("before:run", async () => {
|
||||
// tidy up old synapse log files before each run
|
||||
await fse.emptyDir(path.join("cypress", "synapselogs"));
|
||||
});
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
server_name: "localhost"
|
||||
pid_file: /data/homeserver.pid
|
||||
public_baseurl: "{{PUBLIC_BASEURL}}"
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
resources:
|
||||
- names: [client, federation, consent]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
|
||||
log_config: "/data/log.config"
|
||||
|
||||
rc_messages_per_second: 10000
|
||||
rc_message_burst_count: 10000
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
media_store_path: "/data/media_store"
|
||||
uploads_path: "/data/uploads"
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
disable_msisdn_registration: false
|
||||
registration_shared_secret: "{{REGISTRATION_SECRET}}"
|
||||
report_stats: false
|
||||
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
|
||||
form_secret: "{{FORM_SECRET}}"
|
||||
signing_key_path: "/data/localhost.signing.key"
|
||||
email:
|
||||
enable_notifs: false
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 25
|
||||
smtp_user: "exampleusername"
|
||||
smtp_pass: "examplepassword"
|
||||
require_transport_security: False
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: Matrix
|
||||
notif_template_html: notif_mail.html
|
||||
notif_template_text: notif_mail.txt
|
||||
notif_for_new_users: True
|
||||
client_base_url: "http://localhost/element"
|
||||
|
||||
user_consent:
|
||||
template_dir: /data/res/templates/privacy
|
||||
version: 1.0
|
||||
server_notice_content:
|
||||
msgtype: m.text
|
||||
body: >-
|
||||
To continue using this homeserver you must review and agree to the
|
||||
terms and conditions at %(consent_uri)s
|
||||
send_server_notice_to_guests: True
|
||||
block_events_error: >-
|
||||
To continue using this homeserver you must review and agree to the
|
||||
terms and conditions at %(consent_uri)s
|
||||
require_at_registration: true
|
||||
|
||||
server_notices:
|
||||
system_mxid_localpart: notices
|
||||
system_mxid_display_name: "Server Notices"
|
||||
system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ"
|
||||
room_name: "Server Notices"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
|
@ -1,23 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if has_consented %}
|
||||
<p>
|
||||
Thank you, you've already accepted the license.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Please accept the license!
|
||||
</p>
|
||||
<form method="post" action="consent">
|
||||
<input type="hidden" name="v" value="{{version}}"/>
|
||||
<input type="hidden" name="u" value="{{user}}"/>
|
||||
<input type="hidden" name="h" value="{{userhmac}}"/>
|
||||
<input type="submit" value="Sure thing!"/>
|
||||
</form>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,9 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Test Privacy policy</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Danke schon</p>
|
||||
</body>
|
||||
</html>
|
|
@ -1,76 +0,0 @@
|
|||
server_name: "localhost"
|
||||
pid_file: /data/homeserver.pid
|
||||
public_baseurl: "{{PUBLIC_BASEURL}}"
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ['::']
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
resources:
|
||||
- names: [client]
|
||||
compress: false
|
||||
|
||||
database:
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
|
||||
log_config: "/data/log.config"
|
||||
|
||||
rc_messages_per_second: 10000
|
||||
rc_message_burst_count: 10000
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
rc_joins:
|
||||
local:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
remote:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
rc_joins_per_room:
|
||||
per_second: 9999
|
||||
burst_count: 9999
|
||||
rc_3pid_validation:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
rc_invites:
|
||||
per_room:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
per_user:
|
||||
per_second: 1000
|
||||
burst_count: 1000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
media_store_path: "/data/media_store"
|
||||
uploads_path: "/data/uploads"
|
||||
enable_registration: true
|
||||
enable_registration_without_verification: true
|
||||
disable_msisdn_registration: false
|
||||
registration_shared_secret: "{{REGISTRATION_SECRET}}"
|
||||
report_stats: false
|
||||
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
|
||||
form_secret: "{{FORM_SECRET}}"
|
||||
signing_key_path: "/data/localhost.signing.key"
|
||||
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
||||
|
||||
ui_auth:
|
||||
session_timeout: "300s"
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import * as http from "http";
|
||||
import { AddressInfo } from "net";
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
|
||||
const servers: http.Server[] = [];
|
||||
|
||||
function serveHtmlFile(html: string): string {
|
||||
const server = http.createServer((req, res) => {
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/html",
|
||||
});
|
||||
res.end(html);
|
||||
});
|
||||
server.listen();
|
||||
servers.push(server);
|
||||
|
||||
return `http://localhost:${(server.address() as AddressInfo).port}/`;
|
||||
}
|
||||
|
||||
function stopWebServers(): null {
|
||||
for (const server of servers) {
|
||||
server.close();
|
||||
}
|
||||
servers.splice(0, servers.length); // clear
|
||||
|
||||
return null; // tell cypress we did the task successfully (doesn't allow undefined)
|
||||
}
|
||||
|
||||
export function webserver(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", { serveHtmlFile, stopWebServers });
|
||||
on("after:run", stopWebServers);
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Applies tweaks to the config read from config.json
|
||||
*/
|
||||
tweakConfig(tweaks: Record<string, any>): Chainable<AUTWindow>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("tweakConfig", (tweaks: Record<string, any>): Chainable<AUTWindow> => {
|
||||
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.
|
||||
win.mxReactSdkConfig[k] = v;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import "cypress-axe";
|
||||
import * as axe from "axe-core";
|
||||
import { Options } from "cypress-axe";
|
||||
|
||||
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`,
|
||||
);
|
||||
|
||||
// pluck specific keys to keep the table readable
|
||||
const violationData = violations.map(({ id, impact, description, nodes }) => ({
|
||||
id,
|
||||
impact,
|
||||
description,
|
||||
nodes: nodes.length,
|
||||
}));
|
||||
|
||||
cy.task('table', violationData);
|
||||
}
|
||||
|
||||
Cypress.Commands.overwrite("checkA11y", (
|
||||
originalFn: Chainable["checkA11y"],
|
||||
context?: string | Node | axe.ContextObject | undefined,
|
||||
options: Options = {},
|
||||
violationCallback?: ((violations: axe.Result[]) => void) | undefined,
|
||||
skipFailures?: boolean,
|
||||
): void => {
|
||||
return originalFn(context, {
|
||||
...options,
|
||||
rules: {
|
||||
// Disable contrast checking for now as we have too many issues with it
|
||||
'color-contrast': {
|
||||
enabled: false,
|
||||
},
|
||||
...options.rules,
|
||||
},
|
||||
}, violationCallback ?? terminalLog, skipFailures);
|
||||
});
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
interface CreateBotOpts {
|
||||
/**
|
||||
* Whether the bot should automatically accept all invites.
|
||||
*/
|
||||
autoAcceptInvites?: boolean;
|
||||
/**
|
||||
* The display name to give to that bot user
|
||||
*/
|
||||
displayName?: string;
|
||||
/**
|
||||
* Whether or not to start the syncing client.
|
||||
*/
|
||||
startClient?: boolean;
|
||||
}
|
||||
|
||||
const defaultCreateBotOptions = {
|
||||
autoAcceptInvites: true,
|
||||
startClient: true,
|
||||
} as CreateBotOpts;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Returns a new Bot instance
|
||||
* @param synapse the instance on which to register the bot user
|
||||
* @param opts create bot options
|
||||
*/
|
||||
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient>;
|
||||
/**
|
||||
* Let a bot join a room
|
||||
* @param cli The bot's MatrixClient
|
||||
* @param roomId ID of the room to join
|
||||
*/
|
||||
botJoinRoom(cli: MatrixClient, roomId: string): Chainable<Room>;
|
||||
/**
|
||||
* Let a bot join a room by name
|
||||
* @param cli The bot's MatrixClient
|
||||
* @param roomName Name of the room to join
|
||||
*/
|
||||
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable<Room>;
|
||||
/**
|
||||
* Send a message as a bot into a room
|
||||
* @param cli The bot's MatrixClient
|
||||
* @param roomId ID of the room to join
|
||||
* @param message the message body to send
|
||||
*/
|
||||
botSendMessage(cli: MatrixClient, roomId: string, message: string): Chainable<ISendEventResponse>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient> => {
|
||||
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.window({ log: false }).then(win => {
|
||||
const cli = new win.matrixcs.MatrixClient({
|
||||
baseUrl: synapse.baseUrl,
|
||||
userId: credentials.userId,
|
||||
deviceId: credentials.deviceId,
|
||||
accessToken: credentials.accessToken,
|
||||
store: new win.matrixcs.MemoryStore(),
|
||||
scheduler: new win.matrixcs.MatrixScheduler(),
|
||||
cryptoStore: new win.matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
if (opts.autoAcceptInvites) {
|
||||
cli.on(win.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||
if (member.membership === "invite" && member.userId === cli.getUserId()) {
|
||||
cli.joinRoom(member.roomId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!opts.startClient) {
|
||||
return cy.wrap(cli);
|
||||
}
|
||||
|
||||
return cy.wrap(
|
||||
cli.initCrypto()
|
||||
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
||||
.then(() => cli.startClient())
|
||||
.then(() => cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
}))
|
||||
.then(() => cli),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("botJoinRoom", (cli: MatrixClient, roomId: string): Chainable<Room> => {
|
||||
return cy.wrap(cli.joinRoom(roomId));
|
||||
});
|
||||
|
||||
Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string): Chainable<Room> => {
|
||||
const room = cli.getRooms().find((r) => r.getDefaultRoomName(cli.getUserId()) === roomName);
|
||||
|
||||
if (room) {
|
||||
return cy.botJoinRoom(cli, room.roomId);
|
||||
}
|
||||
|
||||
return cy.wrap(Promise.reject(`Bot room join failed. Cannot find room '${roomName}'`));
|
||||
});
|
||||
|
||||
Cypress.Commands.add("botSendMessage", (
|
||||
cli: MatrixClient,
|
||||
roomId: string,
|
||||
message: string,
|
||||
): Chainable<ISendEventResponse> => {
|
||||
return cy.wrap(cli.sendMessage(roomId, {
|
||||
msgtype: "m.text",
|
||||
body: message,
|
||||
}), { log: false });
|
||||
});
|
|
@ -1,227 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { FileType, Upload, UploadOpts } from "matrix-js-sdk/src/http-api";
|
||||
import type { ICreateRoomOpts, ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Returns the MatrixClient from the MatrixClientPeg
|
||||
*/
|
||||
getClient(): Chainable<MatrixClient | undefined>;
|
||||
/**
|
||||
* Create a room with given options.
|
||||
* @param options the options to apply when creating the room
|
||||
* @return the ID of the newly created room
|
||||
*/
|
||||
createRoom(options: ICreateRoomOpts): Chainable<string>;
|
||||
/**
|
||||
* Create a space with given options.
|
||||
* @param options the options to apply when creating the space
|
||||
* @return the ID of the newly created space (room)
|
||||
*/
|
||||
createSpace(options: ICreateRoomOpts): Chainable<string>;
|
||||
/**
|
||||
* Invites the given user to the given room.
|
||||
* @param roomId the id of the room to invite to
|
||||
* @param userId the id of the user to invite
|
||||
*/
|
||||
inviteUser(roomId: string, userId: string): Chainable<{}>;
|
||||
/**
|
||||
* Sets account data for the user.
|
||||
* @param type The type of account data.
|
||||
* @param data The data to store.
|
||||
*/
|
||||
setAccountData(type: string, data: object): Chainable<{}>;
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} threadId
|
||||
* @param {string} eventType
|
||||
* @param {Object} content
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
sendEvent(
|
||||
roomId: string,
|
||||
threadId: string | null,
|
||||
eventType: string,
|
||||
content: IContent
|
||||
): Chainable<ISendEventResponse>;
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: {} an empty object.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
setDisplayName(name: string): Chainable<{}>;
|
||||
/**
|
||||
* @param {string} url
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: {} an empty object.
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
setAvatarUrl(url: string): Chainable<{}>;
|
||||
/**
|
||||
* Upload a file to the media repository on the homeserver.
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* 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"]>>;
|
||||
/**
|
||||
* Turn an MXC URL into an HTTP one. <strong>This method is experimental and
|
||||
* may change.</strong>
|
||||
* @param {string} mxcUrl The MXC URL
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return null for such URLs.
|
||||
* @return {?string} the avatar URL or null.
|
||||
*/
|
||||
mxcUrlToHttp(
|
||||
mxcUrl: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
resizeMethod?: string,
|
||||
allowDirectLinks?: boolean,
|
||||
): string | null;
|
||||
/**
|
||||
* Gets the list of DMs with a given user
|
||||
* @param userId The ID of the user
|
||||
* @return the list of DMs with that user
|
||||
*/
|
||||
getDmRooms(userId: string): Chainable<string[]>;
|
||||
/**
|
||||
* Boostraps cross-signing.
|
||||
*/
|
||||
bootstrapCrossSigning(): Chainable<void>;
|
||||
/**
|
||||
* Joins the given room by alias or ID
|
||||
* @param roomIdOrAlias the id or alias of the room to join
|
||||
*/
|
||||
joinRoom(roomIdOrAlias: string): Chainable<Room>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getClient", (): Chainable<MatrixClient | undefined> => {
|
||||
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] ?? []);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("createRoom", (options: ICreateRoomOpts): Chainable<string> => {
|
||||
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 => {
|
||||
const onRoom = (room: Room) => {
|
||||
if (room.roomId === roomId) {
|
||||
cli.off(win.matrixcs.ClientEvent.Room, onRoom);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
cli.on(win.matrixcs.ClientEvent.Room, onRoom);
|
||||
});
|
||||
}
|
||||
|
||||
return roomId;
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("createSpace", (options: ICreateRoomOpts): Chainable<string> => {
|
||||
return cy.createRoom({
|
||||
...options,
|
||||
creation_content: {
|
||||
"type": "m.space",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("inviteUser", (roomId: string, userId: string): Chainable<{}> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
return cli.invite(roomId, userId);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("setAccountData", (type: string, data: object): Chainable<{}> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
return cli.setAccountData(type, data);
|
||||
});
|
||||
});
|
||||
|
||||
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) => {
|
||||
return cli.setDisplayName(name);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("uploadContent", (file: FileType, opts?: UploadOpts): Chainable<Awaited<Upload["promise"]>> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
return cli.uploadContent(file, opts);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
|
||||
return cy.getClient().then(async (cli: MatrixClient) => {
|
||||
return cli.setAvatarUrl(url);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("bootstrapCrossSigning", () => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => { await func({}); },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => {
|
||||
return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
// Mock the clipboard, as only Electron gives the app permission to the clipboard API by default
|
||||
// Virtual clipboard
|
||||
let copyText: string;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Mock the clipboard on the current window, ready for calling `getClipboardText`.
|
||||
* Irreversible, refresh the window to restore mock.
|
||||
*/
|
||||
mockClipboard(): Chainable<AUTWindow>;
|
||||
/**
|
||||
* Read text from the mocked clipboard.
|
||||
* @return {string} the clipboard text
|
||||
*/
|
||||
getClipboardText(): Chainable<string>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("mockClipboard", () => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
win.navigator.clipboard.writeText = (text) => {
|
||||
copyText = text;
|
||||
return Promise.resolve();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("getClipboardText", (): Chainable<string> => {
|
||||
return cy.wrap(copyText);
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
// Get the composer element
|
||||
// selects main timeline composer by default
|
||||
// set `isRightPanel` true to select right panel composer
|
||||
getComposer(isRightPanel?: boolean): Chainable<JQuery>;
|
||||
// Open the message composer kebab menu
|
||||
openMessageComposerOptions(isRightPanel?: boolean): Chainable<JQuery>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getComposer", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||
const panelClass = isRightPanel ? '.mx_RightPanel' : '.mx_RoomView_body';
|
||||
return cy.get(`${panelClass} .mx_MessageComposer`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openMessageComposerOptions", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||
cy.getComposer(isRightPanel).within(() => {
|
||||
cy.get('[aria-label="More options"]').click();
|
||||
});
|
||||
return cy.get('.mx_MessageComposer_Menu');
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Gets you into the `body` of the selectable iframe. Best to call
|
||||
* `within({}, () => { ... })` on the returned Chainable to access
|
||||
* further elements.
|
||||
* @param selector The jquery selector to find the frame with.
|
||||
*/
|
||||
accessIframe(selector: string): Chainable<JQuery<HTMLElement>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inspired by https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
|
||||
Cypress.Commands.add("accessIframe", (selector: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(selector)
|
||||
.its("0.contentDocument.body").should("not.be.empty")
|
||||
// Cypress loses types in the mess of wrapping, so force cast
|
||||
.then(cy.wrap) as Chainable<JQuery<HTMLElement>>;
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Enables a labs feature for an element session.
|
||||
* Has to be called before the session is initialized
|
||||
* @param feature labsFeature to enable (e.g. "feature_spotlight")
|
||||
*/
|
||||
enableLabsFeature(feature: string): Chainable<null>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("enableLabsFeature", (feature: string): Chainable<null> => {
|
||||
return cy.window({ log: false }).then(win => {
|
||||
win.localStorage.setItem(`mx_labs_feature_${feature}`, "true");
|
||||
}).then(() => null);
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,134 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
|
||||
export interface UserCredentials {
|
||||
accessToken: string;
|
||||
username: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
password: string;
|
||||
homeServer: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Generates a test user and instantiates an Element session with that user.
|
||||
* @param synapse the synapse returned by startSynapse
|
||||
* @param displayName the displayName to give the test user
|
||||
* @param prelaunchFn optional function to run before the app is visited
|
||||
*/
|
||||
initTestUser(
|
||||
synapse: SynapseInstance,
|
||||
displayName: string,
|
||||
prelaunchFn?: () => void,
|
||||
): Chainable<UserCredentials>;
|
||||
/**
|
||||
* Logs into synapse with the given username/password
|
||||
* @param synapse the synapse returned by startSynapse
|
||||
* @param username login username
|
||||
* @param password login password
|
||||
*/
|
||||
loginUser(
|
||||
synapse: SynapseInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
): Chainable<UserCredentials>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add("loginUser", (synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
|
||||
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
|
||||
return cy.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
device_id: string;
|
||||
home_server: string;
|
||||
}>({
|
||||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
"user": username,
|
||||
},
|
||||
"password": password,
|
||||
},
|
||||
}).then(response => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
homeServer: response.body.home_server,
|
||||
}));
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: string, prelaunchFn?: () => void): Chainable<UserCredentials> => {
|
||||
// XXX: work around Cypress not clearing IDB between tests
|
||||
cy.window({ log: false }).then(win => {
|
||||
win.indexedDB.databases()?.then(databases => {
|
||||
databases.forEach(database => {
|
||||
win.indexedDB.deleteDatabase(database.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy.registerUser(synapse, username, password, displayName).then(() => {
|
||||
return cy.loginUser(synapse, username, password);
|
||||
}).then(response => {
|
||||
cy.window({ log: false }).then(win => {
|
||||
// Seed the localStorage with the required credentials
|
||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
||||
win.localStorage.setItem("mx_user_id", response.userId);
|
||||
win.localStorage.setItem("mx_access_token", response.accessToken);
|
||||
win.localStorage.setItem("mx_device_id", response.deviceId);
|
||||
win.localStorage.setItem("mx_is_guest", "false");
|
||||
win.localStorage.setItem("mx_has_pickle_key", "false");
|
||||
win.localStorage.setItem("mx_has_access_token", "true");
|
||||
|
||||
// Ensure the language is set to a consistent value
|
||||
win.localStorage.setItem("mx_local_settings", '{"language":"en"}');
|
||||
});
|
||||
|
||||
prelaunchFn?.();
|
||||
|
||||
return cy.visit("/").then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 30000 });
|
||||
}).then(() => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.accessToken,
|
||||
userId: response.userId,
|
||||
deviceId: response.deviceId,
|
||||
homeServer: response.homeServer,
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
// Intercept all /_matrix/ networking requests for the logged in user and fail them
|
||||
goOffline(): void;
|
||||
// Remove intercept on all /_matrix/ networking requests
|
||||
goOnline(): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We manage intercepting Matrix APIs here, as fully disabling networking will disconnect
|
||||
// the browser under test from the Cypress runner, so can cause issues.
|
||||
|
||||
Cypress.Commands.add("goOffline", (): void => {
|
||||
cy.log("Going offline");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
}, req => {
|
||||
req.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("goOnline", (): void => {
|
||||
cy.log("Going online");
|
||||
cy.window({ log: false }).then(win => {
|
||||
cy.intercept("**/_matrix/**", {
|
||||
headers: {
|
||||
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||
},
|
||||
}, req => {
|
||||
req.continue();
|
||||
});
|
||||
win.dispatchEvent(new Event("online"));
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
import { SnapshotOptions as PercySnapshotOptions } from '@percy/core';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface SnapshotOptions extends PercySnapshotOptions {
|
||||
domTransformation?: (documentClone: Document) => void;
|
||||
}
|
||||
|
||||
interface Chainable {
|
||||
percySnapshotElement(name?: string, options?: SnapshotOptions);
|
||||
}
|
||||
|
||||
interface Chainable {
|
||||
/**
|
||||
* Takes a Percy snapshot of a given element
|
||||
*/
|
||||
percySnapshotElement(name: string, options: SnapshotOptions): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("percySnapshotElement", { prevSubject: "element" }, (subject, name, options) => {
|
||||
cy.percySnapshot(name, {
|
||||
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;
|
||||
|
||||
return documentClone;
|
||||
}
|
||||
|
||||
export { };
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
import { ProxyInstance } from '../plugins/sliding-sync';
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Start a sliding sync proxy instance.
|
||||
* @param synapse the synapse instance returned by startSynapse
|
||||
*/
|
||||
startProxy(synapse: SynapseInstance): Chainable<ProxyInstance>;
|
||||
|
||||
/**
|
||||
* Custom command wrapping task:proxyStop whilst preventing uncaught exceptions
|
||||
* for if Docker stopping races with the app's background sync loop.
|
||||
* @param proxy the proxy instance returned by startProxy
|
||||
*/
|
||||
stopProxy(proxy: ProxyInstance): Chainable<AUTWindow>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startProxy(synapse: SynapseInstance): Chainable<ProxyInstance> {
|
||||
return cy.task<ProxyInstance>("proxyStart", synapse);
|
||||
}
|
||||
|
||||
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';
|
||||
cy.task("proxyStop", proxy);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add("startProxy", startProxy);
|
||||
Cypress.Commands.add("stopProxy", stopProxy);
|
|
@ -1,177 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import type { SettingLevel } from "../../src/settings/SettingLevel";
|
||||
import type SettingsStore from "../../src/settings/SettingsStore";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Returns the SettingsStore
|
||||
*/
|
||||
getSettingsStore(): Chainable<typeof SettingsStore | undefined>; // XXX: Importing SettingsStore causes a bunch of type lint errors
|
||||
/**
|
||||
* Open the top left user menu, returning a handle to the resulting context menu.
|
||||
*/
|
||||
openUserMenu(): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Open user settings (via user menu), returning a handle to the resulting dialog.
|
||||
* @param tab the name of the tab to switch to after opening, optional.
|
||||
*/
|
||||
openUserSettings(tab?: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Open room settings (via room header menu), returning a handle to the resulting dialog.
|
||||
* @param tab the name of the tab to switch to after opening, optional.
|
||||
*/
|
||||
openRoomSettings(tab?: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Switch settings tab to the one by the given name, ideally call this in the context of the dialog.
|
||||
* @param tab the name of the tab to switch to.
|
||||
*/
|
||||
switchTab(tab: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Close dialog, ideally call this in the context of the dialog.
|
||||
*/
|
||||
closeDialog(): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Join the given beta, the `Labs` tab must already be opened,
|
||||
* ideally call this in the context of the dialog.
|
||||
* @param name the name of the beta to join.
|
||||
*/
|
||||
joinBeta(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Leave the given beta, the `Labs` tab must already be opened,
|
||||
* ideally call this in the context of the dialog.
|
||||
* @param name the name of the beta to leave.
|
||||
*/
|
||||
leaveBeta(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Sets the value for a setting. The room ID is optional if the
|
||||
* setting is not being set for a particular room, otherwise it
|
||||
* should be supplied. The value may be null to indicate that the
|
||||
* level should no longer have an override.
|
||||
* @param {string} settingName The name of the setting to change.
|
||||
* @param {String} roomId The room ID to change the value in, may be
|
||||
* null.
|
||||
* @param {SettingLevel} level The level to change the value at.
|
||||
* @param {*} value The new value of the setting, may be null.
|
||||
* @return {Promise} Resolves when the setting has been changed.
|
||||
*/
|
||||
setSettingValue(settingName: string, roomId: string, level: SettingLevel, value: any): Chainable<void>;
|
||||
|
||||
/**
|
||||
* Gets the value of a setting. The room ID is optional if the
|
||||
* setting is not to be applied to any particular room, otherwise it
|
||||
* should be supplied.
|
||||
* @param {string} settingName The name of the setting to read the
|
||||
* value of.
|
||||
* @param {String} roomId The room ID to read the setting value in,
|
||||
* may be null.
|
||||
* @param {boolean} excludeDefault True to disable using the default
|
||||
* value.
|
||||
* @return {*} The value, or null if not found
|
||||
*/
|
||||
getSettingValue<T>(settingName: string, roomId?: string, excludeDefault?: boolean): Chainable<T>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getSettingsStore", (): Chainable<typeof SettingsStore> => {
|
||||
return cy.window({ log: false }).then(win => win.mxSettingsStore);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("setSettingValue", (
|
||||
name: string,
|
||||
roomId: string,
|
||||
level: SettingLevel,
|
||||
value: any,
|
||||
): Chainable<void> => {
|
||||
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
|
||||
return cy.wrap(store.setValue(name, roomId, level, value));
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add("getSettingValue", <T = any>(name: string, roomId?: string, excludeDefault?: boolean): Chainable<T> => {
|
||||
return cy.getSettingsStore().then((store: typeof SettingsStore) => {
|
||||
return store.getValue(name, roomId, excludeDefault);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openUserMenu", (): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get('[aria-label="User menu"]').click();
|
||||
return cy.get(".mx_ContextualMenu");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openUserSettings", (tab?: string): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.openUserMenu().within(() => {
|
||||
cy.get('[aria-label="All settings"]').click();
|
||||
});
|
||||
return cy.get(".mx_UserSettingsDialog").within(() => {
|
||||
if (tab) {
|
||||
cy.switchTab(tab);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openRoomSettings", (tab?: string): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.get(".mx_RoomHeader_name").click();
|
||||
cy.get(".mx_RoomTile_contextMenu").within(() => {
|
||||
cy.get('[aria-label="Settings"]').click();
|
||||
});
|
||||
return cy.get(".mx_RoomSettingsDialog").within(() => {
|
||||
if (tab) {
|
||||
cy.switchTab(tab);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("switchTab", (tab: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(".mx_TabbedView_tabLabels").within(() => {
|
||||
cy.contains(".mx_TabbedView_tabLabel", tab).click();
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("closeDialog", (): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get('[aria-label="Close dialog"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("joinBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
|
||||
return cy.get(".mx_BetaCard_buttons").contains("Join the beta").click();
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("leaveBeta", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.contains(".mx_BetaCard_title", name).closest(".mx_BetaCard").within(() => {
|
||||
return cy.get(".mx_BetaCard_buttons").contains("Leave the beta").click();
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,122 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Start a synapse instance with a given config template.
|
||||
* @param template path to template within cypress/plugins/synapsedocker/template/ directory.
|
||||
*/
|
||||
startSynapse(template: string): Chainable<SynapseInstance>;
|
||||
|
||||
/**
|
||||
* Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
|
||||
* for if Synapse stopping races with the app's background sync loop.
|
||||
* @param synapse the synapse instance returned by startSynapse
|
||||
*/
|
||||
stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow>;
|
||||
|
||||
/**
|
||||
* Register a user on the given Synapse using the shared registration secret.
|
||||
* @param synapse the synapse instance returned by startSynapse
|
||||
* @param username the username of the user to register
|
||||
* @param password the password of the user to register
|
||||
* @param displayName optional display name to set on the newly registered user
|
||||
*/
|
||||
registerUser(
|
||||
synapse: SynapseInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
): Chainable<Credentials>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startSynapse(template: string): Chainable<SynapseInstance> {
|
||||
return cy.task<SynapseInstance>("synapseStart", template);
|
||||
}
|
||||
|
||||
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';
|
||||
cy.task("synapseStop", synapse.synapseId);
|
||||
});
|
||||
}
|
||||
|
||||
interface Credentials {
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
homeServer: string;
|
||||
}
|
||||
|
||||
function registerUser(
|
||||
synapse: SynapseInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
): Chainable<Credentials> {
|
||||
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
|
||||
return cy.then(() => {
|
||||
// get a nonce
|
||||
return cy.request<{ nonce: string }>({ url });
|
||||
}).then(response => {
|
||||
const { nonce } = response.body;
|
||||
const mac = crypto.createHmac('sha1', synapse.registrationSecret).update(
|
||||
`${nonce}\0${username}\0${password}\0notadmin`,
|
||||
).digest('hex');
|
||||
|
||||
return cy.request<{
|
||||
access_token: string;
|
||||
user_id: string;
|
||||
home_server: string;
|
||||
device_id: string;
|
||||
}>({
|
||||
url,
|
||||
method: "POST",
|
||||
body: {
|
||||
nonce,
|
||||
username,
|
||||
password,
|
||||
mac,
|
||||
admin: false,
|
||||
displayname: displayName,
|
||||
},
|
||||
});
|
||||
}).then(response => ({
|
||||
homeServer: response.body.home_server,
|
||||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
}));
|
||||
}
|
||||
|
||||
Cypress.Commands.add("startSynapse", startSynapse);
|
||||
Cypress.Commands.add("stopSynapse", stopSynapse);
|
||||
Cypress.Commands.add("registerUser", registerUser);
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
// Scroll to the top of the timeline
|
||||
scrollToTop(): void;
|
||||
// Find the event tile matching the given sender & body
|
||||
findEventTile(sender: string, body: string): Chainable<JQuery>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
sender: string;
|
||||
body: string;
|
||||
encrypted: boolean;
|
||||
continuation: boolean;
|
||||
}
|
||||
|
||||
Cypress.Commands.add("scrollToTop", (): void => {
|
||||
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
|
||||
if (ref.scrollTop() > 0) {
|
||||
return cy.scrollToTop();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 => {
|
||||
let latestSender: string;
|
||||
for (let i = 0; i < refs.length; i++) {
|
||||
const ref = refs.eq(i);
|
||||
const displayName = ref.find(".mx_DisambiguatedProfile_displayName");
|
||||
if (displayName) {
|
||||
latestSender = displayName.text();
|
||||
}
|
||||
|
||||
if (latestSender === sender && ref.find(".mx_EventTile_body").text() === body) {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
// @see https://github.com/cypress-io/cypress/issues/915#issuecomment-475862672
|
||||
// Modified due to changes to `cy.queue` https://github.com/cypress-io/cypress/pull/17448
|
||||
// Note: this DOES NOT run Promises in parallel like `Promise.all` due to the nature
|
||||
// of Cypress promise-like objects and command queue. This only makes it convenient to use the same
|
||||
// API but runs the commands sequentially.
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
type ChainableValue<T> = T extends Cypress.Chainable<infer V> ? V : T;
|
||||
|
||||
interface cy {
|
||||
all<T extends Cypress.Chainable[] | []>(
|
||||
commands: T
|
||||
): Cypress.Chainable<{ [P in keyof T]: ChainableValue<T[P]> }>;
|
||||
queue: any;
|
||||
}
|
||||
|
||||
interface Chainable {
|
||||
chainerId: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chainStart = Symbol("chainStart");
|
||||
|
||||
/**
|
||||
* @description Returns a single Chainable that resolves when all of the Chainables pass.
|
||||
* @param {Cypress.Chainable[]} commands - List of Cypress.Chainable to resolve.
|
||||
* @returns {Cypress.Chainable} Cypress when all Chainables are resolved.
|
||||
*/
|
||||
cy.all = function all(commands): Cypress.Chainable {
|
||||
const chain = cy.wrap(null, { log: false });
|
||||
const stopCommand = Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: chain.chainerId },
|
||||
});
|
||||
const startCommand = Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: commands[0].chainerId },
|
||||
});
|
||||
const p = chain.then(() => {
|
||||
return cy.wrap(
|
||||
// @see https://lodash.com/docs/4.17.15#lodash
|
||||
Cypress._(commands)
|
||||
.map(cmd => {
|
||||
return cmd[chainStart]
|
||||
? cmd[chainStart].attributes
|
||||
: Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: cmd.chainerId },
|
||||
}).attributes;
|
||||
})
|
||||
.concat(stopCommand.attributes)
|
||||
.slice(1)
|
||||
.map(cmd => {
|
||||
return cmd.prev.get("subject");
|
||||
})
|
||||
.value(),
|
||||
);
|
||||
});
|
||||
p[chainStart] = startCommand;
|
||||
return p;
|
||||
};
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Opens the given room by name. The room must be visible in the
|
||||
* room list.
|
||||
* @param name The room name to find and click on/open.
|
||||
*/
|
||||
viewRoomByName(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Returns the space panel space button based on a name. The space
|
||||
* must be visible in the space panel
|
||||
* @param name The space name to find
|
||||
*/
|
||||
getSpacePanelButton(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Opens the given space home by name. The space must be visible in
|
||||
* the space list.
|
||||
* @param name The space name to find and click on/open.
|
||||
*/
|
||||
viewSpaceHomeByName(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Opens the given space by name. The space must be visible in the
|
||||
* space list.
|
||||
* @param name The space name to find and click on/open.
|
||||
*/
|
||||
viewSpaceByName(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("viewRoomByName", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(`.mx_RoomTile[aria-label="${name}"]`).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("getSpacePanelButton", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.get(`.mx_SpaceButton[aria-label="${name}"]`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("viewSpaceByName", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.getSpacePanelButton(name).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("viewSpaceHomeByName", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
return cy.getSpacePanelButton(name).dblclick();
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Starts a web server which serves the given HTML.
|
||||
* @param html The HTML to serve
|
||||
* @returns The URL at which the HTML can be accessed.
|
||||
*/
|
||||
serveHtmlFile(html: string): Chainable<string>;
|
||||
|
||||
/**
|
||||
* Stops all running web servers.
|
||||
*/
|
||||
stopWebServers(): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function serveHtmlFile(html: string): Chainable<string> {
|
||||
return cy.task<string>("serveHtmlFile", html);
|
||||
}
|
||||
|
||||
function stopWebServers(): Chainable<void> {
|
||||
return cy.task("stopWebServers");
|
||||
}
|
||||
|
||||
Cypress.Commands.add("serveHtmlFile", serveHtmlFile);
|
||||
Cypress.Commands.add("stopWebServers", stopWebServers);
|
||||
|
||||
// Needed to make this file a module
|
||||
export { };
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-axe",
|
||||
"@percy/cypress"
|
||||
],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
23
docs/SUMMARY.md
Normal file
23
docs/SUMMARY.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Summary
|
||||
|
||||
- [Introduction](../README.md)
|
||||
|
||||
# Customisation
|
||||
|
||||
- [Skinning](skinning.md)
|
||||
|
||||
# Deep dive
|
||||
|
||||
- [Cider editor](ciderEditor.md)
|
||||
- [Iconography](icons.md)
|
||||
- [Jitsi](jitsi.md)
|
||||
- [Local echo](local-echo-dev.md)
|
||||
- [Media](media-handling.md)
|
||||
- [Room List Store](room-list-store.md)
|
||||
- [Scrolling](scrolling.md)
|
||||
- [Usercontent](usercontent.md)
|
||||
- [Widget layouts](widget-layouts.md)
|
||||
|
||||
# Testing
|
||||
|
||||
- [Playwright end to end](playwright.md)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue