mirror of https://github.com/vector-im/riot-web
Remove Cypress & Playwright in their entirety (#12145)
parent
0b6d2f923d
commit
5983528a8d
24
.eslintrc.js
24
.eslintrc.js
|
@ -169,7 +169,7 @@ module.exports = {
|
|||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts", "playwright/**/*.ts"],
|
||||
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": [
|
||||
|
@ -233,14 +233,14 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
{
|
||||
files: ["test/**/*.{ts,tsx}", "cypress/**/*.ts", "playwright/**/*.ts"],
|
||||
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/Cypress specific
|
||||
// Jest/Playwright specific
|
||||
|
||||
// Disabled tests are a reality for now but as soon as all of the xits are
|
||||
// eliminated, we should enforce this.
|
||||
|
@ -255,29 +255,11 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["cypress/**/*.ts"],
|
||||
parserOptions: {
|
||||
project: ["./cypress/tsconfig.json"],
|
||||
},
|
||||
rules: {
|
||||
// Cypress "promises" work differently - disable some related rules
|
||||
"jest/valid-expect": "off",
|
||||
"jest/valid-expect-in-promise": "off",
|
||||
"jest/no-done-callback": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["playwright/**/*.ts"],
|
||||
parserOptions: {
|
||||
project: ["./playwright/tsconfig.json"],
|
||||
},
|
||||
rules: {
|
||||
// Cypress "promises" work differently - disable some related rules
|
||||
"jest/valid-expect": "off",
|
||||
"jest/valid-expect-in-promise": "off",
|
||||
"jest/no-done-callback": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
|
|
|
@ -1,231 +0,0 @@
|
|||
# Triggers after the layered build has finished, taking the artifact and running cypress on it
|
||||
#
|
||||
# Also called by a workflow in matrix-js-sdk.
|
||||
#
|
||||
name: Cypress End to End Tests
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Element Web - Build"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
# support calls 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."
|
||||
secrets:
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS_RUST:
|
||||
required: true
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS_LEGACY:
|
||||
required: true
|
||||
TCMS_USERNAME:
|
||||
required: true
|
||||
TCMS_PASSWORD:
|
||||
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:
|
||||
actions: read
|
||||
issues: read
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
outputs:
|
||||
uuid: ${{ steps.uuid.outputs.value }}
|
||||
pr_id: ${{ steps.prdetails.outputs.pr_id }}
|
||||
percy_enable: ${{ steps.percy.outputs.value || '0' }}
|
||||
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@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: pending
|
||||
context: ${{ github.workflow }} / cypress
|
||||
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' || github.event.workflow_run.event == 'merge_group'
|
||||
uses: matrix-org/pr-details-action@v1.3
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
branch: ${{ github.event.workflow_run.head_branch }}
|
||||
|
||||
# Percy is disabled while we're figuring out https://github.com/vector-im/wat-internal/issues/36
|
||||
# and https://github.com/vector-im/wat-internal/issues/56. We're hoping to turn it back on or switch
|
||||
# to an alternative in the future.
|
||||
# # Only run Percy when it is demanded or we are running the daily build
|
||||
# - name: Enable Percy
|
||||
# id: percy
|
||||
# if: |
|
||||
# github.event.workflow_run.event == 'schedule' ||
|
||||
# (
|
||||
# github.event.workflow_run.event == 'merge_group' &&
|
||||
# contains(fromJSON(steps.prdetails.outputs.data).labels.*.name, 'X-Needs-Percy')
|
||||
# )
|
||||
# run: echo "value=1" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate unique ID 💎
|
||||
id: uuid
|
||||
run: echo "value=sha-$GITHUB_SHA-time-$(date +"%s")" >> $GITHUB_OUTPUT
|
||||
|
||||
tests:
|
||||
name: "Run Tests (${{ matrix.crypto }} crypto)"
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment: Cypress
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Run tests using both crypto stacks
|
||||
crypto: [legacy, rust]
|
||||
ci_node_total: [2]
|
||||
ci_node_index: [0, 1]
|
||||
steps:
|
||||
# The version of chrome shipped by default may not be consistent across runners
|
||||
# so we explicitly use a specific version of chrome here.
|
||||
- uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # v1
|
||||
- run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV
|
||||
|
||||
# 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@f29d1b6a8930683e80acedfbe6baa2930cd646b4 # v2
|
||||
with:
|
||||
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 cypress 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 }}
|
||||
|
||||
# Enable rust crypto if the calling workflow requests it
|
||||
- name: Enable rust crypto
|
||||
if: matrix.crypto == 'rust'
|
||||
run: |
|
||||
echo "CYPRESS_RUST_CRYPTO=1" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run Cypress tests via knapsack pro
|
||||
uses: cypress-io/github-action@ebe8b24c4428922d0f793a5c4c96853a633180e3 # v6.6.0
|
||||
with:
|
||||
working-directory: matrix-react-sdk
|
||||
headed: true
|
||||
start: npx serve -p 8080 -L ../webapp
|
||||
wait-on: "http://localhost:8080"
|
||||
record: false
|
||||
parallel: false
|
||||
# The built-in Electron runner seems to grind to a halt trying to run the tests, so use chrome.
|
||||
command: yarn percy exec --parallel -- npx knapsack-pro-cypress --config trashAssetsBeforeRuns=false --browser ${{ env.BROWSER_PATH }}
|
||||
env:
|
||||
# Knapsack token and config
|
||||
KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS: ${{ matrix.crypto == 'rust' && secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS_RUST || secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS_LEGACY }}
|
||||
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
|
||||
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
|
||||
KNAPSACK_PRO_TEST_FILE_PATTERN: cypress/e2e/**/*.spec.ts
|
||||
KNAPSACK_PRO_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
KNAPSACK_PRO_COMMIT_HASH: ${{ github.event.workflow_run.head_sha }}
|
||||
|
||||
# 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 }}
|
||||
|
||||
# pass the Percy token as an environment variable
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
|
||||
# only run percy on legacy crypto (for now)
|
||||
PERCY_ENABLE: ${{ matrix.crypto == 'legacy' && needs.prepare.outputs.percy_enable || 0 }}
|
||||
PERCY_BROWSER_EXECUTABLE: ${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
# 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_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
# We manually finalize the build in the report stage
|
||||
PERCY_PARALLEL_TOTAL: -1
|
||||
|
||||
- name: 📤 Upload results artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-results-${{ matrix.crypto }}-crypto
|
||||
path: |
|
||||
matrix-react-sdk/cypress/screenshots
|
||||
matrix-react-sdk/cypress/videos
|
||||
matrix-react-sdk/cypress/synapselogs
|
||||
matrix-react-sdk/cypress/results/cypresslogs
|
||||
|
||||
report:
|
||||
name: Finalize results
|
||||
needs:
|
||||
- prepare
|
||||
- tests
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- name: Finalize Percy
|
||||
if: needs.prepare.outputs.percy_enable == '1'
|
||||
run: npx -p @percy/cli percy build:finalize
|
||||
env:
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
|
||||
- name: Skip Percy required check
|
||||
if: needs.prepare.outputs.percy_enable != '1'
|
||||
uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: success
|
||||
description: Percy skipped
|
||||
context: percy/matrix-react-sdk
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
|
||||
with:
|
||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
state: ${{ needs.tests.result == 'success' && 'success' || 'failure' }}
|
||||
context: ${{ github.workflow }} / cypress
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
@ -3,12 +3,6 @@
|
|||
# as an artifact and run integration tests.
|
||||
name: Element Web - Build
|
||||
on:
|
||||
# We only need the nightly run for Percy which is disabled while we're
|
||||
# figuring out https://github.com/vector-im/wat-internal/issues/36 and
|
||||
# https://github.com/vector-im/wat-internal/issues/56. We're hoping to
|
||||
# turn it back on or switch to an alternative in the future.
|
||||
# schedule:
|
||||
# - cron: "17 4 * * 1-5" # every weekday at 04:17 UTC
|
||||
pull_request: {}
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
|
@ -82,7 +76,7 @@ jobs:
|
|||
echo $VERSION > webapp/version
|
||||
working-directory: ./element-web
|
||||
|
||||
# Record the react-sdk sha so our cypress tests are from the same sha
|
||||
# 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
|
||||
|
|
|
@ -19,14 +19,3 @@ package-lock.json
|
|||
|
||||
.vscode
|
||||
.vscode/
|
||||
|
||||
/cypress/videos
|
||||
/cypress/downloads
|
||||
/cypress/screenshots
|
||||
/cypress/synapselogs
|
||||
/cypress/dendritelogs
|
||||
/cypress/results
|
||||
|
||||
# These could have files in them but don't currently
|
||||
# Cypress will still auto-create them though...
|
||||
/cypress/performance
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
version: 2
|
||||
snapshot:
|
||||
widths:
|
||||
- 1024
|
||||
- 1920
|
||||
percy:
|
||||
defer-uploads: true
|
|
@ -2,8 +2,6 @@
|
|||
![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)
|
||||
[![Knapsack Pro Parallel CI builds for Cypress Test - Legacy Crypto](https://img.shields.io/badge/Knapsack%20Pro-Parallel%20%2F%20Cypress%20Test%20--%20Legacy%20Crypto-%230074ff)](https://knapsackpro.com/dashboard/organizations/3882/projects/2469/test_suites/3724/builds?utm_campaign=organization-id-3882&utm_content=test-suite-id-3724&utm_medium=readme&utm_source=knapsack-pro-badge&utm_term=project-id-2469)
|
||||
[![Knapsack Pro Parallel CI builds for Cypress Test - Rust Crypto](https://img.shields.io/badge/Knapsack%20Pro-Parallel%20%2F%20Cypress%20Test%20--%20Rust%20Crypto-%230074ff)](https://knapsackpro.com/dashboard/organizations/3882/projects/2469/test_suites/3729/builds?utm_campaign=organization-id-3882&utm_content=test-suite-id-3729&utm_medium=readme&utm_source=knapsack-pro-badge&utm_term=project-id-2469)
|
||||
[![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)
|
||||
|
@ -206,5 +204,5 @@ Now the yarn commands should work as normal.
|
|||
|
||||
### End-to-End tests
|
||||
|
||||
We use Cypress and Element Web for end-to-end tests. See
|
||||
[`docs/cypress.md`](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,7 +0,0 @@
|
|||
{
|
||||
"reporterEnabled": "spec, mocha-junit-reporter",
|
||||
"mochaJunitReporterReporterOptions": {
|
||||
"mochaFile": "cypress/results/junit/results-[hash].xml",
|
||||
"useFullSuiteTitle": true
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
import registerPlugins from "./cypress/plugins";
|
||||
|
||||
export default defineConfig({
|
||||
videoUploadOnPasses: false,
|
||||
projectId: "ppvnzg",
|
||||
experimentalInteractiveRunEvents: true,
|
||||
experimentalMemoryManagement: true,
|
||||
defaultCommandTimeout: 10000,
|
||||
chromeWebSecurity: false,
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
return registerPlugins(on, config);
|
||||
},
|
||||
baseUrl: "http://localhost:8080",
|
||||
specPattern: "cypress/e2e/**/*.spec.{js,jsx,ts,tsx}",
|
||||
},
|
||||
env: {
|
||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
|
||||
SLIDING_SYNC_PROXY_TAG: "v0.99.3",
|
||||
HOMESERVER: "synapse",
|
||||
},
|
||||
retries: {
|
||||
runMode: 4,
|
||||
openMode: 0,
|
||||
},
|
||||
|
||||
// disable logging of HTTP requests made to the Cypress server. They are noisy and not very helpful.
|
||||
// @ts-ignore https://github.com/cypress-io/cypress/issues/26284
|
||||
morgan: false,
|
||||
|
||||
// Create XML result files
|
||||
reporter: "cypress-multi-reporters",
|
||||
reporterOptions: {
|
||||
configFile: "cypress-ci-reporter-config.json",
|
||||
},
|
||||
});
|
|
@ -1,19 +0,0 @@
|
|||
/*
|
||||
Copyright 2024 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" />
|
||||
|
||||
it("Dummy test to make CI pass", () => {});
|
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
Copyright 2023 Mikhail Aheichyk
|
||||
Copyright 2023 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 type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
/**
|
||||
* Resolves when room state matches predicate.
|
||||
* @param win window object
|
||||
* @param matrixClient MatrixClient instance that can be user or bot
|
||||
* @param roomId room id to find room and check
|
||||
* @param predicate defines condition that is used to check the room state
|
||||
*/
|
||||
export function waitForRoom(
|
||||
win: Cypress.AUTWindow,
|
||||
matrixClient: MatrixClient,
|
||||
roomId: string,
|
||||
predicate: (room: Room) => boolean,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
||||
if (predicate(room)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function onEvent(ev: MatrixEvent) {
|
||||
if (ev.getRoomId() !== roomId) return;
|
||||
|
||||
if (predicate(room)) {
|
||||
matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent);
|
||||
});
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
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 { SettingLevel } from "../src/settings/SettingLevel";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface ApplicationWindow {
|
||||
// XXX: Importing SettingsStore causes a bunch of type lint errors
|
||||
mxSettingsStore: {
|
||||
setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void>;
|
||||
};
|
||||
mxMatrixClientPeg: {
|
||||
matrixClient?: MatrixClient;
|
||||
};
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MatrixClient };
|
|
@ -1,207 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <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";
|
||||
import { HomeserverConfig, HomeserverInstance } from "../utils/homeserver";
|
||||
import { StartHomeserverOpts } from "../../support/homeserver";
|
||||
|
||||
// A cypress plugins to add command to start & stop dendrites in
|
||||
// docker with preset templates.
|
||||
|
||||
const dendrites = new Map<string, HomeserverInstance>();
|
||||
|
||||
const dockerConfigDir = "/etc/dendrite/";
|
||||
const dendriteConfigFile = "dendrite.yaml";
|
||||
|
||||
function randB64Bytes(numBytes: number): string {
|
||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||
}
|
||||
|
||||
async function cfgDirFromTemplate(template: string, dendriteImage: string): Promise<HomeserverConfig> {
|
||||
template = "default";
|
||||
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-dendritedocker-"));
|
||||
|
||||
// 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) !== dendriteConfigFile });
|
||||
|
||||
const registrationSecret = randB64Bytes(16);
|
||||
|
||||
const port = await getFreePort();
|
||||
const baseUrl = `http://localhost:${port}`;
|
||||
|
||||
// now copy homeserver.yaml, applying substitutions
|
||||
console.log(`Gen ${path.join(templateDir, dendriteConfigFile)}`);
|
||||
let hsYaml = await fse.readFile(path.join(templateDir, dendriteConfigFile), "utf8");
|
||||
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
|
||||
await fse.writeFile(path.join(tempDir, dendriteConfigFile), hsYaml);
|
||||
|
||||
await dockerRun({
|
||||
image: dendriteImage,
|
||||
params: ["--rm", "--entrypoint=", "-v", `${tempDir}:/mnt`],
|
||||
containerName: `react-sdk-cypress-dendrite-keygen`,
|
||||
cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"],
|
||||
});
|
||||
|
||||
return {
|
||||
port,
|
||||
baseUrl,
|
||||
configDir: tempDir,
|
||||
registrationSecret,
|
||||
};
|
||||
}
|
||||
|
||||
// Start a dendrite instance: the template must be the name of
|
||||
// one of the templates in the cypress/plugins/dendritedocker/templates
|
||||
// directory
|
||||
async function dendriteStart(opts: StartHomeserverOpts): Promise<HomeserverInstance> {
|
||||
return containerStart(opts.template, false);
|
||||
}
|
||||
|
||||
// Start a dendrite instance using pinecone routing: the template must be the name of
|
||||
// one of the templates in the cypress/plugins/dendritedocker/templates
|
||||
// directory
|
||||
async function dendritePineconeStart(template: string): Promise<HomeserverInstance> {
|
||||
return containerStart(template, true);
|
||||
}
|
||||
|
||||
async function containerStart(template: string, usePinecone: boolean): Promise<HomeserverInstance> {
|
||||
let dendriteImage = "matrixdotorg/dendrite-monolith:main";
|
||||
let dendriteEntrypoint = "/usr/bin/dendrite";
|
||||
if (usePinecone) {
|
||||
dendriteImage = "matrixdotorg/dendrite-demo-pinecone:main";
|
||||
dendriteEntrypoint = "/usr/bin/dendrite-demo-pinecone";
|
||||
}
|
||||
const denCfg = await cfgDirFromTemplate(template, dendriteImage);
|
||||
|
||||
console.log(`Starting dendrite with config dir ${denCfg.configDir}...`);
|
||||
|
||||
const dendriteId = await dockerRun({
|
||||
image: dendriteImage,
|
||||
params: [
|
||||
"--rm",
|
||||
"-v",
|
||||
`${denCfg.configDir}:` + dockerConfigDir,
|
||||
"-p",
|
||||
`${denCfg.port}:8008/tcp`,
|
||||
"--entrypoint",
|
||||
dendriteEntrypoint,
|
||||
],
|
||||
containerName: `react-sdk-cypress-dendrite`,
|
||||
cmd: ["--config", dockerConfigDir + dendriteConfigFile, "--really-enable-open-registration", "true", "run"],
|
||||
});
|
||||
|
||||
console.log(`Started dendrite with id ${dendriteId} on port ${denCfg.port}.`);
|
||||
|
||||
// Await Dendrite healthcheck
|
||||
await dockerExec({
|
||||
containerId: dendriteId,
|
||||
params: [
|
||||
"curl",
|
||||
"--connect-timeout",
|
||||
"30",
|
||||
"--retry",
|
||||
"30",
|
||||
"--retry-delay",
|
||||
"1",
|
||||
"--retry-all-errors",
|
||||
"--silent",
|
||||
"http://localhost:8008/_matrix/client/versions",
|
||||
],
|
||||
});
|
||||
|
||||
const dendrite: HomeserverInstance = { serverId: dendriteId, ...denCfg };
|
||||
dendrites.set(dendriteId, dendrite);
|
||||
return dendrite;
|
||||
}
|
||||
|
||||
async function dendriteStop(id: string): Promise<void> {
|
||||
const denCfg = dendrites.get(id);
|
||||
|
||||
if (!denCfg) throw new Error("Unknown dendrite ID");
|
||||
|
||||
const dendriteLogsPath = path.join("cypress", "dendritelogs", id);
|
||||
await fse.ensureDir(dendriteLogsPath);
|
||||
|
||||
await dockerLogs({
|
||||
containerId: id,
|
||||
stdoutFile: path.join(dendriteLogsPath, "stdout.log"),
|
||||
stderrFile: path.join(dendriteLogsPath, "stderr.log"),
|
||||
});
|
||||
|
||||
await dockerStop({
|
||||
containerId: id,
|
||||
});
|
||||
|
||||
await fse.remove(denCfg.configDir);
|
||||
|
||||
dendrites.delete(id);
|
||||
|
||||
console.log(`Stopped dendrite 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;
|
||||
}
|
||||
|
||||
async function dendritePineconeStop(id: string): Promise<void> {
|
||||
return dendriteStop(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export function dendriteDocker(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
dendriteStart,
|
||||
dendriteStop,
|
||||
dendritePineconeStart,
|
||||
dendritePineconeStop,
|
||||
});
|
||||
|
||||
on("after:spec", async (spec) => {
|
||||
// Cleans up any remaining dendrite instances after a spec run
|
||||
// This is on the theory that we should avoid re-using dendrite
|
||||
// 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 dendrites, we could inadvertently
|
||||
// make our tests depend on each other.
|
||||
for (const denId of dendrites.keys()) {
|
||||
console.warn(`Cleaning up dendrite ID ${denId} after ${spec.name}`);
|
||||
await dendriteStop(denId);
|
||||
}
|
||||
});
|
||||
|
||||
on("before:run", async () => {
|
||||
// tidy up old dendrite log files before each run
|
||||
await fse.emptyDir(path.join("cypress", "dendritelogs"));
|
||||
});
|
||||
}
|
|
@ -1,378 +0,0 @@
|
|||
# This is the Dendrite configuration file.
|
||||
#
|
||||
# The configuration is split up into sections - each Dendrite component has a
|
||||
# configuration section, in addition to the "global" section which applies to
|
||||
# all components.
|
||||
|
||||
# The version of the configuration file.
|
||||
version: 2
|
||||
|
||||
# Global Matrix configuration. This configuration applies to all components.
|
||||
global:
|
||||
# The domain name of this homeserver.
|
||||
server_name: localhost
|
||||
|
||||
# The path to the signing private key file, used to sign requests and events.
|
||||
# Note that this is NOT the same private key as used for TLS! To generate a
|
||||
# signing key, use "./bin/generate-keys --private-key matrix_key.pem".
|
||||
private_key: matrix_key.pem
|
||||
|
||||
# The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
|
||||
# to old signing keys that were formerly in use on this domain name. These
|
||||
# keys will not be used for federation request or event signing, but will be
|
||||
# provided to any other homeserver that asks when trying to verify old events.
|
||||
old_private_keys:
|
||||
# If the old private key file is available:
|
||||
# - private_key: old_matrix_key.pem
|
||||
# expired_at: 1601024554498
|
||||
# If only the public key (in base64 format) and key ID are known:
|
||||
# - public_key: mn59Kxfdq9VziYHSBzI7+EDPDcBS2Xl7jeUdiiQcOnM=
|
||||
# key_id: ed25519:mykeyid
|
||||
# expired_at: 1601024554498
|
||||
|
||||
# How long a remote server can cache our server signing key before requesting it
|
||||
# again. Increasing this number will reduce the number of requests made by other
|
||||
# servers for our key but increases the period that a compromised key will be
|
||||
# considered valid by other homeservers.
|
||||
key_validity_period: 168h0m0s
|
||||
|
||||
# Global database connection pool, for PostgreSQL monolith deployments only. If
|
||||
# this section is populated then you can omit the "database" blocks in all other
|
||||
# sections. For polylith deployments, or monolith deployments using SQLite databases,
|
||||
# you must configure the "database" block for each component instead.
|
||||
# database:
|
||||
# connection_string: postgresql://username:password@hostname/dendrite?sslmode=disable
|
||||
# max_open_conns: 90
|
||||
# max_idle_conns: 5
|
||||
# conn_max_lifetime: -1
|
||||
|
||||
# Configuration for in-memory caches. Caches can often improve performance by
|
||||
# keeping frequently accessed items (like events, identifiers etc.) in memory
|
||||
# rather than having to read them from the database.
|
||||
cache:
|
||||
# The estimated maximum size for the global cache in bytes, or in terabytes,
|
||||
# gigabytes, megabytes or kilobytes when the appropriate 'tb', 'gb', 'mb' or
|
||||
# 'kb' suffix is specified. Note that this is not a hard limit, nor is it a
|
||||
# memory limit for the entire process. A cache that is too small may ultimately
|
||||
# provide little or no benefit.
|
||||
max_size_estimated: 1gb
|
||||
|
||||
# The maximum amount of time that a cache entry can live for in memory before
|
||||
# it will be evicted and/or refreshed from the database. Lower values result in
|
||||
# easier admission of new cache entries but may also increase database load in
|
||||
# comparison to higher values, so adjust conservatively. Higher values may make
|
||||
# it harder for new items to make it into the cache, e.g. if new rooms suddenly
|
||||
# become popular.
|
||||
max_age: 1h
|
||||
|
||||
# The server name to delegate server-server communications to, with optional port
|
||||
# e.g. localhost:443
|
||||
well_known_server_name: ""
|
||||
|
||||
# The server name to delegate client-server communications to, with optional port
|
||||
# e.g. localhost:443
|
||||
well_known_client_name: ""
|
||||
|
||||
# Lists of domains that the server will trust as identity servers to verify third
|
||||
# party identifiers such as phone numbers and email addresses.
|
||||
trusted_third_party_id_servers:
|
||||
- matrix.org
|
||||
- vector.im
|
||||
|
||||
# Disables federation. Dendrite will not be able to communicate with other servers
|
||||
# in the Matrix federation and the federation API will not be exposed.
|
||||
disable_federation: false
|
||||
|
||||
# Configures the handling of presence events. Inbound controls whether we receive
|
||||
# presence events from other servers, outbound controls whether we send presence
|
||||
# events for our local users to other servers.
|
||||
presence:
|
||||
enable_inbound: false
|
||||
enable_outbound: false
|
||||
|
||||
# Configures phone-home statistics reporting. These statistics contain the server
|
||||
# name, number of active users and some information on your deployment config.
|
||||
# We use this information to understand how Dendrite is being used in the wild.
|
||||
report_stats:
|
||||
enabled: false
|
||||
endpoint: https://matrix.org/report-usage-stats/push
|
||||
|
||||
# Server notices allows server admins to send messages to all users on the server.
|
||||
server_notices:
|
||||
enabled: false
|
||||
# The local part, display name and avatar URL (as a mxc:// URL) for the user that
|
||||
# will send the server notices. These are visible to all users on the deployment.
|
||||
local_part: "_server"
|
||||
display_name: "Server Alerts"
|
||||
avatar_url: ""
|
||||
# The room name to be used when sending server notices. This room name will
|
||||
# appear in user clients.
|
||||
room_name: "Server Alerts"
|
||||
|
||||
# Configuration for NATS JetStream
|
||||
jetstream:
|
||||
# A list of NATS Server addresses to connect to. If none are specified, an
|
||||
# internal NATS server will be started automatically when running Dendrite in
|
||||
# monolith mode. For polylith deployments, it is required to specify the address
|
||||
# of at least one NATS Server node.
|
||||
addresses:
|
||||
# - localhost:4222
|
||||
|
||||
# Disable the validation of TLS certificates of NATS. This is
|
||||
# not recommended in production since it may allow NATS traffic
|
||||
# to be sent to an insecure endpoint.
|
||||
disable_tls_validation: false
|
||||
|
||||
# Persistent directory to store JetStream streams in. This directory should be
|
||||
# preserved across Dendrite restarts.
|
||||
storage_path: ./
|
||||
|
||||
# The prefix to use for stream names for this homeserver - really only useful
|
||||
# if you are running more than one Dendrite server on the same NATS deployment.
|
||||
topic_prefix: Dendrite
|
||||
|
||||
# Configuration for Prometheus metric collection.
|
||||
metrics:
|
||||
enabled: false
|
||||
basic_auth:
|
||||
username: metrics
|
||||
password: metrics
|
||||
|
||||
# Optional DNS cache. The DNS cache may reduce the load on DNS servers if there
|
||||
# is no local caching resolver available for use.
|
||||
dns_cache:
|
||||
enabled: false
|
||||
cache_size: 256
|
||||
cache_lifetime: "5m" # 5 minutes; https://pkg.go.dev/time@master#ParseDuration
|
||||
|
||||
# Configuration for the Appservice API.
|
||||
app_service_api:
|
||||
# Disable the validation of TLS certificates of appservices. This is
|
||||
# not recommended in production since it may allow appservice traffic
|
||||
# to be sent to an insecure endpoint.
|
||||
disable_tls_validation: false
|
||||
|
||||
# Appservice configuration files to load into this homeserver.
|
||||
config_files:
|
||||
# - /path/to/appservice_registration.yaml
|
||||
|
||||
# Configuration for the Client API.
|
||||
client_api:
|
||||
# Prevents new users from being able to register on this homeserver, except when
|
||||
# using the registration shared secret below.
|
||||
registration_disabled: false
|
||||
|
||||
# Prevents new guest accounts from being created. Guest registration is also
|
||||
# disabled implicitly by setting 'registration_disabled' above.
|
||||
guests_disabled: true
|
||||
|
||||
# If set, allows registration by anyone who knows the shared secret, regardless
|
||||
# of whether registration is otherwise disabled.
|
||||
registration_shared_secret: "{{REGISTRATION_SECRET}}"
|
||||
|
||||
# Whether to require reCAPTCHA for registration. If you have enabled registration
|
||||
# then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used
|
||||
# for coordinated spam attacks.
|
||||
enable_registration_captcha: false
|
||||
|
||||
# Settings for ReCAPTCHA.
|
||||
recaptcha_public_key: ""
|
||||
recaptcha_private_key: ""
|
||||
recaptcha_bypass_secret: ""
|
||||
|
||||
# To use hcaptcha.com instead of ReCAPTCHA, set the following parameters, otherwise just keep them empty.
|
||||
# recaptcha_siteverify_api: "https://hcaptcha.com/siteverify"
|
||||
# recaptcha_api_js_url: "https://js.hcaptcha.com/1/api.js"
|
||||
# recaptcha_form_field: "h-captcha-response"
|
||||
# recaptcha_sitekey_class: "h-captcha"
|
||||
|
||||
# TURN server information that this homeserver should send to clients.
|
||||
turn:
|
||||
turn_user_lifetime: "5m"
|
||||
turn_uris:
|
||||
# - turn:turn.server.org?transport=udp
|
||||
# - turn:turn.server.org?transport=tcp
|
||||
turn_shared_secret: ""
|
||||
# If your TURN server requires static credentials, then you will need to enter
|
||||
# them here instead of supplying a shared secret. Note that these credentials
|
||||
# will be visible to clients!
|
||||
# turn_username: ""
|
||||
# turn_password: ""
|
||||
|
||||
# Settings for rate-limited endpoints. Rate limiting kicks in after the threshold
|
||||
# number of "slots" have been taken by requests from a specific host. Each "slot"
|
||||
# will be released after the cooloff time in milliseconds. Server administrators
|
||||
# and appservice users are exempt from rate limiting by default.
|
||||
rate_limiting:
|
||||
enabled: true
|
||||
threshold: 20
|
||||
cooloff_ms: 500
|
||||
exempt_user_ids:
|
||||
# - "@user:domain.com"
|
||||
|
||||
# Configuration for the Federation API.
|
||||
federation_api:
|
||||
# How many times we will try to resend a failed transaction to a specific server. The
|
||||
# backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. Once
|
||||
# the max retries are exceeded, Dendrite will no longer try to send transactions to
|
||||
# that server until it comes back to life and connects to us again.
|
||||
send_max_retries: 16
|
||||
|
||||
# Disable the validation of TLS certificates of remote federated homeservers. Do not
|
||||
# enable this option in production as it presents a security risk!
|
||||
disable_tls_validation: false
|
||||
|
||||
# Disable HTTP keepalives, which also prevents connection reuse. Dendrite will typically
|
||||
# keep HTTP connections open to remote hosts for 5 minutes as they can be reused much
|
||||
# more quickly than opening new connections each time. Disabling keepalives will close
|
||||
# HTTP connections immediately after a successful request but may result in more CPU and
|
||||
# memory being used on TLS handshakes for each new connection instead.
|
||||
disable_http_keepalives: false
|
||||
|
||||
# Perspective keyservers to use as a backup when direct key fetches fail. This may
|
||||
# be required to satisfy key requests for servers that are no longer online when
|
||||
# joining some rooms.
|
||||
key_perspectives:
|
||||
- server_name: matrix.org
|
||||
keys:
|
||||
- key_id: ed25519:auto
|
||||
public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
|
||||
- key_id: ed25519:a_RXGa
|
||||
public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ
|
||||
|
||||
# This option will control whether Dendrite will prefer to look up keys directly
|
||||
# or whether it should try perspective servers first, using direct fetches as a
|
||||
# last resort.
|
||||
prefer_direct_fetch: false
|
||||
|
||||
database:
|
||||
connection_string: file:dendrite-federationapi.db
|
||||
|
||||
# Configuration for the Media API.
|
||||
media_api:
|
||||
# Storage path for uploaded media. May be relative or absolute.
|
||||
base_path: ./media_store
|
||||
|
||||
# The maximum allowed file size (in bytes) for media uploads to this homeserver
|
||||
# (0 = unlimited). If using a reverse proxy, ensure it allows requests at least
|
||||
#this large (e.g. the client_max_body_size setting in nginx).
|
||||
max_file_size_bytes: 10485760
|
||||
|
||||
# Whether to dynamically generate thumbnails if needed.
|
||||
dynamic_thumbnails: false
|
||||
|
||||
# The maximum number of simultaneous thumbnail generators to run.
|
||||
max_thumbnail_generators: 10
|
||||
|
||||
# A list of thumbnail sizes to be generated for media content.
|
||||
thumbnail_sizes:
|
||||
- width: 32
|
||||
height: 32
|
||||
method: crop
|
||||
- width: 96
|
||||
height: 96
|
||||
method: crop
|
||||
- width: 640
|
||||
height: 480
|
||||
method: scale
|
||||
|
||||
database:
|
||||
connection_string: file:dendrite-mediaapi.db
|
||||
|
||||
# Configuration for enabling experimental MSCs on this homeserver.
|
||||
mscs:
|
||||
mscs:
|
||||
# - msc2836 # (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836)
|
||||
# - msc2946 # (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946)
|
||||
|
||||
database:
|
||||
connection_string: file:dendrite-msc.db
|
||||
|
||||
# Configuration for the Sync API.
|
||||
sync_api:
|
||||
# This option controls which HTTP header to inspect to find the real remote IP
|
||||
# address of the client. This is likely required if Dendrite is running behind
|
||||
# a reverse proxy server.
|
||||
# real_ip_header: X-Real-IP
|
||||
|
||||
# Configuration for the full-text search engine.
|
||||
search:
|
||||
# Whether or not search is enabled.
|
||||
enabled: false
|
||||
|
||||
# The path where the search index will be created in.
|
||||
index_path: "./searchindex"
|
||||
|
||||
# The language most likely to be used on the server - used when indexing, to
|
||||
# ensure the returned results match expectations. A full list of possible languages
|
||||
# can be found at https://github.com/blevesearch/bleve/tree/master/analysis/lang
|
||||
language: "en"
|
||||
|
||||
database:
|
||||
connection_string: file:dendrite-syncapi.db
|
||||
|
||||
# Configuration for the User API.
|
||||
user_api:
|
||||
# The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31
|
||||
# See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information.
|
||||
# Setting this lower makes registration/login consume less CPU resources at the cost
|
||||
# of security should the database be compromised. Setting this higher makes registration/login
|
||||
# consume more CPU resources but makes it harder to brute force password hashes. This value
|
||||
# can be lowered if performing tests or on embedded Dendrite instances (e.g WASM builds).
|
||||
bcrypt_cost: 10
|
||||
|
||||
# The length of time that a token issued for a relying party from
|
||||
# /_matrix/client/r0/user/{userId}/openid/request_token endpoint
|
||||
# is considered to be valid in milliseconds.
|
||||
# The default lifetime is 3600000ms (60 minutes).
|
||||
# openid_token_lifetime_ms: 3600000
|
||||
|
||||
# Users who register on this homeserver will automatically be joined to the rooms listed under "auto_join_rooms" option.
|
||||
# By default, any room aliases included in this list will be created as a publicly joinable room
|
||||
# when the first user registers for the homeserver. If the room already exists,
|
||||
# make certain it is a publicly joinable room, i.e. the join rule of the room must be set to 'public'.
|
||||
# As Spaces are just rooms under the hood, Space aliases may also be used.
|
||||
auto_join_rooms:
|
||||
# - "#main:matrix.org"
|
||||
|
||||
account_database:
|
||||
connection_string: file:dendrite-userapi.db
|
||||
|
||||
room_server:
|
||||
database:
|
||||
connection_string: file:dendrite-roomserverapi.db
|
||||
|
||||
key_server:
|
||||
database:
|
||||
connection_string: file:dendrite-keyserverapi.db
|
||||
|
||||
relay_api:
|
||||
database:
|
||||
connection_string: file:dendrite-relayapi.db
|
||||
|
||||
# Configuration for Opentracing.
|
||||
# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on
|
||||
# how this works and how to set it up.
|
||||
tracing:
|
||||
enabled: false
|
||||
jaeger:
|
||||
serviceName: ""
|
||||
disabled: false
|
||||
rpc_metrics: false
|
||||
tags: []
|
||||
sampler: null
|
||||
reporter: null
|
||||
headers: null
|
||||
baggage_restrictions: null
|
||||
throttler: null
|
||||
|
||||
# Logging configuration. The "std" logging type controls the logs being sent to
|
||||
# stdout. The "file" logging type controls logs being written to a log folder on
|
||||
# the disk. Supported log levels are "debug", "info", "warn", "error".
|
||||
logging:
|
||||
- type: std
|
||||
level: debug
|
||||
- type: file
|
||||
level: debug
|
||||
params:
|
||||
path: ./logs
|
|
@ -1,180 +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 async 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) {
|
||||
// Run the docker container as our uid:gid to prevent problems with permissions.
|
||||
if (await isPodman()) {
|
||||
// Note: this setup is for podman rootless containers.
|
||||
|
||||
// In podman, run as root in the container, which maps to the current
|
||||
// user on the host. This is probably the default since Synapse's
|
||||
// Dockerfile doesn't specify, but we're being explicit here
|
||||
// because it's important for the permissions to work.
|
||||
params.push("-u", "0:0");
|
||||
|
||||
// Tell Synapse not to switch UID
|
||||
params.push("-e", "UID=0");
|
||||
params.push("-e", "GID=0");
|
||||
} else {
|
||||
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
|
||||
}
|
||||
}
|
||||
|
||||
const args = [
|
||||
"run",
|
||||
"--name",
|
||||
`${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
|
||||
"-d",
|
||||
"--rm",
|
||||
...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());
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects whether the docker command is actually podman.
|
||||
* To do this, it looks for "podman" in the output of "docker --help".
|
||||
*/
|
||||
export function isPodman(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
childProcess.execFile("docker", ["--help"], (err, stdout) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stdout.toLowerCase().includes("podman"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Supply the right hostname to use to talk to the host machine. On Docker this
|
||||
* is "host.docker.internal" and on Podman this is "host.containers.internal".
|
||||
*/
|
||||
export async function hostContainerName() {
|
||||
return (await isPodman()) ? "host.containers.internal" : "host.docker.internal";
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export function docker(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
dockerRun,
|
||||
dockerExec,
|
||||
dockerLogs,
|
||||
dockerStop,
|
||||
dockerRm,
|
||||
dockerIp,
|
||||
});
|
||||
}
|
|
@ -1,46 +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 installLogsPrinter from "cypress-terminal-report/src/installLogsPrinter";
|
||||
import { initPlugins } from "cypress-plugin-init";
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
import { synapseDocker } from "./synapsedocker";
|
||||
import { dendriteDocker } from "./dendritedocker";
|
||||
import { webserver } from "./webserver";
|
||||
import { docker } from "./docker";
|
||||
import { log } from "./log";
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
export default function (on: PluginEvents, config: PluginConfigOptions) {
|
||||
initPlugins(on, [docker, synapseDocker, dendriteDocker, webserver, log], config);
|
||||
installLogsPrinter(on, {
|
||||
printLogsToConsole: "never",
|
||||
|
||||
// write logs to cypress/results/cypresslogs/<spec>.txt
|
||||
outputRoot: "cypress/results",
|
||||
outputTarget: {
|
||||
"cypresslogs|txt": "txt",
|
||||
},
|
||||
|
||||
// strip 'cypress/e2e' from log filenames
|
||||
specRoot: "cypress/e2e",
|
||||
});
|
||||
}
|
|
@ -1,35 +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;
|
||||
|
||||
export function log(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
log(message: string) {
|
||||
console.log(message);
|
||||
|
||||
return null;
|
||||
},
|
||||
table(message: string) {
|
||||
console.table(message);
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import PluginEvents = Cypress.PluginEvents;
|
||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
import { getFreePort } from "../utils/port";
|
||||
import { dockerIp, dockerRun, dockerStop } from "../docker";
|
||||
|
||||
// A cypress plugins to add command to manage an instance of Mailhog in Docker
|
||||
|
||||
export interface Instance {
|
||||
host: string;
|
||||
smtpPort: number;
|
||||
httpPort: number;
|
||||
containerId: string;
|
||||
}
|
||||
|
||||
const instances = new Map<string, Instance>();
|
||||
|
||||
// Start a synapse instance: the template must be the name of
|
||||
// one of the templates in the cypress/plugins/synapsedocker/templates
|
||||
// directory
|
||||
async function mailhogStart(): Promise<Instance> {
|
||||
const smtpPort = await getFreePort();
|
||||
const httpPort = await getFreePort();
|
||||
|
||||
console.log(`Starting mailhog...`);
|
||||
|
||||
const containerId = await dockerRun({
|
||||
image: "mailhog/mailhog:latest",
|
||||
containerName: `react-sdk-cypress-mailhog`,
|
||||
params: ["--rm", "-p", `${smtpPort}:1025/tcp`, "-p", `${httpPort}:8025/tcp`],
|
||||
});
|
||||
|
||||
console.log(`Started mailhog on ports smtp=${smtpPort} http=${httpPort}.`);
|
||||
|
||||
const host = await dockerIp({ containerId });
|
||||
const instance: Instance = { smtpPort, httpPort, containerId, host };
|
||||
instances.set(containerId, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
async function mailhogStop(id: string): Promise<void> {
|
||||
const synCfg = instances.get(id);
|
||||
|
||||
if (!synCfg) throw new Error("Unknown mailhog ID");
|
||||
|
||||
await dockerStop({
|
||||
containerId: id,
|
||||
});
|
||||
|
||||
instances.delete(id);
|
||||
|
||||
console.log(`Stopped mailhog 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 mailhogDocker(on: PluginEvents, config: PluginConfigOptions) {
|
||||
on("task", {
|
||||
mailhogStart,
|
||||
mailhogStop,
|
||||
});
|
||||
|
||||
on("after:spec", async (spec) => {
|
||||
// Cleans up any remaining instances after a spec run
|
||||
for (const synId of instances.keys()) {
|
||||
console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`);
|
||||
await mailhogStop(synId);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,218 +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, hostContainerName, isPodman } from "../docker";
|
||||
import { HomeserverConfig, HomeserverInstance } from "../utils/homeserver";
|
||||
import { StartHomeserverOpts } from "../../support/homeserver";
|
||||
|
||||
// A cypress plugins to add command to start & stop synapses in
|
||||
// docker with preset templates.
|
||||
|
||||
const synapses = new Map<string, HomeserverInstance>();
|
||||
|
||||
function randB64Bytes(numBytes: number): string {
|
||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||
}
|
||||
|
||||
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<HomeserverConfig> {
|
||||
const templateDir = path.join(__dirname, "templates", opts.template);
|
||||
|
||||
const stats = await fse.stat(templateDir);
|
||||
if (!stats?.isDirectory) {
|
||||
throw new Error(`No such template: ${opts.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
|
||||
const templateHomeserver = path.join(templateDir, "homeserver.yaml");
|
||||
const outputHomeserver = path.join(tempDir, "homeserver.yaml");
|
||||
console.log(`Gen ${templateHomeserver} -> ${outputHomeserver}`);
|
||||
let hsYaml = await fse.readFile(templateHomeserver, "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);
|
||||
hsYaml = hsYaml.replace(/{{OAUTH_SERVER_PORT}}/g, opts.oAuthServerPort?.toString());
|
||||
hsYaml = hsYaml.replace(/{{HOST_DOCKER_INTERNAL}}/g, await hostContainerName());
|
||||
if (opts.variables) {
|
||||
let fetchedHostContainer = null;
|
||||
for (const key in opts.variables) {
|
||||
let value = String(opts.variables[key]);
|
||||
|
||||
if (value === "{{HOST_DOCKER_INTERNAL}}") {
|
||||
if (!fetchedHostContainer) {
|
||||
fetchedHostContainer = await hostContainerName();
|
||||
}
|
||||
value = fetchedHostContainer;
|
||||
}
|
||||
|
||||
hsYaml = hsYaml.replace(new RegExp("%" + key + "%", "g"), value);
|
||||
}
|
||||
}
|
||||
|
||||
await fse.writeFile(outputHomeserver, 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);
|
||||
const outputSigningKey = path.join(tempDir, "localhost.signing.key");
|
||||
console.log(`Gen -> ${outputSigningKey}`);
|
||||
await fse.writeFile(outputSigningKey, `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.
|
||||
*
|
||||
* Any value in opts.variables that is set to `{{HOST_DOCKER_INTERNAL}}'
|
||||
* will be replaced with 'host.docker.internal' (if we are on Docker) or
|
||||
* 'host.containers.interal' if we are on Podman.
|
||||
*/
|
||||
async function synapseStart(opts: StartHomeserverOpts): Promise<HomeserverInstance> {
|
||||
const synCfg = await cfgDirFromTemplate(opts);
|
||||
|
||||
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||
|
||||
const dockerSynapseParams = ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`];
|
||||
|
||||
if (await isPodman()) {
|
||||
// Make host.containers.internal work to allow Synapse to talk to the
|
||||
// test OIDC server.
|
||||
dockerSynapseParams.push("--network");
|
||||
dockerSynapseParams.push("slirp4netns:allow_host_loopback=true");
|
||||
} else {
|
||||
// Make host.docker.internal work to allow Synapse to talk to the test
|
||||
// OIDC server.
|
||||
dockerSynapseParams.push("--add-host");
|
||||
dockerSynapseParams.push("host.docker.internal:host-gateway");
|
||||
}
|
||||
|
||||
const synapseId = await dockerRun({
|
||||
image: "matrixdotorg/synapse:develop",
|
||||
containerName: `react-sdk-cypress-synapse`,
|
||||
params: dockerSynapseParams,
|
||||
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: HomeserverInstance = { serverId: 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,3 +0,0 @@
|
|||
# Meta-template for synapse templates
|
||||
|
||||
To make another template, you can copy this directory
|
|
@ -1,72 +0,0 @@
|
|||
server_name: "localhost"
|
||||
pid_file: /data/homeserver.pid
|
||||
# XXX: This won't actually be right: it lets docker allocate an ephemeral port,
|
||||
# so we have a chicken-and-egg problem
|
||||
public_baseurl: http://localhost:8008/
|
||||
# Listener is always port 8008 (configured in the container)
|
||||
listeners:
|
||||
- port: 8008
|
||||
tls: false
|
||||
bind_addresses: ["::"]
|
||||
type: http
|
||||
x_forwarded: true
|
||||
|
||||
resources:
|
||||
- names: [client, federation, consent]
|
||||
compress: false
|
||||
|
||||
# An sqlite in-memory database is fast & automatically wipes each time
|
||||
database:
|
||||
name: "sqlite3"
|
||||
args:
|
||||
database: ":memory:"
|
||||
|
||||
# Needs to be configured to log to the console like a good docker process
|
||||
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
|
||||
# These placeholders will be be replaced with values generated at start
|
||||
registration_shared_secret: "{{REGISTRATION_SECRET}}"
|
||||
report_stats: false
|
||||
macaroon_secret_key: "{{MACAROON_SECRET_KEY}}"
|
||||
form_secret: "{{FORM_SECRET}}"
|
||||
# Signing key must be here: it will be generated to this file
|
||||
signing_key_path: "/data/localhost.signing.key"
|
||||
email:
|
||||
enable_notifs: false
|
||||
smtp_host: "localhost"
|
||||
smtp_port: 25
|
||||
smtp_user: "exampleusername"
|
||||
smtp_pass: "examplepassword"
|
||||
require_transport_security: False
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: Matrix
|
||||
notif_template_html: notif_mail.html
|
||||
notif_template_text: notif_mail.txt
|
||||
notif_for_new_users: True
|
||||
client_base_url: "http://localhost/element"
|
||||
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
|
@ -1,50 +0,0 @@
|
|||
# Log configuration for Synapse.
|
||||
#
|
||||
# This is a YAML file containing a standard Python logging configuration
|
||||
# dictionary. See [1] for details on the valid settings.
|
||||
#
|
||||
# Synapse also supports structured logging for machine readable logs which can
|
||||
# be ingested by ELK stacks. See [2] for details.
|
||||
#
|
||||
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
|
||||
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
|
||||
|
||||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
handlers:
|
||||
# A handler that writes logs to stderr. Unused by default, but can be used
|
||||
# instead of "buffer" and "file" in the logger handlers.
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: INFO
|
||||
|
||||
twisted:
|
||||
# We send the twisted logging directly to the file handler,
|
||||
# to work around https://github.com/matrix-org/synapse/issues/3471
|
||||
# when using "buffer" logger. Use "console" to log to stderr instead.
|
||||
handlers: [console]
|
||||
propagate: false
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
|
||||
# Write logs to the `buffer` handler, which will buffer them together in memory,
|
||||
# then write them to a file.
|
||||
#
|
||||
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
|
||||
# also need to update the configuration for the `twisted` logger above, in
|
||||
# this case.)
|
||||
#
|
||||
handlers: [console]
|
||||
|
||||
disable_existing_loggers: false
|
|
@ -1 +0,0 @@
|
|||
A synapse configured with user privacy consent enabled
|
|
@ -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,50 +0,0 @@
|
|||
# Log configuration for Synapse.
|
||||
#
|
||||
# This is a YAML file containing a standard Python logging configuration
|
||||
# dictionary. See [1] for details on the valid settings.
|
||||
#
|
||||
# Synapse also supports structured logging for machine readable logs which can
|
||||
# be ingested by ELK stacks. See [2] for details.
|
||||
#
|
||||
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
|
||||
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
|
||||
|
||||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
handlers:
|
||||
# A handler that writes logs to stderr. Unused by default, but can be used
|
||||
# instead of "buffer" and "file" in the logger handlers.
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: DEBUG
|
||||
|
||||
twisted:
|
||||
# We send the twisted logging directly to the file handler,
|
||||
# to work around https://github.com/matrix-org/synapse/issues/3471
|
||||
# when using "buffer" logger. Use "console" to log to stderr instead.
|
||||
handlers: [console]
|
||||
propagate: false
|
||||
|
||||
root:
|
||||
level: DEBUG
|
||||
|
||||
# Write logs to the `buffer` handler, which will buffer them together in memory,
|
||||
# then write them to a file.
|
||||
#
|
||||
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
|
||||
# also need to update the configuration for the `twisted` logger above, in
|
||||
# this case.)
|
||||
#
|
||||
handlers: [console]
|
||||
|
||||
disable_existing_loggers: false
|
|
@ -1,19 +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 schoen</p>
|
||||
</body>
|
||||
</html>
|
|
@ -1 +0,0 @@
|
|||
A synapse configured with user privacy consent disabled
|
|
@ -1,94 +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"
|
||||
|
||||
oidc_providers:
|
||||
- idp_id: test
|
||||
idp_name: "OAuth test"
|
||||
issuer: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth"
|
||||
authorization_endpoint: "http://localhost:{{OAUTH_SERVER_PORT}}/oauth/auth.html"
|
||||
# the token endpoint receives requests from synapse, rather than the webapp, so needs to escape the docker container.
|
||||
# Hence, HOST_DOCKER_INTERNAL rather than localhost. This is set to
|
||||
# host.docker.internal on Docker and host.containers.internal on Podman.
|
||||
token_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/token"
|
||||
userinfo_endpoint: "http://{{HOST_DOCKER_INTERNAL}}:{{OAUTH_SERVER_PORT}}/oauth/userinfo"
|
||||
client_id: "synapse"
|
||||
discover: false
|
||||
scopes: ["profile"]
|
||||
skip_verification: true
|
||||
user_mapping_provider:
|
||||
config:
|
||||
display_name_template: "{{ user.name }}"
|
|
@ -1,50 +0,0 @@
|
|||
# Log configuration for Synapse.
|
||||
#
|
||||
# This is a YAML file containing a standard Python logging configuration
|
||||
# dictionary. See [1] for details on the valid settings.
|
||||
#
|
||||
# Synapse also supports structured logging for machine readable logs which can
|
||||
# be ingested by ELK stacks. See [2] for details.
|
||||
#
|
||||
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
|
||||
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
|
||||
|
||||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
handlers:
|
||||
# A handler that writes logs to stderr. Unused by default, but can be used
|
||||
# instead of "buffer" and "file" in the logger handlers.
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: DEBUG
|
||||
|
||||
twisted:
|
||||
# We send the twisted logging directly to the file handler,
|
||||
# to work around https://github.com/matrix-org/synapse/issues/3471
|
||||
# when using "buffer" logger. Use "console" to log to stderr instead.
|
||||
handlers: [console]
|
||||
propagate: false
|
||||
|
||||
root:
|
||||
level: DEBUG
|
||||
|
||||
# Write logs to the `buffer` handler, which will buffer them together in memory,
|
||||
# then write them to a file.
|
||||
#
|
||||
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
|
||||
# also need to update the configuration for the `twisted` logger above, in
|
||||
# this case.)
|
||||
#
|
||||
handlers: [console]
|
||||
|
||||
disable_existing_loggers: false
|
|
@ -1 +0,0 @@
|
|||
A synapse configured to require an email for registration
|
|
@ -1,44 +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"
|
||||
|
||||
media_store_path: "/data/media_store"
|
||||
uploads_path: "/data/uploads"
|
||||
enable_registration: true
|
||||
registrations_require_3pid:
|
||||
- email
|
||||
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"
|
||||
|
||||
email:
|
||||
smtp_host: "%SMTP_HOST%"
|
||||
smtp_port: %SMTP_PORT%
|
||||
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>"
|
||||
app_name: my_branded_matrix_server
|
|
@ -1,50 +0,0 @@
|
|||
# Log configuration for Synapse.
|
||||
#
|
||||
# This is a YAML file containing a standard Python logging configuration
|
||||
# dictionary. See [1] for details on the valid settings.
|
||||
#
|
||||
# Synapse also supports structured logging for machine readable logs which can
|
||||
# be ingested by ELK stacks. See [2] for details.
|
||||
#
|
||||
# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema
|
||||
# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html
|
||||
|
||||
version: 1
|
||||
|
||||
formatters:
|
||||
precise:
|
||||
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||
|
||||
handlers:
|
||||
# A handler that writes logs to stderr. Unused by default, but can be used
|
||||
# instead of "buffer" and "file" in the logger handlers.
|
||||
console:
|
||||
class: logging.StreamHandler
|
||||
formatter: precise
|
||||
|
||||
loggers:
|
||||
synapse.storage.SQL:
|
||||
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||
# information such as access tokens.
|
||||
level: INFO
|
||||
|
||||
twisted:
|
||||
# We send the twisted logging directly to the file handler,
|
||||
# to work around https://github.com/matrix-org/synapse/issues/3471
|
||||
# when using "buffer" logger. Use "console" to log to stderr instead.
|
||||
handlers: [console]
|
||||
propagate: false
|
||||
|
||||
root:
|
||||
level: INFO
|
||||
|
||||
# Write logs to the `buffer` handler, which will buffer them together in memory,
|
||||
# then write them to a file.
|
||||
#
|
||||
# Replace "buffer" with "console" to log to stderr instead. (Note that you'll
|
||||
# also need to update the configuration for the `twisted` logger above, in
|
||||
# this case.)
|
||||
#
|
||||
handlers: [console]
|
||||
|
||||
disable_existing_loggers: false
|
|
@ -1,28 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
export interface HomeserverConfig {
|
||||
configDir: string;
|
||||
registrationSecret: string;
|
||||
baseUrl: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface HomeserverInstance extends HomeserverConfig {
|
||||
serverId: string;
|
||||
}
|
|
@ -1,27 +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 * as net from "net";
|
||||
|
||||
export async function getFreePort(): Promise<number> {
|
||||
return new Promise<number>((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(0, () => {
|
||||
const port = (<net.AddressInfo>srv.address()).port;
|
||||
srv.close(() => resolve(port));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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,101 +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 type { 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Load axe-core into the window under test.
|
||||
//
|
||||
// The injectAxe in cypress-axe attempts to load axe via an `eval`. That conflicts with our CSP
|
||||
// which disallows "unsafe-eval". So, replace it with an implementation that loads it via an
|
||||
// injected <script> element.
|
||||
Cypress.Commands.overwrite("injectAxe", (originalFn: Chainable["injectAxe"]): void => {
|
||||
Cypress.log({ name: "injectAxe" });
|
||||
|
||||
// load the minified axe source, and create an intercept to serve it up
|
||||
cy.readFile("node_modules/axe-core/axe.min.js", { log: false }).then((source) => {
|
||||
cy.intercept("/_axe", source);
|
||||
});
|
||||
|
||||
// inject a script tag to load it
|
||||
cy.get("head", { log: false }).then(
|
||||
(head) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
script.async = true;
|
||||
script.onload = resolve;
|
||||
script.onerror = (_e) => {
|
||||
// Unfortunately there does not seem to be a way to get a reason for the error.
|
||||
// The error event is useless.
|
||||
reject(new Error("Unable to load axe"));
|
||||
};
|
||||
script.src = "/_axe";
|
||||
head.get()[0].appendChild(script);
|
||||
}),
|
||||
);
|
||||
});
|
|
@ -1,369 +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 loglevel from "loglevel";
|
||||
|
||||
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
import type { SecretStorageKeyDescription } from "matrix-js-sdk/src/secret-storage";
|
||||
import { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||
import { Credentials } from "./homeserver";
|
||||
import { collapseLastLogGroup } from "./log";
|
||||
import type { Logger } from "matrix-js-sdk/src/logger";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
interface CreateBotOpts {
|
||||
/**
|
||||
* A prefix to use for the userid. If unspecified, "bot_" will be used.
|
||||
*/
|
||||
userIdPrefix?: string;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* Whether or not to generate cross-signing keys
|
||||
*/
|
||||
bootstrapCrossSigning?: boolean;
|
||||
/**
|
||||
* Whether to use the rust crypto impl. Defaults to false (for now!)
|
||||
*/
|
||||
rustCrypto?: boolean;
|
||||
/**
|
||||
* Whether or not to bootstrap the secret storage
|
||||
*/
|
||||
bootstrapSecretStorage?: boolean;
|
||||
}
|
||||
|
||||
const defaultCreateBotOptions = {
|
||||
userIdPrefix: "bot_",
|
||||
autoAcceptInvites: true,
|
||||
startClient: true,
|
||||
bootstrapCrossSigning: true,
|
||||
} as CreateBotOpts;
|
||||
|
||||
export interface CypressBot extends MatrixClient {
|
||||
__cypress_password: string;
|
||||
__cypress_recovery_key: GeneratedSecretStorageKey;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Returns a new Bot instance
|
||||
* @param homeserver the instance on which to register the bot user
|
||||
* @param opts create bot options
|
||||
*/
|
||||
getBot(homeserver: HomeserverInstance, opts: CreateBotOpts): Chainable<CypressBot>;
|
||||
|
||||
/**
|
||||
* Returns a new Bot instance logged in as an existing user
|
||||
* @param homeserver the instance on which to register the bot user
|
||||
* @param username the username for the bot to log in with
|
||||
* @param password the password for the bot to log in with
|
||||
* @param opts create bot options
|
||||
*/
|
||||
loginBot(
|
||||
homeserver: HomeserverInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
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>;
|
||||
/**
|
||||
* Send a message as a bot into a room in a specific thread
|
||||
* @param cli The bot's MatrixClient
|
||||
* @param threadId the thread within which this message should go
|
||||
* @param roomId ID of the room to join
|
||||
* @param message the message body to send
|
||||
*/
|
||||
botSendThreadMessage(
|
||||
cli: MatrixClient,
|
||||
roomId: string,
|
||||
threadId: string,
|
||||
message: string,
|
||||
): Chainable<ISendEventResponse>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupBotClient(
|
||||
homeserver: HomeserverInstance,
|
||||
credentials: Credentials,
|
||||
opts: CreateBotOpts,
|
||||
): Chainable<MatrixClient> {
|
||||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||
return cy.window({ log: false }).then(
|
||||
// extra timeout, as this sometimes takes a while
|
||||
{ timeout: 30_000 },
|
||||
async (win): Promise<MatrixClient> => {
|
||||
const logger = getLogger(win, `cypress bot ${credentials.userId}`);
|
||||
|
||||
const keys = {};
|
||||
|
||||
const getCrossSigningKey = (type: string) => {
|
||||
return keys[type];
|
||||
};
|
||||
|
||||
const saveCrossSigningKeys = (k: Record<string, Uint8Array>) => {
|
||||
Object.assign(keys, k);
|
||||
};
|
||||
|
||||
// Store the cached secret storage key and return it when `getSecretStorageKey` is called
|
||||
let cachedKey: { keyId: string; key: Uint8Array };
|
||||
const cacheSecretStorageKey = (keyId: string, keyInfo: SecretStorageKeyDescription, key: Uint8Array) => {
|
||||
cachedKey = {
|
||||
keyId,
|
||||
key,
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretStorageKey = () => Promise.resolve<[string, Uint8Array]>([cachedKey.keyId, cachedKey.key]);
|
||||
|
||||
const cryptoCallbacks = {
|
||||
getCrossSigningKey,
|
||||
saveCrossSigningKeys,
|
||||
cacheSecretStorageKey,
|
||||
getSecretStorageKey,
|
||||
};
|
||||
|
||||
const cli = new win.matrixcs.MatrixClient({
|
||||
baseUrl: homeserver.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(),
|
||||
logger: logger,
|
||||
cryptoCallbacks,
|
||||
});
|
||||
|
||||
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 cli;
|
||||
}
|
||||
|
||||
if (opts.rustCrypto) {
|
||||
await cli.initRustCrypto({ useIndexedDB: false });
|
||||
} else {
|
||||
await cli.initCrypto();
|
||||
}
|
||||
cli.setGlobalErrorOnUnknownDevices(false);
|
||||
await cli.startClient();
|
||||
|
||||
if (opts.bootstrapCrossSigning) {
|
||||
await cli.getCrypto()!.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (opts.bootstrapSecretStorage) {
|
||||
const passphrase = "new passphrase";
|
||||
const recoveryKey = await cli.getCrypto().createRecoveryKeyFromPassphrase(passphrase);
|
||||
Object.assign(cli, { __cypress_recovery_key: recoveryKey });
|
||||
|
||||
await cli.getCrypto()!.bootstrapSecretStorage({
|
||||
setupNewSecretStorage: true,
|
||||
setupNewKeyBackup: true,
|
||||
createSecretStorageKey: () => Promise.resolve(recoveryKey),
|
||||
});
|
||||
}
|
||||
|
||||
return cli;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getBot", (homeserver: HomeserverInstance, opts: CreateBotOpts): Chainable<CypressBot> => {
|
||||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||
const username = Cypress._.uniqueId(opts.userIdPrefix);
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
Cypress.log({
|
||||
name: "getBot",
|
||||
message: `Create bot user ${username} with opts ${JSON.stringify(opts)}`,
|
||||
groupStart: true,
|
||||
});
|
||||
return cy
|
||||
.registerUser(homeserver, username, password, opts.displayName)
|
||||
.then((credentials) => {
|
||||
return setupBotClient(homeserver, credentials, opts);
|
||||
})
|
||||
.then((client): Chainable<CypressBot> => {
|
||||
Object.assign(client, { __cypress_password: password });
|
||||
Cypress.log({ groupEnd: true, emitOnly: true });
|
||||
collapseLastLogGroup();
|
||||
return cy.wrap(client as CypressBot, { log: false });
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"loginBot",
|
||||
(
|
||||
homeserver: HomeserverInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
opts: CreateBotOpts,
|
||||
): Chainable<MatrixClient> => {
|
||||
opts = Object.assign({}, defaultCreateBotOptions, { bootstrapCrossSigning: false }, opts);
|
||||
Cypress.log({
|
||||
name: "loginBot",
|
||||
message: `log in as ${username} with opts ${JSON.stringify(opts)}`,
|
||||
groupStart: true,
|
||||
});
|
||||
return cy
|
||||
.loginUser(homeserver, username, password)
|
||||
.then((credentials) => {
|
||||
return setupBotClient(homeserver, credentials, opts);
|
||||
})
|
||||
.then((res) => {
|
||||
Cypress.log({ groupEnd: true, emitOnly: true });
|
||||
collapseLastLogGroup();
|
||||
cy.wrap(res, { log: false });
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
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 },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add(
|
||||
"botSendThreadMessage",
|
||||
(cli: MatrixClient, roomId: string, threadId: string, message: string): Chainable<ISendEventResponse> => {
|
||||
return cy.wrap(
|
||||
cli.sendMessage(roomId, threadId, {
|
||||
msgtype: "m.text",
|
||||
body: message,
|
||||
}),
|
||||
{ log: false },
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/** Get a Logger implementation based on `loglevel` with the given logger name */
|
||||
function getLogger(win: Cypress.AUTWindow, loggerName: string): Logger {
|
||||
const logger = loglevel.getLogger(loggerName);
|
||||
|
||||
// If this is the first time this logger has been returned, turn it into a `Logger` and set the default level
|
||||
if (!("getChild" in logger)) {
|
||||
logger["getChild"] = (namespace: string) => getLogger(win, loggerName + ":" + namespace);
|
||||
logger.methodFactory = makeLogMethodFactory(win);
|
||||
logger.setLevel(loglevel.levels.DEBUG);
|
||||
}
|
||||
|
||||
return logger as unknown as Logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getLogger: a factory for loglevel method factories.
|
||||
*/
|
||||
function makeLogMethodFactory(win: Cypress.AUTWindow): loglevel.MethodFactory {
|
||||
function methodFactory(
|
||||
methodName: loglevel.LogLevelNames,
|
||||
level: loglevel.LogLevelNumbers,
|
||||
loggerName: string | symbol,
|
||||
): loglevel.LoggingMethod {
|
||||
// here's the actual log method, which implements `Logger.info`, `Logger.debug`, etc.
|
||||
return function (first: any, ...rest): void {
|
||||
// include the logger name in the output...
|
||||
first = `\x1B[31m[${loggerName.toString()}]\x1B[m ${first.toString()}`;
|
||||
|
||||
// ... and delegate to the corresponding method in the console of the application under test.
|
||||
// Doing so (rather than using the global `console`) ensures that the output is collected
|
||||
// by the `cypress-terminal-report` plugin.
|
||||
const console = win.console;
|
||||
if (methodName in console) {
|
||||
console[methodName](first, ...rest);
|
||||
} else {
|
||||
console.log(first, ...rest);
|
||||
}
|
||||
};
|
||||
}
|
||||
return methodFactory;
|
||||
}
|
|
@ -1,218 +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,
|
||||
Room,
|
||||
IContent,
|
||||
FileType,
|
||||
Upload,
|
||||
UploadOpts,
|
||||
ICreateRoomOpts,
|
||||
ISendEventResponse,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
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[]>;
|
||||
/**
|
||||
* 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) => {
|
||||
const res = await cli.invite(roomId, userId);
|
||||
Cypress.log({ name: "inviteUser", message: `sent invite in ${roomId} for ${userId}` });
|
||||
return res;
|
||||
});
|
||||
});
|
||||
|
||||
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("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.findByRole("button", { name: "More options" }).click();
|
||||
});
|
||||
return cy.get(".mx_MessageComposer_Menu");
|
||||
});
|
||||
|
||||
// Needed to make this file a module
|
||||
export {};
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/* Intercept requests to `config.json`, so that we can test against a known configuration.
|
||||
*
|
||||
* If we don't do this, we end up testing against the Element config for develop.element.io, which then means
|
||||
* we make requests to the live `matrix.org`, which makes our tests dependent on matrix.org being up and responsive.
|
||||
*/
|
||||
|
||||
import { isRustCryptoEnabled } from "./util";
|
||||
|
||||
const CONFIG_JSON = {
|
||||
// This is deliberately quite a minimal config.json, so that we can test that the default settings
|
||||
// actually work.
|
||||
//
|
||||
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||
// We point that to a guaranteed-invalid domain.
|
||||
default_server_config: {
|
||||
"m.homeserver": {
|
||||
base_url: "https://server.invalid",
|
||||
},
|
||||
},
|
||||
|
||||
// the location tests want a map style url.
|
||||
map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const configJson = CONFIG_JSON;
|
||||
|
||||
// configure element to use rust crypto if the env var tells us so
|
||||
if (isRustCryptoEnabled()) {
|
||||
configJson["features"] = {
|
||||
feature_rust_crypto: true,
|
||||
};
|
||||
}
|
||||
cy.intercept({ method: "GET", pathname: "/config.json" }, { body: configJson });
|
||||
});
|
|
@ -1,59 +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 "@percy/cypress";
|
||||
import "cypress-real-events";
|
||||
import "@testing-library/cypress/add-commands";
|
||||
import installLogsCollector from "cypress-terminal-report/src/installLogsCollector";
|
||||
|
||||
import "./config.json";
|
||||
import "./homeserver";
|
||||
import "./login";
|
||||
import "./labs";
|
||||
import "./client";
|
||||
import "./settings";
|
||||
import "./bot";
|
||||
import "./clipboard";
|
||||
import "./util";
|
||||
import "./app";
|
||||
import "./percy";
|
||||
import "./webserver";
|
||||
import "./views";
|
||||
import "./iframes";
|
||||
import "./composer";
|
||||
import "./axe";
|
||||
import "./promise";
|
||||
|
||||
installLogsCollector({
|
||||
// specify the types of logs to collect (and report to the node console at the end of the test)
|
||||
collectTypes: [
|
||||
"cons:log",
|
||||
"cons:info",
|
||||
"cons:warn",
|
||||
"cons:error",
|
||||
// most of our logs go through `loglevel`, which sets `logger.log` to be an alias of `logger.debug`.
|
||||
// Hence, if we want to capture `logger.log` lines, we need to enable `cons:debug` here.
|
||||
"cons:debug",
|
||||
"cy:log",
|
||||
"cy:xhr",
|
||||
"cy:fetch",
|
||||
"cy:request",
|
||||
"cy:intercept",
|
||||
"cy:command",
|
||||
],
|
||||
});
|
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import * as crypto from "crypto";
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import AUTWindow = Cypress.AUTWindow;
|
||||
import { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||
|
||||
export interface StartHomeserverOpts {
|
||||
/** path to template within cypress/plugins/{homeserver}docker/template/ directory. */
|
||||
template: string;
|
||||
|
||||
/** Port of an OAuth server to configure the homeserver to use */
|
||||
oAuthServerPort?: number;
|
||||
|
||||
/** Additional variables to inject into the configuration template **/
|
||||
variables?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Start a homeserver instance with a given config template.
|
||||
*
|
||||
* @param opts: either the template path (within cypress/plugins/{homeserver}docker/template/), or
|
||||
* an options object
|
||||
*
|
||||
* If any of opts.variables has the special value
|
||||
* '{{HOST_DOCKER_INTERNAL}}', it will be replaced by
|
||||
* 'host.docker.interal' if we are on Docker, or
|
||||
* 'host.containers.internal' on Podman.
|
||||
*/
|
||||
startHomeserver(opts: string | StartHomeserverOpts): Chainable<HomeserverInstance>;
|
||||
|
||||
/**
|
||||
* Custom command wrapping task:{homeserver}Stop whilst preventing uncaught exceptions
|
||||
* for if Homeserver stopping races with the app's background sync loop.
|
||||
*
|
||||
* @param homeserver the homeserver instance returned by {homeserver}Start (e.g. synapseStart).
|
||||
*/
|
||||
stopHomeserver(homeserver: HomeserverInstance): Chainable<AUTWindow>;
|
||||
|
||||
/**
|
||||
* Register a user on the given Homeserver using the shared registration secret.
|
||||
* @param homeserver the homeserver instance returned by start{Homeserver}
|
||||
* @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(
|
||||
homeserver: HomeserverInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
): Chainable<Credentials>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startHomeserver(opts: string | StartHomeserverOpts): Chainable<HomeserverInstance> {
|
||||
const homeserverName = Cypress.env("HOMESERVER");
|
||||
if (typeof opts === "string") {
|
||||
opts = { template: opts };
|
||||
}
|
||||
|
||||
return cy.task<HomeserverInstance>(homeserverName + "Start", opts, { log: false }).then((x) => {
|
||||
Cypress.log({ name: "startHomeserver", message: `Started homeserver instance ${x.serverId}` });
|
||||
});
|
||||
}
|
||||
|
||||
function stopHomeserver(homeserver?: HomeserverInstance): Chainable<AUTWindow> {
|
||||
if (!homeserver) return;
|
||||
// Navigate away from app to stop the background network requests which will race with Homeserver shutting down
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
win.location.href = "about:blank";
|
||||
const homeserverName = Cypress.env("HOMESERVER");
|
||||
cy.task(homeserverName + "Stop", homeserver.serverId);
|
||||
});
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
homeServer: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function registerUser(
|
||||
homeserver: HomeserverInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
displayName?: string,
|
||||
): Chainable<Credentials> {
|
||||
const url = `${homeserver.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", homeserver.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,
|
||||
password: password,
|
||||
}));
|
||||
}
|
||||
|
||||
Cypress.Commands.add("startHomeserver", startHomeserver);
|
||||
Cypress.Commands.add("stopHomeserver", stopHomeserver);
|
||||
Cypress.Commands.add("registerUser", registerUser);
|
|
@ -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 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,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.
|
||||
*/
|
||||
|
||||
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,45 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
// secret undocumented options to Cypress.log
|
||||
interface LogConfig {
|
||||
/** begin a new log group; remember to match with `groupEnd` */
|
||||
groupStart: boolean;
|
||||
|
||||
/** end a log group that was previously started with `groupStart` */
|
||||
groupEnd: boolean;
|
||||
|
||||
/** suppress regular output: useful for closing a log group without writing another log line */
|
||||
emitOnly: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** collapse the last open log group in the Cypress UI
|
||||
*
|
||||
* Credit to https://j1000.github.io/blog/2022/10/27/enhanced_cypress_logging.html
|
||||
*/
|
||||
export function collapseLastLogGroup() {
|
||||
const openExpanders = window.top.document.getElementsByClassName("command-expander-is-open");
|
||||
const numExpanders = openExpanders.length;
|
||||
const el = openExpanders[numExpanders - 1];
|
||||
if (el) el.parentElement.click();
|
||||
}
|
|
@ -1,169 +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 { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||
import { collapseLastLogGroup } from "./log";
|
||||
|
||||
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
|
||||
* @param userIdPrefix optional prefix to use for the generated user id. If unspecified, `user_` will be
|
||||
* useed.
|
||||
*/
|
||||
initTestUser(
|
||||
homeserver: HomeserverInstance,
|
||||
displayName: string,
|
||||
prelaunchFn?: () => void,
|
||||
userIdPrefix?: string,
|
||||
): 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: HomeserverInstance, username: string, password: string): Chainable<UserCredentials>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
Cypress.Commands.add(
|
||||
"loginUser",
|
||||
(homeserver: HomeserverInstance, username: string, password: string): Chainable<UserCredentials> => {
|
||||
const url = `${homeserver.baseUrl}/_matrix/client/v3/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",
|
||||
(
|
||||
homeserver: HomeserverInstance,
|
||||
displayName: string,
|
||||
prelaunchFn?: () => void,
|
||||
userIdPrefix = "user_",
|
||||
): Chainable<UserCredentials> => {
|
||||
Cypress.log({ name: "initTestUser", groupStart: true });
|
||||
// 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(userIdPrefix);
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy
|
||||
.registerUser(homeserver, username, password, displayName)
|
||||
.then(() => {
|
||||
return cy.loginUser(homeserver, username, password);
|
||||
})
|
||||
.then((response) => {
|
||||
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||
cy.window({ log: false }).then((win) => {
|
||||
// Seed the localStorage with the required credentials
|
||||
win.localStorage.setItem("mx_hs_url", homeserver.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("/", {
|
||||
onBeforeLoad(win) {
|
||||
// reset notification permissions so we have predictable behaviour
|
||||
// of notifications toast
|
||||
// @ts-ignore allow setting default
|
||||
cy.stub(win.Notification, "permission", "default");
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 30000 });
|
||||
})
|
||||
.then(() => {
|
||||
Cypress.log({
|
||||
groupEnd: true,
|
||||
emitOnly: true,
|
||||
});
|
||||
collapseLastLogGroup();
|
||||
})
|
||||
.then(() => ({
|
||||
password,
|
||||
username,
|
||||
accessToken: response.accessToken,
|
||||
userId: response.userId,
|
||||
deviceId: response.deviceId,
|
||||
homeServer: response.homeServer,
|
||||
}));
|
||||
});
|
||||
},
|
||||
);
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import mailhog from "mailhog";
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { Instance } from "../plugins/mailhog";
|
||||
|
||||
export interface Mailhog {
|
||||
api: mailhog.API;
|
||||
instance: Instance;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
startMailhog(): Chainable<Mailhog>;
|
||||
stopMailhog(instance: Mailhog): Chainable<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("startMailhog", (): Chainable<Mailhog> => {
|
||||
return cy.task<Instance>("mailhogStart", { log: false }).then((x) => {
|
||||
Cypress.log({ name: "startHomeserver", message: `Started mailhog instance ${x.containerId}` });
|
||||
return {
|
||||
api: mailhog({
|
||||
host: "localhost",
|
||||
port: x.httpPort,
|
||||
}),
|
||||
instance: x,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("stopMailhog", (mailhog: Mailhog): Chainable<void> => {
|
||||
return cy.task("mailhogStop", mailhog.instance.containerId);
|
||||
});
|
|
@ -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 { 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;
|
||||
allowSpinners?: boolean;
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!options?.allowSpinners) {
|
||||
// Await spinners to vanish
|
||||
cy.get(".mx_Spinner", { log: false }).should("not.exist");
|
||||
// But like really no more spinners please
|
||||
cy.get(".mx_Spinner", { log: false }).should("not.exist");
|
||||
// Await inline spinners to vanish
|
||||
cy.get(".mx_InlineSpinner", { log: false }).should("not.exist");
|
||||
}
|
||||
|
||||
let selector = subject.selector;
|
||||
// cy.findByTestId sets the selector to `findByTestId(<testId>)`
|
||||
// which is not usable as a scope
|
||||
if (selector.startsWith("findByTestId")) {
|
||||
selector = `[data-testid="${subject.attr("data-testid")}"]`;
|
||||
}
|
||||
cy.percySnapshot(name, {
|
||||
domTransformation: (documentClone) => scope(documentClone, 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 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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Utility wrapper around promises to help control flow in tests
|
||||
* Calls `fn` function `tries` times, with a sleep of `interval` between calls.
|
||||
* Ensure you do not rely on any effects of calling any `cy.*` functions within the body of `fn`
|
||||
* as the calls will not happen until after waitForPromise returns.
|
||||
* @param fn the function to retry
|
||||
* @param tries the number of tries to call it
|
||||
* @param interval the time interval between tries
|
||||
*/
|
||||
waitForPromise(fn: () => Promise<unknown>, tries?: number, interval?: number): Chainable<unknown>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function waitForPromise(fn: () => Promise<unknown>, tries = 10, interval = 1000): Chainable<unknown> {
|
||||
return cy.then(
|
||||
() =>
|
||||
new Cypress.Promise(async (resolve, reject) => {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
try {
|
||||
const v = await fn();
|
||||
resolve(v);
|
||||
} catch {
|
||||
await new Cypress.Promise((resolve) => setTimeout(resolve, interval));
|
||||
}
|
||||
}
|
||||
reject();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Cypress.Commands.add("waitForPromise", waitForPromise);
|
||||
|
||||
export {};
|
|
@ -1,190 +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 Loggable = Cypress.Loggable;
|
||||
import Timeoutable = Cypress.Timeoutable;
|
||||
import Withinable = Cypress.Withinable;
|
||||
import Shadow = Cypress.Shadow;
|
||||
import type { SettingLevel } from "../../src/settings/SettingLevel";
|
||||
import ApplicationWindow = Cypress.ApplicationWindow;
|
||||
|
||||
export enum Filter {
|
||||
People = "people",
|
||||
PublicRooms = "public_rooms",
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
/**
|
||||
* Returns the SettingsStore
|
||||
*/
|
||||
getSettingsStore(): Chainable<ApplicationWindow["mxSettingsStore"] | undefined>;
|
||||
/**
|
||||
* 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>>;
|
||||
|
||||
/**
|
||||
* 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>>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
/**
|
||||
* 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>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getSettingsStore", (): Chainable<ApplicationWindow["mxSettingsStore"]> => {
|
||||
return cy.window({ log: false }).then((win) => win.mxSettingsStore);
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"setSettingValue",
|
||||
(name: string, roomId: string, level: SettingLevel, value: any): Chainable<void> => {
|
||||
return cy.getSettingsStore().then((store: ApplicationWindow["mxSettingsStore"]) => {
|
||||
return cy.wrap(store.setValue(name, roomId, level, value));
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("openUserMenu", (): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.findByRole("button", { name: "User menu" }).click();
|
||||
return cy.get(".mx_ContextualMenu");
|
||||
});
|
||||
|
||||
Cypress.Commands.add("openUserSettings", (tab?: string): Chainable<JQuery<HTMLElement>> => {
|
||||
cy.openUserMenu().within(() => {
|
||||
cy.findByRole("menuitem", { name: "All settings" }).click();
|
||||
});
|
||||
return cy.get(".mx_UserSettingsDialog").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.findByRole("button", { name: "Close dialog" }).click();
|
||||
});
|
||||
|
||||
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", options).findByRole("textbox", { name: "Search" });
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
|
||||
// Needed to make this file a module
|
||||
export {};
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
/**
|
||||
* Assert that a toast with the given title exists, and return it
|
||||
*
|
||||
* @param expectedTitle - Expected title of the test
|
||||
* @returns a Chainable for the DOM element of the toast
|
||||
*/
|
||||
export function getToast(expectedTitle: string): Cypress.Chainable<JQuery> {
|
||||
return cy.contains(".mx_Toast_toast h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
Copyright 2022-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.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
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]> }>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 resultArray = [];
|
||||
|
||||
// as each command completes, store the result in the corresponding location of resultArray.
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
commands[i].then((val) => {
|
||||
resultArray[i] = val;
|
||||
});
|
||||
}
|
||||
|
||||
// add an entry to the log which, when clicked, will write the results to the console.
|
||||
Cypress.log({
|
||||
name: "all",
|
||||
consoleProps: () => ({ Results: resultArray }),
|
||||
});
|
||||
|
||||
// return a chainable which wraps the resultArray. Although this doesn't have a direct dependency on the input
|
||||
// commands, cypress won't process it until the commands that precede it on the command queue (which must include
|
||||
// the input commands) have passed.
|
||||
return cy.wrap(resultArray, { log: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if Cypress has been configured to enable rust crypto, and bail out if so.
|
||||
*/
|
||||
export function skipIfRustCrypto() {
|
||||
if (isRustCryptoEnabled()) {
|
||||
cy.log("Skipping due to rust crypto");
|
||||
//@ts-ignore: 'state' is a secret internal command
|
||||
cy.state("runnable").skip();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if Cypress has been configured to enable rust crypto (by checking the environment variable)
|
||||
*/
|
||||
export function isRustCryptoEnabled(): boolean {
|
||||
return !!Cypress.env("RUST_CRYPTO");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise which will resolve when the given event emitter emits a given event
|
||||
*/
|
||||
export function emitPromise(e: EventEmitter, k: string | symbol) {
|
||||
return new Promise((r) => e.once(k, r));
|
||||
}
|
|
@ -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 {
|
||||
/**
|
||||
* Opens the given room by name. The room must be visible in the
|
||||
* room list, but the room list may be folded horizontally, and the
|
||||
* room may contain unread messages.
|
||||
*
|
||||
* @param name The exact room name to find and click on/open.
|
||||
*/
|
||||
viewRoomByName(name: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Opens the given room by room ID.
|
||||
*
|
||||
* This works by browsing to `/#/room/${id}`, so it will also work for room aliases.
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
viewRoomById(id: string): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("viewRoomByName", (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||
// We look for the room inside the room list, which is a tree called Rooms.
|
||||
//
|
||||
// There are 3 cases:
|
||||
// - the room list is folded:
|
||||
// then the aria-label on the room tile is the name (with nothing extra)
|
||||
// - the room list is unfolder and the room has messages:
|
||||
// then the aria-label contains the unread count, but the title of the
|
||||
// div inside the titleContainer equals the room name
|
||||
// - the room list is unfolded and the room has no messages:
|
||||
// then the aria-label is the name and so is the title of a div
|
||||
//
|
||||
// So by matching EITHER title=name OR aria-label=name we find this exact
|
||||
// room in all three cases.
|
||||
return cy.findByRole("tree", { name: "Rooms" }).find(`[title="${name}"],[aria-label="${name}"]`).first().click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("viewRoomById", (id: string): void => {
|
||||
cy.visit(`/#/room/${id}`);
|
||||
});
|
||||
|
||||
// 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,13 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2016",
|
||||
"jsx": "react",
|
||||
"lib": ["es2021", "dom", "dom.iterable"],
|
||||
"types": ["cypress", "cypress-axe", "@percy/cypress", "@testing-library/cypress"],
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "es2022"
|
||||
},
|
||||
"include": ["**/*.ts"]
|
||||
}
|
|
@ -20,4 +20,4 @@
|
|||
|
||||
# Testing
|
||||
|
||||
- [Cypress end to end](cypress.md)
|
||||
- [Playwright end to end](playwright.md)
|
||||
|
|
281
docs/cypress.md
281
docs/cypress.md
|
@ -1,281 +0,0 @@
|
|||
# Cypress in Element Web
|
||||
|
||||
# 🚨 We are moving away from Cypress in favour of Playwright.
|
||||
|
||||
Please do not write any new tests in Cypress and check out [the Playwright docs](playwright.md).
|
||||
|
||||
## Contents
|
||||
|
||||
- How to run the tests
|
||||
- How the tests work
|
||||
- How to write great Cypress tests
|
||||
- Visual testing
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Our Cypress tests run automatically as part of our CI along with our other tests,
|
||||
on every pull request and on every merge to develop & master.
|
||||
|
||||
However the Cypress tests are run, an element-web must be running on
|
||||
http://localhost:8080 (this is configured in `cypress.json`) - this is what will
|
||||
be tested. When running Cypress tests yourself, the standard `yarn start` from the
|
||||
element-web project is fine: leave it running it a different terminal as you would
|
||||
when developing.
|
||||
|
||||
The tests use Docker to launch Homeserver (Synapse or Dendrite) instances to test against, so you'll also
|
||||
need to have Docker installed and working in order to run the Cypress tests.
|
||||
|
||||
There are a few different ways to run the tests yourself. The simplest is to run:
|
||||
|
||||
```
|
||||
docker pull matrixdotorg/synapse:develop
|
||||
yarn run test:cypress
|
||||
```
|
||||
|
||||
This will run the Cypress tests once, non-interactively.
|
||||
|
||||
Note: you don't need to run the `docker pull` command every time, but you should
|
||||
do it regularly to ensure you are running against an up-to-date Synapse.
|
||||
|
||||
You can also run individual tests this way too, as you'd expect:
|
||||
|
||||
```
|
||||
yarn run test:cypress --spec cypress/e2e/1-register/register.spec.ts
|
||||
```
|
||||
|
||||
Cypress also has its own UI that you can use to run and debug the tests.
|
||||
To launch it:
|
||||
|
||||
```
|
||||
yarn run test:cypress:open
|
||||
```
|
||||
|
||||
### Matching the CI environment
|
||||
|
||||
In our Continuous Integration environment, we run the Cypress tests in the
|
||||
Chrome browser, and with the latest Synapse image from Docker Hub.
|
||||
|
||||
In some rare cases, tests behave differently between different browsers, so if
|
||||
you see CI failures for the Cypress tests, but those tests work OK on your local
|
||||
machine, try running them in Chrome like this:
|
||||
|
||||
```bash
|
||||
yarn run test:cypress --browser=chrome
|
||||
```
|
||||
|
||||
(Use `--browser=chromium` if you'd prefer to use Chromium.)
|
||||
|
||||
If you launch the interactive UI you can choose the browser you want to use. To
|
||||
match the CI setup, choose Chrome.
|
||||
|
||||
Note that you will need to have Chrome installed on your system to run the tests
|
||||
inside those browsers, whereas the default is to use Electron, which is included
|
||||
within the Cypress dependency.
|
||||
|
||||
Another cause of inconsistency between local and CI is the Synapse version. The
|
||||
first time you run the tests, they automatically fetch the latest Docker image
|
||||
of Synapse, but this won't update again unless you do it explicitly. To update
|
||||
the Synapse you are using, run:
|
||||
|
||||
```
|
||||
docker pull matrixdotorg/synapse:develop
|
||||
```
|
||||
|
||||
and then run the tests as normal.
|
||||
|
||||
### Running with Rust cryptography
|
||||
|
||||
`matrix-js-sdk` is currently in the
|
||||
[process](https://github.com/vector-im/element-web/issues/21972) of being
|
||||
updated to replace its end-to-end encryption implementation to use the [Matrix
|
||||
Rust SDK](https://github.com/matrix-org/matrix-rust-sdk). This is not currently
|
||||
enabled by default, but it is possible to have Cypress configure Element to use
|
||||
the Rust crypto implementation by setting the environment variable
|
||||
`CYPRESS_RUST_CRYPTO=1`.
|
||||
|
||||
## How the Tests Work
|
||||
|
||||
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
|
||||
as is typical for Cypress tests. Likewise, tests live in `cypress/e2e`.
|
||||
|
||||
`cypress/plugins/synapsedocker` contains a Cypress plugin that starts instances
|
||||
of Synapse in Docker containers. These synapses are what Element-web runs against
|
||||
in the Cypress tests.
|
||||
|
||||
Synapse can be launched with different configurations in order to test element
|
||||
in different configurations. `cypress/plugins/synapsedocker/templates` contains
|
||||
template configuration files for each different configuration.
|
||||
|
||||
Each test suite can then launch whatever Synapse instances it needs in whatever
|
||||
configurations.
|
||||
|
||||
Note that although tests should stop the Homeserver instances after running and the
|
||||
plugin also stop any remaining instances after all tests have run, it is possible
|
||||
to be left with some stray containers if, for example, you terminate a test such
|
||||
that the `after()` does not run and also exit Cypress uncleanly. All the containers
|
||||
it starts are prefixed, so they are easy to recognise. They can be removed safely.
|
||||
|
||||
After each test run, logs from the Synapse instances are saved in `cypress/synapselogs`
|
||||
with each instance in a separate directory named after its ID. These logs are removed
|
||||
at the start of each test run.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Mostly this is the same advice as for writing any other Cypress test: the Cypress
|
||||
docs are well worth a read if you're not already familiar with Cypress testing, eg.
|
||||
https://docs.cypress.io/guides/references/best-practices. To avoid your tests being
|
||||
flaky it is also recommended to give https://docs.cypress.io/guides/core-concepts/retry-ability
|
||||
a read.
|
||||
|
||||
### Getting a Synapse
|
||||
|
||||
The key difference is in starting Synapse instances. Tests use this plugin via
|
||||
`cy.startHomeserver()` to provide a Homeserver instance to log into:
|
||||
|
||||
```javascript
|
||||
cy.startHomeserver("consent").then((result) => {
|
||||
homeserver = result;
|
||||
});
|
||||
```
|
||||
|
||||
This returns an object with information about the Homeserver instance, including what port
|
||||
it was started on and the ID that needs to be passed to shut it down again. It also
|
||||
returns the registration shared secret (`registrationSecret`) that can be used to
|
||||
register users via the REST API. The Homeserver has been ensured ready to go by awaiting
|
||||
its internal health-check.
|
||||
|
||||
Homeserver instances should be reasonably cheap to start (you may see the first one take a
|
||||
while as it pulls the Docker image), so it's generally expected that tests will start a
|
||||
Homeserver instance for each test suite, i.e. in `before()`, and then tear it down in `after()`.
|
||||
|
||||
To later destroy your Homeserver you should call `stopHomeserver`, passing the HomeserverInstance
|
||||
object you received when starting it.
|
||||
|
||||
```javascript
|
||||
cy.stopHomeserver(homeserver);
|
||||
```
|
||||
|
||||
### Synapse Config Templates
|
||||
|
||||
When a Synapse instance is started, it's given a config generated from one of the config
|
||||
templates in `cypress/plugins/synapsedocker/templates`. There are a couple of special files
|
||||
in these templates:
|
||||
|
||||
- `homeserver.yaml`:
|
||||
Template substitution happens in this file. Template variables are:
|
||||
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
||||
- `MACAROON_SECRET_KEY`: Generated each time for security
|
||||
- `FORM_SECRET`: Generated each time for security
|
||||
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
||||
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
||||
Config templates should not contain a signing key and instead assume that one will exist
|
||||
in this file.
|
||||
|
||||
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
|
||||
in a template can be referenced in the config as `/data/foo.html`.
|
||||
|
||||
### Logging In
|
||||
|
||||
There exists a basic utility to start the app with a random user already logged in:
|
||||
|
||||
```javascript
|
||||
cy.initTestUser(homeserver, "Jeff");
|
||||
```
|
||||
|
||||
It takes the HomeserverInstance you received from `startHomeserver` and a display name for your test user.
|
||||
This custom command will register a random userId using the registrationSecret with a random password
|
||||
and the given display name. The returned Chainable will contain details about the credentials for if
|
||||
they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
|
||||
and the app loaded (path `/`).
|
||||
|
||||
The internals of how this custom command run may be swapped out later,
|
||||
but the signature can be maintained for simpler maintenance.
|
||||
|
||||
### Joining a Room
|
||||
|
||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
||||
way to do this may be to get an access token for the user and use this to create a room with the REST
|
||||
API before logging the user in. You can make use of `cy.getBot(homeserver)` and `cy.getClient()` to do this.
|
||||
|
||||
### Convenience APIs
|
||||
|
||||
We should probably end up with convenience APIs that wrap the homeserver creation, logging in and room
|
||||
creation that can be called to set up tests.
|
||||
|
||||
### Try to write tests from the users's perspective
|
||||
|
||||
Like for instance a user will not look for a button by querying a CSS selector. Instead you should work
|
||||
with roles / labels etc.. You can make use of `cy.findBy…` queries provided by
|
||||
[Cypress Testing Library](https://github.com/testing-library/cypress-testing-library).
|
||||
|
||||
### Using matrix-js-sdk
|
||||
|
||||
Due to the way we run the Cypress tests in CI, at this time you can only use the matrix-js-sdk module
|
||||
exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
|
||||
This may be revisited in the future.
|
||||
|
||||
## Good Test Hygiene
|
||||
|
||||
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
|
||||
already familiar with Cypress.
|
||||
|
||||
1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's
|
||||
wrong when they fail.
|
||||
1. Don't depend on state from other tests: any given test should be able to run in isolation.
|
||||
1. Try to avoid driving the UI for anything other than the UI you're trying to test. e.g. if you're
|
||||
testing that the user can send a reaction to a message, it's best to send a message using a REST
|
||||
API, then react to it using the UI, rather than using the element-web UI to send the message.
|
||||
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
|
||||
all assertions are retired until they either pass or time out, so you should never need to
|
||||
manually wait for an element.
|
||||
- For example, for asserting about editing an already-edited message, you can't wait for the
|
||||
'edited' element to appear as there was already one there, but you can assert that the body
|
||||
of the message is what is should be after the second edit and this assertion will pass once
|
||||
it becomes true. You can then assert that the 'edited' element is still in the DOM.
|
||||
- You can also wait for other things like network requests in the
|
||||
browser to complete (https://docs.cypress.io/guides/guides/network-requests#Waiting).
|
||||
Needing to wait for things can also be because of race conditions in the app itself, which ideally
|
||||
shouldn't be there!
|
||||
|
||||
This is a small selection - the Cypress best practices guide, linked above, has more good advice, and we
|
||||
should generally try to adhere to them.
|
||||
|
||||
## Screenshot testing with Percy
|
||||
|
||||
**⚠️ Percy is disabled while we're figuring out https://github.com/vector-im/wat-internal/issues/36**
|
||||
**and https://github.com/vector-im/wat-internal/issues/56. We're hoping to turn it back on or switch**
|
||||
**to an alternative in the future.**
|
||||
|
||||
We also support visual testing via [Percy](https://percy.io). Within many of our
|
||||
Cypress tests you can see lines calling `cy.percySnapshot()`. This creates a
|
||||
screenshot and uses Percy to check whether it has changed from the last time
|
||||
this test was run.
|
||||
|
||||
It can help to pass `percyCSS` in as the 2nd argument to `percySnapshot` to hide
|
||||
elements that vary (e.g. timestamps). See the existing code for examples of
|
||||
this. (Note: it is also possible for team members to mark certain parts of a
|
||||
screenshot to be ignored. This is done within the Percy UI.)
|
||||
|
||||
Percy screenshots are created using custom renderers based on Safari, Firefox,
|
||||
Chrome and Edge. Each `percySnapshot` actually creates 8 screenshots (4
|
||||
browsers, 2 sizes). Since we have a limited budget for Percy screenshots, by
|
||||
default we only run Percy once per day against the `develop` branch, based on a
|
||||
nightly build at approximately 04:00 UTC every day. (The schedule is defined in
|
||||
[element-web.yaml](../.github/workflows/element-web.yaml) and the Percy tests are
|
||||
enabled/disabled in [cypress.yaml](../.github/workflows/cypress.yaml).)
|
||||
|
||||
If your pull request makes visual changes, you are encouraged to request Percy
|
||||
to run by adding the label `X-Needs-Percy` to the PR, these will only run in
|
||||
the merge queue to save snapshots. This will help us find any
|
||||
visual bugs or validate visual changes at the time they are made, instead of
|
||||
having to figure it out later after the nightly build. If you don't have
|
||||
permission to add a label, please ask your reviewer to do it. Note: it's best to
|
||||
add this label when the change is nearly ready, because the screenshots will be
|
||||
re-created every time you make a change to your PR.
|
||||
|
||||
Some UI elements render differently between test runs, such as BaseAvatar when
|
||||
there is no avatar set, choosing a colour from the theme palette based on the
|
||||
hash of the user/room's Matrix ID. To avoid this creating flaky tests we can use
|
||||
the `@media only percy` CSS query to override the variable colour into a fixed one
|
||||
for tests where it is not feasible to fix the underlying identifiers issued by the
|
||||
server. See https://docs.percy.io/docs/percy-specific-css#percy-css-media-query.
|
18
package.json
18
package.json
|
@ -46,13 +46,11 @@
|
|||
"start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build",
|
||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style && yarn lint:workflows",
|
||||
"lint:js": "eslint --max-warnings 0 src test cypress playwright && prettier --check .",
|
||||
"lint:js-fix": "eslint --fix src test cypress playwright && prettier --log-level=warn --write .",
|
||||
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p cypress && tsc --noEmit --jsx react -p playwright",
|
||||
"lint:js": "eslint --max-warnings 0 src test playwright && prettier --check .",
|
||||
"lint:js-fix": "eslint --fix src test playwright && prettier --log-level=warn --write .",
|
||||
"lint:types": "tsc --noEmit --jsx react && tsc --noEmit --jsx react -p playwright",
|
||||
"lint:style": "stylelint \"res/css/**/*.pcss\"",
|
||||
"test": "jest",
|
||||
"test:cypress": "cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"test:playwright": "playwright test",
|
||||
"test:playwright:open": "yarn test:playwright --ui",
|
||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
||||
|
@ -152,12 +150,8 @@
|
|||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@knapsack-pro/cypress": "^8.0.1",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@percy/cli": "^1.11.0",
|
||||
"@percy/cypress": "^3.1.2",
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@testing-library/cypress": "^9.0.0",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
|
@ -194,12 +188,6 @@
|
|||
"axe-core": "4.8.3",
|
||||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^7.0.0",
|
||||
"cypress": "^12.0.0",
|
||||
"cypress-axe": "^1.0.0",
|
||||
"cypress-multi-reporters": "^1.6.1",
|
||||
"cypress-plugin-init": "^0.0.8",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"cypress-terminal-report": "^5.3.2",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
|
|
|
@ -139,12 +139,12 @@ test.describe("Audio player", () => {
|
|||
});
|
||||
|
||||
test("should be correctly rendered - light theme", async ({ page, app }) => {
|
||||
await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme)");
|
||||
});
|
||||
|
||||
test("should be correctly rendered - light theme with monospace font", async ({ page, app }) => {
|
||||
await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (light theme, monospace font)", true); // Enable monospace
|
||||
});
|
||||
|
@ -160,7 +160,7 @@ test.describe("Audio player", () => {
|
|||
|
||||
await app.closeDialog();
|
||||
|
||||
await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (high contrast)");
|
||||
});
|
||||
|
@ -169,13 +169,13 @@ test.describe("Audio player", () => {
|
|||
// Enable dark theme
|
||||
await app.settings.setValue("theme", null, SettingLevel.ACCOUNT, "dark");
|
||||
|
||||
await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
|
||||
await takeSnapshots(page, app, "Selected EventTile of audio player (dark theme)");
|
||||
});
|
||||
|
||||
test("should play an audio file", async ({ page, app }) => {
|
||||
await uploadFile(page, "cypress/fixtures/1sec.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container");
|
||||
|
@ -197,7 +197,7 @@ test.describe("Audio player", () => {
|
|||
});
|
||||
|
||||
test("should support downloading an audio file", async ({ page, app }) => {
|
||||
await uploadFile(page, "cypress/fixtures/1sec.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
|
||||
|
@ -212,7 +212,7 @@ test.describe("Audio player", () => {
|
|||
});
|
||||
|
||||
test("should support replying to audio file with another audio file", async ({ page, app }) => {
|
||||
await uploadFile(page, "cypress/fixtures/1sec.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
@ -223,7 +223,7 @@ test.describe("Audio player", () => {
|
|||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "cypress/fixtures/1sec.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||
|
@ -250,7 +250,7 @@ test.describe("Audio player", () => {
|
|||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
};
|
||||
|
||||
await uploadFile(page, "cypress/fixtures/upload-first.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
@ -258,7 +258,7 @@ test.describe("Audio player", () => {
|
|||
await clickButtonReply();
|
||||
|
||||
// Reply to the player with another audio file
|
||||
await uploadFile(page, "cypress/fixtures/upload-second.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
|
@ -266,7 +266,7 @@ test.describe("Audio player", () => {
|
|||
await clickButtonReply();
|
||||
|
||||
// Reply to the player with yet another audio file to create a reply chain
|
||||
await uploadFile(page, "cypress/fixtures/upload-third.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||
|
@ -299,7 +299,7 @@ test.describe("Audio player", () => {
|
|||
});
|
||||
|
||||
test("should be rendered, play, and support replying on a thread", async ({ page, app }) => {
|
||||
await uploadFile(page, "cypress/fixtures/1sec-long-name-audio-file.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec-long-name-audio-file.ogg");
|
||||
|
||||
// On the main timeline
|
||||
const messageList = page.locator(".mx_RoomView_MessageList");
|
||||
|
|
|
@ -58,9 +58,9 @@ test.describe("FilePanel", () => {
|
|||
|
||||
test("should list tiles on the panel", async ({ page }) => {
|
||||
// Upload multiple files
|
||||
await uploadFile(page, "cypress/fixtures/riot.png"); // Image
|
||||
await uploadFile(page, "cypress/fixtures/1sec.ogg"); // Audio
|
||||
await uploadFile(page, "cypress/fixtures/matrix-org-client-versions.json"); // JSON
|
||||
await uploadFile(page, "playwright/sample-files/riot.png"); // Image
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg"); // Audio
|
||||
await uploadFile(page, "playwright/sample-files/matrix-org-client-versions.json"); // JSON
|
||||
|
||||
const roomViewBody = page.locator(".mx_RoomView_body");
|
||||
// Assert that all of the file were uploaded and rendered
|
||||
|
@ -143,7 +143,7 @@ test.describe("FilePanel", () => {
|
|||
|
||||
test("should render the audio player and play the audio file on the panel", async ({ page }) => {
|
||||
// Upload an image file
|
||||
await uploadFile(page, "cypress/fixtures/1sec.ogg");
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
const audioBody = page.locator(
|
||||
".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
|
||||
|
@ -178,7 +178,7 @@ test.describe("FilePanel", () => {
|
|||
const size = "1.12 KB"; // actual file size in kibibytes (1024 bytes)
|
||||
|
||||
// Upload a file
|
||||
await uploadFile(page, "cypress/fixtures/matrix-org-client-versions.json");
|
||||
await uploadFile(page, "playwright/sample-files/matrix-org-client-versions.json");
|
||||
|
||||
const tile = page.locator(".mx_FilePanel .mx_EventTile");
|
||||
// Assert that the file size is displayed in kibibytes, not kilobytes (1000 bytes)
|
||||
|
@ -192,7 +192,7 @@ test.describe("FilePanel", () => {
|
|||
test.describe("download", () => {
|
||||
test("should download an image via the link on the panel", async ({ page, context }) => {
|
||||
// Upload an image file
|
||||
await uploadFile(page, "cypress/fixtures/riot.png");
|
||||
await uploadFile(page, "playwright/sample-files/riot.png");
|
||||
|
||||
// Detect the image file on the panel
|
||||
const imageBody = page.locator(
|
||||
|
|
|
@ -71,7 +71,7 @@ test.describe("Spaces", () => {
|
|||
|
||||
await contextMenu
|
||||
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.setInputFiles("cypress/fixtures/riot.png");
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
await contextMenu.getByRole("textbox", { name: "Name" }).fill("Let's have a Riot");
|
||||
await expect(contextMenu.getByRole("textbox", { name: "Address" })).toHaveValue("lets-have-a-riot");
|
||||
await contextMenu.getByRole("textbox", { name: "Description" }).fill("This is a space to reminisce Riot.im!");
|
||||
|
@ -102,7 +102,7 @@ test.describe("Spaces", () => {
|
|||
|
||||
await menu
|
||||
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.setInputFiles("cypress/fixtures/riot.png");
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
await menu.getByRole("textbox", { name: "Name" }).fill("This is not a Riot");
|
||||
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
|
||||
await menu.getByRole("textbox", { name: "Description" }).fill("This is a private space of mourning Riot.im...");
|
||||
|
@ -147,7 +147,7 @@ test.describe("Spaces", () => {
|
|||
|
||||
await menu
|
||||
.locator('.mx_SpaceBasicSettings_avatarContainer input[type="file"]')
|
||||
.setInputFiles("cypress/fixtures/riot.png");
|
||||
.setInputFiles("playwright/sample-files/riot.png");
|
||||
await expect(menu.getByRole("textbox", { name: "Address" })).not.toBeVisible();
|
||||
await menu.getByRole("textbox", { name: "Description" }).fill("This is a personal space to mourn Riot.im...");
|
||||
await menu.getByRole("textbox", { name: "Name" }).fill("This is my Riot");
|
||||
|
|
|
@ -31,8 +31,8 @@ const AVATAR_SIZE = 30;
|
|||
const AVATAR_RESIZE_METHOD = "crop";
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const OLD_AVATAR = fs.readFileSync("cypress/fixtures/riot.png");
|
||||
const NEW_AVATAR = fs.readFileSync("cypress/fixtures/element.png");
|
||||
const OLD_AVATAR = fs.readFileSync("playwright/sample-files/riot.png");
|
||||
const NEW_AVATAR = fs.readFileSync("playwright/sample-files/element.png");
|
||||
const OLD_NAME = "Alan";
|
||||
const NEW_NAME = "Alan (away)";
|
||||
|
||||
|
@ -139,7 +139,7 @@ test.describe("Timeline", () => {
|
|||
),
|
||||
).toBeVisible();
|
||||
|
||||
// wait for the date separator to appear to have a stable percy snapshot
|
||||
// wait for the date separator to appear to have a stable screenshot
|
||||
await expect(page.locator(".mx_TimelineSeparator")).toHaveText("today");
|
||||
|
||||
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("configured-room-irc-layout.png");
|
||||
|
@ -684,7 +684,7 @@ test.describe("Timeline", () => {
|
|||
// Upload a file from the message composer
|
||||
await page
|
||||
.locator(".mx_MessageComposer_actions input[type='file']")
|
||||
.setInputFiles("cypress/fixtures/matrix-org-client-versions.json");
|
||||
.setInputFiles("playwright/sample-files/matrix-org-client-versions.json");
|
||||
|
||||
// Click "Upload" button
|
||||
await page.locator(".mx_Dialog").getByRole("button", { name: "Upload" }).click();
|
||||
|
@ -707,7 +707,7 @@ test.describe("Timeline", () => {
|
|||
"**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
|
||||
async (route) => {
|
||||
await route.fulfill({
|
||||
path: "cypress/fixtures/riot.png",
|
||||
path: "playwright/sample-files/riot.png",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -1048,7 +1048,7 @@ test.describe("Timeline", () => {
|
|||
.getByText(OLD_NAME + " created and configured the room."),
|
||||
).toBeVisible();
|
||||
|
||||
// Set the display name to "LONG_STRING 2" in order to avoid a warning in Percy tests from being triggered
|
||||
// Set the display name to "LONG_STRING 2" in order to avoid screenshot tests from failing
|
||||
// due to the generated random mxid being displayed inside the GELS summary.
|
||||
await app.client.setDisplayName(`${LONG_STRING} 2`);
|
||||
|
||||
|
@ -1089,7 +1089,7 @@ test.describe("Timeline", () => {
|
|||
|
||||
// Make sure the strings do not overflow on IRC layout
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
// Scroll to the bottom to have Percy take a snapshot of the whole viewport
|
||||
// Scroll to the bottom to take a snapshot of the whole viewport
|
||||
await app.timeline.scrollToBottom();
|
||||
// Assert that both avatar in the introduction and the last message are visible at the same time
|
||||
await expect(page.locator(".mx_NewRoomIntro .mx_BaseAvatar")).toBeVisible();
|
||||
|
|
|
@ -59,7 +59,7 @@ export class SlidingSyncProxy {
|
|||
|
||||
const postgresId = await this.postgresDocker.run({
|
||||
image: "postgres",
|
||||
containerName: "react-sdk-cypress-sliding-sync-postgres",
|
||||
containerName: "react-sdk-playwright-sliding-sync-postgres",
|
||||
params: ["--rm", "-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`],
|
||||
});
|
||||
|
||||
|
@ -72,7 +72,7 @@ export class SlidingSyncProxy {
|
|||
console.log(new Date(), "starting proxy container...", SLIDING_SYNC_PROXY_TAG);
|
||||
const containerId = await this.proxyDocker.run({
|
||||
image: "ghcr.io/matrix-org/sliding-sync:" + SLIDING_SYNC_PROXY_TAG,
|
||||
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
||||
containerName: "react-sdk-playwright-sliding-sync-proxy",
|
||||
params: [
|
||||
"--rm",
|
||||
"-p",
|
||||
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
@ -141,16 +141,3 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only Percy {
|
||||
/* Remove the list style in percy tests for screenshot consistency */
|
||||
:is(ul, ol) {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
list-style: none !important;
|
||||
|
||||
.mx_EventTile_last {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,12 +5,12 @@ sonar.organization=matrix-org
|
|||
#sonar.sourceEncoding=UTF-8
|
||||
|
||||
sonar.sources=src,res
|
||||
sonar.tests=test,cypress
|
||||
sonar.tests=test,playwright
|
||||
sonar.exclusions=__mocks__,docs
|
||||
|
||||
sonar.cpd.exclusions=src/i18n/strings/*.json
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
# instrumentation is disabled on SessionLock
|
||||
sonar.coverage.exclusions=test/**/*,cypress/**/*,src/components/views/dialogs/devtools/**/*,src/utils/SessionLock.ts
|
||||
sonar.coverage.exclusions=test/**/*,playwright/**/*,src/components/views/dialogs/devtools/**/*,src/utils/SessionLock.ts
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
|
|
|
@ -481,7 +481,7 @@ class NotifierClass {
|
|||
const room = MatrixClientPeg.safeGet().getRoom(roomId);
|
||||
if (!room) {
|
||||
// e.g we are in the process of joining a room.
|
||||
// Seen in the cypress lazy-loading test.
|
||||
// Seen in the Playwright lazy-loading test.
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -250,7 +250,7 @@ export default class ChangePassword extends React.Component<IProps, IState> {
|
|||
const newPassword = this.state.newPassword;
|
||||
const confirmPassword = this.state.newPasswordConfirm;
|
||||
try {
|
||||
// TODO: We can remove this check (but should add some Cypress tests to
|
||||
// TODO: We can remove this check (but should add some Playwright tests to
|
||||
// sanity check this flow). This logic is redundant with the input field
|
||||
// validation we do and `verifyFieldsBeforeSubmit()` above. See
|
||||
// https://github.com/matrix-org/matrix-react-sdk/pull/10615#discussion_r1167364214
|
||||
|
|
Loading…
Reference in New Issue