Port remaining Puppeteer tests over to Cypress (#9104)

* Port remaining Puppeteer tests over to Cypress

* Remove puppeteer support files

* Fix lifecycle matrixclientpeg setup race condition

* Alternative solution to the lifecycle problem

* Dismiss the notifications toast
This commit is contained in:
Michael Telatynski 2022-07-29 15:03:25 +01:00 committed by GitHub
parent 1e4c336fed
commit f566c600e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 105 additions and 4524 deletions

View file

@ -1,57 +0,0 @@
name: End-to-end Tests
on:
# These tests won't work for non-develop branches at the moment as they
# won't pull in the right versions of other repos, so they're only enabled
# on develop.
push:
branches: [ develop, master ]
pull_request:
branches: [ develop ]
repository_dispatch:
types: [ upstream-sdk-notify ]
env:
# These must be set for fetchdep.sh to get the right branch
REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
end-to-end:
runs-on: ubuntu-latest
container: vectorim/element-web-ci-e2etests-env:latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- name: Prepare End-to-End tests
run: ./scripts/ci/prepare-end-to-end-tests.sh
- name: Run End-to-End tests
run: ./scripts/ci/run-end-to-end-tests.sh
- name: Archive logs
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
path: |
test/end-to-end-tests/logs/**/*
test/end-to-end-tests/synapse/installations/consent/homeserver.log
retention-days: 14
- name: Store benchmark result
if: github.ref == 'refs/heads/develop' && github.repository == 'matrix-org/matrix-react-sdk'
uses: matrix-org/github-action-benchmark@jsperfentry-1
with:
tool: 'jsperformanceentry'
output-file-path: test/end-to-end-tests/performance-entries.json
# This is the default dashboard path. It's included here anyway to
# make the difference from the Cypress variant in
# `element-build-and-test.yaml` more obvious.
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/dev/bench/
benchmark-data-dir-path: dev/bench
fail-on-alert: false
comment-on-alert: false
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
auto-push: ${{ github.ref == 'refs/heads/develop' }}

View file

@ -0,0 +1,98 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/// <reference types="cypress" />
import { SynapseInstance } from "../../plugins/synapsedocker";
import Chainable = Cypress.Chainable;
function assertNoToasts(): void {
cy.get(".mx_Toast_toast").should("not.exist");
}
function getToast(expectedTitle: string): Chainable<JQuery> {
return cy.get(".mx_Toast_toast").contains("h2", expectedTitle).should("exist").closest(".mx_Toast_toast");
}
function acceptToast(expectedTitle: string): void {
getToast(expectedTitle).within(() => {
cy.get(".mx_Toast_buttons .mx_AccessibleButton_kind_primary").click();
});
}
function rejectToast(expectedTitle: string): void {
getToast(expectedTitle).within(() => {
cy.get(".mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline").click();
});
}
describe("Analytics Toast", () => {
let synapse: SynapseInstance;
afterEach(() => {
cy.stopSynapse(synapse);
});
it("should not show an analytics toast if config has nothing about posthog", () => {
cy.intercept("/config.json?cachebuster=*", req => {
req.continue(res => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { posthog, ...body } = res.body;
res.send(200, body);
});
});
cy.startSynapse("default").then(data => {
synapse = data;
cy.initTestUser(synapse, "Tod");
});
rejectToast("Notifications");
assertNoToasts();
});
describe("with posthog enabled", () => {
beforeEach(() => {
cy.intercept("/config.json?cachebuster=*", req => {
req.continue(res => {
res.send(200, {
...res.body,
posthog: {
project_api_key: "foo",
api_host: "bar",
},
});
});
});
cy.startSynapse("default").then(data => {
synapse = data;
cy.initTestUser(synapse, "Tod");
rejectToast("Notifications");
});
});
it("should show an analytics toast which can be accepted", () => {
acceptToast("Help improve Element");
assertNoToasts();
});
it("should show an analytics toast which can be rejected", () => {
rejectToast("Help improve Element");
assertNoToasts();
});
});
});

View file

@ -52,7 +52,6 @@
"test": "jest",
"test:cypress": "cypress run",
"test:cypress:open": "cypress open",
"test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080",
"coverage": "yarn test --coverage"
},
"dependencies": {

View file

@ -1,27 +0,0 @@
#!/bin/bash
set -ev
handle_error() {
EXIT_CODE=$?
exit $EXIT_CODE
}
trap 'handle_error' ERR
echo "--- Building Element"
scripts/ci/layered.sh
cd element-web
element_web_dir=`pwd`
CI_PACKAGE=true yarn build
cd ..
# prepare end to end tests
pushd test/end-to-end-tests
ln -s $element_web_dir element/element-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
echo "--- Install synapse & other dependencies"
./install.sh
# install static webserver to server symlinked local copy of element
./element/install-webserver.sh
popd

View file

@ -1,19 +0,0 @@
#!/bin/bash
set -ev
handle_error() {
EXIT_CODE=$?
exit $EXIT_CODE
}
trap 'handle_error' ERR
# run end to end tests
pushd test/end-to-end-tests
rm -r logs || true
mkdir logs
echo "--- Running end-to-end tests"
TESTS_STARTED=1
./run.sh --no-sandbox --log-directory logs/
popd

View file

@ -635,8 +635,8 @@ async function doSetLoggedIn(
}
dis.fire(Action.OnLoggedIn);
await startMatrixClient(/*startSyncing=*/!softLogout);
return client;
}

View file

