diff --git a/.eslintrc.js b/.eslintrc.js index 9efdd128548..67e6ab1e642 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -92,6 +92,7 @@ module.exports = { files: [ "src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", + "cypress/**/*.ts", ], extends: [ "plugin:matrix-org/typescript", diff --git a/.github/workflows/element-build-and-test.yaml b/.github/workflows/element-build-and-test.yaml new file mode 100644 index 00000000000..e88c5105464 --- /dev/null +++ b/.github/workflows/element-build-and-test.yaml @@ -0,0 +1,49 @@ +# Produce a build of element-web with this version of react-sdk +# and any matching branches of element-web and js-sdk, output it +# as an artifact and run integration tests. +name: Element Web - Build and Test +on: + pull_request: +jobs: + build: + runs-on: ubuntu-latest + env: + # This must be set for fetchdep.sh to get the right branch + PR_NUMBER: ${{github.event.number}} + steps: + - uses: actions/checkout@v2 + - name: Build + run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: previewbuild + path: element-web/webapp + # We'll only use this in a triggered job, then we're done with it + retention-days: 1 + cypress: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Download build + uses: actions/download-artifact@v3 + with: + name: previewbuild + path: webapp + - name: Run Cypress tests + uses: cypress-io/github-action@v2 + with: + # The built in Electron runner seems to grind to a halt trying + # to run the tests, so use chrome. + browser: chrome + start: npx serve -p 8080 webapp + - name: Upload Artifact + if: failure() + uses: actions/upload-artifact@v2 + with: + name: cypress-results + path: | + cypress/screenshots + cypress/videos + cypress/dockerlogs diff --git a/.github/workflows/layered-build.yaml b/.github/workflows/layered-build.yaml deleted file mode 100644 index 1610f0e66e2..00000000000 --- a/.github/workflows/layered-build.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Produce a 'layered build' (a build of element-web with this version of -# react-sdk) and output it as an artifact -name: Layered Preview Build -on: - pull_request: -jobs: - build: - runs-on: ubuntu-latest - env: - # This must be set for fetchdep.sh to get the right branch - PR_NUMBER: ${{github.event.number}} - steps: - - uses: actions/checkout@v2 - - name: Build - run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build - - name: Upload Artifact - uses: actions/upload-artifact@v2 - with: - name: previewbuild - path: element-web/webapp - # We'll only use this in a triggered job, then we're done with it - retention-days: 1 - diff --git a/.gitignore b/.gitignore index 3137cd555b1..8e14ba9057b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,11 @@ package-lock.json .vscode .vscode/ + +/cypress/videos +/cypress/downloads +/cypress/screenshots +/cypress/synapselogs +# These could have files in them but don't currently +# Cypress will still auto-create them though... +/cypress/fixtures diff --git a/cypress.json b/cypress.json new file mode 100644 index 00000000000..4c1ed2d5856 --- /dev/null +++ b/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:8080", + "videoUploadOnPasses": false +} diff --git a/cypress/integration/1-register/register.spec.ts b/cypress/integration/1-register/register.spec.ts new file mode 100644 index 00000000000..f719da55477 --- /dev/null +++ b/cypress/integration/1-register/register.spec.ts @@ -0,0 +1,52 @@ +/* +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 { SynapseInstance } from "../../plugins/synapsedocker/index"; + +describe("Registration", () => { + let synapseId; + let synapsePort; + + beforeEach(() => { + cy.task("synapseStart", "consent").then(result => { + synapseId = result.synapseId; + synapsePort = result.port; + }); + cy.visit("/#/register"); + }); + + afterEach(() => { + cy.task("synapseStop", synapseId); + }); + + it("registers an account and lands on the home screen", () => { + cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click(); + cy.get(".mx_ServerPickerDialog_otherHomeserver").type(`http://localhost:${synapsePort}`); + cy.get(".mx_ServerPickerDialog_continue").click(); + // wait for the dialog to go away + cy.get('.mx_ServerPickerDialog').should('not.exist'); + cy.get("#mx_RegistrationForm_username").type("alice"); + cy.get("#mx_RegistrationForm_password").type("totally a great password"); + cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password"); + cy.get(".mx_Login_submit").click(); + cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click(); + cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click(); + cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click(); + cy.url().should('contain', '/#/home'); + }); +}); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts new file mode 100644 index 00000000000..db01ceceb4f --- /dev/null +++ b/cypress/plugins/index.ts @@ -0,0 +1,23 @@ +/* +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 { synapseDocker } from "./synapsedocker/index"; + +export default function(on, config) { + synapseDocker(on, config); +} diff --git a/cypress/plugins/synapsedocker/index.ts b/cypress/plugins/synapsedocker/index.ts new file mode 100644 index 00000000000..0f029e7b2ed --- /dev/null +++ b/cypress/plugins/synapsedocker/index.ts @@ -0,0 +1,212 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/// + +import * as path from "path"; +import * as os from "os"; +import * as crypto from "crypto"; +import * as childProcess from "child_process"; +import * as fse from "fs-extra"; + +// A cypress plugins to add command to start & stop synapses in +// docker with preset templates. + +interface SynapseConfig { + configDir: string; + registrationSecret: string; +} + +export interface SynapseInstance extends SynapseConfig { + synapseId: string; + port: number; +} + +const synapses = new Map(); + +function randB64Bytes(numBytes: number): string { + return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, ""); +} + +async function cfgDirFromTemplate(template: string): Promise { + const templateDir = path.join(__dirname, "templates", template); + + const stats = await fse.stat(templateDir); + if (!stats?.isDirectory) { + throw new Error(`No such template: ${template}`); + } + const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-')); + + // change permissions on the temp directory so the docker container can see its contents + await fse.chmod(tempDir, 0o777); + + // copy the contents of the template dir, omitting homeserver.yaml as we'll template that + console.log(`Copy ${templateDir} -> ${tempDir}`); + await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' }); + + const registrationSecret = randB64Bytes(16); + const macaroonSecret = randB64Bytes(16); + const formSecret = randB64Bytes(16); + + // now copy homeserver.yaml, applying sustitutions + console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`); + let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8"); + hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret); + hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret); + hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret); + await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml); + + // now generate a signing key (we could use synapse's config generation for + // this, or we could just do this...) + // NB. This assumes the homeserver.yaml specifies the key in this location + const signingKey = randB64Bytes(32); + console.log(`Gen ${path.join(templateDir, "localhost.signing.key")}`); + await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`); + + return { + configDir: tempDir, + registrationSecret, + }; +} + +// Start a synapse instance: the template must be the name of +// one of the templates in the cypress/plugins/synapsedocker/templates +// directory +async function synapseStart(template: string): Promise { + const synCfg = await cfgDirFromTemplate(template); + + console.log(`Starting synapse with config dir ${synCfg.configDir}...`); + + const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`; + + const synapseId = await new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "run", + "--name", containerName, + "-d", + "-v", `${synCfg.configDir}:/data`, + "-p", "8008/tcp", + "matrixdotorg/synapse:develop", + "run", + ], (err, stdout) => { + if (err) reject(err); + resolve(stdout.trim()); + }); + }); + + // Get the port that docker allocated: specifying only one + // port above leaves docker to just grab a free one, although + // in hindsight we need to put the port in public_baseurl in the + // config really, so this will probably need changing to use a fixed + // / configured port. + const port = await new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "port", synapseId, "8008", + ], { encoding: 'utf8' }, (err, stdout) => { + if (err) reject(err); + resolve(Number(stdout.trim().split(":")[1])); + }); + }); + + synapses.set(synapseId, Object.assign({ + port, + synapseId, + }, synCfg)); + + console.log(`Started synapse with id ${synapseId} on port ${port}.`); + return synapses.get(synapseId); +} + +async function synapseStop(id) { + const synCfg = synapses.get(id); + + if (!synCfg) throw new Error("Unknown synapse ID"); + + try { + const synapseLogsPath = path.join("cypress", "synapselogs", id); + await fse.ensureDir(synapseLogsPath); + + const stdoutFile = await fse.open(path.join(synapseLogsPath, "stdout.log"), "w"); + const stderrFile = await fse.open(path.join(synapseLogsPath, "stderr.log"), "w"); + await new Promise((resolve, reject) => { + childProcess.spawn('docker', [ + "logs", + id, + ], { + stdio: ["ignore", stdoutFile, stderrFile], + }).once('close', resolve); + }); + await fse.close(stdoutFile); + await fse.close(stderrFile); + + await new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "stop", + id, + ], err => { + if (err) reject(err); + resolve(); + }); + }); + } finally { + await new Promise((resolve, reject) => { + childProcess.execFile('docker', [ + "rm", + id, + ], err => { + if (err) reject(err); + resolve(); + }); + }); + } + + await fse.remove(synCfg.configDir); + + synapses.delete(id); + + console.log(`Stopped synapse id ${id}.`); + // cypres deliberately fails if you return 'undefined', so + // return null to signal all is well and we've handled the task. + return null; +} + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +export function synapseDocker(on, config) { + on("task", { + synapseStart, synapseStop, + }); + + on("after:spec", async (spec) => { + // Cleans up any remaining synapse instances after a spec run + // This is on the theory that we should avoid re-using synapse + // instances between spec runs: they should be cheap enough to + // start that we can have a separate one for each spec run or even + // test. If we accidentally re-use synapses, we could inadvertantly + // make our tests depend on each other. + for (const synId of synapses.keys()) { + console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`); + await synapseStop(synId); + } + }); + + on("before:run", async () => { + // tidy up old synapse log files before each run + await fse.emptyDir(path.join("cypress", "synapselogs")); + }); +} diff --git a/cypress/plugins/synapsedocker/templates/COPYME/README.md b/cypress/plugins/synapsedocker/templates/COPYME/README.md new file mode 100644 index 00000000000..df1ed89e6e4 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/COPYME/README.md @@ -0,0 +1,3 @@ +# Meta-template for synapse templates + +To make another template, you can copy this directory diff --git a/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml b/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml new file mode 100644 index 00000000000..fab1bc1c451 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/COPYME/homeserver.yaml @@ -0,0 +1,72 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +# XXX: This won't actually be right: it lets docker allocate an ephemeral port, +# so we have a chicken-and-egg problem +public_baseurl: http://localhost:8008/ +# Listener is always port 8008 (configured in the container) +listeners: + - port: 8008 + tls: false + bind_addresses: ['::'] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + +# An sqlite in-memory database is fast & automatically wipes each time +database: + name: "sqlite3" + args: + database: ":memory:" + +# Needs to be configured to log to the console like a good docker process +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +# These placeholders will be be replaced with values generated at start +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +# Signing key must be here: it will be generated to this file +signing_key_path: "/data/localhost.signing.key" +email: + enable_notifs: false + smtp_host: "localhost" + smtp_port: 25 + smtp_user: "exampleusername" + smtp_pass: "examplepassword" + require_transport_security: False + notif_from: "Your Friendly %(app)s homeserver " + app_name: Matrix + notif_template_html: notif_mail.html + notif_template_text: notif_mail.txt + notif_for_new_users: True + client_base_url: "http://localhost/element" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true diff --git a/cypress/plugins/synapsedocker/templates/COPYME/log.config b/cypress/plugins/synapsedocker/templates/COPYME/log.config new file mode 100644 index 00000000000..ac232762da3 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/COPYME/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/plugins/synapsedocker/templates/consent/README.md b/cypress/plugins/synapsedocker/templates/consent/README.md new file mode 100644 index 00000000000..713e55f9d51 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/README.md @@ -0,0 +1 @@ +A synapse configured with user privacy consent enabled diff --git a/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml b/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml new file mode 100644 index 00000000000..e26133f6d11 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/homeserver.yaml @@ -0,0 +1,84 @@ +server_name: "localhost" +pid_file: /data/homeserver.pid +public_baseurl: http://localhost:5005/ +listeners: + - port: 8008 + tls: false + bind_addresses: ['::'] + type: http + x_forwarded: true + + resources: + - names: [client, federation, consent] + compress: false + +database: + name: "sqlite3" + args: + database: ":memory:" + +log_config: "/data/log.config" + +rc_messages_per_second: 10000 +rc_message_burst_count: 10000 +rc_registration: + per_second: 10000 + burst_count: 10000 + +rc_login: + address: + per_second: 10000 + burst_count: 10000 + account: + per_second: 10000 + burst_count: 10000 + failed_attempts: + per_second: 10000 + burst_count: 10000 + +media_store_path: "/data/media_store" +uploads_path: "/data/uploads" +enable_registration: true +enable_registration_without_verification: true +disable_msisdn_registration: false +registration_shared_secret: "{{REGISTRATION_SECRET}}" +report_stats: false +macaroon_secret_key: "{{MACAROON_SECRET_KEY}}" +form_secret: "{{FORM_SECRET}}" +signing_key_path: "/data/localhost.signing.key" +email: + enable_notifs: false + smtp_host: "localhost" + smtp_port: 25 + smtp_user: "exampleusername" + smtp_pass: "examplepassword" + require_transport_security: False + notif_from: "Your Friendly %(app)s homeserver " + app_name: Matrix + notif_template_html: notif_mail.html + notif_template_text: notif_mail.txt + notif_for_new_users: True + client_base_url: "http://localhost/element" + +user_consent: + template_dir: /data/res/templates/privacy + version: 1.0 + server_notice_content: + msgtype: m.text + body: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + send_server_notice_to_guests: True + block_events_error: >- + To continue using this homeserver you must review and agree to the + terms and conditions at %(consent_uri)s + require_at_registration: true + +server_notices: + system_mxid_localpart: notices + system_mxid_display_name: "Server Notices" + system_mxid_avatar_url: "mxc://localhost:5005/oumMVlgDnLYFaPVkExemNVVZ" + room_name: "Server Notices" +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true diff --git a/cypress/plugins/synapsedocker/templates/consent/log.config b/cypress/plugins/synapsedocker/templates/consent/log.config new file mode 100644 index 00000000000..ac232762da3 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/log.config @@ -0,0 +1,50 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3.7/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + + twisted: + # We send the twisted logging directly to the file handler, + # to work around https://github.com/matrix-org/synapse/issues/3471 + # when using "buffer" logger. Use "console" to log to stderr instead. + handlers: [console] + propagate: false + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [console] + +disable_existing_loggers: false diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html new file mode 100644 index 00000000000..d4959b4bcb3 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/1.0.html @@ -0,0 +1,23 @@ + + + + Test Privacy policy + + + {% if has_consented %} +

+ Thank you, you've already accepted the license. +

+ {% else %} +

+ Please accept the license! +

+
+ + + + +
+ {% endif %} + + \ No newline at end of file diff --git a/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html new file mode 100644 index 00000000000..abe27d87ca1 --- /dev/null +++ b/cypress/plugins/synapsedocker/templates/consent/res/templates/privacy/en/success.html @@ -0,0 +1,9 @@ + + + + Test Privacy policy + + +

Danke schon

+ + \ No newline at end of file diff --git a/cypress/support/index.ts b/cypress/support/index.ts new file mode 100644 index 00000000000..9901ef4cb80 --- /dev/null +++ b/cypress/support/index.ts @@ -0,0 +1,3 @@ +// Empty file to prevent cypress from recreating a helpful example +// file on every run (their example file doesn't use semicolons and +// so fails our lint rules). diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 00000000000..85239e1a2a7 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "target": "es2016", + "lib": ["es2020", "dom"], + "types": ["cypress"], + "moduleResolution": "node" + }, + "include": ["**/*.ts"] +} diff --git a/package.json b/package.json index dd1db92a717..987c12eb06d 100644 --- a/package.json +++ b/package.json @@ -44,11 +44,13 @@ "start:all": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:build", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:js && yarn lint:style", - "lint:js": "eslint --max-warnings 0 src test", - "lint:js-fix": "eslint --fix src test", - "lint:types": "tsc --noEmit --jsx react", + "lint:js": "eslint --max-warnings 0 src test cypress", + "lint:js-fix": "eslint --fix src test cypress", + "lint:types": "tsc --noEmit --jsx react && tsc --noEmit -p cypress", "lint:style": "stylelint \"res/css/**/*.scss\"", "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" }, @@ -132,7 +134,7 @@ "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz", "@peculiar/webcrypto": "^1.1.4", "@sentry/types": "^6.10.0", - "@sinonjs/fake-timers": "^7.0.2", + "@sinonjs/fake-timers": "^9.1.2", "@types/classnames": "^2.2.11", "@types/commonmark": "^0.27.4", "@types/counterpart": "^0.18.1", @@ -142,6 +144,7 @@ "@types/escape-html": "^1.0.1", "@types/file-saver": "^2.0.3", "@types/flux": "^3.1.9", + "@types/fs-extra": "^9.0.13", "@types/jest": "^26.0.20", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", @@ -163,6 +166,7 @@ "babel-jest": "^26.6.3", "blob-polyfill": "^6.0.20211015", "chokidar": "^3.5.1", + "cypress": "^9.5.4", "enzyme": "^3.11.0", "enzyme-to-json": "^3.6.2", "eslint": "8.9.0", @@ -172,6 +176,7 @@ "eslint-plugin-matrix-org": "^0.4.0", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", + "fs-extra": "^10.0.1", "glob": "^7.1.6", "jest": "^27.4.0", "jest-canvas-mock": "^2.3.0", diff --git a/test/end-to-end-tests/src/scenario.ts b/test/end-to-end-tests/src/scenario.ts index e6a588eac96..dc6e1309d78 100644 --- a/test/end-to-end-tests/src/scenario.ts +++ b/test/end-to-end-tests/src/scenario.ts @@ -41,6 +41,7 @@ export async function scenario(createSession: (s: string) => Promise=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: +minimist@>=1.2.2, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -6614,7 +7021,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^4.0.1: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -6751,7 +7158,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -6787,6 +7194,11 @@ opus-recorder@^8.0.3: resolved "https://registry.yarnpkg.com/opus-recorder/-/opus-recorder-8.0.5.tgz#06d3e32e15da57ebc3f57e41b93033475fcb4e3e" integrity sha512-tBRXc9Btds7i3bVfA7d5rekAlyOcfsivt5vSIXHxRV1Oa+s6iXFW8omZ0Lm3ABWotVcEyKt96iIIUcgbV07YOw== +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= + p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -6827,6 +7239,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-retry@^4.5.0: version "4.6.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.1.tgz#8fcddd5cdf7a67a0911a9cf2ef0e5df7f602316c" @@ -6949,6 +7368,11 @@ pbf@^3.2.1: ieee754 "^1.1.12" resolve-protobuf-schema "^2.1.0" +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" @@ -6969,6 +7393,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -7118,6 +7547,11 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + pretty-format@^26.0.0, pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" @@ -7176,6 +7610,11 @@ protocol-buffers-schema@^3.3.1: resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03" integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw== +proxy-from-env@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" + integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= + psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -7581,6 +8020,13 @@ repeat-string@^1.0.0, repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= + dependencies: + throttleit "^1.0.0" + request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -7673,6 +8119,14 @@ resolve@^2.0.0-next.3: is-core-module "^2.2.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -7693,6 +8147,11 @@ rfc4648@^1.4.0: resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.1.tgz#b0b16756e33d9de8c0c7833e94b28e627ec372a4" integrity sha512-60e/YWs2/D3MV1ErdjhJHcmlgnyLUiG4X/14dgsfm9/zmCWLN16xI6YqJYSCd/OANM7bUNzJqPY5B8/02S9Ibw== +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -7730,6 +8189,13 @@ rw@^1.3.3: resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= +rxjs@^7.5.1: + version "7.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" + integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== + dependencies: + tslib "^2.1.0" + safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -7910,6 +8376,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -8041,7 +8516,7 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sshpk@^1.7.0: +sshpk@^1.14.1, sshpk@^1.7.0: version "1.17.0" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== @@ -8311,7 +8786,7 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: +supports-color@^8.0.0, supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== @@ -8384,6 +8859,16 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + +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= + timers-ext@^0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" @@ -8407,6 +8892,13 @@ tmatch@^2.0.1: resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf" integrity sha1-DFYkbzPzDaG409colauvFmYPOM8= +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -8503,7 +8995,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -8692,6 +9184,11 @@ universalify@^0.1.2: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" @@ -8700,6 +9197,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -8753,6 +9255,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -8993,6 +9500,15 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -9112,6 +9628,14 @@ yargs@^17.0.1: y18n "^5.0.5" yargs-parser "^21.0.0" +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" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"