diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8de45c9f3d4d..5c122f5ef1e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,6 @@ Thanks for taking the time to contribute! :smile: ## Table of Contents -- [CI Status](#ci-status) - [Code of Conduct](#code-of-conduct) - [Opening Issues](#opening-issues) - [Triaging Issues](#triaging-issues) @@ -42,20 +41,6 @@ Thanks for taking the time to contribute! :smile: - [Code Review of Dependency Updates](#Code-Review-of-Dependency-Updates) - [Deployment](#deployment) -## CI status - -| Build status | Description | -| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------- | -| [![CircleCI](https://circleci.com/gh/cypress-io/cypress-test-node-versions.svg?style=svg&circle-token=6a7c4e7e7ab427e11bea6c2af3df29c4491d2376)](https://circleci.com/gh/cypress-io/cypress-test-node-versions) | [cypress-test-node-versions](https://github.com/cypress-io/cypress-test-node-versions) | -| [![CircleCI](https://circleci.com/gh/cypress-io/cypress-test-ci-environments.svg?style=svg&circle-token=66a4d36c3966cbe476f13e7dfbe3af0693db3fb9)](https://circleci.com/gh/cypress-io/cypress-test-ci-environments) | [cypress-test-ci-environments](https://github.com/cypress-io/cypress-test-ci-environments) | -| [![CircleCI](https://circleci.com/gh/cypress-io/cypress-test-module-api.svg?style=svg&circle-token=317f79ae796e0ffd6cc7dd90859c0f67e5a306e7)](https://circleci.com/gh/cypress-io/cypress-test-module-api) | [cypress-test-module-api](https://github.com/cypress-io/cypress-test-module-api) | -| [![CircleCI](https://circleci.com/gh/cypress-io/cypress-test-nested-projects.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress-test-nested-projects) | [cypress-test-nested-projects](https://github.com/cypress-io/cypress-test-nested-projects) | -| [![CircleCI](https://circleci.com/gh/cypress-io/cypress-on.svg?style=svg&circle-token=51ba85f5720654ee58212f45f6b9afc56d55d52a)](https://circleci.com/gh/cypress-io/cypress-on) | [cypress-on](https://github.com/cypress-io/cypress-on) | -| [![CircleCI](https://circleci.com/gh/cypress-io/cypress-test-node-versions.svg?style=svg&circle-token=6a7c4e7e7ab427e11bea6c2af3df29c4491d2376)](https://circleci.com/gh/cypress-io/cypress-test-node-versions) | [cypress-test-example-repos](https://github.com/cypress-io/cypress-test-example-repos) | -| [![CircleCI](https://circleci.com/gh/cypress-io/docsearch-scraper.svg?style=svg&circle-token=8087137233788ec1eab4f79d4451392ca53183b2)](https://circleci.com/gh/cypress-io/docsearch-scraper) | [docsearch-scraper](https://github.com/cypress-io/docsearch-scraper) | -| [![Docker Build Status](https://img.shields.io/docker/build/cypress/base.svg)](https://hub.docker.com/r/cypress/base/) | [cypress-docker-images](https://github.com/cypress-io/cypress-docker-images) | -| [![Build status](https://ci.appveyor.com/api/projects/status/ln8tg3dv42nk916c?svg=true)](https://ci.appveyor.com/project/cypress-io/cypress) | Windows CI | - ## Code of Conduct All contributors are expecting to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). diff --git a/__snapshots__/bump-spec.js b/__snapshots__/bump-spec.js index f9bd3d1682ce..73a99a47b3bd 100644 --- a/__snapshots__/bump-spec.js +++ b/__snapshots__/bump-spec.js @@ -1,40 +1,15 @@ exports['list of all projects'] = [ - { - "repo": "cypress-io/cypress-test-example-repos", - "provider": "circle", - "platform": "win32" - }, { "repo": "cypress-io/cypress-test-module-api", "provider": "circle", "platform": "linux" - }, - { - "repo": "cypress-io/cypress-test-node-versions", - "provider": "circle", - "platform": "linux" - }, - { - "repo": "cypress-io/cypress-test-ci-environments", - "provider": "circle", - "platform": "linux" - }, - { - "repo": "cypress-io/cypress-test-example-repos", - "provider": "circle", - "platform": "linux" - }, - { - "repo": "cypress-io/cypress-test-example-repos", - "provider": "circle", - "platform": "darwin" } ] -exports['should have just circle and darwin projects'] = [ +exports['should have just circle and linux projects'] = [ { - "repo": "cypress-io/cypress-test-example-repos", + "repo": "cypress-io/cypress-test-module-api", "provider": "circle", - "platform": "darwin" + "platform": "linux" } ] diff --git a/browser-versions.json b/browser-versions.json index beec05263965..b2731c658fbd 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { "chrome:beta": "99.0.4844.27", - "chrome:stable": "98.0.4758.80" + "chrome:stable": "98.0.4758.102" } diff --git a/circle.yml b/circle.yml index c292c4913797..2edd9135a6af 100644 --- a/circle.yml +++ b/circle.yml @@ -11,7 +11,7 @@ defaults: &defaults type: boolean default: false executor: <> - environment: + environment: &defaultsEnvironment ## set specific timezone TZ: "/usr/share/zoneinfo/America/New_York" @@ -29,7 +29,7 @@ mainBuildFilters: &mainBuildFilters only: - develop - 10.0-release - - fix-ci-artifact-uploads + - fix-darwin-win32-node-modules-install # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -38,7 +38,7 @@ macWorkflowFilters: &mac-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ renovate/cypress-request-2.x, << pipeline.git.branch >> ] + - equal: [ fix-darwin-win32-node-modules-install, << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -48,7 +48,7 @@ windowsWorkflowFilters: &windows-workflow-filters or: - equal: [ master, << pipeline.git.branch >> ] - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ test-binary-downstream-windows, << pipeline.git.branch >> ] + - equal: [ fix-darwin-win32-node-modules-install, << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -167,9 +167,12 @@ commands: - run: name: Generate Circle Cache Key command: node scripts/circle-cache.js --action cacheKey > circle_cache_key + - run: + name: Generate platform key + command: echo $PLATFORM > platform_key - restore_cache: name: Restore cache state, to check for known modules cache existence - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} - run: name: Move node_modules back from /tmp command: | @@ -190,11 +193,14 @@ commands: - run: name: Generate Circle Cache key for system tests command: ./system-tests/scripts/cache-key.sh > system_tests_cache_key + - run: + name: Generate platform key + command: echo $PLATFORM > platform_key - restore_cache: name: Restore system tests node_modules cache keys: - - v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - - v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache- + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache- update_cached_system_tests_deps: description: 'Update the cached node_modules for projects in "system-tests/projects/**"' @@ -202,36 +208,42 @@ commands: - run: name: Generate Circle Cache key for system tests command: ./system-tests/scripts/cache-key.sh > system_tests_cache_key + - run: + name: Generate platform key + command: echo $PLATFORM > platform_key - restore_cache: name: Restore cache state, to check for known modules cache existence keys: - - v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache-state-{{ checksum "system_tests_cache_key" }} + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-state-{{ checksum "system_tests_cache_key" }} + - run: + name: Send root honeycomb event for this CI build + command: cd system-tests/scripts && node ./send-root-honecomb-event.js - run: name: Bail if specific cache exists command: | - if [[ -f "system_tests_node_modules_installed" ]]; then + if [[ -f "/tmp/system_tests_node_modules_installed" ]]; then echo "No updates to system tests node modules, exiting" circleci-agent step halt fi - restore_cache: name: Restore system tests node_modules cache keys: - - v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - - v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache- + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache- - run: name: Update system-tests node_modules cache command: yarn workspace @tooling/system-tests projects:yarn:install - save_cache: name: Save system tests node_modules cache - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} paths: - ~/.cache/cy-system-tests-node-modules - - run: touch system_tests_node_modules_installed + - run: touch /tmp/system_tests_node_modules_installed - save_cache: name: Save system tests node_modules cache state key - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-system-tests-projects-node-modules-cache-state-{{ checksum "system_tests_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-state-{{ checksum "system_tests_cache_key" }} paths: - - system_tests_node_modules_installed + - /tmp/system_tests_node_modules_installed caching-dependency-installer: description: 'Installs & caches the dependencies based on yarn lock & package json dependencies' @@ -244,13 +256,16 @@ commands: - run: name: Generate Circle Cache Key command: node scripts/circle-cache.js --action cacheKey > circle_cache_key + - run: + name: Generate platform key + command: echo $PLATFORM > platform_key - restore_cache: name: Restore cache state, to check for known modules cache existence - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-state-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-state-{{ checksum "circle_cache_key" }} - run: name: Bail if cache exists command: | - if [[ -f "node_modules_installed" ]]; then + if [[ -f "/tmp/node_modules_installed" ]]; then echo "Node modules already cached for dependencies, exiting" circleci-agent step halt fi @@ -258,7 +273,7 @@ commands: - restore_cache: name: Restore weekly yarn cache keys: - - v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-deps-root-weekly-{{ checksum "cache_date" }} + - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }} - run: name: Install Node Modules command: | @@ -271,7 +286,7 @@ commands: steps: - save_cache: name: Saving node modules for root, cli, and all globbed workspace packages - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} paths: - node_modules - cli/node_modules @@ -282,18 +297,18 @@ commands: steps: - save_cache: name: Saving node modules for root, cli, and all globbed workspace packages - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} paths: - /tmp/node_modules_cache - - run: touch node_modules_installed + - run: touch /tmp/node_modules_installed - save_cache: name: Saving node-modules cache state key - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-node-modules-cache-state-{{ checksum "circle_cache_key" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-state-{{ checksum "circle_cache_key" }} paths: - - node_modules_installed + - /tmp/node_modules_installed - save_cache: name: Save weekly yarn cache - key: v{{ .Environment.CACHE_VERSION }}-{{ arch }}-test2-deps-root-weekly-{{ checksum "cache_date" }} + key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }} paths: - ~/.yarn @@ -516,6 +531,24 @@ commands: path: /tmp/artifacts - store-npm-logs + run-binary-system-tests: + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + name: Run system tests + command: | + ALL_SPECS=`circleci tests glob "$HOME/cypress/system-tests/test-binary/*spec*"` + SPECS=`echo $ALL_SPECS | xargs -n 1 | circleci tests split --split-by=timings` + echo SPECS=$SPECS + yarn workspace @tooling/system-tests test:ci $SPECS + - verify-mocha-results + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + store-npm-logs: description: Saves any NPM debug logs as artifacts in case there is a problem steps: @@ -1159,6 +1192,20 @@ jobs: - restore_cached_workspace - update_cached_system_tests_deps + binary-system-tests: + parallelism: 2 + working_directory: ~/cypress + environment: + <<: *defaultsEnvironment + PLATFORM: linux + machine: + # using `machine` gives us a Linux VM that can run Docker + image: ubuntu-2004:202111-02 + docker_layer_caching: true + resource_class: medium + steps: + - run-binary-system-tests + system-tests-chrome: <<: *defaults resource_class: medium @@ -1579,7 +1626,7 @@ jobs: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "fix-ci-artifact-uploads" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "fix-darwin-win32-node-modules-install" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -1887,6 +1934,22 @@ jobs: repo: cypress-example-recipes command: npm run test:ci:firefox + test-binary-against-recipes-chrome: + <<: *defaults + parallelism: 3 + steps: + - test-binary-against-repo: + repo: cypress-example-recipes + command: npm run test:ci:chrome + + test-binary-against-recipes: + <<: *defaults + parallelism: 3 + steps: + - test-binary-against-repo: + repo: cypress-example-recipes + command: npm run test:ci + # This is a special job. It allows you to test the current # built test runner against a pull request in the repo # cypress-example-recipes. @@ -2042,6 +2105,7 @@ linux-workflow: &linux-workflow requires: - build - system-tests-node-modules-install: + context: test-runner:performance-tracking requires: - build - system-tests-chrome: @@ -2222,16 +2286,22 @@ linux-workflow: &linux-workflow <<: *mainBuildFilters requires: - create-build-artifacts - - test-binary-against-kitchensink-chrome: <<: *mainBuildFilters requires: - create-build-artifacts - - test-binary-against-recipes-firefox: <<: *mainBuildFilters requires: - create-build-artifacts + - test-binary-against-recipes-chrome: + <<: *mainBuildFilters + requires: + - create-build-artifacts + - test-binary-against-recipes: + <<: *mainBuildFilters + requires: + - create-build-artifacts - test-binary-against-kitchensink-firefox: <<: *mainBuildFilters requires: @@ -2244,17 +2314,19 @@ linux-workflow: &linux-workflow <<: *mainBuildFilters requires: - create-build-artifacts - - test-binary-as-specific-user: name: "test binary as a non-root user" executor: non-root-docker-user requires: - create-build-artifacts - - test-binary-as-specific-user: name: "test binary as a root user" requires: - create-build-artifacts + - binary-system-tests: + requires: + - create-build-artifacts + - system-tests-node-modules-install mac-workflow: &mac-workflow jobs: diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index fbef758f4646..272fdd908e40 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -1922,26 +1922,6 @@ declare namespace Cypress { * }) */ switchToDomain(domain: string, data: T[], fn: (data: T[]) => void): Chainable - /** - * Enables running Cypress commands in a secondary domain - * @see https://on.cypress.io/switchToDomain - * @example - * cy.switchToDomain('example.com', done, () => { - * done() - * }) - */ - switchToDomain(domain: string, done: Mocha.Done, fn: () => void): Chainable - /** - * Enables running Cypress commands in a secondary domain - * @see https://on.cypress.io/switchToDomain - * @example - * cy.switchToDomain('example.com', done, [{ key: 'value' }, 'foo'], ([{ key }, foo]) => { - * expect(key).to.equal('value') - * expect(foo).to.equal('foo') - * done() - * }) - */ - switchToDomain(domain: string, done: Mocha.Done, data: T[], fn: (data: T[]) => void): Chainable /** * Run a task in Node via the plugins file. diff --git a/guides/release-process.md b/guides/release-process.md index ce2c0e8e5392..44a8b0c1b678 100644 --- a/guides/release-process.md +++ b/guides/release-process.md @@ -197,10 +197,8 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy - [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1) - [cypress-example-realworld](https://github.com/cypress-io/cypress-example-realworld/issues/2) - [cypress-example-recipes](https://github.com/cypress-io/cypress-example-recipes/issues/225) - - [cypress-example-api-testing](https://github.com/cypress-io/cypress-example-api-testing/issues/15) - [angular-pizza-creator](https://github.com/cypress-io/angular-pizza-creator/issues/5) - [cypress-fiddle](https://github.com/cypress-io/cypress-fiddle/issues/5) - - [cypress-example-piechopper](https://github.com/cypress-io/cypress-example-piechopper/issues/75) - [cypress-documentation](https://github.com/cypress-io/cypress-documentation/issues/1313) - [cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose) - Doesn't have a Renovate issue, but will auto-create and auto-merge non-major Cypress updates as long as the tests pass. @@ -209,11 +207,6 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy **Test Repos** - [cypress-test-tiny](https://github.com/cypress-io/cypress-test-tiny) - - [cypress-test-nested-projects](https://github.com/cypress-io/cypress-test-nested-projects) - - [cypress-test-example-repos](https://github.com/cypress-io/cypress-test-example-repos) - - [cypress-test-node-versions](https://github.com/cypress-io/cypress-test-node-versions) - - [cypress-test-module-api](https://github.com/cypress-io/cypress-test-module-api) - - [cypress-test-ci-environments](https://github.com/cypress-io/cypress-test-ci-environments) **Example Repos** @@ -222,8 +215,6 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy - [cypress-example-realworld](https://github.com/cypress-io/cypress-example-realworld) - [cypress-example-recipes](https://github.com/cypress-io/cypress-example-recipes) - [cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose) - - [cypress-example-api-testing](https://github.com/cypress-io/cypress-example-api-testing) - - [cypress-example-piechopper](https://github.com/cypress-io/cypress-example-piechopper) - [cypress-documentation](https://github.com/cypress-io/cypress-documentation) Take a break, you deserve it! :sunglasses: diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json index 09f48045c209..dda6627a31f3 100644 --- a/npm/create-cypress-tests/package.json +++ b/npm/create-cypress-tests/package.json @@ -23,7 +23,7 @@ "commander": "6.1.0", "fast-glob": "3.2.7", "find-up": "5.0.0", - "fs-extra": "^9.0.1", + "fs-extra": "^9.1.0", "glob": "^7.1.6", "inquirer": "7.3.3", "ora": "^5.1.0" diff --git a/npm/react/CHANGELOG.md b/npm/react/CHANGELOG.md index ed06d61a820f..ef127dafa1fe 100644 --- a/npm/react/CHANGELOG.md +++ b/npm/react/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/react-v5.12.3](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.12.2...@cypress/react-v5.12.3) (2022-02-10) + + +### Bug Fixes + +* set correct default when using react-scripts plugin ([#20141](https://github.com/cypress-io/cypress/issues/20141)) ([9b967e0](https://github.com/cypress-io/cypress/commit/9b967e06f5df1e8ae2c5b13d5c7f7170b069f5bc)) + # [@cypress/react-v5.12.2](https://github.com/cypress-io/cypress/compare/@cypress/react-v5.12.1...@cypress/react-v5.12.2) (2022-02-08) diff --git a/npm/react/examples/react-scripts/cypress/plugins/index.js b/npm/react/examples/react-scripts/cypress/plugins/index.js index 33bce3362bea..df575d3a8745 100644 --- a/npm/react/examples/react-scripts/cypress/plugins/index.js +++ b/npm/react/examples/react-scripts/cypress/plugins/index.js @@ -8,7 +8,7 @@ const devServer = require('@cypress/react/plugins/react-scripts') * @type {Cypress.PluginConfig} */ module.exports = (on, config) => { - devServer(on, config) + devServer(on, config, {}) // IMPORTANT to return the config object // with the any changed environment variables diff --git a/npm/react/plugins/react-scripts/findReactScriptsWebpackConfig.js b/npm/react/plugins/react-scripts/findReactScriptsWebpackConfig.js index 48c8402f7051..98d9fc7fb058 100644 --- a/npm/react/plugins/react-scripts/findReactScriptsWebpackConfig.js +++ b/npm/react/plugins/react-scripts/findReactScriptsWebpackConfig.js @@ -7,9 +7,11 @@ const { getTranspileFolders } = require('../utils/get-transpile-folders') const { addFolderToBabelLoaderTranspileInPlace } = require('../utils/babel-helpers') const { reactScriptsFiveModifications, isReactScripts5 } = require('../../dist/react-scripts/reactScriptsFive') -module.exports = function findReactScriptsWebpackConfig (config, { - webpackConfigPath, -} = { webpackConfigPath: 'react-scripts/config/webpack.config' }) { +module.exports = function findReactScriptsWebpackConfig (config, devServerOptions) { + const webpackConfigPath = (devServerOptions && devServerOptions.webpackConfigPath) + ? devServerOptions.webpackConfigPath + : 'react-scripts/config/webpack.config' + // this is required because // 1) we use our own HMR and we don't need react-refresh transpiling overhead // 2) it doesn't work with process.env=test @see https://github.com/cypress-io/cypress-realworld-app/pull/832 diff --git a/npm/vue/CHANGELOG.md b/npm/vue/CHANGELOG.md index 7bf89fc516c0..36a6bc47d375 100644 --- a/npm/vue/CHANGELOG.md +++ b/npm/vue/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/vue-v3.1.1](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.1.0...@cypress/vue-v3.1.1) (2022-02-10) + + +### Bug Fixes + +* create a dummy commit to trigger release ([97e6c14](https://github.com/cypress-io/cypress/commit/97e6c14b91661658b856038da8a0f5fa4319b19b)) + # [@cypress/vue-v3.1.0](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.0.5...@cypress/vue-v3.1.0) (2021-12-16) diff --git a/npm/vue/src/shims-vue.d.ts b/npm/vue/src/shims-vue.d.ts index a1dbcdb9adfe..f43ed923b679 100644 --- a/npm/vue/src/shims-vue.d.ts +++ b/npm/vue/src/shims-vue.d.ts @@ -2,4 +2,4 @@ declare module '*.vue' { import { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component - } \ No newline at end of file +} \ No newline at end of file diff --git a/npm/webpack-batteries-included-preprocessor/package.json b/npm/webpack-batteries-included-preprocessor/package.json index 11b198f730b7..0d7f17f02c64 100644 --- a/npm/webpack-batteries-included-preprocessor/package.json +++ b/npm/webpack-batteries-included-preprocessor/package.json @@ -37,7 +37,7 @@ "eslint-plugin-json-format": "^2.0.1", "eslint-plugin-mocha": "^8.1.0", "eslint-plugin-react": "^7.22.0", - "fs-extra": "^9.0.1", + "fs-extra": "^9.1.0", "graphql": "14.0.0", "mocha": "^8.1.1", "react": "^16.13.1", diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index 125cb37601e3..81d9b558daac 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -46,7 +46,7 @@ "eslint-plugin-mocha": "8.1.0", "fast-glob": "3.1.1", "find-webpack": "1.5.0", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "mocha": "^7.1.0", "mockery": "2.1.0", "proxyquire": "2.1.3", diff --git a/package.json b/package.json index 3aba6632ac4d..f6dcda649938 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "9.4.1", + "version": "9.5.0", "description": "Cypress.io end to end testing tool", "private": true, "scripts": { @@ -91,7 +91,7 @@ "@types/debug": "4.1.5", "@types/enzyme-adapter-react-16": "1.0.5", "@types/execa": "0.9.0", - "@types/fs-extra": "^8.0.1", + "@types/fs-extra": "^9.0.13", "@types/glob": "7.1.1", "@types/lodash": "^4.14.168", "@types/markdown-it": "0.0.9", @@ -134,7 +134,7 @@ "execa-wrap": "1.4.0", "filesize": "4.1.2", "find-package-json": "1.2.0", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "gift": "0.10.2", "glob": "7.1.6", "gulp": "4.0.2", diff --git a/packages/driver/cypress/integration/cypress/error_utils_spec.ts b/packages/driver/cypress/integration/cypress/error_utils_spec.ts index 7a5078b5c50d..fcd5a0b81db5 100644 --- a/packages/driver/cypress/integration/cypress/error_utils_spec.ts +++ b/packages/driver/cypress/integration/cypress/error_utils_spec.ts @@ -622,4 +622,18 @@ describe('driver/src/cypress/error_utils', () => { expect(stack).not.to.include('removeMeAndAbove') }) }) + + context('.wrapErr', () => { + [ + { value: undefined, label: 'undefined' }, + { value: null, label: 'null' }, + { value: '', label: 'empty string' }, + { value: true, label: 'boolean' }, + { value: 1, label: 'number' }, + ].forEach((err) => { + it(`returns undefined if err is ${err.label}`, () => { + expect($errUtils.wrapErr(err.value)).to.be.undefined + }) + }) + }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_actions.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_actions.spec.ts index c4ffe2c7d31e..91599319b0df 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_actions.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_actions.spec.ts @@ -41,43 +41,61 @@ context('multi-domain actions', { experimentalSessionSupport: true, experimental }) }) - it('.submit()', (done) => { + it('.submit()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('form:submitted', () => done()) + cy.switchToDomain('foobar.com', () => { + const afterFormSubmitted = new Promise((resolve) => { + cy.once('form:submitted', resolve) + }) cy.get('#input-type-submit').submit() + cy.wrap(afterFormSubmitted) }) }) - it('.click()', (done) => { + it('.click()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.switchToDomain('foobar.com', done, () => { - cy.get('#button').then((btn) => { - btn.on('click', () => done()) - }).click() + cy.switchToDomain('foobar.com', () => { + cy.get('#button').then(($btn) => { + const onClick = new Promise((resolve) => { + $btn.on('click', () => resolve()) + }) + + cy.wrap($btn).click() + cy.wrap(onClick) + }) }) }) - it('.dblclick()', (done) => { + it('.dblclick()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.switchToDomain('foobar.com', done, () => { - cy.get('#button').then((btn) => { - btn.on('dblclick', () => done()) - }).dblclick() + cy.switchToDomain('foobar.com', () => { + cy.get('#button').then(($btn) => { + const afterDblClick = new Promise((resolve) => { + $btn.on('dblclick', () => resolve()) + }) + + cy.wrap($btn).dblclick() + cy.wrap(afterDblClick) + }) }) }) - it('.rightclick()', (done) => { + it('.rightclick()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.switchToDomain('foobar.com', done, () => { - cy.get('#button').then((btn) => { - btn.on('contextmenu', () => done()) - }).rightclick() + cy.switchToDomain('foobar.com', () => { + cy.get('#button').then(($btn) => { + const afterContextmenu = new Promise((resolve) => { + $btn.on('contextmenu', () => resolve()) + }) + + cy.wrap($btn).rightclick() + cy.wrap(afterContextmenu) + }) }) }) @@ -129,13 +147,18 @@ context('multi-domain actions', { experimentalSessionSupport: true, experimental }) }) - it('.trigger()', (done) => { + it('.trigger()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.switchToDomain('foobar.com', done, () => { - cy.get('#button').then((btn) => { - btn.on('click', () => done()) - }).trigger('click') + cy.switchToDomain('foobar.com', () => { + cy.get('#button').then(($btn) => { + const afterClick = new Promise((resolve) => { + $btn.on('click', () => resolve()) + }) + + cy.wrap($btn).trigger('click') + cy.wrap(afterClick) + }) }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts index 5d4077fcf169..38071563b6fd 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_misc.spec.ts @@ -7,18 +7,35 @@ context('multi-domain misc', { experimentalSessionSupport: true, experimentalMul it('verifies number of cy commands', () => { // @ts-ignore - expect(Object.keys(cy.commandFns)).to.deep.equal( - [ - 'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select', - 'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then', - 'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile', - 'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit', - 'focused', 'get', 'contains', 'shadow', 'root', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not', - 'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', - 'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'switchToDomain', - ], - 'The number of cy commands has changed. Please ensure any newly added commands are also tested in multi-domain.', - ) + const actualCommands = Object.keys(cy.commandFns) + const expectedCommands = [ + 'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select', + 'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then', + 'invoke', 'its', 'getCookie', 'getCookies', 'setCookie', 'clearCookie', 'clearCookies', 'pause', 'debug', 'exec', 'readFile', + 'writeFile', 'fixture', 'clearLocalStorage', 'url', 'hash', 'location', 'end', 'noop', 'log', 'wrap', 'reload', 'go', 'visit', + 'focused', 'get', 'contains', 'root', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not', + 'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', + 'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'switchToDomain', + ] + const addedCommands = Cypress._.difference(actualCommands, expectedCommands) + const removedCommands = Cypress._.difference(expectedCommands, actualCommands) + + if (addedCommands.length && removedCommands.length) { + throw new Error(`Commands have been added to and removed from Cypress. + + The following command(s) were added: ${addedCommands.join(', ')} + The following command(s) were removed: ${removedCommands.join(', ')} + + Update this test accordingly.`) + } + + if (addedCommands.length) { + throw new Error(`The following command(s) have been added to Cypress: ${addedCommands.join(', ')}. Please add the command(s) to this test.`) + } + + if (removedCommands.length) { + throw new Error(`The following command(s) have been removed from Cypress: ${removedCommands.join(', ')}. Please remove the command(s) from this test.`) + } }) it('.end()', () => { @@ -51,28 +68,33 @@ context('multi-domain misc', { experimentalSessionSupport: true, experimentalMul }) }) - it('.log()', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('log:added', () => { - done() + it('.log()', () => { + cy.switchToDomain('foobar.com', () => { + const afterLogAdded = new Promise((resolve) => { + cy.once('log:added', () => { + resolve() + }) }) cy.log('test log in multi-domain') + cy.wrap(afterLogAdded) }) }) - it('.pause()', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('paused', () => { - // if running in open mode, pause will take effect - Cypress.emit('resume:all') - done() + it('.pause()', () => { + cy.switchToDomain('foobar.com', () => { + const afterPaused = new Promise((resolve) => { + cy.once('paused', () => { + Cypress.emit('resume:all') + resolve() + }) }) - // Otherwise, the subject of pause is returned in run mode and pause does NOT take effect - cy.pause().wrap({}).should('deep.eq', {}).then(function () { - done() - }) + cy.pause().wrap({}).should('deep.eq', {}) + // pause is a noop in run mode, so only wait for it if in open mode + if (Cypress.config('isInteractive')) { + cy.wrap(afterPaused) + } }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts index 746041425cff..e26b5f597cef 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/commands/multi_domain_screenshot.spec.ts @@ -1,5 +1,5 @@ // @ts-ignore / session support is needed for visiting about:blank between tests -context('screenshot specs', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => { +context('multi-domain screenshot', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => { beforeEach(() => { this.serverResult = { path: '/path/to/screenshot', @@ -68,7 +68,7 @@ context('screenshot specs', { experimentalSessionSupport: true, experimentalMult cy.screenshot() .then(() => { - expect(automationStub.args[0][1].titles).to.deep.equal(['screenshot specs', 'supports multiple titles']) + expect(automationStub.args[0][1].titles).to.deep.equal(['multi-domain screenshot', 'supports multiple titles']) }) }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_config_env_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_config_env_spec.ts index 456e890f9dcd..3965df149ecc 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_config_env_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_config_env_spec.ts @@ -83,8 +83,7 @@ }) }) - // FIXME: unskip this test once tail-end waiting is implemented - it.skip(`syncs serializable Cypress.${fnName}() values outwards from secondary (commands/async)`, () => { + it(`syncs serializable Cypress.${fnName}() values outwards from secondary (commands/async)`, () => { cy.switchToDomain('foobar.com', [fnName, USED_KEYS], ([fnName, USED_KEYS]) => { cy.then(() => { // @ts-ignore @@ -100,9 +99,9 @@ it(`persists Cypress.${fnName}() changes made in the secondary, assuming primary has not overwritten with a serializable value`, () => { cy.switchToDomain('foobar.com', [fnName, USED_KEYS], ([fnName, USED_KEYS]) => { // @ts-ignore - const baz = Cypress[fnName](USED_KEYS.bar) + const quux = Cypress[fnName](USED_KEYS.bar) - expect(baz).to.equal('baz') + expect(quux).to.equal('quux') }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts index 1ff4f36fec49..b80d897012b6 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_cypress_api.spec.ts @@ -204,19 +204,28 @@ describe('multi-domain Cypress API', { experimentalSessionSupport: true, experim }) }) - it('log()', (done) => { - cy.on('log:changed', (changedLog) => { - if (changedLog?.message === 'test log' && changedLog.ended) { - // just make sure Big Cypress logs make their way back to the primary - done() - } - }) - + // FIXME: convert to cypress-in-cypress tests once possible + it('log()', () => { cy.switchToDomain('foobar.com', () => { Cypress.log({ message: 'test log', }) }) + + // logs in the secondary domain skip the primary driver, going through + // the runner to the reporter, so we have to break out of the AUT and + // test the actual command log. + // this is a bit convoluted since otherwise the test could falsely pass + // by finding its own log if we simply did `.contains('test log')` + cy.wrap(Cypress.$(window.top!.document.body)) + .find('.reporter') + .contains('.runnable-title', 'log()') + .closest('.runnable') + .find('.runnable-commands-region .hook-item') + .eq(1) + .contains('.command-number', '2') + .closest('.command-wrapper-text') + .contains('test log') }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_event_specs.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_event_specs.ts deleted file mode 100644 index 7c2757ae005d..000000000000 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_event_specs.ts +++ /dev/null @@ -1,141 +0,0 @@ -// @ts-ignore / session support is needed for visiting about:blank between tests -describe('multi-domain', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => { - it('window:before:load event', () => { - cy.visit('/fixtures/multi-domain.html') - cy.on('window:before:load', (win: {testPrimaryDomainBeforeLoad: boolean}) => { - win.testPrimaryDomainBeforeLoad = true - }) - - cy.window().its('testPrimaryDomainBeforeLoad').should('be.true') - cy.get('a[data-cy="multi-domain-secondary-link"]').click() - cy.switchToDomain('foobar.com', () => { - cy.on('window:before:load', (win: {testSecondaryWindowBeforeLoad: boolean}) => { - win.testSecondaryWindowBeforeLoad = true - }) - - cy.window().its('testSecondaryWindowBeforeLoad').should('be.true') - cy.window().its('testPrimaryDomainBeforeLoad').should('be.undefined') - cy - .get('[data-cy="window-before-load"]') - .invoke('text') - .should('equal', 'Window Before Load Called') - }) - - // TODO enable once we can re-visit the primary domain. - // cy.visit('/fixtures/multi-domain.html') - - // cy.window().its('testPrimaryDomainBeforeLoad').should('be.true') - // cy.window().its('testSecondaryWindowBeforeLoad').should('be.undefined') - }) - - describe('post window load events', () => { - beforeEach(() => { - cy.visit('/fixtures/multi-domain.html') - cy.get('a[data-cy="multi-domain-secondary-link"]').click() - }) - - it('form:submitted', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('form:submitted', (e) => { - const $form = cy.$$('form') - - expect(e.target).to.eq($form.get(0)) - done() - }) - - cy.get('form').submit() - }) - }) - - // FIXME: reloading the page is problematic because the proxy delays the - // request, but the driver currently waits for a switchToDomain, which - // has already been called and won't be called again. need to handle any - // sort of page reloading in the AUT when it's cross-domain - it.skip('window:before:unload', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('window:before:unload', () => { - expect(location.host).to.equal('foobar.com') - done() - }) - - cy.window().then((window) => { - window.location.href = '/fixtures/multi-domain.html' - }) - }) - }) - - // FIXME: currently causes tests to hang. need to implement proper - // stability-handling on secondary domains - it.skip('window:unload', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('window:unload', () => { - expect(location.host).to.equal('foobar.com') - done() - }) - - cy.window().then((window) => { - window.location.href = '/fixtures/multi-domain.html' - }) - }) - }) - - it('window:alert', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('window:alert', (text) => { - expect(location.host).to.equal('foobar.com') - expect(`window:alert ${text}`).to.equal('window:alert the alert text') - done() - }) - - cy.get('[data-cy="alert"]').click() - }) - }) - - it('window:confirm', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('window:confirm', (text) => { - expect(location.host).to.equal('foobar.com') - expect(`window:confirm ${text}`).to.equal('window:confirm the confirm text') - done() - }) - - cy.get('[data-cy="confirm"]').click() - }) - }) - - it('window:confirmed - true when no window:confirm listeners return false', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('window:confirmed', (text, returnedFalse) => { - expect(location.host).to.equal('foobar.com') - expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - true') - done() - }) - - Cypress.on('window:confirm', () => {}) - Cypress.on('window:confirm', () => { - return true - }) - - cy.get('[data-cy="confirm"]').click() - }) - }) - - it('window:confirmed - false when any window:confirm listeners return false', (done) => { - cy.switchToDomain('foobar.com', done, () => { - Cypress.once('window:confirmed', (text, returnedFalse) => { - expect(location.host).to.equal('foobar.com') - expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - false') - done() - }) - - Cypress.on('window:confirm', () => { - return false - }) - - Cypress.on('window:confirm', () => {}) - - cy.get('[data-cy="confirm"]').click() - }) - }) - }) -}) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts new file mode 100644 index 000000000000..b7a13bf62975 --- /dev/null +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_events_spec.ts @@ -0,0 +1,158 @@ +// @ts-ignore / session support is needed for visiting about:blank between tests +describe('multi-domain', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => { + it('window:before:load event', () => { + cy.visit('/fixtures/multi-domain.html') + cy.on('window:before:load', (win: {testPrimaryDomainBeforeLoad: boolean}) => { + win.testPrimaryDomainBeforeLoad = true + }) + + cy.window().its('testPrimaryDomainBeforeLoad').should('be.true') + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + cy.switchToDomain('foobar.com', () => { + cy.on('window:before:load', (win: {testSecondaryWindowBeforeLoad: boolean}) => { + win.testSecondaryWindowBeforeLoad = true + }) + + cy.window().its('testSecondaryWindowBeforeLoad').should('be.true') + cy.window().its('testPrimaryDomainBeforeLoad').should('be.undefined') + cy + .get('[data-cy="window-before-load"]') + .invoke('text') + .should('equal', 'Window Before Load Called') + }) + + // TODO enable once we can re-visit the primary domain. + // cy.visit('/fixtures/multi-domain.html') + + // cy.window().its('testPrimaryDomainBeforeLoad').should('be.true') + // cy.window().its('testSecondaryWindowBeforeLoad').should('be.undefined') + }) + + describe('post window load events', () => { + beforeEach(() => { + cy.visit('/fixtures/multi-domain.html') + cy.get('a[data-cy="multi-domain-secondary-link"]').click() + }) + + it('form:submitted', () => { + cy.switchToDomain('foobar.com', () => { + const afterFormSubmitted = new Promise((resolve) => { + Cypress.once('form:submitted', (e) => { + const $form = cy.$$('form') + + expect(e.target).to.eq($form.get(0)) + resolve() + }) + }) + + cy.get('form').submit() + cy.wrap(afterFormSubmitted) + }) + }) + + it('window:before:unload', () => { + cy.switchToDomain('foobar.com', () => { + const afterWindowBeforeUnload = new Promise((resolve) => { + Cypress.once('window:before:unload', () => { + expect(location.host).to.equal('foobar.com') + resolve() + }) + }) + + cy.window().then((window) => { + window.location.href = '/fixtures/multi-domain.html' + }) + + cy.wrap(afterWindowBeforeUnload) + }) + }) + + it('window:unload', () => { + cy.switchToDomain('foobar.com', () => { + const afterWindowUnload = new Promise((resolve) => { + Cypress.once('window:unload', () => { + expect(location.host).to.equal('foobar.com') + resolve() + }) + }) + + cy.window().then((window) => { + window.location.href = '/fixtures/multi-domain.html' + }) + + cy.wrap(afterWindowUnload) + }) + }) + + it('window:alert', () => { + cy.switchToDomain('foobar.com', () => { + const afterWindowAlert = new Promise((resolve) => { + Cypress.once('window:alert', (text) => { + expect(location.host).to.equal('foobar.com') + expect(`window:alert ${text}`).to.equal('window:alert the alert text') + resolve() + }) + }) + + cy.get('[data-cy="alert"]').click() + cy.wrap(afterWindowAlert) + }) + }) + + it('window:confirm', () => { + cy.switchToDomain('foobar.com', () => { + const afterWindowConfirm = new Promise((resolve) => { + Cypress.once('window:confirm', (text) => { + expect(location.host).to.equal('foobar.com') + expect(`window:confirm ${text}`).to.equal('window:confirm the confirm text') + resolve() + }) + }) + + cy.get('[data-cy="confirm"]').click() + cy.wrap(afterWindowConfirm) + }) + }) + + it('window:confirmed - true when no window:confirm listeners return false', () => { + cy.switchToDomain('foobar.com', () => { + const afterWindowConfirmed = new Promise((resolve) => { + Cypress.once('window:confirmed', (text, returnedFalse) => { + expect(location.host).to.equal('foobar.com') + expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - true') + resolve() + }) + }) + + Cypress.on('window:confirm', () => {}) + Cypress.on('window:confirm', () => { + return true + }) + + cy.get('[data-cy="confirm"]').click() + cy.wrap(afterWindowConfirmed) + }) + }) + + it('window:confirmed - false when any window:confirm listeners return false', () => { + cy.switchToDomain('foobar.com', () => { + const afterWindowConfirmed = new Promise((resolve) => { + Cypress.once('window:confirmed', (text, returnedFalse) => { + expect(location.host).to.equal('foobar.com') + expect(`window:confirmed ${text} - ${returnedFalse}`).to.equal('window:confirmed the confirm text - false') + resolve() + }) + }) + + Cypress.on('window:confirm', () => { + return false + }) + + Cypress.on('window:confirm', () => {}) + + cy.get('[data-cy="confirm"]').click() + cy.wrap(afterWindowConfirmed) + }) + }) + }) +}) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_spec.ts index aab665f62402..969e768e9c9f 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_spec.ts @@ -1,5 +1,3 @@ -import _ from 'lodash' - // @ts-ignore / session support is needed for visiting about:blank between tests describe('multi-domain', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => { beforeEach(() => { @@ -144,18 +142,6 @@ describe('multi-domain', { experimentalSessionSupport: true, experimentalMultiDo expect(bool).to.be.true }) }) - - it('works with done callback', (done) => { - cy.switchToDomain('foobar.com', done, [true], ([bool]) => { - expect(bool).to.be.true - - Cypress.once('form:submitted', () => { - done() - }) - - cy.get('form').submit() - }) - }) }) describe('errors', () => { @@ -273,62 +259,14 @@ describe('multi-domain', { experimentalSessionSupport: true, experimentalMultiDo }) }) - it('errors if three or more arguments are used and the second argument is not the done() fn', (done) => { - cy.on('fail', (err) => { - expect(err.message).to.equal('`cy.switchToDomain()` must have done as its second argument when three or more arguments are used.') - - done() - }) - - cy.switchToDomain('foobar.com', () => {}, () => {}) - }) - - it('waits for all logs to finish streaming in from switchToDomain, expecting commands to not be pending', (done) => { - const domain = 'foobar.com' - const logsAddedNeedingUpdate = {} - const logsChangedGivingUpdate = {} - - cy.on('log:added', (addedLog) => { - if (!addedLog?.ended && addedLog?.id.includes(domain)) { - logsAddedNeedingUpdate[addedLog.id] = addedLog - } - }) - - cy.on('log:changed', (changedLog) => { - if (changedLog?.ended && changedLog?.id.includes(domain)) { - logsChangedGivingUpdate[changedLog.id] = changedLog - } - - const addedLogsSize = _.size(logsAddedNeedingUpdate) - const changedLogsSize = _.size(logsChangedGivingUpdate) - - // if all logs are done streaming - if (addedLogsSize === changedLogsSize && addedLogsSize > 0) { - // make sure each log added in the secondary domain is finished and has passed - _.forOwn(logsAddedNeedingUpdate, (_, key) => { - expect(logsChangedGivingUpdate[key].ended).to.be.true - expect(logsChangedGivingUpdate[key].state).to.not.equal('pending') - }) - - done() - } - }) - - cy.switchToDomain(domain, () => { - cy.get('form').submit() - }) - }) - it('receives command failures from the secondary domain', (done) => { - const timeout = 1000 + const timeout = 50 - cy.on('fail', (e) => { - const errString = e.toString() - - expect(errString).to.have.string(`Timed out retrying after ${timeout}ms: Expected to find element: \`#doesnt-exist\`, but never found it`) + cy.on('fail', (err) => { + expect(err.message).to.include(`Timed out retrying after ${timeout}ms: Expected to find element: \`#doesnt-exist\`, but never found it`) // make sure that the secondary domain failures do NOT show up as spec failures or AUT failures - expect(errString).to.not.have.string(`The following error originated from your test code, not from Cypress`) - expect(errString).to.not.have.string(`The following error originated from your application code, not from Cypress`) + expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`) + expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`) done() }) @@ -338,10 +276,5 @@ describe('multi-domain', { experimentalSessionSupport: true, experimentalMultiDo }) }) }) - - // TODO: this following tests needs to be implemented in a cy-in-cy test or more e2e style test as we need to test the 'done' function - it('propagates user defined secondary domain errors to the primary') - - it('short circuits the secondary domain command queue when "done()" is called early') }) }) diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts index 5bb1bd666e10..890c803ce921 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_uncaught_errors_spec.ts @@ -6,7 +6,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) describe('sync errors', () => { - it('fails the current test/command if sync errors are thrown from the switchToDomain context', () => { + it('fails the current test/command if sync errors are thrown from the switchToDomain callback', () => { const uncaughtExceptionSpy = cy.spy() const r = cy.state('runnable') @@ -78,9 +78,8 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - // FIXME: skip for now until refactor and stability are in - describe.skip('async errors', () => { - it('fails the current test/command if async errors are thrown from the test code in switchToDomain while the callback window is still open', (done) => { + describe('async errors', () => { + it('fails the current test/command if async errors are thrown from the switchToDomain callback', (done) => { cy.on('fail', (err) => { expect(err.name).to.eq('Error') expect(err.message).to.include('setTimeout error') @@ -100,7 +99,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - it('fails the current test/command if async errors are thrown from the switchToDomain context while the callback window is still open', () => { + it('fails the current test/command if async errors are thrown from the secondary domain AUT', () => { const uncaughtExceptionSpy = cy.spy() const r = cy.state('runnable') @@ -127,7 +126,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - it('passes the current test/command if async errors are thrown from the switchToDomain context, but the callback window is now closed', () => { + it('passes the current test/command if async errors are thrown from the secondary domain AUT, but the switchToDomain callback is finished running', () => { const uncaughtExceptionSpy = cy.spy() const failureSpy = cy.spy() @@ -144,9 +143,8 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - // FIXME: Remove skip once support is added for handling errors from switchToDomain after the callback windows closes - it.skip('fails the current test/command if async errors are thrown from the test code in switchToDomain while the callback window is now closed', (done) => { - cy.on('fail', (err) => { + it('fails the current test/command if async errors are thrown from the switchToDomain callback after it is finished running', (done) => { + cy.once('fail', (err) => { expect(err.name).to.eq('Error') expect(err.message).to.include('setTimeout error') expect(err.message).to.include('The following error originated from your test code, not from Cypress.') @@ -165,24 +163,27 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) describe('unhandled rejections', () => { - it('unhandled rejection triggers uncaught:exception and has promise as third argument', (done) => { - cy.switchToDomain('foobar.com', done, () => { + it('unhandled rejection triggers uncaught:exception and has promise as third argument', () => { + cy.switchToDomain('foobar.com', () => { const r = cy.state('runnable') - cy.once('uncaught:exception', (err, runnable, promise) => { - expect(err.stack).to.include('promise rejection') - expect(err.stack).to.include('one') - expect(err.stack).to.include('two') - expect(err.stack).to.include('three') - expect(runnable).to.be.equal(r) - expect(promise).to.be.a('promise') + const afterUncaughtException = new Promise((resolve) => { + cy.once('uncaught:exception', (err, runnable, promise) => { + expect(err.stack).to.include('promise rejection') + expect(err.stack).to.include('one') + expect(err.stack).to.include('two') + expect(err.stack).to.include('three') + expect(runnable).to.be.equal(r) + expect(promise).to.be.a('promise') - done() + resolve() - return false + return false + }) }) cy.get('.trigger-unhandled-rejection').click() + cy.wrap(afterUncaughtException) }) }) @@ -196,7 +197,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - it('fails the current test/command if a promise is rejected from the test code in switchToDomain while the callback window is still open', (done) => { + it('fails the current test/command if a promise is rejected from the test code in switchToDomain', (done) => { cy.on('fail', (err) => { expect(err.name).to.eq('Error') expect(err.message).to.include('rejected promise') @@ -215,8 +216,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - // FIXME: Remove skip once support is added for handling errors from switchToDomain after the callback windows closes - it.skip('fails the current test/command if a promise is rejected from the test code in switchToDomain while the callback window is now closed', (done) => { + it('fails the current test/command if a promise is rejected from the switchToDomain callback after it is finished running', (done) => { cy.on('fail', (err) => { expect(err.name).to.eq('Error') expect(err.message).to.include('rejected promise') @@ -234,7 +234,7 @@ describe('multi-domain - uncaught errors', { experimentalSessionSupport: true, e }) }) - it('does not fail if thrown custom error with readonly name', (done) => { + it('does not fail if thrown custom error has a readonly name', (done) => { cy.once('fail', (err) => { expect(err.name).to.include('CustomError') expect(err.message).to.include('custom error') diff --git a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts index 015c75adc8ae..7013a1677358 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/multi_domain_yield_spec.ts @@ -29,20 +29,18 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental .get('[data-cy="dom-check"]') .invoke('text') - const p = new Promise((resolve, reject) => { + return new Promise((resolve) => { setTimeout(() => { resolve('text') }, 50) }) - - return p }).should('equal', 'From a secondary domain') }) it('errors if a cy command is present and it returns a sync value', (done) => { cy.on('fail', (err) => { - assertLogLength(logs, 6) - expect(logs[5].get('error')).to.eq(err) + assertLogLength(logs, 5) + expect(logs[4].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` failed because you are mixing up async and sync code.') done() @@ -97,41 +95,41 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental symbol: Symbol(''), } }).then((obj) => { - // This will fail accessing the symbol // @ts-ignore - return obj.symbol + return obj.symbol // This will fail accessing the symbol }) }) it('succeeds if subject cannot be serialized and is not accessed', () => { cy.switchToDomain('foobar.com', () => { - cy - .get('[data-cy="dom-check"]') - }).then((obj) => { + cy.get('[data-cy="dom-check"]') + }) + .then(() => { return 'object not accessed' - }).should('equal', 'object not accessed') + }) + .should('equal', 'object not accessed') }) it('throws if subject cannot be serialized and is accessed', (done) => { cy.on('fail', (err) => { - assertLogLength(logs, 8) - expect(logs[7].get('error')).to.eq(err) + assertLogLength(logs, 6) + expect(logs[5].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.') done() }) cy.switchToDomain('foobar.com', () => { - cy - .get('[data-cy="dom-check"]') - }).invoke('text') + cy.get('[data-cy="dom-check"]') + }) + .then((subject) => subject.text()) .should('equal', 'From a secondary domain') }) it('throws if an object contains a function', (done) => { cy.on('fail', (err) => { - assertLogLength(logs, 8) - expect(logs[7].get('error')).to.eq(err) + assertLogLength(logs, 6) + expect(logs[5].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.') done() @@ -143,13 +141,15 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental return 'whoops' }, }) - }).invoke('key').should('equal', 'whoops') + }) + .then((subject) => subject.key()) + .should('equal', 'whoops') }) it('throws if an object contains a symbol', (done) => { cy.on('fail', (err) => { - assertLogLength(logs, 8) - expect(logs[7].get('error')).to.eq(err) + assertLogLength(logs, 6) + expect(logs[5].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.') done() @@ -159,13 +159,14 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental cy.wrap({ key: Symbol('whoops'), }) - }).should('equal', undefined) + }) + .should('equal', undefined) }) it('throws if an object is a function', (done) => { cy.on('fail', (err) => { - assertLogLength(logs, 8) - expect(logs[7].get('error')).to.eq(err) + assertLogLength(logs, 6) + expect(logs[5].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to functions not being supported by the structured clone algorithm.') done() @@ -175,7 +176,8 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental cy.wrap(() => { return 'text' }) - }).then((obj) => { + }) + .then((obj) => { // @ts-ignore obj() }) @@ -183,8 +185,8 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental it('throws if an object is a symbol', (done) => { cy.on('fail', (err) => { - assertLogLength(logs, 8) - expect(logs[7].get('error')).to.eq(err) + assertLogLength(logs, 6) + expect(logs[5].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to symbols not being supported by the structured clone algorithm.') done() @@ -192,7 +194,8 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental cy.switchToDomain('foobar.com', () => { cy.wrap(Symbol('symbol')) - }).should('equal', 'symbol') + }) + .should('equal', 'symbol') }) // NOTE: Errors can only be serialized on chromium browsers. @@ -201,8 +204,8 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental cy.on('fail', (err) => { if (!isChromium) { - assertLogLength(logs, 8) - expect(logs[7].get('error')).to.eq(err) + assertLogLength(logs, 6) + expect(logs[5].get('error')).to.eq(err) expect(err.message).to.include('`cy.switchToDomain()` could not serialize the subject due to one of its properties not being supported by the structured clone algorithm.') } @@ -213,7 +216,8 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental cy.wrap({ key: new Error('Boom goes the dynamite'), }) - }).its('key.message') + }) + .its('key.message') .should('equal', 'Boom goes the dynamite').then(() => { done() }) @@ -235,7 +239,8 @@ describe('multi-domain yields', { experimentalSessionSupport: true, experimental }, string: 'string', }) - }).should('deep.equal', { + }) + .should('deep.equal', { array: [ 1, 2, diff --git a/packages/driver/cypress/integration/e2e/multi-domain/navigation_spec.ts b/packages/driver/cypress/integration/e2e/multi-domain/navigation_spec.ts index e51ea8abdccc..b396023866d2 100644 --- a/packages/driver/cypress/integration/e2e/multi-domain/navigation_spec.ts +++ b/packages/driver/cypress/integration/e2e/multi-domain/navigation_spec.ts @@ -1,5 +1,16 @@ import { assertLogLength } from '../../../support/utils' +// makes logs coming from secondary domain work with `assertLogLength` +const reifyLogs = (logs) => { + return logs.map((attrs) => { + return { + get (name) { + return attrs[name] + }, + } + }) +} + // @ts-ignore / session support is needed for visiting about:blank between tests describe('navigation events', { experimentalSessionSupport: true, experimentalMultiDomain: true }, () => { let logs: any = [] @@ -18,7 +29,7 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu describe('navigation:changed', () => { it('navigation:changed via hashChange', () => { cy.switchToDomain('foobar.com', () => { - const p = new Promise((resolve) => { + const afterNavigationChanged = new Promise((resolve) => { const listener = () => { cy.location().should((loc) => { expect(loc.host).to.equal('www.foobar.com:3500') @@ -33,7 +44,7 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu }) cy.get('a[data-cy="hashChange"]').click() - cy.wrap(p) + cy.wrap(afterNavigationChanged) }) }) @@ -61,7 +72,13 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu describe('window:load', () => { it('reloads', () => { cy.switchToDomain('foobar.com', () => { - const p = new Promise((resolve) => { + const logs: any[] = [] + + cy.on('log:added', (attrs, log) => { + logs.push(log) + }) + + const afterWindowLoad = new Promise((resolve) => { let times = 0 const listener = (win) => { times++ @@ -78,16 +95,26 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu }) cy.get('button[data-cy="reload"]').click() - cy.wrap(p) - }).then(() => { - assertLogLength(logs, 14) - expect(logs[10].get('name')).to.eq('page load') + cy.wrap(afterWindowLoad).then(() => { + return logs.map((log) => ({ name: log.get('name') })) + }) + }) + .then(reifyLogs) + .then((secondaryLogs) => { + assertLogLength(secondaryLogs, 9) + expect(secondaryLogs[5].get('name')).to.eq('page load') }) }) it('navigates to a new page', () => { cy.switchToDomain('foobar.com', () => { - const p = new Promise((resolve) => { + const logs: any[] = [] + + cy.on('log:added', (attrs, log) => { + logs.push(log) + }) + + const afterWindowLoad = new Promise((resolve) => { let times = 0 const listener = (win) => { times++ @@ -109,12 +136,16 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu cy.get('a[data-cy="multi-domain-page"]').click() cy.get('a[data-cy="multi-domain-secondary-link').invoke('text').should('equal', 'http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') - cy.wrap(p) - }).then(() => { - assertLogLength(logs, 18) - expect(logs[10].get('name')).to.eq('page load') - expect(logs[11].get('name')).to.eq('new url') - expect(logs[11].get('message')).to.eq('http://www.foobar.com:3500/fixtures/multi-domain.html') + cy.wrap(afterWindowLoad).then(() => { + return logs.map((log) => ({ name: log.get('name'), message: log.get('message') })) + }) + }) + .then(reifyLogs) + .then((secondaryLogs) => { + assertLogLength(secondaryLogs, 13) + expect(secondaryLogs[5].get('name')).to.eq('page load') + expect(secondaryLogs[6].get('name')).to.eq('new url') + expect(secondaryLogs[6].get('message')).to.eq('http://www.foobar.com:3500/fixtures/multi-domain.html') }) }) }) @@ -122,7 +153,7 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu describe('url:changed', () => { it('reloads', () => { cy.switchToDomain('foobar.com', () => { - const p = new Promise((resolve) => { + const afterUrlChanged = new Promise((resolve) => { cy.once('url:changed', (url) => { expect(url).to.equal('http://www.foobar.com:3500/fixtures/multi-domain-secondary.html') resolve() @@ -130,13 +161,13 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu }) cy.get('button[data-cy="reload"]').click() - cy.wrap(p) + cy.wrap(afterUrlChanged) }) }) it('navigates to a new page', () => { cy.switchToDomain('foobar.com', () => { - const p = new Promise((resolve) => { + const afterUrlChanged = new Promise((resolve) => { let times = 0 const listener = (url) => { times++ @@ -155,7 +186,7 @@ describe('navigation events', { experimentalSessionSupport: true, experimentalMu }) cy.get('a[data-cy="multi-domain-page"]').click() - cy.wrap(p) + cy.wrap(afterUrlChanged) }) }) diff --git a/packages/driver/package.json b/packages/driver/package.json index 2c3d9f011d05..2f6e95a288ca 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -82,7 +82,7 @@ "text-mask-addons": "3.8.0", "underscore.string": "3.3.5", "unfetch": "4.1.0", - "url-parse": "1.5.2", + "url-parse": "1.5.6", "vanilla-text-mask": "5.1.1", "vite": "^2.4.4", "webpack": "4.41.2", diff --git a/packages/driver/src/cy/multi-domain/commands_manager.ts b/packages/driver/src/cy/multi-domain/commands_manager.ts deleted file mode 100644 index 0a38211e1653..000000000000 --- a/packages/driver/src/cy/multi-domain/commands_manager.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { PrimaryDomainCommunicator } from '../../multi-domain/communicator' -import { createDeferred, Deferred } from '../../util/deferred' -import { correctStackForCrossDomainError } from './util' -import { failedToSerializeSubject } from './failedSerializeSubjectProxy' - -export class CommandsManager { - // these are proxy commands that represent real commands in a - // secondary domain. this way, the queue runs in the primary domain - // with all commands, making it possible to sync up timing for - // the reporter command log, etc - commands: { [key: string]: { - deferred: Deferred - name: string - }} = {} - - communicator: PrimaryDomainCommunicator - isDoneFnAvailable: boolean - userInvocationStack: string - - constructor ({ communicator, isDoneFnAvailable, userInvocationStack }) { - this.communicator = communicator - this.isDoneFnAvailable = isDoneFnAvailable - this.userInvocationStack = userInvocationStack - } - - get hasCommands () { - return Object.keys(this.commands).length > 0 - } - - listen () { - this.communicator.on('reject', this.reject) - this.communicator.on('command:enqueued', this.addCommand) - this.communicator.on('command:end', this.endCommand) - } - - addCommand = (attrs) => { - const deferred = createDeferred() - - this.commands[attrs.id] = { - deferred, - name: attrs.name, - } - - attrs.fn = () => { - // the real command running in the secondary domain handles its - // own timeout - // TODO: add a special, long timeout in case inter-domain - // communication breaks down somehow - cy.clearTimeout() - - this.communicator.toSpecBridge('run:command', { - name: attrs.name, - isDoneFnAvailable: this.isDoneFnAvailable, - }) - - return deferred.promise - } - - Cypress.action('cy:enqueue:command', attrs) - } - - endCommand = ({ id, subject, failedToSerializeSubjectOfType, name, err, logId }) => { - const command = this.commands[id] - - if (!command) return - - delete this.commands[id] - - if (!err) { - return command.deferred.resolve(failedToSerializeSubjectOfType ? failedToSerializeSubject(failedToSerializeSubjectOfType) : subject) - } - - // If the command has failed, cast the error back to a proper Error object - const parsedError = correctStackForCrossDomainError(err, this.userInvocationStack) - - if (logId) { - // Then, look up the logId associated with the failed command and stub out the onFail handler - // to short circuit any added reporter command logs if a log exists for the failed command - parsedError.onFail = () => undefined - } else { - delete parsedError.onFail - } - - command.deferred.reject(parsedError) - - // finally, free up any memory and unbind any handlers now that the command/test has failed - this.cleanup() - } - - reject = ({ err }) => { - // parse the error back to a proper Error object - const parsedError = correctStackForCrossDomainError(err, this.userInvocationStack) - - delete parsedError.onFail - - const r = cy.state('reject') - - if (r) { - r(parsedError) - } - - // finally, free up any memory and unbind any handlers now that the test has failed - this.cleanup() - } - - async cleanup () { - this.communicator.off('reject', this.reject) - this.communicator.off('command:enqueued', this.addCommand) - - // don't allow for new commands to be enqueued, but wait for commands - // to update in the secondary domain - const pendingCommands = Object.values(this.commands).map((command) => { - return command.deferred.promise - }) - - try { - await Promise.all(pendingCommands) - } finally { - this.communicator.off('command:end', this.endCommand) - } - } -} diff --git a/packages/driver/src/cy/multi-domain/index.ts b/packages/driver/src/cy/multi-domain/index.ts index 8a94624302fe..ae9ff7eefd80 100644 --- a/packages/driver/src/cy/multi-domain/index.ts +++ b/packages/driver/src/cy/multi-domain/index.ts @@ -1,11 +1,9 @@ import Bluebird from 'bluebird' import $errUtils from '../../cypress/error_utils' -import { CommandsManager } from './commands_manager' -import { LogsManager } from './logs_manager' import { Validator } from './validator' -import { correctStackForCrossDomainError, serializeRunnable } from './util' +import { createUnserializableSubjectProxy } from './unserializable_subject_proxy' +import { serializeRunnable } from './util' import { preprocessConfig, preprocessEnv, syncConfigToCurrentDomain, syncEnvToCurrentDomain } from '../../util/config' -import { failedToSerializeSubject } from './failedSerializeSubjectProxy' export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: Cypress.State, config: Cypress.InternalConfig) { let timeoutId @@ -34,37 +32,25 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, }) Commands.addAll({ - switchToDomain (domain: string, doneOrDataOrFn: T[] | Mocha.Done | (() => {}), dataOrFn?: T[] | (() => {}), fn?: (data?: T[]) => {}) { - // store the invocation stack in the case that `switchToDomain` errors - const userInvocationStack = state('current').get('userInvocationStack') - + switchToDomain (domain: string, dataOrFn: T[] | (() => {}), fn?: (data?: T[]) => {}) { clearTimeout(timeoutId) + // this command runs for as long as the commands in the secondary + // domain run, so it can't have its own timeout + cy.clearTimeout() if (!config('experimentalMultiDomain')) { $errUtils.throwErrByPath('switchToDomain.experiment_not_enabled') } - let done let data let callbackFn if (fn) { callbackFn = fn - done = doneOrDataOrFn data = dataOrFn - - // if done has been provided to the test, allow the user to call done - // from the switchToDomain context running in the secondary domain. - } else if (dataOrFn) { - callbackFn = dataOrFn - - if (typeof doneOrDataOrFn === 'function') { - done = doneOrDataOrFn - } else { - data = doneOrDataOrFn - } } else { - callbackFn = doneOrDataOrFn + callbackFn = dataOrFn + data = [] } const log = Cypress.log({ @@ -83,84 +69,69 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, callbackFn, data, domain, - done, - doneReference: state('done'), }) - const commandsManager = new CommandsManager({ - communicator, - isDoneFnAvailable: !!done, - userInvocationStack, - }) + return new Bluebird((resolve, reject) => { + const _resolve = ({ subject, unserializableSubjectType }) => { + resolve(unserializableSubjectType ? createUnserializableSubjectProxy(unserializableSubjectType) : subject) + } - const logsManager = new LogsManager({ - communicator, - userInvocationStack, - }) + const _reject = (err) => { + log.error(err) + if (typeof err === 'object') { + err.onFail = () => {} + } - const cleanup = () => { - commandsManager.cleanup() - logsManager.cleanup() - } + reject(err) + } - const doneAndCleanup = async ({ err }) => { - communicator.off('done:called', doneAndCleanup) - // If done is called, immediately unbind command listeners to prevent - // any commands from being enqueued, but wait for log updates to - // trickle in before invoking done - commandsManager.cleanup() - await logsManager.cleanup() - done(err ? correctStackForCrossDomainError(err, userInvocationStack) : err) - } + const onQueueFinished = ({ err, subject, unserializableSubjectType }) => { + if (err) { + return _reject(err) + } - if (done) { - communicator.once('done:called', doneAndCleanup) - } + _resolve({ subject, unserializableSubjectType }) + } - commandsManager.listen() - logsManager.listen() + const cleanup = () => { + communicator.off('queue:finished', onQueueFinished) + } - return new Bluebird((resolve, reject) => { communicator.once('sync:config', ({ config, env }) => { syncConfigToCurrentDomain(config) syncEnvToCurrentDomain(env) }) - communicator.once('ran:domain:fn', ({ subject, failedToSerializeSubjectOfType, err }) => { + communicator.once('ran:domain:fn', (details) => { + const { subject, unserializableSubjectType, err, finished } = details + sendReadyForDomain() - if (err) { - if (done) { - communicator.off('done:called', doneAndCleanup) - } else { - communicator.off('queue:finished', cleanup) - } + if (err) { cleanup() - reject(err) - return + return _reject(err) } - // If done is passed into switchToDomain, wait to unbind any listeners - // Otherwise, all commands in the secondary domain (SD) should be - // enqueued by now. Go ahead and bind the cleanup method for when - // the command queue finishes in the SD. Otherwise, if no commands - // are enqueued, clean up the command and log listeners. This case - // is common if there are only assertions enqueued in the SD. - if (!commandsManager.hasCommands && !done) { + // if there are not commands and a synchronous return from the callback, + // this resolves immediately + if (finished || subject || unserializableSubjectType) { cleanup() - // This handles when a subject is returned synchronously - resolve(failedToSerializeSubjectOfType ? failedToSerializeSubject(failedToSerializeSubjectOfType) : subject) - } else { - resolve() + _resolve({ subject, unserializableSubjectType }) } }) - // If done is NOT passed into switchToDomain, wait for the command queue - // to finish in the secondary domain before starting any cleanup - if (!done) { - communicator.once('queue:finished', cleanup) + communicator.once('queue:finished', onQueueFinished) + + // We don't unbind this even after queue:finished, because an async + // error could be thrown after the queue is done, but make sure not + // to stack up listeners on it after it's originally bound + if (!communicator.listeners('uncaught:error').length) { + communicator.once('uncaught:error', ({ err }) => { + // @ts-ignore + Cypress.runner.onSpecError('error')({ error: err }) + }) } // fired once the spec bridge is set up and ready to receive messages @@ -179,7 +150,6 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, communicator.toSpecBridge('run:domain:fn', { data, fn: callbackFn.toString(), - isDoneFnAvailable: !!done, // let the spec bridge version of Cypress know if config read-only values can be overwritten since window.top cannot be accessed in cross-origin iframes // this should only be used for internal testing. Cast to boolean to guarantee serialization // @ts-ignore @@ -189,6 +159,7 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, viewportHeight: Cypress.state('viewportHeight'), runnable: serializeRunnable(Cypress.state('runnable')), duringUserTestExecution: Cypress.state('duringUserTestExecution'), + hookId: state('hookId'), }, config: preprocessConfig(Cypress.config()), env: preprocessEnv(Cypress.env()), @@ -201,7 +172,6 @@ export function addCommands (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, cleanup() reject(wrappedErr) } finally { - state('readyForMultidomain', false) // @ts-ignore cy.isAnticipatingMultiDomain(false) } diff --git a/packages/driver/src/cy/multi-domain/logs_manager.ts b/packages/driver/src/cy/multi-domain/logs_manager.ts deleted file mode 100644 index e467b9efa838..000000000000 --- a/packages/driver/src/cy/multi-domain/logs_manager.ts +++ /dev/null @@ -1,98 +0,0 @@ -import _ from 'lodash' -import type { PrimaryDomainCommunicator } from '../../multi-domain/communicator' -import { createDeferred, Deferred } from '../../util/deferred' -import { correctStackForCrossDomainError } from './util' - -export class LogsManager { - logs: { [key: string]: { - deferred: Deferred - log: Cypress.Log - }} = {} - - communicator: PrimaryDomainCommunicator - userInvocationStack: string - - constructor ({ communicator, userInvocationStack }) { - this.communicator = communicator - this.userInvocationStack = userInvocationStack - } - - listen () { - this.communicator.on('log:added', this.onLogAdded) - this.communicator.on('log:changed', this.onLogChanged) - } - - onLogAdded = (attrs) => { - if (!attrs) return - - attrs.consoleProps = () => attrs.consoleProps - attrs.renderProps = () => attrs.renderProps - - const log = Cypress.log(attrs) - - // if the log needs to stream updates, defer its result to make sure all streamed updates come in - if (!attrs.ended) { - this.logs[log.get('id')] = { - log, - deferred: createDeferred(), - } - } - } - - onLogChanged = (attrs) => { - const changedLog = this.logs[attrs?.id] - - // NOTE: sometimes debug logs that are created in the secondary are only emitted through 'log:changed' events - // and no initial log is created in 'log:added - // These logs are not important to the primary domain, so we can ignore them - if (!changedLog) return - - const { deferred, log } = changedLog - const logAttrs = log.get() - - _.forEach(attrs, (value: any, key: string) => { - if ( - value != null - && !(_.isObject(value) && _.isEmpty(value)) - && !_.isEqual(value, logAttrs[key]) - ) { - log.set(key as keyof Cypress.LogConfig, value) - } - }) - - const isEnded = log.get('ended') - - if (!isEnded) return - - if (log.get('state') === 'failed') { - let parsedError = correctStackForCrossDomainError(log.get('err'), this.userInvocationStack) - - // The toJSON method on the Cypress.Log converts the 'error' property to 'err', so when the log gets - // serialized from the SD to the PD, we need to do the opposite - log.set('error', parsedError) - if ((logAttrs.consoleProps as any)?.Error) { - // Update consoleProps for when users pin failed commands in the reporter so correct error messages are displayed in the console - (logAttrs.consoleProps as any).Error = parsedError.stack - } - } - - delete this.logs[attrs.id] - deferred.resolve() - } - - async cleanup () { - this.communicator.off('log:added', this.onLogAdded) - - // don't allow for new logs to be added, but wait for logs - // to update changes in the secondary domain - const pendingLogs = Object.values(this.logs).map((log) => { - return log.deferred.promise - }) - - try { - await Promise.all(pendingLogs) - } finally { - this.communicator.off('log:changed', this.onLogChanged) - } - } -} diff --git a/packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts b/packages/driver/src/cy/multi-domain/unserializable_subject_proxy.ts similarity index 91% rename from packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts rename to packages/driver/src/cy/multi-domain/unserializable_subject_proxy.ts index 1e6376da28dd..cd986e542dc3 100644 --- a/packages/driver/src/cy/multi-domain/failedSerializeSubjectProxy.ts +++ b/packages/driver/src/cy/multi-domain/unserializable_subject_proxy.ts @@ -20,7 +20,7 @@ const passThroughProps = [ * @param type The type of operand that failed to serialize * @returns A proxy object that will fail when accessed. */ -const failedToSerializeSubject = (type: string) => { +export const createUnserializableSubjectProxy = (type: string) => { let target = {} // If the failed subject is a function, use a function as the target. @@ -37,7 +37,7 @@ const failedToSerializeSubject = (type: string) => { * @param thisArg this * @param argumentsList args passed. */ - apply (target, thisArg, argumentsList) { + apply () { $errUtils.throwErrByPath('switchToDomain.failed_to_serialize_function') }, @@ -48,7 +48,7 @@ const failedToSerializeSubject = (type: string) => { * @param receiver Either the proxy or an object that inherits from the proxy. * @returns either an error or the result of the allowed get on the target. */ - get (target, prop, receiver) { + get (target, prop) { if (passThroughProps.includes(prop)) { return target[prop] } @@ -62,5 +62,3 @@ const failedToSerializeSubject = (type: string) => { }, }) } - -export { failedToSerializeSubject } diff --git a/packages/driver/src/cy/multi-domain/validator.ts b/packages/driver/src/cy/multi-domain/validator.ts index bc216db0e1f5..d54fff8dd7f3 100644 --- a/packages/driver/src/cy/multi-domain/validator.ts +++ b/packages/driver/src/cy/multi-domain/validator.ts @@ -11,7 +11,7 @@ export class Validator { this.onFailure = onFailure } - validate ({ callbackFn, data, domain, done, doneReference }) { + validate ({ callbackFn, data, domain }) { if (typeof domain !== 'string') { this.onFailure() @@ -38,14 +38,5 @@ export class Validator { args: { arg: $utils.stringify(callbackFn) }, }) } - - // verifies the done argument is actually the done fn - if (done && done !== doneReference) { - this.onFailure() - - $errUtils.throwErrByPath('switchToDomain.done_reference_mismatch', { - onFail: this.log, - }) - } } } diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index 7b5ba6f32cc7..ecb7c37da230 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -252,40 +252,11 @@ export class CommandQueue extends Queue { // TypeScript doesn't allow overriding functions with different type signatures // @ts-ignore - run (autoRun = true) { - let pause = false - + run () { const next = () => { // start at 0 index if one is not already set let index = this.state('index') || this.state('index', 0) - // if at the end of the queue when not auto-running, pause will be true - // but since there's nothing left in the queue to move things forward, - // ignore and reset the pause, then let the queue finish - if (!autoRun && pause && !this.at(index)) { - pause = false - - return next() - } - - // when running in a secondary domain (SD), the primary domain (PD) - // has proxy commands that represent the real commands run in the SD. - // for everything to sync up properly, the PD controls the running of - // the queue by running each proxy command which in turns communicates - // to its real command, telling it to run. this means in the SD, we - // need to pause the queue and wait for the signal to run the next - // command. this is done here by inserting this promise into the queue - // and resolving once state('next') is called (said signal) - if (!autoRun && pause) { - return new Promise((resolve) => { - this.state('next', () => { - this.state('next', null) - pause = false - resolve(next()) - }) - }) - } - // bail if we've been told to abort in case // an old command continues to run after if (this.stopped) { @@ -308,10 +279,6 @@ export class CommandQueue extends Queue { Cypress.action('cy:skipped:command:end', command) - if (!autoRun) { - pause = true - } - return next() } @@ -365,10 +332,6 @@ export class CommandQueue extends Queue { fn = this.state('onPaused') - if (!autoRun) { - pause = true - } - if (fn) { return new Bluebird((resolve) => { return fn(resolve) diff --git a/packages/driver/src/cypress/error_utils.ts b/packages/driver/src/cypress/error_utils.ts index 9c58d6950aa8..3d001726ecff 100644 --- a/packages/driver/src/cypress/error_utils.ts +++ b/packages/driver/src/cypress/error_utils.ts @@ -52,8 +52,14 @@ const prepareErrorForSerialization = (err) => { return err } +// some errors, probably from user callbacks, might be boolean, number or falsy values +// which means serializing will not provide any useful context +const isSerializableError = (err) => { + return !!err && (typeof err === 'object' || typeof err === 'string') +} + const wrapErr = (err) => { - if (!err) return + if (!isSerializableError(err)) return prepareErrorForSerialization(err) diff --git a/packages/driver/src/multi-domain/commands.ts b/packages/driver/src/multi-domain/commands.ts deleted file mode 100644 index 529d8d2c6149..000000000000 --- a/packages/driver/src/multi-domain/commands.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { $Cy } from '../cypress/cy' -import type { SpecBridgeDomainCommunicator } from './communicator' - -export const handleCommands = (Cypress: Cypress.Cypress, cy: $Cy, specBridgeCommunicator: SpecBridgeDomainCommunicator) => { - const onCommandEnqueued = (commandAttrs: Cypress.EnqueuedCommand) => { - const { id, name } = commandAttrs - - // it's not strictly necessary to send the name, but it can be useful - // for debugging purposes - specBridgeCommunicator.toPrimary('command:enqueued', { id, name }) - } - - const onCommandEnd = (command: Cypress.CommandQueue) => { - const id = command.get('id') - const name = command.get('name') - const logId = command.getLastLog()?.get('id') - - let subject: string | undefined = undefined - - // Prevent serialization if this isn't the last command in the queue. - if (cy.queue.last()?.get('id') === id) { - subject = cy.state('subject') - } - - // we need to serialize and send back the subject on each command because the next chained - // command outside of the multi-domain context will not wait for the queue finished event. - specBridgeCommunicator.toPrimaryCommandEnd({ id, name, subject, logId }) - } - - const onRunCommand = () => { - const next = cy.state('next') - - if (next) { - return next() - } - - // if there's no state('next') for running the next command, - // the queue hasn't started yet, so run it - cy.queue.run(false) - .then(() => { - specBridgeCommunicator.toPrimaryQueueFinished() - }) - } - - Cypress.on('command:enqueued', onCommandEnqueued) - Cypress.on('command:end', onCommandEnd) - Cypress.on('skipped:command:end', onCommandEnd) - - specBridgeCommunicator.on('run:command', onRunCommand) -} diff --git a/packages/driver/src/multi-domain/communicator.ts b/packages/driver/src/multi-domain/communicator.ts index 03e140092bc5..897469141f6f 100644 --- a/packages/driver/src/multi-domain/communicator.ts +++ b/packages/driver/src/multi-domain/communicator.ts @@ -127,30 +127,30 @@ export class PrimaryDomainCommunicator extends EventEmitter { export class SpecBridgeDomainCommunicator extends EventEmitter { private windowReference - private handleSubjectAndErr = (event, data) => { - const { subject, err, ...other } = data + private handleSubjectAndErr = (data: any = {}, send: (data: any) => void) => { + const { subject, err, ...rest } = data + + if (!subject && !err) { + return send(rest) + } try { // We always want to make sure errors are posted, so clean it up to send. - const preProcessedError = preprocessErrorForPostMessage(err) - - this.toPrimary(event, { subject, err: preProcessedError, ...other }) - } catch (error: any) { - if (subject && error.name === 'DataCloneError') { + send({ ...rest, subject, err: preprocessErrorForPostMessage(err) }) + } catch (err: any) { + if (subject && err.name === 'DataCloneError') { // Send the type of object that failed to serialize. - const failedToSerializeSubjectOfType = typeof subject - - // If the subject threw the 'DataCloneError', the subject cannot be serialized at which point try again with an undefined subject. - this.handleSubjectAndErr(event, { failedToSerializeSubjectOfType, ...other }) - } else { - // Try to send the message again, with the new error. - this.handleSubjectAndErr(event, { err: error, ...other }) - throw error + // If the subject threw the 'DataCloneError', the subject cannot be + // serialized, at which point try again with an undefined subject. + return this.handleSubjectAndErr({ ...rest, unserializableSubjectType: typeof subject }, send) } + + // Try to send the message again, with the new error. + this.handleSubjectAndErr({ ...rest, err }, send) } } - private resyncConfigEnvToPrimary = () => { + private syncConfigEnvToPrimary = () => { this.toPrimary('sync:config', { config: preprocessConfig(Cypress.config()), env: preprocessEnv(Cypress.env()), @@ -179,31 +179,14 @@ export class SpecBridgeDomainCommunicator extends EventEmitter { * @param {string} event - the name of the event to be sent. * @param {any} data - any meta data to be sent with the event. */ - toPrimary (event: string, data?: any) { - let prefixedEvent = `${CROSS_DOMAIN_PREFIX}${event}` - - this.windowReference.top.postMessage({ event: prefixedEvent, data }, '*') - } - - toPrimaryCommandEnd (data: {id: string, subject?: any, name: string, err?: any, logId: string }) { - this.handleSubjectAndErr('command:end', data) - } - - toPrimaryRanDomainFn (data: { subject?: any, err?: any, resyncConfig: boolean }) { - if (data?.resyncConfig) { - this.resyncConfigEnvToPrimary() - } - - this.handleSubjectAndErr('ran:domain:fn', data) - } - - toPrimaryQueueFinished () { - this.resyncConfigEnvToPrimary() - this.toPrimary('queue:finished') - } - - toPrimaryError (event, data: { subject?: any, err?: any}) { - this.resyncConfigEnvToPrimary() - this.handleSubjectAndErr(event, data) + toPrimary (event: string, data?: any, options = { syncConfig: false }) { + if (options.syncConfig) this.syncConfigEnvToPrimary() + + this.handleSubjectAndErr(data, (data: any) => { + this.windowReference.top.postMessage({ + event: `${CROSS_DOMAIN_PREFIX}${event}`, + data, + }, '*') + }) } } diff --git a/packages/driver/src/multi-domain/cypress.ts b/packages/driver/src/multi-domain/cypress.ts index 34c51331c70f..b9ab29f8fd09 100644 --- a/packages/driver/src/multi-domain/cypress.ts +++ b/packages/driver/src/multi-domain/cypress.ts @@ -11,13 +11,12 @@ import $Log from '../cypress/log' import { bindToListeners } from '../cy/listeners' import { SpecBridgeDomainCommunicator } from './communicator' import { handleDomainFn } from './domain_fn' -import { handleCommands } from './commands' import { handleLogs } from './events/logs' import { handleSocketEvents } from './events/socket' -import { handleSpecWindowEvents } from './events/spec_window_events' +import { handleSpecWindowEvents } from './events/spec_window' import { handleErrorEvent } from './events/errors' import { handleScreenshots } from './events/screenshots' -import { handleTestEvents } from './events/test_events' +import { handleTestEvents } from './events/test' import { handleMiscEvents } from './events/misc' import { handleUnsupportedAPIs } from './unsupported_apis' @@ -67,7 +66,6 @@ const setup = (cypressConfig: Cypress.Config, env: Cypress.ObjectLike) => { Cypress.isCy = cy.isCy handleDomainFn(cy, specBridgeCommunicator) - handleCommands(Cypress, cy, specBridgeCommunicator) handleLogs(Cypress, specBridgeCommunicator) handleSocketEvents(Cypress) handleSpecWindowEvents(cy) diff --git a/packages/driver/src/multi-domain/domain_fn.ts b/packages/driver/src/multi-domain/domain_fn.ts index 893f295fccfc..70983997135b 100644 --- a/packages/driver/src/multi-domain/domain_fn.ts +++ b/packages/driver/src/multi-domain/domain_fn.ts @@ -4,32 +4,16 @@ import $errUtils from '../cypress/error_utils' import $utils from '../cypress/utils' import { syncConfigToCurrentDomain, syncEnvToCurrentDomain } from '../util/config' -export const handleDomainFn = (cy: $Cy, specBridgeCommunicator: SpecBridgeDomainCommunicator) => { - const doneEarly = () => { - cy.queue.stop() - - // we only need to worry about doneEarly when - // it comes from a manual event such as stopping - // Cypress or when we yield a (done) callback - // and could arbitrarily call it whenever we want - const p = cy.state('promise') - - // if our outer promise is pending - // then cancel outer and inner - // and set canceled to be true - if (p && p.isPending()) { - cy.state('canceled', true) - cy.state('cancel')() - } - - // if a command fails then after each commands - // could also fail unless we clear this out - cy.state('commandIntermediateValue', undefined) - - // reset the nestedIndex back to null - cy.state('nestedIndex', null) - } +interface RunDomainFnOptions { + config: Cypress.Config + data: any[] + env: Cypress.ObjectLike + fn: string + skipConfigValidation: boolean + state: {} +} +export const handleDomainFn = (cy: $Cy, specBridgeCommunicator: SpecBridgeDomainCommunicator) => { const reset = (state) => { cy.reset({}) @@ -51,9 +35,17 @@ export const handleDomainFn = (cy: $Cy, specBridgeCommunicator: SpecBridgeDomain // Set the state ctx to the runnable ctx to ensure they remain in sync cy.state('ctx', cy.state('runnable').ctx) + + // Stability is always false when we start as the page will always be + // loading at this point + cy.isStable(false, 'multi-domain-start') } - specBridgeCommunicator.on('run:domain:fn', async ({ data, fn, config, env, state, isDoneFnAvailable = false, skipConfigValidation = false }: { data: any[], fn: string, config: Cypress.Config, env: Cypress.ObjectLike, state: {}, isDoneFnAvailable: boolean, skipConfigValidation: boolean}) => { + specBridgeCommunicator.on('run:domain:fn', async (options: RunDomainFnOptions) => { + const { config, data, env, fn, state, skipConfigValidation } = options + + let queueFinished = false + reset(state) // @ts-ignore @@ -63,54 +55,21 @@ export const handleDomainFn = (cy: $Cy, specBridgeCommunicator: SpecBridgeDomain syncConfigToCurrentDomain(config) syncEnvToCurrentDomain(env) - let fnWrapper = `(${fn})` - - if (isDoneFnAvailable) { - // stub out the 'done' function if available in the primary domain - // to notify the primary domain if the done() callback is invoked - // within the spec bridge - const done = (err = undefined) => { - doneEarly() - - // signal to the primary domain that done has been called and to signal that the command queue is finished in the secondary domain - specBridgeCommunicator.toPrimaryError('done:called', { err }) - - specBridgeCommunicator.toPrimaryQueueFinished() - - return null - } - - // similar to the primary domain, the done() callback will be stored in - // state (necessary for error handling). if undefined and a user tries to - // call done, the same effect is granted - cy.state('done', done) - - fnWrapper = `((data) => { - const done = cy.state('done'); - return ${fnWrapper}(data) - })` - } - cy.state('onFail', (err) => { - const command = cy.state('current') + if (queueFinished) { + // If the queue is already finished, send this event instead because + // the primary won't be listening for 'queue:finished' anymore + specBridgeCommunicator.toPrimary('uncaught:error', { err }) - // If there isn't a current command, just reject to fail the test - if (!command) { - return specBridgeCommunicator.toPrimaryError('reject', { err }) + return } - const id = command.get('id') - const name = command.get('name') - const logId = command.getLastLog()?.get('id') - - specBridgeCommunicator.toPrimaryCommandEnd({ id, name, err, logId }) + cy.stop() + specBridgeCommunicator.toPrimary('queue:finished', { err }, { syncConfig: true }) }) try { - // await the eval func, whether it is a promise or not - // we should not need to transpile this as our target browsers support async/await - // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function for more details - const value = window.eval(fnWrapper)(data) + const value = window.eval(`(${fn})`)(data) // If we detect a non promise value with commands in queue, throw an error if (value && cy.queue.length > 0 && !value.then) { @@ -118,21 +77,44 @@ export const handleDomainFn = (cy: $Cy, specBridgeCommunicator: SpecBridgeDomain args: { value: $utils.stringify(value) }, }) } else { - const subject = await value + const hasCommands = !!cy.queue.length + + // If there are queued commands, their yielded value will be preferred + // over the value resolved by a return promise. Don't send the subject + // or the primary will think we're done already with a sync-returned + // value + const subject = hasCommands ? undefined : await value - specBridgeCommunicator.toPrimaryRanDomainFn({ + specBridgeCommunicator.toPrimary('ran:domain:fn', { subject, - // only sync the config if there are no commands in queue (for instance, only assertions exist in the callback) - resyncConfig: cy.queue.length === 0, + finished: !hasCommands, + }, { + // Only sync the config if there are no commands in queue + // (for instance, only assertions exist in the callback) + // since it means the callback is finished at this point + syncConfig: !hasCommands, }) + + if (!hasCommands) { + queueFinished = true + + return + } } } catch (err) { - specBridgeCommunicator.toPrimaryRanDomainFn({ - err, - resyncConfig: true, - }) - } finally { - cy.state('done', undefined) + specBridgeCommunicator.toPrimary('ran:domain:fn', { err }, { syncConfig: true }) + + return } + + cy.queue.run() + .then(() => { + queueFinished = true + specBridgeCommunicator.toPrimary('queue:finished', { + subject: cy.state('subject'), + }, { + syncConfig: true, + }) + }) }) } diff --git a/packages/driver/src/multi-domain/events/logs.ts b/packages/driver/src/multi-domain/events/logs.ts index a6943a1bc354..17a383c978cd 100644 --- a/packages/driver/src/multi-domain/events/logs.ts +++ b/packages/driver/src/multi-domain/events/logs.ts @@ -4,11 +4,21 @@ import $Log from '../../cypress/log' export const handleLogs = (Cypress: Cypress.Cypress, specBridgeCommunicator: SpecBridgeDomainCommunicator) => { const onLogAdded = (attrs) => { - specBridgeCommunicator.toPrimary('log:added', $Log.toSerializedJSON(attrs)) + // TODO: + // - handle printing console props (need to add to runner) + // this.runner.addLog(args[0], this.config('isInteractive')) + + specBridgeCommunicator.toPrimary('log:added', $Log.getDisplayProps(attrs)) } const onLogChanged = (attrs) => { - specBridgeCommunicator.toPrimary('log:changed', $Log.toSerializedJSON(attrs)) + // TODO: + // - add invocation stack if error: + // let parsedError = correctStackForCrossDomainError(log.get('err'), this.userInvocationStack) + // - notify runner? maybe not + // this.runner.addLog(args[0], this.config('isInteractive')) + + specBridgeCommunicator.toPrimary('log:changed', $Log.getDisplayProps(attrs)) } Cypress.on('log:added', onLogAdded) diff --git a/packages/driver/src/multi-domain/events/spec_window_events.ts b/packages/driver/src/multi-domain/events/spec_window.ts similarity index 100% rename from packages/driver/src/multi-domain/events/spec_window_events.ts rename to packages/driver/src/multi-domain/events/spec_window.ts diff --git a/packages/driver/src/multi-domain/events/test_events.ts b/packages/driver/src/multi-domain/events/test.ts similarity index 100% rename from packages/driver/src/multi-domain/events/test_events.ts rename to packages/driver/src/multi-domain/events/test.ts diff --git a/packages/electron/package.json b/packages/electron/package.json index bf431cfa6dbd..8e507ddc14ff 100644 --- a/packages/electron/package.json +++ b/packages/electron/package.json @@ -18,7 +18,7 @@ "@cypress/icons": "0.7.0", "bluebird": "3.5.3", "debug": "^4.3.2", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "lodash": "^4.17.21", "minimist": "1.2.5" }, diff --git a/packages/extension/package.json b/packages/extension/package.json index 61038a021614..8dc10fc6256f 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -26,7 +26,7 @@ "coffeescript": "1.12.7", "cross-env": "6.0.3", "eol": "0.9.1", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "gulp": "4.0.2", "gulp-clean": "0.4.0", "gulp-rename": "1.4.0", diff --git a/packages/https-proxy/package.json b/packages/https-proxy/package.json index 4a898947e106..2ac7d4767c20 100644 --- a/packages/https-proxy/package.json +++ b/packages/https-proxy/package.json @@ -16,7 +16,7 @@ "dependencies": { "bluebird": "3.5.3", "debug": "^4.3.2", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "lodash": "^4.17.21", "node-forge": "1.0.0", "semaphore": "1.1.0" diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 4686478286fb..313daf60aa3a 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -15,7 +15,7 @@ "bluebird": "3.5.3", "debug": "^4.3.2", "execa": "4.0.0", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "lodash": "^4.17.21", "plist": "3.0.1", "semver": "7.3.5" diff --git a/packages/net-stubbing/lib/server/util.ts b/packages/net-stubbing/lib/server/util.ts index c33d4be1464f..68d31bc26f9d 100644 --- a/packages/net-stubbing/lib/server/util.ts +++ b/packages/net-stubbing/lib/server/util.ts @@ -251,6 +251,10 @@ export function getBodyEncoding (req: CyHttpMessages.IncomingRequest): BodyEncod if (contentType.includes('charset=utf-8') || contentType.includes('charset="utf-8"')) { return 'utf8' } + + if (contentType.includes('multipart/form-data')) { + return 'binary' + } } // with fallback to inspecting the buffer using diff --git a/packages/net-stubbing/test/unit/util-spec.ts b/packages/net-stubbing/test/unit/util-spec.ts index 3775f48ffa8d..f2e1db390975 100644 --- a/packages/net-stubbing/test/unit/util-spec.ts +++ b/packages/net-stubbing/test/unit/util-spec.ts @@ -69,5 +69,19 @@ describe('net-stubbing util', () => { expect(getBodyEncoding(req), 'image').to.equal('binary') }) + + it('returns binary for form-data bodies', () => { + const formDataRequest = { + body: Buffer.from('hello world'), + headers: { + 'content-type': 'multipart/form-data', + }, + method: 'POST', + url: 'somewhere', + httpVersion: '1.1', + } + + expect(getBodyEncoding(formDataRequest)).to.equal('binary') + }) }) }) diff --git a/packages/network/package.json b/packages/network/package.json index 96fde1583b3b..e2fa897ab9b8 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -17,7 +17,7 @@ "bluebird": "3.5.3", "concat-stream": "1.6.2", "debug": "^4.3.2", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "lodash": "^4.17.21", "node-forge": "1.0.0", "proxy-from-env": "1.0.0" diff --git a/packages/resolve-dist/package.json b/packages/resolve-dist/package.json index 01347e9d6539..ee0f5b15ac42 100644 --- a/packages/resolve-dist/package.json +++ b/packages/resolve-dist/package.json @@ -13,7 +13,7 @@ "test-watch": "yarn test-unit --watch" }, "dependencies": { - "fs-extra": "8.1.0" + "fs-extra": "9.1.0" }, "devDependencies": { "@packages/ts": "0.0.0-development" diff --git a/packages/rewriter/package.json b/packages/rewriter/package.json index b130d523e49c..0c0a5a7a2655 100644 --- a/packages/rewriter/package.json +++ b/packages/rewriter/package.json @@ -22,7 +22,7 @@ "devDependencies": { "@cypress/request-promise": "4.2.6", "@types/parse5-html-rewriting-stream": "5.1.1", - "fs-extra": "9.0.0", + "fs-extra": "9.1.0", "nock": "12.0.3", "sinon": "9.0.2", "sinon-chai": "3.5.0", diff --git a/packages/runner-shared/src/event-manager.js b/packages/runner-shared/src/event-manager.js index ac62e8f13982..395470d15224 100644 --- a/packages/runner-shared/src/event-manager.js +++ b/packages/runner-shared/src/event-manager.js @@ -126,7 +126,10 @@ export const eventManager = { _.each(socketToDriverEvents, (event) => { ws.on(event, (...args) => { - Cypress.emit(event, ...args) + // these events are set up before Cypress is instantiated, so it's + // possible it's undefined when an event fires, but it's okay to + // ignore at that point + Cypress?.emit(event, ...args) }) }) @@ -548,6 +551,14 @@ export const eventManager = { }) Cypress.multiDomainCommunicator.on('after:screenshot', handleAfterScreenshot) + + Cypress.multiDomainCommunicator.on('log:added', (attrs) => { + reporterBus.emit('reporter:log:add', attrs) + }) + + Cypress.multiDomainCommunicator.on('log:changed', (attrs) => { + reporterBus.emit('reporter:log:state:changed', attrs) + }) }, _runDriver (state) { @@ -592,6 +603,7 @@ export const eventManager = { // but we want to be aggressive here // and force GC early and often Cypress.removeAllListeners() + Cypress.multiDomainCommunicator.removeAllListeners() localBus.emit('restart') }) diff --git a/packages/runner/package.json b/packages/runner/package.json index a1d7e4ca429c..f94cd80cd246 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -17,7 +17,7 @@ "watch": "webpack --watch --progress" }, "dependencies": { - "fs-extra": "8.1.0" + "fs-extra": "9.1.0" }, "devDependencies": { "@cypress/design-system": "0.0.0-development", diff --git a/packages/server/lib/environment.js b/packages/server/lib/environment.js index c9e7c59e9e9f..4a20b9df2443 100644 --- a/packages/server/lib/environment.js +++ b/packages/server/lib/environment.js @@ -62,6 +62,10 @@ try { // https://github.com/cypress-io/cypress/issues/15814 app.commandLine.appendSwitch('disable-dev-shm-usage') + // prevent navigation throttling when navigating in the browser rapid fire + // https://github.com/cypress-io/cypress/pull/20271 + app.commandLine.appendSwitch('disable-ipc-flooding-protection') + if (os.platform() === 'linux') { app.disableHardwareAcceleration() } diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index 63ba4fd62562..90672a20e6b5 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -212,6 +212,8 @@ const runPlugins = (ipc, pluginsFile, projectRoot) => { ipc.on('execute', (event, ids, args) => { execute(ipc, event, ids, args) }) + + ipc.send('ready') } // for testing purposes diff --git a/packages/server/lib/plugins/index.js b/packages/server/lib/plugins/index.js index e356a684763e..635f94487414 100644 --- a/packages/server/lib/plugins/index.js +++ b/packages/server/lib/plugins/index.js @@ -139,7 +139,9 @@ const init = (config, options) => { Object.keys(config).sort().forEach((key) => orderedConfig[key] = config[key]) config = orderedConfig - ipc.send('load', config) + ipc.on('ready', () => { + ipc.send('load', config) + }) ipc.on('loaded', (newCfg, registrations) => { _.omit(config, 'projectRoot', 'configFile') diff --git a/packages/server/package.json b/packages/server/package.json index d7c95a7e2759..de8fc69c5d96 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -61,7 +61,7 @@ "firefox-profile": "4.0.0", "fix-path": "3.0.0", "fluent-ffmpeg": "2.1.2", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "get-port": "5.1.1", "getos": "3.2.1", "glob": "7.1.3", @@ -116,7 +116,7 @@ "tsconfig-paths": "3.10.1", "tslib": "2.3.0", "underscore.string": "3.3.5", - "url-parse": "1.5.2", + "url-parse": "1.5.6", "uuid": "8.3.2", "which": "2.0.2", "widest-line": "3.1.0", diff --git a/packages/server/test/unit/plugins/index_spec.js b/packages/server/test/unit/plugins/index_spec.js index 46469f4c2432..c5898c8e92c0 100644 --- a/packages/server/test/unit/plugins/index_spec.js +++ b/packages/server/test/unit/plugins/index_spec.js @@ -115,15 +115,19 @@ describe('lib/plugins/index', () => { }) }) - it('sends \'load\' event with config via ipc', () => { - ipc.on.withArgs('loaded').yields([]) + it('sends \'load\' event with config via ipc once it receives \'ready\'', () => { const config = { pluginsFile: 'cypress-plugin', testingType: 'e2e' } - return plugins.init(config, getOptions({ testingType: 'e2e' })).then(() => { - expect(ipc.send).to.be.calledWith('load', { - ...config, - ...configExtras, - }) + plugins.init(config, getOptions({ testingType: 'e2e' })) + + expect(ipc.send).to.not.be.called + + // simulate async ready event + ipc.on.withArgs('ready').firstCall.callback() + + expect(ipc.send).to.be.calledWith('load', { + ...config, + ...configExtras, }) }) diff --git a/scripts/binary/bump.js b/scripts/binary/bump.js index 8a3cfa0befbc..f368f9aadc2a 100644 --- a/scripts/binary/bump.js +++ b/scripts/binary/bump.js @@ -17,15 +17,6 @@ const _PROVIDERS = { main: 'cypress-io/cypress', linux: [ 'cypress-io/cypress-test-module-api', - 'cypress-io/cypress-test-node-versions', - 'cypress-io/cypress-test-ci-environments', - 'cypress-io/cypress-test-example-repos', - ], - darwin: [ - 'cypress-io/cypress-test-example-repos', - ], - win32: [ - 'cypress-io/cypress-test-example-repos', ], }, } diff --git a/scripts/get-next-version.js b/scripts/get-next-version.js index b389a28dc7f6..949e9ba7fc22 100644 --- a/scripts/get-next-version.js +++ b/scripts/get-next-version.js @@ -24,6 +24,12 @@ const getNextVersionForPath = async (path) => { return semver.inc(currentVersion, releaseType || 'patch') } +if (require.main !== module) { + module.exports.getNextVersionForPath = getNextVersionForPath + + return +} + Bluebird.mapSeries(paths, async (path) => { const pathNextVersion = await getNextVersionForPath(path) diff --git a/scripts/unit/binary/bump-spec.js b/scripts/unit/binary/bump-spec.js index 4ebc06e0bf89..e5c6acb6dbc7 100644 --- a/scripts/unit/binary/bump-spec.js +++ b/scripts/unit/binary/bump-spec.js @@ -29,7 +29,7 @@ describe('bump', () => { ) }) - it('returns a filter function for circle and darwin', () => { + it('returns a filter function for circle and linux', () => { const projects = bump.remapProjects(bump._PROVIDERS) la( @@ -38,11 +38,11 @@ describe('bump', () => { projects, ) - const filter = bump.getFilterByProvider('circle', 'darwin') + const filter = bump.getFilterByProvider('circle', 'linux') const filtered = projects.filter(filter) la(filtered.length, 'there should be at least a few projects', filtered) - snapshot('should have just circle and darwin projects', filtered) + snapshot('should have just circle and linux projects', filtered) }) }) }) diff --git a/system-tests/README.md b/system-tests/README.md index 2a3ad47b4b70..a3504765de02 100644 --- a/system-tests/README.md +++ b/system-tests/README.md @@ -49,7 +49,7 @@ describe('my new project', () => { systemTests.setup() systemTests.it('fails as expected', { - project: Fixtures.projectPath('my-new-project'), + project: 'my-new-project', snapshot: true, spec: '*', expectedExitCode: 2 @@ -61,6 +61,36 @@ From here, you could run this test with `yarn test my-new-project`. There are many more options available for `systemTests.it` and `systemTests.setup`. You can massage the stdout, do pre-run tasks, set up HTTP/S servers, and more. Explore the typedocs in [`./lib/system-tests`](./lib/system-tests) for more information. +These tests run in the `system-tests-*` CI jobs. + +### Developing Docker-based tests against built binary + +Specs in the [`./test`](./test) directory are run against an unbuilt Cypress App. They don't test `cypress` NPM package installation or other prod app behavior. This is done so that they can run as fast as possible in CI, without waiting for a full build of the Cypress App. + +Specs in [`./test-binary`](./test-binary) are run against the *built Cypress App*. They also run inside of their own Docker containers to give a blank slate environment for Cypress to run in. Before each test, the prod CLI is `npm install`ed along with the built Cypress `.zip`, and real `cypress run` commands are used to run the tests. There should be no functional difference between running a project in these tests and running real prod Cypress inside of Docker in CI. + +The purpose of these tests is to test things that we normally can't inside of regular `system-tests`, such as testing Cypress with different Node versions, with/without Xvfb, or inside of different operating system versions. + +An example of using `dockerImage` and `withBinary` to write a binary system test: + +```ts +// ./test-binary/node-versions.spec.ts +import systemTests from '../lib/system-tests' +import Fixtures from '../lib/fixtures' + +describe('node versions', () => { + systemTests.it('runs in node 12', { + dockerImage: 'cypress:node/12', + project: 'todos', + withBinary: true, + }) +}) +``` + +Running `yarn test node-versions` would spin up a local Docker container for `cypress:node/12`, install Cypress from `../cypress.zip` and `../cli/build`, and then call the regular `cypress run` command within the container. Other options for `systemTests.it` such as `onRun` and `expectedExitCode` still function normally. + +These tests run in the `binary-system-tests` CI job. + ### Updating Snaphots Prepend `SNAPSHOT_UPDATE=1` to any test command. See [`snap-shot-it` instructions](https://github.com/bahmutov/snap-shot-it#advanced-use) for more info. diff --git a/system-tests/lib/docker.ts b/system-tests/lib/docker.ts new file mode 100644 index 000000000000..9d82af21d81b --- /dev/null +++ b/system-tests/lib/docker.ts @@ -0,0 +1,151 @@ +import type { SpawnerResult, Spawner } from './system-tests' +import Docker from 'dockerode' +import stream from 'stream' +import EventEmitter from 'events' +import path from 'path' +import { promises as fs } from 'fs' +import execa from 'execa' +import Fixtures from './fixtures' +import { nock } from './spec_helper' + +let docker: Docker | null = null + +const getDocker = () => { + return docker || (docker = new Docker()) +} + +const log = (...args) => { + console.error('🐋', ...args) +} + +class DockerProcess extends EventEmitter implements SpawnerResult { + stdout = new stream.PassThrough() + stderr = new stream.PassThrough() + + constructor (private dockerImage: string) { + super() + } + + pull () { + return new Promise((resolve, reject) => { + log('Pulling image', this.dockerImage) + getDocker().pull(this.dockerImage, null, (err, stream) => { + if (err) return reject(err) + + const onFinished = (err) => { + log('Pull complete', { err }) + if (err) return reject(err) + + resolve() + } + + const onProgress = (event) => { + log('Pull progress', JSON.stringify(event)) + } + + docker.modem.followProgress(stream, onFinished, onProgress) + }, null) + }) + } + + run (opts: { + cmd: string + args: string[] + env: Record + }) { + const containerCreateEnv = [] + + for (const k in opts.env) { + // skip problematic env vars that we don't wanna preserve from `process.env` + if (['DISPLAY', 'USER', 'HOME', 'USERNAME', 'PATH'].includes(k)) continue + + containerCreateEnv.push([k, opts.env[k]].join('=')) + } + + log('Running image', this.dockerImage) + + const cmd = [opts.cmd, ...opts.args] + + log('Running cmd', cmd.join(' ')) + + getDocker().run( + this.dockerImage, + cmd, + [this.stdout, this.stderr], + // option docs: https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate + { + AutoRemove: true, + Entrypoint: 'bash', + Tty: false, // so we can use stdout and stderr + Env: containerCreateEnv, + Binds: [ + [path.join(__dirname, '..', '..'), '/cypress'], + // map tmpDir to the same absolute path on the container to make it easier to reason about paths in tests + [Fixtures.cyTmpDir, Fixtures.cyTmpDir], + ].map((a) => a.join(':')), + }, + // option docs: https://docs.docker.com/engine/api/v1.37/#operation/ContainerStart + {}, + (err, data) => { + if (err) { + log('Docker run errored:', { err, data }) + + return this.emit('error', err) + } + + log('Docker run exited:', { err, data }) + this.emit('exit', data.StatusCode) + }, + ) + } +} + +const checkBuiltBinary = async () => { + try { + await fs.stat(path.join(__dirname, '..', '..', 'cypress.zip')) + } catch (err) { + throw new Error('Expected built cypress.zip at project root. Run `yarn binary-build` and `yarn binary-zip`.') + } + + try { + await fs.stat(path.join(__dirname, '..', '..', 'cli/build/package.json')) + } catch (err) { + throw new Error('Expected built CLI in /cli/build. Run `yarn build` in `cli`.') + } +} + +export const dockerSpawner: Spawner = async (cmd, args, env, options) => { + await checkBuiltBinary() + + const projectPath = Fixtures.projectPath(options.project) + + log('Running chmod 0777 on', projectPath, 'to avoid Docker permissions issues.') + await execa('chmod', `-R 0777 ${projectPath}`.split(' ')) + + const proc = new DockerProcess(options.dockerImage) + + nock.enableNetConnect('localhost') + + await proc.pull() + + if (options.withBinary) { + args = [cmd, ...args] + cmd = `/cypress/system-tests/scripts/bootstrap-docker-container.sh` + } else { + throw new Error('Docker testing is only supported with built binaries (withBinary: true)') + } + + env = { + ...env, + TEST_PROJECT_DIR: projectPath, + REPO_DIR: '/cypress', + } + + proc.run({ + cmd, + args, + env, + }) + + return proc +} diff --git a/system-tests/lib/fixtures.ts b/system-tests/lib/fixtures.ts index 5f7d7725be66..4d7f96a73476 100644 --- a/system-tests/lib/fixtures.ts +++ b/system-tests/lib/fixtures.ts @@ -9,7 +9,8 @@ const root = _path.join(__dirname, '..') const serverRoot = _path.join(__dirname, '../../packages/server/') const projects = _path.join(root, 'projects') -const cyTmpDir = _path.join(tempDir, 'cy-projects') + +export const cyTmpDir = _path.join(tempDir, 'cy-projects') // copy contents instead of deleting+creating new file, which can cause // filewatchers to lose track of toFile. diff --git a/system-tests/lib/performance-reporter.js b/system-tests/lib/performance-reporter.js index 8200ae4effca..234b1a2858e8 100644 --- a/system-tests/lib/performance-reporter.js +++ b/system-tests/lib/performance-reporter.js @@ -1,12 +1,62 @@ const path = require('path') const chalk = require('chalk') const Libhoney = require('libhoney') +const { v4: uuidv4 } = require('uuid') -const pkg = require('@packages/root') const ciProvider = require('@packages/server/lib/util/ci_provider') const { commitInfo } = require('@cypress/commit-info') +const { getNextVersionForPath } = require('../../scripts/get-next-version') -class StatsdReporter { +const honey = new Libhoney({ + dataset: 'systemtest-performance', + writeKey: process.env.HONEYCOMB_API_KEY, +}) + +// This event is created here independently every time the reporter +// is imported (in each parallel instance of the system-tests +// in circleci) so that we can use it as the parent, +// but ../scripts/send-root-honeycomb-event.js +// is only invoked once at the start of the build, +// and is responsible for sending it to honeycomb. +const spanId = process.env.CIRCLE_WORKFLOW_ID || uuidv4() +const circleCiRootEvent = honey.newEvent() + +circleCiRootEvent.timestamp = Date.now() +circleCiRootEvent.add({ + buildUrl: process.env.CIRCLE_BUILD_URL, + platform: process.platform, + arch: process.arch, + name: 'ci_run', + + spanId, + traceId: spanId, +}) + +// Mocha events ('test', 'test end', etc) have no way to wait +// for async callbacks, so we can't guarantee we have this +// data ready by the time any of the reporter's events are emitted. + +// Therefore, we have each honeycomb event await this promise +// before sending itself. +let asyncInfo = Promise.all([getNextVersionForPath(path.resolve(__dirname, '../../packages')), commitInfo()]) +.then(([nextVersion, commitInformation]) => { + const ciInformation = ciProvider.commitParams() || {} + + return { + nextVersion, + branch: commitInformation.branch || ciInformation.branch, + commitSha: commitInformation.sha || ciInformation.sha, + } +}) + +function addAsyncInfoAndSend (honeycombEvent) { + return asyncInfo.then((info) => { + honeycombEvent.add(info) + honeycombEvent.send() + }) +} + +class HoneycombReporter { constructor (runner) { if (!process.env.HONEYCOMB_API_KEY) { return @@ -14,23 +64,46 @@ class StatsdReporter { console.log(chalk.green('Reporting to honeycomb')) - let branch - let commitSha + runner.on('suite', (suite) => { + if (!suite.title) { + return + } - this.honey = new Libhoney({ - dataset: 'systemtest-performance', - writeKey: process.env.HONEYCOMB_API_KEY, - }) + const parent = suite.parent && suite.parent.honeycombEvent ? suite.parent.honeycombEvent : circleCiRootEvent - commitInfo().then((commitInformation) => { - const ciInformation = ciProvider.commitParams() || {} + suite.honeycombEvent = honey.newEvent() + suite.honeycombEvent.timestamp = Date.now() + suite.honeycombEvent.add({ + ...parent.data, + suite: suite.title, + specFile: suite.file && path.basename(suite.file), + name: 'spec_execution', - branch = commitInformation.branch || ciInformation.branch - commitSha = commitInformation.sha || ciInformation.sha + spanId: uuidv4(), + parentId: parent.data.spanId, + }) }) runner.on('test', (test) => { - test.wallclockStart = Date.now() + const path = test.titlePath() + // This regex pulls apart a string like `failing1 [electron]` + // into `failing1` and `electron`, letting us use the same + // test name for all browsers, with the browser as a separate field. + // The browser capture group is optional because some tests aren't browser specific, + // in which case it will be undefined and not passed as a field to honeycomb. + const [, testTitle, browser] = path[path.length - 1].match(/(.+?)(?: \[([a-z]+)\])?$/) + + test.honeycombEvent = honey.newEvent() + test.honeycombEvent.timestamp = Date.now() + test.honeycombEvent.add({ + ...test.parent.honeycombEvent.data, + test: testTitle, + browser, + name: 'test_execution', + + spanId: uuidv4(), + parentId: test.parent.honeycombEvent.data.spanId, + }) }) runner.on('test end', (test) => { @@ -39,44 +112,42 @@ class StatsdReporter { return } - const title = test.titlePath().join(' / ') - // This regex pulls apart a string like `e2e async timeouts / failing1 [electron]` - // into `e2e async timeouts / failing1` and `electron`, letting us use the same - // test name for all browsers, with the browser as a separate field. - // The browser capture group is optional because some tests aren't browser specific, - // in which case it will be undefined and not passed as a field to honeycomb. - const [, testTitle, browser] = title.match(/(.+?)(?: \[([a-z]+)\])?$/) - - const honeycombEvent = this.honey.newEvent() - - honeycombEvent.timestamp = test.wallclockStart - honeycombEvent.add({ - test: testTitle, - specFile: path.basename(test.file), - browser, + test.honeycombEvent.add({ state: test.state, err: test.err && test.err.message, errStack: test.err && test.err.stack, - durationMs: Date.now() - test.wallclockStart, - mochaDurationMs: test.duration, - branch, - commitSha, - buildUrl: process.env.CIRCLE_BUILD_URL, - platform: process.platform, - arch: process.arch, - version: pkg.version, + durationMs: Date.now() - test.honeycombEvent.timestamp, }) - honeycombEvent.send() + addAsyncInfoAndSend(test.honeycombEvent) + }) + + runner.on('suite end', (suite) => { + if (!suite.honeycombEvent) { + return + } + + suite.honeycombEvent.add({ + durationMs: Date.now() - suite.honeycombEvent.timestamp, + }) + + addAsyncInfoAndSend(suite.honeycombEvent) }) } - // If there is no done callback, then mocha-multi-reporter will kill the process without waiting for our honeycomb post to complete. + // If there is no done method, then mocha-multi-reporter will kill the process + // without waiting for our honeycomb posts to complete. done (failures, callback) { - if (this.honey) { - this.honey.flush().then(callback) - } + // Await the asyncInfo promise one last time, to ensure all events have + // added the data and sent themselves before we flush honeycomb's queue and exit. + asyncInfo + .then(() => honey.flush()) + .then(callback) } } -module.exports = StatsdReporter +module.exports = HoneycombReporter + +HoneycombReporter.honey = honey +HoneycombReporter.circleCiRootEvent = circleCiRootEvent +HoneycombReporter.addAsyncInfoAndSend = addAsyncInfoAndSend diff --git a/system-tests/lib/system-tests.ts b/system-tests/lib/system-tests.ts index 16d8ab2161f3..aa90d2bb6bb9 100644 --- a/system-tests/lib/system-tests.ts +++ b/system-tests/lib/system-tests.ts @@ -1,7 +1,9 @@ const snapshot = require('snap-shot-it') import { SpawnOptions } from 'child_process' +import stream from 'stream' import { expect } from './spec_helper' +import { dockerSpawner } from './docker' require('mocha-banner').register() const chalk = require('chalk').default @@ -11,7 +13,6 @@ const path = require('path') const http = require('http') const human = require('human-interval') const morgan = require('morgan') -const stream = require('stream') const express = require('express') const Bluebird = require('bluebird') const debug = require('debug')('cypress:system-tests') @@ -19,7 +20,6 @@ const httpsProxy = require('@packages/https-proxy') const Fixtures = require('./fixtures') const { allowDestroy } = require(`@packages/server/lib/util/server_destroy`) -const cypress = require(`@packages/server/lib/cypress`) const screenshots = require(`@packages/server/lib/screenshots`) const videoCapture = require(`@packages/server/lib/video_capture`) const settings = require(`@packages/server/lib/util/settings`) @@ -40,7 +40,7 @@ type ExecResult = { type ExecFn = (options?: ExecOptions) => Promise -type ItOptions = ExecOptions & { +export type ItOptions = ExecOptions & { /** * If a function is supplied, it will be executed instead of running the `systemTests.exec` function immediately. */ @@ -58,6 +58,14 @@ type ItOptions = ExecOptions & { } type ExecOptions = { + /** + * If set, `docker exec` will be used to run this test. Requires Docker. + */ + dockerImage?: string + /* + * If set, test using the built Cypress CLI and binary. Expects a built CLI in `/cli/build` and packed binary in `/cypress.zip`. + */ + withBinary?: boolean /** * Deprecated. Use `--cypress-inspect-brk` from command line instead. * @deprecated @@ -84,6 +92,10 @@ type ExecOptions = { * The spec argument to pass to Cypress. */ spec?: string + /** + * If set, use a non-default spec dir. + */ + specDir?: string /** * The project fixture to scaffold and pass to Cypress. */ @@ -244,11 +256,31 @@ type SetupOptions = { settings?: CypressConfig } +export type Spawner = (cmd, args, env, options: ExecOptions) => SpawnerResult | Promise + +export type SpawnerResult = { + stdout: stream.Readable + stderr: stream.Readable + on(event: 'error', cb: (err: Error) => void): void + on(event: 'exit', cb: (exitCode: number) => void): void +} + +const cpSpawner: Spawner = (cmd, args, env, options) => { + if (options.withBinary) { + throw new Error('withBinary is not supported without the use of dockerImage') + } + + return cp.spawn(cmd, args, { + env, + ...options.spawnOpts, + }) +} + const serverPath = path.dirname(require.resolve('@packages/server')) cp = Bluebird.promisifyAll(cp) -const env = _.clone(process.env) +const processEnvCache = _.clone(process.env) Bluebird.config({ longStackTraces: true, @@ -689,7 +721,7 @@ const systemTests = { }) afterEach(async function () { - process.env = _.clone(env) + process.env = _.clone(processEnvCache) this.timeout(human('2 minutes')) @@ -748,9 +780,10 @@ const systemTests = { return spec } - const specDir = options.testingType === 'component' ? 'component' : 'integration' + const specDir = options.specDir + || (options.testingType === 'component' ? 'cypress/component' : 'cypress/integration') - return path.join(projectPath, 'cypress', specDir, spec) + return path.join(projectPath, specDir, spec) }) // normalize the path to the spec @@ -763,10 +796,15 @@ const systemTests = { args (options: ExecOptions) { debug('converting options to args %o', { options }) - const args = [ + const projectPath = Fixtures.projectPath(options.project) + const args = options.withBinary ? [ + `run`, + `--project=${projectPath}`, + ] : [ + require.resolve('@packages/server'), // hides a user warning to go through NPM module `--cwd=${serverPath}`, - `--run-project=${Fixtures.projectPath(options.project)}`, + `--run-project=${projectPath}`, `--testingType=${options.testingType || 'e2e'}`, ] @@ -862,20 +900,6 @@ const systemTests = { return args }, - start (ctx, options: ExecOptions) { - options = this.options(ctx, options) - const args = this.args(options) - - return cypress.start(args) - .then(() => { - const { expectedExitCode } = options - - maybeVerifyExitCode(expectedExitCode, () => { - expect(process.exit).to.be.calledWith(expectedExitCode) - }) - }) - }, - /** * Executes a given project and optionally sanitizes and checks output. * @example @@ -896,7 +920,7 @@ const systemTests = { debug('systemTests.exec options %o', options) options = this.options(ctx, options) debug('processed options %o', options) - let args = this.args(options) + const args = options.args || this.args(options) const specifiedBrowser = process.env.BROWSER @@ -905,7 +929,8 @@ const systemTests = { } if (!options.skipScaffold) { - await Fixtures.scaffoldCommonNodeModules() + // symlinks won't work via docker + options.dockerImage || await Fixtures.scaffoldCommonNodeModules() Fixtures.scaffoldProject(options.project) await Fixtures.scaffoldProjectNodeModules(options.project) } @@ -918,10 +943,6 @@ const systemTests = { await settings.write(e2ePath, ctx.settings) } - const serverEntryFile = require.resolve('@packages/server') - - args = options.args || [serverEntryFile].concat(args) - let stdout = '' let stderr = '' @@ -997,41 +1018,43 @@ const systemTests = { } debug('spawning Cypress %o', { args }) - const cmd = options.command || 'node' - const sp = cp.spawn(cmd, args, { - env: _.chain(process.env) - .omit('CYPRESS_DEBUG') - .extend({ - // FYI: color will be disabled - // because we are piping the child process - COLUMNS: 100, - LINES: 24, - }) - .defaults({ - // match CircleCI's filesystem limits, so screenshot names in snapshots match - CYPRESS_MAX_SAFE_FILENAME_BYTES: 242, - FAKE_CWD_PATH: '/XXX/XXX/XXX', - DEBUG_COLORS: '1', - // prevent any Compression progress - // messages from showing up - VIDEO_COMPRESSION_THROTTLE: 120000, - - // don't fail our own tests running from forked PR's - CYPRESS_INTERNAL_SYSTEM_TESTS: '1', - - // Emulate no typescript environment - CYPRESS_INTERNAL_NO_TYPESCRIPT: options.noTypeScript ? '1' : '0', - - // disable frame skipping to make quick Chromium tests have matching snapshots/working video - CYPRESS_EVERY_NTH_FRAME: 1, - - // force file watching for use with --no-exit - ...(options.noExit ? { CYPRESS_INTERNAL_FORCE_FILEWATCH: '1' } : {}), - }) - .extend(options.processEnv) - .value(), - ...options.spawnOpts, + + const cmd = options.command || (options.withBinary ? 'cypress' : 'node') + + const env = _.chain(process.env) + .omit('CYPRESS_DEBUG') + .extend({ + // FYI: color will be disabled + // because we are piping the child process + COLUMNS: 100, + LINES: 24, + }) + .defaults({ + // match CircleCI's filesystem limits, so screenshot names in snapshots match + CYPRESS_MAX_SAFE_FILENAME_BYTES: 242, + FAKE_CWD_PATH: '/XXX/XXX/XXX', + DEBUG_COLORS: '1', + // prevent any Compression progress + // messages from showing up + VIDEO_COMPRESSION_THROTTLE: 120000, + + // don't fail our own tests running from forked PR's + CYPRESS_INTERNAL_SYSTEM_TESTS: '1', + + // Emulate no typescript environment + CYPRESS_INTERNAL_NO_TYPESCRIPT: options.noTypeScript ? '1' : '0', + + // disable frame skipping to make quick Chromium tests have matching snapshots/working video + CYPRESS_EVERY_NTH_FRAME: 1, + + // force file watching for use with --no-exit + ...(options.noExit ? { CYPRESS_INTERNAL_FORCE_FILEWATCH: '1' } : {}), }) + .extend(options.processEnv) + .value() + + const spawnerFn: Spawner = options.dockerImage ? dockerSpawner : cpSpawner + const sp: SpawnerResult = await spawnerFn(cmd, args, env, options) const ColorOutput = function () { const colorOutput = new stream.Transform() diff --git a/system-tests/package.json b/system-tests/package.json index 08a7e4b0ae25..68fcd25150ac 100644 --- a/system-tests/package.json +++ b/system-tests/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "projects:yarn:install": "node ./scripts/projects-yarn-install.js", - "test": "node ./scripts/run.js --glob-in-dir=test", + "test": "node ./scripts/run.js --glob-in-dir='{test,test-binary}'", "test:ci": "node ./scripts/run.js" }, "devDependencies": { @@ -43,12 +43,13 @@ "cors": "2.8.5", "dayjs": "^1.9.3", "debug": "^4.3.2", + "dockerode": "3.3.1", "execa": "1.0.0", "express": "4.17.1", "express-session": "1.16.1", "express-useragent": "1.0.15", "fluent-ffmpeg": "2.1.2", - "fs-extra": "8.1.0", + "fs-extra": "9.1.0", "glob": "7.2.0", "https-proxy-agent": "3.0.1", "human-interval": "1.0.0", diff --git a/system-tests/projects/e2e/cypress/integration/screenshots_spec.js b/system-tests/projects/e2e/cypress/integration/screenshots_spec.js index 2858c235882e..4f622da0919f 100644 --- a/system-tests/projects/e2e/cypress/integration/screenshots_spec.js +++ b/system-tests/projects/e2e/cypress/integration/screenshots_spec.js @@ -308,6 +308,7 @@ describe('taking screenshots', () => { cy.visit('http://localhost:3322/color/yellow') cy.screenshot('overwrite-test', { overwrite: false, + capture: 'viewport', clip: { x: 10, y: 10, width: 160, height: 80 }, }) @@ -320,6 +321,7 @@ describe('taking screenshots', () => { cy.screenshot('overwrite-test', { overwrite: true, + capture: 'viewport', clip: { x: 10, y: 10, width: 100, height: 50 }, }) @@ -342,6 +344,7 @@ describe('taking screenshots', () => { cy.viewport(600, 200) cy.visit('http://localhost:3322/color/yellow') cy.screenshot('overwrite-test', { + capture: 'viewport', clip: { x: 10, y: 10, width: 160, height: 80 }, }) @@ -353,6 +356,7 @@ describe('taking screenshots', () => { }) cy.screenshot('overwrite-test', { + capture: 'viewport', clip: { x: 10, y: 10, width: 100, height: 50 }, }) diff --git a/system-tests/scripts/bootstrap-docker-container.sh b/system-tests/scripts/bootstrap-docker-container.sh new file mode 100755 index 000000000000..058f215c5489 --- /dev/null +++ b/system-tests/scripts/bootstrap-docker-container.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -e # exit on error + +echo "$0 running as $(whoami)" +echo "Node version: $(node -v)" + +if [ ! -d "$TEST_PROJECT_DIR" ]; then + echo "Missing TEST_PROJECT_DIR=$TEST_PROJECT_DIR. Check Docker Bind+Env config" + exit 1 +fi + +if [ ! -d "$REPO_DIR" ]; then + echo "Missing REPO_DIR=$REPO_DIR. Check Docker Bind+Env config" + exit 1 +fi + +ZIP_PATH=$REPO_DIR/cypress.zip +CLI_PATH=$REPO_DIR/cli/build + +if [ ! -f "$ZIP_PATH" ]; then + echo "Missing $ZIP_PATH. Check Docker Bind config" + exit 1 +fi + +if [ ! -d "$CLI_PATH" ]; then + echo "Missing $CLI_PATH. Check Docker Bind config" + exit 1 +fi + +set -x # log commands + +cd $TEST_PROJECT_DIR + +export CYPRESS_INSTALL_BINARY=$ZIP_PATH +export CYPRESS_CACHE_FOLDER=/tmp/CYPRESS_CACHE_FOLDER/ + +npm install --save-dev --unsafe-perm --allow-root $CLI_PATH + +PATH=$PATH:./node_modules/.bin + +cypress install + +# run command passed in argv and store exit code +set +e +$@ +EXIT_CODE=$? +set -e + +# delete tmp to avoid permissions issues on the host +cd - +rm -rf $TEST_PROJECT_DIR + +exit $EXIT_CODE \ No newline at end of file diff --git a/system-tests/scripts/send-root-honecomb-event.js b/system-tests/scripts/send-root-honecomb-event.js new file mode 100644 index 000000000000..e3dc7bcdc67f --- /dev/null +++ b/system-tests/scripts/send-root-honecomb-event.js @@ -0,0 +1,13 @@ +const { addAsyncInfoAndSend, circleCiRootEvent, honey } = require('../lib/performance-reporter') + +// This file is executed once during the circleci build, +// so that we can send the root event honeycomb event for this +// run of the system tests exactly once. +// All the system test build hosts reference this root event, +// joining them into a single trace. +if (require.main === module) { + addAsyncInfoAndSend(circleCiRootEvent).then(() => { + console.log(circleCiRootEvent.data) + honey.flush() + }) +} diff --git a/system-tests/test-binary/ci_environments_spec.ts b/system-tests/test-binary/ci_environments_spec.ts new file mode 100644 index 000000000000..39d15c1d3fbb --- /dev/null +++ b/system-tests/test-binary/ci_environments_spec.ts @@ -0,0 +1,41 @@ +import systemTests, { ItOptions } from '../lib/system-tests' + +function smokeTestDockerImage (title: string, dockerImage: string, expectedExitCode: number, onRun?: ItOptions['onRun']) { + systemTests.it(title, { + withBinary: true, + browser: 'electron', + dockerImage, + spec: 'test1.js', + specDir: 'tests', + project: 'todos', + expectedExitCode, + onRun, + }) +} + +describe('e2e binary CI environments', () => { + smokeTestDockerImage( + 'bare node image fails (lacks xvfb)', + 'node:12', 1, + async (exec) => { + const { stdout } = await exec() + + expect(stdout).to.include('Your system is missing the dependency: Xvfb') + }, + ) + + smokeTestDockerImage( + 'bare xvfb image fails', + 'cypressinternal/xvfb:12.13.0', 1, + ) + + smokeTestDockerImage( + 'ubuntu 16 passes', + 'cypress/base:ubuntu16-12.13.1', 0, + ) + + smokeTestDockerImage( + 'ubuntu 19 passes', + 'cypress/base:ubuntu19-node12.14.1', 0, + ) +}) diff --git a/system-tests/test-binary/node_versions_spec.ts b/system-tests/test-binary/node_versions_spec.ts new file mode 100644 index 000000000000..8d1483ab410c --- /dev/null +++ b/system-tests/test-binary/node_versions_spec.ts @@ -0,0 +1,21 @@ +import systemTests from '../lib/system-tests' + +function smokeTestDockerImage (dockerImage: string) { + systemTests.it(`can run in ${dockerImage}`, { + withBinary: true, + browser: 'electron', + dockerImage, + spec: 'test1.js', + specDir: 'tests', + project: 'todos', + }) +} + +describe('e2e binary node versions', () => { + [ + 'cypress/base:12', + 'cypress/base:14', + 'cypress/base:16.5.0', + 'cypress/base:17.3.0', + ].forEach(smokeTestDockerImage) +}) diff --git a/yarn.lock b/yarn.lock index ad8082250864..3a3c172dc0a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7938,7 +7938,7 @@ dependencies: "@types/node" "*" -"@types/fs-extra@^9.0.11": +"@types/fs-extra@^9.0.11", "@types/fs-extra@^9.0.13": version "9.0.13" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== @@ -10849,10 +10849,10 @@ asn1.js@^5.2.0: minimalistic-assert "^1.0.0" safer-buffer "^2.1.0" -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== +asn1@^0.2.4, asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" @@ -12388,7 +12388,7 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= -bcrypt-pbkdf@^1.0.0: +bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= @@ -15212,6 +15212,13 @@ cp-file@^7.0.0: nested-error-stacks "^2.0.0" p-event "^4.1.0" +cpu-features@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz#9f636156f1155fd04bdbaa028bb3c2fbef3cea7a" + integrity sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA== + dependencies: + nan "^2.14.1" + cpy@^8.1.1: version "8.1.2" resolved "https://registry.yarnpkg.com/cpy/-/cpy-8.1.2.tgz#e339ea54797ad23f8e3919a5cffd37bfc3f25935" @@ -17042,6 +17049,24 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docker-modem@^3.0.0: + version "3.0.3" + resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz#ac4bb1f32f81ac2e7120c5e99a068fab2458a32f" + integrity sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.4.0" + +dockerode@3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz#74f66e239e092e7910e2beae6322d35c44b08cdc" + integrity sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ== + dependencies: + docker-modem "^3.0.0" + tar-fs "~2.0.1" + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -20109,6 +20134,16 @@ fs-extra@9.0.0: jsonfile "^6.0.1" universalify "^1.0.0" +fs-extra@9.1.0, fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" @@ -20156,16 +20191,6 @@ fs-extra@^6.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - fs-minipass@^1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -28089,10 +28114,10 @@ mz@^2.4.0, mz@^2.5.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.10.0, nan@^2.12.1: - version "2.14.2" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" - integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== +nan@^2.10.0, nan@^2.12.1, nan@^2.14.1, nan@^2.15.0: + version "2.15.0" + resolved "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanoid@3.1.20: version "3.1.20" @@ -36540,6 +36565,11 @@ spin.js@2.x: resolved "https://registry.yarnpkg.com/spin.js/-/spin.js-2.3.2.tgz#6caa56d520673450fd5cfbc6971e6d0772c37a1a" integrity sha1-bKpW1SBnNFD9XPvGlx5tB3LDeho= +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY= + split-on-first@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" @@ -36607,6 +36637,17 @@ ssestream@1.0.1: resolved "https://registry.yarnpkg.com/ssestream/-/ssestream-1.0.1.tgz#351551b12c00e91e7550f38d558323f3f47b54c2" integrity sha1-NRVRsSwA6R51UPONVYMj8/R7VMI= +ssh2@^1.4.0: + version "1.6.0" + resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.6.0.tgz#61aebc3a6910fe488f9c85cd8355bdf8d4724e05" + integrity sha512-lxc+uvXqOxyQ99N2M7k5o4pkYDO5GptOTYduWw7hIM41icxvoBcCNHcj+LTKrjkL0vFcAl+qfZekthoSFRJn2Q== + dependencies: + asn1 "^0.2.4" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "0.0.2" + nan "^2.15.0" + sshpk@^1.14.1, sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -37635,7 +37676,17 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" -tar-stream@^2.1.4: +tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + +tar-stream@^2.0.0, 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== @@ -39351,7 +39402,15 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@1.5.2, url-parse@^1.4.3, url-parse@^1.4.7: +url-parse@1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.6.tgz#b2a41d5a233645f3c31204cc8be60e76a15230a2" + integrity sha512-xj3QdUJ1DttD1LeSfvJlU1eiF1RvBSBfUu8GplFGdUzSO28y5yUtEl7wb//PI4Af6qh0o/K8545vUmucRrfWsw== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url-parse@^1.4.3, url-parse@^1.4.7: version "1.5.2" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.2.tgz#a4eff6fd5ff9fe6ab98ac1f79641819d13247cda" integrity sha512-6bTUPERy1muxxYClbzoRo5qtQuyoGEbzbQvi0SW4/8U8UyVkAQhWFBlnigqJkRm4su4x1zDQfNbEzWkt+vchcg==