@ -1326,11 +1326,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.showScreenAfterLogin();
}
// Will be moved to a pre-login flow as well
if (PosthogAnalytics.instance.isEnabled() && SettingsStore.isLevelSupported(SettingLevel.ACCOUNT)) {
this.initPosthogAnalyticsToast();
}
if (SdkConfig.get("mobile_guide_toast")) {
// The toast contains further logic to detect mobile platforms,
// check if it has been dismissed before, etc.
@ -1646,6 +1641,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// we implement more settings.
cli.setGlobalErrorOnUnknownDevices(false);
}
// Cannot be done in OnLoggedIn as at that point the AccountSettingsHandler doesn't yet have a client
// Will be moved to a pre-login flow as well
if (PosthogAnalytics.instance.isEnabled() && SettingsStore.isLevelSupported(SettingLevel.ACCOUNT)) {
this.initPosthogAnalyticsToast();
}
}
public showScreen(screen: string, params?: {[key: string]: any}) {

View file

@ -1,7 +0,0 @@
node_modules
*.png
element/env
performance-entries.json
lib
logs
homeserver.log

View file

@ -1,49 +0,0 @@
# Matrix React SDK End-to-End tests
This directory contains tests for matrix-react-sdk. The tests fire up a headless Chrome and simulate user interaction (end-to-end). Note that end-to-end has little to do with the end-to-end encryption Matrix supports, just that we test the full stack, going from user interaction to expected DOM in the browser.
## Setup
Run `./install.sh`. This will:
- install Synapse, fetches the develop branch at the moment. If anything fails here, please refer to the Synapse README to see if you're missing one of the prerequisites.
- install Element Web, this fetches the develop branch at the moment.
- install dependencies (will download copy of Chrome)
## Running the tests
Run tests with `./run.sh`.
### Debug tests locally.
`./run.sh` will run the tests against the Element copy present in `element/element-web` served by a static Python HTTP server. You can symlink your `element-web` develop copy here but that doesn't work well with Webpack recompiling. You can run the test runner directly and specify parameters to get more insight into a failure or run the tests against your local Webpack server.
```
./synapse/stop.sh && \
./synapse/start.sh && \
node start.js <parameters>
```
It's important to always stop and start Synapse before each run of the tests to clear the in-memory SQLite database it uses, as the tests assume a blank slate.
start.js accepts these parameters (and more, see `node start.js --help`) that can help running the tests locally:
- `--app-url <url>` don't use the Element Web copy and static server provided by the tests, but use a running server like the Webpack watch server to run the tests against.
- `--slow-mo` type at a human speed, useful with `--windowed`.
- `--throttle-cpu <factor>` throttle cpu in the browser by the given factor. Useful to reproduce failures because of insufficient timeouts happening on the slower CI server.
- `--windowed` run the tests in an actual browser window Try to limit interacting with the windows while the tests are running. Hovering over the window tends to fail the tests, dragging the title bar should be fine though.
- `--dev-tools` open the devtools in the browser window, only applies if `--windowed` is set as well.
For god level debug (e.g. for debugging slow tests):
`env DEBUG="puppeteer:*" ./test/end-to-end-tests/run.sh --app-url http://localhost:8080 --log-directory `pwd`/logs --dev-tools --windowed` 2>&1 | cat
(piping everything through cat means you get proper timestamps on the debugging, and the chromiums hang around at the end)
Developer Guide
===============
Please follow the standard Matrix contributor's guide:
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
Please follow the Matrix JS/React code style as per:
https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md

View file

@ -1,8 +0,0 @@
- join a peekable room by directory
- join a peekable room by invite
- join a non-peekable room by directory
- join a non-peekable room by invite
- leave a room and check we move to the "next" room
- get kicked from a room and check that we show the correct message
- get banned "

View file

@ -1,45 +0,0 @@
# Running the end-to-end tests on Windows
Windows is not the best platform to run the tests on, but if you have to, enable Windows Subsystem for Linux (WSL)
and start following these steps to get going:
1. Navigate to your working directory (`cd /mnt/c/users/travisr/whatever/matrix-react-sdk` for example).
2. Run `sudo apt-get install unzip python3 virtualenv dos2unix`
3. Run `dos2unix ./test/end-to-end-tests/*.sh ./test/end-to-end-tests/synapse/*.sh ./test/end-to-end-tests/element/*.sh`
4. Install NodeJS for ubuntu:
```bash
curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash -
sudo apt-get update
sudo apt-get install nodejs
```
5. Run `yarn link` and `yarn install` for all layers from WSL if you haven't already. If you want to switch back to
your Windows host after your tests then you'll need to re-run `yarn install` (and possibly `yarn link`) there too.
Though, do note that you can access `http://localhost:8080` in your Windows-based browser when running webpack in
the WSL environment (it does *not* work the other way around, annoyingly).
6. In WSL, run `yarn start` at the element-web layer to get things going.
7. While that builds... Run:
```bash
sudo apt-get install x11-apps
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome-stable_current_amd64.deb
sudo apt -f install
```
8. Run:
```bash
cd ./test/end-to-end-tests
./synapse/install.sh
./install.sh
./run.sh --app-url http://localhost:8080 --log-directory ./logs
```
Note that using `yarn test:e2e` probably won't work for you. You might also have to use the config.json from the
`element/config-template` directory in order to actually succeed at the tests.
Also note that you'll have to use `--no-sandbox` otherwise Chrome will complain that there's no sandbox available. You
could probably fix this with enough effort, or you could run a headless Chrome in the WSL container without a sandbox.
Reference material that isn't fully represented in the steps above (but snippets have been borrowed):
* https://virtualizationreview.com/articles/2017/02/08/graphical-programs-on-windows-subsystem-on-linux.aspx
* https://gist.github.com/drexler/d70ab957f964dbef1153d46bd853c775
* https://docs.microsoft.com/en-us/windows/wsl/networking#accessing-windows-networking-apps-from-linux-host-ip

View file

@ -1,2 +0,0 @@
element-web
element.pid

View file

@ -1,28 +0,0 @@
{
"default_hs_url": "http://localhost:5005",
"default_is_url": "https://vector.im",
"disable_custom_urls": false,
"disable_guests": false,
"disable_login_language_selector": false,
"disable_3pid_login": false,
"brand": "Element",
"integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api",
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"showLabsSettings": true,
"default_federate": true,
"welcomePageUrl": "home.html",
"default_theme": "light",
"roomDirectory": {
"servers": [
"localhost:5005"
]
},
"posthog": {
"projectApiKey": "not-a-real-api-key",
"apiHost": "http://localhost:5005"
},
"enable_presence_by_hs_url": {
"https://matrix.org": false
}
}

View file

@ -1,18 +0,0 @@
#!/bin/bash
set -e
BASE_DIR=$(cd $(dirname $0) && pwd)
cd $BASE_DIR
# Install ComplexHttpServer (a drop in replacement for Python's SimpleHttpServer
# but with support for multiple threads) into a virtualenv.
(
virtualenv -p python3 env
source env/bin/activate
pip install --upgrade pip
pip install --upgrade setuptools
pip install ComplexHttpServer
deactivate
)

View file

@ -1,16 +0,0 @@
#!/bin/bash
set -e
ELEMENT_BRANCH=develop
if [ -d $BASE_DIR/element-web ]; then
echo "Element is already installed"
exit
fi
curl -L https://github.com/vector-im/element-web/archive/${ELEMENT_BRANCH}.zip --output element.zip
unzip -q element.zip
rm element.zip
mv element-web-${ELEMENT_BRANCH} element-web
cd element-web
yarn install --pure-lockfile
yarn run build

View file

@ -1,54 +0,0 @@
#!/usr/bin/env bash
set -e
PORT=5000
BASE_DIR=$(cd $(dirname $0) && pwd)
PIDFILE=$BASE_DIR/element.pid
CONFIG_BACKUP=config.e2etests_backup.json
if [ -f $PIDFILE ]; then
exit
fi
cd $BASE_DIR/
echo -n "Starting Element on http://localhost:$PORT ... "
pushd element-web/webapp/ > /dev/null
# backup config file before we copy template
if [ -f config.json ]; then
mv config.json $CONFIG_BACKUP
fi
cp $BASE_DIR/config-template/config.json .
LOGFILE=$(mktemp)
# run web server in the background, showing output on error
(
source $BASE_DIR/env/bin/activate
python -m ComplexHTTPServer $PORT > $LOGFILE 2>&1 &
PID=$!
echo $PID > $PIDFILE
# wait so subshell does not exit
# otherwise sleep below would not work
wait $PID; RESULT=$?
# NOT expected SIGTERM (128 + 15)
# from stop.sh?
if [ $RESULT -ne 143 ]; then
echo "Failed"
cat $LOGFILE
rm $PIDFILE 2> /dev/null
fi
rm $LOGFILE
exit $RESULT
)&
# to be able to return the exit code for immediate errors (like address already in use)
# we wait for a short amount of time in the background and exit when the first
# child process exits
sleep 0.5 &
# wait the first child process to exit (python or sleep)
wait -n; RESULT=$?
# return exit code of first child to exit
if [ $RESULT -eq 0 ]; then
echo "Running"
fi
exit $RESULT

View file

@ -1,22 +0,0 @@
#!/bin/bash
set -e
BASE_DIR=$(cd $(dirname $0) && pwd)
PIDFILE=element.pid
CONFIG_BACKUP=config.e2etests_backup.json
cd $BASE_DIR
if [ -f $PIDFILE ]; then
echo "Stopping Element server ..."
PID=$(cat $PIDFILE)
rm $PIDFILE
kill $PID
# revert config file
cd element-web/webapp
rm config.json
if [ -f $CONFIG_BACKUP ]; then
mv $CONFIG_BACKUP config.json
fi
fi

View file

@ -1,24 +0,0 @@
/*
Copyright 2019 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.
*/
// used from run.sh as getopts doesn't support long parameters
const idx = process.argv.indexOf("--app-url");
let hasAppUrl = false;
if (idx !== -1) {
const value = process.argv[idx + 1];
hasAppUrl = !!value;
}
process.stdout.write(hasAppUrl ? "1" : "0");

View file

@ -1,7 +0,0 @@
#!/bin/bash
# run with PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true sh install.sh if chrome is already installed
set -e
./synapse/install.sh
# local testing doesn't need a Element fetched from master,
# so not installing that by default
yarn install --pure-lockfile

View file

@ -1,24 +0,0 @@
{
"name": "e2e-tests",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc -p ./tsconfig.json"
},
"author": "",
"license": "ISC",
"dependencies": {
"cheerio": "^1.0.0-rc.2",
"commander": "^9",
"puppeteer": "13.4.1",
"request": "^2.88.0",
"request-promise-native": "^1.0.7",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/puppeteer": "^5.4.4",
"typescript": "^4.5.3"
}
}

View file

@ -1,26 +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.
*/
const path = require('path');
// used from run.sh as getopts doesn't support long parameters
const idx = process.argv.indexOf("--log-directory");
if (idx !== -1) {
const value = process.argv[idx + 1];
process.stdout.write(path.join(path.resolve(value), 'homeserver.log'));
} else {
process.stdout.write(path.join(process.cwd(), 'homeserver.log'));
}

View file

@ -1,44 +0,0 @@
#!/bin/bash
set -e
BASE_DIR=$(cd $(dirname $0) && pwd)
pushd $BASE_DIR
if [ ! -d "synapse/installations" ] || [ ! -d "node_modules" ]; then
echo "Please first run $BASE_DIR/install.sh"
exit 1
fi
has_custom_app=$(node has-custom-app.js $@)
synapse_log_file=$(node pick-synapse-log-file.js $@)
touch $synapse_log_file
if [ ! -d "element/element-web" ] && [ $has_custom_app -ne "1" ]; then
echo "Please provide an instance of Element to test against by passing --app-url <url> or running $BASE_DIR/element/install.sh"
exit 1
fi
stop_servers() {
if [ $has_custom_app -ne "1" ]; then
./element/stop.sh
fi
./synapse/stop.sh
}
handle_error() {
EXIT_CODE=$?
echo "Tests fell over with a non-zero exit code: stopping servers"
stop_servers
exit $EXIT_CODE
}
trap 'handle_error' ERR
LOGFILE=$synapse_log_file ./synapse/start.sh
reg_secret=`./synapse/getcfg.sh registration_shared_secret`
if [ $has_custom_app -ne "1" ]; then
./element/start.sh
fi
yarn build
node lib/start.js --registration-shared-secret=$reg_secret $@
stop_servers

View file

@ -1,24 +0,0 @@
/*
Copyright 2021 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 "matrix-react-sdk/src/@types/global"; // load matrix-react-sdk's type extensions first
declare global {
interface Window {
mxPerformanceMonitor: any;
mxPerformanceEntryNames: any;
}
}

View file

@ -1,34 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { Page, PageEventObject } from "puppeteer";
export class LogBuffer<EventMapperArg extends Parameters<Parameters<Page['on']>[1]>[0]> {
buffer: string;
constructor(
page: Page,
eventName: keyof PageEventObject,
eventMapper: (arg: EventMapperArg) => Promise<string>,
initialValue = "",
) {
this.buffer = initialValue;
page.on(eventName, (arg: EventMapperArg) => {
eventMapper(arg).then((r) => this.buffer += r);
});
}
}

View file

@ -1,62 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export class Logger {
private indent = 0;
private muted = false;
constructor(readonly username: string) {}
public startGroup(description: string): Logger {
if (!this.muted) {
const indent = " ".repeat(this.indent * 2);
console.log(`${new Date().toISOString()} ${indent} * ${this.username} ${description}:`);
}
this.indent += 1;
return this;
}
public endGroup(): Logger {
this.indent -= 1;
return this;
}
public step(description: string): Logger {
if (!this.muted) {
const indent = " ".repeat(this.indent * 2);
process.stdout.write(`${new Date().toISOString()} ${indent} * ${this.username} ${description} ... `);
}
return this;
}
public done(status = "done"): Logger {
if (!this.muted) {
process.stdout.write(status + "\n");
}
return this;
}
public mute(): Logger {
this.muted = true;
return this;
}
public unmute(): Logger {
this.muted = false;
return this;
}
}

View file

@ -1,40 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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 { signup } from './usecases/signup';
import { toastScenarios } from './scenarios/toast';
import { ElementSession } from "./session";
export async function scenario(createSession: (s: string) => Promise<ElementSession>): Promise<void> {
let firstUser = true;
async function createUser(username: string) {
const session = await createSession(username);
if (firstUser) {
// only show browser version for first browser opened
console.log(`running tests on ${await session.browser.version()} ...`);
firstUser = false;
}
// ported to cyprus (registration test)
await signup(session, session.username, 'testsarefun!!!', session.hsUrl);
return session;
}
const alice = await createUser("alice");
const bob = await createUser("bob");
await toastScenarios(alice, bob);
}

View file

@ -1 +0,0 @@
scenarios contains the high-level playbook for the test suite

View file

@ -1,42 +0,0 @@
/*
Copyright 2020 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 { assertNoToasts, acceptToast, rejectToast } from "../usecases/toasts";
import { ElementSession } from "../session";
export async function toastScenarios(alice: ElementSession, bob: ElementSession): Promise<void> {
console.log(" checking and clearing toasts:");
alice.log.startGroup(`clears toasts`);
alice.log.step(`accepts analytics toast`);
await acceptToast(alice, "Help improve Element");
alice.log.done();
alice.log.step(`checks no remaining toasts`);
await assertNoToasts(alice);
alice.log.done();
alice.log.endGroup();
bob.log.startGroup(`clears toasts`);
bob.log.step(`reject analytics toast`);
await rejectToast(bob, "Help improve Element");
bob.log.done();
bob.log.step(`checks no remaining toasts`);
await assertNoToasts(bob);
bob.log.done();
bob.log.endGroup();
}

View file

@ -1,153 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 puppeteer from 'puppeteer';
import { Logger } from './logger';
import { LogBuffer } from './logbuffer';
import { delay, serializeLog } from './util';
const DEFAULT_TIMEOUT = 20000;
export class ElementSession {
readonly consoleLog: LogBuffer<puppeteer.ConsoleMessage>;
readonly networkLog: LogBuffer<puppeteer.HTTPRequest>;
readonly log: Logger;
constructor(readonly browser: puppeteer.Browser, readonly page: puppeteer.Page, readonly username: string,
readonly elementServer: string, readonly hsUrl: string) {
this.consoleLog = new LogBuffer(page, "console",
async (msg: puppeteer.ConsoleMessage) => `${await serializeLog(msg)}\n`);
this.networkLog = new LogBuffer(page,
"requestfinished", async (req: puppeteer.HTTPRequest) => {
const type = req.resourceType();
const response = await req.response();
return new Date().toISOString() +
` ${type} ${response?.status() ?? '<no response>'} ${req.method()} ${req.url()} \n`;
});
this.log = new Logger(this.username);
}
public static async create(username: string, puppeteerOptions: Parameters<typeof puppeteer.launch>[0],
elementServer: string, hsUrl: string, throttleCpuFactor = 1): Promise<ElementSession> {
const browser = await puppeteer.launch(puppeteerOptions);
const page = await browser.newPage();
await page.setViewport({
width: 1280,
height: 800,
});
if (throttleCpuFactor !== 1) {
const client = await page.target().createCDPSession();
console.log("throttling cpu by a factor of", throttleCpuFactor);
await client.send('Emulation.setCPUThrottlingRate', { rate: throttleCpuFactor });
}
return new ElementSession(browser, page, username, elementServer, hsUrl);
}
public async tryGetInnertext(selector: string): Promise<string> {
const field = await this.page.$(selector);
if (field != null) {
const textHandle = await field.getProperty('innerText');
return await textHandle.jsonValue();
}
return null;
}
public async getElementProperty(handle: puppeteer.ElementHandle, property: string): Promise<string> {
const propHandle = await handle.getProperty(property);
return await propHandle.jsonValue();
}
public innerText(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'innerText');
}
public isChecked(field: puppeteer.ElementHandle): Promise<string> {
return this.getElementProperty(field, 'checked');
}
public consoleLogs(): string {
return this.consoleLog.buffer;
}
public networkLogs(): string {
return this.networkLog.buffer;
}
public async replaceInputText(input: puppeteer.ElementHandle, text: string): Promise<void> {
// click 3 times to select all text
await input.click({ clickCount: 3 });
// waiting here solves not having selected all the text by the 3x click above,
// presumably because of the Field label animation.
await this.delay(300);
// then remove it with backspace
await input.press('Backspace');
// and type the new text
await input.type(text);
}
public query(
selector: string,
timeout: number = DEFAULT_TIMEOUT,
hidden = false,
): Promise<puppeteer.ElementHandle> {
return this.page.waitForSelector(selector, { visible: true, timeout, hidden });
}
public queryWithoutWaiting(selector: string): Promise<puppeteer.ElementHandle> {
return this.page.$(selector);
}
public async queryAll(selector: string): Promise<puppeteer.ElementHandle[]> {
const timeout = DEFAULT_TIMEOUT;
await this.query(selector, timeout);
return await this.page.$$(selector);
}
public async waitNoSpinner(): Promise<void> {
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
}
public goto(url: string): Promise<puppeteer.HTTPResponse> {
return this.page.goto(url);
}
public url(path: string): string {
return this.elementServer + path;
}
public delay(ms: number) {
return delay(ms);
}
public async close(): Promise<void> {
return this.browser.close();
}
public async poll(callback: () => Promise<boolean>, interval = 100): Promise<boolean> {
const timeout = DEFAULT_TIMEOUT;
let waited = 0;
while (waited < timeout) {
await this.delay(interval);
waited += interval;
if (await callback()) {
return true;
}
}
return false;
}
}

View file

@ -1,2 +0,0 @@
use cases contains the detailed DOM interactions to perform a given use case, may also do some assertions.
use cases are often used in multiple scenarios.

View file

@ -1,39 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { findSublist } from "./create-room";
import { ElementSession } from "../session";
export async function acceptInvite(session: ElementSession, name: string): Promise<void> {
session.log.step(`accepts "${name}" invite`);
const inviteSublist = await findSublist(session, "invites");
const invitesHandles = await inviteSublist.$$(".mx_RoomTile_title");
const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => {
const text = await session.innerText(inviteHandle);
return { inviteHandle, text };
}));
const inviteHandle = invitesWithText.find(({ inviteHandle, text }) => {
return text.trim() === name;
}).inviteHandle;
await inviteHandle.click();
const acceptInvitationLink = await session.query(".mx_RoomPreviewBar_Invite .mx_AccessibleButton_kind_primary");
await acceptInvitationLink.click();
session.log.done();
}

View file

@ -1,88 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 puppeteer from "puppeteer";
import { measureStart, measureStop } from '../util';
import { ElementSession } from "../session";
export async function openRoomDirectory(session: ElementSession): Promise<void> {
const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
await roomDirectoryButton.click();
}
export async function findSublist(session: ElementSession, name: string): Promise<puppeteer.ElementHandle> {
return await session.query(`.mx_RoomSublist[aria-label="${name}" i]`);
}
export async function createRoom(session: ElementSession, roomName: string, encrypted = false): Promise<void> {
session.log.step(`creates room "${roomName}"`);
const roomsSublist = await findSublist(session, "rooms");
const addRoomButton = await roomsSublist.$(".mx_RoomSublist_auxButton");
await addRoomButton.click();
const createRoomButton = await session.query('.mx_AccessibleButton[aria-label="New room"]');
await createRoomButton.click();
const roomNameInput = await session.query('.mx_CreateRoomDialog_name input');
await session.replaceInputText(roomNameInput, roomName);
if (!encrypted) {
const encryptionToggle = await session.query('.mx_CreateRoomDialog_e2eSwitch .mx_ToggleSwitch');
await encryptionToggle.click();
}
const createButton = await session.query('.mx_Dialog_primary');
await createButton.click();
await session.query('.mx_MessageComposer');
session.log.done();
}
export async function createDm(session: ElementSession, invitees: string[]): Promise<void> {
session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
await measureStart(session, "mx_CreateDM");
const dmsSublist = await findSublist(session, "people");
const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton");
await startChatButton.click();
const inviteesEditor = await session.query('.mx_InviteDialog_editor input');
for (const target of invitees) {
await session.replaceInputText(inviteesEditor, target);
await session.delay(1000); // give it a moment to figure out a suggestion
// find the suggestion and accept it
const suggestions = await session.queryAll('.mx_InviteDialog_tile_nameStack_userId');
const suggestionTexts = await Promise.all(suggestions.map(s => session.innerText(s)));
const suggestionIndex = suggestionTexts.indexOf(target);
if (suggestionIndex === -1) {
throw new Error(`failed to find a suggestion in the DM dialog to invite ${target} with`);
}
await suggestions[suggestionIndex].click();
}
// press the go button and hope for the best
const goButton = await session.query('.mx_InviteDialog_goButton');
await goButton.click();
await session.query('.mx_MessageComposer');
session.log.done();
await measureStop(session, "mx_CreateDM");
}

View file

@ -1,47 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { strict as assert } from 'assert';
import { ElementSession } from "../session";
export async function assertDialog(session: ElementSession, expectedTitle: string): Promise<void> {
const titleElement = await session.query(".mx_Dialog .mx_Dialog_title");
const dialogHeader = await session.innerText(titleElement);
assert.equal(dialogHeader, expectedTitle);
}
export async function acceptDialog(session: ElementSession, expectedTitle: string): Promise<void> {
const foundDialog = await acceptDialogMaybe(session, expectedTitle);
if (!foundDialog) {
throw new Error("could not find a dialog");
}
}
export async function acceptDialogMaybe(session: ElementSession, expectedTitle: string): Promise<boolean> {
let primaryButton = null;
try {
primaryButton = await session.query(".mx_Dialog .mx_Dialog_primary");
} catch (err) {
return false;
}
if (expectedTitle) {
await assertDialog(session, expectedTitle);
}
await primaryButton.click();
return true;
}

View file

@ -1,43 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { ElementSession } from "../session";
export async function invite(session: ElementSession, userId: string): Promise<void> {
session.log.step(`invites "${userId}" to room`);
await session.delay(1000);
const memberPanelButton = await session.query(".mx_RightPanel_membersButton");
try {
await session.query(".mx_RightPanel_headerButton_highlight", 500);
// Right panel is open - toggle it to ensure it's the member list
// Sometimes our tests have this opened to MemberInfo
await memberPanelButton.click();
await memberPanelButton.click();
} catch (e) {
// Member list is closed - open it
await memberPanelButton.click();
}
const inviteButton = await session.query(".mx_MemberList_invite");
await inviteButton.click();
const inviteTextArea = await session.query(".mx_InviteDialog_editor input");
await inviteTextArea.type(userId);
const selectUserItem = await session.query(".mx_InviteDialog_tile--room");
await selectUserItem.click();
const confirmButton = await session.query(".mx_InviteDialog_goButton");
await confirmButton.click();
session.log.done();
}

View file

@ -1,34 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { openRoomDirectory } from './create-room';
import { measureStart, measureStop } from '../util';
import { ElementSession } from "../session";
export async function join(session: ElementSession, roomName: string): Promise<void> {
session.log.step(`joins room "${roomName}"`);
await measureStart(session, "mx_JoinRoom");
await openRoomDirectory(session);
const roomInput = await session.query('.mx_DirectorySearchBox input');
await session.replaceInputText(roomInput, roomName);
const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton');
await joinFirstLink.click();
await session.query('.mx_MessageComposer');
await measureStop(session, "mx_JoinRoom");
session.log.done();
}

View file

@ -1,91 +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 { strict as assert } from 'assert';
import { ElementSession } from "../session";
export async function login(
session: ElementSession,
username: string, password: string,
homeserver: string,
): Promise<void> {
session.log.startGroup("logs in");
session.log.step("Navigates to login page");
const navPromise = session.page.waitForNavigation();
await session.goto(session.url('/#/login'));
await navPromise;
session.log.done();
// for reasons I still don't fully understand, this seems to be flakey
// such that when it's trying to click on 'mx_ServerPicker_change',
// it ends up clicking instead on the dropdown for username / email / phone.
// Waiting for the serverpicker to appear before proceeding seems to make
// it reliable...
await session.query('.mx_ServerPicker');
// wait until no spinners visible
await session.waitNoSpinner();
// change the homeserver by clicking the advanced section
if (homeserver) {
session.log.step("Clicks button to change homeserver");
const changeButton = await session.query('.mx_ServerPicker_change');
await changeButton.click();
session.log.done();
session.log.step("Enters homeserver");
const hsInputField = await session.query('.mx_ServerPickerDialog_otherHomeserver');
await session.replaceInputText(hsInputField, homeserver);
session.log.done();
session.log.step("Clicks next");
const nextButton = await session.query('.mx_ServerPickerDialog_continue');
// accept homeserver
await nextButton.click();
session.log.done();
}
// Delay required because of local race condition on macOS
// Where the form is not query-able despite being present in the DOM
await session.delay(100);
session.log.step("Fills in login form");
//fill out form
const usernameField = await session.query("#mx_LoginForm_username");
const passwordField = await session.query("#mx_LoginForm_password");
await session.replaceInputText(usernameField, username);
await session.replaceInputText(passwordField, password);
session.log.done();
session.log.step("Clicks login");
const loginButton = await session.query('.mx_Login_submit');
await loginButton.focus();
//check no errors
const errorText = await session.tryGetInnertext('.mx_Login_error');
assert.strictEqual(errorText, null);
//submit form
//await page.screenshot({path: "beforesubmit.png", fullPage: true});
await loginButton.click();
session.log.done();
const foundHomeUrl = await session.poll(async () => {
const url = session.page.url();
return url === session.url('/#/home');
});
assert(foundHomeUrl);
session.log.endGroup();
}

View file

@ -1,46 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { ElementHandle } from "puppeteer";
import { openRoomSummaryCard } from "./rightpanel";
import { ElementSession } from "../session";
export async function openMemberInfo(session: ElementSession, name: String): Promise<void> {
const membersAndNames = await getMembersInMemberlist(session);
const matchingLabel = membersAndNames.filter((m) => {
return m.displayName === name;
}).map((m) => m.label)[0];
await matchingLabel.click();
}
interface MemberName {
label: ElementHandle;
displayName: string;
}
export async function getMembersInMemberlist(session: ElementSession): Promise<MemberName[]> {
await openRoomSummaryCard(session);
const memberPanelButton = await session.query(".mx_RoomSummaryCard_icon_people");
// We are back at the room summary card
await memberPanelButton.click();
const memberNameElements = await session.queryAll(".mx_MemberList .mx_EntityTile_name");
return Promise.all(memberNameElements.map(async (el) => {
return { label: el, displayName: await session.innerText(el) };
}));
}

View file

@ -1,74 +0,0 @@
/*
Copyright 2020 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 { ElementSession } from "../session";
export async function closeRoomRightPanel(session: ElementSession): Promise<void> {
const button = await session.query(".mx_BaseCard_close");
await button.click();
}
export async function openThreadListPanel(session: ElementSession): Promise<void> {
await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"]');
const button = await session.queryWithoutWaiting('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"]' +
':not(.mx_RightPanel_headerButton_highlight)');
await button?.click();
}
export async function assertThreadListHasUnreadIndicator(session: ElementSession): Promise<void> {
await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Threads"] ' +
'.mx_RightPanel_headerButton_unreadIndicator');
}
export async function clickLatestThreadInThreadListPanel(session: ElementSession): Promise<void> {
const threads = await session.queryAll(".mx_ThreadPanel .mx_EventTile");
await threads[threads.length - 1].click();
}
export async function openRoomRightPanel(session: ElementSession): Promise<void> {
// block until we have a roomSummaryButton
const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');
// check if it's highlighted
const highlightedRoomSummaryButton = await session.queryWithoutWaiting(
'.mx_RoomHeader .mx_RightPanel_headerButton_highlight[aria-label="Room Info"]',
);
if (!highlightedRoomSummaryButton) {
// If the room summary is not yet open, open it
await roomSummaryButton.click();
}
}
export async function goBackToRoomSummaryCard(session: ElementSession): Promise<void> {
for (let i = 0; i < 5; i++) {
try {
const backButton = await session.query(".mx_BaseCard_back", 500);
// Right panel is open to the wrong thing - go back up to the Room Summary Card
// Sometimes our tests have this opened to MemberInfo
await backButton.click();
} catch (e) {
// explicitly check for TimeoutError as this sometimes returned
// `Error: Node is detached from document` due to a re-render race or similar
if (e.name === "TimeoutError") {
break; // stop trying to go further back
}
}
}
}
export async function openRoomSummaryCard(session: ElementSession) {
await openRoomRightPanel(session);
await goBackToRoomSummaryCard(session);
}

View file

@ -1,240 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { strict as assert } from 'assert';
import { ElementHandle } from "puppeteer";
import { openRoomSummaryCard } from "./rightpanel";
import { acceptDialog } from './dialog';
import { ElementSession } from "../session";
export async function setSettingsToggle(session: ElementSession, toggle: ElementHandle, enabled): Promise<boolean> {
const className = await session.getElementProperty(toggle, "className");
const checked = className.includes("mx_ToggleSwitch_on");
if (checked !== enabled) {
await toggle.click();
session.log.done();
return true;
} else {
session.log.done("already set");
}
}
export async function checkSettingsToggle(session: ElementSession,
toggle: ElementHandle, shouldBeEnabled: boolean): Promise<void> {
const className = await session.getElementProperty(toggle, "className");
const checked = className.includes("mx_ToggleSwitch_on");
if (checked === shouldBeEnabled) {
session.log.done('set as expected');
} else {
// other logs in the area should give more context as to what this means.
throw new Error("settings toggle value didn't match expectation");
}
}
interface Tabs {
securityTabButton: ElementHandle;
generalTabButton: ElementHandle;
}
async function findTabs(session: ElementSession): Promise<Tabs> {
/// XXX delay is needed here, possibly because the header is being rerendered
/// click doesn't do anything otherwise
await session.delay(1000);
await openRoomSummaryCard(session);
const settingsButton = await session.query(".mx_RoomSummaryCard_icon_settings");
await settingsButton.click();
//find tabs
const tabButtons = await session.queryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel");
const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t)));
const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))];
const generalTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("general"))];
return { securityTabButton, generalTabButton };
}
interface Settings {
encryption?: boolean;
directory?: boolean;
alias?: string;
publishedAlias?: string;
visibility?: string;
}
export async function checkRoomSettings(session: ElementSession, expectedSettings: Settings): Promise<void> {
session.log.startGroup(`checks the room settings`);
const { securityTabButton } = await findTabs(session);
const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
const isDirectory = generalSwitches[0];
if (typeof expectedSettings.directory === 'boolean') {
session.log.step(`checks directory listing is ${expectedSettings.directory}`);
await checkSettingsToggle(session, isDirectory, expectedSettings.directory);
}
if (expectedSettings.publishedAlias) {
session.log.step(`checks for published alias of ${expectedSettings.publishedAlias}`);
const publishedAliases = await session.queryAll('#roomAltAliases .mx_EditableItem_item, #roomAltAliases li');
const publishedAliasTexts = await Promise.all(publishedAliases.map(a => session.innerText(a)));
if (publishedAliasTexts.find(a => a.includes(expectedSettings.publishedAlias))) {
session.log.done("present");
} else {
throw new Error(`could not find published alias ${expectedSettings.publishedAlias}`);
}
}
if (expectedSettings.alias) {
session.log.step(`checks for local alias of ${expectedSettings.alias}`);
const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
await summary.click();
const localAliases = await session.queryAll('#roomAliases .mx_EditableItem_item, #roomAliases li');
const localAliasTexts = await Promise.all(localAliases.map(a => session.innerText(a)));
if (localAliasTexts.find(a => a.includes(expectedSettings.alias))) {
session.log.done("present");
} else {
throw new Error(`could not find local alias ${expectedSettings.alias}`);
}
}
await securityTabButton.click();
await session.delay(500);
const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
const e2eEncryptionToggle = securitySwitches[0];
if (typeof expectedSettings.encryption === "boolean") {
session.log.step(`checks room e2e encryption is ${expectedSettings.encryption}`);
await checkSettingsToggle(session, e2eEncryptionToggle, expectedSettings.encryption);
}
if (expectedSettings.visibility) {
session.log.step(`checks visibility is ${expectedSettings.visibility}`);
const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]");
// the "Who can read history?" "Anyone" radio option is only shown if visibility is set to public
assert.equal(radios.length, expectedSettings.visibility === "public" ? 7 : 6);
const [inviteOnlyRoom, spaceMembers, publicRoom] = radios;
let expectedRadio = null;
if (expectedSettings.visibility === "invite_only") {
expectedRadio = inviteOnlyRoom;
} else if (expectedSettings.visibility === "space_members") {
expectedRadio = spaceMembers;
} else if (expectedSettings.visibility === "public") {
expectedRadio = publicRoom;
} else {
throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`);
}
if (await session.isChecked(expectedRadio)) {
session.log.done();
} else {
throw new Error("room visibility is not as expected");
}
}
const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton");
await closeButton.click();
session.log.endGroup();
}
async function getValidationError(session: ElementSession): Promise<string | undefined> {
try {
// give it 500ms to fail to produce a validation error
const validationDetail = await session.query(".mx_Validation_detail", 500);
return session.innerText(validationDetail);
} catch (e) {
// no validation tooltips
return undefined;
}
}
export async function changeRoomSettings(session: ElementSession, settings: Settings) {
session.log.startGroup(`changes the room settings`);
const { securityTabButton, generalTabButton } = await findTabs(session);
securityTabButton.click();
await session.delay(500);
const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
const e2eEncryptionToggle = securitySwitches[0];
if (typeof settings.encryption === "boolean") {
session.log.step(`sets room e2e encryption to ${settings.encryption}`);
const clicked = await setSettingsToggle(session, e2eEncryptionToggle, settings.encryption);
// if enabling, accept beta warning dialog
if (clicked && settings.encryption) {
await acceptDialog(session, "Enable encryption?");
}
}
if (settings.visibility) {
session.log.step(`sets visibility to ${settings.visibility}`);
const radios = await session.queryAll(".mx_RoomSettingsDialog label");
assert.equal(radios.length, 7);
const [inviteOnlyRoom,, publicRoom] = radios;
if (settings.visibility === "invite_only") {
await inviteOnlyRoom.click();
} else if (settings.visibility === "public") {
await publicRoom.click();
} else {
throw new Error(`unrecognized room visibility setting: ${settings.visibility}`);
}
await session.delay(100);
session.log.done();
}
generalTabButton.click();
await session.delay(500);
const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
const isDirectory = generalSwitches[0];
if (typeof settings.directory === "boolean") {
session.log.step(`sets directory listing to ${settings.directory}`);
await setSettingsToggle(session, isDirectory, settings.directory);
}
if (settings.alias) {
session.log.step(`adding local alias ${settings.alias}`);
const aliasField = await session.query("#roomAliases input[type=text]");
await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
const addButton = await session.query("#roomAliases .mx_AccessibleButton");
await addButton.click();
await session.query("#roomAliases .mx_Field_valid, #roomAliases .mx_Field_invalid"); // await validator
assert.equal(await getValidationError(session), undefined);
session.log.done();
}
if (settings.publishedAlias) {
session.log.step(`adding published alias ${settings.alias}`);
const aliasField = await session.query("#roomAltAliases input[type=text]");
await session.replaceInputText(aliasField, settings.alias.substring(1));
const addButton = await session.query("#roomAltAliases .mx_AccessibleButton");
await addButton.click();
await session.query("#roomAltAliases .mx_Field_valid, #roomAltAliases .mx_Field_invalid"); // await validator
assert.equal(await getValidationError(session), undefined);
session.log.done();
}
const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton");
await closeButton.click();
session.log.endGroup();
}

View file

@ -1,43 +0,0 @@
/*
Copyright 2020 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 { acceptToast } from "./toasts";
import { ElementSession } from "../session";
export async function setupSecureBackup(session: ElementSession): Promise<void> {
session.log.step("sets up Secure Backup");
await acceptToast(session, "Set up Secure Backup");
// Continue with the default (generate a security key)
const xsignContButton = await session.query('.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary');
await xsignContButton.click();
//ignore the recovery key
//TODO: It's probably important for the tests to know the recovery key
const copyButton = await session.query('.mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn');
await copyButton.click();
//acknowledge that we copied the recovery key to a safe place
const copyContinueButton = await session.query(
'.mx_CreateSecretStorageDialog .mx_Dialog_buttons .mx_Dialog_primary',
);
await copyContinueButton.click();
session.log.done();
}
module.exports = { setupSecureBackup };

View file

@ -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.
*/
import { findSublist } from "./create-room";
import { ElementSession } from "../session";
export async function selectRoom(session: ElementSession, name: string): Promise<void> {
session.log.step(`select "${name}" room`);
const inviteSublist = await findSublist(session, "rooms");
const invitesHandles = await inviteSublist.$$(".mx_RoomTile_title");
const invitesWithText = await Promise.all(invitesHandles.map(async (roomHandle) => {
const text = await session.innerText(roomHandle);
return { roomHandle, text };
}));
const roomHandle = invitesWithText.find(({ roomHandle, text }) => {
return text.trim() === name;
}).roomHandle;
await roomHandle.click();
session.log.done();
}

View file

@ -1,36 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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 { strict as assert } from 'assert';
import { ElementSession } from "../session";
export async function sendMessage(session: ElementSession, message: string): Promise<void> {
session.log.step(`writes "${message}" in room`);
// this selector needs to be the element that has contenteditable=true,
// not any if its parents, otherwise it behaves flaky at best.
const composer = await session.query('.mx_SendMessageComposer');
// sometimes the focus that type() does internally doesn't seem to work
// and calling click before seems to fix it 🤷
await composer.click();
await composer.type(message);
const text = await session.innerText(composer);
assert.equal(text.trim(), message.trim());
await composer.press("Enter");
// wait for the message to appear sent
await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)");
session.log.done();
}

View file

@ -1,85 +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 { Frame } from "puppeteer";
import { ElementSession } from "../session";
export async function sendSticker(session: ElementSession): Promise<void> {
session.log.step(`opens composer menu`);
const kebabButton = await session.query('.mx_MessageComposer_buttonMenu');
await kebabButton.click();
session.log.done();
let stickerFrame: Frame;
// look to see if the sticker picker is already there (it's persistent, so
// it will only load a new frame the first time we open it)
for (const f of session.page.frames()) {
if ((await f.title()) === "Fake Sticker Picker") {
stickerFrame = f;
}
}
const stickerFramePromise = new Promise<Frame>(resolve => {
session.page.once('frameattached', async f => {
await f.waitForNavigation();
resolve(f);
});
});
session.log.step(`opens sticker picker`);
const stickerOption = await session.query('#stickersButton');
await stickerOption.click();
if (stickerFrame === undefined) {
stickerFrame = await stickerFramePromise;
}
if (stickerFrame === undefined) throw new Error("Couldn't find sticker picker frame");
session.log.done();
session.log.step(`clicks sticker button`);
const sendStickerButton = await stickerFrame.waitForSelector('#sendsticker');
sendStickerButton.click();
// wait for the message to appear sent
await session.query(".mx_EventTile_last:not(.mx_EventTile_sending)");
const stickerSrc = await session.page.evaluate(() => {
return document.querySelector(
'.mx_EventTile_last .mx_MStickerBody_wrapper img',
).getAttribute('src');
});
if (!stickerSrc.split('?')[0].endsWith('/_matrix/media/r0/thumbnail/somewhere')) {
throw new Error("Unexpected image src for sticker: got " + stickerSrc);
}
const stickerAlt = await session.page.evaluate(() => {
return document.querySelector(
'.mx_EventTile_last .mx_MStickerBody_wrapper img',
).getAttribute('alt');
});
if (stickerAlt !== "Test Sticker") {
throw new Error("Unexpected image alt for sticker: got " + stickerAlt);
}
session.log.done();
}

View file

@ -1,30 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { ElementSession } from "../session";
export async function openSettings(session: ElementSession, section: string): Promise<void> {
const menuButton = await session.query(".mx_UserMenu");
await menuButton.click();
const settingsItem = await session.query(".mx_UserMenu_iconSettings");
await settingsItem.click();
if (section) {
const sectionButton = await session.query(
`.mx_UserSettingsDialog .mx_TabbedView_tabLabels .mx_UserSettingsDialog_${section}Icon`);
await sectionButton.click();
}
}

View file

@ -1,93 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { strict as assert } from 'assert';
import { ElementSession } from "../session";
export async function signup(
session: ElementSession,
username: string,
password: string,
homeserver: string,
): Promise<void> {
session.log.step("signs up");
await session.goto(session.url('/#/register'));
// change the homeserver by clicking the advanced section
if (homeserver) {
const changeButton = await session.query('.mx_ServerPicker_change');
await changeButton.click();
const hsInputField = await session.query('.mx_ServerPickerDialog_otherHomeserver');
await session.replaceInputText(hsInputField, homeserver);
const nextButton = await session.query('.mx_ServerPickerDialog_continue');
// accept homeserver
await nextButton.click();
}
// Delay required because of local race condition on macOs
// Where the form is not query-able despite being present in the DOM
await session.delay(100);
//fill out form
const usernameField = await session.query("#mx_RegistrationForm_username");
const passwordField = await session.query("#mx_RegistrationForm_password");
const passwordRepeatField = await session.query("#mx_RegistrationForm_passwordConfirm");
await session.replaceInputText(usernameField, username);
await session.replaceInputText(passwordField, password);
await session.replaceInputText(passwordRepeatField, password);
//wait 300ms because Registration/ServerConfig have a 250ms
//delay to internally set the homeserver url
//see Registration::render and ServerConfig::props::delayTimeMs
await session.delay(300);
/// focus on the button to make sure error validation
/// has happened before checking the form is good to go
const registerButton = await session.query('.mx_Login_submit');
await registerButton.focus();
// Password validation is async, wait for it to complete before submit
await session.query(".mx_Field_valid #mx_RegistrationForm_password");
//check no errors
const errorText = await session.tryGetInnertext('.mx_Login_error');
assert.strictEqual(errorText, null);
//submit form
//await page.screenshot({path: "beforesubmit.png", fullPage: true});
await registerButton.click();
//confirm dialog saying you cant log back in without e-mail
const continueButton = await session.query('.mx_RegistrationEmailPromptDialog button.mx_Dialog_primary');
await continueButton.click();
//find the privacy policy checkbox and check it
const policyCheckbox = await session.query('.mx_InteractiveAuthEntryComponents_termsPolicy input');
await policyCheckbox.click();
//now click the 'Accept' button to agree to the privacy policy
const acceptButton = await session.query('.mx_InteractiveAuthEntryComponents_termsSubmit');
await acceptButton.click();
//now click the 'Skip' button to skip the use case selection
const skipUseCaseButton = await session.query('.mx_UseCaseSelection_skip .mx_AccessibleButton');
await skipUseCaseButton.click();
//wait for registration to finish so the hash gets set
//onhashchange better?
const foundHomeUrl = await session.poll(async () => {
const url = session.page.url();
return url === session.url('/#/home');
});
assert(foundHomeUrl);
session.log.done();
}

View file

@ -1,89 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 { strict as assert } from 'assert';
import { ElementHandle } from "puppeteer";
import { ElementSession } from "../session";
interface Message {
sender: string;
encrypted?: boolean;
body: string;
continuation?: boolean;
}
export async function receiveMessage(session: ElementSession, expectedMessage: Message): Promise<void> {
session.log.step(`receives message "${expectedMessage.body}" from ${expectedMessage.sender}`);
// wait for a response to come in that contains the message
// crude, but effective
async function getLastMessage(): Promise<Message> {
const lastTile = await getLastEventTile(session);
return getMessageFromEventTile(lastTile);
}
let lastMessage;
await session.poll(async () => {
try {
lastMessage = await getLastMessage();
} catch (err) {
return false;
}
// stop polling when found the expected message
return lastMessage &&
lastMessage.body === expectedMessage.body &&
lastMessage.sender === expectedMessage.sender;
});
assertMessage(lastMessage, expectedMessage);
session.log.done();
}
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
assert.equal(foundMessage.body, expectedMessage.body);
assert.equal(foundMessage.sender, expectedMessage.sender);
if (expectedMessage.hasOwnProperty("encrypted")) {
assert.equal(foundMessage.encrypted, expectedMessage.encrypted);
}
}
function getLastEventTile(session: ElementSession): Promise<ElementHandle> {
return session.query(".mx_EventTile_last");
}
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
const className: string = await (await eventTile.getProperty("className")).jsonValue();
const classNames = className.split(" ");
const bodyElement = await eventTile.$(".mx_EventTile_body");
let sender = null;
if (senderElement) {
sender = await(await senderElement.getProperty("innerText")).jsonValue();
}
if (!bodyElement) {
return null;
}
const body: string = await(await bodyElement.getProperty("innerText")).jsonValue();
return {
sender,
body,
encrypted: classNames.includes("mx_EventTile_verified"),
continuation: classNames.includes("mx_EventTile_continuation"),
};
}

View file

@ -1,47 +0,0 @@
/*
Copyright 2020 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 { strict as assert } from 'assert';
import { ElementSession } from "../session";
export async function assertNoToasts(session: ElementSession): Promise<void> {
try {
await session.query('.mx_Toast_toast', 1000, true);
} catch (e) {
const h2Element = await session.query('.mx_Toast_title h2', 1000);
const toastTitle = await session.innerText(h2Element);
throw new Error(`"${toastTitle}" toast found when none expected`);
}
}
export async function assertToast(session: ElementSession, expectedTitle: string): Promise<void> {
const h2Element = await session.query('.mx_Toast_title h2');
const toastTitle = await session.innerText(h2Element);
assert.equal(toastTitle, expectedTitle);
}
export async function acceptToast(session: ElementSession, expectedTitle: string): Promise<void> {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_primary');
await btn.click();
}
export async function rejectToast(session: ElementSession, expectedTitle: string): Promise<void> {
await assertToast(session, expectedTitle);
const btn = await session.query('.mx_Toast_buttons .mx_AccessibleButton_kind_danger_outline');
await btn.click();
}

View file

@ -1,115 +0,0 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019, 2020 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 { strict as assert } from 'assert';
import { openMemberInfo } from "./memberlist";
import { ElementSession } from "../session";
export async function startVerification(session: ElementSession, name: string): Promise<void> {
session.log.step("opens their opponent's profile and starts verification");
await openMemberInfo(session, name);
// click verify in member info
const firstVerifyButton = await session.query(".mx_UserInfo_verifyButton");
await firstVerifyButton.click();
// wait for the animation to finish
await session.delay(1000);
// click 'start verification'
const startVerifyButton = await session.query('.mx_UserInfo_container .mx_UserInfo_startVerification');
await startVerifyButton.click();
session.log.done();
}
async function getSasCodes(session: ElementSession): Promise<string[]> {
const sasLabelElements = await session.queryAll(
".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label");
const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e)));
return sasLabels;
}
async function doSasVerification(session: ElementSession): Promise<string[]> {
session.log.step("hunts for the emoji to yell at their opponent");
const sasCodes = await getSasCodes(session);
session.log.done(sasCodes.join("\n"));
// Assume they match
session.log.step("assumes the emoji match");
const matchButton = await session.query(".mx_VerificationShowSas .mx_AccessibleButton_kind_primary");
await matchButton.click();
session.log.done();
// Wait for a big green shield (universal sign that it worked)
session.log.step("waits for a green shield");
await session.query(".mx_VerificationPanel_verified_section .mx_E2EIcon_verified");
session.log.done();
// Click 'Got It'
session.log.step("confirms the green shield");
const doneButton = await session.query(".mx_VerificationPanel_verified_section .mx_AccessibleButton_kind_primary");
await doneButton.click();
session.log.done();
// Wait a bit for the animation
session.log.step("confirms their opponent has a green shield");
await session.delay(1000);
// Verify that we now have a green shield in their name (proving it still works)
await session.query('.mx_UserInfo_profile .mx_E2EIcon_verified');
session.log.done();
return sasCodes;
}
export async function startSasVerification(session: ElementSession, name: string): Promise<string[]> {
session.log.startGroup("starts verification");
await startVerification(session, name);
// expect to be waiting (the presence of a spinner is a good thing)
await session.query('.mx_UserInfo_container .mx_EncryptionInfo_spinner');
const sasCodes = await doSasVerification(session);
session.log.endGroup();
return sasCodes;
}
export async function acceptSasVerification(session: ElementSession, name: string): Promise<string[]> {
session.log.startGroup("accepts verification");
const requestToast = await session.query('.mx_Toast_icon_verification');
// verify the toast is for verification
const toastHeader = await requestToast.$("h2");
const toastHeaderText = await session.innerText(toastHeader);
assert.equal(toastHeaderText, 'Verification requested');
const toastDescription = await requestToast.$(".mx_Toast_description");
const toastDescText = await session.innerText(toastDescription);
assert.equal(toastDescText.startsWith(name), true,
`verification opponent mismatch: expected to start with '${name}', got '${toastDescText}'`);
// accept the verification
const acceptButton = await requestToast.$(".mx_AccessibleButton_kind_primary");
await acceptButton.click();
// find the emoji button
const startEmojiButton = await session.query(".mx_VerificationPanel_verifyByEmojiButton");
await startEmojiButton.click();
const sasCodes = await doSasVerification(session);
session.log.endGroup();
return sasCodes;
}

View file

@ -1,97 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 - 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 { ConsoleMessage } from "puppeteer";
import { padEnd } from "lodash";
import { ElementSession } from "./session";
export const delay = function(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const measureStart = function(session: ElementSession, name: string): Promise<void> {
return session.page.evaluate(_name => {
window.mxPerformanceMonitor.start(_name);
}, name);
};
export const measureStop = function(session: ElementSession, name: string): Promise<void> {
return session.page.evaluate(_name => {
window.mxPerformanceMonitor.stop(_name);
}, name);
};
export async function serializeLog(msg: ConsoleMessage): Promise<string> {
// 9 characters padding is somewhat arbitrary ("warning".length + some)
let s = `${new Date().toISOString()} | ${ padEnd(msg.type(), 9, ' ')}| ${msg.text()} `; // trailing space is intentional
const args = msg.args();
for (let i = 0; i < args.length; i++) {
const arg = args[i];
let val;
try {
val = await arg.jsonValue();
} catch (error) {
val = `<error: ${error}>`;
}
// We handle strings a bit differently because the `jsonValue` will be in a weird-looking
// shape ("JSHandle:words are here"). Weirdly, `msg.text()` also catches text nodes that
// we can't with our parsing, so we trust that it's correct whenever we can.
if (typeof val === 'string') {
if (i === 0) {
// if it's a string, just ignore it because it should have already been caught
// by the `msg.text()` in the initial `s` construction.
continue;
}
// evaluate the arg as a string by running it through the page context
s += `${await arg.evaluate(a => a.toString())} `; // trailing space is intentional
continue;
}
// Try and parse the value as an error object first (which will be an empty JSON
// object). Otherwise, parse the object to a string.
//
// Note: we have to run the checks against the object in the page context, so call
// evaluate instead of just doing it ourselves.
const stringyArg: string = await arg.evaluate((argInContext: any) => {
// sometimes the argument will be `null` or similar - treat it safely.
if (argInContext?.stack || (argInContext instanceof Error)) {
// probably an error - toString it and append any properties which might not be
// caught. For example, on HTTP errors the JSON stringification will capture the
// status code.
//
// String format is a bit weird, but basically we're trying to get away from the
// stack trace so the context doesn't blend in but is otherwise indented correctly.
return `${argInContext.toString()}\n\n Error context: ${JSON.stringify(argInContext)}`;
}
// not an error, as far as we're concerned - return it as human-readable JSON
let ret;
try {
ret = JSON.stringify(argInContext, null, 4);
} catch (error) {
ret = `<error: ${error}>`;
}
return ret;
});
s += `${stringyArg} `; // trailing space is intentional
}
return s;
}

View file

@ -1,149 +0,0 @@
/*
Copyright 2018 New Vector Ltd
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 fs from "fs";
import { Command } from "commander";
import { ElementSession } from './src/session';
import { scenario } from './src/scenario';
const program = new Command();
program
.option('--no-logs', "don't output logs, document html on error", false)
.option('--app-url [url]', "url to test", "http://localhost:5000")
.option('--windowed', "dont run tests headless", false)
.option('--slow-mo', "type at a human speed", false)
.option('--dev-tools', "open chrome devtools in browser window", false)
.option('--throttle-cpu [factor]', "factor to slow down the cpu with", parseFloat, 1.0)
.option('--no-sandbox', "same as puppeteer arg", false)
.option('--log-directory <dir>', 'a directory to dump html and network logs in when the tests fail')
.requiredOption('--registration-shared-secret <secret>', 'the secret to use for registering users')
.parse(process.argv);
const hsUrl = 'http://localhost:5005';
async function runTests() {
const sessions = [];
const options = {
slowMo: program.opts().slowMo ? 20 : undefined,
devtools: program.opts().devTools,
headless: !program.opts().windowed,
args: [],
};
if (!program.opts().sandbox) {
options.args.push('--no-sandbox', '--disable-setuid-sandbox');
}
if (process.env.CHROME_PATH) {
const path = process.env.CHROME_PATH;
console.log(`(using external chrome/chromium at ${path}, make sure it's compatible with puppeteer)`);
options['executablePath'] = path;
}
async function createSession(username: string) {
const session = await ElementSession.create(
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
);
sessions.push(session);
return session;
}
let failure = false;
try {
await scenario(createSession);
} catch (err) {
failure = true;
console.log('failure: ', err);
if (program.opts().logDirectory) {
await writeLogs(sessions, program.opts().logDirectory);
}
}
// wait 5 minutes on failure if not running headless
// to inspect what went wrong
if (failure && options.headless === false) {
await new Promise((resolve) => setTimeout(resolve, 5 * 60 * 1000));
}
let performanceEntries;
await Promise.all(sessions.map(async (session) => {
// Collecting all performance monitoring data before closing the session
const measurements = await session.page.evaluate(() => {
let measurements;
// Some tests do redirects away from the app, so don't count those sessions.
if (!window.mxPerformanceMonitor) return JSON.stringify([]);
window.mxPerformanceMonitor.addPerformanceDataCallback({
entryNames: [
window.mxPerformanceEntryNames.REGISTER,
window.mxPerformanceEntryNames.LOGIN,
window.mxPerformanceEntryNames.JOIN_ROOM,
window.mxPerformanceEntryNames.CREATE_DM,
window.mxPerformanceEntryNames.VERIFY_E2EE_USER,
],
callback: (events) => {
measurements = JSON.stringify(events);
},
}, true);
return measurements;
});
/**
* TODO: temporary only use one user session data
*/
performanceEntries = JSON.parse(measurements ?? "[]");
return session.close();
}));
if (performanceEntries?.length > 0) {
fs.writeFileSync(`performance-entries.json`, JSON.stringify(performanceEntries));
}
if (failure) {
process.exit(-1);
} else {
console.log('all tests finished successfully');
}
}
async function writeLogs(sessions, dir) {
const logs = "";
for (let i = 0; i < sessions.length; ++i) {
const session = sessions[i];
const userLogDir = `${dir}/${session.username}`;
try {
fs.mkdirSync(userLogDir);
} catch (e) {
// typically this will be EEXIST. If it's something worse, the next few
// lines will fail too.
console.warn(`non-fatal error creating ${userLogDir} :`, e.message);
}
const consoleLogName = `${userLogDir}/console.log`;
const networkLogName = `${userLogDir}/network.log`;
const appHtmlName = `${userLogDir}/app.html`;
const documentHtml = await session.page.content();
fs.writeFileSync(appHtmlName, documentHtml);
fs.writeFileSync(networkLogName, session.networkLogs());
fs.writeFileSync(consoleLogName, session.consoleLogs());
await session.page.screenshot({ path: `${userLogDir}/screenshot.png` });
}
return logs;
}
runTests().catch(function(err) {
console.log(err);
process.exit(-1);
});

View file

@ -1,2 +0,0 @@
installations
synapse.zip

View file

@ -1,23 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Test Privacy policy</title>
</head>
<body>
{% if has_consented %}
<p>
Thank you, you've already accepted the license.
</p>
{% else %}
<p>
Please accept the license!
</p>
<form method="post" action="consent">
<input type="hidden" name="v" value="{{version}}"/>
<input type="hidden" name="u" value="{{user}}"/>
<input type="hidden" name="h" value="{{userhmac}}"/>
<input type="submit" value="Sure thing!"/>
</form>
{% endif %}
</body>
</html>

View file

@ -1,9 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<title>Test Privacy policy</title>
</head>
<body>
<p>Danke schon</p>
</body>
</html>

View file

@ -1,17 +0,0 @@
#!/bin/bash
set -e
if [ $# -eq 0 ]
then
echo "Prints a configuration directive from the synapse installation"
echo "Usage: getcfg.sh <synapse config file directive>"
exit 1
fi
# activate the virtualenv so we have pyyaml
BASE_DIR=$(cd $(dirname $0) && pwd)
cd $BASE_DIR
cd installations/consent/env/bin/
source activate
python -c "from yaml import load, Loader; import sys; print(load(sys.stdin, Loader=Loader)['$1'])" < homeserver.yaml

View file

@ -1,43 +0,0 @@
#!/bin/bash
set -e
# config
SYNAPSE_BRANCH=develop
INSTALLATION_NAME=consent
SERVER_DIR=installations/$INSTALLATION_NAME
CONFIG_TEMPLATE=consent
PORT=5005
# set current directory to script directory
BASE_DIR=$(cd $(dirname $0) && pwd)
if [ -d $BASE_DIR/$SERVER_DIR ]; then
echo "synapse is already installed"
exit
fi
cd $BASE_DIR
mkdir -p $SERVER_DIR
cd $SERVER_DIR
virtualenv -p python3 env
source env/bin/activate
pip install --upgrade pip
pip install --upgrade setuptools
pip install https://codeload.github.com/matrix-org/synapse/zip/$SYNAPSE_BRANCH
# apply configuration
pushd env/bin/
cp -r $BASE_DIR/config-templates/$CONFIG_TEMPLATE/. ./
# Hashes used instead of slashes because we'll get a value back from $(pwd) that'll be
# full of un-escapable slashes.
# Use .bak suffix as using no suffix doesn't work macOS.
sed -i.bak "s#{{SYNAPSE_ROOT}}#$(pwd)/#g" homeserver.yaml
sed -i.bak "s#{{SYNAPSE_PORT}}#${PORT}#g" homeserver.yaml
sed -i.bak "s#{{FORM_SECRET}}#$(uuidgen)#g" homeserver.yaml
sed -i.bak "s#{{REGISTRATION_SHARED_SECRET}}#$(uuidgen)#g" homeserver.yaml
sed -i.bak "s#{{MACAROON_SECRET_KEY}}#$(uuidgen)#g" homeserver.yaml
rm *.bak
popd
# generate signing keys for signing_key_path
python -m synapse.app.homeserver --generate-keys --config-dir env/bin/ -c env/bin/homeserver.yaml

View file

@ -1,9 +0,0 @@
#!/bin/bash
set -e
BASE_DIR=$(cd $(dirname $0) && pwd)
cd $BASE_DIR
cd installations/consent/env/bin/
source activate
echo "Synapse log file at $LOGFILE"
./synctl start 2> $LOGFILE

View file

@ -1,8 +0,0 @@
#!/bin/bash
set -e
BASE_DIR=$(cd $(dirname $0) && pwd)
cd $BASE_DIR
cd installations/consent/env/bin/
source activate
./synctl stop

View file

@ -1,23 +0,0 @@
{
"compilerOptions": {
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"resolveJsonModule": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
"noImplicitAny": false,
"sourceMap": false,
"outDir": "./lib",
"declaration": true,
"lib": [
"es2019",
"dom",
"dom.iterable"
],
},
"include": [
"./src/**/*.ts",
"start.ts"
]
}

View file

@ -1,886 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@*":
version "17.0.23"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da"
integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==
"@types/puppeteer@^5.4.4":
version "5.4.5"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.5.tgz#154e3850a77bfd3967f036680de8ddc88eb3a12b"
integrity sha512-lxCjpDEY+DZ66+W3x5Af4oHnEmUXt0HuaRzkBGE2UZiZEp/V1d3StpLPlmNVu/ea091bdNmVPl44lu8Wy/0ZCA==
dependencies:
"@types/node" "*"
"@types/yauzl@^2.9.1":
version "2.9.1"
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af"
integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==
dependencies:
"@types/node" "*"
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
ajv@^6.5.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
dependencies:
safer-buffer "~2.1.0"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
dependencies:
tweetnacl "^0.14.3"
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer@^5.2.1, buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
cheerio@^1.0.0-rc.2:
version "1.0.0-rc.2"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
integrity sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=
dependencies:
css-select "~1.2.0"
dom-serializer "~0.1.0"
entities "~1.1.1"
htmlparser2 "^3.9.1"
lodash "^4.15.0"
parse5 "^3.0.1"
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
combined-stream@^1.0.6, combined-stream@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
dependencies:
delayed-stream "~1.0.0"
commander@^9:
version "9.0.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40"
integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
core-util-is@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
cross-fetch@3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
css-select@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
dependencies:
boolbase "~1.0.0"
css-what "2.1"
domutils "1.5.1"
nth-check "~1.0.1"
css-what@2.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
dependencies:
assert-plus "^1.0.0"
debug@4, debug@^4.1.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
dependencies:
ms "2.1.2"
debug@4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
devtools-protocol@0.0.960912:
version "0.0.960912"
resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.960912.tgz#411c1fa355eddb72f06c4a8743f2808766db6245"
integrity sha512-I3hWmV9rWHbdnUdmMKHF2NuYutIM2kXz2mdXW8ha7TbRlGTVs+PF+PsB5QWvpCek4Fy9B+msiispCfwlhG5Sqg==
dom-serializer@0, dom-serializer@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
dependencies:
domelementtype "^1.3.0"
entities "^1.1.1"
domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domhandler@^2.3.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==
dependencies:
domelementtype "1"
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=
dependencies:
dom-serializer "0"
domelementtype "1"
domutils@^1.5.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
dependencies:
dom-serializer "0"
domelementtype "1"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
entities@^1.1.1, entities@~1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
extract-zip@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
dependencies:
debug "^4.1.1"
get-stream "^5.1.0"
yauzl "^2.10.0"
optionalDependencies:
"@types/yauzl" "^2.9.1"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
find-up@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
dependencies:
locate-path "^5.0.0"
path-exists "^4.0.0"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
form-data@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
get-stream@^5.1.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
dependencies:
pump "^3.0.0"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
dependencies:
assert-plus "^1.0.0"
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
har-validator@~5.1.0:
version "5.1.3"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
dependencies:
ajv "^6.5.5"
har-schema "^2.0.0"
htmlparser2@^3.9.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
dependencies:
domelementtype "^1.3.1"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
readable-stream "^3.1.1"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
https-proxy-agent@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
dependencies:
agent-base "6"
debug "4"
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.2.3"
verror "1.10.0"
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
dependencies:
p-locate "^4.1.0"
lodash@^4.15.0, lodash@^4.17.11:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
mime-db@~1.38.0:
version "1.38.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.22"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd"
integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==
dependencies:
mime-db "~1.38.0"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
nth-check@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
dependencies:
boolbase "~1.0.0"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
dependencies:
p-try "^2.0.0"
p-locate@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
dependencies:
p-limit "^2.2.0"
p-try@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
parse5@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
dependencies:
"@types/node" "*"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pkg-dir@4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
dependencies:
find-up "^4.0.0"
progress@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
proxy-from-env@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
psl@^1.1.24, psl@^1.1.28:
version "1.1.31"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
puppeteer@13.4.1:
version "13.4.1"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-13.4.1.tgz#495b91d2fae3e9761a31bab1820ad179caac0fd9"
integrity sha512-2arcYPEGvLV9HvOw01Zv1b1IAXrMWHqsFJn0Hn00qe9HtCmaF0b8FlrbdLjCIbkaFc6icH5+GqcG8R5KxlJSRg==
dependencies:
cross-fetch "3.1.5"
debug "4.3.3"
devtools-protocol "0.0.960912"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
pkg-dir "4.2.0"
progress "2.0.3"
proxy-from-env "1.1.0"
rimraf "3.0.2"
tar-fs "2.1.1"
unbzip2-stream "1.4.3"
ws "8.5.0"
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
readable-stream@^3.1.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@^3.4.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
request-promise-core@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.2.tgz#339f6aababcafdb31c799ff158700336301d3346"
integrity sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==
dependencies:
lodash "^4.17.11"
request-promise-native@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.7.tgz#a49868a624bdea5069f1251d0a836e0d89aa2c59"
integrity sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==
dependencies:
request-promise-core "1.1.2"
stealthy-require "^1.1.1"
tough-cookie "^2.3.3"
request@^2.88.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.0"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.4.3"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
rimraf@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
bcrypt-pbkdf "^1.0.0"
dashdash "^1.12.0"
ecc-jsbn "~0.1.1"
getpass "^0.1.1"
jsbn "~0.1.0"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stealthy-require@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
dependencies:
safe-buffer "~5.1.0"
tar-fs@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
tough-cookie@^2.3.3:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
dependencies:
psl "^1.1.28"
punycode "^2.1.1"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
dependencies:
psl "^1.1.24"
punycode "^1.4.1"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
dependencies:
safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
typescript@^4.5.3:
version "4.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.3.tgz#afaa858e68c7103317d89eb90c5d8906268d353c"
integrity sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==
unbzip2-stream@1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
dependencies:
buffer "^5.2.1"
through "^2.3.8"
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f"
integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"