diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt new file mode 100644 index 00000000000..6b1d4c8dee4 --- /dev/null +++ b/.circleci/cache-version.txt @@ -0,0 +1,3 @@ +# Bump this version to force CI to re-create the cache from scratch. + +07-02-24 diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..5639d07f702 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,71 @@ +version: 2.1 +setup: true + +orbs: + continuation: circleci/continuation@0.3.1 + +jobs: + verify-ci-should-run: + resource_class: small + docker: + - image: cimg/node:current + steps: + - run: + name: Verify CI should run + command: | + # run CI when manually triggers via CircleCi Dashboard + if [ <> == 'api' ]; then + echo "Always run CI when manually triggered from the UI." + exit 0 + fi + + if [[ "$CIRCLE_BRANCH" == "develop" || "$CIRCLE_BRANCH" == "release/"* ]]; then + echo "Always run CI for develop and for release candidate branches." + exit 0 + fi + + LAST_COMMIT_MESSAGE=$(curl --silent "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/commits/${CIRCLE_BRANCH}" | jq '.commit.message') + + if [[ "$LAST_COMMIT_MESSAGE" =~ "run ci" ]]; then + echo "Always run CI when the commit message includes 'run ci'." + exit 0 + fi + + cancel_build () { + echo "Canceling the CI build..." + circleci-agent step halt + } + + TRIGGER_INSTRUCTIONS="to trigger CI , include 'run ci' in the commit message or click the 'Trigger Pipeline' button in the CircleCI UI." + + if [ ! -z "${CIRCLE_PULL_REQUEST##*/}" ]; then + DRAFT=$(curl --silent "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/pulls/${CIRCLE_PULL_REQUEST##*/}" | jq '.draft') + + if [[ "${DRAFT}" == true ]]; then + echo "Skipping CI; PR is in draft - $TRIGGER_INSTRUCTIONS" + cancel_build + fi + + echo "Always run CI for PR that is ready for review." + exit 0 + fi + + echo "Skipping CI; branch in progress - $TRIGGER_INSTRUCTIONS" + cancel_build + + - run: + name: Download .circleci/workflows.yaml + command: | + if [[ "$CIRCLE_BRANCH" == "pull/"* ]]; then + curl -o workflows.yml https://raw.githubusercontent.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/develop/.circleci/workflows.yml + else + curl -o workflows.yml https://raw.githubusercontent.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BRANCH}/.circleci/workflows.yml + fi + - continuation/continue: + configuration_path: workflows.yml + +workflows: + # the setup-workflow workflow is always triggered. + setup-workflow: + jobs: + - verify-ci-should-run diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml new file mode 100644 index 00000000000..85be15842a0 --- /dev/null +++ b/.circleci/workflows.yml @@ -0,0 +1,3569 @@ +version: 2.1 + +defaults: &defaults + parallelism: 1 + working_directory: ~/cypress + parameters: &defaultsParameters + executor: + type: executor + default: cy-doc + only-cache-for-root-user: + type: boolean + default: false + executor: <> + environment: &defaultsEnvironment + ## set specific timezone + TZ: "/usr/share/zoneinfo/America/New_York" + + ## store artifacts here + CIRCLE_ARTIFACTS: /tmp/artifacts + + ## set so that e2e tests are consistent + COLUMNS: 100 + LINES: 24 + +mainBuildFilters: &mainBuildFilters + filters: + branches: + only: + - develop + - /^release\/\d+\.\d+\.\d+$/ + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - 'update-v8-snapshot-cache-on-develop' + - 'investigate/darwin-ci-build-order' + - 'publish-binary' + - 'angular-signals-ct-harness' + +# 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 +# so just add your branch to the list here to build and test on Mac +macWorkflowFilters: &darwin-workflow-filters + when: + or: + - equal: [ develop, << pipeline.git.branch >> ] + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'angular-signals-ct-harness', << pipeline.git.branch >> ] + - equal: [ 'investigate/darwin-ci-build-order', << pipeline.git.branch >> ] + - matches: + pattern: /^release\/\d+\.\d+\.\d+$/ + value: << pipeline.git.branch >> + +linuxArm64WorkflowFilters: &linux-arm64-workflow-filters + when: + or: + - equal: [ develop, << pipeline.git.branch >> ] + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'chore/fix_kitchensink_against_staging_job', << pipeline.git.branch >> ] + - matches: + pattern: /^release\/\d+\.\d+\.\d+$/ + value: << pipeline.git.branch >> + +# uncomment & add to the branch conditions below to disable the main linux +# flow if we don't want to test it for a certain branch +linuxWorkflowExcludeFilters: &linux-x64-workflow-exclude-filters + unless: + or: + - matches: + pattern: /^pull\/[0-9]+/ + value: << pipeline.git.branch >> + - false + +# windows is slow and expensive in CI, so it normally only runs on main branches +# add your branch to this list to run the full Windows build on your PR +windowsWorkflowFilters: &windows-workflow-filters + when: + or: + - equal: [ develop, << pipeline.git.branch >> ] + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] + - equal: [ 'angular-signals-ct-harness', << pipeline.git.branch >> ] + - matches: + pattern: /^release\/\d+\.\d+\.\d+$/ + value: << pipeline.git.branch >> + +executors: + # the Docker image with Cypress dependencies and Chrome browser + cy-doc: + docker: + - image: cypress/browsers-internal:node18.17.1-chrome124-ff125 + # by default, we use "medium" to balance performance + CI costs. bump or reduce on a per-job basis if needed. + resource_class: medium + environment: + PLATFORM: linux + CI_DOCKER: "true" + + kitchensink-executor: + docker: + - image: cypress/browsers-internal:node20.15.0-chrome126-ff127 + # by default, we use "medium" to balance performance + CI costs. bump or reduce on a per-job basis if needed. + resource_class: medium + environment: + PLATFORM: linux + CI_DOCKER: "true" + + # Docker image with non-root "node" user + non-root-docker-user: + docker: + - image: cypress/browsers-internal:node18.17.1-chrome124-ff125 + user: node + environment: + PLATFORM: linux + + # executor to run on Mac OS + # https://circleci.com/docs/2.0/executor-types/#using-macos + # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions + darwin-amd64: + machine: true + environment: + PLATFORM: darwin + + # executor to run on Windows - based off of the windows-orb default executor since it is + # not customizable enough to align with our existing setup. + # https://github.com/CircleCI-Public/windows-orb/blob/master/src/executors/default.yml + # https://circleci.com/docs/2.0/hello-world-windows/#software-pre-installed-in-the-windows-image + windows: &windows-executor + machine: + image: windows-server-2022-gui:stable + shell: bash.exe -eo pipefail + resource_class: windows.large + environment: + PLATFORM: windows + + darwin-arm64: &darwin-arm64-executor + machine: true + environment: + PLATFORM: darwin + + linux-arm64: &linux-arm64-executor + machine: + image: ubuntu-2004:2023.07.1 + resource_class: arm.medium + environment: + PLATFORM: linux + # TODO: Disabling snapshots for now on Linux Arm 64 architectures. Will revisit with https://github.com/cypress-io/cypress/issues/23557 + DISABLE_SNAPSHOT_REQUIRE: 1 + +commands: + # This command inserts SHOULD_PERSIST_ARTIFACTS into BASH_ENV. This way, we can define the variable in one place and use it in multiple steps. + # Run this command in a job before you want to use the SHOULD_PERSIST_ARTIFACTS variable. + setup_should_persist_artifacts: + steps: + - run: + name: Set environment variable to determine whether or not to persist artifacts + command: | + echo "Setting SHOULD_PERSIST_ARTIFACTS variable" + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "chore/fix_kitchensink_against_staging_job" ]]; then + export SHOULD_PERSIST_ARTIFACTS=true + fi' >> "$BASH_ENV" + # You must run `setup_should_persist_artifacts` command and be using bash before running this command + verify_should_persist_artifacts: + steps: + - run: + name: Check current branch to persist artifacts + command: | + if [[ -z "$SHOULD_PERSIST_ARTIFACTS" ]]; then + echo "Not uploading artifacts or posting install comment for this branch." + circleci-agent step halt + fi + + maybe_skip_binary_jobs: + steps: + - run: + name: Skip binary job if external PR + command: | + if [[ -z "$CIRCLE_TOKEN" ]]; then + echo "There is no CIRCLE_TOKEN set for this job. Cannot trigger binary build. Skipping job." + circleci-agent step halt + fi + + restore_workspace_binaries: + steps: + - attach_workspace: + at: ~/ + # make sure we have cypress.zip received + - run: ls -l + - run: ls -l cypress.zip cypress.tgz + - run: node --version + - run: npm --version + + restore_cached_workspace: + steps: + - attach_workspace: + at: ~/ + - install-required-node + - unpack-dependencies + + restore_cached_binary: + steps: + - attach_workspace: + at: ~/ + + prepare-modules-cache: + parameters: + dont-move: + type: boolean + default: false + steps: + - run: node scripts/circle-cache.js --action prepare + - unless: + condition: << parameters.dont-move >> + steps: + - run: + name: Move to /tmp dir for consistent caching across root/non-root users + command: | + mkdir -p /tmp/node_modules_cache + mv ~/cypress/node_modules /tmp/node_modules_cache/root_node_modules + mv ~/cypress/cli/node_modules /tmp/node_modules_cache/cli_node_modules + mv ~/cypress/system-tests/node_modules /tmp/node_modules_cache/system-tests_node_modules + mv ~/cypress/globbed_node_modules /tmp/node_modules_cache/globbed_node_modules + + install-webkit-deps: + steps: + - run: + name: Install WebKit dependencies + command: | + npx playwright install webkit + npx playwright install-deps webkit + + build-and-persist: + description: Save entire folder as artifact for other jobs to run without reinstalling + steps: + - run: + name: Sync Cloud Validations + command: | + source ./scripts/ensure-node.sh + yarn gulp syncCloudValidations + - run: + name: Build packages + command: | + source ./scripts/ensure-node.sh + yarn build + - run: + name: Generate v8 snapshot + command: | + source ./scripts/ensure-node.sh + # Minification takes some time. We only really need to do that for the binary (and we regenerate snapshots separately there) + V8_SNAPSHOT_DISABLE_MINIFY=1 yarn build-v8-snapshot-prod + - prepare-modules-cache # So we don't throw these in the workspace cache + - persist_to_workspace: + root: ~/ + paths: + - cypress + + install_cache_helpers_dependencies: + steps: + - run: + # Dependencies needed by circle-cache.js, before we "yarn" or unpack cached node_modules + name: Cache Helper Dependencies + working_directory: ~/ + command: npm i glob@7.1.6 fs-extra@10.0.0 minimist@1.2.5 fast-json-stable-stringify@2.1.0 + + unpack-dependencies: + description: 'Unpacks dependencies associated with the current workflow' + steps: + - install_cache_helpers_dependencies + - run: + name: Generate Circle Cache Key + command: node scripts/circle-cache.js --action cacheKey > circle_cache_key + - run: + name: Generate platform key + command: node ./scripts/get-platform-key.js > platform_key + - restore_cache: + name: Restore cache state, to check for known modules cache existence + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} + - run: + name: Move node_modules back from /tmp + command: | + if [[ -d "/tmp/node_modules_cache" ]]; then + mv /tmp/node_modules_cache/root_node_modules ~/cypress/node_modules + mv /tmp/node_modules_cache/cli_node_modules ~/cypress/cli/node_modules + mv /tmp/node_modules_cache/system-tests_node_modules ~/cypress/system-tests/node_modules + mv /tmp/node_modules_cache/globbed_node_modules ~/cypress/globbed_node_modules + rm -rf /tmp/node_modules_cache + fi + - run: + name: Restore all node_modules to proper workspace folders + command: node scripts/circle-cache.js --action unpack + + restore_cached_system_tests_deps: + description: 'Restore the cached node_modules for projects in "system-tests/projects/**"' + steps: + - 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: node ./scripts/get-platform-key.js > platform_key + - restore_cache: + name: Restore system tests node_modules cache + keys: + - v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + + update_cached_system_tests_deps: + description: 'Update the cached node_modules for projects in "system-tests/projects/**"' + steps: + - 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: node ./scripts/get-platform-key.js > platform_key + - restore_cache: + name: Restore cache state, to check for known modules cache existence + keys: + - v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-state-of-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + - run: + name: Bail if specific cache exists + command: | + 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{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + - v{{ checksum ".circleci/cache-version.txt" }}-{{ 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{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + paths: + - /tmp/cy-system-tests-node-modules + - run: touch /tmp/system_tests_node_modules_installed + - save_cache: + name: Save system tests node_modules cache state key + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-state-of-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} + paths: + - /tmp/system_tests_node_modules_installed + + caching-dependency-installer: + description: 'Installs & caches the dependencies based on yarn lock & package json dependencies' + parameters: + only-cache-for-root-user: + type: boolean + default: false + build-better-sqlite3: + type: boolean + default: false + steps: + - install_cache_helpers_dependencies + - run: + name: Generate Circle Cache Key + command: node scripts/circle-cache.js --action cacheKey > circle_cache_key + - run: + name: Generate platform key + command: node ./scripts/get-platform-key.js > platform_key + - when: + condition: <> + steps: + - restore_cache: + name: Restore cache state, to check for known modules cache existence + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }}-{{ checksum "centos7-builder.Dockerfile" }} + - unless: + condition: <> + steps: + - restore_cache: + name: Restore cache state, to check for known modules cache existence + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }} + - run: + name: Bail if cache exists + command: | + if [[ -f "node_modules_installed" ]]; then + echo "Node modules already cached for dependencies, exiting" + circleci-agent step halt + fi + - run: date +%Y-%U > cache_date + - restore_cache: + name: Restore weekly yarn cache + keys: + - v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }} + - run: + name: Install Node Modules + command: | + source ./scripts/ensure-node.sh + # avoid installing Percy's Chromium every time we use @percy/cli + # https://docs.percy.io/docs/caching-asset-discovery-browser-in-ci + PERCY_POSTINSTALL_BROWSER=true \ + yarn --prefer-offline --frozen-lockfile --cache-folder ~/.yarn + no_output_timeout: 20m + - when: + condition: <> + steps: + - build-better-sqlite3 + - prepare-modules-cache: + dont-move: <> # we don't move, so we don't hit any issues unpacking symlinks + - when: + condition: <> # we don't move to /tmp since we don't need to worry about different users + steps: + - save_cache: + name: Saving node modules for root, cli, and all globbed workspace packages + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} + paths: + - node_modules + - cli/node_modules + - system-tests/node_modules + - globbed_node_modules + - unless: + condition: <> + steps: + - save_cache: + name: Saving node modules for root, cli, and all globbed workspace packages + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} + paths: + - /tmp/node_modules_cache + - run: touch node_modules_installed + - when: + condition: <> + steps: + - save_cache: + name: Saving node-modules cache state key + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }}-{{ checksum "centos7-builder.Dockerfile" }} + paths: + - node_modules_installed + - unless: + condition: <> + steps: + - save_cache: + name: Saving node-modules cache state key + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }} + paths: + - node_modules_installed + - save_cache: + name: Save weekly yarn cache + key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }} + paths: + - ~/.yarn + - ~/.cy-npm-cache + + verify-build-setup: + description: Common commands run when setting up for build or yarn install + parameters: + executor: + type: executor + default: cy-doc + steps: + - run: pwd + - run: + name: print global yarn cache path + command: echo $(yarn global bin) + - run: + name: print yarn version + command: yarn versions + - unless: + condition: + # stop-only does not correctly match on windows: https://github.com/bahmutov/stop-only/issues/78 + equal: [ *windows-executor, << parameters.executor >> ] + steps: + - run: + name: Stop .only + # this will catch ".only"s in js/coffee as well + command: | + source ./scripts/ensure-node.sh + yarn stop-only-all + - run: + name: Check terminal variables + ## make sure the TERM is set to 'xterm' in node (Linux only) + ## else colors (and tests) will fail + ## See the following information + ## * http://andykdocs.de/development/Docker/Fixing+the+Docker+TERM+variable+issue + ## * https://unix.stackexchange.com/questions/43945/whats-the-difference-between-various-term-variables + command: | + source ./scripts/ensure-node.sh + yarn check-terminal + + install-required-node: + # https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/2 + description: Install Node version matching .node-version + steps: + - run: + name: Install Node + command: | + source ./scripts/ensure-node.sh + echo "Installing Yarn" + npm install yarn --location=global # ensure yarn is installed with the correct node engine + yarn check-node-version + - run: + name: Check Node + command: | + source ./scripts/ensure-node.sh + yarn check-node-version + + install-chrome: + description: Install Google Chrome + parameters: + channel: + description: browser channel to install + type: string + version: + description: browser version to install + type: string + steps: + - run: + name: Install Google Chrome (<>) + command: | + echo "Installing Chrome (<>) v<>" + wget -O /usr/src/google-chrome-<>_<>_amd64.deb "http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-<>/google-chrome-<>_<>-1_amd64.deb" && \ + dpkg -i /usr/src/google-chrome-<>_<>_amd64.deb ; \ + apt-get install -f -y && \ + rm -f /usr/src/google-chrome-<>_<>_amd64.deb + which google-chrome-<> || (printf "\n\033[0;31mChrome was not successfully downloaded - bailing\033[0m\n\n" && exit 1) + echo "Location of Google Chrome Installation: `which google-chrome-<>`" + echo "Google Chrome Version: `google-chrome-<> --version`" + + # This code builds better-sqlite3 on CentOS 7. This is necessary because CentOS 7 has the oldest glibc version + # that we support. The script checks for the existence of the Centos7-builder image tar file, and skips if it already + # exists. If you want to rebuild the image, set the REBUILD_CENTOS_BUILDER_IMAGE environment variable to any value. + # Since this is running Docker remote, we need to copy the project into the container, and copy the built plugin out + # of the container because the host running docker does not have access to the project directory so volume mounts are + # not possible. The built plugin is copied to the project directory so it can be injected into the final binary. + build-better-sqlite3: + description: Build better-sqlite3 for CentOS 7 + steps: + - setup_remote_docker + - restore_cache: + keys: + - cypress-centos7-builder-{{ checksum "centos7-builder.Dockerfile" }} + - restore_cache: + keys: + - better-sqlite3-{{ checksum "node_modules/better-sqlite3/package.json" }}-{{ checksum "node_modules/electron/package.json" }}-{{ checksum "centos7-builder.Dockerfile" }} + - run: + name: Build or load centos7-builder image + command: | + if [[ ! -f better_sqlite3.node ]]; then + set -x + apt update && apt install -y docker.io + if [[ ! -f centos7-builder.tar || -n $REBUILD_CENTOS_BUILDER_IMAGE ]]; then + echo "*" > .dockerignore + docker build -t centos7-builder -f centos7-builder.Dockerfile . + docker save centos7-builder > centos7-builder.tar + rm .dockerignore + else + docker load < centos7-builder.tar + fi + fi + - save_cache: + key: cypress-centos7-builder-{{ checksum "centos7-builder.Dockerfile" }} + paths: + - centos7-builder.tar + - run: + name: Build better-sqlite3 for CentOS 7 + command: | + if [[ ! -f better_sqlite3.node ]]; then + docker run -d --name centos7-builder centos7-builder /bin/bash -c "sleep 1000000000" + docker cp ~/cypress/node_modules/better-sqlite3 centos7-builder:/better-sqlite3 + docker exec -it centos7-builder /bin/bash -c "cd /better-sqlite3 && source /root/.bashrc && chown -R root:root . && npm install --ignore-scripts && npx --no-install prebuild -r electron -t 27.1.3 --include-regex 'better_sqlite3.node$'" + docker cp centos7-builder:/better-sqlite3/build/Release/better_sqlite3.node ~/cypress/node_modules/better-sqlite3/build/Release/better_sqlite3.node + docker rm -f centos7-builder + cp ~/cypress/node_modules/better-sqlite3/build/Release/better_sqlite3.node ~/cypress/better_sqlite3.node + else + cp ~/cypress/better_sqlite3.node ~/cypress/node_modules/better-sqlite3/build/Release/better_sqlite3.node + fi + - save_cache: + key: better-sqlite3-{{ checksum "node_modules/better-sqlite3/package.json" }}-{{ checksum "node_modules/electron/package.json" }}-{{ checksum "centos7-builder.Dockerfile" }} + paths: + - better_sqlite3.node + - run: + name: Clean up top level better-sqlite3 file + command: | + rm ~/cypress/better_sqlite3.node + + run-driver-integration-tests: + parameters: + browser: + description: browser shortname to target + type: string + install-chrome-channel: + description: chrome channel to install + type: string + default: '' + steps: + - restore_cached_workspace + - when: + condition: <> + steps: + - install-chrome: + channel: <> + version: $(node ./scripts/get-browser-version.js chrome:<>) + - when: + condition: + equal: [ webkit, << parameters.browser >> ] + steps: + - install-webkit-deps + - run: + name: Run driver tests in Cypress + environment: + CYPRESS_CONFIG_ENV: production + command: | + echo Current working directory is $PWD + echo Total containers $CIRCLE_NODE_TOTAL + + if [[ -v MAIN_RECORD_KEY ]]; then + # internal PR + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + CYPRESS_INTERNAL_ENABLE_TELEMETRY="true" \ + yarn cypress:run --record --parallel --group 5x-driver-<> --browser <> --runner-ui + else + # external PR + TESTFILES=$(circleci tests glob "cypress/e2e/**/*.cy.*" | circleci tests split --total=$CIRCLE_NODE_TOTAL) + echo "Test files for this machine are $TESTFILES" + + if [[ -z "$TESTFILES" ]]; then + echo "Empty list of test files" + fi + yarn cypress:run --browser <> --spec $TESTFILES --runner-ui + fi + working_directory: packages/driver + - verify-mocha-results + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + windows-install-chrome: + parameters: + browser: + description: browser shortname to target + type: string + steps: + - run: + # TODO: How can we have preinstalled browsers on CircleCI? + name: 'Install Chrome on Windows' + command: | + # install with `--ignore-checksums` to avoid checksum error + # https://www.gep13.co.uk/blog/chocolatey-error-hashes-do-not-match + [[ $PLATFORM == 'windows' && '<>' == 'chrome' ]] && choco install googlechrome -y --ignore-checksums || [[ $PLATFORM != 'windows' ]] + + run-new-ui-tests: + parameters: + package: + description: package to target + type: enum + enum: ['frontend-shared', 'launchpad', 'app', 'reporter'] + browser: + description: browser shortname to target + type: string + percy: + description: enable percy + type: boolean + default: false + type: + description: ct or e2e + type: enum + enum: ['ct', 'e2e'] + debug: + description: debug option + type: string + default: '' + steps: + - restore_cached_workspace + - windows-install-chrome: + browser: <> + - run: + command: | + echo Current working directory is $PWD + echo Total containers $CIRCLE_NODE_TOTAL + + if [[ -v MAIN_RECORD_KEY ]]; then + # internal PR + cmd=$([[ <> == 'true' ]] && echo 'yarn percy exec --parallel -- --') || true + + DEBUG=<> \ + CYPRESS_CONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + CYPRESS_INTERNAL_ENABLE_TELEMETRY="true" \ + $cmd yarn workspace @packages/<> cypress:run:<> --browser <> --record --parallel --group <>-<> + else + # external PR + + # To make `circleci tests` work correctly, we need to step into the package folder. + cd packages/<> + + if [[ <> == 'ct' ]]; then + # component tests are located side by side with the source codes. + # for the app component tests, ignore specs that are known to cause failures on contributor PRs (see https://discuss.circleci.com/t/how-to-exclude-certain-files-from-circleci-test-globbing/41028) + TESTFILES=$(find src -regextype posix-extended -name '*.cy.*' -not -regex '.*(FileMatch|PromoAction|SelectorPlayground|useDurationFormat|useTestingType|SpecPatterns).cy.*' | circleci tests split --total=$CIRCLE_NODE_TOTAL) + else + GLOB="cypress/e2e/**/*cy.*" + TESTFILES=$(circleci tests glob "$GLOB" | circleci tests split --total=$CIRCLE_NODE_TOTAL) + fi + + echo "Test files for this machine are $TESTFILES" + + # To run the `yarn` command, we need to walk out of the package folder. + cd ../.. + + DEBUG=<> \ + CYPRESS_CONFIG_ENV=production \ + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn workspace @packages/<> cypress:run:<> --browser <> --spec $TESTFILES + fi + - run: + command: | + if [[ <> == 'app' && <> == 'true' && -d "packages/app/cypress/screenshots/runner/screenshot/screenshot.cy.tsx/percy" ]]; then + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy upload packages/app/cypress/screenshots/runner/screenshot/screenshot.cy.tsx/percy + else + echo "skipping percy screenshots uploading" + fi + - store_test_results: + path: /tmp/cypress + - store-npm-logs + + run-system-tests: + parameters: + browser: + description: browser shortname to target + type: string + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - when: + condition: + equal: [ webkit, << parameters.browser >> ] + steps: + - install-webkit-deps + - run: + name: Run system tests + environment: + CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0' + command: | + ALL_SPECS=`circleci tests glob "/root/cypress/system-tests/test/*spec*"` + SPECS= + for file in $ALL_SPECS; do + # filter out non_root tests, they have their own stage + if [[ "$file" == *"non_root"* ]]; then + echo "Skipping $file" + continue + fi + SPECS="$SPECS $file" + done + SPECS=`echo $SPECS | xargs -n 1 | circleci tests split --split-by=timings` + echo SPECS=$SPECS + yarn workspace @tooling/system-tests test:ci $SPECS --browser <> + - verify-mocha-results + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + run-binary-system-tests: + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + name: Run system tests + environment: + CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0' + 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: + - store_artifacts: + path: ~/.npm/_logs + + post-install-comment: + parameters: + package_url_path: + type: string + default: npm-package-url.json + binary_url_path: + type: string + default: binary-url.json + description: Post GitHub comment with a blurb on how to install pre-release version + steps: + - run: + name: Post pre-release install comment + command: | + node scripts/add-install-comment.js \ + --npm << parameters.package_url_path >> \ + --binary << parameters.binary_url_path >> + + verify-mocha-results: + description: Double-check that Mocha tests ran as expected. + parameters: + expectedResultCount: + description: The number of result files to expect, ie, the number of Mocha test suites that ran. + type: integer + ## by default, assert that at least 1 test ran + default: 0 + steps: + - run: + name: 'Verify Mocha Results' + command: | + source ./scripts/ensure-node.sh + yarn verify:mocha:results <> + + clone-repo-and-checkout-branch: + description: | + Clones an external repo and then checks out the branch that matches the next version otherwise uses 'master' branch. + parameters: + repo: + description: "Name of the github repo to clone like: cypress-example-kitchensink" + type: string + pull_request_id: + description: Pull request number to check out before installing and testing + type: integer + default: 0 + steps: + - restore_cached_binary + - run: + name: "Cloning test project and checking out release branch: <>" + working_directory: /tmp/<> + command: | + git clone --depth 1 --no-single-branch https://github.com/cypress-io/<>.git . + + cd ~/cypress/.. + # install some deps for get-next-version + npm i semver@7.3.2 conventional-recommended-bump@6.1.0 conventional-changelog-angular@5.0.12 minimist@1.2.5 + NEXT_VERSION=$(node ./cypress/scripts/get-next-version.js) + cd - + + git checkout $NEXT_VERSION || true + - when: + condition: <> + steps: + - run: + name: Check out PR <> + working_directory: /tmp/<> + command: | + git fetch origin pull/<>/head:pr-<> + git checkout pr-<> + + test-binary-against-rwa: + description: | + Takes the built binary and NPM package, clones the RWA repo + and runs the new version of Cypress against it. + parameters: + repo: + description: "Name of the github repo to clone like" + type: string + default: "cypress-realworld-app" + browser: + description: Name of the browser to use, like "electron", "chrome", "firefox" + type: enum + enum: ["", "electron", "chrome", "firefox"] + default: "" + command: + description: Test command to run to start Cypress tests + type: string + default: "CYPRESS_INTERNAL_ENABLE_TELEMETRY=1 CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY CYPRESS_PROJECT_ID=ypt4pf yarn cypress:run" + # if the repo to clone and test is a monorepo, you can + # run tests inside a specific subfolder + folder: + description: Subfolder to test in + type: string + default: "" + # you can test new features in the test runner against recipes or other repos + # by opening a pull request in those repos and running this test job + # against a pull request number in the example repo + pull_request_id: + description: Pull request number to check out before installing and testing + type: integer + default: 0 + wait-on: + description: Whether to use wait-on to wait on a server to be booted + type: string + default: "" + server-start-command: + description: Server start command for repo + type: string + default: "CI=true yarn start" + steps: + - clone-repo-and-checkout-branch: + repo: <> + - when: + condition: <> + steps: + - run: + name: Check out PR <> + working_directory: /tmp/<> + command: | + git fetch origin pull/<>/head:pr-<> + git checkout pr-<> + git log -n 2 + - run: + command: yarn + working_directory: /tmp/<> + - run: + name: Install Cypress + working_directory: /tmp/<> + # force installing the freshly built binary + command: | + CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i --legacy-peer-deps ~/cypress/cypress.tgz && [[ -f yarn.lock ]] && yarn + - run: + name: Print Cypress version + working_directory: /tmp/<> + command: npx cypress version + - run: + name: Types check 🧩 (maybe) + working_directory: /tmp/<> + command: yarn types + - run: + # NOTE: we do not need to wait for the vite dev server to start + working_directory: /tmp/<> + command: <> + background: true + - when: + condition: <> + steps: + - when: + condition: <> + steps: + - run: + name: Run tests using browser "<>" + working_directory: /tmp/<>/<> + command: | + <> --browser <> --record false + - unless: + condition: <> + steps: + - run: + name: Run tests using command + working_directory: /tmp/<>/<> + command: <> + - unless: + condition: <> + steps: + - when: + condition: <> + steps: + - run: + name: Run tests using browser "<>" + working_directory: /tmp/<> + command: <> --browser <> --record false + - unless: + condition: <> + steps: + - run: + name: Run tests using command + working_directory: /tmp/<> + command: <> + - store-npm-logs + + test-binary-against-repo: + description: | + Takes the built binary and NPM package, clones given example repo + and runs the new version of Cypress against it. + parameters: + repo: + description: "Name of the github repo to clone like: cypress-example-kitchensink" + type: string + browser: + description: Name of the browser to use, like "electron", "chrome", "firefox" + type: enum + enum: ["", "electron", "chrome", "firefox"] + default: "" + command: + description: Test command to run to start Cypress tests + type: string + default: "npm run e2e" + build-project: + description: Should the project build script be executed + type: boolean + default: true + # if the repo to clone and test is a monorepo, you can + # run tests inside a specific subfolder + folder: + description: Subfolder to test in + type: string + default: "" + # you can test new features in the test runner against recipes or other repos + # by opening a pull request in those repos and running this test job + # against a pull request number in the example repo + pull_request_id: + description: Pull request number to check out before installing and testing + type: integer + default: 0 + wait-on: + description: Whether to use wait-on to wait on a server to be booted + type: string + default: "" + server-start-command: + description: Server start command for repo + type: string + default: "npm start --if-present" + steps: + - run: + name: Install yarn if not already installed + command: | + yarn --version || npm i -g yarn + - clone-repo-and-checkout-branch: + repo: <> + pull_request_id: <> + - run: + # Ensure we're installing the node-version for the cloned repo + command: | + if [[ -f .node-version ]]; then + branch="<< pipeline.git.branch >>" + + externalBranchPattern='^pull\/[0-9]+' + if [[ $branch =~ $externalBranchPattern ]]; then + # We are unable to curl from the external PR branch location + # so we fall back to develop + branch="develop" + fi + + curl -L https://raw.githubusercontent.com/cypress-io/cypress/$branch/scripts/ensure-node.sh --output ci-ensure-node.sh + else + # if no .node-version file exists, we no-op the node script and use the global yarn + echo '' > ci-ensure-node.sh + fi + working_directory: /tmp/<> + - run: + # Install deps + Cypress binary with yarn if yarn.lock present + command: | + source ./ci-ensure-node.sh + if [[ -f yarn.lock ]]; then + yarn --frozen-lockfile + CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip yarn add -D ~/cypress/cypress.tgz + else + npm install + CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm install --legacy-peer-deps ~/cypress/cypress.tgz + fi + working_directory: /tmp/<> + - run: + name: Scaffold new config file + working_directory: /tmp/<> + environment: + CYPRESS_INTERNAL_FORCE_SCAFFOLD: "1" + command: | + if [[ -f cypress.json ]]; then + rm -rf cypress.json + echo 'module.exports = { e2e: {} }' > cypress.config.js + fi + - run: + name: Rename support file + working_directory: /tmp/<> + command: | + if [[ -f cypress/support/index.js ]]; then + mv cypress/support/index.js cypress/support/e2e.js + fi + - run: + name: Print Cypress version + working_directory: /tmp/<> + command: | + source ./ci-ensure-node.sh + npx cypress version + - run: + name: Types check 🧩 (maybe) + working_directory: /tmp/<> + command: | + source ./ci-ensure-node.sh + [[ -f yarn.lock ]] && yarn types || npm run types --if-present + - when: + condition: <> + steps: + - run: + name: Build 🏗 (maybe) + working_directory: /tmp/<> + command: | + source ./ci-ensure-node.sh + [[ -f yarn.lock ]] && yarn build || npm run build --if-present + - run: + working_directory: /tmp/<> + command: | + source ./ci-ensure-node.sh + <> + background: true + - run: + condition: <> + name: "Waiting on server to boot: <>" + command: | + npx wait-on <> --timeout 120000 + - windows-install-chrome: + browser: <> + - when: + condition: <> + steps: + - when: + condition: <> + steps: + - run: + name: Run tests using browser "<>" + working_directory: /tmp/<>/<> + command: | + <> -- --browser <> + - unless: + condition: <> + steps: + - run: + name: Run tests using command + working_directory: /tmp/<>/<> + command: <> + - unless: + condition: <> + steps: + - when: + condition: <> + steps: + - run: + name: Run tests using browser "<>" + working_directory: /tmp/<> + command: | + source ./ci-ensure-node.sh + <> -- --browser <> + - unless: + condition: <> + steps: + - run: + name: Run tests using command + working_directory: /tmp/<> + command: | + source ./ci-ensure-node.sh + <> + - store-npm-logs + + check-if-binary-exists: + steps: + - run: + name: Check if binary exists, exit if it does + command: | + source ./scripts/ensure-node.sh + yarn gulp e2eTestScaffold + yarn check-binary-on-cdn --version $(node ./scripts/get-next-version.js) --type binary --file cypress.zip + + build-and-package-binary: + steps: + - run: + name: Check environment variables before code sign (if on Mac/Windows) + # NOTE + # our code sign works via electron-builder + # by default, electron-builder will NOT sign app built in a pull request + # even our internal one (!) + # Usually this is not a problem, since we only build and test binary + # built on the "develop" branch + # but if you need to really build and sign a binary in a PR + # set variable CSC_FOR_PULL_REQUEST=true + command: | + set -e + NEEDS_CODE_SIGNING_WINDOWS=`node -p 'process.platform === "win32"'` + NEEDS_CODE_SIGNING_MAC=`node -p 'process.platform === "darwin"'` + + if [[ "$NEEDS_CODE_SIGNING_MAC" == "true" ]]; then + echo "Checking for required environment variables..." + if [ -z "$CSC_LINK" ]; then + echo "Need to provide environment variable CSC_LINK" + echo "with base64 encoded certificate .p12 file" + exit 1 + fi + if [ -z "$CSC_KEY_PASSWORD" ]; then + echo "Need to provide environment variable CSC_KEY_PASSWORD" + echo "with password for unlocking certificate .p12 file" + exit 1 + fi + echo "Succeeded." + elif [[ "$NEEDS_CODE_SIGNING_WINDOWS" == "true" ]]; then + echo "Checking for required environment variables..." + if [ -z "$WINDOWS_SIGN_USER_NAME" ]; then + echo "Need to provide environment variable WINDOWS_SIGN_USER_NAME" + echo "with password for fetching and signing certificate" + exit 1 + fi + if [ -z "$WINDOWS_SIGN_USER_PASSWORD" ]; then + echo "Need to provide environment variable WINDOWS_SIGN_USER_PASSWORD" + echo "for fetching and signing certificate" + exit 1 + fi + if [ -z "$WINDOWS_SIGN_CREDENTIAL_ID" ]; then + echo "Need to provide environment variable WINDOWS_SIGN_CREDENTIAL_ID" + echo "for identifying certificate" + exit 1 + fi + if [ -z "$WINDOWS_SIGN_USER_TOTP" ]; then + echo "Need to provide environment variable WINDOWS_SIGN_USER_TOTP" + echo "for signing certificate" + exit 1 + fi + echo "Succeeded." + else + echo "Not code signing for this platform" + fi + - run: + name: Build the Cypress binary + no_output_timeout: "45m" + command: | + source ./scripts/ensure-node.sh + node --version + if [[ `node ./scripts/get-platform-key.js` == 'linux-arm64' ]]; then + # these are missing on Circle and there is no way to pre-install them on Arm + sudo apt-get update + sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb + DISABLE_SNAPSHOT_REQUIRE=1 yarn binary-build --version $(node ./scripts/get-next-version.js) + else + yarn binary-build --version $(node ./scripts/get-next-version.js) + fi + - run: + name: Package the Cypress binary + environment: + DEBUG: electron-builder,electron-osx-sign*,electron-notarize* + # notarization on Mac can take a while + no_output_timeout: "45m" + command: | + source ./scripts/ensure-node.sh + node --version + if [[ `node ./scripts/get-platform-key.js` == 'linux-arm64' ]]; then + # these are missing on Circle and there is no way to pre-install them on Arm + sudo apt-get update + sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb + DISABLE_SNAPSHOT_REQUIRE=1 yarn binary-package --version $(node ./scripts/get-next-version.js) + else + yarn binary-package --version $(node ./scripts/get-next-version.js) + fi + - run: + name: Smoke Test the Cypress binary + command: | + source ./scripts/ensure-node.sh + node --version + yarn binary-smoke-test --version $(node ./scripts/get-next-version.js) + - run: + name: Zip the binary + command: | + if [[ $PLATFORM == 'linux' ]]; then + # on Arm, CI runs as non-root, on x64 CI runs as root but there is no sudo binary + if [[ `whoami` == 'root' ]]; then + apt-get update && apt-get install -y zip + else + sudo apt-get update && sudo apt-get install -y zip + fi + fi + source ./scripts/ensure-node.sh + yarn binary-zip + - store-npm-logs + - persist_to_workspace: + root: ~/ + paths: + - cypress/cypress.zip + + trigger-publish-binary-pipeline: + steps: + - run: + name: "Trigger publish-binary pipeline" + command: | + source ./scripts/ensure-node.sh + echo $SHOULD_PERSIST_ARTIFACTS + node ./scripts/binary/trigger-publish-binary-pipeline.js + - persist_to_workspace: + root: ~/ + paths: + - triggered_pipeline.json + + build-cypress-npm-package: + parameters: + executor: + type: executor + default: cy-doc + steps: + - run: + name: Bump NPM version + command: | + source ./scripts/ensure-node.sh + yarn get-next-version --npm + - run: + name: Build NPM package + command: | + source ./scripts/ensure-node.sh + yarn lerna run build-cli + - run: + command: ls -la types + working_directory: cli/build + - run: + command: ls -la vue vue2 mount-utils react + working_directory: cli/build + - unless: + condition: + equal: [ *windows-executor, << parameters.executor >> ] + steps: + - run: + name: list NPM package contents + command: | + source ./scripts/ensure-node.sh + yarn workspace cypress size + - run: + name: pack NPM package + working_directory: cli/build + command: yarn pack --filename ../../cypress.tgz + - run: + name: list created NPM package + command: ls -l + - store-npm-logs + - persist_to_workspace: + root: ~/ + paths: + - cypress/cypress.tgz + + upload-build-artifacts: + steps: + - run: ls -l + - run: + name: Upload unique binary to S3 + command: | + node scripts/binary.js upload-build-artifact \ + --type binary \ + --file cypress.zip \ + --version $(node -p "require('./package.json').version") + - run: + name: Upload NPM package to S3 + command: | + node scripts/binary.js upload-build-artifact \ + --type npm-package \ + --file cypress.tgz \ + --version $(node -p "require('./package.json').version") + - store-npm-logs + - run: ls -l + - run: cat binary-url.json + - run: cat npm-package-url.json + - persist_to_workspace: + root: ~/ + paths: + - cypress/binary-url.json + - cypress/npm-package-url.json + + update_known_hosts: + description: Ensures that we have the latest Git public keys to prevent git+ssh from failing. + steps: + - run: + name: Update known_hosts with github.com keys + command: | + mkdir -p ~/.ssh + ssh-keyscan github.com >> ~/.ssh/known_hosts + +jobs: + ## Checks if we already have a valid cache for the node_modules_install and if it has, + ## skips ahead to the build step, otherwise installs and caches the node_modules + node_modules_install: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + build-better-sqlite3: + type: boolean + default: false + resource_class: << parameters.resource_class >> + steps: + - update_known_hosts + - checkout + - install-required-node + - verify-build-setup: + executor: << parameters.executor >> + - persist_to_workspace: + root: ~/ + paths: + - cypress + - .ssh + - .nvm # mac / linux + - ProgramData/nvm # windows + - caching-dependency-installer: + only-cache-for-root-user: <> + build-better-sqlite3: <> + - store-npm-logs + + ## restores node_modules from previous step & builds if first step skipped + build: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: large + resource_class: << parameters.resource_class >> + steps: + - restore_cached_workspace + - run: + name: Top level packages + command: yarn list --depth=0 || true + - run: + name: Check env canaries on Linux + command: | + # only Docker has the required env data for this + if [[ $CI_DOCKER == 'true' ]]; then + node ./scripts/circle-env.js --check-canaries + fi + - build-and-persist + - store-npm-logs + + lint: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Linting 🧹 + command: | + yarn clean + git clean -df + yarn lint + - run: + name: cypress info (dev) + command: node cli/bin/cypress info --dev + - store-npm-logs + + check-ts: + <<: *defaults + steps: + - restore_cached_workspace + - install-required-node + - run: + name: Check TS Types + command: NODE_OPTIONS=--max_old_space_size=4096 yarn check-ts --concurrency=1 + + # a special job that closes the Percy build started by the required jobs + percy-finalize: + <<: *defaults + resource_class: small + parameters: + <<: *defaultsParameters + required_env_var: + type: env_var_name + steps: + - restore_cached_workspace + - run: + # if this is an external pull request, the environment variables + # are NOT set for security reasons, thus no need to to finalize Percy, + # since there will be no visual tests + name: Check if <> is set + command: | + if [[ -v <> ]]; then + echo "Internal PR, good to go" + else + echo "This is an external PR, cannot access other services" + circleci-agent step halt + fi + - run: + # Sometimes, even though all the circle jobs have finished, Percy times out during `build:finalize` + # If all other jobs finish but `build:finalize` fails, we retry it once + name: Finalize percy build - allows single retry + command: | + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + yarn percy build:finalize || yarn percy build:finalize + + ready-to-release: + <<: *defaults + resource_class: small + parameters: + <<: *defaultsParameters + steps: + - run: + name: Ready to release + command: echo 'Ready to release' + + cli-visual-tests: + <<: *defaults + resource_class: small + steps: + - restore_cached_workspace + - run: mkdir -p cli/visual-snapshots + - run: + command: node cli/bin/cypress info --dev | yarn --silent term-to-html | node scripts/sanitize --type cli-info > cli/visual-snapshots/cypress-info.html + environment: + FORCE_COLOR: 2 + - run: + command: node cli/bin/cypress help | yarn --silent term-to-html > cli/visual-snapshots/cypress-help.html + environment: + FORCE_COLOR: 2 + - store_artifacts: + path: cli/visual-snapshots + - run: + name: Upload CLI snapshots for diffing + command: | + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy snapshot ./cli/visual-snapshots + + v8-integration-tests: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + resource_class: << parameters.resource_class >> + parallelism: 1 + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + # TODO: Remove this once we switch off self-hosted M1 runners + - when: + condition: + equal: [ *darwin-arm64-executor, << parameters.executor >> ] + steps: + - run: rm -f /tmp/cypress/junit/* + - unless: + condition: + or: + - equal: [ *linux-arm64-executor, << parameters.executor >> ] # TODO: Figure out how to support linux-arm64 when we get to linux arm64 build: https://github.com/cypress-io/cypress/issues/23557 + steps: + - run: + name: Run v8 integration tests + command: | + source ./scripts/ensure-node.sh + yarn test-integration --scope "'@tooling/{packherd,v8-snapshot,electron-mksnapshot}'" + - verify-mocha-results: + expectedResultCount: 3 + - when: + condition: + or: + - equal: [ *linux-arm64-executor, << parameters.executor >> ] + steps: + - run: + name: Run v8 integration tests + command: | + source ./scripts/ensure-node.sh + yarn test-integration --scope "'@tooling/packherd'" + - verify-mocha-results: + expectedResultCount: 1 + - store_test_results: + path: /tmp/cypress + - store-npm-logs + + driver-integration-memory-tests: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + resource_class: << parameters.resource_class >> + parallelism: 1 + steps: + - restore_cached_workspace + - run: + name: Driver memory tests in Electron + environment: + CYPRESS_CONFIG_ENV: production + command: | + echo Current working directory is $PWD + node --version + if [[ `node ../../scripts/get-platform-key.js` == 'linux-arm64' ]]; then + # these are missing on Circle and there is no way to pre-install them on Arm + sudo apt-get update + sudo apt-get install -y libgbm-dev + fi + + CYPRESS_INTERNAL_MEMORY_SAVE_STATS=true \ + DEBUG=cypress*memory \ + yarn cypress:run --browser electron --spec "cypress/e2e/memory/*.cy.*" + working_directory: packages/driver + - store_test_results: + path: /tmp/cypress + - store-npm-logs + - store_artifacts: + path: packages/driver/cypress/logs/memory + + unit-tests: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + resource_class: << parameters.resource_class >> + parallelism: 1 + steps: + - restore_cached_workspace + - when: + condition: + # several snapshots fails for windows due to paths. + # until these are fixed, run the tests that are working. + equal: [ *windows-executor, << parameters.executor >> ] + steps: + - run: yarn test-scripts scripts/**/*spec.js + - unless: + condition: + equal: [ *windows-executor, << parameters.executor >> ] + steps: + - run: yarn test-scripts + # run unit tests from each individual package + - run: yarn test + # run type checking for each individual package + - run: yarn lerna run types + - verify-mocha-results: + expectedResultCount: 19 + - store_test_results: + path: /tmp/cypress + # CLI tests generate HTML files with sample CLI command output + - store_artifacts: + path: cli/test/html + - store_artifacts: + path: packages/errors/__snapshot-images__ + - store-npm-logs + + verify-release-readiness: + <<: *defaults + resource_class: small + parallelism: 1 + environment: + GITHUB_TOKEN: $GH_TOKEN + steps: + - restore_cached_workspace + - update_known_hosts + - run: yarn test-npm-package-release-script + - run: node ./scripts/semantic-commits/validate-binary-changelog.js + - store_artifacts: + path: /tmp/releaseData + + lint-types: + <<: *defaults + parallelism: 1 + steps: + - restore_cached_workspace + - run: + command: ls -la types + working_directory: cli + - run: + command: ls -la chai + working_directory: cli/types + - run: + name: "Lint types 🧹" + command: yarn workspace cypress dtslint + - store-npm-logs + + server-unit-tests: + <<: *defaults + parallelism: 1 + steps: + - restore_cached_workspace + - run: yarn test-unit --scope @packages/server + - verify-mocha-results: + expectedResultCount: 1 + - store_test_results: + path: /tmp/cypress + - store-npm-logs + + server-unit-tests-cloud-environment: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + resource_class: << parameters.resource_class >> + parallelism: 1 + steps: + - restore_cached_workspace + # TODO: Remove this once we switch off self-hosted M1 runners + - when: + condition: + equal: [ *darwin-arm64-executor, << parameters.executor >> ] + steps: + - run: rm -f /tmp/cypress/junit/* + - run: yarn workspace @packages/server test-unit cloud/environment_spec.ts + - verify-mocha-results: + expectedResultCount: 1 + - store_test_results: + path: /tmp/cypress + - store-npm-logs + + server-integration-tests: + <<: *defaults + parallelism: 1 + steps: + - restore_cached_workspace + - run: yarn test-integration --scope @packages/server + - verify-mocha-results: + expectedResultCount: 1 + - store_test_results: + path: /tmp/cypress + - store-npm-logs + + server-performance-tests: + <<: *defaults + steps: + - restore_cached_workspace + - run: + command: yarn workspace @packages/server test-performance + - verify-mocha-results: + expectedResultCount: 1 + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + system-tests-node-modules-install: + <<: *defaults + steps: + - 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: + - maybe_skip_binary_jobs + - run-binary-system-tests + + system-tests-chrome: + <<: *defaults + resource_class: medium+ + parallelism: 8 + steps: + - run-system-tests: + browser: chrome + + system-tests-electron: + <<: *defaults + resource_class: medium+ + parallelism: 8 + steps: + - run-system-tests: + browser: electron + + system-tests-firefox: + <<: *defaults + resource_class: medium+ + parallelism: 8 + steps: + - run-system-tests: + browser: firefox + + system-tests-webkit: + <<: *defaults + resource_class: medium+ + parallelism: 8 + steps: + - run-system-tests: + browser: webkit + + system-tests-non-root: + <<: *defaults + steps: + - restore_cached_workspace + - run: + environment: + CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0' + command: yarn workspace @tooling/system-tests test:ci "test/non_root*spec*" --browser electron + - verify-mocha-results + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + run-frontend-shared-component-tests-chrome: + <<: *defaults + parameters: + <<: *defaultsParameters + percy: + type: boolean + default: false + parallelism: 3 + steps: + - run-new-ui-tests: + browser: chrome + percy: << parameters.percy >> + package: frontend-shared + type: ct + + run-launchpad-component-tests-chrome: + <<: *defaults + parameters: + <<: *defaultsParameters + percy: + type: boolean + default: false + parallelism: 7 + steps: + - run-new-ui-tests: + browser: chrome + percy: << parameters.percy >> + package: launchpad + type: ct + # debug: cypress:*,engine:socket + + run-launchpad-integration-tests-chrome: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: large + percy: + type: boolean + default: false + resource_class: << parameters.resource_class >> + parallelism: 3 + steps: + - run-new-ui-tests: + browser: chrome + percy: << parameters.percy >> + package: launchpad + type: e2e + + run-app-component-tests-chrome: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium+ + percy: + type: boolean + default: false + parallelism: 7 + steps: + - run-new-ui-tests: + browser: chrome + percy: << parameters.percy >> + package: app + type: ct + + run-app-integration-tests-chrome: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: large + percy: + type: boolean + default: false + resource_class: << parameters.resource_class >> + parallelism: 8 + steps: + - run-new-ui-tests: + browser: chrome + percy: << parameters.percy >> + package: app + type: e2e + + driver-integration-tests-chrome: + <<: *defaults + parallelism: 5 + resource_class: medium+ + steps: + - run-driver-integration-tests: + browser: chrome + install-chrome-channel: stable + + driver-integration-tests-chrome-beta: + <<: *defaults + resource_class: medium+ + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: chrome:beta + install-chrome-channel: beta + + driver-integration-tests-firefox: + <<: *defaults + resource_class: medium+ + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: firefox + + driver-integration-tests-electron: + <<: *defaults + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: electron + + driver-integration-tests-webkit: + <<: *defaults + resource_class: large + parallelism: 5 + steps: + - run-driver-integration-tests: + browser: webkit + + run-reporter-component-tests-chrome: + <<: *defaults + parameters: + <<: *defaultsParameters + percy: + type: boolean + default: false + parallelism: 2 + steps: + - run-new-ui-tests: + browser: chrome + percy: << parameters.percy >> + package: reporter + type: ct + + reporter-integration-tests: + <<: *defaults + resource_class: medium+ + parallelism: 3 + steps: + - restore_cached_workspace + - run: + command: yarn build-for-tests + working_directory: packages/reporter + - run: + command: | + CYPRESS_CONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy exec --parallel -- -- \ + yarn cypress:run --record --parallel --group reporter --runner-ui + working_directory: packages/reporter + - verify-mocha-results + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + run-webpack-dev-server-integration-tests: + <<: *defaults + resource_class: medium+ + parallelism: 2 + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + command: | + CYPRESS_CONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy exec --parallel -- -- \ + yarn cypress:run --record --parallel --group webpack-dev-server + working_directory: npm/webpack-dev-server + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + run-vite-dev-server-integration-tests: + <<: *defaults + # parallelism: 3 TODO: Add parallelism once we have more specs + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + command: | + CYPRESS_CONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy exec --parallel -- -- \ + yarn cypress:run --record --parallel --group vite-dev-server + working_directory: npm/vite-dev-server + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + npm-webpack-preprocessor: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/webpack-preprocessor + - run: + name: Run tests + command: yarn workspace @cypress/webpack-preprocessor test + - store-npm-logs + + npm-webpack-dev-server: + <<: *defaults + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + name: Run tests + command: yarn workspace @cypress/webpack-dev-server test + - run: + name: Run tests + command: yarn workspace @cypress/webpack-dev-server test + + npm-vite-dev-server: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Run tests + command: yarn test + working_directory: npm/vite-dev-server + - store_test_results: + path: npm/vite-dev-server/test_results + - store-npm-logs + + npm-webpack-batteries-included-preprocessor: + <<: *defaults + resource_class: small + steps: + - restore_cached_workspace + - run: + name: Run tests + command: yarn workspace @cypress/webpack-batteries-included-preprocessor test + + npm-vue: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/vue + - store_test_results: + path: npm/vue/test_results + - store_artifacts: + path: npm/vue/test_results + - store-npm-logs + + npm-angular: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/angular + - store-npm-logs + + npm-angular-signals: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/angular-signals + - store-npm-logs + + npm-puppeteer-unit-tests: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/puppeteer + - run: + name: Run tests + command: yarn test + working_directory: npm/puppeteer + - store_test_results: + path: npm/puppeteer/test_results + - store_artifacts: + path: npm/puppeteer/test_results + - store-npm-logs + + npm-puppeteer-cypress-tests: + <<: *defaults + resource_class: small + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + command: yarn cypress:run + working_directory: npm/puppeteer + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + + npm-react: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/react + - run: + name: Run tests + command: yarn test + working_directory: npm/react + - store_test_results: + path: npm/react/test_results + - store_artifacts: + path: npm/react/test_results + - store-npm-logs + + npm-vite-plugin-cypress-esm: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/vite-plugin-cypress-esm + - run: + name: Run tests + command: yarn test + working_directory: npm/vite-plugin-cypress-esm + - store_test_results: + path: npm/vite-plugin-cypress-esm/test_results + - store_artifacts: + path: npm/vite-plugin-cypress-esm/test_results + - store-npm-logs + + npm-mount-utils: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/mount-utils + - store-npm-logs + + npm-grep: + <<: *defaults + resource_class: small + steps: + - restore_cached_workspace + - run: + name: Run tests + command: yarn workspace @cypress/grep cy:run + - store_test_results: + path: npm/grep/test_results + - store_artifacts: + path: npm/grep/test_results + - store-npm-logs + + npm-eslint-plugin-dev: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Run tests + command: yarn workspace @cypress/eslint-plugin-dev test + + npm-cypress-schematic: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + Install + command: | + yarn lerna run build --scope @cypress/schematic + - run: + name: Run unit tests + command: | + yarn test + working_directory: npm/cypress-schematic + - store-npm-logs + + npm-release: + <<: *defaults + resource_class: medium+ + steps: + - restore_cached_workspace + - run: + name: Release packages after all jobs pass + command: yarn npm-release + + create-build-artifacts: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: xlarge + resource_class: << parameters.resource_class >> + steps: + - restore_cached_workspace + - check-if-binary-exists + - build-and-package-binary + - build-cypress-npm-package: + executor: << parameters.executor >> + - setup_should_persist_artifacts + - verify_should_persist_artifacts + - upload-build-artifacts + - post-install-comment + + create-and-trigger-packaging-artifacts: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: small + resource_class: << parameters.resource_class >> + steps: + - maybe_skip_binary_jobs + - restore_cached_workspace + - check-if-binary-exists + - setup_should_persist_artifacts + - trigger-publish-binary-pipeline + + get-published-artifacts: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium + resource_class: << parameters.resource_class >> + steps: + - maybe_skip_binary_jobs + - restore_cached_workspace + - run: + name: Check pipeline info + command: cat ~/triggered_pipeline.json + - setup_should_persist_artifacts + - run: + name: Download binary artifacts + command: | + source ./scripts/ensure-node.sh + node ./scripts/binary/get-published-artifacts.js --pipelineInfo ~/triggered_pipeline.json --platformKey $(node ./scripts/get-platform-key.js) + - persist_to_workspace: + root: ~/ + paths: + - cypress/cypress.zip + - cypress/cypress.tgz + - verify_should_persist_artifacts + - persist_to_workspace: + root: ~/ + paths: + - cypress/binary-url.json + - cypress/npm-package-url.json + - post-install-comment: + package_url_path: ~/cypress/npm-package-url.json + binary_url_path: ~/cypress/binary-url.json + + test-kitchensink: + <<: *defaults + parameters: + <<: *defaultsParameters + resource_class: + type: string + default: medium+ + resource_class: << parameters.resource_class >> + steps: + - restore_cached_workspace + - clone-repo-and-checkout-branch: + repo: cypress-example-kitchensink + - install-required-node + - run: + name: Install prod dependencies + command: yarn --production --ignore-engines + working_directory: /tmp/cypress-example-kitchensink + - run: + name: Example server + command: yarn start + working_directory: /tmp/cypress-example-kitchensink + background: true + - run: + name: Run Kitchensink example project + command: | + yarn cypress:run --project /tmp/cypress-example-kitchensink + - store-npm-logs + + test-kitchensink-against-staging: + <<: *defaults + steps: + - restore_cached_workspace + - clone-repo-and-checkout-branch: + repo: cypress-example-kitchensink + - install-required-node + - run: + name: Install prod dependencies + command: yarn --production + working_directory: /tmp/cypress-example-kitchensink + - run: + name: Example server + command: yarn start + working_directory: /tmp/cypress-example-kitchensink + background: true + - run: + name: Run Kitchensink example project + command: | + CYPRESS_PROJECT_ID=$TEST_KITCHENSINK_PROJECT_ID \ + CYPRESS_RECORD_KEY=$TEST_KITCHENSINK_RECORD_KEY \ + CYPRESS_INTERNAL_ENV=staging \ + yarn cypress:run --project /tmp/cypress-example-kitchensink --record + - store-npm-logs + + test-against-staging: + <<: *defaults + steps: + - restore_cached_workspace + - clone-repo-and-checkout-branch: + repo: cypress-test-tiny + - run: + name: Run test project + command: | + CYPRESS_PROJECT_ID=$TEST_TINY_PROJECT_ID \ + CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \ + CYPRESS_INTERNAL_ENV=staging \ + yarn cypress:run --project /tmp/cypress-test-tiny --record + - store-npm-logs + + test-npm-module-and-verify-binary: + <<: *defaults + resource_class: small + steps: + - restore_cached_workspace + # make sure we have cypress.zip received + - run: ls -l + - run: ls -l cypress.zip cypress.tgz + - run: mkdir test-binary + - run: + name: Create new NPM package + working_directory: test-binary + command: npm init -y + - run: + # install NPM from built NPM package folder + name: Install Cypress + working_directory: test-binary + # force installing the freshly built binary + command: CYPRESS_INSTALL_BINARY=/root/cypress/cypress.zip npm i /root/cypress/cypress.tgz + - run: + name: Cypress version + working_directory: test-binary + command: $(yarn bin cypress) version + - run: + name: Verify Cypress binary + working_directory: test-binary + command: $(yarn bin cypress) verify + - run: + name: Cypress help + working_directory: test-binary + command: $(yarn bin cypress) help + - run: + name: Cypress info + working_directory: test-binary + command: $(yarn bin cypress) info + - store-npm-logs + + test-npm-module-on-minimum-node-version: + <<: *defaults + resource_class: small + docker: + - image: cypress/base-internal:18.17.1 + steps: + - maybe_skip_binary_jobs + - restore_workspace_binaries + - run: mkdir test-binary + - run: + name: Create new NPM package + working_directory: test-binary + command: npm init -y + - run: + name: Install Cypress + working_directory: test-binary + command: CYPRESS_INSTALL_BINARY=/root/cypress/cypress.zip npm install /root/cypress/cypress.tgz + - run: + name: Verify Cypress binary + working_directory: test-binary + command: npx cypress verify + - run: + name: Print Cypress version + working_directory: test-binary + command: npx cypress version + - run: + name: Cypress info + working_directory: test-binary + command: npx cypress info + + test-types-cypress-and-jest: + parameters: + executor: + description: Executor name to use + type: executor + default: cy-doc + wd: + description: Working directory, should be OUTSIDE cypress monorepo folder + type: string + default: /root/test-cypress-and-jest + <<: *defaults + resource_class: small + steps: + - maybe_skip_binary_jobs + - restore_workspace_binaries + - run: mkdir <> + - run: + name: Create new NPM package ⚗️ + working_directory: <> + command: npm init -y + - run: + name: Install dependencies 📦 + working_directory: <> + environment: + CYPRESS_INSTALL_BINARY: /root/cypress/cypress.zip + # let's install Cypress, Jest and any other package that might conflict + # https://github.com/cypress-io/cypress/issues/6690 + + # Todo: Add `jest` back into the list once https://github.com/yargs/yargs-parser/issues/452 + # is resolved. + command: | + npm install /root/cypress/cypress.tgz \ + typescript @types/jest enzyme @types/enzyme + - run: + name: Test types clash ⚔️ + working_directory: <> + command: | + echo "console.log('hello world')" > hello.ts + npx tsc hello.ts --noEmit + + test-full-typescript-project: + parameters: + executor: + description: Executor name to use + type: executor + default: cy-doc + <<: *defaults + resource_class: small + steps: + - maybe_skip_binary_jobs + - restore_workspace_binaries + - clone-repo-and-checkout-branch: + repo: cypress-test-tiny + - run: + name: Checkout Typescript Example + working_directory: /tmp/cypress-test-tiny + command: | + git checkout full-typescript + - run: + name: Install dependencies 📦 + working_directory: /tmp/cypress-test-tiny + environment: + CYPRESS_INSTALL_BINARY: /root/cypress/cypress.zip + command: | + npm install /root/cypress/cypress.tgz typescript + - run: + name: Run project tests 🗳 + working_directory: /tmp/cypress-test-tiny + command: npm run cypress:run + + # install NPM + binary zip and run against staging API + test-binary-against-staging: + <<: *defaults + steps: + - restore_workspace_binaries + - clone-repo-and-checkout-branch: + repo: cypress-test-tiny + - run: + name: Install Cypress + working_directory: /tmp/cypress-test-tiny + # force installing the freshly built binary + command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i --legacy-peer-deps ~/cypress/cypress.tgz + - run: + name: Run test project + working_directory: /tmp/cypress-test-tiny + command: | + CYPRESS_PROJECT_ID=$TEST_TINY_PROJECT_ID \ + CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \ + CYPRESS_INTERNAL_ENV=staging \ + $(yarn bin cypress) run --record + - store-npm-logs + + test-binary-against-recipes-firefox: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-recipes + command: npm run test:ci:firefox + browser: firefox + + test-binary-against-recipes-chrome: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-recipes + command: npm run test:ci:chrome + browser: chrome + + test-binary-against-recipes: + <<: *defaults + parallelism: 4 + steps: + - test-binary-against-repo: + repo: cypress-example-recipes + # Split the specs up across 4 different machines to run in parallel + command: npm run test:ci -- --chunk $CIRCLE_NODE_INDEX --total-chunks $CIRCLE_NODE_TOTAL + browser: electron + + # 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. + # Imagine you are working on a feature and want to show / test a recipe + # You would need to run the built test runner before release + # against a PR that cannot be merged until the new version + # of the test runner is released. + # Use: + # specify pull request number + # and the recipe folder + + # test-binary-against-recipe-pull-request: + # <<: *defaults + # steps: + # # test a specific pull request by number from cypress-example-recipes + # - test-binary-against-repo: + # repo: cypress-example-recipes + # command: npm run test:ci + # pull_request_id: 515 + # folder: examples/fundamentals__typescript + + test-binary-against-kitchensink: + <<: *defaults + steps: + - maybe_skip_binary_jobs + - test-binary-against-repo: + repo: cypress-example-kitchensink + browser: electron + + test-binary-against-kitchensink-firefox: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-kitchensink + browser: firefox + + test-binary-against-kitchensink-chrome: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-kitchensink + browser: chrome + + test-binary-against-todomvc-firefox: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-todomvc + browser: firefox + + test-binary-against-conduit-chrome: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-conduit-app + browser: chrome + command: "npm run cypress:run" + wait-on: http://localhost:3000 + + test-binary-against-api-testing-firefox: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-api-testing + browser: firefox + command: "npm run cy:run" + + test-binary-against-piechopper-firefox: + <<: *defaults + steps: + - test-binary-against-repo: + repo: cypress-example-piechopper + browser: firefox + command: "npm run cypress:run" + + test-binary-against-cypress-realworld-app: + <<: *defaults + resource_class: medium+ + steps: + - test-binary-against-rwa: + repo: cypress-realworld-app + browser: chrome + wait-on: http://localhost:3000 + + test-binary-as-specific-user: + <<: *defaults + steps: + - maybe_skip_binary_jobs + - restore_workspace_binaries + - clone-repo-and-checkout-branch: + repo: cypress-test-tiny + # the user should be "node" + - run: whoami + - run: pwd + # prints the current user's effective user id + # for root it is 0 + # for other users it is a positive integer + - run: node -e 'console.log(process.geteuid())' + # make sure the binary and NPM package files are present + - run: ls -l + - run: ls -l cypress.zip cypress.tgz + - run: + # install NPM from built NPM package folder + name: Install Cypress + working_directory: /tmp/cypress-test-tiny + # force installing the freshly built binary + command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz + - run: + name: Cypress help + working_directory: /tmp/cypress-test-tiny + command: $(yarn bin cypress) help + - run: + name: Cypress info + working_directory: /tmp/cypress-test-tiny + command: $(yarn bin cypress) info + - run: + name: Verify Cypress binary + working_directory: /tmp/cypress-test-tiny + command: DEBUG=cypress:cli $(yarn bin cypress) verify + - run: + name: Run Cypress binary + working_directory: /tmp/cypress-test-tiny + command: DEBUG=cypress:cli $(yarn bin cypress) run + - store-npm-logs + +linux-x64-workflow: &linux-x64-workflow + jobs: + - node_modules_install: + # NOTE: Centos reached EOL on July 1st, 2024. The centos7-builder.Dockerfile no longer works in the manner in which we are using to rebuild better-sqlite3, so for now to unblock + # develop we will not be rebuilding better-sqlite3. However, a solution to this is likely needed as it can be considered a breaking change due to centos 7 extended support. + build-better-sqlite3: false + - build: + context: test-runner:env-canary + requires: + - node_modules_install + - check-ts: + requires: + - build + - lint: + name: linux-lint + requires: + - build + - percy-finalize: + context: test-runner:percy + required_env_var: PERCY_TOKEN + requires: + - cli-visual-tests + - reporter-integration-tests + - run-app-component-tests-chrome + - run-app-integration-tests-chrome + - run-frontend-shared-component-tests-chrome + - run-launchpad-component-tests-chrome + - run-launchpad-integration-tests-chrome + - run-reporter-component-tests-chrome + - run-webpack-dev-server-integration-tests + - run-vite-dev-server-integration-tests + - lint-types: + requires: + - build + # unit, integration and e2e tests + - cli-visual-tests: + context: test-runner:percy + requires: + - build + - unit-tests: + requires: + - build + - verify-release-readiness: + context: test-runner:npm-release + requires: + - build + - server-unit-tests: + requires: + - build + - server-integration-tests: + requires: + - build + - server-performance-tests: + requires: + - build + - system-tests-node-modules-install: + context: test-runner:performance-tracking + requires: + - build + - system-tests-chrome: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-electron: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-firefox: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-webkit: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-non-root: + context: test-runner:performance-tracking + executor: non-root-docker-user + requires: + - system-tests-node-modules-install + - driver-integration-tests-chrome: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-chrome-beta: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-firefox: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-electron: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-tests-webkit: + context: test-runner:cypress-record-key + requires: + - build + - driver-integration-memory-tests: + requires: + - build + - run-frontend-shared-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - build + - run-launchpad-integration-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - build + - run-launchpad-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - build + - run-app-integration-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - build + - run-webpack-dev-server-integration-tests: + context: [test-runner:cypress-record-key, test-runner:percy] + requires: + - system-tests-node-modules-install + - run-vite-dev-server-integration-tests: + context: [test-runner:cypress-record-key, test-runner:percy] + requires: + - system-tests-node-modules-install + - run-app-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - build + - run-reporter-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:percy] + percy: true + requires: + - build + - reporter-integration-tests: + context: [test-runner:cypress-record-key, test-runner:percy] + requires: + - build + - npm-webpack-dev-server: + requires: + - system-tests-node-modules-install + - npm-vite-dev-server: + requires: + - build + - npm-vite-plugin-cypress-esm: + requires: + - build + - npm-webpack-preprocessor: + requires: + - build + - npm-webpack-batteries-included-preprocessor: + requires: + - build + - npm-vue: + requires: + - build + - npm-puppeteer-unit-tests: + requires: + - build + - npm-puppeteer-cypress-tests: + requires: + - build + - npm-react: + requires: + - build + - npm-angular: + requires: + - build + - npm-angular-signals: + requires: + - build + - npm-mount-utils: + requires: + - build + - npm-eslint-plugin-dev: + requires: + - build + - npm-cypress-schematic: + requires: + - build + - v8-integration-tests: + requires: + - system-tests-node-modules-install + + - ready-to-release: + # <<: *mainBuildFilters + requires: + - check-ts + - npm-angular + - npm-angular-signals + - npm-eslint-plugin-dev + - npm-puppeteer-unit-tests + - npm-puppeteer-cypress-tests + - npm-react + - npm-mount-utils + - npm-vue + - npm-webpack-batteries-included-preprocessor + - npm-webpack-preprocessor + - npm-vite-dev-server + - npm-vite-plugin-cypress-esm + - npm-webpack-dev-server + - npm-cypress-schematic + - lint-types + - linux-lint + - percy-finalize + - driver-integration-tests-firefox + - driver-integration-tests-chrome + - driver-integration-tests-chrome-beta + - driver-integration-tests-electron + - driver-integration-tests-webkit + - driver-integration-memory-tests + - system-tests-non-root + - system-tests-firefox + - system-tests-electron + - system-tests-chrome + - system-tests-webkit + - server-performance-tests + - server-integration-tests + - server-unit-tests + - "test binary as a non-root user" + - "test binary as a root user" + - test-types-cypress-and-jest + - test-full-typescript-project + - test-binary-against-kitchensink + - test-npm-module-on-minimum-node-version + - binary-system-tests + - test-kitchensink + - unit-tests + - verify-release-readiness + - v8-integration-tests + + - npm-release: + <<: *mainBuildFilters + context: test-runner:npm-release + requires: + - ready-to-release + + - create-and-trigger-packaging-artifacts: + context: + - test-runner:upload + - test-runner:build-binary + - publish-binary + requires: + - node_modules_install + - wait-for-binary-publish: + type: approval + requires: + - create-and-trigger-packaging-artifacts + - get-published-artifacts: + context: + - publish-binary + - test-runner:commit-status-checks + requires: + - wait-for-binary-publish + # various testing scenarios, like building full binary + # and testing it on a real project + - test-against-staging: + context: test-runner:record-tests + <<: *mainBuildFilters + requires: + - build + - test-kitchensink: + requires: + - build + - test-kitchensink-against-staging: + executor: kitchensink-executor + context: test-runner:record-tests + <<: *mainBuildFilters + requires: + - build + - test-npm-module-on-minimum-node-version: + context: publish-binary + requires: + - get-published-artifacts + - test-types-cypress-and-jest: + context: publish-binary + requires: + - get-published-artifacts + - test-full-typescript-project: + context: publish-binary + requires: + - get-published-artifacts + - test-binary-against-kitchensink: + context: publish-binary + requires: + - get-published-artifacts + - test-npm-module-and-verify-binary: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-staging: + context: test-runner:record-tests + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-kitchensink-chrome: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-recipes-firefox: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-recipes-chrome: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-recipes: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-kitchensink-firefox: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-todomvc-firefox: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-cypress-realworld-app: + context: test-runner:cypress-record-key + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-as-specific-user: + name: "test binary as a non-root user" + executor: non-root-docker-user + context: publish-binary + requires: + - get-published-artifacts + - test-binary-as-specific-user: + name: "test binary as a root user" + context: publish-binary + requires: + - get-published-artifacts + - binary-system-tests: + context: publish-binary + requires: + - get-published-artifacts + - system-tests-node-modules-install + +linux-x64-contributor-workflow: &linux-x64-contributor-workflow + jobs: + - node_modules_install + - build: + requires: + - node_modules_install + # In subsequent jobs, we use some contexts that are restricted to members of the Cypress organization. + # This job will allow for a Cypress member to approve and run the rest of the restricted jobs in the pipeline after the contributor code has been reviewed. + - contributor-pr: + type: approval + requires: + - build + + - check-ts: + requires: + - build + - lint: + name: linux-lint + requires: + - build + - percy-finalize: + context: test-runner:percy + required_env_var: PERCY_TOKEN # skips job if not defined (external PR) + requires: + - cli-visual-tests + - reporter-integration-tests + - run-app-component-tests-chrome + - run-app-integration-tests-chrome + - run-frontend-shared-component-tests-chrome + - run-launchpad-component-tests-chrome + - run-launchpad-integration-tests-chrome + - run-reporter-component-tests-chrome + - run-webpack-dev-server-integration-tests + - run-vite-dev-server-integration-tests + - lint-types: + requires: + - build + # unit, integration and e2e tests + - cli-visual-tests: + context: test-runner:percy + requires: + - contributor-pr + - unit-tests: + requires: + - build + - verify-release-readiness: + context: test-runner:npm-release + requires: + - contributor-pr + - server-unit-tests: + requires: + - build + - server-integration-tests: + requires: + - build + - server-performance-tests: + requires: + - build + - system-tests-node-modules-install: + context: test-runner:performance-tracking + requires: + - contributor-pr + - system-tests-chrome: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-electron: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-firefox: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-webkit: + context: test-runner:performance-tracking + requires: + - system-tests-node-modules-install + - system-tests-non-root: + context: test-runner:performance-tracking + executor: non-root-docker-user + requires: + - system-tests-node-modules-install + - driver-integration-tests-chrome: + context: test-runner:cypress-record-key + requires: + - contributor-pr + - driver-integration-tests-chrome-beta: + context: test-runner:cypress-record-key + requires: + - contributor-pr + - driver-integration-tests-firefox: + context: test-runner:cypress-record-key + requires: + - contributor-pr + - driver-integration-tests-electron: + context: test-runner:cypress-record-key + requires: + - contributor-pr + - driver-integration-tests-webkit: + context: test-runner:cypress-record-key + requires: + - contributor-pr + - driver-integration-memory-tests: + requires: + - build + - run-frontend-shared-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - contributor-pr + - run-launchpad-integration-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - contributor-pr + - run-launchpad-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - contributor-pr + - run-app-integration-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - contributor-pr + - run-webpack-dev-server-integration-tests: + context: [test-runner:cypress-record-key, test-runner:percy] + requires: + - system-tests-node-modules-install + - run-vite-dev-server-integration-tests: + context: [test-runner:cypress-record-key, test-runner:percy] + requires: + - system-tests-node-modules-install + - run-app-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] + percy: true + requires: + - contributor-pr + - run-reporter-component-tests-chrome: + context: [test-runner:cypress-record-key, test-runner:percy] + percy: true + requires: + - contributor-pr + - reporter-integration-tests: + context: [test-runner:cypress-record-key, test-runner:percy] + requires: + - contributor-pr + - npm-webpack-dev-server: + requires: + - system-tests-node-modules-install + - npm-vite-dev-server: + requires: + - build + - npm-vite-plugin-cypress-esm: + requires: + - build + - npm-webpack-preprocessor: + requires: + - build + - npm-webpack-batteries-included-preprocessor: + requires: + - build + - npm-vue: + requires: + - build + - npm-puppeteer-unit-tests: + requires: + - build + - npm-puppeteer-cypress-tests: + requires: + - build + - npm-react: + requires: + - build + - npm-angular: + requires: + - build + - npm-angular-signals: + requires: + - build + - npm-mount-utils: + requires: + - build + - npm-eslint-plugin-dev: + requires: + - build + - npm-cypress-schematic: + requires: + - build + - v8-integration-tests: + requires: + - system-tests-node-modules-install + + - ready-to-release: + requires: + - check-ts + - npm-angular + - npm-angular-signals + - npm-eslint-plugin-dev + - npm-puppeteer-unit-tests + - npm-puppeteer-cypress-tests + - npm-react + - npm-mount-utils + - npm-vue + - npm-webpack-batteries-included-preprocessor + - npm-webpack-preprocessor + - npm-vite-dev-server + - npm-vite-plugin-cypress-esm + - npm-webpack-dev-server + - npm-cypress-schematic + - lint-types + - linux-lint + - percy-finalize + - driver-integration-tests-firefox + - driver-integration-tests-chrome + - driver-integration-tests-chrome-beta + - driver-integration-tests-electron + - driver-integration-tests-webkit + - driver-integration-memory-tests + - system-tests-non-root + - system-tests-firefox + - system-tests-electron + - system-tests-chrome + - system-tests-webkit + - server-performance-tests + - server-integration-tests + - server-unit-tests + - "test binary as a non-root user" + - "test binary as a root user" + - test-types-cypress-and-jest + - test-full-typescript-project + - test-binary-against-kitchensink + - test-npm-module-on-minimum-node-version + - binary-system-tests + - test-kitchensink + - unit-tests + - verify-release-readiness + - v8-integration-tests + + - npm-release: + context: test-runner:npm-release + requires: + - ready-to-release + + - create-and-trigger-packaging-artifacts: + context: [test-runner:upload, test-runner:build-binary, publish-binary] + requires: + - contributor-pr + - get-published-artifacts: + context: [publish-binary, test-runner:commit-status-checks] + requires: + - create-and-trigger-packaging-artifacts + # various testing scenarios, like building full binary + # and testing it on a real project + - test-against-staging: + context: test-runner:record-tests + <<: *mainBuildFilters + requires: + - build + - test-kitchensink: + requires: + - build + - test-kitchensink-against-staging: + executor: kitchensink-executor + context: test-runner:record-tests + <<: *mainBuildFilters + requires: + - build + - test-npm-module-on-minimum-node-version: + context: publish-binary + requires: + - get-published-artifacts + - test-types-cypress-and-jest: + context: publish-binary + requires: + - get-published-artifacts + - test-full-typescript-project: + context: publish-binary + requires: + - get-published-artifacts + - test-binary-against-kitchensink: + context: publish-binary + requires: + - get-published-artifacts + - test-npm-module-and-verify-binary: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-staging: + context: test-runner:record-tests + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-kitchensink-chrome: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-recipes-firefox: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-recipes-chrome: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-recipes: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-kitchensink-firefox: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-todomvc-firefox: + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-against-cypress-realworld-app: + context: test-runner:cypress-record-key + <<: *mainBuildFilters + requires: + - get-published-artifacts + - test-binary-as-specific-user: + name: "test binary as a non-root user" + executor: non-root-docker-user + context: publish-binary + requires: + - get-published-artifacts + - test-binary-as-specific-user: + name: "test binary as a root user" + context: publish-binary + requires: + - get-published-artifacts + - binary-system-tests: + context: publish-binary + requires: + - get-published-artifacts + - system-tests-node-modules-install + +linux-arm64-workflow: &linux-arm64-workflow + jobs: + - node_modules_install: + name: linux-arm64-node-modules-install + executor: linux-arm64 + resource_class: arm.medium + only-cache-for-root-user: true + + - build: + name: linux-arm64-build + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-node-modules-install + + - create-and-trigger-packaging-artifacts: + name: linux-arm64-create-and-trigger-packaging-artifacts + context: [test-runner:upload, test-runner:commit-status-checks, test-runner:build-binary, publish-binary] + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-node-modules-install + + - wait-for-binary-publish: + name: linux-arm64-wait-for-binary-publish + type: approval + requires: + - linux-arm64-create-and-trigger-packaging-artifacts + + - get-published-artifacts: + name: linux-arm64-get-published-artifacts + context: [publish-binary, test-runner:commit-status-checks] + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-wait-for-binary-publish + + - v8-integration-tests: + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-build + - driver-integration-memory-tests: + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-build + - server-unit-tests-cloud-environment: + executor: linux-arm64 + resource_class: arm.medium + requires: + - linux-arm64-build + +darwin-x64-workflow: &darwin-x64-workflow + jobs: + - node_modules_install: + name: darwin-x64-node-modules-install + executor: darwin-amd64 + resource_class: cypress-io/intel-macstadium + only-cache-for-root-user: true + + - build: + name: darwin-x64-build + context: test-runner:env-canary + executor: darwin-amd64 + resource_class: cypress-io/intel-macstadium + requires: + - darwin-x64-node-modules-install + + - create-build-artifacts: + name: darwin-x64-create-build-artifacts + context: + - test-runner:sign-mac-binary + - test-runner:upload + - test-runner:commit-status-checks + - test-runner:build-binary + executor: darwin-amd64 + resource_class: cypress-io/intel-macstadium + requires: + - darwin-x64-build + + - v8-integration-tests: + name: darwin-x64-v8-integration-tests + executor: darwin-amd64 + resource_class: cypress-io/intel-macstadium + requires: + - darwin-x64-build + + - driver-integration-memory-tests: + name: darwin-x64-driver-integration-memory-tests + executor: darwin-amd64 + resource_class: cypress-io/intel-macstadium + requires: + - darwin-x64-build + + - server-unit-tests-cloud-environment: + name: darwin-x64-driver-server-unit-tests-cloud-environment + executor: darwin-amd64 + resource_class: cypress-io/intel-macstadium + requires: + - darwin-x64-build + +darwin-arm64-workflow: &darwin-arm64-workflow + jobs: + - node_modules_install: + name: darwin-arm64-node-modules-install + executor: darwin-arm64 + resource_class: cypress-io/m1-macstadium + only-cache-for-root-user: true + + - build: + name: darwin-arm64-build + executor: darwin-arm64 + resource_class: cypress-io/m1-macstadium + requires: + - darwin-arm64-node-modules-install + + - create-build-artifacts: + name: darwin-arm64-create-build-artifacts + context: + - test-runner:sign-mac-binary + - test-runner:upload + - test-runner:commit-status-checks + - test-runner:build-binary + executor: darwin-arm64 + resource_class: cypress-io/m1-macstadium + requires: + - darwin-arm64-build + + - v8-integration-tests: + name: darwin-arm64-v8-integration-tests + executor: darwin-arm64 + resource_class: cypress-io/m1-macstadium + requires: + - darwin-arm64-build + - driver-integration-memory-tests: + name: darwin-arm64-driver-integration-memory-tests + executor: darwin-arm64 + resource_class: cypress-io/m1-macstadium + requires: + - darwin-arm64-build + - server-unit-tests-cloud-environment: + name: darwin-arm64-server-unit-tests-cloud-environment + executor: darwin-arm64 + resource_class: cypress-io/m1-macstadium + requires: + - darwin-arm64-build + +windows-workflow: &windows-workflow + jobs: + - node_modules_install: + name: windows-node-modules-install + executor: windows + resource_class: windows.medium + only-cache-for-root-user: true + + - build: + name: windows-build + context: test-runner:env-canary + executor: windows + resource_class: windows.large + requires: + - windows-node-modules-install + + - run-app-integration-tests-chrome: + name: windows-run-app-integration-tests-chrome + executor: windows + resource_class: windows.large + context: [test-runner:cypress-record-key, test-runner:launchpad-tests] + requires: + - windows-build + + - run-launchpad-integration-tests-chrome: + name: windows-run-launchpad-integration-tests-chrome + executor: windows + resource_class: windows.large + context: [test-runner:cypress-record-key, test-runner:launchpad-tests] + requires: + - windows-build + + - unit-tests: + name: windows-unit-tests + executor: windows + resource_class: windows.medium + requires: + - windows-build + + - server-unit-tests-cloud-environment: + name: windows-server-unit-tests-cloud-environment + executor: windows + resource_class: windows.medium + requires: + - windows-build + + - create-build-artifacts: + name: windows-create-build-artifacts + executor: windows + resource_class: windows.large + context: + - test-runner:sign-windows-binary + - test-runner:upload + - test-runner:commit-status-checks + - test-runner:build-binary + requires: + - windows-build + + - test-binary-against-kitchensink-chrome: + name: windows-test-binary-against-kitchensink-chrome + executor: windows + requires: + - windows-create-build-artifacts + + - v8-integration-tests: + name: windows-v8-integration-tests + executor: windows + resource_class: windows.medium + requires: + - windows-build + - driver-integration-memory-tests: + name: windows-driver-integration-memory-tests + executor: windows + resource_class: windows.medium + requires: + - windows-build + +workflows: + linux-x64: + <<: *linux-x64-workflow + <<: *linux-x64-workflow-exclude-filters + linux-x64-contributor: + <<: *linux-x64-contributor-workflow + when: + matches: + pattern: /^pull\/[0-9]+/ + value: << pipeline.git.branch >> + linux-arm64: + <<: *linux-arm64-workflow + <<: *linux-arm64-workflow-filters + darwin-x64: + <<: *darwin-x64-workflow + <<: *darwin-workflow-filters + darwin-arm64: + <<: *darwin-arm64-workflow + <<: *darwin-workflow-filters + windows: + <<: *windows-workflow + <<: *windows-workflow-filters diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 6247d1b9cb8..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,113 +0,0 @@ -# unignore hidden files -!.* - -**/__snapshots__ -**/_test-output -**/build -**/cypress/fixtures -**/dist -**/dist-* -**/node_modules -**/support/fixtures/* -!**/support/fixtures/projects -**/support/fixtures/projects/**/_fixtures/* -**/support/fixtures/projects/**/static/* -**/support/fixtures/projects/**/*.jsx -**/support/fixtures/projects/**/fail.js - -system-tests/fixtures/* -!system-tests/projects -system-tests/projects/**/_fixtures/* -system-tests/projects/**/static/* -system-tests/projects/**/*.jsx -system-tests/projects/**/fail.js -system-tests/lib/scaffold/plugins/index.js -system-tests/lib/scaffold/support/e2e.js -system-tests/lib/scaffold/support/component.js -system-tests/lib/scaffold/support/commands.js -system-tests/test/support/projects/e2e/cypress/ -system-tests/projects/e2e/cypress/e2e/stdout_exit_early_failing.cy.js -system-tests/projects/e2e/cypress/e2e/typescript_syntax_error.cy.ts -system-tests/projects/config-with-ts-syntax-error/** -system-tests/projects/config-with-ts-module-error/** -system-tests/projects/no-specs-vue-2/** - - -**/test/fixtures -**/vendor - -# cli/types is linted by tslint/dtslint -cli/types - -# cli/react, cli/vue, and cli/mount-utils are all copied from dist'd builds -cli/react -cli/vue -cli/vue2 -cli/mount-utils - -# packages/example is not linted (think about changing this) -packages/example - -packages/extension/test/helpers/background.js -e2e/stdout_exit_early_failing_spec.js - -npm/webpack-preprocessor/cypress/tests/e2e/compile-error.js -npm/webpack-preprocessor/examples/use-babelrc/cypress/e2e/spec.cy.js - -npm/cypress-schematic/src/**/*.js - -**/.projects -**/*.d.ts -**/package-lock.json -**/tsconfig.json -**/.vscode -**/.history -**/.cy -**/.git - -/npm/react/bin/* -/npm/react/**/coverage -**/.next/** -/npm/create-cypress-tests/initial-template -/npm/create-cypress-tests/**/*.template.* - -# The global eslint configuration is not set up to parse vue@2 files -/npm/vue2/**/*.vue - -packages/data-context/test/unit/codegen/files -packages/config/test/__fixtures__/**/* -packages/config/test/__babel_fixtures__/**/* - -# community templates we test against, no need to lint -system-tests/projects/cra-4/**/* -system-tests/projects/cra-5/**/* -system-tests/projects/cra-ejected/**/* -system-tests/projects/create-react-app-configured/**/* -system-tests/projects/create-react-app-unconfigured/**/* -system-tests/projects/create-react-app-custom-index-html - -system-tests/projects/vueclivue2-unconfigured/**/* -system-tests/projects/vueclivue2-configured/**/* -system-tests/projects/outdated-deps-vuecli3/**/* - -system-tests/projects/vueclivue3-unconfigured/**/* -system-tests/projects/vueclivue3-configured/**/* -system-tests/projects/vueclivue3-custom-index-html - -system-tests/projects/vuecli5vue3-unconfigured/**/* -system-tests/projects/vuecli5vue3-configured/**/* - -system-tests/projects/vue3-vite-ts-unconfigured/**/* -system-tests/projects/vue3-vite-ts-configured/**/* -system-tests/projects/vue3-vite-ts-custom-index-html - -system-tests/projects/nextjs-unconfigured/**/* -system-tests/projects/nextjs-configured/**/* - -system-tests/projects/nuxtjs-vue2-unconfigured/**/* -system-tests/projects/nuxtjs-vue2-configured/**/* - -system-tests/projects/react-app-webpack-5-unconfigured/**/* - -system-tests/project-fixtures/** -system-tests/projects/**/*/expected-cypress*/**/* diff --git a/.eslintrc.js b/.eslintrc.js index 7abb027d019..94dcd6358b3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,6 +25,7 @@ const validators = specifiedRules ) module.exports = { + root: true, plugins: [ '@cypress/dev', 'graphql', @@ -34,6 +35,14 @@ module.exports = { 'plugin:@cypress/dev/tests', ], parser: '@typescript-eslint/parser', + ignorePatterns: [ + // cli types are checked by dtslint + 'cli/types/**', + // these fixtures are supposed to fail linting + 'npm/eslint-plugin-dev/test/fixtures/**', + // Cloud generated + 'system-tests/lib/validations/**', + ], overrides: [ { files: [ @@ -41,23 +50,23 @@ module.exports = { '**/scripts/**', '**/test/**', '**/system-tests/**', + 'tooling/**', 'packages/{app,driver,frontend-shared,launchpad}/cypress/**', '*.test.ts', - // ignore in packages that don't run in the Cypress process - 'npm/create-cypress-tests/**', ], rules: { 'no-restricted-properties': 'off', 'no-restricted-syntax': 'off', }, }, + { + files: ['*.json'], + extends: 'plugin:@cypress/dev/general', + }, ], rules: { 'no-duplicate-imports': 'off', - 'import/no-duplicates': 'off', - '@typescript-eslint/no-duplicate-imports': [ - 'error', - ], + 'import/no-duplicates': 'error', 'prefer-spread': 'off', 'prefer-rest-params': 'off', 'no-useless-constructor': 'off', diff --git a/.gitattributes b/.gitattributes index ebbd9782ff0..4a597b0345e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,4 +2,7 @@ *.json text eol=lf -packages/errors/__snapshot-html__/** linguist-generated=true \ No newline at end of file +**/.eslintrc text eol=lf + +packages/errors/__snapshot-html__/** linguist-generated=true +system-tests/lib/validations/** linguist-generated=true \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000000..21255064a7d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners +# +# Changes to the Module API, after:run, or after:spec results should be +# reviewed by Brian and/or Jennifer +/system-tests/__snapshots__/results_spec.ts.js @brian-mann @jennifer-shehane diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml index d345678e364..2f269318a1d 100644 --- a/.github/ISSUE_TEMPLATE/1-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -4,7 +4,10 @@ body: - type: markdown attributes: value: | - Have a question? 👉 [Start a new discussion](https://github.com/cypress-io/cypress/discussions) or [ask in chat](https://on.cypress.io/discord). + ## If you are a customer of Cypress Cloud please utilize our [Support Portal](https://www.cypress.io/support/) for our fastest support! + + ### Have a question? 👉 [Ask in chat](https://on.cypress.io/discord) or [start a new discussion](https://github.com/cypress-io/cypress/discussions). + Issues in the Cypress repo are reserved for bugs and feature requests only. Questions on how to use Cypress will be closed. - type: textarea id: current-behavior attributes: @@ -40,7 +43,7 @@ body: attributes: label: Node version description: What version of node.js are you using to run Cypress? - placeholder: ex. v16.16.0 + placeholder: ex. v18.17.0 validations: required: true - type: input diff --git a/.github/ISSUE_TEMPLATE/2-install-issue.yml b/.github/ISSUE_TEMPLATE/2-install-issue.yml deleted file mode 100644 index 99eb13e5b07..00000000000 --- a/.github/ISSUE_TEMPLATE/2-install-issue.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: "⬇️ Issue during install" -description: Report an issue while downloading Cypress. -labels: ['topic: installation'] -body: - - type: markdown - attributes: - value: | - Have a question? 👉 [Start a new discussion](https://github.com/cypress-io/cypress/discussions) or [ask in chat](https://on.cypress.io/discord). - - If you're behind a corporate proxy, make sure to [configure it properly](https://on.cypress.io/proxy-configuration) before install. - - type: textarea - id: current-behavior - attributes: - label: Current behavior - description: Please provide a description including screenshots, stack traces, DEBUG logs, etc. [Troubleshooting tips](https://on.cypress.io/troubleshooting) - placeholder: When I try to download Cypress... - validations: - required: true - - type: textarea - id: debug-logs - attributes: - label: Debug logs - description: Include DEBUG logs setting [`DEBUG=cypress:*`](https://on.cypress.io/troubleshooting#Print-DEBUG-logs/). Include npm/yarn logs if applicable. - placeholder: Debug logs - render: Text - - type: input - id: version - attributes: - label: Cypress Version - description: What version of Cypress are you trying to install? - placeholder: ex. 10.3.1 - validations: - required: true - - type: input - id: node-version - attributes: - label: Node version - description: What version of node.js are you using to run Cypress? - placeholder: ex. v16.16.0 - validations: - required: true - - type: dropdown - id: package-manager - attributes: - label: Package Manager - options: - - npm - - yarn - - Direct download - - pnpm - - other - validations: - required: true - - type: input - id: package-manager-version - attributes: - label: Package Manager Version - description: What is the version of your chosen package manager? - validations: - required: true - - type: dropdown - id: operating-system - attributes: - label: Operating system - options: - - Linux - - Mac - - Windows - - other - validations: - required: true - - type: input - id: os-version - attributes: - label: Operating System Version - description: What is the version of the operating system you are running Cypress on? - placeholder: ex 12.4 - validations: - required: true - - type: textarea - id: other - attributes: - label: Other - placeholder: Any other details? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/2-memory-issue.yml b/.github/ISSUE_TEMPLATE/2-memory-issue.yml new file mode 100644 index 00000000000..28e5bbabb1a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-memory-issue.yml @@ -0,0 +1,81 @@ +name: "💾 Memory issue report" +description: Report a memory issue found while using Cypress. +body: + - type: markdown + attributes: + value: | + Have a question? 👉 [Ask in chat](https://on.cypress.io/discord) or [start a new discussion](https://github.com/cypress-io/cypress/discussions). + + Prior to logging a memory issue, please make sure you are running the latest version of Cypress and have enabled [`experimentalMemoryManagement`](https://on.cypress.io/experiments) for Chromium-based browsers (introduced in Cypress 12.4.0). + + If you are running in `cypress open` mode, you can also try lowering [`numTestsKeptInMemory`](https://docs.cypress.io/guides/references/configuration#Options) in your config file. + + If you are still experiencing the issue, please fill out the following information. + - type: textarea + id: reproduction + attributes: + label: Test code to reproduce + description: Please provide a failing test or repo we can run. You can fork [this repo](https://github.com/cypress-io/cypress-test-tiny), set up a failing test, then link to your fork. If you have never done this before, watch [this video](https://youtu.be/NnriKHmj5T8) for example. + placeholder: Here is my failing test code and the app code to run the tests on... + validations: + required: true + - type: dropdown + id: mode + attributes: + label: Cypress Mode + description: What mode of Cypress are you running? + options: + - cypress run + - cypress open + - both modes + validations: + required: true + - type: input + id: version + attributes: + label: Cypress Version + description: What version of Cypress are you running? Run `cypress version` to see your current version. If possible, please update Cypress to the latest version first. + placeholder: ex. 10.3.1 + validations: + required: true + - type: input + id: browser + attributes: + label: Browser Version + description: What browser(s) is Cypress running against when you are encountering your problem? The more specific the better. ie Chrome 109.0.5414.87 or Firefox 107.0 + placeholder: ex. Chrome 109.0.5414.87 + validations: + required: true + - type: input + id: node-version + attributes: + label: Node version + description: What version of node.js are you using to run Cypress? + placeholder: ex. v18.17.0 + validations: + required: true + - type: input + id: os + attributes: + label: Operating System + description: What operating system is Cypress running on when you are encountering your problem? The more specific the better. ie macOS 12.4 or Windows 10.0.19044.1889 + placeholder: ex. macOS 12.4 + validations: + required: true + - type: textarea + id: debug-logs + attributes: + label: Memory Debug Logs + description: | + If running a Chromium-based browser, please set the `CYPRESS_INTERNAL_MEMORY_SAVE_STATS` environment variable to `1` or `true` and include the `cypress\logs\memory\.json` file here. + + Alternatively, you can run Cypress in [debug mode](https://docs.cypress.io/guides/references/troubleshooting#Print-DEBUG-logs) setting `DEBUG=cypress*memory` and include the entire set of logs here. + placeholder: Debug logging output + render: shell + validations: + required: false + - type: textarea + id: other + attributes: + label: Other + placeholder: Any other details? diff --git a/.github/ISSUE_TEMPLATE/3-feature.yml b/.github/ISSUE_TEMPLATE/3-feature.yml deleted file mode 100644 index 48d7a366701..00000000000 --- a/.github/ISSUE_TEMPLATE/3-feature.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "✨ Feature" -description: Suggest a feature or enhancement to improve Cypress. -body: - - type: markdown - attributes: - value: | - Have a question? 👉 [Start a new discussion](https://github.com/cypress-io/cypress/discussions) or [ask in chat](https://on.cypress.io/discord). - - type: textarea - id: feature - attributes: - label: What would you like? - description: A clear description of the feature or enhancement wanted in Cypress. - placeholder: I'd like to be able to... - validations: - required: true - - type: textarea - id: reason - attributes: - label: Why is this needed? - description: Remember, we're not familiar with the app you're testing, so please provide a clear description of why this would be useful to your project. - placeholder: I want this because... - - type: textarea - id: other - attributes: - label: Other - placeholder: Any other details? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/3-install-issue.yml b/.github/ISSUE_TEMPLATE/3-install-issue.yml new file mode 100644 index 00000000000..6e513901e5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-install-issue.yml @@ -0,0 +1,86 @@ +name: "⬇️ Issue during install" +description: Report an issue while downloading Cypress. +labels: ['topic: installation'] +body: + - type: markdown + attributes: + value: | + ## If you are a customer of Cypress Cloud please utilize our [Support Portal](https://www.cypress.io/support/) for our fastest support! + + ### Have a question? 👉 [Ask in chat](https://on.cypress.io/discord) or [start a new discussion](https://github.com/cypress-io/cypress/discussions). + + If you're behind a corporate proxy, make sure to [configure it properly](https://on.cypress.io/proxy-configuration) before install. + - type: textarea + id: current-behavior + attributes: + label: Current behavior + description: Please provide a description including screenshots, stack traces, DEBUG logs, etc. [Troubleshooting tips](https://on.cypress.io/troubleshooting) + placeholder: When I try to download Cypress... + validations: + required: true + - type: textarea + id: debug-logs + attributes: + label: Debug logs + description: Include DEBUG logs setting [`DEBUG=cypress:*`](https://on.cypress.io/troubleshooting#Print-DEBUG-logs/). Include npm/yarn logs if applicable. + placeholder: Debug logs + render: Text + - type: input + id: version + attributes: + label: Cypress Version + description: What version of Cypress are you trying to install? + placeholder: ex. 10.3.1 + validations: + required: true + - type: input + id: node-version + attributes: + label: Node version + description: What version of node.js are you using to run Cypress? + placeholder: ex. v18.17.0 + validations: + required: true + - type: dropdown + id: package-manager + attributes: + label: Package Manager + options: + - npm + - yarn + - Direct download + - pnpm + - other + validations: + required: true + - type: input + id: package-manager-version + attributes: + label: Package Manager Version + description: What is the version of your chosen package manager? + validations: + required: true + - type: dropdown + id: operating-system + attributes: + label: Operating system + options: + - Linux + - Mac + - Windows + - other + validations: + required: true + - type: input + id: os-version + attributes: + label: Operating System Version + description: What is the version of the operating system you are running Cypress on? + placeholder: ex 12.4 + validations: + required: true + - type: textarea + id: other + attributes: + label: Other + placeholder: Any other details? diff --git a/.github/ISSUE_TEMPLATE/4-feature.yml b/.github/ISSUE_TEMPLATE/4-feature.yml new file mode 100644 index 00000000000..f69e48523d8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-feature.yml @@ -0,0 +1,28 @@ +name: "✨ Feature" +description: Suggest a feature or enhancement to improve Cypress. +body: + - type: markdown + attributes: + value: | + ## If you are a customer of Cypress Cloud please utilize our [Support Portal](https://www.cypress.io/support/) for our fastest support! + + ### Have a question? 👉 [Ask in chat](https://on.cypress.io/discord) or [start a new discussion](https://github.com/cypress-io/cypress/discussions). + - type: textarea + id: feature + attributes: + label: What would you like? + description: A clear description of the feature or enhancement wanted in Cypress. + placeholder: I'd like to be able to... + validations: + required: true + - type: textarea + id: reason + attributes: + label: Why is this needed? + description: Remember, we're not familiar with the app you're testing, so please provide a clear description of why this would be useful to your project. + placeholder: I want this because... + - type: textarea + id: other + attributes: + label: Other + placeholder: Any other details? diff --git a/.github/ISSUE_TEMPLATE/4-flaky-test.yml b/.github/ISSUE_TEMPLATE/4-flaky-test.yml deleted file mode 100644 index abcc4baa955..00000000000 --- a/.github/ISSUE_TEMPLATE/4-flaky-test.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "❄️ Flaky test in `cypress-io/cypress` repository" -title: "Flaky test: " -labels: ["topic: flake ❄️", "stage: fire watch"] -description: Report a flaky test in the Cypress open-source repository. -body: - - type: markdown - attributes: - value: | - Have a question? 👉 [Start a new discussion](https://github.com/cypress-io/cypress/discussions) or [ask in chat](https://on.cypress.io/discord). - - type: textarea - id: dashboard - attributes: - label: Link to dashboard or CircleCI failure - description: Please include a link to the failure in the Cypress Dashboard or in CircleCI. - validations: - required: true - - type: textarea - id: github-link - attributes: - label: Link to failing test in GitHub - description: Provide the GitHub link to the failing test with the line number. - validations: - required: true - - type: textarea - id: analysis - attributes: - label: Analysis - description: If you can, provide a quick analysis of why this test is flaky. - placeholder: ex. The test appears to be flaky because... - validations: - required: true - - type: input - id: version - attributes: - label: Cypress Version - description: Provide the version of Cypress where the flake is occurring. - placeholder: ex. 10.4.0 - validations: - required: true - - type: textarea - id: other - attributes: - label: Other - placeholder: Any other details? diff --git a/.github/ISSUE_TEMPLATE/5-flaky-test.yml b/.github/ISSUE_TEMPLATE/5-flaky-test.yml new file mode 100644 index 00000000000..da406ba8106 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5-flaky-test.yml @@ -0,0 +1,44 @@ +name: "❄️ Flaky test in `cypress-io/cypress` repository" +title: "Flaky test: " +labels: ["topic: flake ❄️", "stage: fire watch"] +description: Report a flaky test in the Cypress open-source repository. +body: + - type: markdown + attributes: + value: | + Have a question? 👉 [Ask in chat](https://on.cypress.io/discord) or [start a new discussion](https://github.com/cypress-io/cypress/discussions). + - type: textarea + id: dashboard + attributes: + label: Link to Cypress Cloud or CircleCI failure + description: Please include a link to the failure in Cypress Cloud or in CircleCI. + validations: + required: true + - type: textarea + id: github-link + attributes: + label: Link to failing test in GitHub + description: Provide the GitHub link to the failing test with the line number. + validations: + required: true + - type: textarea + id: analysis + attributes: + label: Analysis + description: If you can, provide a quick analysis of why this test is flaky. + placeholder: ex. The test appears to be flaky because... + validations: + required: true + - type: input + id: version + attributes: + label: Cypress Version + description: Provide the version of Cypress where the flake is occurring. + placeholder: ex. 10.4.0 + validations: + required: true + - type: textarea + id: other + attributes: + label: Other + placeholder: Any other details? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 242ecd78299..ff1b1ac51f3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,12 +7,6 @@ - Closes -### User facing changelog - - ### Additional details - [ ] Have tests been added/updated? -- [ ] Has the original issue (or this PR, if no issue exists) been tagged with a release in ZenHub? (user-facing changes only) - [ ] Has a PR for user-facing changes been opened in [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation)? - [ ] Have API changes been updated in the [`type definitions`](https://github.com/cypress-io/cypress/blob/develop/cli/types/cypress.d.ts)? diff --git a/.github/workflows/merge-master-into-develop.yml b/.github/workflows/merge-master-into-develop.yml deleted file mode 100644 index 2b15adabc19..00000000000 --- a/.github/workflows/merge-master-into-develop.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Merge master into develop -on: - push: - branches: - - master -jobs: - merge-master-into-develop: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - # the default `GITHUB_TOKEN` cannot push to protected branches, so use `cypress-app-bot`'s token instead - token: ${{ secrets.BOT_GITHUB_TOKEN }} - - name: Set committer info - run: | - git config --local user.email "$(git log --format='%ae' HEAD^!)" - git config --local user.name "$(git log --format='%an' HEAD^!)" - - name: Checkout develop branch - run: git checkout develop - - name: Check for merge conflict - id: check-conflict - run: echo "::set-output name=merge_conflict::$(git merge-tree $(git merge-base HEAD master) master HEAD | egrep '<<<<<<<')" - - name: Merge master into develop - id: merge-master - run: git merge master - if: ${{ !steps.check-conflict.outputs.merge_conflict }} - - name: Failed merge, set merged status as failed - run: echo "::set-output name=merge_conflict::'failed merge'" - if: ${{ steps.merge-master.outcome != 'success' }} - - name: Push - run: git push - if: ${{ !steps.check-conflict.outputs.merge_conflict }} - - name: Checkout master - run: git checkout master - if: ${{ steps.check-conflict.outputs.merge_conflict }} - - name: Determine name of new branch - id: gen-names - run: | - echo "::set-output name=sha::$(git rev-parse --short HEAD)" - echo "::set-output name=branch_name::$(git rev-parse --short HEAD)-master-into-develop" - if: ${{ steps.check-conflict.outputs.merge_conflict }} - - name: Create a copy of master on a new branch - run: git checkout -b ${{ steps.gen-names.outputs.branch_name }} master - if: ${{ steps.check-conflict.outputs.merge_conflict }} - - name: Push branch to remote - run: git push origin ${{ steps.gen-names.outputs.branch_name }} - if: ${{ steps.check-conflict.outputs.merge_conflict }} - - name: Create Pull Request - uses: actions/github-script@v3 - with: - script: | - const pull = await github.pulls.create({ - owner: context.repo.owner, - repo: context.repo.repo, - base: 'develop', - head: '${{ steps.gen-names.outputs.branch_name }}', - title: 'chore: merge master (${{ steps.gen-names.outputs.sha }}) into develop', - body: `There was a merge conflict when trying to automatically merge master into develop. Please resolve the conflict and complete the merge. - - DO NOT SQUASH AND MERGE - - @${context.actor}`, - maintainer_can_modify: true, - }) - await github.pulls.requestReviewers({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pull.data.number, - reviewers: [context.actor], - }) - await github.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pull.data.number, - labels: ['auto-merge'], - }) - if: ${{ steps.check-conflict.outputs.merge_conflict }} diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index 469b50ef547..e54d669634c 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -1,5 +1,21 @@ name: "Semantic Pull Request" - +# @see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +permissions: + actions: none + checks: none + # to check out & read the repository + contents: read + deployments: none + id-token: none + issues: none + discussions: none + packages: none + pages: none + # to read pull-request data, including commits/issues linked + pull-requests: read + repository-projects: none + security-events: none + statuses: none on: pull_request_target: types: @@ -9,13 +25,21 @@ on: jobs: main: - name: Lint Title + name: Semantic Pull Request runs-on: ubuntu-latest steps: - # use a fork of the GitHub action - we cannot pull in untrusted third party actions - # see https://github.com/cypress-io/cypress/pull/20091#discussion_r801799647 - - uses: cypress-io/action-semantic-pull-request@v4 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + - run: npm install + working-directory: scripts/github-actions/semantic-pull-request/ + - name: Lint PR Title and Cypress Changelog Entry + if: github.event_name == 'pull_request_target' + uses: actions/github-script@v7 with: - validateSingleCommit: true \ No newline at end of file + script: | + const verifyPullRequest = require('./scripts/github-actions/semantic-pull-request') + + await verifyPullRequest({ context, core, github }) diff --git a/.github/workflows/snyk_sca_scan.yaml b/.github/workflows/snyk_sca_scan.yaml index d9b21b0ab8e..45f504bf3a4 100644 --- a/.github/workflows/snyk_sca_scan.yaml +++ b/.github/workflows/snyk_sca_scan.yaml @@ -1,33 +1,45 @@ name: Snyk Software Composition Analysis Scan -# This git workflow leverages Snyk actions to perform a Software Composition -# Analysis scan on our Opensource libraries upon Pull Requests to Master & -# Develop branches. We use this as a control to prevent vulnerable packages -# from being introduced into the codebase. +# This git workflow leverages Snyk actions to perform a Software Composition +# Analysis scan on our Opensource libraries upon Pull Requests to the +# "develop" branch. We use this as a control to prevent vulnerable packages +# from being introduced into the codebase. +# Enhancements were made to this action to build the yarn packages to reduce +# Snyk scan errors that were complaining about the yarn.locks etc. Also +# implemented PAT token for actions to resolve an issue with the action not +# running and reporting back to the PR status checks on: - pull_request_target: - types: - - opened - branches: - - master + pull_request: + branches: - develop jobs: Snyk_SCA_Scan: + # Skip this job on PRs from forks + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - - uses: actions/checkout@v2 - - name: Setting up Node - uses: actions/setup-node@v1 + - name: Checkout + uses: actions/checkout@v4 with: - node-version: ${{ matrix.node-version }} + fetch-depth: 0 + token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'yarn' + - name: Run yarn + run: yarn + - name: Run build + run: yarn build - name: Installing snyk-delta and dependencies run: npm i -g snyk-delta - uses: snyk/actions/setup@master - name: Perform SCA Scan continue-on-error: false run: | - snyk test --yarn-workspaces --strict-out-of-sync=false --detection-depth=6 --exclude=docker,Dockerfile --severity-threshold=critical + snyk test --all-projects --strict-out-of-sync=false --detection-depth=6 --exclude=system-tests,tooling,docker,Dockerfile --severity-threshold=critical env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} diff --git a/.github/workflows/snyk_static_analysis_scan.yaml b/.github/workflows/snyk_static_analysis_scan.yaml index f34b3de41e1..58596a407c8 100644 --- a/.github/workflows/snyk_static_analysis_scan.yaml +++ b/.github/workflows/snyk_static_analysis_scan.yaml @@ -1,20 +1,32 @@ name: Snyk Static Analysis Scan -# This git workflow leverages Snyk actions to perform a Static Application -# Testing scan (SAST) on our first-party code upon Pull Requests to Master & -# Develop branches. We use this as a control to prevent vulnerabilities -# from being introduced into the codebase. +# This git workflow leverages Snyk actions to perform a Static Application +# Testing scan (SAST) on our first-party code upon Pull Requests to the +# "develop" branch. We use this as a control to prevent vulnerabilities +# from being introduced into the codebase. on: - pull_request_target: - types: - - opened - branches: - - master + pull_request: + branches: - develop jobs: - Snyk_SAST_Scan : + Snyk_SAST_Scan: + # Skip this job on PRs from forks + if: github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'yarn' + - name: Run yarn + run: yarn + - name: Run build + run: yarn build - uses: snyk/actions/setup@master - name: Perform Static Analysis Test continue-on-error: true diff --git a/.github/workflows/stale_issues_and_pr_cleanup.yml b/.github/workflows/stale_issues_and_pr_cleanup.yml new file mode 100644 index 00000000000..ece421f0aad --- /dev/null +++ b/.github/workflows/stale_issues_and_pr_cleanup.yml @@ -0,0 +1,60 @@ +name: 'Close stale issues and PRs' +on: + workflow_dispatch: + inputs: + debug-only: + description: 'debug-only' + required: false + default: false + max-operations-per-run: + description: 'max operations per run' + required: false + default: 3000 + days-before-stale: + description: 'days-before-stale' + required: false + default: 180 + days-before-close: + description: 'days-before-close' + required: false + default: 14 + exempt-issue-labels: + description: 'exempt-issue-labels' + required: false + default: 'type: feature,type: enhancement,routed-to-e2e,routed-to-ct,routed-to-tools,routed-to-cloud,prevent-stale,triaged' + exempt-pr-labels: + description: 'exempt-pr-labels' + required: false + default: 'type: feature,type: enhancement,prevent-stale,triaged' + schedule: + - cron: '30 1 * * *' +permissions: + issues: write + pull-requests: write +env: + DEFAULT_DEBUG_ONLY: false + DEFAULT_MAX_OPS: 3000 + DEFAULT_DAYS_BEFORE_STALE: 180 + DEFAULT_DAYS_BEFORE_CLOSE: 14 + DEFAULT_EXEMPT_ISSUE_LABELS: 'type: feature,type: enhancement,routed-to-e2e,routed-to-ct,routed-to-tools,routed-to-cloud,prevent-stale,triaged' + DEFAULT_EXEMPT_PR_LABELS: 'type: feature,type: enhancement,prevent-stale,triaged' +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + days-before-stale: ${{ github.event.inputs.days-before-stale || env.DEFAULT_DAYS_BEFORE_STALE }} + days-before-close: ${{ github.event.inputs.days-before-close || env.DEFAULT_DAYS_BEFORE_CLOSE }} + stale-issue-message: 'This issue has not had any activity in ${{ github.event.inputs.days-before-stale || env.DEFAULT_DAYS_BEFORE_STALE }} days. Cypress evolves quickly and the reported behavior should be tested on the latest version of Cypress to verify the behavior is still occurring. It will be closed in ${{ github.event.inputs.days-before-close || env.DEFAULT_DAYS_BEFORE_CLOSE }} days if no updates are provided.' + stale-pr-message: 'This PR has not had any activity in ${{ github.event.inputs.days-before-stale || env.DEFAULT_DAYS_BEFORE_STALE }} days. If no activity is detected in the next ${{ github.event.inputs.days-before-close || env.DEFAULT_DAYS_BEFORE_CLOSE }} days, this PR will be closed.' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + close-issue-message: 'This issue has been closed due to inactivity.' + close-pr-message: 'This PR has been closed due to inactivity' + exempt-issue-labels: ${{ github.event.inputs.exempt-issue-labels || env.DEFAULT_EXEMPT_ISSUE_LABELS }} + exempt-pr-labels: ${{ github.event.inputs.exempt-pr-labels || env.DEFAULT_EXEMPT_PR_LABELS }} + exempt-all-milestones: true + operations-per-run: ${{ github.event.inputs.max-operations-per-run || env.DEFAULT_MAX_OPS }} #keeping this a bit higher because it processes newest tickets to oldest + debug-only: ${{ github.event.inputs.debug-only || env.DEFAULT_DEBUG_ONLY }} + repo-token: ${{ secrets.TRIAGE_BOARD_TOKEN }} diff --git a/.github/workflows/triage_add_to_project.yml b/.github/workflows/triage_add_to_project.yml index 4dcf519a2a1..846c5c2df94 100644 --- a/.github/workflows/triage_add_to_project.yml +++ b/.github/workflows/triage_add_to_project.yml @@ -1,6 +1,15 @@ name: 'Triage: add issue/PR to project' on: + # makes this workflow reusable + workflow_call: + secrets: + ADD_TO_TRIAGE_BOARD_TOKEN: + required: true + TRIAGE_BOARD_TOKEN: + required: true + WORKFLOW_DEPLOY_KEY: + required: true issues: types: - opened @@ -12,9 +21,48 @@ jobs: add-to-triage-project: name: Add to triage project runs-on: ubuntu-latest + permissions: + issues: write + env: + PROJECT_NUMBER: 9 + GITHUB_TOKEN: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} steps: - - uses: actions/add-to-project@v0.3.0 + - name: is-collaborator + run: | + gh api graphql -f query=' + query($org: String!, $repo: String!, $user: String!) { + repository(owner: $org, name: $repo) { + collaborators(query: $user, first: 1) { + totalCount + } + } + } ' -f org=${{ github.repository_owner }} -f repo=${{ github.event.repository.name }} -f user=${{ github.event.pull_request.user.login || github.event.issue.user.login }} > collaborators.json + + echo 'IS_COLLABORATOR='$(jq -r '.data.repository.collaborators.totalCount' collaborators.json) >> $GITHUB_ENV + - uses: actions/add-to-project@v0.4.1 + # only add issues/prs from outside contributors to the project + if: ${{ env.IS_COLLABORATOR == 0 || github.event.repository.name == 'cypress-support-internal' || github.event.pull_request.user.login == 'github-actions[bot]' || github.event.issue.user.login == 'github-actions[bot]' }} + with: + project-url: https://github.com/orgs/${{github.repository_owner}}/projects/${{env.PROJECT_NUMBER}} + github-token: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} + add-contributor-pr-comment: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 with: - project-url: https://github.com/orgs/cypress-io/projects/9 - github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} - + repository: 'cypress-io/release-automations' + ref: 'master' + ssh-key: ${{ secrets.WORKFLOW_DEPLOY_KEY }} + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Run comment_workflow.js Script + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.TRIAGE_BOARD_TOKEN }} + script: | + const script = require('./scripts/triage/add_contributing_comment.js') + await script.addContributingComment(github, context); + diff --git a/.github/workflows/triage_add_to_routed_project.yml b/.github/workflows/triage_add_to_routed_project.yml index 6bb13d85fa3..c50f4233cc2 100644 --- a/.github/workflows/triage_add_to_routed_project.yml +++ b/.github/workflows/triage_add_to_routed_project.yml @@ -1,5 +1,10 @@ name: 'Triage: route to team project board' on: +# makes this workflow reusable + workflow_call: + secrets: + ADD_TO_TRIAGE_BOARD_TOKEN: + required: true issues: types: - labeled @@ -10,7 +15,7 @@ jobs: steps: - name: Get project data env: - GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} ORGANIZATION: 'cypress-io' PROJECT_NUMBER: 10 run: | @@ -26,7 +31,7 @@ jobs: echo 'PROJECT_ID='$(jq -r '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV - name: add issue to e2e project env: - GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_TOKEN }} + GITHUB_TOKEN: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} ISSUE_ID: ${{ github.event.issue.node_id }} run: | gh api graphql -f query=' diff --git a/.github/workflows/triage_closed_issue_comment.yml b/.github/workflows/triage_closed_issue_comment.yml deleted file mode 100644 index 1caf8abdbb3..00000000000 --- a/.github/workflows/triage_closed_issue_comment.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: 'Triage: closed issue comment' -on: - issue_comment: - types: - - created -jobs: - move-to-new-issue-status: - if: | - !github.event.issue.pull_request && - github.event.issue.state == 'closed' && - github.event.comment.created_at != github.event.issue.closed_at && - github.event.sender.login != 'cypress-bot' - runs-on: ubuntu-latest - steps: - - name: Get project data - env: - GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_TOKEN }} - ORGANIZATION: 'cypress-io' - REPOSITORY: 'cypress' - PROJECT_NUMBER: 9 - ISSUE_NUMBER: ${{ github.event.issue.number }} - run: | - gh api graphql -f query=' - query($org: String!, $repo: String!, $project: Int!, $issue: Int!) { - organization(login: $org) { - repository(name: $repo) { - issue(number: $issue) { - projectItems(first: 10, includeArchived: false) { - nodes { - id - fieldValueByName(name: "Status") { - ... on ProjectV2ItemFieldSingleSelectValue { - name - field { - ... on ProjectV2SingleSelectField { - project { - ... on ProjectV2 { - id - number - } - } - } - } - } - } - } - } - } - } - projectV2(number: $project) { - field(name: "Status") { - ... on ProjectV2SingleSelectField { - id - options { - id - name - } - } - } - } - } - }' -f org=$ORGANIZATION -f repo=$REPOSITORY -F issue=$ISSUE_NUMBER -F project=$PROJECT_NUMBER > project_data.json - - echo 'PROJECT_ID='$(jq -r '.data.organization.repository.issue.projectItems.nodes[].fieldValueByName.field.project | select(.number == ${{ env.PROJECT_NUMBER }}) | .id' project_data.json) >> $GITHUB_ENV - echo 'PROJECT_ITEM_ID='$(jq -r '.data.organization.repository.issue.projectItems.nodes[] | select(.fieldValueByName.field.project.number == ${{ env.PROJECT_NUMBER }}) | .id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_FIELD_ID='$(jq -r '.data.organization.projectV2.field | .id' project_data.json) >> $GITHUB_ENV - echo 'STATUS='$(jq -r '.data.organization.repository.issue.projectItems.nodes[].fieldValueByName | select(.field.project.number == ${{ env.PROJECT_NUMBER }}) | .name' project_data.json) >> $GITHUB_ENV - echo 'NEW_ISSUE_OPTION_ID='$(jq -r '.data.organization.projectV2.field.options[] | select(.name== "New Issue") | .id' project_data.json) >> $GITHUB_ENV - - name: Move issue to New Issue status - env: - GITHUB_TOKEN: ${{ secrets.ADD_TO_PROJECT_TOKEN }} - if: env.STATUS == 'Closed' - run: | - gh api graphql -f query=' - mutation ( - $project: ID! - $item: ID! - $status_field: ID! - $status_value: String! - ) { - updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $status_field - value: { - singleSelectOptionId: $status_value - } - }) { - projectV2Item { - id - } - } - }' -f project=$PROJECT_ID -f item=$PROJECT_ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=$NEW_ISSUE_OPTION_ID diff --git a/.github/workflows/triage_handle_new_comments.yml b/.github/workflows/triage_handle_new_comments.yml new file mode 100644 index 00000000000..5778b08c555 --- /dev/null +++ b/.github/workflows/triage_handle_new_comments.yml @@ -0,0 +1,32 @@ +name: 'Handle Issue/PR Comment Workflow' +on: + # makes this workflow reusable + workflow_call: + secrets: + TRIAGE_BOARD_TOKEN: + required: true + + issue_comment: + types: [created] + +jobs: + handle-comment-scenarios: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + repository: 'cypress-io/release-automations' + ref: 'master' + ssh-key: ${{ secrets.WORKFLOW_DEPLOY_KEY }} + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Run comment_workflow.js Script + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.TRIAGE_BOARD_TOKEN }} + script: | + const script = require('./scripts/triage/comment_workflow.js') + await script.handleComment(github, context); diff --git a/.github/workflows/triage_issue_metrics.yml b/.github/workflows/triage_issue_metrics.yml deleted file mode 100644 index 55c8101d3c6..00000000000 --- a/.github/workflows/triage_issue_metrics.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: 'Triage: issue metrics' - -on: - workflow_dispatch: - inputs: - startDate: - description: 'Start date (YYYY-MM-DD)' - type: date - endDate: - description: 'End date (YYYY-MM-DD)' - type: date -jobs: - seven-day-close: - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v6 - env: - ORGANIZATION: 'cypress-io' - REPOSITORY: 'cypress' - PROJECT_NUMBER: 9 - with: - github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} - script: | - const ROUTED_TO_LABELS = ['routed-to-e2e', 'routed-to-ct'] - const MS_PER_DAY = 1000 * 60 * 60 * 24 - const { REPOSITORY, ORGANIZATION, PROJECT_NUMBER } = process.env - - const issues = [] - - const determineDateRange = () => { - const inputStartDate = '${{ inputs.startDate }}' - const inputEndDate = '${{ inputs.endDate }}' - - if (inputStartDate && inputEndDate) { - return { startDate: inputStartDate, endDate: inputEndDate } - } - - if (inputStartDate || inputEndDate) { - core.setFailed('Both startDate and endDate are required if one is provided.') - } - - const startDate = new Date() - - startDate.setDate(startDate.getDate() - 6) - - return { startDate: startDate.toISOString().split('T')[0], endDate: (new Date()).toISOString().split('T')[0] } - } - - const dateRange = determineDateRange() - const query = `is:issue+repo:${ORGANIZATION}/${REPOSITORY}+project:${ORGANIZATION}/${PROJECT_NUMBER}+created:${dateRange.startDate}..${dateRange.endDate}` - - const findLabelDateTime = async (issueNumber) => { - const iterator = github.paginate.iterator(github.rest.issues.listEventsForTimeline, { - owner: ORGANIZATION, - repo: REPOSITORY, - issue_number: issueNumber, - }) - - for await (const { data: timelineData } of iterator) { - for (const timelineItem of timelineData) { - if (timelineItem.event === 'labeled' && ROUTED_TO_LABELS.includes(timelineItem.label.name)) { - return timelineItem.created_at - } - } - } - } - - const calculateElapsedDays = (createdAt, routedOrClosedAt) => { - return Math.round((new Date(routedOrClosedAt) - new Date(createdAt)) / MS_PER_DAY, 0) - } - - const iterator = github.paginate.iterator(github.rest.search.issuesAndPullRequests, { - q: query, - per_page: 100, - }) - - for await (const { data } of iterator) { - for (const issue of data) { - let routedOrClosedAt - - if (!issue.pull_request) { - const routedLabel = issue.labels.find((label) => ROUTED_TO_LABELS.includes(label.name)) - - if (routedLabel) { - routedOrClosedAt = await findLabelDateTime(issue.number) - } else if (issue.state === 'closed') { - routedOrClosedAt = issue.closed_at - } - - let elapsedDays - - if (routedOrClosedAt) { - elapsedDays = calculateElapsedDays(issue.created_at, routedOrClosedAt) - } - - issues.push({ - number: issue.number, - title: issue.title, - state: issue.state, - url: issue.html_url, - createdAt: issue.created_at, - routedOrClosedAt, - elapsedDays, - }) - } - } - } - - const issuesRoutedOrClosedIn7Days = issues.filter((issue) => issue.elapsedDays <= 7).length - const percentage = Number(issues.length > 0 ? issuesRoutedOrClosedIn7Days / issues.length : 0).toLocaleString(undefined, { style: 'percent', minimumFractionDigits: 2 }) - - console.log(`Triage Metrics (${dateRange.startDate} - ${dateRange.endDate})`) - console.log('Total issues:', issues.length) - console.log(`Issues routed/closed within 7 days: ${issuesRoutedOrClosedIn7Days} (${percentage})`) diff --git a/.github/workflows/triage_update_status.yml b/.github/workflows/triage_update_status.yml new file mode 100644 index 00000000000..b71b0daf077 --- /dev/null +++ b/.github/workflows/triage_update_status.yml @@ -0,0 +1,97 @@ +name: 'Set Issue Status' +on: + # makes this workflow reusable + workflow_call: + inputs: + status: + description: 'Which status column?' + default: 'New Issue' + required: false + type: string + secrets: + ADD_TO_TRIAGE_BOARD_TOKEN: + required: true + +jobs: + move-to-requested-status: + runs-on: ubuntu-latest + steps: + - name: Get project data + env: + STATUS: ${{ inputs.status || 'New Issue' }} + GITHUB_TOKEN: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} + ORGANIZATION: 'cypress-io' + REPOSITORY: ${{ github.event.repository.name }} + PROJECT_NUMBER: 9 + ISSUE_NUMBER: ${{ github.event.issue.number }} + + run: | + gh api graphql -f query=' + query($org: String!, $repo: String!, $project: Int!, $issue: Int!) { + organization(login: $org) { + repository(name: $repo) { + issue(number: $issue) { + projectItems(first: 10, includeArchived: false) { + nodes { + id + fieldValueByName(name: "Status") { + ... on ProjectV2ItemFieldSingleSelectValue { + name + field { + ... on ProjectV2SingleSelectField { + project { + ... on ProjectV2 { + id + number + } + } + } + } + } + } + } + } + } + } + projectV2(number: $project) { + field(name: "Status") { + ... on ProjectV2SingleSelectField { + id + options { + id + name + } + } + } + } + } + }' -f org=$ORGANIZATION -f repo=$REPOSITORY -F issue=$ISSUE_NUMBER -F project=$PROJECT_NUMBER > project_data.json + + echo 'PROJECT_ID='$(jq -r '.data.organization.repository.issue.projectItems.nodes[].fieldValueByName.field.project | select(.number == ${{ env.PROJECT_NUMBER }}) | .id' project_data.json) >> $GITHUB_ENV + echo 'PROJECT_ITEM_ID='$(jq -r '.data.organization.repository.issue.projectItems.nodes[] | select(.fieldValueByName.field.project.number == ${{ env.PROJECT_NUMBER }}) | .id' project_data.json) >> $GITHUB_ENV + echo 'STATUS_FIELD_ID='$(jq -r '.data.organization.projectV2.field | .id' project_data.json) >> $GITHUB_ENV + echo 'STATUS_OPTION_ID='$(jq -r '.data.organization.projectV2.field.options[] | select(.name== "${{ env.STATUS }}") | .id' project_data.json) >> $GITHUB_ENV + - name: Move issue to new status + env: + GITHUB_TOKEN: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} + run: | + gh api graphql -f query=' + mutation ( + $project: ID! + $item: ID! + $status_field: ID! + $status_value: String! + ) { + updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $status_field + value: { + singleSelectOptionId: $status_value + } + }) { + projectV2Item { + id + } + } + }' -f project=$PROJECT_ID -f item=$PROJECT_ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=$STATUS_OPTION_ID diff --git a/.github/workflows/update-browser-versions.yml b/.github/workflows/update-browser-versions.yml index 0d73441e469..330464273fb 100644 --- a/.github/workflows/update-browser-versions.yml +++ b/.github/workflows/update-browser-versions.yml @@ -1,5 +1,7 @@ name: Update Browser Versions on: + workflow_dispatch: + schedule: - cron: '0 8 * * *' # every day at 8am UTC (3/4am EST/EDT) jobs: @@ -8,12 +10,14 @@ jobs: env: CYPRESS_BOT_APP_ID: ${{ secrets.CYPRESS_BOT_APP_ID }} BASE_BRANCH: develop + # Prevent from running this workflow on forks + if: github.repository == 'cypress-io/cypress' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} - name: Set committer info ## attribute the commit to cypress-bot: https://github.community/t/logging-into-git-as-a-github-app/115916 run: | @@ -24,12 +28,12 @@ jobs: git fetch origin git checkout ${{ env.BASE_BRANCH }} - name: Set up Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 18 - name: Check for new Chrome versions id: get-versions - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: script: | const { getVersions } = require('./scripts/github-actions/update-browser-versions.js') @@ -40,13 +44,13 @@ jobs: env: BRANCH_NAME: update-chrome-stable-from-${{ steps.get-versions.outputs.current_stable_version }}-beta-from-${{ steps.get-versions.outputs.current_beta_version }} run: | - echo "::set-output name=branch_name::${{ env.BRANCH_NAME }}" - echo "::set-output name=branch_exists::$(git show-ref --verify --quiet refs/remotes/origin/${{ env.BRANCH_NAME }} && echo 'true')" + echo "branch_name=${{ env.BRANCH_NAME }}" >> $GITHUB_OUTPUT + echo "branch_exists=$(git show-ref --verify --quiet refs/remotes/origin/${{ env.BRANCH_NAME }} && echo 'true')" >> $GITHUB_OUTPUT - name: Check need for PR or branch update id: check-need-for-pr run: | - echo "::set-output name=needs_pr::${{ steps.get-versions.outputs.has_update == 'true' && steps.check-branch.outputs.branch_exists != 'true' }}" - echo "::set-output name=needs_branch_update::${{ steps.get-versions.outputs.has_update == 'true' && steps.check-branch.outputs.branch_exists == 'true' }}" + echo "needs_pr=${{ steps.get-versions.outputs.has_update == 'true' && steps.check-branch.outputs.branch_exists != 'true' }}" >> $GITHUB_OUTPUT + echo "needs_branch_update=${{ steps.get-versions.outputs.has_update == 'true' && steps.check-branch.outputs.branch_exists == 'true' }}" >> $GITHUB_OUTPUT ## Update available and a branch/PR already exists - name: Checkout existing branch if: ${{ steps.check-need-for-pr.outputs.needs_branch_update == 'true' }} @@ -54,7 +58,7 @@ jobs: - name: Check need for update on existing branch if: ${{ steps.check-need-for-pr.outputs.needs_branch_update == 'true' }} id: check-need-for-branch-update - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: script: | const { checkNeedForBranchUpdate } = require('./scripts/github-actions/update-browser-versions.js') @@ -71,7 +75,7 @@ jobs: ## Both - name: Update Browser Versions File if: ${{ steps.check-need-for-pr.outputs.needs_pr == 'true' || steps.check-need-for-branch-update.outputs.has_newer_update == 'true' }} - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: script: | const { updateBrowserVersionsFile } = require('./scripts/github-actions/update-browser-versions.js') @@ -90,7 +94,7 @@ jobs: ## Update available and a branch/PR already exists - name: Update PR Title if: ${{ steps.check-need-for-pr.outputs.needs_branch_update == 'true' }} - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: script: | const { updatePRTitle } = require('./scripts/github-actions/update-browser-versions.js') @@ -104,11 +108,12 @@ jobs: }) # Update available and a PR doesn't already exist - name: Create Pull Request + id: create-pr if: ${{ steps.check-need-for-pr.outputs.needs_pr == 'true' }} - uses: actions/github-script@v4 + uses: actions/github-script@v7 with: script: | - const { createPullRequest } = require('./scripts/github-actions/update-browser-versions.js') + const { createPullRequest } = require('./scripts/github-actions/create-pull-request.js') await createPullRequest({ context, @@ -116,4 +121,6 @@ jobs: baseBranch: '${{ env.BASE_BRANCH }}', branchName: '${{ steps.check-branch.outputs.branch_name }}', description: '${{ steps.get-versions.outputs.description }}', + body: 'This PR was auto-generated to update the version(s) of Chrome for driver tests', + addToProjectBoard: true, }) diff --git a/.github/workflows/update_v8_snapshot_cache.yml b/.github/workflows/update_v8_snapshot_cache.yml new file mode 100644 index 00000000000..fc36072a260 --- /dev/null +++ b/.github/workflows/update_v8_snapshot_cache.yml @@ -0,0 +1,158 @@ +name: Update V8 Snapshot Cache +on: + schedule: + # Run everyday except Wednesday at 00:00 UTC + - cron: '0 0 * * 0,1,2,4,5,6' + # Run every Wednesday at 00:00 UTC + - cron: '0 0 * * 3' + push: + branches: + - 'release/**' + paths-ignore: + - .husky/** + - .vscode/** + - .eslintrc.js + - .gitattributes + - .gitignore + - .percy.yml + - .prettierignore + - .releaserc.js + - .yarnclean + - CHANGELOG.md + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - LICENSE + - README.md + - ROADMAP.md + - SECURITY.md + workflow_dispatch: + inputs: + branch: + description: 'Branch to update' + required: true + default: 'develop' + generate_from_scratch: + description: 'Generate from scratch' + type: boolean + default: false + commit_directly_to_branch: + description: 'Commit directly to branch' + type: boolean + default: false +concurrency: + group: ${{ inputs.branch || github.ref }} + cancel-in-progress: true +jobs: + update-v8-snapshot-cache: + strategy: + max-parallel: 1 + matrix: + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + env: + CYPRESS_BOT_APP_ID: ${{ secrets.RAM_APP }} + BASE_BRANCH: ${{ inputs.branch || github.ref_name }} + # Flex the generate from scratch option based on manual input or if we are on the weekly schedule + GENERATE_FROM_SCRATCH: ${{ inputs.generate_from_scratch == true || (github.event_name == 'schedule' && github.event.schedule == '0 0 * * 3') }} + steps: + - name: Determine snapshot files - Windows + if: ${{ matrix.platform == 'windows-latest' }} + run: echo "SNAPSHOT_FILES='tooling\v8-snapshot\cache\win32\snapshot-meta.json'" >> $GITHUB_ENV + shell: bash + - name: Determine snapshot files - Linux + if: ${{ matrix.platform == 'ubuntu-latest' }} + run: echo "SNAPSHOT_FILES='tooling/v8-snapshot/cache/linux/snapshot-meta.json'" >> $GITHUB_ENV + - name: Determine snapshot files - Mac + if: ${{ matrix.platform == 'macos-latest' }} + run: echo "SNAPSHOT_FILES='tooling/v8-snapshot/cache/darwin/snapshot-meta.json'" >> $GITHUB_ENV + - name: Install setuptools - Mac + if: ${{ matrix.platform == 'macos-latest' }} + run: sudo -H pip install setuptools + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} + ref: ${{ env.BASE_BRANCH }} + - name: Set committer info + ## attribute the commit to cypress-bot: https://github.community/t/logging-into-git-as-a-github-app/115916 + run: | + git config --local user.email "${{ env.CYPRESS_BOT_APP_ID }}+cypress-bot[bot]@users.noreply.github.com" + git config --local user.name "cypress-bot[bot]" + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: 'yarn' + - name: Run yarn + run: yarn + - name: Run build + run: yarn build + - name: Generate prod snapshot from scratch + if: ${{ env.GENERATE_FROM_SCRATCH == 'true' }} + run: yarn cross-env V8_SNAPSHOT_FROM_SCRATCH=1 V8_UPDATE_METAFILE=1 yarn build-v8-snapshot-prod + - name: Generate prod snapshot iteratively + if: ${{ env.GENERATE_FROM_SCRATCH != 'true' }} + run: yarn cross-env V8_UPDATE_METAFILE=1 yarn build-v8-snapshot-prod + - name: Check for v8 snapshot cache changes + id: check-for-v8-snapshot-cache-changes + run: | + echo "has_changes=$(test "$(git status --porcelain -- ${{ env.SNAPSHOT_FILES }})" && echo 'true')" >> $GITHUB_OUTPUT + shell: bash + - name: Determine branch name - commit directly to branch + if: ${{ inputs.commit_directly_to_branch == true }} + run: | + echo "BRANCH_NAME=${{ env.BASE_BRANCH }}" >> $GITHUB_ENV + echo "BRANCH_EXISTS=true" >> $GITHUB_ENV + shell: bash + - name: Determine branch name - commit to separate branch + if: ${{ inputs.commit_directly_to_branch != true }} + run: | + echo "BRANCH_NAME=update-v8-snapshot-cache-on-${{ env.BASE_BRANCH }}" >> $GITHUB_ENV + echo "BRANCH_EXISTS=$(git show-ref --verify --quiet refs/remotes/origin/update-v8-snapshot-cache-on-${{ env.BASE_BRANCH }} && echo 'true')" >> $GITHUB_ENV + shell: bash + - name: Check need for PR or branch update + id: check-need-for-pr + run: | + echo "needs_pr=${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS != 'true' }}" >> $GITHUB_OUTPUT + echo "needs_branch_update=${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS == 'true' }}" >> $GITHUB_OUTPUT + shell: bash + ## Update available and a branch/PR already exists + - name: Checkout existing branch + if: ${{ steps.check-need-for-pr.outputs.needs_branch_update == 'true' }} + run: | + git stash push -- ${{ env.SNAPSHOT_FILES }} + git reset --hard + git checkout ${{ env.BRANCH_NAME }} + git pull origin ${{ env.BRANCH_NAME }} + git merge --squash -Xtheirs stash + ## Update available and a PR doesn't already exist + - name: Checkout new branch + if: ${{ steps.check-need-for-pr.outputs.needs_pr == 'true' }} + run: git checkout -b ${{ env.BRANCH_NAME }} ${{ env.BASE_BRANCH }} + ## Commit changes if present + - name: Commit the changes + if: ${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' }} + run: | + git diff-index --quiet HEAD || git commit -am "chore: updating v8 snapshot cache" + ## Push branch + - name: Push branch to remote + if: ${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' }} + run: git push origin ${{ env.BRANCH_NAME }} + # PR needs to be created + - name: Create Pull Request + if: ${{ steps.check-need-for-pr.outputs.needs_pr == 'true' }} + uses: actions/github-script@v7 + with: + script: | + const { createPullRequest } = require('./scripts/github-actions/create-pull-request.js') + + await createPullRequest({ + context, + github, + baseBranch: '${{ env.BASE_BRANCH }}', + branchName: '${{ env.BRANCH_NAME }}', + description: 'Update v8 snapshot cache', + body: 'This PR was automatically generated by the [update-v8-snapshot-cache](https://github.com/cypress-io/cypress/actions/workflows/update_v8_snapshot_cache.yml) github action.', + reviewers: ['ryanthemanuel'] + }) diff --git a/.github/workflows/upload_release_asset.yml b/.github/workflows/upload_release_asset.yml index 1934d2fc22b..b089f42a480 100644 --- a/.github/workflows/upload_release_asset.yml +++ b/.github/workflows/upload_release_asset.yml @@ -13,9 +13,11 @@ jobs: FOSSA_API_KEY: ${{secrets.FOSSAAPIKEY}} repo-token: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + permissions: + contents: write steps: - name: Check out repository code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download Fossa binary and install # This step utilizes a curl of the latest install pkg from Fossa. Using a git action from # Fossa doesn't produce the SBOM artifact needed. This manual approach is the only way to diff --git a/.gitignore b/.gitignore index 5f8a277767c..cb6f213a1c8 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,9 @@ system-tests/lib/fixtureDirs.ts # from npm/webpack-dev-server /npm/webpack-dev-server/cypress/videos +# from npm/grep +/npm/grep/cypress/videos + # from errors /packages/errors/__snapshot-images__ /packages/errors/__snapshot-md__ @@ -89,7 +92,7 @@ system-tests/lib/fixtureDirs.ts /packages/frontend-shared/src/generated /packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts -# from npm/create-cypress-tests +# from old npm/create-cypress-tests /npm/create-cypress-tests/initial-template /npm/create-cypress-tests/src/test-output @@ -112,6 +115,9 @@ cli/visual-snapshots # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +# Fleet +.fleet/ + # User-specific stuff .idea .idea/**/workspace.xml @@ -377,3 +383,18 @@ globbed_node_modules # Autogenerated files, typically from graphql-code-generator *.gen.ts *.gen.json + +# Snapshot Binaries +snapshot_blob.bin +v8_context_snapshot.x86_64.bin + +# Legacy snapshot cache files +tooling/v8-snapshot/cache/dev-darwin +tooling/v8-snapshot/cache/dev-linux +tooling/v8-snapshot/cache/dev-win32 +tooling/v8-snapshot/cache/prod-darwin +tooling/v8-snapshot/cache/prod-linux +tooling/v8-snapshot/cache/prod-win32 + +# Cloud API validations +system-tests/lib/validations diff --git a/.node-version b/.node-version index a1fe1878845..4a1f488b6c3 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -16.14.2 \ No newline at end of file +18.17.1 diff --git a/.releaserc.base.js b/.releaserc.base.js deleted file mode 100644 index 95f6bbf9400..00000000000 --- a/.releaserc.base.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - plugins: [ - '@semantic-release/commit-analyzer', - '@semantic-release/release-notes-generator', - ['@semantic-release/changelog', { - changelogFile: 'CHANGELOG.md', - }], - ['@semantic-release/git', { - assets: [ - './CHANGELOG.md', - ], - message: 'chore: release ${nextRelease.gitTag}\n\n[skip ci]', - }], - '@semantic-release/npm', - ], - extends: 'semantic-release-monorepo', - branches: [ - 'master', - ], -} diff --git a/.releaserc.js b/.releaserc.js index 4025bd1ff58..77b55505f85 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,7 +1,31 @@ +const { parserOpts, releaseRules } = require('./scripts/semantic-commits/change-categories') + module.exports = { - ...require('./.releaserc.base'), + plugins: [ + ['@semantic-release/commit-analyzer', { + preset: 'angular', + parserOpts, + releaseRules, + }], + ['@semantic-release/release-notes-generator', + { + preset: 'angular', + parserOpts, + } + ], + ['@semantic-release/changelog', { + changelogFile: 'CHANGELOG.md', + }], + ['@semantic-release/git', { + assets: [ + './CHANGELOG.md', + ], + message: 'chore: release ${nextRelease.gitTag}\n\n[skip ci]', + }], + '@semantic-release/npm', + ], + extends: 'semantic-release-monorepo', branches: [ - 'master', - { name: 'chore/webpack-5', channel: 'channel-next' }, + { name: 'develop', channel: 'latest' }, ], } diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 5649488074b..bc6c6cdb3f6 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -8,21 +8,29 @@ "composables", "dedup", "ERRORED", + "esbuild", "execa", "Fetchable", "Fetchables", "forcedefault", "getenv", + "GIBIBYTES", "graphcache", "headlessui", "Iconify", "intlify", + "KIBIBYTE", + "kibibytes", "Lachlan", "loggedin", + "mksnapshot", "msapplication", + "norewrite", "NOTESTS", "OVERLIMIT", "overscan", + "packherd", + "pidusage", "Pinia", "pnpm", "pseudoclass", @@ -30,22 +38,29 @@ "Screenshotting", "semibold", "shiki", + "snapbuild", + "snapgen", + "snapshottable", + "snapshotted", + "snapshotting", + "sourcemaps", "speclist", + "systeminformation", "testid", "TIMEDOUT", "titleize", "topnav", "unconfigured", "unplugin", + "unref", "unrunnable", "unstaged", "urql", "viewports", "vite", "vitejs", - "vueuse", - "Windi" + "vueuse" ], "ignoreWords": [], "import": [] -} \ No newline at end of file +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 845b2fc5567..c28ee62bcb0 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -22,9 +22,9 @@ // Description: Adds syntax highlighting for all gql tags. "apollographql.vscode-apollo", - // Name: WindiCSS Intellisense - // Description: Automatically sorts your WindiCSS classes. - "voorjaar.windicss-intellisense", + // Name: TailwindCSS Intellisense + // Description: Automatically sorts your TailwindCSS classes. + "bradlc.vscode-tailwindcss", // Name: Volar // Description: Language server for Vue. Required for any syntax highlighting in Vue files. diff --git a/.vscode/launch.json b/.vscode/launch.json index ff2618c4bf7..2142cdfd3ce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,6 +8,21 @@ "processId": "${command:PickProcess}", "continueOnAttach": true }, + { + "type": "node", + "request": "attach", + "name": "Attach to port 5566", + "port": 5566, + "continueOnAttach": true, + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Docker", + "port": 5566, + "continueOnAttach": true, + "remoteRoot": "/opt/cypress", + }, { "type": "node", "request": "attach", diff --git a/.vscode/settings.json b/.vscode/settings.json index 47436c0f111..3e90e64819c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,13 +19,10 @@ "json" ], "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib", - // A flag that controls whether or not Windi CSS classes will be sorted on save on save. - "windicss.sortOnSave": true, - // Support autocompletion and preview of strings. // Additionally, support extraction of hardcoded strings into key-values. "i18n-ally.localesPaths": "packages/frontend-shared/src/locales", diff --git a/.yarnclean b/.yarnclean new file mode 100644 index 00000000000..1b0696546cd --- /dev/null +++ b/.yarnclean @@ -0,0 +1,50 @@ +# test directories +__tests__ +test +tests +powered-test + +# yaml package has a `doc` folder that we need +!yaml/**/doc/* +website +images +assets +!mochawesome-report-generator/dist/assets + +# Do NOT clean out app assets from cypress-example-kitchensink to avoid broken deployments +# Needed for https://example.cypress.io/ +!cypress-example-kitchensink/app/assets + +# examples +example +!@packages/example +examples + +# code coverage directories +coverage +.nyc_output + +# build scripts +Makefile +Gulpfile.js +Gruntfile.js + +# configs +appveyor.yml +circle.yml +codeship-services.yml +codeship-steps.yml +wercker.yml +.tern-project +.gitattributes +.editorconfig +.*ignore +.eslintrc +.jshintrc +.flowconfig +.documentup.json +.yarn-metadata.json +.travis.yml + +# misc +*.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a007ead1b9..c387bf62a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,16 @@ -https://on.cypress.io/changelog +# Changelogs + +- [Cypress App](https://on.cypress.io/changelog) +- [`@cypress/angular`](https://github.com/cypress-io/cypress/blob/develop/npm/angular/CHANGELOG.md) +- [`@cypress/angular-signals`](https://github.com/cypress-io/cypress/blob/develop/npm/angular-signals/CHANGELOG.md) +- [`@cypress/eslint-plugin-dev`](https://github.com/cypress-io/cypress/blob/develop/npm/eslint-plugin-dev/CHANGELOG.md) +- [`@cypress/mount-utils`](https://github.com/cypress-io/cypress/blob/develop/npm/mount-utils/CHANGELOG.md) +- [`@cypress/react`](https://github.com/cypress-io/cypress/blob/develop/npm/react/CHANGELOG.md) +- [`@cypress/react18`](https://github.com/cypress-io/cypress/blob/develop/npm/react18/CHANGELOG.md) +- [`@cypress/svelte`](https://github.com/cypress-io/cypress/blob/develop/npm/svelte/CHANGELOG.md) +- [`@cypress/vite-dev-server`](https://github.com/cypress-io/cypress/blob/develop/npm/vite-dev-server/CHANGELOG.md) +- [`@cypress/vue`](https://github.com/cypress-io/cypress/blob/develop/npm/vue/CHANGELOG.md) +- [`@cypress/vue2`](https://github.com/cypress-io/cypress/blob/develop/npm/vue2/CHANGELOG.md) +- [`@cypress/webpack-batteries-included-preprocessor`](https://github.com/cypress-io/cypress/blob/develop/npm/webpack-batteries-included-preprocessor/CHANGELOG.md) +- [`@cypress/webpack-dev-server`](https://github.com/cypress-io/cypress/blob/develop/npm/webpack-dev-server/CHANGELOG.md) +- [`@cypress/webpack-preprocessor`](https://github.com/cypress-io/cypress/blob/develop/npm/webpack-preprocessor/CHANGELOG.md) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index cb65de563b2..2e9de105357 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,7 +2,7 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards @@ -20,13 +20,13 @@ Examples of unacceptable behavior by participants include: * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +* Other conduct that could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c81b48eb623..b48e83f6f3b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,8 @@ Thanks for taking the time to contribute! :smile: **Once you learn how to use Cypress, you can contribute in many ways:** -- Join the [Cypress Discord](https://on.cypress.io/discord) and answer questions. Teaching others how to use Cypress is a great way to learn more about how it works. -- Blog about Cypress. We display blogs featuring Cypress on our [Examples](https://on.cypress.io/examples) page. If you'd like your blog featured, [open a PR to add it to our docs](https://github.com/cypress-io/cypress-documentation/blob/develop/CONTRIBUTING.md#adding-examples). +- Join the [Cypress Discord](https://on.cypress.io/chat) and answer questions. Teaching others how to use Cypress is a great way to learn more about how it works. +- Blog about Cypress. We display blogs featuring Cypress on our [Examples](https://on.cypress.io/examples) page. If you'd like your blog featured, [open a PR to add it to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#adding-examples). - Write some documentation or improve our existing docs. See our [guide to contributing to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). - Give a talk about Cypress. [Contact us](mailto:support@cypress.io) ahead of time and we'll send you some swag. :shirt: @@ -20,7 +20,6 @@ Thanks for taking the time to contribute! :smile: - [Code of Conduct](#code-of-conduct) - [Opening Issues](#opening-issues) -- [Triaging Issues](#triaging-issues) - [Writing Documentation](#writing-documentation) - [Writing Code](#writing-code) - [What you need to know before getting started](#what-you-need-to-know-before-getting-started) @@ -43,7 +42,7 @@ Thanks for taking the time to contribute! :smile: ## Code of Conduct -All contributors are expecting to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). +All contributors are expected to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). ## Opening Issues @@ -111,134 +110,11 @@ test execution | Running tests inside a single spec | [open](https://github.com/ typescript | Transpiling or bundling TypeScript | [open](https://github.com/cypress-io/cypress/labels/topic%3A%20typescript), [closed](https://github.com/cypress-io/cypress/issues?q=label%3A%22topic%3A+typescript%22+is%3Aclosed) video | Problems with video recordings | [open](https://github.com/cypress-io/cypress/labels/topic%3A%20video%20%F0%9F%93%B9), [closed](https://github.com/cypress-io/cypress/issues?q=label%3A%22topic%3A+video+%F0%9F%93%B9%22+is%3Aclosed) -## Triaging Issues - -When an issue is opened in [cypress](https://github.com/cypress-io/cypress), we need to evaluate the issue to determine what steps should be taken next. So, when approaching new issues, there are some steps that should be taken. - -### Is this a question? - -Some opened issues are questions, not bug reports or feature requests. Issues are reserved for potential bugs or feature requests *only*. If this is the case, you should: - -- Explain that issues in our GitHub repo are reserved for potential bugs or feature requests and that the issue will be closed since it appears to be neither a bug nor a feature request. -- Guide them to existing resources where their questions can be asked like our [Discussions](https://github.com/cypress-io/cypress/discussions), [community chat](https://on.cypress.io/chat), [Discord](https://on.cypress.io/discord), or [Stack Overflow](https://stackoverflow.com/questions/tagged/cypress). -- Cypress offers support via email when signing up for any of our [paid plans](https://www.cypress.io/pricing/), so remind them that this is an option if they already have a paid account. -- Move the issue to [Discussions](https://github.com/cypress-io/cypress/discussions). - -### Does this issue belong in this repository? - -#### Other open source repos - -Issues may be opened about wanting changes to our [documentation](https://github.com/cypress-io/cypress-documentation), our [example-kitchensink app](https://github.com/cypress-io/cypress-example-kitchensink), or [another repository](https://github.com/cypress-io). In this case you should: - -- Thank them for their contribution. -- Explain that this repo is only for bugs or feature requests of the Cypress App. -- If you have permission to 'Transfer the issue', do so. If not, explain that they can open an issue in our other repository and link to the repository. -- Close the issue (if not already transferred). - -#### Cypress Dashboard - -Issues may be opened about wanting features in our Dashboard Service. In this case you should: - -- Thank them for opening an issue. -- Add the `external: dashboard` label. - -#### Component Testing - -Issues may be opened about wanting features in Component Testing. In this case you should: - -- Thank them for opening an issue. -- Add the `component testing` label. - -### Is this already an open issue? - -Search [all issues](https://github.com/cypress-io/cypress/issues) for keywords from the issue to ensure there isn't already an issue open for this. GitHub has some [search tips](https://help.github.com/articles/searching-issues-and-pull-requests/) that may help you better find the relevant issue. - -If an issue already exists you should: - -- Thank them for their contribution. -- Explain that this issue is a duplicate of another issue, linking to the relevant issue (`#1234`). -- Add the `type: duplicate` label to the issue. -- Close the issue. - -### Does the issue provide all the information from our issue template? - -When opening an issue, there is a provided issue template based on the type of issue. If the opened issue does not provide enough information asked from the issue template you should: - -- Explain that we require new issues follow our provided issue template and that issues that are opened without this information are automatically closed per our [contributing guidelines](#fill-out-our-issue-template). -- Close the issue. - -### Are they running the current version of Cypress? - -If they listed an older version of Cypress in their issue. We don't want to spend the time to set up a reproducible project (which can be time consuming) only to find that bumping the Cypress version fixes it. You should: - -- Ask them to update to the newest version of Cypress and comment about the results. -- Add the `stage: awaiting response` label to the issue. - -### Is the fix or feature within our vision for Cypress? - -There will inevitably be suggestions that will not fit within the scope of Cypress's vision for our product. If an issue or pull request falls under this category you should: - -- Thank them for their contribution. -- Explain why it doesn't fit into the scope at Cypress, and offer clear suggestions for improvement, if you're able. Be kind, but firm. -- Link to relevant documentation, if there is any. If you notice repeated requests for things that are not within scope, add them into the [documentation](https://github.com/cypress-io/cypress-documentation) to avoid repeating yourself. -- Add the `stage: wontfix` label to the issue. -- Close the issue/pull request. - -### Is what they're describing actually happening? - -The best way to determine the validity of a bug is to recreate it yourself. Follow the directions or information provided to recreate the bug that is described. Did they provide a repository that demonstrates the bug? Great - fork it and run the project and steps required. If they didn't provide a repository, the best way to reproduce the issue is to have a 'sandbox' project up and running locally for Cypress. This is just a simple project with Cypress installed where you can freely edit the application under test and the tests themselves to recreate the problem. - -**Attempting to recreate the bug will lead to a few scenarios:** - -#### 1. You can't recreate the bug - - If you can't recreate the situation happening you should: - -- Thank them for their contribution. -- Explain that there isn't enough information to reproduce the bug. Provide information on how you went about recreating the scenario, if you're able. Note your OS, Browser, Cypress version and any other information. -- Note that if no reproducible example is provided, we will unfortunately have to close the issue. -- Add the `stage: needs information` label to the issue. - -#### 2. You can recreate the bug - -If you can recreate the bug you should: - -- Thank them for their contribution. -- Explain that you're able to recreate the bug. Provide the exact test code ran and the versions of Cypress, OS, and browser you used to recreate it. -- If you know where the code is that could possibly fix this issue - link to the file or line of code from the [cypress](https://github.com/cypress-io/cypress) repo and remind the user that we are open source and that we gladly accept PRs, even if they are a work in progress. -- Add the `stage: ready for work` label to the issue. - -#### 3. You can tell the problem is a user error - -In recreating the issue, you may realize that they had a typo or used the Cypress API incorrectly, etc. In this case you should: - -- Leave a comment informing the user of their error. -- Link to relevant documentation, if there is any. If you notice repeated user errors for the same situation, add them into the [documentation](https://github.com/cypress-io/cypress-documentation) to avoid repeating yourself. -- Close the issue. - -### Has the issue gone stale? - -Some issues are opened and sadly forgotten about by the person originally opening the issue. - -#### Not enough information ever provided - -Sometimes we request more information to be provided (label `stage: needs information`) for an open issue, but no one is able to provide a reproducible example or they simply never respond. **This does not mean that we don't believe that there is a bug!** We just, unfortunately, don't have a path forward to fix it without this information. In this case you should: - -- Add a comment reminding them or our request for more information and that the issue will be closed if it is not provided. Sometimes issues get forgotten about, and all the person needs is a gentle reminder. -- If there is still no response after a weeks time, explain that you are closing the issue due to not enough information or inactivity and that they can comment in the issue with a reproducible example and we will reopen the issue. -- Close the issue. - -#### They already solved their issue - -Some issues are resolved by the community, by giving some guidance or a workaround, but the original opener of the issue forgets to close the issue. In this case you should: - -- Explain that you are closing the issue as resolved and that they can comment if they are still having the issue and we will consider reopening it. -- Close the issue. ## Writing Documentation Cypress documentation lives in a separate repository with its own dependencies and build tools. -See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). +See [Documentation Contributing Guidelines](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). ## Writing code @@ -252,29 +128,53 @@ Cypress is a large open source project. When you want to contribute to Cypress, Cypress uses a monorepo, which means there are many independent packages in this repository. There are two main types of packages: private and public. -Private packages generally live within the [`packages`](./packages) directory and are in the `@packages/` namespace. These packages are combined to form the main Cypress app that you get when you `npm install cypress`. They are discrete modules with different responsibilities, but each is necessary for the Cypress app and is not necessarily useful outside of the Cypress app. Since these modules are all compiled and bundled into a binary upon release, they are sometimes collectively referred to as the Cypress binary. +Private packages included in the app generally live within the [`packages`](./packages) directory and are in the `@packages/` namespace. These packages are combined to form the main Cypress app that you get when you `npm install cypress`. They are discrete modules with different responsibilities, but each is necessary for the Cypress app and is not necessarily useful outside of the Cypress app. Since these modules are all compiled and bundled into a binary upon release, they are sometimes collectively referred to as the Cypress binary. Here is a list of the core packages in this repository with a short description, located within the [`packages`](./packages) directory: | Folder Name | Package Name | Purpose | | :------------------------------------ | :---------------------- | :--------------------------------------------------------------------------- | | [cli](./cli) | `cypress` | The command-line tool that is packaged as an `npm` module. | + | [app](./packages/app) | `@packages/app` | The front-end for the Cypress App that renders in the launched browser instance. | + | [config](./packages/config) | `@packages/config` | The Cypress configuration types and validation used in the server, data-context and driver. | + | [data-context](./packages/data-context) | `@packages/data-context` | Centralized data access for the Cypress application. | | [driver](./packages/driver) | `@packages/driver` | The code that is used to drive the behavior of the API commands. | | [electron](./packages/electron) | `@packages/electron` | The Cypress implementation of Electron. | + | [errors](./packages/errors) | `@packages/errors` | Error definitions and utilities for Cypress | | [example](./packages/example) | `@packages/example` | Our example kitchen-sink application. | | [extension](./packages/extension) | `@packages/extension` | The Cypress Chrome browser extension | + | [frontend-shared](./packages/frontend-shared) | `@packages/frontend-shared` | Shared components and styles used in the `app` and `launchpad`. | + | [graphql](./packages/graphql) | `@packages/graphql` | The GraphQL layer that the `launchpad` and `app` use to interact with the `server`. | | [https-proxy](./packages/https-proxy) | `@packages/https-proxy` | This does https proxy for handling http certs and traffic. | + | [icons](./packages/icons) | `@packages/icons` | The Cypress icons. | + | [launcher](./packages/launcher) | `@packages/launcher` | Finds and launches browsers installed on your system. | + | [launchpad](./packages/launchpad) | `@packages/launcher` | The portal to running Cypress that displays in `open` mode. | | [net-stubbing](./packages/net-stubbing) | `@packages/net-stubbing` | Contains server side code for Cypress' network stubbing features. | | [network](./packages/network) | `@packages/network` | Various utilities related to networking. | + | [packherd-require](./packages/packherd-require) | `@packages/packherd-require` | Loads modules that have been bundled by `@tooling/packherd`. | | [proxy](./packages/proxy) | `@packages/proxy` | Code for Cypress' network proxy layer. | - | [launcher](./packages/launcher) | `@packages/launcher` | Finds and launches browsers installed on your system. | | [reporter](./packages/reporter) | `@packages/reporter` | The reporter shows the running results of the tests (The Command Log UI). | + | [resolve-dist](./packages/resolve-dist) | `@packages/resolve-dist` | Centralizes the resolution of paths to compiled/static assets from server-side code.. | + | [rewriter](./packages/rewriter) | `@packages/rewriter` | The logic to rewrite JS and HTML that flows through the Cypress proxy. | [root](./packages/root) | `@packages/root` | Dummy package pointing at the root of the repository. | - | [runner](./packages/runner) | `@packages/runner` | The runner is the minimal "chrome" around the user's application under test. | + | [runner](./packages/runner) | `@packages/runner` | (deprecated) The runner is the minimal "chrome" around the user's application under test. | + | [scaffold-config](./packages/scaffold-config) | `@packages/scaffold-config` | The logic related to scaffolding new projects using launchpad. | | [server](./packages/server) | `@packages/server` | The <3 of Cypress. This orchestrates everything. The backend node process. | - | [server-ct](./packages/server-ct) | `@packages/server-ct` | Some Component Testing specific overrides. Mostly extends functionality from `@packages/server` | | [socket](./packages/socket) | `@packages/socket` | A wrapper around socket.io to provide common libraries. | | [ts](./packages/ts) | `@packages/ts` | A centralized version of typescript. | + | [types](./packages/types) | `@packages/types` | The shared internal Cypress types. | + | [v8-snapshot-require](./packages/v8-snapshot-require) | `@packages/v8-snapshot-require` | Tool to load a snapshot for Electron applications that was created by `@tooling/v8-snapshot`. | + | [web-config](./packages/web-config) | `@packages/web-config` | The web-related configuration. | + +Private packages involved in development of the app live within the [`tooling`](./tooling) directory and are in the `@tooling/` namespace. They are discrete modules with different responsibilities, but each is necessary for development of the Cypress app and is not necessarily useful outside of the Cypress app. + +Here is a list of the packages in this repository with a short description, located within the [`tooling`](./tooling) directory: + + | Folder Name | Package Name | Purpose | + | :------------------------------------ | :---------------------- | :--------------------------------------------------------------------------- | + | [electron-mksnapshot](./electron-mksnapshot) | `electron-mksnapshot` | A rewrite of [electron/mksnapshot](https://github.com/electron/mksnapshot) to support multiple versions. | + | [packherd](./tooling/packherd) | `packherd` | Herds all dependencies reachable from an entry and packs them. | + | [v8-snapshot](./tooling/v8-snapshot) | `v8-snapshot` | Tool to create a snapshot for Electron applications. | Public packages live within the [`npm`](./npm) folder and are standalone modules that get independently published to npm under the `@cypress/` namespace. These packages generally contain extensions, plugins, or other packages that are complementary to, yet independent of, the main Cypress app. @@ -283,16 +183,20 @@ Here is a list of the npm packages in this repository: | Folder Name | Package Name | Purpose | | :----------------------------------------------------- | :--------------------------------- | :--------------------------------------------------------------------------- | | [angular](./npm/angular) | `@cypress/angular` | Cypress component testing for Angular. | - | [create-cypress-tests](./npm/create-cypress-tests) | `@cypress/create-cypress-tests` | Tooling to scaffold Cypress configuration and demo test files. | + | [angular signals](./npm/angular-signals) | `@cypress/angular-signals` | Cypress component testing for Angular 17/18 including support for signals. | | [eslint-plugin-dev](./npm/eslint-plugin-dev) | `@cypress/eslint-plugin-dev` | Eslint plugin for internal development. | + | [grep](./npm/grep) | `@cypress/grep` | Filter tests using substring | | [mount-utils](./npm/mount-utils) | `@cypress/mount-utils` | Common functionality for Vue/React/Angular adapters. | | [react](./npm/react) | `@cypress/react` | Cypress component testing for React. | | [react18](./npm/react18) | `@cypress/react18` | Cypress component testing for React 18. | + | [schematic](./npm/cypress-schematic) | `@cypress/schematic` | Official Angular Schematic and Builder for the Angular CLI.| + | [svelte](./npm/svelte) | `@cypress/svelte` | Cypress component testing for Svelte. | | [vite-dev-server](./npm/vite-dev-server) | `@cypress/vite-dev-server` | Vite powered dev server for Component Testing. | - | [webpack-preprocessor](./npm/webpack-preprocessor) | `@cypress/webpack-preprocessor` | Cypress preprocessor for bundling JavaScript via webpack. | - | [webpack-dev-server](./npm/webpack-dev-server) | `@cypress/webpack-dev-server` | Webpack powered dev server for Component Testing. | | [vue](./npm/vue) | `@cypress/vue` | Cypress component testing for Vue 3. | | [vue2](./npm/vue2) | `@cypress/vue2` | Cypress component testing for Vue 2. | + | [webpack-batteries-included-preprocessor](./npm/webpack-batteries-included-preprocessor) | `@cypress/webpack-batteries-included-preprocessor` | Cypress preprocessor for bundling JavaScript via webpack with dependencies included and support for various ES features, TypeScript, and CoffeeScript. | + | [webpack-dev-server](./npm/webpack-dev-server) | `@cypress/webpack-dev-server` | Webpack powered dev server for Component Testing. | + | [webpack-preprocessor](./npm/webpack-preprocessor) | `@cypress/webpack-preprocessor` | Cypress preprocessor for bundling JavaScript via webpack. | We try to tag all issues with a `pkg/` or `npm/` tag describing the appropriate package the work is required in. For public packages, we use their qualified package name: For example, issues relating to the webpack preprocessor are tagged under [`npm: @cypress/webpack-preprocessor`](https://github.com/cypress-io/cypress/labels/npm%3A%20%40cypress%2Fwebpack-preprocessor) label and issues related to the `driver` package are tagged with the [`pkg/driver`](https://github.com/cypress-io/cypress/labels/pkg%2Fdriver) label. @@ -300,9 +204,35 @@ We try to tag all issues with a `pkg/` or `npm/` tag describing the appropriate You must have the following installed on your system to contribute locally: -- [`Node.js`](https://nodejs.org/en/) (See the root [.node-version](.node-version) file for minimum version requirements. You can use [avn](https://github.com/wbyoung/avn) to automatically switch to the right version of Node.js for this repo.) +- [`Node.js`](https://nodejs.org/en/) (See the root [.node-version](.node-version) file for the required version. You can find a list of tools on [node-version-usage](https://github.com/shadowspawn/node-version-usage) to switch the version of [`Node.js`](https://nodejs.org/en/) based on [.node-version](.node-version).) - [`yarn`](https://yarnpkg.com/en/docs/install) -- [`python`](https://www.python.org/downloads/) (since we use `node-gyp`. See their [repo](https://github.com/nodejs/node-gyp) for Python version requirements.) +- [`python`](https://www.python.org/downloads/) (since we use `node-gyp`. See their [repo](https://github.com/nodejs/node-gyp) for Python version requirements. Use Python `3.11` or lower.) + +#### Debian/Ubuntu + +`sudo apt install g++ make` meets the additional requirements to run `node-gyp` in the context of building Cypress from source. +`python` is pre-installed on Debian-based systems including Ubuntu. +The Python versions shipped with Ubuntu versions `20.04`, `23.10` and `22.04` are compatible with Cypress requirements. + +Only on Ubuntu `24.04` install Python `3.11` by executing the following commands: + +```shell +sudo add-apt-repository ppa:deadsnakes/ppa +sudo apt update +sudo apt install python3.11 +``` + +Add the environment variable `NODE_GYP_FORCE_PYTHON` to `~/.bashrc`: + +```shell +export NODE_GYP_FORCE_PYTHON=/usr/bin/python3.11 +``` + +For Ubuntu `24.04` refer also to the [Release notes](https://discourse.ubuntu.com/t/noble-numbat-release-notes/39890) in the section [Unprivileged user namespace restrictions](https://discourse.ubuntu.com/t/noble-numbat-release-notes/39890#unprivileged-user-namespace-restrictions-15) and apply one of the workarounds to disable unprivileged user namespace restrictions for the entire system, either for one boot or persistently, as described. If you do not do this you may receive an error which includes the text `FATAL:setuid_sandbox_host.cc` when you try to run Cypress on this version of Ubuntu after building Cypress from source. + +#### Windows + +When installing the Visual Studio C++ environment recommended by [node-gyp](https://github.com/nodejs/node-gyp), install also a Windows 10 SDK. The currently used version of `node-gyp` may otherwise fail to recognise the Visual Studio installation. ### Getting Started @@ -333,12 +263,14 @@ If there are errors building the packages, prefix the commands with `DEBUG=cypre When running `yarn start` this routes through the CLI and eventually calls `yarn dev` with the proper arguments. This enables Cypress day-to-day development to match the logic of the built binary + CLI integration. -If you want to bypass the CLI entirely, you can use the `yarn dev` task and pass arguments directly. For example, to headlessly run a project in a given folder, while trying to record to the Dashboard +CLI flags can be passed to `yarn` targets to control application behavior when running locally. For example, to headlessly run a project in a given folder, while trying to record to Cypress Cloud: ```text -yarn dev --run-project /project/folder --record --key +yarn cypress:run --project /project/folder --record --key ``` +Alternatively, you can run `yarn dev` at the root of this repository to bypass the CLI. This will launch "global" mode, where you can then select a project. + #### Adding new Dependencies ⚠️ There is a [bug in yarn](https://github.com/yarnpkg/yarn/issues/7734) that may cause issues adding a new dependency to a workspace. You can avoid this by downgrading yarn to 1.19.1 (temporarily downgrade using `npx yarn@1.19.1 workspace @packages/server add my-new-dep1`). @@ -413,13 +345,13 @@ Each package is responsible for building itself and testing itself and can do so When executing top or package level scripts, [Vite](https://vitejs.dev/) may be used to build/host parts of the application. This section is to serve as a general reference for these environment variables that may be leverage throughout the repository. ###### `CYPRESS_INTERNAL_VITE_DEV` Set to `1` if wanting to leverage [vite's](https://vitejs.dev/guide/#command-line-interface) `vite dev` over `vite build` to avoid a full [production build](https://vitejs.dev/guide/build.html). -###### `CYPRESS_INTERNAL_VITE_INSPECT` +###### `CYPRESS_INTERNAL_VITE_INSPECT` Used internally to leverage [vite-plugin-inspect](https://github.com/antfu/vite-plugin-inspect) to view intermediary vite plugin state. The `CYPRESS_INTERNAL_VITE_DEV` is required for this to be applied correctly. Set to `1` to enable. -###### `CYPRESS_INTERNAL_VITE_OPEN_MODE_TESTING` +###### `CYPRESS_INTERNAL_VITE_OPEN_MODE_TESTING` Leveraged only for internal cy-in-cy type tests to access the Cypress instance from the parent frame. Please see the [E2E Open Mode Testing](./guides/e2e-open-testing.md) Guide. Set to `true` when doing -###### `CYPRESS_INTERNAL_VITE_APP_PORT` +###### `CYPRESS_INTERNAL_VITE_APP_PORT` Leveraged only when `CYPRESS_INTERNAL_VITE_DEV` is set to spawn the vite dev server for the app on the specified port. The default port is `3333`. -###### `CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT` +###### `CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT` Leveraged only when `CYPRESS_INTERNAL_VITE_DEV` is set to spawn the vite dev server for the launchpad on the specified port. The default port is `3001`. #### Debug Logs @@ -428,16 +360,10 @@ Many Cypress packages print out debugging information to console via the `debug` ### Coding Style We use [eslint](https://eslint.org/) to lint all JavaScript code and follow rules specified in -[@cypress/eslint-plugin-dev](./npm/eslint-plugin-cypress) plugin. - -When you edit files, you can quickly fix all changed files before you commit using - -```bash -$ yarn lint-changed --fix -``` +[@cypress/eslint-plugin-dev](./npm/eslint-plugin-dev) plugin. -When committing files, we run a Git pre-commit hook to lint the staged JS files. See the [`lint-staged` project](https://github.com/okonet/lint-staged). -If this command fails, you may need to run `yarn lint-changed --fix` and commit those changes. +This project uses a Git pre-commit hook to lint staged files before committing. See the [`lint-staged` project](https://github.com/okonet/lint-staged) for details. +`lint-staged` will try to auto-fix any lint errors with `eslint --fix`, so if it fails, you must manually fix the lint errors before committing. We **DO NOT** use Prettier to format code. You can find [.prettierignore](.prettierignore) file that ignores all files in this repository. To ensure this file is loaded, please always open _the root repository folder_ in your text editor, otherwise your code formatter might execute, reformatting lots of source files. @@ -453,7 +379,7 @@ This is to ensure that links do not go dead in older versions of Cypress when th ### Tests -For most packages there are typically unit and integration tests. +For most packages there are typically unit and integration tests. For UI packages there are E2E and component tests. Please refer to each packages' `README.md` which documents how to run tests. It is not feasible to try to run all of the tests together. We run our entire test fleet across over a dozen containers in CI. @@ -461,7 +387,9 @@ There are also a set of system tests in [`system-tests`](system-tests) which att Additionally, we test the code by running it against various other example projects in CI. See CI badges and links at the top of this document. -If you're curious how we manage all of these tests in CI check out our [`circle.yml`](circle.yml) file found in the root `cypress` directory. +If you're curious how we manage all of these tests in CI check out our [CircleCI config](.circleci/config.yml). + +Some of our test jobs in CircleCI require access to environment variables that are sensitive and are restricted to Cypress maintainers only. If you are not a Cypress maintainer, when your CI job runs, only a subset of jobs will run at first. A Cypress maintainer will need to approve the `contributor-pr` job in your workflow in order for your CI pipeline to complete. #### Docker @@ -474,7 +402,7 @@ Sometimes tests pass locally, but fail in CI. Our CI environment is dockerized. $ yarn docker ``` -There is a script [scripts/run-docker-local.sh](scripts/run-docker-local.sh) that runs the cypress image (see [circle.yml](circle.yml) for the current image name). +There is a script [scripts/run-docker-local.sh](scripts/run-docker-local.sh) that runs the cypress image (see [CircleCI config](.circleci/config.yml) for the current image name). The image will start and will map the root of the repository to `/cypress` inside the image. Now you can modify the files using your favorite environment and rerun tests inside the docker environment. @@ -496,54 +424,89 @@ $ yarn add https://cdn.cypress.io/beta/npm/.../cypress.tgz Note that unzipping the Linux binary inside a Docker container onto a mapped volume drive is *slow*. But once this is done you can modify the application resource folder in the local folder `/tmp/test-folder/node_modules/cypress/cypress-cache/3.3.0/Cypress/resources/app` to debug issues. -### Packages +#### Docker as a performance constrained environment -Generally when making contributions, you are typically making them to a small number of packages. Most of your local development work will be inside a single package at a time. +Sometimes performance issues are easier to reproduce in performance constrained environments. A docker container can be a good way to simulate this locally and allow for quick iteration. -Each package documents how to best work with it, so consult the `README.md` of each package. +In a fresh cypress repository run the following command: -They will outline development and test procedures. When in doubt just look at the `scripts` of each `package.json` file. Everything we do at Cypress is contained there. +```shell +docker compose run --service-port dev +``` -## Committing Code +This will spin up a docker container based off cypress/browsers:latest and start up the bash terminal. From here you can yarn install and develop as normal, although slower. It's recommend that you run this in a fresh repo because node modules may differ between an install on your local device and from within a linux docker image. -### Branches +Ports 5566 and 5567 are available to attach debuggers to, please note that docker compose run only maps ports if the `--service-port` command is used. -The repository is setup with two main (protected) branches. +### Packages -- `master` is the code already published, both for the main Cypress app and independent npm packages. -- `develop` is the current latest "pre-release" code. This branch is set as the default branch, and all pull requests that update the main Cypress binary should be made against this branch. +Generally when making contributions, you are typically making them to a small number of packages. Most of your local development work will be inside a single package at a time. -In general, we want to publish our [standalone npm packages](./npm) continuously as new features are added. Therefore, any pull requests that only change independent `@cypress/` packages in the [`npm`](./npm) directory should be made directly off the `master` branch. We use [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/) to automatically publish these packages to npm when a PR is merged directly into master. +Each package documents how to best work with it, so consult the `README.md` of each package. -When updating the main Cypress app, pull requests should be made against the `develop` branch. We do not continuously deploy the Cypress binary, so `develop` contains all of the new features and fixes that are staged to go out in the next update of the main Cypress app. In addition, if you make changes to an npm package that can't be published until the binary is also updated, you should make a pull request against the `develop` branch. +They will outline development and test procedures. When in doubt just look at the `scripts` of each `package.json` file. Everything we do at Cypress is contained there. -Essentially, if you only change files within the [`npm`](./npm) folder, then you should make a pull request against `master`. Otherwise, make it against `develop`. +## Committing Code -All updates to `master` are automatically merged into `develop`, so `develop` always has the latest version of every package. +### Branches -#### Workflow Diagrams +The repository has one protected branch: - - - +- `develop` contains the current latest "pre-release" code for the Cypress app and contains the already published code of all [standalone npm packages](./npm) Cypress maintains. This branch is set as the default branch, and all pull requests should be made against this branch. -### Independent Packages CI Workflow +We want to publish our [standalone npm packages](./npm) continuously as new features are added. Therefore, after any pull request that changes independent `@cypress/` packages in the [`npm`](./npm) directory will automatically publish when a PR is merged directly into `develop` and the entire build passes. We used [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/) to automate the release of these packages to npm. -Independent packages are automatically released when code is merged into `master` and the entire build passes. +We do not continuously deploy the Cypress binary, so `develop` contains all of the new features and fixes that are staged to go out in the next update of the main Cypress app. If you make changes to an npm package that can't be published until the binary is also updated, the pull request should clearly state that it should not be merged until the next scheduled Cypress app release date. ### Pull Requests - Break down pull requests into the smallest necessary parts to address the original issue or feature. This helps you get a timely review and helps the reviewer clearly understand which pieces of the code changes are relevant. - When opening a PR for a specific issue already open, please name the branch you are working on using the convention `issue-[issue number]`. For example, if your PR fixes Issue #803, name your branch `issue-803`. If the PR is a larger issue, you can add more context like `issue-803-new-scrollable-area`. If there's not an associated open issue, **[create an issue](https://github.com/cypress-io/cypress/issues/new/choose)**. -- PR's can be opened before all the work is finished. In fact we encourage this! Please create a [Draft Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) if your PR is not ready for review. [Mark the PR as **Ready for Review**](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review) when you're ready for a Cypress team member to review the PR. -- Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format as defined [here](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type). For example, if your PR is fixing a bug, you should prefix the PR title with `fix:`. -- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PR's will not be reviewed if this template is not filled in. -- If the PR is a user facing change and you're a Cypress team member that has logged into [ZenHub](https://www.zenhub.com/) and downloaded the [ZenHub for GitHub extension](https://www.zenhub.com/extension), set the release the PR is intended to ship in from the sidebar of the PR. Follow semantic versioning to select the intended release. This is used to generate the changelog for the release. If you don't tag a PR for release, it won't be mentioned in the changelog. - ![Select release for PR](https://user-images.githubusercontent.com/1271364/135139641-657015d6-2dca-42d4-a4fb-16478f61d63f.png) +- PRs can be opened before all the work is finished. In fact we encourage this! Please create a [Draft Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) if your PR is not ready for review. [Mark the PR as **Ready for Review**](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review) when you're ready for a Cypress team member to review the PR. +- Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format using one of the following definitions. Once committed to develop, this prefix will determine the appropriate 'next version' of Cypress or the corresponding npm module. + - Changes has user-facing impact: + - `breaking` - A breaking change that will require a MVB + - `dependency` - A change to a dependency that impact the user + - `deprecation` - An API deprecation notice for users + - `feat` - A new feature + - `fix` - A bug fix or regression fix. + - `misc` - a misc user-facing change, like a UI update which is not a fix or enhancement to how Cypress works + - `perf` - A code change that improves performance + - Changes that improves the codebase or system but has no user-facing impact: + - `chore` - Changes to the build process or auxiliary tools and libraries such as documentation generation + - `docs` - Documentation only changes + - `refactor` - A code change that neither fixes a bug nor adds a feature + - `revert` - Reverts a previous commit + - `test` - Adding missing or correcting existing tests +- For user-facing changes that will be released with the next Cypress version, be sure to add a changelog entry to the appropriate section in [`cli/CHANGELOG.md`](./cli/CHANGELOG.md). See [Writing the Cypress Changelog Guide](./guides/writing-the-cypress-changelog.md) for more details. +- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PRs will not be reviewed if this template is not filled in. - Please check the "Allow edits from maintainers" checkbox when submitting your PR. This will make it easier for the maintainers to make minor adjustments, to help with tests or any other changes we may need. ![Allow edits from maintainers checkbox](https://user-images.githubusercontent.com/1271181/31393427-b3105d44-ada9-11e7-80f2-0dac51e3919e.png) +- All Pull Requests require a minimum of **two** approvals. - After the PR is approved, the original contributor can merge the PR (if the original contributor has access). -- When you merge a PR into `develop`, select [**Squash and merge**](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits). This will squash all commits into a single commit. *The only exception to squashing is when converting files to another language and there is a clear commit history needed to maintain from the file conversion.* +- When you merge a PR into `develop`, select [**Squash and merge**](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits). This will squash all commits into a single commit. + +*The only exceptions to squashing are:* + +1. When converting files to another language and there is a clear commit history needed to maintain from the file conversion. +2. When merging a `release/*` branch to `develop`. Individual PRs were already squashed when they were merged to the release branch, and we want that history intact on develop. + +### Write Some Tests + +If you are adding a new feature or fixing a regression, ensure you add tests for it. Broadly speaking, there are four categories of tests you might consider: + +1. Unit tests. Those are inside of `test/unit`, if the package has them. These are the fastest and cheapest to execute. +2. Component Tests. These are co-located with components in the `src` directory of UI-related packages. These test individual UI components in isolation. They can exhaustively test all expected variations of a component and are faster than E2E tests. +3. E2E/Integration tests. Those are inside `cypress/e2e`, if the package has them. These are between Unit Tests and System Tests when it comes to speed of execution. +4. System tests. Those go in the [`system-tests`](https://github.com/cypress-io/cypress/tree/develop/system-tests) directory. The README explains how they work. These are the slowest to run, so you generally only want to add a system-test if it's absolutely required (but don't let that discourage you; they are also the most realistic way to test Cypress). + +When choosing what's most appropriate, consider: + +- ease of understanding +- ease of debugging +- resilience to refactoring + +It is also worth considering when a failure will be noticed. A unit or component test is likely to be run while the related code is being modified and provides very fast feedback. E2E tests and System Tests are more likely to only fail in CI since they are slower to run. ### Dependencies @@ -600,10 +563,6 @@ Below are guidelines to help during code review. If any of the following require - [ ] There is no irrelevant code to the issue being addressed. If there is, ask the contributor to break the work out into a separate PR. - [ ] Tests are testing the code's intended functionality in the best way possible. -#### Internal - -- [ ] The original issue has been tagged with a release in ZenHub. - ### Code Review of Dependency Updates Below are some guidelines Cypress uses when reviewing dependency updates. @@ -618,14 +577,15 @@ Below are some guidelines Cypress uses when reviewing dependency updates. - [ ] Code using the dependency has been updated to accommodate any breaking changes - [ ] The dependency still supports the version of Node that the package requires. -- [ ] The PR been tagged with a release in ZenHub. - [ ] Appropriate labels have been added to the PR (for example: label `type: breaking change` if it is a breaking change) -## Deployment +## Releases + +[Standalone npm packages](./npm) are deployed immediately when a PR is merged into `develop` and the entire build passes. -We will try to review and merge pull requests quickly. If you want to know our build process or build your own Cypress binary, read [the "Release Process" guide](./guides/release-process.md). +The Cypress app is typically released every two weeks. All PRs merged to `develop` will build a "pre-released" Cypress app which can be installed to verify or leverage your changes before the scheduled release. Read these instructions for [installing pre-release versions](https://docs.cypress.io/guides/references/advanced-installation#Install-pre-release-version). -Independent packages are deployed immediately upon being merged into master. You can read more [above](#independent-packages-ci-workflow). +If you want to know our build process or build your own Cypress binary, read [the "Release Process" guide](./guides/release-process.md). ## Known problems diff --git a/LICENSE b/LICENSE index 4ce6792e2a7..c7379e8b1d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Cypress.io +Copyright (c) 2023 Cypress.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d10a7089342..b1eee75c8f6 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Cypress Logo - +

@@ -21,15 +21,15 @@ Fast, easy and reliable testing for anything that runs in a browser.

- Join us, we're hiring. + Register for 🚀 Cypressconf 2024.

npm - - Gitter chat + + Discord chat StackShare @@ -57,26 +57,35 @@ or ```bash yarn add cypress --dev ``` +or +```bash +pnpm add cypress --save-dev +``` -![installing-cli e1693232](https://user-images.githubusercontent.com/1271364/31740846-7bf607f0-b420-11e7-855f-41c996040d31.gif) - +![installing-cli e1693232](./assets/cypress-installation.gif) ## Contributing -- [![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/develop.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress/tree/develop) - `develop` branch -- [![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/master.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress/tree/master) - `master` branch +[![cypress](https://img.shields.io/endpoint?url=https://cloud.cypress.io/badge/simple/ypt4pf/develop&style=flat&logo=cypress)](https://cloud.cypress.io/projects/ypt4pf/runs) +[![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/develop.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress/tree/develop) - `develop` branch Please see our [Contributing Guideline](./CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps. ## License -[![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cypress-io/cypress/blob/master/LICENSE) +[![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cypress-io/cypress/blob/develop/LICENSE) This project is licensed under the terms of the [MIT license](/LICENSE). ## Badges -Let the world know your project is using Cypress.io to test with this cool badge +Configure a badge for your project's README to show your test status or test count in the [Cypress Cloud](https://www.cypress.io/cloud). + +[![cypress](https://img.shields.io/endpoint?url=https://cloud.cypress.io/badge/simple/ypt4pf/develop&style=flat&logo=cypress)](https://cloud.cypress.io/projects/ypt4pf/runs) + +[![cypress](https://img.shields.io/endpoint?url=https://cloud.cypress.io/badge/count/ypt4pf/develop&style=flat&logo=cypress)](https://cloud.cypress.io/projects/ypt4pf/runs) + +Or let the world know your project is using Cypress with the badge below. [![Cypress.io](https://img.shields.io/badge/tested%20with-Cypress-04C38E.svg)](https://www.cypress.io/) diff --git a/__snapshots__/questions-remain-spec.js b/__snapshots__/questions-remain-spec.js new file mode 100644 index 00000000000..5cf6a2e17f1 --- /dev/null +++ b/__snapshots__/questions-remain-spec.js @@ -0,0 +1,9 @@ +exports['questions-remain returns object if all questions have been answered 1'] = { + 'foo': 'foo is specified', + 'bar': 'so is bar', +} + +exports['questions-remain asks questions for missing options 1'] = { + 'foo': 'foo is specified', + 'bar': 'bar user answer', +} diff --git a/assets/DIAGRAMS.md b/assets/DIAGRAMS.md deleted file mode 100644 index bf1145597d3..00000000000 --- a/assets/DIAGRAMS.md +++ /dev/null @@ -1,16 +0,0 @@ -## Diagram assets in this repo - -> :warning: These will eventually move to the docs site, link with caution - -### Updating Diagrams - -1. Visit a diagram link -1. Make your changes -1. Go to Export -> URL... and paste the new URL in this document -1. Go to Export -> PNG and replace the asset - -### List of Diagrams - -**Branching and Contributing** -1. [Choosing a Branch - Develop or Master](https://viewer.diagrams.net/?highlight=0000ff&edit=_blank&layers=1&nav=1&title=git-2.drawio#RzVltc9o4EP41%2FtiO38EfgYS2c%2B1NrulMm4%2FCFrYutuXIcsD3629lS%2Fg9QBpCYQBrLa2l3WefXQnNWiX7Twxl0Tca4Fgz9WCvWTeaaRqW68KPkJS1xHOkIGQkkJ0awT35D0uhLqUFCXDe6cgpjTnJukKfpin2eUeGGKO7brctjbtPzVCIB4J7H8VD6U8S8KiWzh29kX%2FGJIzUkw1d3kmQ6iwFeYQCumuJrFvNWjFKeX2V7Fc4FsZTdqnHrSfuHibGcMpPGfDX0%2B33z%2Ff7X1m42j39mH%2F6JzazD1LLM4oLuWA5WV4qCzBapAEWSnTNWu4iwvF9hnxxdwc%2BB1nEkxhaBlxKdZhxvJ%2Bcp3FYPcAG0wRzVkIXNcCrR5TKonVz11jfsma1LGpZ3lbAQtLj4UFzYxS4kHY5w0ZDk%2BAAMCKblPGIhjRF8W0jXTbSr5Rm0jj%2FYs5LCXFUcNo13ZamfI0SEouF%2F0ARTRBI%2FYI9V8ZvrCuefq5tGY4RJ8%2FdcWOWkkPvKAGNjU%2Fm849O1y1zvauEIxZiLse1YXhcldNTldOC%2BXigasEYKlvdMtEhP2vSrn7u3Loj4KKeRYOmg5FfDzBrJAjdGBy6zDOUdqDnPhWCLyqwfMgrKC2gg2Fk%2B%2BYmXIXidxWhNATyFMFTzSKk8EULsWIi9FZxhfGj4M%2FSh0eYsABdPbxQejYMpX4khm631Td83eBnHAOyZV9Y96F7S1bPX4l7UXSESsbjYQsrWdGYskqHtd1uTd8XluKMPuLWncDduEAJ1rLhrzciKMvqgcRSnN%2FiKEPRfpuj3B7Q34yi7CsgaBdhofnuu5ic6JJgYIDgNAx9QznH7Prowe4EembeRtcvgR6Ayx8HH%2Bd9MhxYiJW%2FRCEBFpDNB1lXVI2bfadVytYJmRHvCa81z2eObD80T4JWo1s0ylbjDjMCdsRMyk7NsmCwKlG9YFhZytS58Vj8%2Fm7WPjc%2Fmvash0TTNl7Mj%2Ba8j93eiMvkR3eS3QQyquJfBW5DYBBV4jXktAfBZweOqTVMcIzAwFe0gS1NB8coJmEK1z7gQaBmKQiBwJ5hIW8kJAiqIGEYGBZtKn0CWtIloNxZas6NXEGLd5yVeL%2FENHKjI5Vqh%2B1FG57TQT5JS%2FpHw%2Fa6lbeihdfWjqoLMH6OudYnqTcAxuydeOtALobb5ZbzyeWCHHgF3rKuwVveERI6YYD3DlX9fMBaf9NrMcwQOSfWNa9mm9kRttFNddRQdh74mxtVdaChlJofHbur43Js5A0cvmBY1MVi4b6qpOvMs2Eq6eQ%2BzUTVrHNa2dV%2FRKP9SJqTAKvtF4%2FwsItfZgAHGLtOs0SrD7sEetZD2MUxyXKBnzxCmRD6MS2Cer%2FgkzQEiXn8tOe0ItsR77Ei261ewyRYvy5RfHuTxxjt2ns2UnsbxqWKb6W4hZufEa6QEwhIfIHPYfe0ZTQZcyiYhF%2BOQkRKrLeKL7kdpDKFGhfZdw9pfMR33ojvzIu5buT4dK1r3lpb2Jpnr2i6LXJhgvUwWL%2BIMC4hEbfJQfer3foOEcFpfEflAY24tUE5riM%2FmTiKOYqJbgSmNMW9cJWi07EzRhBXgoc72Fd7Q3yY9gg%2BZhfDhzmFD8%2FQFp52a2rAP%2FMbJTS15XAT8u6hLb3kTJedb%2B47w%2Bk7b4SXx2j5cq6bPpSVm87jR2r26JEadGJkA2Znp25Dr4UAw35HBNh9BMyGCHgjcodm87dbXf01f15at%2F8D) -1. [Sample Branching Workflow](https://viewer.diagrams.net/?highlight=0000ff&edit=_blank&layers=1&nav=1#R7Vzdc9o4EP9rPHP3QMff2I8JhLYzl7vO5W7S9k3YMrgxliuLEPrXV7Ilf4NNwJhJeAF7Ja3W%2B9tda1cCSZusXj5iEC3vkQsDSZXdF0mbSqqq6PaYfjHKNqXYspoSFth3U5KcEx78X5CPFNS178KY01ISQSggflQmOigMoUNKNIAx2pS7eShwS4QILGCN8OCAoE599F2yTKmWIef0T9BfLMXMisxbVkB05oR4CVy0KZC0O0mbYIRIerV6mcCAKa%2Bsl9mO1kwwDEPSZcBPH1kPNzb%2BNP%2F0%2FfN38v8%2F44%2FGSFE5Ps8gWPNH5uKSrdABRuvQhYyNLGm3m6VP4EMEHNa6oahT2pKsAnqn0Mu6WFzSZ4gJfCmQuJgfIVpBgre0i2i10xHcZHRLS%2B83OQC6wbkuC8rXNJMDz0FfZKxzvdALrpoD1GSoNS09gFVEFaTKjwg%2FeQFFllkXpp9%2Ff7lPNOA8UeOIa9qkSiBllYHAX4T02qEqg5gSmKp8aoM3vGHluy4bfoth7P8C84QVwyJCfkiShzVuJWPKeK0JilMvYqw9FJIZWPkB0%2BV%2FYIlWgFO5qykmY5sBfCIE7QqCtlJDUB3LdQSFr5weQL1u5jNZsmcSFe3Gku5UyZIlayqIqnR7ewByAfTIyXHjCBnyThxPjptmlXAzjDpuTbCNe4PNaIhOZkC4RkrwmD%2FXSDSMUlXe0A6KHr3kjfRqwb4ntBP251TpWDCkAqY80x4Xgr6iD4e%2BXkffPqfT2idAX6XoJ7BXDOARMAjYYiJ5Vz%2BxSA7YOmIJwiRqJy3pZHN81FSSLjvbiBoB5TrbwDl7M4zofYSRQ4kXb4HqYBZoqnULVJpMUOvLBJXsGeo2iILjzIKzCfyj2EyW0Hli9orWzCPuQczWELlFMfYnnfA%2Bd5Y5BqGzlFgAEL1mEJA1Zh2U0tDexJmixFtjCjgzFLoY63O2CabPl0zInv%2FLv0wJIKYuQPXvecnnWUB4XMJQCOAzcVYQL5gYEwENIonfJCaeNBbkkqn3s7g3hc8wQFGfgn5m%2BiBLmJiELxQnBKJZmxf4NG3L5M6fyElU7e4VjhKZF3aOlzHB6AlOUECDrjYNUciCo0eVVCF1jatNuVD7klvtZcldjpzjhhW32RA5s3y1h9BZT5pqEC2oHqIj1JEl%2FvzlJhVz6yY1qUZZUU2LHFUIWnrFGOO%2BFKVo7YqCoXvDyhm5jdbMWtQntE5GyNaVlOmMRYh0UQCDecJfWDsjuQv4wIVAmCzRAoUguMupt%2FDFJ18Zgw8Gv%2FvGhWLX0xfBm91s%2BU3R4SRV8zxPdZyad9IW15ybBktQnTV%2B3mMd%2B%2B2PPcReDypAbzS4iKBhGADiP5cLQ03WwGf4whZZhbWN%2FsEom17F82K0xg7ko4qFnDZGNU4E0PBKapwS68ye%2BxiDbcim35HBuga0XL3JYC11rplvxWAVc1wys2qW39VeKZ%2BKwSpKN4OlBgS2hW48bdkncWUmWz5QtPIAepHKcGL%2FaUoq3o%2F%2FeB40dwT8sT2X5WP9JzXMPT2t8WU5mlK1wte%2BGuwqo3O%2FGsQSsyFdpqx5gQXHkJSSiTXxRtbuDDmOQNiYhDip5bAEBC%2Fmf8hpGiG%2B%2FkxYyEmm4nHXYF2J8A7RWCkWFhrSGVlLiPAq8RDRtuF2wBr1xGbpRQAJTbBGVGDHDxf1kSwjGfl0pRvykbKYL2khNJ%2BOPdpfjEz8Pk1r3TLXbOAcOE%2BLZPk8qqhD1a1UE6pu8wtDKMX14ygAXCF%2BGPhiJi9AgFSmr1cAKultCtD%2BLKxl26hbyfOAnZKWhKIlZemcdpmq8LiWGnlvJSpzd4XqAMfJfSd3H1Xe4UGUEVhFSaOmMbS4S1XJZQ7Vmry819NKzVVnU9v9TXRpdLlSY5PXiQ7NjidaW3xPLrifXPBAMbzJCbPHrvhhve5Vq928Ry8cyunGHWoblI0fxbvKREWt78iHu63pamjsTqPPFRTV8iJEoFAssZxzH2ncJV29gsVfaEOD1bTpdwWrGSx7aLDMK1idwVLUgdGyri%2BtA9Ayhkary67AFS3e2Roaresaozta2enewdC6LjIOQEsfGq0OZ5RfjxavRZ8MLVHCPhdaVrXmO3S6ZVlXuA6Aa%2BiEy7KvcB0A19Apl6KYdXhad%2B1%2BrFeR6MHr%2BnnHvxCKuCJ%2FQEK2HBJ2JLSMLtUu3n4t3rC9P74TyG7z7b%2FkbpvdVfcyyxuRr93OvLR9RWEb6Sbbno4ib27dgOy8s3jcQeumXbtrCNgVAgZP5O0u5weueF1MKm%2F3Wnh5e3gNnczbvZZe3hxeg6fzdq%2FFl7eH19AJfXbo%2FHpm4V2dWSj%2FOOg9n1sQrdrgx4cU8YcMfQTP7KT0qYJndsD6bAgZl1avURSjjs%2FZCgD9JPKvLSyc%2BWB%2BawFAkfnKsbUCIFLu01UAOh%2B%2B73gk%2BPjD94p2jsP0co%2BL9bcYvwYvYMpdCs79%2FPqBR5lCYPlWiis7y5fH%2FmrizIGqNfxkb%2F1L%2BQmEXDVTvWJ%2FXX8CMdJeG%2FDaIxK9zf%2BvKe2e%2F%2BuVdvcb) diff --git a/assets/branching-diagram.png b/assets/branching-diagram.png deleted file mode 100644 index 3d404987d9c..00000000000 Binary files a/assets/branching-diagram.png and /dev/null differ diff --git a/assets/cypress-installation.gif b/assets/cypress-installation.gif new file mode 100644 index 00000000000..bb1d1b07117 Binary files /dev/null and b/assets/cypress-installation.gif differ diff --git a/assets/sample-workflow.png b/assets/sample-workflow.png deleted file mode 100644 index bb9e567e7fa..00000000000 Binary files a/assets/sample-workflow.png and /dev/null differ diff --git a/browser-versions.json b/browser-versions.json index 3152d133869..00025b6f642 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,5 +1,5 @@ { - "chrome:beta": "105.0.5195.28", - "chrome:stable": "104.0.5112.101", + "chrome:beta": "127.0.6533.26", + "chrome:stable": "126.0.6478.126", "chrome:minimum": "64.0.3282.0" } diff --git a/centos7-builder.Dockerfile b/centos7-builder.Dockerfile new file mode 100644 index 00000000000..a939fa4087f --- /dev/null +++ b/centos7-builder.Dockerfile @@ -0,0 +1,9 @@ +FROM centos:7 +# Install dependencies for re-building better-sqlite and setting devtoolset-8 as the default compiler. +RUN yum -y install centos-release-scl curl python3 make atk-devel atk java-atk-wrapper at-spi2-atk gtk3 libXt libdrm mesa-libgbm Xvfb && yum -y install devtoolset-8-gcc devtoolset-8-gcc-c++ +RUN echo >> /etc/profile.d/devtoolset-8.sh 'source scl_source enable devtoolset-8' +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash +RUN echo >> /etc/profile.d/nvm.sh 'source ~/.nvm/nvm.sh' +# Node 16 is the most recent version that supports CentOS 7. We only need it to +# re-build better-sqlite, so there should be minimal risk of security issues. +RUN source ~/.nvm/nvm.sh && nvm install 16.20.2 && npm install -g yarn diff --git a/circle.yml b/circle.yml deleted file mode 100644 index e35a6ceeefc..00000000000 --- a/circle.yml +++ /dev/null @@ -1,2817 +0,0 @@ -version: 2.1 - -defaults: &defaults - parallelism: 1 - working_directory: ~/cypress - parameters: &defaultsParameters - executor: - type: executor - default: cy-doc - only-cache-for-root-user: - type: boolean - default: false - executor: <> - environment: &defaultsEnvironment - ## set specific timezone - TZ: "/usr/share/zoneinfo/America/New_York" - - ## store artifacts here - CIRCLE_ARTIFACTS: /tmp/artifacts - - ## set so that e2e tests are consistent - COLUMNS: 100 - LINES: 24 - -mainBuildFilters: &mainBuildFilters - filters: - branches: - only: - - develop - - fix-ci-deps - -# 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 -# so just add your branch to the list here to build and test on Mac -macWorkflowFilters: &darwin-workflow-filters - when: - or: - - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'correct-dashboard-results', << pipeline.git.branch >> ] - - matches: - pattern: "-release$" - value: << pipeline.git.branch >> - -linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - when: - or: - - equal: [ develop, << pipeline.git.branch >> ] - - matches: - pattern: "-release$" - value: << pipeline.git.branch >> - -# uncomment & add to the branch conditions below to disable the main linux -# flow if we don't want to test it for a certain branch -linuxWorkflowExcludeFilters: &linux-x64-workflow-exclude-filters - unless: - or: - - false - # - equal: [ 'tgriesser/chore/fix-windows-build', << pipeline.git.branch >> ] - -# windows is slow and expensive in CI, so it normally only runs on main branches -# add your branch to this list to run the full Windows build on your PR -windowsWorkflowFilters: &windows-workflow-filters - when: - or: - - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ linux-arm64, << pipeline.git.branch >> ] - - equal: [ 'lmiller/fixing-flake-1', << pipeline.git.branch >> ] - - matches: - pattern: "-release$" - value: << pipeline.git.branch >> - -executors: - # the Docker image with Cypress dependencies and Chrome browser - cy-doc: - docker: - - image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge - # by default, we use "medium" to balance performance + CI costs. bump or reduce on a per-job basis if needed. - resource_class: medium - environment: - PLATFORM: linux - CI_DOCKER: "true" - - # Docker image with non-root "node" user - non-root-docker-user: - docker: - - image: cypress/browsers:node16.14.2-slim-chrome100-ff99-edge - user: node - environment: - PLATFORM: linux - - # executor to run on Mac OS - # https://circleci.com/docs/2.0/executor-types/#using-macos - # https://circleci.com/docs/2.0/testing-ios/#supported-xcode-versions - mac: - macos: - # Executor should have Node >= required version - xcode: "13.0.0" - resource_class: macos.x86.medium.gen2 - environment: - PLATFORM: darwin - - # executor to run on Windows - based off of the windows-orb default executor since it is - # not customizable enough to align with our existing setup. - # https://github.com/CircleCI-Public/windows-orb/blob/master/src/executors/default.yml - # https://circleci.com/docs/2.0/hello-world-windows/#software-pre-installed-in-the-windows-image - windows: &windows-executor - machine: - image: windows-server-2019-vs2019:stable - shell: bash.exe -eo pipefail - resource_class: windows.large - environment: - PLATFORM: windows - - darwin-arm64: - machine: true - environment: - PLATFORM: darwin - - linux-arm64: - machine: - image: ubuntu-2004:2022.04.1 - resource_class: arm.medium - environment: - PLATFORM: linux - -commands: - verify_should_persist_artifacts: - steps: - - run: - name: Check current branch to persist artifacts - command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "webkit-experimental" ]]; then - echo "Not uploading artifacts or posting install comment for this branch." - circleci-agent step halt - fi - - restore_workspace_binaries: - steps: - - attach_workspace: - at: ~/ - # make sure we have cypress.zip received - - run: ls -l - - run: ls -l cypress.zip cypress.tgz - - run: node --version - - run: npm --version - - restore_cached_workspace: - steps: - - attach_workspace: - at: ~/ - - install-required-node - - unpack-dependencies - - restore_cached_binary: - steps: - - attach_workspace: - at: ~/ - - prepare-modules-cache: - parameters: - dont-move: - type: boolean - default: false - steps: - - run: node scripts/circle-cache.js --action prepare - - unless: - condition: << parameters.dont-move >> - steps: - - run: - name: Move to /tmp dir for consistent caching across root/non-root users - command: | - mkdir -p /tmp/node_modules_cache - mv ~/cypress/node_modules /tmp/node_modules_cache/root_node_modules - mv ~/cypress/cli/node_modules /tmp/node_modules_cache/cli_node_modules - mv ~/cypress/system-tests/node_modules /tmp/node_modules_cache/system-tests_node_modules - mv ~/cypress/globbed_node_modules /tmp/node_modules_cache/globbed_node_modules - - install-webkit-deps: - steps: - - run: - name: Install WebKit dependencies - command: | - npx playwright install webkit - npx playwright install-deps webkit - - build-and-persist: - description: Save entire folder as artifact for other jobs to run without reinstalling - steps: - - run: - name: Build all codegen - command: | - source ./scripts/ensure-node.sh - yarn gulp buildProd - - run: - name: Build packages - command: | - source ./scripts/ensure-node.sh - yarn build - - prepare-modules-cache # So we don't throw these in the workspace cache - - persist_to_workspace: - root: ~/ - paths: - - cypress - - .ssh - - node_modules # contains the npm i -g modules - - install_cache_helpers_dependencies: - steps: - - run: - # Dependencies needed by circle-cache.js, before we "yarn" or unpack cached node_modules - name: Cache Helper Dependencies - working_directory: ~/ - command: npm i glob@7.1.6 fs-extra@10.0.0 minimist@1.2.5 fast-json-stable-stringify@2.1.0 - - unpack-dependencies: - description: 'Unpacks dependencies associated with the current workflow' - steps: - - install_cache_helpers_dependencies - - run: - name: Generate Circle Cache Key - command: node scripts/circle-cache.js --action cacheKey > circle_cache_key - - run: - name: Generate platform key - command: node ./scripts/get-platform-key.js > platform_key - - restore_cache: - name: Restore cache state, to check for known modules cache existence - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} - - run: - name: Move node_modules back from /tmp - command: | - if [[ -d "/tmp/node_modules_cache" ]]; then - mv /tmp/node_modules_cache/root_node_modules ~/cypress/node_modules - mv /tmp/node_modules_cache/cli_node_modules ~/cypress/cli/node_modules - mv /tmp/node_modules_cache/system-tests_node_modules ~/cypress/system-tests/node_modules - mv /tmp/node_modules_cache/globbed_node_modules ~/cypress/globbed_node_modules - rm -rf /tmp/node_modules_cache - fi - - run: - name: Restore all node_modules to proper workspace folders - command: node scripts/circle-cache.js --action unpack - - restore_cached_system_tests_deps: - description: 'Restore the cached node_modules for projects in "system-tests/projects/**"' - steps: - - 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: node ./scripts/get-platform-key.js > platform_key - - restore_cache: - name: Restore system tests node_modules cache - keys: - - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - - update_cached_system_tests_deps: - description: 'Update the cached node_modules for projects in "system-tests/projects/**"' - steps: - - 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: node ./scripts/get-platform-key.js > platform_key - - restore_cache: - name: Restore cache state, to check for known modules cache existence - keys: - - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - - run: - name: Send root honeycomb event for this CI build - command: cd system-tests/scripts && node ./send-root-honeycomb-event.js - - run: - name: Bail if specific cache exists - command: | - 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 }}-{{ 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 }}-{{ checksum "platform_key" }}-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - paths: - - /tmp/cy-system-tests-node-modules - - run: touch /tmp/system_tests_node_modules_installed - - save_cache: - name: Save system tests node_modules cache state key - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-system-tests-projects-node-modules-cache-{{ checksum "system_tests_cache_key" }} - paths: - - /tmp/system_tests_node_modules_installed - - caching-dependency-installer: - description: 'Installs & caches the dependencies based on yarn lock & package json dependencies' - parameters: - only-cache-for-root-user: - type: boolean - default: false - steps: - - install_cache_helpers_dependencies - - run: - name: Generate Circle Cache Key - command: node scripts/circle-cache.js --action cacheKey > circle_cache_key - - run: - name: Generate platform key - command: node ./scripts/get-platform-key.js > platform_key - - restore_cache: - name: Restore cache state, to check for known modules cache existence - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }} - - run: - name: Bail if cache exists - command: | - if [[ -f "node_modules_installed" ]]; then - echo "Node modules already cached for dependencies, exiting" - circleci-agent step halt - fi - - run: date +%Y-%U > cache_date - - restore_cache: - name: Restore weekly yarn cache - keys: - - v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }} - - run: - name: Install Node Modules - command: | - source ./scripts/ensure-node.sh - # avoid installing Percy's Chromium every time we use @percy/cli - # https://docs.percy.io/docs/caching-asset-discovery-browser-in-ci - PERCY_POSTINSTALL_BROWSER=true \ - yarn --prefer-offline --frozen-lockfile --cache-folder ~/.yarn - no_output_timeout: 20m - - prepare-modules-cache: - dont-move: <> # we don't move, so we don't hit any issues unpacking symlinks - - when: - condition: <> # we don't move to /tmp since we don't need to worry about different users - steps: - - save_cache: - name: Saving node modules for root, cli, and all globbed workspace packages - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-node-modules-cache-{{ checksum "circle_cache_key" }} - paths: - - node_modules - - cli/node_modules - - system-tests/node_modules - - globbed_node_modules - - unless: - condition: <> - steps: - - save_cache: - name: Saving node modules for root, cli, and all globbed workspace packages - 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 - - save_cache: - name: Saving node-modules cache state key - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-state-of-node-modules-cache-{{ checksum "circle_cache_key" }} - paths: - - node_modules_installed - - save_cache: - name: Save weekly yarn cache - key: v{{ .Environment.CACHE_VERSION }}-{{ checksum "platform_key" }}-deps-root-weekly-{{ checksum "cache_date" }} - paths: - - ~/.yarn - - ~/.cy-npm-cache - - verify-build-setup: - description: Common commands run when setting up for build or yarn install - parameters: - executor: - type: executor - default: cy-doc - steps: - - run: pwd - - run: - name: print global yarn cache path - command: echo $(yarn global bin) - - run: - name: print yarn version - command: yarn versions - - unless: - condition: - # stop-only does not correctly match on windows: https://github.com/bahmutov/stop-only/issues/78 - equal: [ *windows-executor, << parameters.executor >> ] - steps: - - run: - name: Stop .only - # this will catch ".only"s in js/coffee as well - command: | - source ./scripts/ensure-node.sh - yarn stop-only-all - - run: - name: Check terminal variables - ## make sure the TERM is set to 'xterm' in node (Linux only) - ## else colors (and tests) will fail - ## See the following information - ## * http://andykdocs.de/development/Docker/Fixing+the+Docker+TERM+variable+issue - ## * https://unix.stackexchange.com/questions/43945/whats-the-difference-between-various-term-variables - command: | - source ./scripts/ensure-node.sh - yarn check-terminal - - install-required-node: - # https://discuss.circleci.com/t/switch-nodejs-version-on-machine-executor-solved/26675/2 - description: Install Node version matching .node-version - steps: - # installing NVM will use git+ssh, so update known_hosts - - update_known_hosts - - run: - name: Install Node - command: | - node_version=$(cat .node-version) - source ./scripts/ensure-node.sh - echo "Installing Yarn" - npm install yarn -g # ensure yarn is installed with the correct node engine - yarn check-node-version - - run: - name: Check Node - command: | - source ./scripts/ensure-node.sh - yarn check-node-version - - install-chrome: - description: Install Google Chrome - parameters: - channel: - description: browser channel to install - type: string - version: - description: browser version to install - type: string - steps: - - run: - name: Install Google Chrome (<>) - command: | - echo "Installing Chrome (<>) v<>" - wget -O /usr/src/google-chrome-<>_<>_amd64.deb "http://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-<>/google-chrome-<>_<>-1_amd64.deb" && \ - dpkg -i /usr/src/google-chrome-<>_<>_amd64.deb ; \ - apt-get install -f -y && \ - rm -f /usr/src/google-chrome-<>_<>_amd64.deb - which google-chrome-<> || (printf "\n\033[0;31mChrome was not successfully downloaded - bailing\033[0m\n\n" && exit 1) - echo "Location of Google Chrome Installation: `which google-chrome-<>`" - echo "Google Chrome Version: `google-chrome-<> --version`" - - run-driver-integration-tests: - parameters: - browser: - description: browser shortname to target - type: string - install-chrome-channel: - description: chrome channel to install - type: string - default: '' - experimentalSessionAndOrigin: - description: experimental flag to apply - type: boolean - default: false - steps: - - restore_cached_workspace - - when: - condition: <> - steps: - - install-chrome: - channel: <> - version: $(node ./scripts/get-browser-version.js chrome:<>) - - when: - condition: - equal: [ webkit, << parameters.browser >> ] - steps: - - install-webkit-deps - - run: - name: Run driver tests in Cypress - environment: - CYPRESS_KONFIG_ENV: production - command: | - echo Current working directory is $PWD - echo Total containers $CIRCLE_NODE_TOTAL - - if [[ -v MAIN_RECORD_KEY ]]; then - # internal PR - if <>; then - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - yarn cypress:run-experimentalSessionAndOrigin --record --parallel --group 5x-driver-<>-experimentalSessionAndOrigin --browser <> - else - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - yarn cypress:run --record --parallel --group 5x-driver-<> --browser <> - fi - else - # external PR - TESTFILES=$(circleci tests glob "cypress/e2e/**/*.cy.*" | circleci tests split --total=$CIRCLE_NODE_TOTAL) - echo "Test files for this machine are $TESTFILES" - - if [[ -z "$TESTFILES" ]]; then - echo "Empty list of test files" - fi - if <>; then - yarn cypress:run-experimentalSessionAndOrigin --browser <> --spec $TESTFILES - else - yarn cypress:run --browser <> --spec $TESTFILES - fi - fi - working_directory: packages/driver - - verify-mocha-results - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - windows-install-chrome: - parameters: - browser: - description: browser shortname to target - type: string - steps: - - run: - # TODO: How can we have preinstalled browsers on CircleCI? - name: 'Install Chrome on Windows' - command: | - # install with `--ignore-checksums` to avoid checksum error - # https://www.gep13.co.uk/blog/chocolatey-error-hashes-do-not-match - [[ $PLATFORM == 'windows' && '<>' == 'chrome' ]] && choco install googlechrome --ignore-checksums || [[ $PLATFORM != 'windows' ]] - - run-new-ui-tests: - parameters: - package: - description: package to target - type: enum - enum: ['frontend-shared', 'launchpad', 'app', 'reporter'] - browser: - description: browser shortname to target - type: string - percy: - description: enable percy - type: boolean - default: false - type: - description: ct or e2e - type: enum - enum: ['ct', 'e2e'] - debug: - description: debug option - type: string - default: '' - steps: - - restore_cached_workspace - - windows-install-chrome: - browser: <> - - run: - command: | - echo Current working directory is $PWD - echo Total containers $CIRCLE_NODE_TOTAL - - if [[ -v MAIN_RECORD_KEY ]]; then - # internal PR - cmd=$([[ <> == 'true' ]] && echo 'yarn percy exec --parallel -- --') || true - DEBUG=<> \ - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=${TEST_LAUNCHPAD_RECORD_KEY:-$MAIN_RECORD_KEY} \ - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - $cmd yarn workspace @packages/<> cypress:run:<> --browser <> --record --parallel --group <>-<> - else - # external PR - - # To make `circleci tests` work correctly, we need to step into the package folder. - cd packages/<> - - GLOB="cypress/e2e/**/*cy.*" - - if [[ <> == 'ct' ]]; then - # component tests are located side by side with the source codes. - GLOB="src/**/*cy.*" - fi - - TESTFILES=$(circleci tests glob "$GLOB" | circleci tests split --total=$CIRCLE_NODE_TOTAL) - echo "Test files for this machine are $TESTFILES" - - # To run the `yarn` command, we need to walk out of the package folder. - cd ../.. - - DEBUG=<> \ - CYPRESS_KONFIG_ENV=production \ - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn workspace @packages/<> cypress:run:<> --browser <> --spec $TESTFILES - fi - - run: - command: | - if [[ <> == 'app' && <> == 'true' && -d "packages/app/cypress/screenshots/runner/screenshot/screenshot.cy.tsx/percy" ]]; then - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn percy upload packages/app/cypress/screenshots/runner/screenshot/screenshot.cy.tsx/percy - else - echo "skipping percy screenshots uploading" - fi - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: ./packages/<>/cypress/videos - - store-npm-logs - - run-system-tests: - parameters: - browser: - description: browser shortname to target - type: string - steps: - - restore_cached_workspace - - restore_cached_system_tests_deps - - when: - condition: - equal: [ webkit, << parameters.browser >> ] - steps: - - install-webkit-deps - - run: - name: Run system tests - command: | - ALL_SPECS=`circleci tests glob "/root/cypress/system-tests/test/*spec*"` - SPECS= - for file in $ALL_SPECS; do - # filter out non_root tests, they have their own stage - if [[ "$file" == *"non_root"* ]]; then - echo "Skipping $file" - continue - fi - SPECS="$SPECS $file" - done - SPECS=`echo $SPECS | xargs -n 1 | circleci tests split --split-by=timings` - echo SPECS=$SPECS - yarn workspace @tooling/system-tests test:ci $SPECS --browser <> - - verify-mocha-results - - store_test_results: - path: /tmp/cypress - - store_artifacts: - 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: - - store_artifacts: - path: ~/.npm/_logs - - post-install-comment: - description: Post GitHub comment with a blurb on how to install pre-release version - steps: - - run: - name: Post pre-release install comment - command: | - node scripts/add-install-comment.js \ - --npm npm-package-url.json \ - --binary binary-url.json - - verify-mocha-results: - description: Double-check that Mocha tests ran as expected. - parameters: - expectedResultCount: - description: The number of result files to expect, ie, the number of Mocha test suites that ran. - type: integer - ## by default, assert that at least 1 test ran - default: 0 - steps: - - run: - name: 'Verify Mocha Results' - command: | - source ./scripts/ensure-node.sh - yarn verify:mocha:results <> - - clone-repo-and-checkout-branch: - description: | - Clones an external repo and then checks out the branch that matches the next version otherwise uses 'master' branch. - parameters: - repo: - description: "Name of the github repo to clone like: cypress-example-kitchensink" - type: string - pull_request_id: - description: Pull request number to check out before installing and testing - type: integer - default: 0 - steps: - - restore_cached_binary - - run: - name: "Cloning test project and checking out release branch: <>" - working_directory: /tmp/<> - command: | - git clone --depth 1 --no-single-branch https://github.com/cypress-io/<>.git . - - cd ~/cypress/.. - # install some deps for get-next-version - npm i semver@7.3.2 conventional-recommended-bump@6.1.0 conventional-changelog-angular@5.0.12 - NEXT_VERSION=$(node ./cypress/scripts/get-next-version.js) - cd - - - git checkout $NEXT_VERSION || true - - when: - condition: <> - steps: - - run: - name: Check out PR <> - working_directory: /tmp/<> - command: | - git fetch origin pull/<>/head:pr-<> - git checkout pr-<> - - test-binary-against-rwa: - description: | - Takes the built binary and NPM package, clones the RWA repo - and runs the new version of Cypress against it. - parameters: - repo: - description: "Name of the github repo to clone like" - type: string - default: "cypress-realworld-app" - browser: - description: Name of the browser to use, like "electron", "chrome", "firefox" - type: enum - enum: ["", "electron", "chrome", "firefox"] - default: "" - command: - description: Test command to run to start Cypress tests - type: string - default: "yarn cypress:run" - # if the repo to clone and test is a monorepo, you can - # run tests inside a specific subfolder - folder: - description: Subfolder to test in - type: string - default: "" - # you can test new features in the test runner against recipes or other repos - # by opening a pull request in those repos and running this test job - # against a pull request number in the example repo - pull_request_id: - description: Pull request number to check out before installing and testing - type: integer - default: 0 - wait-on: - description: Whether to use wait-on to wait on a server to be booted - type: string - default: "" - server-start-command: - description: Server start command for repo - type: string - default: "CI=true yarn start" - steps: - - clone-repo-and-checkout-branch: - repo: <> - - when: - condition: <> - steps: - - run: - name: Check out PR <> - working_directory: /tmp/<> - command: | - git fetch origin pull/<>/head:pr-<> - git checkout pr-<> - git log -n 2 - - run: - command: yarn - working_directory: /tmp/<> - - run: - name: Install Cypress - working_directory: /tmp/<> - # force installing the freshly built binary - command: | - CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i --legacy-peer-deps ~/cypress/cypress.tgz && [[ -f yarn.lock ]] && yarn - - run: - name: Print Cypress version - working_directory: /tmp/<> - command: npx cypress version - - run: - name: Types check 🧩 (maybe) - working_directory: /tmp/<> - command: yarn types - - run: - working_directory: /tmp/<> - command: <> - background: true - - run: - condition: <> - name: "Waiting on server to boot: <>" - command: "npx wait-on <>" - - when: - condition: <> - steps: - - when: - condition: <> - steps: - - run: - name: Run tests using browser "<>" - working_directory: /tmp/<>/<> - command: | - <> -- --browser <> - - unless: - condition: <> - steps: - - run: - name: Run tests using command - working_directory: /tmp/<>/<> - command: <> - - unless: - condition: <> - steps: - - when: - condition: <> - steps: - - run: - name: Run tests using browser "<>" - working_directory: /tmp/<> - command: <> -- --browser <> - - unless: - condition: <> - steps: - - run: - name: Run tests using command - working_directory: /tmp/<> - command: <> - - store-npm-logs - - test-binary-against-repo: - description: | - Takes the built binary and NPM package, clones given example repo - and runs the new version of Cypress against it. - parameters: - repo: - description: "Name of the github repo to clone like: cypress-example-kitchensink" - type: string - browser: - description: Name of the browser to use, like "electron", "chrome", "firefox" - type: enum - enum: ["", "electron", "chrome", "firefox"] - default: "" - command: - description: Test command to run to start Cypress tests - type: string - default: "npm run e2e" - build-project: - description: Should the project build script be executed - type: boolean - default: true - # if the repo to clone and test is a monorepo, you can - # run tests inside a specific subfolder - folder: - description: Subfolder to test in - type: string - default: "" - # you can test new features in the test runner against recipes or other repos - # by opening a pull request in those repos and running this test job - # against a pull request number in the example repo - pull_request_id: - description: Pull request number to check out before installing and testing - type: integer - default: 0 - wait-on: - description: Whether to use wait-on to wait on a server to be booted - type: string - default: "" - server-start-command: - description: Server start command for repo - type: string - default: "npm start --if-present" - steps: - - clone-repo-and-checkout-branch: - repo: <> - pull_request_id: <> - - run: - # Ensure we're installing the node-version for the cloned repo - command: | - if [[ -f .node-version ]]; then - branch="<< pipeline.git.branch >>" - - externalBranchPattern='^pull\/[0-9]+' - if [[ $branch =~ $externalBranchPattern ]]; then - # We are unable to curl from the external PR branch location - # so we fall back to develop - branch="develop" - fi - - curl -L https://raw.githubusercontent.com/cypress-io/cypress/$branch/scripts/ensure-node.sh --output ci-ensure-node.sh - else - # if no .node-version file exists, we no-op the node script and use the global yarn - echo '' > ci-ensure-node.sh - fi - working_directory: /tmp/<> - - run: - # Install deps + Cypress binary with yarn if yarn.lock present - command: | - source ./ci-ensure-node.sh - if [[ -f yarn.lock ]]; then - yarn --frozen-lockfile - CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip yarn add -D ~/cypress/cypress.tgz - else - npm install - CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm install --legacy-peer-deps ~/cypress/cypress.tgz - fi - working_directory: /tmp/<> - - run: - name: Scaffold new config file - working_directory: /tmp/<> - environment: - CYPRESS_INTERNAL_FORCE_SCAFFOLD: "1" - command: | - if [[ -f cypress.json ]]; then - rm -rf cypress.json - echo 'module.exports = { e2e: {} }' > cypress.config.js - fi - - run: - name: Rename support file - working_directory: /tmp/<> - command: | - if [[ -f cypress/support/index.js ]]; then - mv cypress/support/index.js cypress/support/e2e.js - fi - - run: - name: Print Cypress version - working_directory: /tmp/<> - command: | - source ./ci-ensure-node.sh - npx cypress version - - run: - name: Types check 🧩 (maybe) - working_directory: /tmp/<> - command: | - source ./ci-ensure-node.sh - [[ -f yarn.lock ]] && yarn types || npm run types --if-present - - when: - condition: <> - steps: - - run: - name: Build 🏗 (maybe) - working_directory: /tmp/<> - command: | - source ./ci-ensure-node.sh - [[ -f yarn.lock ]] && yarn build || npm run build --if-present - - run: - working_directory: /tmp/<> - command: | - source ./ci-ensure-node.sh - <> - background: true - - run: - condition: <> - name: "Waiting on server to boot: <>" - command: | - npx wait-on <> --timeout 120000 - - windows-install-chrome: - browser: <> - - when: - condition: <> - steps: - - when: - condition: <> - steps: - - run: - name: Run tests using browser "<>" - working_directory: /tmp/<>/<> - command: | - <> -- --browser <> - - unless: - condition: <> - steps: - - run: - name: Run tests using command - working_directory: /tmp/<>/<> - command: <> - - unless: - condition: <> - steps: - - when: - condition: <> - steps: - - run: - name: Run tests using browser "<>" - working_directory: /tmp/<> - command: | - source ./ci-ensure-node.sh - <> -- --browser <> - - unless: - condition: <> - steps: - - run: - name: Run tests using command - working_directory: /tmp/<> - command: | - source ./ci-ensure-node.sh - <> - - store-npm-logs - - wait-on-circle-jobs: - description: Polls certain Circle CI jobs until they finish - parameters: - job-names: - description: comma separated list of circle ci job names to wait for - type: string - steps: - - run: - name: "Waiting on Circle CI jobs: <>" - command: node ./scripts/wait-on-circle-jobs.js --job-names="<>" - - build-binary: - steps: - - run: - name: Check environment variables before code sign (if on Mac/Windows) - # NOTE - # our code sign works via electron-builder - # by default, electron-builder will NOT sign app built in a pull request - # even our internal one (!) - # Usually this is not a problem, since we only build and test binary - # built on "develop" and "master" branches - # but if you need to really build and sign a binary in a PR - # set variable CSC_FOR_PULL_REQUEST=true - command: | - set -e - NEEDS_CODE_SIGNING=`node -p 'process.platform === "win32" || process.platform === "darwin"'` - if [[ "$NEEDS_CODE_SIGNING" == "true" ]]; then - echo "Checking for required environment variables..." - if [ -z "$CSC_LINK" ]; then - echo "Need to provide environment variable CSC_LINK" - echo "with base64 encoded certificate .p12 file" - exit 1 - fi - if [ -z "$CSC_KEY_PASSWORD" ]; then - echo "Need to provide environment variable CSC_KEY_PASSWORD" - echo "with password for unlocking certificate .p12 file" - exit 1 - fi - echo "Succeeded." - else - echo "Not code signing for this platform" - fi - - run: - name: Build the Cypress binary - environment: - DEBUG: electron-builder,electron-osx-sign* - # notarization on Mac can take a while - no_output_timeout: "45m" - command: | - if [[ `node ./scripts/get-platform-key.js` == 'linux-arm64' ]]; then - # these are missing on Circle and there is no way to pre-install them on Arm - sudo apt-get update - sudo apt-get install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb - fi - source ./scripts/ensure-node.sh - node --version - yarn binary-build --version $(node ./scripts/get-next-version.js) - - run: - name: Zip the binary - command: | - if [[ $PLATFORM == 'linux' ]]; then - # on Arm, CI runs as non-root, on x64 CI runs as root but there is no sudo binary - if [[ `whoami` == 'root' ]]; then - apt-get update && apt-get install -y zip - else - sudo apt-get update && sudo apt-get install -y zip - fi - fi - source ./scripts/ensure-node.sh - yarn binary-zip - - store-npm-logs - - persist_to_workspace: - root: ~/ - paths: - - cypress/cypress.zip - - build-cypress-npm-package: - parameters: - executor: - type: executor - default: cy-doc - steps: - - run: - name: Bump NPM version - command: | - source ./scripts/ensure-node.sh - yarn get-next-version --npm - - run: - name: Build NPM package - command: | - source ./scripts/ensure-node.sh - yarn build --scope cypress - - run: - name: Copy Re-exported NPM Packages - command: node ./scripts/post-build.js - working_directory: cli - - run: - command: ls -la types - working_directory: cli/build - - run: - command: ls -la vue vue2 mount-utils react - working_directory: cli/build - - unless: - condition: - equal: [ *windows-executor, << parameters.executor >> ] - steps: - - run: - name: list NPM package contents - command: | - source ./scripts/ensure-node.sh - yarn workspace cypress size - - run: - name: pack NPM package - working_directory: cli/build - command: yarn pack --filename ../../cypress.tgz - - run: - name: list created NPM package - command: ls -l - - store-npm-logs - - persist_to_workspace: - root: ~/ - paths: - - cypress/cypress.tgz - - upload-build-artifacts: - steps: - - run: ls -l - - run: - name: Upload unique binary to S3 - command: | - node scripts/binary.js upload-build-artifact \ - --type binary \ - --file cypress.zip \ - --version $(node -p "require('./package.json').version") - - run: - name: Upload NPM package to S3 - command: | - node scripts/binary.js upload-build-artifact \ - --type npm-package \ - --file cypress.tgz \ - --version $(node -p "require('./package.json').version") - - store-npm-logs - - run: ls -l - - run: cat binary-url.json - - run: cat npm-package-url.json - - persist_to_workspace: - root: ~/ - paths: - - cypress/binary-url.json - - cypress/npm-package-url.json - - update_known_hosts: - description: Ensures that we have the latest Git public keys to prevent git+ssh from failing. - steps: - - run: - name: Update known_hosts with github.com keys - command: | - mkdir -p ~/.ssh - ssh-keyscan github.com >> ~/.ssh/known_hosts - -jobs: - ## Checks if we already have a valid cache for the node_modules_install and if it has, - ## skips ahead to the build step, otherwise installs and caches the node_modules - node_modules_install: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium - resource_class: << parameters.resource_class >> - steps: - - checkout - - install-required-node - - verify-build-setup: - executor: << parameters.executor >> - - persist_to_workspace: - root: ~/ - paths: - - cypress - - .nvm # mac / linux - - ProgramData/nvm # windows - - caching-dependency-installer: - only-cache-for-root-user: <> - - store-npm-logs - - ## restores node_modules from previous step & builds if first step skipped - build: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium+ - resource_class: << parameters.resource_class >> - steps: - - restore_cached_workspace - - run: - name: Top level packages - command: yarn list --depth=0 || true - - run: - name: Check env canaries on Linux - command: | - # only Docker has the required env data for this - if [[ $CI_DOCKER == 'true' ]]; then - node ./scripts/circle-env.js --check-canaries - fi - - build-and-persist - - store-npm-logs - - lint: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Linting 🧹 - command: | - yarn clean - git clean -df - yarn lint - - run: - name: cypress info (dev) - command: node cli/bin/cypress info --dev - - store-npm-logs - - check-ts: - <<: *defaults - steps: - - restore_cached_workspace - - install-required-node - - run: - name: Check TS Types - command: NODE_OPTIONS=--max_old_space_size=4096 yarn gulp checkTs - - - # a special job that keeps polling Circle and when all - # individual jobs are finished, it closes the Percy build - percy-finalize: - <<: *defaults - resource_class: small - parameters: - <<: *defaultsParameters - required_env_var: - type: env_var_name - steps: - - restore_cached_workspace - - run: - # if this is an external pull request, the environment variables - # are NOT set for security reasons, thus no need to poll - - # and no need to finalize Percy, since there will be no visual tests - name: Check if <> is set - command: | - if [[ -v <> ]]; then - echo "Internal PR, good to go" - else - echo "This is an external PR, cannot access other services" - circleci-agent step halt - fi - - wait-on-circle-jobs: - job-names: > - cli-visual-tests, - reporter-integration-tests, - run-app-component-tests-chrome, - run-app-integration-tests-chrome, - run-frontend-shared-component-tests-chrome, - run-launchpad-component-tests-chrome, - run-launchpad-integration-tests-chrome, - run-reporter-component-tests-chrome, - run-webpack-dev-server-integration-tests, - run-vite-dev-server-integration-tests - - run: - # Sometimes, even though all the circle jobs have finished, Percy times out during `build:finalize` - # If all other jobs finish but `build:finalize` fails, we retry it once - name: Finalize percy build - allows single retry - command: | - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - yarn percy build:finalize || yarn percy build:finalize - - cli-visual-tests: - <<: *defaults - resource_class: small - steps: - - restore_cached_workspace - - run: mkdir -p cli/visual-snapshots - - run: - command: node cli/bin/cypress info --dev | yarn --silent term-to-html | node scripts/sanitize --type cli-info > cli/visual-snapshots/cypress-info.html - environment: - FORCE_COLOR: 2 - - run: - command: node cli/bin/cypress help | yarn --silent term-to-html > cli/visual-snapshots/cypress-help.html - environment: - FORCE_COLOR: 2 - - store_artifacts: - path: cli/visual-snapshots - - run: - name: Upload CLI snapshots for diffing - command: | - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn percy snapshot ./cli/visual-snapshots - - unit-tests: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium - resource_class: << parameters.resource_class >> - parallelism: 1 - steps: - - restore_cached_workspace - - when: - condition: - # several snapshots fails for windows due to paths. - # until these are fixed, run the tests that are working. - equal: [ *windows-executor, << parameters.executor >> ] - steps: - - run: yarn test-scripts scripts/**/*spec.js - - unless: - condition: - equal: [ *windows-executor, << parameters.executor >> ] - steps: - - run: yarn test-scripts - # make sure packages with TypeScript can be transpiled to JS - - run: yarn lerna run build-prod --stream --concurrency 4 - # run unit tests from each individual package - - run: yarn test - # run type checking for each individual package - - run: yarn lerna run types - - verify-mocha-results: - expectedResultCount: 10 - - store_test_results: - path: /tmp/cypress - # CLI tests generate HTML files with sample CLI command output - - store_artifacts: - path: cli/test/html - - store_artifacts: - path: packages/errors/__snapshot-images__ - - store-npm-logs - - unit-tests-release: - <<: *defaults - resource_class: small - parallelism: 1 - steps: - - restore_cached_workspace - - update_known_hosts - - run: yarn test-npm-package-release-script - - lint-types: - <<: *defaults - parallelism: 1 - steps: - - restore_cached_workspace - - run: - command: ls -la types - working_directory: cli - - run: - command: ls -la chai - working_directory: cli/types - - run: - name: "Lint types 🧹" - command: yarn workspace cypress dtslint - # todo(lachlan): do we need this? yarn check-ts does something very similar - # - run: - # name: "TypeScript check 🧩" - # command: yarn type-check --ignore-progress - - store-npm-logs - - server-unit-tests: - <<: *defaults - parallelism: 1 - steps: - - restore_cached_workspace - - run: yarn test-unit --scope @packages/server - - verify-mocha-results: - expectedResultCount: 1 - - store_test_results: - path: /tmp/cypress - - store-npm-logs - - server-integration-tests: - <<: *defaults - parallelism: 1 - steps: - - restore_cached_workspace - - run: yarn test-integration --scope @packages/server - - verify-mocha-results: - expectedResultCount: 1 - - store_test_results: - path: /tmp/cypress - - store-npm-logs - - server-performance-tests: - <<: *defaults - steps: - - restore_cached_workspace - - run: - command: yarn workspace @packages/server test-performance - - verify-mocha-results: - expectedResultCount: 1 - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - system-tests-node-modules-install: - <<: *defaults - steps: - - 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 - parallelism: 8 - steps: - - run-system-tests: - browser: chrome - - system-tests-electron: - <<: *defaults - parallelism: 8 - steps: - - run-system-tests: - browser: electron - - system-tests-firefox: - <<: *defaults - parallelism: 8 - steps: - - run-system-tests: - browser: firefox - - system-tests-webkit: - <<: *defaults - parallelism: 8 - steps: - - run-system-tests: - browser: webkit - - system-tests-non-root: - <<: *defaults - steps: - - restore_cached_workspace - - run: - command: yarn workspace @tooling/system-tests test:ci "test/non_root*spec*" --browser electron - - verify-mocha-results - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - run-frontend-shared-component-tests-chrome: - <<: *defaults - parameters: - <<: *defaultsParameters - percy: - type: boolean - default: false - parallelism: 3 - steps: - - run-new-ui-tests: - browser: chrome - percy: << parameters.percy >> - package: frontend-shared - type: ct - - run-launchpad-component-tests-chrome: - <<: *defaults - parameters: - <<: *defaultsParameters - percy: - type: boolean - default: false - parallelism: 7 - steps: - - run-new-ui-tests: - browser: chrome - percy: << parameters.percy >> - package: launchpad - type: ct - # debug: cypress:*,engine:socket - - run-launchpad-integration-tests-chrome: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium - percy: - type: boolean - default: false - resource_class: << parameters.resource_class >> - parallelism: 3 - steps: - - run-new-ui-tests: - browser: chrome - percy: << parameters.percy >> - package: launchpad - type: e2e - - run-app-component-tests-chrome: - <<: *defaults - parameters: - <<: *defaultsParameters - percy: - type: boolean - default: false - parallelism: 7 - steps: - - run-new-ui-tests: - browser: chrome - percy: << parameters.percy >> - package: app - type: ct - - run-app-integration-tests-chrome: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium - percy: - type: boolean - default: false - resource_class: << parameters.resource_class >> - parallelism: 8 - steps: - - run-new-ui-tests: - browser: chrome - percy: << parameters.percy >> - package: app - type: e2e - - driver-integration-tests-chrome: - <<: *defaults - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: chrome - install-chrome-channel: stable - - driver-integration-tests-chrome-beta: - <<: *defaults - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: chrome:beta - install-chrome-channel: beta - - driver-integration-tests-firefox: - <<: *defaults - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: firefox - - driver-integration-tests-electron: - <<: *defaults - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: electron - - driver-integration-tests-webkit: - <<: *defaults - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: webkit - - driver-integration-tests-chrome-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: chrome - install-chrome-channel: stable - experimentalSessionAndOrigin: true - - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: chrome:beta - install-chrome-channel: beta - experimentalSessionAndOrigin: true - - driver-integration-tests-firefox-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: firefox - experimentalSessionAndOrigin: true - - driver-integration-tests-electron-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: electron - experimentalSessionAndOrigin: true - - driver-integration-tests-webkit-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: webkit - experimentalSessionAndOrigin: true - - run-reporter-component-tests-chrome: - <<: *defaults - parameters: - <<: *defaultsParameters - percy: - type: boolean - default: false - parallelism: 2 - steps: - - run-new-ui-tests: - browser: chrome - percy: << parameters.percy >> - package: reporter - type: ct - - reporter-integration-tests: - <<: *defaults - parallelism: 3 - steps: - - restore_cached_workspace - - run: - command: yarn build-for-tests - working_directory: packages/reporter - - run: - command: | - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn percy exec --parallel -- -- \ - yarn cypress:run --record --parallel --group reporter - working_directory: packages/reporter - - verify-mocha-results - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - run-webpack-dev-server-integration-tests: - <<: *defaults - parallelism: 2 - steps: - - restore_cached_workspace - - restore_cached_system_tests_deps - - run: - command: | - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn percy exec --parallel -- -- \ - yarn cypress:run --record --parallel --group webpack-dev-server - working_directory: npm/webpack-dev-server - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - run-vite-dev-server-integration-tests: - <<: *defaults - # parallelism: 3 TODO: Add parallelism once we have more specs - steps: - - restore_cached_workspace - - restore_cached_system_tests_deps - - run: - command: | - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_WORKSPACE_ID \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn percy exec --parallel -- -- \ - yarn cypress:run --record --parallel --group vite-dev-server - working_directory: npm/vite-dev-server - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - ui-components-integration-tests: - <<: *defaults - steps: - - restore_cached_workspace - - run: - command: yarn build-for-tests - working_directory: packages/ui-components - - run: - command: | - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - yarn cypress:run --record --parallel --group ui-components - working_directory: packages/ui-components - - verify-mocha-results - - store_test_results: - path: /tmp/cypress - - store_artifacts: - path: /tmp/artifacts - - store-npm-logs - - npm-webpack-preprocessor: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Build - command: yarn workspace @cypress/webpack-preprocessor build - - run: - name: Test babelrc - command: yarn test - working_directory: npm/webpack-preprocessor/examples/use-babelrc - - run: - name: Build ts-loader - command: yarn install - working_directory: npm/webpack-preprocessor/examples/use-ts-loader - - run: - name: Types ts-loader - command: yarn types - working_directory: npm/webpack-preprocessor/examples/use-ts-loader - - run: - name: Test ts-loader - command: yarn test - working_directory: npm/webpack-preprocessor/examples/use-ts-loader - - run: - name: Start React app - command: yarn start - background: true - working_directory: npm/webpack-preprocessor/examples/react-app - - run: - name: Test React app - command: yarn test - working_directory: npm/webpack-preprocessor/examples/react-app - - run: - name: Run tests - command: yarn workspace @cypress/webpack-preprocessor test - - store-npm-logs - - npm-webpack-dev-server: - <<: *defaults - steps: - - restore_cached_workspace - - restore_cached_system_tests_deps - - run: - name: Run tests - command: yarn workspace @cypress/webpack-dev-server test - - run: - name: Run tests - command: yarn workspace @cypress/webpack-dev-server test - - npm-vite-dev-server: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Run tests - command: yarn test - working_directory: npm/vite-dev-server - - store_test_results: - path: npm/vite-dev-server/test_results - - store-npm-logs - - npm-webpack-batteries-included-preprocessor: - <<: *defaults - resource_class: small - steps: - - restore_cached_workspace - - run: - name: Run tests - command: yarn workspace @cypress/webpack-batteries-included-preprocessor test - - npm-vue: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Build - command: yarn workspace @cypress/vue build - - run: - name: Type Check - command: yarn typecheck - working_directory: npm/vue - - store_test_results: - path: npm/vue/test_results - - store_artifacts: - path: npm/vue/test_results - - store-npm-logs - - npm-angular: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Build - command: yarn workspace @cypress/angular build - - store-npm-logs - - npm-react: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Build - command: yarn workspace @cypress/react build - - run: - name: Run tests - command: yarn test - working_directory: npm/react - - store_test_results: - path: npm/react/test_results - - store_artifacts: - path: npm/react/test_results - - store-npm-logs - - npm-mount-utils: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Build - command: yarn workspace @cypress/mount-utils build - - store-npm-logs - - npm-create-cypress-tests: - <<: *defaults - resource_class: small - steps: - - restore_cached_workspace - - run: yarn workspace create-cypress-tests build - - npm-eslint-plugin-dev: - <<: *defaults - steps: - - restore_cached_workspace - - run: - name: Run tests - command: yarn workspace @cypress/eslint-plugin-dev test - - npm-cypress-schematic: - <<: *defaults - resource_class: small - steps: - - restore_cached_workspace - - run: - name: Build + Install - command: | - yarn workspace @cypress/schematic build - working_directory: npm/cypress-schematic - - run: - name: Run unit tests - command: | - yarn test - working_directory: npm/cypress-schematic - - store-npm-logs - - npm-release: - <<: *defaults - resource_class: medium+ - steps: - - restore_cached_workspace - - run: - name: Release packages after all jobs pass - command: yarn npm-release - - create-build-artifacts: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium+ - resource_class: << parameters.resource_class >> - steps: - - restore_cached_workspace - - build-binary - - build-cypress-npm-package: - executor: << parameters.executor >> - - verify_should_persist_artifacts - - upload-build-artifacts - - post-install-comment - - test-kitchensink: - <<: *defaults - parameters: - <<: *defaultsParameters - resource_class: - type: string - default: medium+ - steps: - - clone-repo-and-checkout-branch: - repo: cypress-example-kitchensink - - install-required-node - - run: - name: Remove cypress.json - description: Remove cypress.json in case it exists - working_directory: /tmp/cypress-example-kitchensink - environment: - CYPRESS_INTERNAL_FORCE_SCAFFOLD: "1" - command: rm -rf cypress.json - - run: - name: Install prod dependencies - command: yarn --production - working_directory: /tmp/cypress-example-kitchensink - - run: - name: Example server - command: yarn start - working_directory: /tmp/cypress-example-kitchensink - background: true - - run: - name: Rename support file - working_directory: /tmp/cypress-example-kitchensink - command: | - if [[ -f cypress/support/index.js ]]; then - mv cypress/support/index.js cypress/support/e2e.js - fi - - run: - name: Run Kitchensink example project - command: | - yarn cypress:run --project /tmp/cypress-example-kitchensink - - store-npm-logs - - test-kitchensink-against-staging: - <<: *defaults - steps: - - clone-repo-and-checkout-branch: - repo: cypress-example-kitchensink - - install-required-node - - run: - name: Install prod dependencies - command: yarn --production - working_directory: /tmp/cypress-example-kitchensink - - run: - name: Example server - command: yarn start - working_directory: /tmp/cypress-example-kitchensink - background: true - - run: - name: Run Kitchensink example project - command: | - CYPRESS_PROJECT_ID=$TEST_KITCHENSINK_PROJECT_ID \ - CYPRESS_RECORD_KEY=$TEST_KITCHENSINK_RECORD_KEY \ - CYPRESS_INTERNAL_ENV=staging \ - CYPRESS_video=false \ - yarn cypress:run --project /tmp/cypress-example-kitchensink --record - - store-npm-logs - - test-against-staging: - <<: *defaults - steps: - - clone-repo-and-checkout-branch: - repo: cypress-test-tiny - - run: - name: Run test project - command: | - CYPRESS_PROJECT_ID=$TEST_TINY_PROJECT_ID \ - CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \ - CYPRESS_INTERNAL_ENV=staging \ - yarn cypress:run --project /tmp/cypress-test-tiny --record - - store-npm-logs - - test-npm-module-and-verify-binary: - <<: *defaults - steps: - - restore_cached_workspace - # make sure we have cypress.zip received - - run: ls -l - - run: ls -l cypress.zip cypress.tgz - - run: mkdir test-binary - - run: - name: Create new NPM package - working_directory: test-binary - command: npm init -y - - run: - # install NPM from built NPM package folder - name: Install Cypress - working_directory: test-binary - # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=/root/cypress/cypress.zip npm i /root/cypress/cypress.tgz - - run: - name: Cypress version - working_directory: test-binary - command: $(yarn bin cypress) version - - run: - name: Verify Cypress binary - working_directory: test-binary - command: $(yarn bin cypress) verify - - run: - name: Cypress help - working_directory: test-binary - command: $(yarn bin cypress) help - - run: - name: Cypress info - working_directory: test-binary - command: $(yarn bin cypress) info - - store-npm-logs - - test-npm-module-on-minimum-node-version: - <<: *defaults - resource_class: small - docker: - - image: cypress/base:12.0.0-libgbm - steps: - - restore_workspace_binaries - - run: mkdir test-binary - - run: - name: Create new NPM package - working_directory: test-binary - command: npm init -y - - run: - name: Install Cypress - working_directory: test-binary - command: CYPRESS_INSTALL_BINARY=/root/cypress/cypress.zip npm install /root/cypress/cypress.tgz - - run: - name: Verify Cypress binary - working_directory: test-binary - command: $(npm bin)/cypress verify - - run: - name: Print Cypress version - working_directory: test-binary - command: $(npm bin)/cypress version - - run: - name: Cypress info - working_directory: test-binary - command: $(npm bin)/cypress info - - test-types-cypress-and-jest: - parameters: - executor: - description: Executor name to use - type: executor - default: cy-doc - wd: - description: Working directory, should be OUTSIDE cypress monorepo folder - type: string - default: /root/test-cypress-and-jest - <<: *defaults - resource_class: small - steps: - - restore_workspace_binaries - - run: mkdir <> - - run: - name: Create new NPM package ⚗️ - working_directory: <> - command: npm init -y - - run: - name: Install dependencies 📦 - working_directory: <> - environment: - CYPRESS_INSTALL_BINARY: /root/cypress/cypress.zip - # let's install Cypress, Jest and any other package that might conflict - # https://github.com/cypress-io/cypress/issues/6690 - - # Todo: Add `jest` back into the list once https://github.com/yargs/yargs-parser/issues/452 - # is resolved. - command: | - npm install /root/cypress/cypress.tgz \ - typescript @types/jest enzyme @types/enzyme - - run: - name: Test types clash ⚔️ - working_directory: <> - command: | - echo "console.log('hello world')" > hello.ts - npx tsc hello.ts --noEmit - - test-full-typescript-project: - parameters: - executor: - description: Executor name to use - type: executor - default: cy-doc - wd: - description: Working directory, should be OUTSIDE cypress monorepo folder - type: string - default: /root/test-full-typescript - <<: *defaults - resource_class: small - steps: - - restore_workspace_binaries - - run: mkdir <> - - run: - name: Create new NPM package ⚗️ - working_directory: <> - command: npm init -y - - run: - name: Install dependencies 📦 - working_directory: <> - environment: - CYPRESS_INSTALL_BINARY: /root/cypress/cypress.zip - command: | - npm install /root/cypress/cypress.tgz typescript - - run: - name: Scaffold full TypeScript project 🏗 - working_directory: <> - command: npx @bahmutov/cly@1.9.0 init --typescript - - run: - name: Run project tests 🗳 - working_directory: <> - command: npx cypress run - - # install NPM + binary zip and run against staging API - test-binary-against-staging: - <<: *defaults - steps: - - restore_workspace_binaries - - clone-repo-and-checkout-branch: - repo: cypress-test-tiny - - run: - name: Install Cypress - working_directory: /tmp/cypress-test-tiny - # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i --legacy-peer-deps ~/cypress/cypress.tgz - - run: - name: Run test project - working_directory: /tmp/cypress-test-tiny - command: | - CYPRESS_PROJECT_ID=$TEST_TINY_PROJECT_ID \ - CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \ - CYPRESS_INTERNAL_ENV=staging \ - $(yarn bin cypress) run --record - - store-npm-logs - - test-binary-against-recipes-firefox: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-recipes - command: npm run test:ci:firefox - - test-binary-against-recipes-chrome: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-recipes - command: npm run test:ci:chrome - - test-binary-against-recipes: - <<: *defaults - 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. - # Imagine you are working on a feature and want to show / test a recipe - # You would need to run the built test runner before release - # against a PR that cannot be merged until the new version - # of the test runner is released. - # Use: - # specify pull request number - # and the recipe folder - - # test-binary-against-recipe-pull-request: - # <<: *defaults - # steps: - # # test a specific pull request by number from cypress-example-recipes - # - test-binary-against-repo: - # repo: cypress-example-recipes - # command: npm run test:ci - # pull_request_id: 515 - # folder: examples/fundamentals__typescript - - test-binary-against-kitchensink: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-kitchensink - browser: "electron" - - test-binary-against-kitchensink-firefox: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-kitchensink - browser: firefox - - test-binary-against-kitchensink-chrome: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-kitchensink - browser: chrome - - test-binary-against-todomvc-firefox: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-todomvc - browser: firefox - - test-binary-against-conduit-chrome: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-conduit-app - browser: chrome - command: "npm run cypress:run" - wait-on: http://localhost:3000 - - test-binary-against-api-testing-firefox: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-api-testing - browser: firefox - command: "npm run cy:run" - - test-binary-against-piechopper-firefox: - <<: *defaults - steps: - - test-binary-against-repo: - repo: cypress-example-piechopper - browser: firefox - command: "npm run cypress:run" - - test-binary-against-cypress-realworld-app: - <<: *defaults - resource_class: medium+ - steps: - - test-binary-against-rwa: - repo: cypress-realworld-app - browser: chrome - wait-on: http://localhost:3000 - - test-binary-as-specific-user: - <<: *defaults - steps: - - restore_workspace_binaries - # the user should be "node" - - run: whoami - - run: pwd - # prints the current user's effective user id - # for root it is 0 - # for other users it is a positive integer - - run: node -e 'console.log(process.geteuid())' - # make sure the binary and NPM package files are present - - run: ls -l - - run: ls -l cypress.zip cypress.tgz - - run: mkdir test-binary - - run: - name: Create new NPM package - working_directory: test-binary - command: npm init -y - - run: - # install NPM from built NPM package folder - name: Install Cypress - working_directory: test-binary - # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz - - run: - name: Cypress help - working_directory: test-binary - command: $(yarn bin cypress) help - - run: - name: Cypress info - working_directory: test-binary - command: $(yarn bin cypress) info - - run: - name: Add Cypress demo - working_directory: test-binary - command: npx @bahmutov/cly@1.9.0 init - - run: - name: Verify Cypress binary - working_directory: test-binary - command: DEBUG=cypress:cli $(yarn bin cypress) verify - - run: - name: Run Cypress binary - working_directory: test-binary - command: DEBUG=cypress:cli $(yarn bin cypress) run - - store-npm-logs - -linux-x64-workflow: &linux-x64-workflow - jobs: - - node_modules_install - - build: - context: test-runner:env-canary - requires: - - node_modules_install - - check-ts: - requires: - - build - - lint: - name: linux-lint - requires: - - build - - percy-finalize: - context: [test-runner:poll-circle-workflow, test-runner:percy] - required_env_var: PERCY_TOKEN # skips job if not defined (external PR) - requires: - - build - - lint-types: - requires: - - build - # unit, integration and e2e tests - - cli-visual-tests: - context: test-runner:percy - requires: - - build - - unit-tests: - requires: - - build - - unit-tests-release: - context: test-runner:npm-release - requires: - - build - - server-unit-tests: - requires: - - build - - server-integration-tests: - requires: - - build - - server-performance-tests: - requires: - - build - - system-tests-node-modules-install: - context: test-runner:performance-tracking - requires: - - build - - system-tests-chrome: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-electron: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-firefox: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-webkit: - context: test-runner:performance-tracking - requires: - - system-tests-node-modules-install - - system-tests-non-root: - context: test-runner:performance-tracking - executor: non-root-docker-user - requires: - - system-tests-node-modules-install - - driver-integration-tests-chrome: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-chrome-beta: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-firefox: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-electron: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-webkit: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-chrome-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-firefox-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-electron-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - # TODO: Implement WebKit network automation to fix the majority of these tests before re-enabling - # - driver-integration-tests-webkit-experimentalSessionAndOrigin: - # context: test-runner:cypress-record-key - # requires: - # - build - - run-frontend-shared-component-tests-chrome: - context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] - percy: true - requires: - - build - - run-launchpad-integration-tests-chrome: - context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] - percy: true - requires: - - build - - run-launchpad-component-tests-chrome: - context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] - percy: true - requires: - - build - - run-app-integration-tests-chrome: - context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] - percy: true - requires: - - build - - run-webpack-dev-server-integration-tests: - context: [test-runner:cypress-record-key, test-runner:percy] - requires: - - system-tests-node-modules-install - - run-vite-dev-server-integration-tests: - context: [test-runner:cypress-record-key, test-runner:percy] - requires: - - system-tests-node-modules-install - - run-app-component-tests-chrome: - context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] - percy: true - requires: - - build - - run-reporter-component-tests-chrome: - context: [test-runner:cypress-record-key, test-runner:percy] - percy: true - requires: - - build - - reporter-integration-tests: - context: [test-runner:cypress-record-key, test-runner:percy] - requires: - - build - - ui-components-integration-tests: - context: test-runner:cypress-record-key - requires: - - build - - npm-webpack-dev-server: - requires: - - system-tests-node-modules-install - - npm-vite-dev-server: - requires: - - build - - npm-webpack-preprocessor: - requires: - - build - - npm-webpack-batteries-included-preprocessor: - requires: - - build - - npm-vue: - requires: - - build - - npm-react: - requires: - - build - - npm-angular: - requires: - - build - - npm-mount-utils: - requires: - - build - - npm-create-cypress-tests: - requires: - - build - - npm-eslint-plugin-dev: - requires: - - build - - npm-cypress-schematic: - requires: - - build - # This release definition must be updated with any new jobs - # Any attempts to automate this are welcome - # If CircleCI provided an "after all" hook, then this wouldn't be necessary - - npm-release: - context: test-runner:npm-release - requires: - - build - - check-ts - - npm-angular - - npm-eslint-plugin-dev - - npm-create-cypress-tests - - npm-react - - npm-mount-utils - - npm-vue - - npm-webpack-batteries-included-preprocessor - - npm-webpack-preprocessor - - npm-vite-dev-server - - npm-webpack-dev-server - - npm-cypress-schematic - - lint-types - - linux-lint - - percy-finalize - - driver-integration-tests-firefox - - driver-integration-tests-chrome - - driver-integration-tests-chrome-beta - - driver-integration-tests-electron - - driver-integration-tests-firefox-experimentalSessionAndOrigin - - driver-integration-tests-chrome-experimentalSessionAndOrigin - - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin - - driver-integration-tests-electron-experimentalSessionAndOrigin - - system-tests-non-root - - system-tests-firefox - - system-tests-electron - - system-tests-chrome - - server-performance-tests - - server-integration-tests - - server-unit-tests - - test-kitchensink - - ui-components-integration-tests - - unit-tests - - unit-tests-release - - cli-visual-tests - - reporter-integration-tests - - run-app-component-tests-chrome - - run-app-integration-tests-chrome - - run-frontend-shared-component-tests-chrome - - run-launchpad-component-tests-chrome - - run-launchpad-integration-tests-chrome - - run-reporter-component-tests-chrome - - run-webpack-dev-server-integration-tests - - run-vite-dev-server-integration-tests - - # various testing scenarios, like building full binary - # and testing it on a real project - - test-against-staging: - context: test-runner:record-tests - <<: *mainBuildFilters - requires: - - build - - test-kitchensink: - requires: - - build - - test-kitchensink-against-staging: - context: test-runner:record-tests - <<: *mainBuildFilters - requires: - - build - - create-build-artifacts: - context: - - test-runner:upload - - test-runner:commit-status-checks - requires: - - build - - test-npm-module-on-minimum-node-version: - requires: - - create-build-artifacts - - test-types-cypress-and-jest: - requires: - - create-build-artifacts - - test-full-typescript-project: - requires: - - create-build-artifacts - - test-binary-against-kitchensink: - requires: - - create-build-artifacts - - test-npm-module-and-verify-binary: - <<: *mainBuildFilters - requires: - - create-build-artifacts - - test-binary-against-staging: - context: test-runner:record-tests - <<: *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: - - create-build-artifacts - - test-binary-against-todomvc-firefox: - <<: *mainBuildFilters - requires: - - create-build-artifacts - - test-binary-against-cypress-realworld-app: - <<: *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 - -linux-arm64-workflow: &linux-arm64-workflow - jobs: - - node_modules_install: - name: linux-arm64-node-modules-install - executor: linux-arm64 - resource_class: arm.medium - only-cache-for-root-user: true - - - build: - name: linux-arm64-build - executor: linux-arm64 - resource_class: arm.medium - requires: - - linux-arm64-node-modules-install - - - create-build-artifacts: - name: linux-arm64-create-build-artifacts - context: - - test-runner:upload - - test-runner:commit-status-checks - executor: linux-arm64 - resource_class: arm.medium - requires: - - linux-arm64-build - -darwin-x64-workflow: &darwin-x64-workflow - jobs: - - node_modules_install: - name: darwin-x64-node-modules-install - executor: mac - resource_class: macos.x86.medium.gen2 - only-cache-for-root-user: true - - - build: - name: darwin-x64-build - context: test-runner:env-canary - executor: mac - resource_class: macos.x86.medium.gen2 - requires: - - darwin-x64-node-modules-install - - - lint: - name: darwin-x64-lint - executor: mac - requires: - - darwin-x64-build - - - create-build-artifacts: - name: darwin-x64-create-build-artifacts - context: - - test-runner:sign-mac-binary - - test-runner:upload - - test-runner:commit-status-checks - executor: mac - resource_class: macos.x86.medium.gen2 - requires: - - darwin-x64-build - - - test-kitchensink: - name: darwin-x64-test-kitchensink - executor: mac - requires: - - darwin-x64-build - -darwin-arm64-workflow: &darwin-arm64-workflow - jobs: - - node_modules_install: - name: darwin-arm64-node-modules-install - executor: darwin-arm64 - resource_class: cypress-io/latest_m1 - only-cache-for-root-user: true - - - build: - name: darwin-arm64-build - executor: darwin-arm64 - resource_class: cypress-io/latest_m1 - requires: - - darwin-arm64-node-modules-install - - - create-build-artifacts: - name: darwin-arm64-create-build-artifacts - context: - - test-runner:sign-mac-binary - - test-runner:upload - - test-runner:commit-status-checks - executor: darwin-arm64 - resource_class: cypress-io/latest_m1 - requires: - - darwin-arm64-build - -windows-workflow: &windows-workflow - jobs: - - node_modules_install: - name: windows-node-modules-install - executor: windows - resource_class: windows.large - only-cache-for-root-user: true - - - build: - name: windows-build - context: test-runner:env-canary - executor: windows - resource_class: windows.large - requires: - - windows-node-modules-install - - - run-app-integration-tests-chrome: - name: windows-run-app-integration-tests-chrome - executor: windows - resource_class: windows.large - context: [test-runner:cypress-record-key, test-runner:launchpad-tests] - requires: - - windows-build - - - run-launchpad-integration-tests-chrome: - name: windows-run-launchpad-integration-tests-chrome - executor: windows - resource_class: windows.large - context: [test-runner:cypress-record-key, test-runner:launchpad-tests] - requires: - - windows-build - - - lint: - name: windows-lint - executor: windows - requires: - - windows-build - - - unit-tests: - name: windows-unit-tests - executor: windows - resource_class: windows.large - requires: - - windows-build - - - create-build-artifacts: - name: windows-create-build-artifacts - executor: windows - resource_class: windows.large - context: - - test-runner:sign-windows-binary - - test-runner:upload - - test-runner:commit-status-checks - requires: - - windows-build - - test-binary-against-kitchensink-chrome: - name: windows-test-binary-against-kitchensink-chrome - executor: windows - requires: - - windows-create-build-artifacts - -workflows: - linux-x64: - <<: *linux-x64-workflow - <<: *linux-x64-workflow-exclude-filters - linux-arm64: - <<: *linux-arm64-workflow - <<: *linux-arm64-workflow-filters - darwin-x64: - <<: *darwin-x64-workflow - <<: *darwin-workflow-filters - darwin-arm64: - <<: *darwin-arm64-workflow - <<: *darwin-workflow-filters - windows: - <<: *windows-workflow - <<: *windows-workflow-filters diff --git a/cli/.eslintignore b/cli/.eslintignore new file mode 100644 index 00000000000..a0fd3df705d --- /dev/null +++ b/cli/.eslintignore @@ -0,0 +1,19 @@ +**/__snapshots__ +/build + +# do not lint package.json, it incorrect re-orders the `exports` +# https://github.com/cypress-io/cypress/pull/26630 +package.json + +# cli/types is linted by tslint/dtslint +/types + +# these are all copied from dist'd builds from the individual libs +/angular +/angular-signals +/react +/react18 +/vue +/vue2 +/svelte +/mount-utils \ No newline at end of file diff --git a/cli/.eslintrc.json b/cli/.eslintrc.json index 0ec2a731da3..c3cecddc84c 100644 --- a/cli/.eslintrc.json +++ b/cli/.eslintrc.json @@ -1,5 +1,4 @@ { - "extends": "../.eslintrc.js", "rules": { "no-restricted-syntax": [ "error", diff --git a/cli/.gitignore b/cli/.gitignore index d13afcd7f1b..81806971abc 100644 --- a/cli/.gitignore +++ b/cli/.gitignore @@ -21,3 +21,4 @@ react* mount-utils angular svelte +angular-signals diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md new file mode 100644 index 00000000000..3894b5c6b12 --- /dev/null +++ b/cli/CHANGELOG.md @@ -0,0 +1,996 @@ + +## 13.13.1 + +_Released 7/16/2024 (PENDING)_ + +**Bugfixes:** + +- Fixed an issue where the ReadStream used to upload a Test Replay recording could erroneously be re-used when retrying in cases of retryable upload failures. Fixes [#29227](https://github.com/cypress-io/cypress/issues/29227) + +## 13.13.0 + +_Released 7/01/2024_ + +**Performance:** + +- Improved performance of `experimentalSourceRewriting` option. Fixed in [#29540](https://github.com/cypress-io/cypress/pull/29540). + +**Features:** + +- Adds Signal support for Angular Component Testing versions 17.2 and up. Addresses [#29264](https://github.com/cypress-io/cypress/issues/29264). + +**Bugfixes:** + +- Fixed an issue where Chrome launch instances would not recreate the browser CRI client correctly after recovering from an unexpected browser closure. Fixes [#27657](https://github.com/cypress-io/cypress/issues/27657). Fixed in [#29663](https://github.com/cypress-io/cypress/pull/29663). +- Fixed an issue where Firefox 129 (Firefox Nightly) would not launch with Cypress. Fixes [#29713](https://github.com/cypress-io/cypress/issues/29713). Fixed in [#29720](https://github.com/cypress-io/cypress/pull/29720). + +**Dependency Updates:** + +- Updated `launch-editor` from `2.3.0` to `2.8.0`. Addressed in [#29770](https://github.com/cypress-io/cypress/pull/29770). +- Updated `memfs` from `3.4.12` to `3.5.3`. Addressed in [#29746](https://github.com/cypress-io/cypress/pull/29746). +- Updated `tmp` from `0.2.1` to `0.2.3`. Addresses [#29693](https://github.com/cypress-io/cypress/issues/29693). +- Updated `ws` from `5.2.3` to `5.2.4`. Addressed in [#29698](https://github.com/cypress-io/cypress/pull/29698). + +## 13.12.0 + +_Released 6/18/2024_ + +**Features:** + +- Added Component Testing support for Angular version 18. Addresses [#29309](https://github.com/cypress-io/cypress/issues/29309). + +**Bugfixes:** + +- We now trigger `input` and `change` events when typing `{upArrow}` and `{downArrow}` via `.type()` on `input[type=number]` elements. Fixes [#29611](https://github.com/cypress-io/cypress/issues/29611) +- Fixed an issue where auto scrolling the reporter would sometimes be disabled without the user's intent. Fixes [#25084](https://github.com/cypress-io/cypress/issues/25084). +- Fixed an issue where `inlineSourceMaps` was still being used when `sourceMaps` was provided in a users typescript config for typescript version 5. Fixes [#26203](https://github.com/cypress-io/cypress/issues/26203). +- When capture protocol script fails verification, an appropriate error is now displayed. Previously, an error regarding Test Replay archive location was shown. Addressed in [#29603](https://github.com/cypress-io/cypress/pull/29603). +- Fixed an issue where receiving HTTP responses with invalid headers raised an error. Now cypress removes the invalid headers and gives a warning in the console with debug mode on. Fixes [#28865](https://github.com/cypress-io/cypress/issues/28865). + +**Misc:** + +- Report afterSpec durations to Cloud API when running in record mode with Test Replay enabled. Addressed in [#29500](https://github.com/cypress-io/cypress/pull/29500). + +**Dependency Updates:** + +- Updated firefox-profile from `4.3.1` to `4.6.0`. Addressed in [#29662](https://github.com/cypress-io/cypress/pull/29662). +- Updated typescript from `4.7.4` to `5.3.3`. Addressed in [#29568](https://github.com/cypress-io/cypress/pull/29568). +- Updated url-parse from `1.5.9` to `1.5.10`. Addressed in [#29650](https://github.com/cypress-io/cypress/pull/29650). + +## 13.11.0 + +_Released 6/4/2024_ + +**Performance:** + +- Improved performance when setting console props within `Cypress.log`. Addressed in [#29501](https://github.com/cypress-io/cypress/pull/29501). + +**Features:** + +- Added support for [Next.js 14](https://nextjs.org/blog/next-14) for component testing. Addresses [#28185](https://github.com/cypress-io/cypress/issues/28185). +- Added an `IGNORE_CHROME_PREFERENCES` environment variable to ignore Chrome preferences when launching Chrome. Addresses [#29330](https://github.com/cypress-io/cypress/issues/29330). + +**Bugfixes:** + +- Fixed a situation where the Launchpad would hang if the project config had not been loaded when the Launchpad first queries the current project. Fixes [#29486](https://github.com/cypress-io/cypress/issues/29486). +- Pre-emptively fix behavior with Chrome for when `unload` events are forcefully deprecated by using `pagehide` as a proxy. Fixes [#29241](https://github.com/cypress-io/cypress/issues/29241). + + +**Misc:** + +- Enhanced the type definitions available to `cy.intercept` and `cy.wait`. The `body` property of both the request and response in an interception can optionally be specified with user-defined types. Addresses [#29507](https://github.com/cypress-io/cypress/issues/29507). + +## 13.10.0 + +_Released 5/21/2024_ + +**Features:** + +- Added support for `vite` `v5` to `@cypress/vite-dev-server`. Addresses [#28347](https://github.com/cypress-io/cypress/issues/28347). + +**Bugfixes:** + +- Fixed an issue where orphaned Electron processes were inadvertently terminating the browser's CRI client. Fixes [#28397](https://github.com/cypress-io/cypress/issues/28397). Fixed in [#29515](https://github.com/cypress-io/cypress/pull/29515). +- Fixed an issue where Cypress would use the wrong URL to upload Test Replay recordings when it wasn't able to determine the upload URL. It now displays an error when the upload URL cannot be determined, rather than a "Request Entity Too Large" error. Addressed in [#29512](https://github.com/cypress-io/cypress/pull/29512). +- Fixed an issue where Cypress was unable to search in the Specs list for files or folders containing numbers. Fixes [#29034](https://github.com/cypress-io/cypress/issues/29034). +- Fixed an issue setting the `x-cypress-file-path` header when there are invalid header characters in the file path. Fixes [#25839](https://github.com/cypress-io/cypress/issues/25839). +- Fixed the display of some command assertions. Fixed in [#29517](https://github.com/cypress-io/cypress/pull/29517). + +**Dependency Updates:** + +- Updated js-cookie from `2.2.1` to `3.0.5`. Addressed in [#29497](https://github.com/cypress-io/cypress/pull/29497). +- Updated randomstring from `1.1.5` to `1.3.0`. Addressed in [#29503](https://github.com/cypress-io/cypress/pull/29503). + +## 13.9.0 + +_Released 5/7/2024_ + +**Features:** + +- Added more descriptive error messages when Test Replay fails to record or upload. Addresses [#29022](https://github.com/cypress-io/cypress/issues/29022). + +**Bugfixes:** + +- Fixed a bug where promises rejected with `undefined` were failing inside `cy.origin()`. Addresses [#23937](https://github.com/cypress-io/cypress/issues/23937). +- We now pass the same default Chromium flags to Electron as we do to Chrome. As a result of this change, the application under test's `navigator.webdriver` property will now correctly be `true` when testing in Electron. Fixes [#27939](https://github.com/cypress-io/cypress/issues/27939). +- Fixed network issues in requests using fetch for users where Cypress is run behind a proxy that performs HTTPS decryption (common among corporate proxies). Fixes [#29171](https://github.com/cypress-io/cypress/issues/29171). +- Fixed an issue where extra windows weren't being closed between specs in Firefox causing potential issues in subsequent specs. Fixes [#29473](https://github.com/cypress-io/cypress/issues/29473). + +**Misc:** + +- Improved accessibility of the Cypress App in some areas. Addressed in [#29322](https://github.com/cypress-io/cypress/pull/29322). + +**Dependency Updates:** + +- Updated electron from `27.1.3` to `27.3.10` to address [CVE-2024-3156](https://nvd.nist.gov/vuln/detail/CVE-2024-3156). Addressed in [#29431](https://github.com/cypress-io/cypress/pull/29431). + +## 13.8.1 + +_Released 4/23/2024_ + +**Performance:** + +- Fixed a performance issue with activated service workers that aren't controlling clients which could lead to correlation timeouts. Fixes [#29333](https://github.com/cypress-io/cypress/issues/29333) and [#29126](https://github.com/cypress-io/cypress/issues/29126). + +**Bugfixes:** + +- Fixed a regression introduced in [`13.6.0`](https://docs.cypress.io/guides/references/changelog#13-6-0) where Cypress would occasionally exit with status code 1, even when a test run was successful, due to an unhandled WebSocket exception (`Error: WebSocket connection closed`). Addresses [#28523](https://github.com/cypress-io/cypress/issues/28523). +- Fixed an issue where Cypress would hang on some commands when an invalid `timeout` option was provided. Fixes [#29323](https://github.com/cypress-io/cypress/issues/29323). + +**Misc:** + +- `.its()` type now excludes null and undefined. Fixes [#28872](https://github.com/cypress-io/cypress/issues/28872). + +**Dependency Updates:** + +- Updated zod from `3.20.3` to `3.22.5`. Addressed in [#29367](https://github.com/cypress-io/cypress/pull/29367). + +## 13.8.0 + +_Released 4/18/2024_ + +**Features:** + +- Added support for `webpack-dev-server` `v5` to `@cypress/webpack-dev-server`. Addresses [#29305](https://github.com/cypress-io/cypress/issues/29305). + +**Bugfixes:** + +- Fixed a regression introduced in [`13.7.3`](https://docs.cypress.io/guides/references/changelog#13-7-3) where Cypress could hang handling long assertion messages. Fixes [#29350](https://github.com/cypress-io/cypress/issues/29350). + +**Misc:** + +- The [`SEMAPHORE_GIT_PR_NUMBER`](https://docs.semaphoreci.com/ci-cd-environment/environment-variables/#semaphore_git_pr_number) environment variable from [Semaphore](https://semaphoreci.com/) CI is now captured to display the linked PR number in the Cloud. Addressed in [#29314](https://github.com/cypress-io/cypress/pull/29314). + +## 13.7.3 + +_Released 4/11/2024_ + +**Bugfixes:** + +- Fixed an issue where asserts with custom messages weren't displaying properly. Fixes [#29167](https://github.com/cypress-io/cypress/issues/29167). +- Fixed and issue where Cypress launch arguments were not being escaped correctly with multiple values inside quotes. Fixes [#27454](https://github.com/cypress-io/cypress/issues/27454). + +**Misc:** + +- Updated the Chrome flags to not show the "Enhanced Ad Privacy" dialog. Addresses [#29199](https://github.com/cypress-io/cypress/issues/29199). +- Suppresses benign warnings that reference Vulkan on GPU-less hosts. Addresses [#29085](https://github.com/cypress-io/cypress/issues/29085). Addressed in [#29278](https://github.com/cypress-io/cypress/pull/29278). + +## 13.7.2 + +_Released 4/2/2024_ + +**Performance:** + +- Improvements to Test Replay upload resiliency. Fixes [#28890](https://github.com/cypress-io/cypress/issues/28890). Addressed in [#29174](https://github.com/cypress-io/cypress/pull/29174) + +**Bugfixes:** + +- Fixed an issue where Cypress was not executing beyond the first spec in `cypress run` for versions of Firefox 124 and up when a custom user agent was provided. Fixes [#29190](https://github.com/cypress-io/cypress/issues/29190). +- Fixed a bug where fields using arrays in `cypress.config` are not correctly processed. Fixes [#27103](https://github.com/cypress-io/cypress/issues/27103). Fixed in [#27312](https://github.com/cypress-io/cypress/pull/27312). +- Fixed a hang where Cypress would run indefinitely while recording to the cloud when CDP disconnects during the middle of a test. Fixes [#29209](https://github.com/cypress-io/cypress/issues/29209). +- Fixed a bug where option values containing quotation marks could not be selected. Fixes [#29213](https://github.com/cypress-io/cypress/issues/29213) + +**Dependency Updates:** + +- Updated express from `4.17.3` to `4.19.2`. Addressed in [#29211](https://github.com/cypress-io/cypress/pull/29211). + +## 13.7.1 + +_Released 3/21/2024_ + +**Bugfixes:** + +- Fixed an issue where Cypress was not executing beyond the first spec in `cypress run` for versions of Firefox 124 and up. Fixes [#29172](https://github.com/cypress-io/cypress/issues/29172). +- Fixed an issue blurring shadow dom elements. Fixed in [#29125](https://github.com/cypress-io/cypress/pull/29125). + +**Dependency Updates:** + +- Updated jose from `4.11.2` to `4.15.5`. Addressed in [#29086](https://github.com/cypress-io/cypress/pull/29086). + +## 13.7.0 + +_Released 3/13/2024_ + +**Features:** + +- Added shadow DOM snapshot support within Test Replay in order to highlight elements correctly within the Cypress reporter. Addressed in [#28823](https://github.com/cypress-io/cypress/pull/28823). +- Added TypeScript support for [Vue 2.7+](https://github.com/vuejs/vue/blob/main/CHANGELOG.md#270-2022-07-01). Addresses [#28591](https://github.com/cypress-io/cypress/issues/28591). +- Adds additional context to error messages displayed when Test Replay artifacts fail to upload. Addressed in [#28986](https://github.com/cypress-io/cypress/pull/28986) + +**Performance:** + +- Fixed a performance regression from [`13.6.3`](https://docs.cypress.io/guides/references/changelog#13-6-3) where unhandled service worker requests may not correlate correctly. Fixes [#28868](https://github.com/cypress-io/cypress/issues/28868). +- Reduces the number of attempts to retry failed Test Replay artifact uploads from 8 to 3, to reduce time spent on artifact upload attempts that will not succeed. Addressed in [#28986](https://github.com/cypress-io/cypress/pull/28986) + +**Bugfixes:** + +- Changed screenshot capture behavior in Chromium to activate the main Cypress tab before capturing. This prevents screenshot capture from timing out in certain situations. Fixed in [#29038](https://github.com/cypress-io/cypress/pull/29038). Fixes [#5016](https://github.com/cypress-io/cypress/issues/5016) +- Fixed an issue where `.click()` commands on children of disabled elements would still produce "click" events -- even without `{ force: true }`. Fixes [#28788](https://github.com/cypress-io/cypress/issues/28788). +- Changed RequestBody type to allow for boolean and null literals to be passed as body values. [#28789](https://github.com/cypress-io/cypress/issues/28789) + +**Misc:** + +- Changed Component Testing scaffolding instruction to `pnpm add` to add framework dependencies when a project uses pnpm as package manager. Addresses [#29052](https://github.com/cypress-io/cypress/issues/29052). +- Command messages in the Cypress command logs will now truncate display at 100 lines instead of 50. Fixes [#29023](https://github.com/cypress-io/cypress/issues/29023). +- Capture the `beforeTest` timestamp inside the browser for the purposes of accurately determining test start for Test Replay. Addressed in [#29061](https://github.com/cypress-io/cypress/pull/29061). + +**Dependency Updates:** + +- Updated jimp from `0.14.0` to `0.22.12`. Addressed in [#29055](https://github.com/cypress-io/cypress/pull/29055). +- Updated http-proxy-middleware from `2.0.4` to `2.0.6`. Addressed in [#28902](https://github.com/cypress-io/cypress/pull/28902). +- Updated signal-exit from `3.0.3` to `3.0.7`. Addressed in [#28979](https://github.com/cypress-io/cypress/pull/28979). + +## 13.6.6 + +_Released 2/22/2024_ + +**Bugfixes:** + +- Fixed an issue where `cypress verify` was failing for `nx` users. Fixes [#28982](https://github.com/cypress-io/cypress/issues/28982). + +## 13.6.5 + +_Released 2/20/2024_ + +**Bugfixes:** + +- Fixed tests hanging when the Chrome browser extension is disabled. Fixes [#28392](https://github.com/cypress-io/cypress/issues/28392). +- Fixed an issue which caused the browser to relaunch after closing the browser from the Launchpad. Fixes [#28852](https://github.com/cypress-io/cypress/issues/28852). +- Fixed an issue with the unzip promise never being rejected when an empty error happens. Fixed in [#28850](https://github.com/cypress-io/cypress/pull/28850). +- Fixed a regression introduced in [`13.6.3`](https://docs.cypress.io/guides/references/changelog#13-6-3) where Cypress could crash when processing service worker requests through our proxy. Fixes [#28950](https://github.com/cypress-io/cypress/issues/28950). +- Fixed incorrect type definition of `dom.getContainsSelector`. Fixed in [#28339](https://github.com/cypress-io/cypress/pull/28339). + +**Misc:** + +- Improved accessibility of the Cypress App in some areas. Addressed in [#28774](https://github.com/cypress-io/cypress/pull/28774). +- Changed references of LayerCI to webapp.io. Addressed in [#28874](https://github.com/cypress-io/cypress/pull/28874). + +**Dependency Updates:** + +- Upgraded `electron` from `25.8.4` to `27.1.3`. +- Upgraded bundled Node.js version from `18.15.0` to `18.17.0`. +- Upgraded bundled Chromium version from `114.0.5735.289` to `118.0.5993.117`. +- Updated buffer from `5.6.0` to `5.7.1`. Addressed in [#28934](https://github.com/cypress-io/cypress/pull/28934). +- Updated [`duplexify`](https://www.npmjs.com/package/duplexify) from `4.1.1` to `4.1.2`. Addressed in [#28941](https://github.com/cypress-io/cypress/pull/28941). +- Updated [`is-ci`](https://www.npmjs.com/package/is-ci) from `3.0.0` to `3.0.1`. Addressed in [#28933](https://github.com/cypress-io/cypress/pull/28933). + +## 13.6.4 + +_Released 1/30/2024_ + +**Performance:** + +- Fixed a performance regression from [`13.3.2`](https://docs.cypress.io/guides/references/changelog#13.3.2) where aborted requests may not correlate correctly. Fixes [#28734](https://github.com/cypress-io/cypress/issues/28734). + +**Bugfixes:** + +- Fixed an issue with capturing assets for Test Replay when service workers are registered in Cypress support files. This issue would cause styles to not render properly in Test Replay. Fixes [#28747](https://github.com/cypress-io/cypress/issues/28747). + +**Misc:** + +- Added missing properties to the `Cypress.spec` interface for TypeScript users. Addresses [#27835](https://github.com/cypress-io/cypress/issues/27835). + +## 13.6.3 + +_Released 1/16/2024_ + +**Bugfixes:** + +- Force `moduleResolution` to `node` when `typescript` projects are detected to correctly run Cypress. This change should not have a large impact as `commonjs` is already forced when `ts-node` is registered. This fix does not impact the ESM Typescript configuration loader. Fixes [#27731](https://github.com/cypress-io/cypress/issues/27731). +- No longer wait for additional frames when recording a video for a spec that was skipped by the Cloud due to Auto Cancellation. Fixes [#27898](https://github.com/cypress-io/cypress/issues/27898). +- Now `node_modules` will not be ignored if a project path or a provided path to spec files contains it. Fixes [#23616](https://github.com/cypress-io/cypress/issues/23616). +- Updated display of assertions and commands with a URL argument to escape markdown formatting so that values are displayed as is and assertion values display as bold. Fixes [#24960](https://github.com/cypress-io/cypress/issues/24960) and [#28100](https://github.com/cypress-io/cypress/issues/28100). +- When generating assertions via Cypress Studio, the preview of the generated assertions now correctly displays the past tense of 'expected' instead of 'expect'. Fixed in [#28593](https://github.com/cypress-io/cypress/pull/28593). +- Fixed a regression in [`13.6.2`](https://docs.cypress.io/guides/references/changelog#13.6.2) where the `body` element was not highlighted correctly in Test Replay. Fixed in [#28627](https://github.com/cypress-io/cypress/pull/28627). +- Correctly sync `Cypress.currentRetry` with secondary origin so test retries that leverage `cy.origin()` render logs as expected. Fixes [#28574](https://github.com/cypress-io/cypress/issues/28574). +- Fixed an issue where some cross-origin logs, like assertions or cy.clock(), were getting too many dom snapshots. Fixes [#28609](https://github.com/cypress-io/cypress/issues/28609). +- Fixed asset capture for Test Replay for requests that are routed through service workers. This addresses an issue where styles were not being applied properly in Test Replay and `cy.intercept()` was not working properly for requests in this scenario. Fixes [#28516](https://github.com/cypress-io/cypress/issues/28516). +- Fixed an issue where visiting an `http://` site would result in an infinite reload/redirect loop in Chrome 114+. Fixes [#25891](https://github.com/cypress-io/cypress/issues/25891). +- Fixed an issue where requests made from extra tabs do not include their original headers. Fixes [#28641](https://github.com/cypress-io/cypress/issues/28641). +- Fixed an issue where `cy.wait()` would sometimes throw an error reading a property of undefined when returning responses. Fixes [#28233](https://github.com/cypress-io/cypress/issues/28233). + +**Performance:** + +- Fixed a performance regression from [`13.3.2`](https://docs.cypress.io/guides/references/changelog#13.3.2) where requests may not correlate correctly when test isolation is off. Fixes [#28545](https://github.com/cypress-io/cypress/issues/28545). + +**Dependency Updates:** + +- Remove dependency on `@types/node` package. Addresses [#28473](https://github.com/cypress-io/cypress/issues/28473). +- Updated [`@cypress/unique-selector`](https://www.npmjs.com/package/@cypress/unique-selector) to include a performance optimization. It's possible this could improve performance of the selector playground. Addressed in [#28571](https://github.com/cypress-io/cypress/pull/28571). +- Replace [`CircularJSON`](https://www.npmjs.com/package/circular-json) with its successor [`flatted`](https://www.npmjs.com/package/flatted) version `3.2.9`. This resolves decoding issues observed in complex objects sent from the browser. Addressed in [#28683](https://github.com/cypress-io/cypress/pull/28683). +- Updated [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3) from `8.7.0` to `9.2.2` to fix macOS Catalina issues. Addresses [#28697](https://github.com/cypress-io/cypress/issues/28697). + +**Misc:** + +- Improved accessibility of some areas of the Cypress App. Addressed in [#28628](https://github.com/cypress-io/cypress/pull/28628). +- Updated some documentation links to go through on.cypress.io. Addressed in [#28623](https://github.com/cypress-io/cypress/pull/28623). + + +## 13.6.2 + +_Released 12/26/2023_ + +**Bugfixes:** + +- Fixed a regression in [`13.6.1`](https://docs.cypress.io/guides/references/changelog#13.6.1) where a malformed URI would crash Cypress. Fixes [#28521](https://github.com/cypress-io/cypress/issues/28521). +- Fixed a regression in [`12.4.0`](https://docs.cypress.io/guides/references/changelog#12.4.0) where erroneous `
` tags were displaying in error messages in the Command Log making them less readable. Fixes [#28452](https://github.com/cypress-io/cypress/issues/28452). + +**Performance:** + +- Improved performance when finding unique selectors for command log snapshots for Test Replay. Addressed in [#28536](https://github.com/cypress-io/cypress/pull/28536). + +**Dependency Updates:** + +- Updated ts-node from `10.9.1` to `10.9.2`. Cypress will longer error during `cypress run` or `cypress open` when using Typescript 5.3.2+ with `extends` in `tsconfig.json`. Addresses [#28385](https://github.com/cypress-io/cypress/issues/28385). + +## 13.6.1 + +_Released 12/5/2023_ + +**Bugfixes:** + +- Fixed an issue where pages or downloads opened in a new tab were missing basic auth headers. Fixes [#28350](https://github.com/cypress-io/cypress/issues/28350). +- Fixed an issue where request logging would default the `message` to the `args` of the currently running command even though those `args` would not apply to the request log and are not displayed. If the `args` are sufficiently large (e.g. when running the `cy.task` from the [code-coverage](https://github.com/cypress-io/code-coverage/) plugin) there could be performance/memory implications. Addressed in [#28411](https://github.com/cypress-io/cypress/pull/28411). +- Fixed an issue where commands would fail with the error `must only be invoked from the spec file or support file` if the project's `baseUrl` included basic auth credentials. Fixes [#27457](https://github.com/cypress-io/cypress/issues/27457) and [#28336](https://github.com/cypress-io/cypress/issues/28336). +- Fixed an issue where some URLs would timeout in pre-request correlation. Addressed in [#28427](https://github.com/cypress-io/cypress/pull/28427). +- Cypress will now correctly log errors and debug logs on Linux machines. Fixes [#5051](https://github.com/cypress-io/cypress/issues/5051) and [#24713](https://github.com/cypress-io/cypress/issues/24713). + +**Misc:** + +- Artifact upload duration is now reported to Cypress Cloud. Fixes [#28238](https://github.com/cypress-io/cypress/issues/28238). Addressed in [#28418](https://github.com/cypress-io/cypress/pull/28418). + +## 13.6.0 + +_Released 11/21/2023_ + +**Features:** + +- Added an activity indicator to CLI output when artifacts (screenshots, videos, or Test Replay) are being uploaded to the cloud. Addresses [#28239](https://github.com/cypress-io/cypress/issues/28239). Addressed in [#28277](https://github.com/cypress-io/cypress/pull/28277). +- When artifacts are uploaded to the Cypress Cloud, the duration of each upload will be displayed in the terminal. Addresses [#28237](https://github.com/cypress-io/cypress/issues/28237). + +**Bugfixes:** + +- We now allow absolute paths when setting `component.indexHtmlFile` in the Cypress config. Fixes [#27750](https://github.com/cypress-io/cypress/issues/27750). +- Fixed an issue where dynamic intercept aliases now show with alias name instead of "no alias" in driver. Addresses [#24653](https://github.com/cypress-io/cypress/issues/24653) +- Fixed an issue where [aliasing individual requests](https://docs.cypress.io/api/commands/intercept#Aliasing-individual-requests) with `cy.intercept()` led to an error when retrieving all of the aliases with `cy.get(@alias.all)` . Addresses [#25448](https://github.com/cypress-io/cypress/issues/25448) +- The URL of the application under test and command error "Learn more" links now open externally instead of in the Cypress-launched browser. Fixes [#24572](https://github.com/cypress-io/cypress/issues/24572). +- Fixed issue where some URLs would timeout in pre-request correlation. Addressed in [#28354](https://github.com/cypress-io/cypress/pull/28354). + +**Misc:** + +- Browser tabs and windows other than the Cypress tab are now closed between tests in Chromium-based browsers. Addressed in [#28204](https://github.com/cypress-io/cypress/pull/28204). +- Cypress now ensures the main browser tab is active before running each command in Chromium-based browsers. Addressed in [#28334](https://github.com/cypress-io/cypress/pull/28334). + +**Dependency Updates:** + +- Upgraded [`chrome-remote-interface`](https://www.npmjs.com/package/chrome-remote-interface) from `0.31.3` to `0.33.0` to increase the max payload from 100MB to 256MB. Addressed in [#27998](https://github.com/cypress-io/cypress/pull/27998). + +## 13.5.1 + +_Released 11/14/2023_ + +**Bugfixes:** + +- Fixed a regression in [`13.5.0`](https://docs.cypress.io/guides/references/changelog#13.5.0) where requests cached within a given spec may take longer to load than they did previously. Addresses [#28295](https://github.com/cypress-io/cypress/issues/28295). +- Fixed an issue where pages opened in a new tab were missing response headers, causing them not to load properly. Fixes [#28293](https://github.com/cypress-io/cypress/issues/28293) and [#28303](https://github.com/cypress-io/cypress/issues/28303). +- We now pass a flag to Chromium browsers to disable default component extensions. This is a common flag passed during browser automation. Fixed in [#28294](https://github.com/cypress-io/cypress/pull/28294). + +## 13.5.0 + +_Released 11/8/2023_ + +**Features:** + + - Added Component Testing support for [Angular](https://angular.io/) version 17. Addresses [#28153](https://github.com/cypress-io/cypress/issues/28153). + +**Bugfixes:** + +- Fixed an issue in chromium based browsers, where global style updates can trigger flooding of font face requests in DevTools and Test Replay. This can affect performance due to the flooding of messages in CDP. Fixes [#28150](https://github.com/cypress-io/cypress/issues/28150) and [#28215](https://github.com/cypress-io/cypress/issues/28215). +- Fixed a regression in [`13.3.3`](https://docs.cypress.io/guides/references/changelog#13.3.3) where Cypress would hang on loading shared workers when using `cy.reload` to reload the page. Fixes [#28248](https://github.com/cypress-io/cypress/issues/28248). +- Fixed an issue where network requests made from tabs, or windows other than the main Cypress tab, would be delayed. Fixes [#28113](https://github.com/cypress-io/cypress/issues/28113). +- Fixed an issue with 'other' targets (e.g. pdf documents embedded in an object tag) not fully loading. Fixes [#28228](https://github.com/cypress-io/cypress/issues/28228) and [#28162](https://github.com/cypress-io/cypress/issues/28162). +- Fixed an issue where clicking a link to download a file could cause a page load timeout when the download attribute was missing. Note: download behaviors in experimental Webkit are still an issue. Fixes [#14857](https://github.com/cypress-io/cypress/issues/14857). +- Fixed an issue to account for canceled and failed downloads to correctly reflect these status in Command log as a download failure where previously it would be pending. Fixed in [#28222](https://github.com/cypress-io/cypress/pull/28222). +- Fixed an issue determining visibility when an element is hidden by an ancestor with a shared edge. Fixes [#27514](https://github.com/cypress-io/cypress/issues/27514). +- We now pass a flag to Chromium browsers to disable Chrome translation, both the manual option and the popup prompt, when a page with a differing language is detected. Fixes [#28225](https://github.com/cypress-io/cypress/issues/28225). +- Stopped processing CDP events at the end of a spec when Test Isolation is off and Test Replay is enabled. Addressed in [#28213](https://github.com/cypress-io/cypress/pull/28213). + +## 13.4.0 + +_Released 10/30/2023_ + +**Features:** + +- Introduced experimental configuration options for advanced retry logic: adds `experimentalStrategy` and `experimentalOptions` keys to the `retry` configuration key. See [Experimental Flake Detection Features](https://docs.cypress.io/guides/references/experiments/#Experimental-Flake-Detection-Features) in the documentation. Addressed in [#27930](https://github.com/cypress-io/cypress/pull/27930). + +**Bugfixes:** + +- Fixed a regression in [`13.3.2`](https://docs.cypress.io/guides/references/changelog#13.3.2) where Cypress would crash with 'Inspected target navigated or closed' or 'Session with given id not found'. Fixes [#28141](https://github.com/cypress-io/cypress/issues/28141) and [#28148](https://github.com/cypress-io/cypress/issues/28148). + +## 13.3.3 + +_Released 10/24/2023_ + +**Bugfixes:** + +- Fixed a performance regression in `13.3.1` with proxy correlation timeouts and requests issued from web and shared workers. Fixes [#28104](https://github.com/cypress-io/cypress/issues/28104). +- Fixed a performance problem with proxy correlation when requests get aborted and then get miscorrelated with follow up requests. Addressed in [#28094](https://github.com/cypress-io/cypress/pull/28094). +- Fixed a regression in [10.0.0](#10.0.0), where search would not find a spec if the file name contains "-" or "\_", but search prompt contains " " instead (e.g. search file "spec-file.cy.ts" with prompt "spec file"). Fixes [#25303](https://github.com/cypress-io/cypress/issues/25303). + +## 13.3.2 + +_Released 10/18/2023_ + +**Bugfixes:** + +- Fixed a performance regression in `13.3.1` with proxy correlation timeouts and requests issued from service workers. Fixes [#28054](https://github.com/cypress-io/cypress/issues/28054) and [#28056](https://github.com/cypress-io/cypress/issues/28056). +- Fixed an issue where proxy correlation would leak over from a previous spec causing performance problems, `cy.intercept` problems, and Test Replay asset capturing issues. Addressed in [#28060](https://github.com/cypress-io/cypress/pull/28060). +- Fixed an issue where redirects of requests that knowingly don't have CDP traffic should also be assumed to not have CDP traffic. Addressed in [#28060](https://github.com/cypress-io/cypress/pull/28060). +- Fixed an issue with Accept Encoding headers by forcing gzip when no accept encoding header is sent and using identity if gzip is not sent. Fixes [#28025](https://github.com/cypress-io/cypress/issues/28025). + +**Dependency Updates:** + +- Upgraded [`@babel/core`](https://www.npmjs.com/package/@babel/core) from `7.22.9` to `7.23.2` to address the [SNYK-JS-SEMVER-3247795](https://snyk.io/vuln/SNYK-JS-SEMVER-3247795) security vulnerability. Addressed in [#28063](https://github.com/cypress-io/cypress/pull/28063). +- Upgraded [`@babel/traverse`](https://www.npmjs.com/package/@babel/traverse) from `7.22.8` to `7.23.2` to address the [SNYK-JS-BABELTRAVERSE-5962462](https://snyk.io/vuln/SNYK-JS-BABELTRAVERSE-5962462) security vulnerability. Addressed in [#28063](https://github.com/cypress-io/cypress/pull/28063). +- Upgraded [`react-docgen`](https://www.npmjs.com/package/react-docgen) from `6.0.0-alpha.3` to `6.0.4` to address the [SNYK-JS-BABELTRAVERSE-5962462](https://snyk.io/vuln/SNYK-JS-BABELTRAVERSE-5962462) security vulnerability. Addressed in [#28063](https://github.com/cypress-io/cypress/pull/28063). + +## 13.3.1 + +_Released 10/11/2023_ + +**Bugfixes:** + +- Fixed an issue where requests were correlated in the wrong order in the proxy. This could cause an issue where the wrong request is used for `cy.intercept` or assets (e.g. stylesheets or images) may not properly be available in Test Replay. Addressed in [#27892](https://github.com/cypress-io/cypress/pull/27892). +- Fixed an issue where a crashed Chrome renderer can cause the Test Replay recorder to hang. Addressed in [#27909](https://github.com/cypress-io/cypress/pull/27909). +- Fixed an issue where multiple responses yielded from calls to `cy.wait()` would sometimes be out of order. Fixes [#27337](https://github.com/cypress-io/cypress/issues/27337). +- Fixed an issue where requests were timing out in the proxy. This could cause an issue where the wrong request is used for `cy.intercept` or assets (e.g. stylesheets or images) may not properly be available in Test Replay. Addressed in [#27976](https://github.com/cypress-io/cypress/pull/27976). +- Fixed an issue where Test Replay couldn't record tests due to issues involving `GLIBC`. Fixed deprecation warnings during the rebuild of better-sqlite3. Fixes [#27891](https://github.com/cypress-io/cypress/issues/27891) and [#27902](https://github.com/cypress-io/cypress/issues/27902). +- Enables test replay for executed specs in runs that have a spec that causes a browser crash. Addressed in [#27786](https://github.com/cypress-io/cypress/pull/27786). + +## 13.3.0 + +_Released 09/27/2023_ + +**Features:** + + - Introduces new layout for Runs page providing additional run information. Addresses [#27203](https://github.com/cypress-io/cypress/issues/27203). + +**Bugfixes:** + +- Fixed an issue where actionability checks trigger a flood of font requests. Removing the font requests has the potential to improve performance and removes clutter from Test Replay. Addressed in [#27860](https://github.com/cypress-io/cypress/pull/27860). +- Fixed network stubbing not permitting status code 999. Fixes [#27567](https://github.com/cypress-io/cypress/issues/27567). Addressed in [#27853](https://github.com/cypress-io/cypress/pull/27853). + +## 13.2.0 + +_Released 09/12/2023_ + +**Features:** + + - Adds support for Nx users who want to run Angular Component Testing in parallel. Addressed in [#27723](https://github.com/cypress-io/cypress/pull/27723). + +**Bugfixes:** + +- Edge cases where `cy.intercept()` would not properly intercept and asset response bodies would not properly be captured for Test Replay have been addressed. Addressed in [#27771](https://github.com/cypress-io/cypress/pull/27771). +- Fixed an issue where `enter`, `keyup`, and `space` events were not triggering `click` events properly in some versions of Firefox. Addressed in [#27715](https://github.com/cypress-io/cypress/pull/27715). +- Fixed a regression in `13.0.0` where tests using Basic Authorization can potentially hang indefinitely on chromium browsers. Addressed in [#27781](https://github.com/cypress-io/cypress/pull/27781). +- Fixed a regression in `13.0.0` where component tests using an intercept that matches all requests can potentially hang indefinitely. Addressed in [#27788](https://github.com/cypress-io/cypress/pull/27788). + +**Dependency Updates:** + +- Upgraded Electron from `21.0.0` to `25.8.0`, which updates bundled Chromium from `106.0.5249.51` to `114.0.5735.289`. Additionally, the Node version binary has been upgraded from `16.16.0` to `18.15.0`. This does **NOT** have an impact on the node version you are using with Cypress and is merely an internal update to the repository & shipped binary. Addressed in [#27715](https://github.com/cypress-io/cypress/pull/27715). Addresses [#27595](https://github.com/cypress-io/cypress/issues/27595). + +## 13.1.0 + +_Released 08/31/2023_ + +**Features:** + + - Introduces a status icon representing the `latest` test run in the Sidebar for the Runs Page. Addresses [#27206](https://github.com/cypress-io/cypress/issues/27206). + +**Bugfixes:** + +- Fixed a regression introduced in Cypress [13.0.0](#13-0-0) where the [Module API](https://docs.cypress.io/guides/guides/module-api), [`after:run`](https://docs.cypress.io/api/plugins/after-run-api), and [`after:spec`](https://docs.cypress.io/api/plugins/after-spec-api) results did not include the `stats.skipped` field for each run result. Fixes [#27694](https://github.com/cypress-io/cypress/issues/27694). Addressed in [#27695](https://github.com/cypress-io/cypress/pull/27695). +- Individual CDP errors that occur while capturing data for Test Replay will no longer prevent the entire run from being available. Addressed in [#27709](https://github.com/cypress-io/cypress/pull/27709). +- Fixed an issue where the release date on the `v13` landing page was a day behind. Fixed in [#27711](https://github.com/cypress-io/cypress/pull/27711). +- Fixed an issue where fatal protocol errors would leak between specs causing all subsequent specs to fail to upload protocol information. Fixed in [#27720](https://github.com/cypress-io/cypress/pull/27720) +- Updated `plist` from `3.0.6` to `3.1.0` to address [CVE-2022-37616](https://github.com/advisories/GHSA-9pgh-qqpf-7wqj) and [CVE-2022-39353](https://github.com/advisories/GHSA-crh6-fp67-6883). Fixed in [#27710](https://github.com/cypress-io/cypress/pull/27710). + +## 13.0.0 + +_Released 08/29/2023_ + +**Breaking Changes:** + +- The [`video`](https://docs.cypress.io/guides/references/configuration#Videos) configuration option now defaults to `false`. Addresses [#26157](https://github.com/cypress-io/cypress/issues/26157). +- The [`videoCompression`](https://docs.cypress.io/guides/references/configuration#Videos) configuration option now defaults to `false`. Addresses [#26160](https://github.com/cypress-io/cypress/issues/26160). +- The [`videoUploadOnPasses`](https://docs.cypress.io/guides/references/configuration#Videos) configuration option has been removed. Please see our [screenshots & videos guide](https://docs.cypress.io/guides/guides/screenshots-and-videos#Delete-videos-for-specs-without-failing-or-retried-tests) on how to accomplish similar functionality. Addresses [#26899](https://github.com/cypress-io/cypress/issues/26899). +- Requests for assets at relative paths for component testing are now correctly forwarded to the dev server. Fixes [#26725](https://github.com/cypress-io/cypress/issues/26725). +- The [`cy.readFile()`](/api/commands/readfile) command is now retry-able as a [query command](https://on.cypress.io/retry-ability). This should not affect any tests using it; the functionality is unchanged. However, it can no longer be overwritten using [`Cypress.Commands.overwrite()`](/api/cypress-api/custom-commands#Overwrite-Existing-Commands). Addressed in [#25595](https://github.com/cypress-io/cypress/pull/25595). +- The current spec path is now passed from the AUT iframe using a query parameter rather than a path segment. This allows for requests for assets at relative paths to be correctly forwarded to the dev server. Fixes [#26725](https://github.com/cypress-io/cypress/issues/26725). +- The deprecated configuration option `nodeVersion` has been removed. Addresses [#27016](https://github.com/cypress-io/cypress/issues/27016). +- The properties and values returned by the [Module API](https://docs.cypress.io/guides/guides/module-api) and included in the arguments of handlers for the [`after:run`](https://docs.cypress.io/api/plugins/after-run-api) and [`after:spec`](https://docs.cypress.io/api/plugins/after-spec-api) have been changed to be more consistent. Addresses [#23805](https://github.com/cypress-io/cypress/issues/23805). +- For Cypress Cloud runs with Test Replay enabled, the Cypress Runner UI is now hidden during the run since the Runner will be visible during Test Replay. As such, if video is recorded (which is now defaulted to `false`) during the run, the Runner will not be visible. In addition, if a runner screenshot (`cy.screenshot({ capture: runner })`) is captured, it will no longer contain the Runner. +- The browser and browser page unexpectedly closing in the middle of a test run are now gracefully handled. Addressed in [#27592](https://github.com/cypress-io/cypress/issues/27592). +- Automation performance is now improved by switching away from websockets to direct CDP calls for Chrome and Electron browsers. Addressed in [#27592](https://github.com/cypress-io/cypress/issues/27592). +- Edge cases where `cy.intercept()` would not properly intercept have been addressed. Addressed in [#27592](https://github.com/cypress-io/cypress/issues/27592). +- Node 14 support has been removed and Node 16 support has been deprecated. Node 16 may continue to work with Cypress `v13`, but will not be supported moving forward to closer coincide with [Node 16's end-of-life](https://nodejs.org/en/blog/announcements/nodejs16-eol) schedule. It is recommended that users update to at least Node 18. +- The minimum supported Typescript version is `4.x`. + +**Features:** + +- Consolidates and improves terminal output when uploading test artifacts to Cypress Cloud. Addressed in [#27402](https://github.com/cypress-io/cypress/pull/27402) + +**Bugfixes:** + +- Fixed an issue where Cypress's internal `tsconfig` would conflict with properties set in the user's `tsconfig.json` such as `module` and `moduleResolution`. Fixes [#26308](https://github.com/cypress-io/cypress/issues/26308) and [#27448](https://github.com/cypress-io/cypress/issues/27448). +- Clarified Svelte 4 works correctly with Component Testing and updated dependencies checks to reflect this. It was incorrectly flagged as not supported. Fixes [#27465](https://github.com/cypress-io/cypress/issues/27465). +- Resolve the `process/browser` global inside `@cypress/webpack-batteries-included-preprocessor` to resolve to `process/browser.js` in order to explicitly provide the file extension. File resolution must include the extension for `.mjs` and `.js` files inside ESM packages in order to resolve correctly. Fixes[#27599](https://github.com/cypress-io/cypress/issues/27599). +- Fixed an issue where the correct `pnp` process was not being discovered. Fixes [#27562](https://github.com/cypress-io/cypress/issues/27562). +- Fixed incorrect type declarations for Cypress and Chai globals that asserted them to be local variables of the global scope rather than properties on the global object. Fixes [#27539](https://github.com/cypress-io/cypress/issues/27539). Fixed in [#27540](https://github.com/cypress-io/cypress/pull/27540). +- Dev Servers will now respect and use the `port` configuration option if present. Fixes [#27675](https://github.com/cypress-io/cypress/issues/27675). + +**Dependency Updates:** + +- Upgraded [`@cypress/request`](https://www.npmjs.com/package/@cypress/request) from `^2.88.11` to `^3.0.0` to address the [CVE-2023-28155](https://github.com/advisories/GHSA-p8p7-x288-28g6) security vulnerability. Addresses [#27535](https://github.com/cypress-io/cypress/issues/27535). Addressed in [#27495](https://github.com/cypress-io/cypress/pull/27495). + +## 12.17.4 + +_Released 08/15/2023_ + +**Bugfixes:** + +- Fixed an issue where having `cypress.config` in a nested directory would cause problems with locating the `component-index.html` file when using component testing. Fixes [#26400](https://github.com/cypress-io/cypress/issues/26400). + +**Dependency Updates:** + +- Upgraded [`webpack`](https://www.npmjs.com/package/webpack) from `v4` to `v5`. This means that we are now bundling your `e2e` tests with webpack 5. We don't anticipate this causing any noticeable changes. However, if you'd like to keep bundling your `e2e` tests with wepback 4 you can use the same process as before by pinning [@cypress/webpack-batteries-included-preprocessor](https://www.npmjs.com/package/@cypress/webpack-batteries-included-preprocessor) to `v2.x.x` and hooking into the [file:preprocessor](https://docs.cypress.io/api/plugins/preprocessors-api#Usage) plugin event. This will restore the previous bundling process. Additionally, if you're using [@cypress/webpack-batteries-included-preprocessor](https://www.npmjs.com/package/@cypress/webpack-batteries-included-preprocessor) already, a new version has been published to support webpack `v5`. +- Upgraded [`tough-cookie`](https://www.npmjs.com/package/tough-cookie) from `4.0` to `4.1.3`, [`@cypress/request`](https://www.npmjs.com/package/@cypress/request) from `2.88.11` to `2.88.12` and [`@cypress/request-promise`](https://www.npmjs.com/package/@cypress/request-promise) from `4.2.6` to `4.2.7` to address a [security vulnerability](https://security.snyk.io/vuln/SNYK-JS-TOUGHCOOKIE-5672873). Fixes [#27261](https://github.com/cypress-io/cypress/issues/27261). + +## 12.17.3 + +_Released 08/01/2023_ + +**Bugfixes:** + +- Fixed an issue where unexpected branch names were being recorded for cypress runs when executed by GitHub Actions. The HEAD branch name will now be recorded by default for pull request workflows if a branch name cannot otherwise be detected from user overrides or from local git data. Fixes [#27389](https://github.com/cypress-io/cypress/issues/27389). + +**Performance:** + +- Fixed an issue where unnecessary requests were being paused. No longer sends `X-Cypress-Is-XHR-Or-Fetch` header and infers resource type off of the server pre-request object. Fixes [#26620](https://github.com/cypress-io/cypress/issues/26620) and [#26622](https://github.com/cypress-io/cypress/issues/26622). + +## 12.17.2 + +_Released 07/20/2023_ + +**Bugfixes:** + +- Fixed an issue where commands would fail with the error `must only be invoked from the spec file or support file` if their arguments were mutated. Fixes [#27200](https://github.com/cypress-io/cypress/issues/27200). +- Fixed an issue where `cy.writeFile()` would erroneously fail with the error `cy.writeFile() must only be invoked from the spec file or support file`. Fixes [#27097](https://github.com/cypress-io/cypress/issues/27097). +- Fixed an issue where web workers could not be created within a spec. Fixes [#27298](https://github.com/cypress-io/cypress/issues/27298). + +## 12.17.1 + +_Released 07/10/2023_ + +**Bugfixes:** + +- Fixed invalid stored preference when enabling in-app notifications that could cause the application to crash. Fixes [#27228](https://github.com/cypress-io/cypress/issues/27228). +- Fixed an issue with the Typescript types of [`cy.screenshot()`](https://docs.cypress.io/api/commands/screenshot). Fixed in [#27130](https://github.com/cypress-io/cypress/pull/27130). + +**Dependency Updates:** + +- Upgraded [`@cypress/request`](https://www.npmjs.com/package/@cypress/request) from `2.88.10` to `2.88.11` to address [CVE-2022-24999](https://www.cve.org/CVERecord?id=CVE-2022-24999) security vulnerability. Addressed in [#27005](https://github.com/cypress-io/cypress/pull/27005). + +## 12.17.0 + +_Released 07/05/2023_ + +**Features:** + +- Cypress Cloud users can now receive desktop notifications about their runs, including when one starts, finishes, or fails. Addresses [#26686](https://github.com/cypress-io/cypress/issues/26686). + +**Bugfixes:** + +- Fixed issues where commands would fail with the error `must only be invoked from the spec file or support file`. Fixes [#27149](https://github.com/cypress-io/cypress/issues/27149) and [#27163](https://github.com/cypress-io/cypress/issues/27163). +- Fixed a regression introduced in Cypress [12.12.0](#12-12-0) where Cypress may fail to reconnect to the Chrome DevTools Protocol in Electron. Fixes [#26900](https://github.com/cypress-io/cypress/issues/26900). +- Fixed an issue where chrome was not recovering from browser crashes properly. Fixes [#24650](https://github.com/cypress-io/cypress/issues/24650). +- Fixed a race condition that was causing a GraphQL error to appear on the [Debug page](https://docs.cypress.io/guides/cloud/runs#Debug) when viewing a running Cypress Cloud build. Fixed in [#27134](https://github.com/cypress-io/cypress/pull/27134). +- Fixed a race condition in electron where the test window exiting prematurely during the browser launch process was causing the whole test run to fail. Addressed in [#27167](https://github.com/cypress-io/cypress/pull/27167). +- Fixed minor issues with Typescript types in the CLI. Fixes [#24110](https://github.com/cypress-io/cypress/issues/24110). +- Fixed an issue where a value for the Electron debug port would not be respected if defined using the `ELECTRON_EXTRA_LAUNCH_ARGS` environment variable. Fixes [#26711](https://github.com/cypress-io/cypress/issues/26711). + +**Dependency Updates:** + +- Update dependency semver to ^7.5.3. Addressed in [#27151](https://github.com/cypress-io/cypress/pull/27151). + +## 12.16.0 + +_Released 06/26/2023_ + +**Features:** + +- Added support for Angular 16.1.0 in Cypress Component Testing. Addresses [#27049](https://github.com/cypress-io/cypress/issues/27049). + +**Bugfixes:** + +- Fixed an issue where certain commands would fail with the error `must only be invoked from the spec file or support file` when invoked with a large argument. Fixes [#27099](https://github.com/cypress-io/cypress/issues/27099). + +## 12.15.0 + +_Released 06/20/2023_ + +**Features:** + +- Added support for running Cypress tests with [Chrome's new `--headless=new` flag](https://developer.chrome.com/articles/new-headless/). Chrome versions 112 and above will now be run in the `headless` mode that matches the `headed` browser implementation. Addresses [#25972](https://github.com/cypress-io/cypress/issues/25972). +- Cypress can now test pages with targeted `Content-Security-Policy` and `Content-Security-Policy-Report-Only` header directives by specifying the allow list via the [`experimentalCspAllowList`](https://docs.cypress.io/guides/references/configuration#Experimental-Csp-Allow-List) configuration option. Addresses [#1030](https://github.com/cypress-io/cypress/issues/1030). Addressed in [#26483](https://github.com/cypress-io/cypress/pull/26483) +- The [`videoCompression`](https://docs.cypress.io/guides/references/configuration#Videos) configuration option now accepts both a boolean or a Constant Rate Factor (CRF) number between `1` and `51`. The `videoCompression` default value is still `32` CRF and when `videoCompression` is set to `true` the default of `32` CRF will be used. Addresses [#26658](https://github.com/cypress-io/cypress/issues/26658). +- The Cypress Cloud data shown on the [Specs](https://docs.cypress.io/guides/core-concepts/cypress-app#Specs) page and [Runs](https://docs.cypress.io/guides/core-concepts/cypress-app#Runs) page will now reflect Cloud Runs that match the current Git tree if Git is being used. Addresses [#26693](https://github.com/cypress-io/cypress/issues/26693). + +**Bugfixes:** + +- Fixed an issue where video output was not being logged to the console when `videoCompression` was turned off. Videos will now log to the terminal regardless of the compression value. Addresses [#25945](https://github.com/cypress-io/cypress/issues/25945). + +**Dependency Updates:** + +- Removed [`@cypress/mocha-teamcity-reporter`](https://www.npmjs.com/package/@cypress/mocha-teamcity-reporter) as this package was no longer being referenced. Addressed in [#26938](https://github.com/cypress-io/cypress/pull/26938). + +## 12.14.0 + +_Released 06/07/2023_ + +**Features:** + +- A new testing type switcher has been added to the Spec Explorer to make it easier to move between E2E and Component Testing. An informational overview of each type is displayed if it hasn't already been configured to help educate and onboard new users to each testing type. Addresses [#26448](https://github.com/cypress-io/cypress/issues/26448), [#26836](https://github.com/cypress-io/cypress/issues/26836) and [#26837](https://github.com/cypress-io/cypress/issues/26837). + +**Bugfixes:** + +- Fixed an issue to now correctly detect Angular 16 dependencies +([@angular/cli](https://www.npmjs.com/package/@angular/cli), +[@angular-devkit/build-angular](https://www.npmjs.com/package/@angular-devkit/build-angular), +[@angular/core](https://www.npmjs.com/package/@angular/core), [@angular/common](https://www.npmjs.com/package/@angular/common), +[@angular/platform-browser-dynamic](https://www.npmjs.com/package/@angular/platform-browser-dynamic)) +during Component Testing onboarding. Addresses [#26852](https://github.com/cypress-io/cypress/issues/26852). +- Ensures Git-related messages on the [Runs page](https://docs.cypress.io/guides/core-concepts/cypress-app#Runs) remain dismissed. Addresses [#26808](https://github.com/cypress-io/cypress/issues/26808). + +**Dependency Updates:** + +- Upgraded [`find-process`](https://www.npmjs.com/package/find-process) from `1.4.1` to `1.4.7` to address this [Synk](https://security.snyk.io/vuln/SNYK-JS-FINDPROCESS-1090284) security vulnerability. Addressed in [#26906](https://github.com/cypress-io/cypress/pull/26906). +- Upgraded [`firefox-profile`](https://www.npmjs.com/package/firefox-profile) from `4.0.0` to `4.3.2` to address security vulnerabilities within sub-dependencies. Addressed in [#26912](https://github.com/cypress-io/cypress/pull/26912). + +## 12.13.0 + +_Released 05/23/2023_ + +**Features:** + +- Adds Git-related messages for the [Runs page](https://docs.cypress.io/guides/core-concepts/cypress-app#Runs) and [Debug page](https://docs.cypress.io/guides/cloud/runs#Debug) when users aren't using Git or there are no recorded runs for the current branch. Addresses [#26680](https://github.com/cypress-io/cypress/issues/26680). + +**Bugfixes:** + +- Reverted [#26452](https://github.com/cypress-io/cypress/pull/26452) which introduced a bug that prevents users from using End to End with Yarn 3. Fixed in [#26735](https://github.com/cypress-io/cypress/pull/26735). Fixes [#26676](https://github.com/cypress-io/cypress/issues/26676). +- Moved `types` condition to the front of `package.json#exports` since keys there are meant to be order-sensitive. Fixed in [#26630](https://github.com/cypress-io/cypress/pull/26630). +- Fixed an issue where newly-installed dependencies would not be detected during Component Testing setup. Addresses [#26685](https://github.com/cypress-io/cypress/issues/26685). +- Fixed a UI regression that was flashing an "empty" state inappropriately when loading the Debug page. Fixed in [#26761](https://github.com/cypress-io/cypress/pull/26761). +- Fixed an issue in Component Testing setup where TypeScript version 5 was not properly detected. Fixes [#26204](https://github.com/cypress-io/cypress/issues/26204). + +**Misc:** + +- Updated styling & content of Cypress Cloud slideshows when not logged in or no runs have been recorded. Addresses [#26181](https://github.com/cypress-io/cypress/issues/26181). +- Changed the nomenclature of 'processing' to 'compressing' when terminal video output is printed during a run. Addresses [#26657](https://github.com/cypress-io/cypress/issues/26657). +- Changed the nomenclature of 'Upload Results' to 'Uploading Screenshots & Videos' when terminal output is printed during a run. Addresses [#26759](https://github.com/cypress-io/cypress/issues/26759). + +## 12.12.0 + +_Released 05/09/2023_ + +**Features:** + +- Added a new informational banner to help get started with component testing from an existing end-to-end test suite. Addresses [#26511](https://github.com/cypress-io/cypress/issues/26511). + +**Bugfixes:** + +- Fixed an issue in Electron where devtools gets out of sync with the DOM occasionally. Addresses [#15932](https://github.com/cypress-io/cypress/issues/15932). +- Updated the Chromium renderer process crash message to be more terse. Addressed in [#26597](https://github.com/cypress-io/cypress/pull/26597). +- Fixed an issue with `CYPRESS_DOWNLOAD_PATH_TEMPLATE` regex to allow multiple replacements. Addresses [#23670](https://github.com/cypress-io/cypress/issues/23670). +- Moved `types` condition to the front of `package.json#exports` since keys there are meant to be order-sensitive. Fixed in [#26630](https://github.com/cypress-io/cypress/pull/26630). + +**Dependency Updates:** + +- Upgraded [`plist`](https://www.npmjs.com/package/plist) from `3.0.5` to `3.0.6` to address [CVE-2022-26260](https://nvd.nist.gov/vuln/detail/CVE-2022-22912#range-8131646) NVD security vulnerability. Addressed in [#26631](https://github.com/cypress-io/cypress/pull/26631). +- Upgraded [`engine.io`](https://www.npmjs.com/package/engine.io) from `6.2.1` to `6.4.2` to address [CVE-2023-31125](https://github.com/socketio/engine.io/security/advisories/GHSA-q9mw-68c2-j6m5) NVD security vulnerability. Addressed in [#26664](https://github.com/cypress-io/cypress/pull/26664). +- Upgraded [`@vue/test-utils`](https://www.npmjs.com/package/@vue/test-utils) from `2.0.2` to `2.3.2`. Addresses [#26575](https://github.com/cypress-io/cypress/issues/26575). + +## 12.11.0 + +_Released 04/26/2023_ + +**Features:** + +- Adds Component Testing support for Angular 16. Addresses [#26044](https://github.com/cypress-io/cypress/issues/26044). +- The run navigation component on the [Debug page](https://on.cypress.io/debug-page) will now display a warning message if there are more relevant runs than can be displayed in the list. Addresses [#26288](https://github.com/cypress-io/cypress/issues/26288). + +**Bugfixes:** + +- Fixed an issue where setting `videoCompression` to `0` would cause the video output to be broken. `0` is now treated as false. Addresses [#5191](https://github.com/cypress-io/cypress/issues/5191) and [#24595](https://github.com/cypress-io/cypress/issues/24595). +- Fixed an issue on the [Debug page](https://on.cypress.io/debug-page) where the passing run status would appear even if the Cypress Cloud organization was over its monthly test result limit. Addresses [#26528](https://github.com/cypress-io/cypress/issues/26528). + +**Misc:** + +- Cleaned up our open telemetry dependencies, reducing the size of the open telemetry modules. Addressed in [#26522](https://github.com/cypress-io/cypress/pull/26522). + +**Dependency Updates:** + +- Upgraded [`vue`](https://www.npmjs.com/package/vue) from `3.2.31` to `3.2.47`. Addressed in [#26555](https://github.com/cypress-io/cypress/pull/26555). + +## 12.10.0 + +_Released 04/17/2023_ + +**Features:** + +- The Component Testing setup wizard will now show a warning message if an issue is encountered with an installed [third party framework definition](https://on.cypress.io/component-integrations). Addresses [#25838](https://github.com/cypress-io/cypress/issues/25838). + +**Bugfixes:** + +- Capture the [Azure](https://azure.microsoft.com/) CI provider's environment variable [`SYSTEM_PULLREQUEST_PULLREQUESTNUMBER`](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml#system-variables-devops-services) to display the linked PR number in the Cloud. Addressed in [#26215](https://github.com/cypress-io/cypress/pull/26215). +- Fixed an issue in the onboarding wizard where project framework & bundler would not be auto-detected when opening directly into component testing mode using the `--component` CLI flag. Fixes [#22777](https://github.com/cypress-io/cypress/issues/22777) and [#26388](https://github.com/cypress-io/cypress/issues/26388). +- Updated to use the `SEMAPHORE_GIT_WORKING_BRANCH` [Semphore](https://docs.semaphoreci.com) CI environment variable to correctly associate a Cloud run to the current branch. Previously this was incorrectly associating a run to the target branch. Fixes [#26309](https://github.com/cypress-io/cypress/issues/26309). +- Fix an edge case in Component Testing where a custom `baseUrl` in `tsconfig.json` for Next.js 13.2.0+ is not respected. This was partially fixed in [#26005](https://github.com/cypress-io/cypress/pull/26005), but an edge case was missed. Fixes [#25951](https://github.com/cypress-io/cypress/issues/25951). +- Fixed an issue where `click` events fired on `.type('{enter}')` did not propagate through shadow roots. Fixes [#26392](https://github.com/cypress-io/cypress/issues/26392). + +**Misc:** + +- Removed unintentional debug logs. Addressed in [#26411](https://github.com/cypress-io/cypress/pull/26411). +- Improved styling on the [Runs Page](https://docs.cypress.io/guides/core-concepts/cypress-app#Runs). Addresses [#26180](https://github.com/cypress-io/cypress/issues/26180). + +**Dependency Updates:** + +- Upgraded [`commander`](https://www.npmjs.com/package/commander) from `^5.1.0` to `^6.2.1`. Addressed in [#26226](https://github.com/cypress-io/cypress/pull/26226). +- Upgraded [`minimist`](https://www.npmjs.com/package/minimist) from `1.2.6` to `1.2.8` to address this [CVE-2021-44906](https://github.com/advisories/GHSA-xvch-5gv4-984h) NVD security vulnerability. Addressed in [#26254](https://github.com/cypress-io/cypress/pull/26254). + +## 12.9.0 + +_Released 03/28/2023_ + +**Features:** + +- The [Debug page](https://docs.cypress.io/guides/cloud/runs#Debug) now allows for navigating between all runs recorded for a commit. Addresses [#25899](https://github.com/cypress-io/cypress/issues/25899) and [#26018](https://github.com/cypress-io/cypress/issues/26018). + +**Bugfixes:** + +- Fixed a compatibility issue so that component test projects can use [Vite](https://vitejs.dev/) version 4.2.0 and greater. Fixes [#26138](https://github.com/cypress-io/cypress/issues/26138). +- Fixed an issue where [`cy.intercept()`](https://docs.cypress.io/api/commands/intercept) added an additional `content-length` header to spied requests that did not set a `content-length` header on the original request. Fixes [#24407](https://github.com/cypress-io/cypress/issues/24407). +- Changed the way that Git hashes are loaded so that non-relevant runs are excluded from the Debug page. Fixes [#26058](https://github.com/cypress-io/cypress/issues/26058). +- Corrected the [`.type()`](https://docs.cypress.io/api/commands/type) command to account for shadow root elements when determining whether or not focus needs to be simulated before typing. Fixes [#26198](https://github.com/cypress-io/cypress/issues/26198). +- Fixed an issue where an incorrect working directory could be used for Git operations on Windows. Fixes [#23317](https://github.com/cypress-io/cypress/issues/23317). +- Capture the [Buildkite](https://buildkite.com/) CI provider's environment variable `BUILDKITE_RETRY_COUNT` to handle CI retries in the Cloud. Addressed in [#25750](https://github.com/cypress-io/cypress/pull/25750). + +**Misc:** + +- Made some minor styling updates to the Debug page. Addresses [#26041](https://github.com/cypress-io/cypress/issues/26041). + +## 12.8.1 + +_Released 03/15/2023_ + +**Bugfixes:** + +- Fixed a regression in Cypress [10](https://docs.cypress.io/guides/references/changelog#10-0-0) where the reporter auto-scroll configuration inside user preferences was unintentionally being toggled off. User's must now explicitly enable/disable auto-scroll under user preferences, which is enabled by default. Fixes [#24171](https://github.com/cypress-io/cypress/issues/24171) and [#26113](https://github.com/cypress-io/cypress/issues/26113). + +**Dependency Updates:** + +- Upgraded [`ejs`](https://www.npmjs.com/package/ejs) from `3.1.6` to `3.1.8` to address this [CVE-2022-29078](https://github.com/advisories/GHSA-phwq-j96m-2c2q) NVD security vulnerability. Addressed in [#25279](https://github.com/cypress-io/cypress/pull/25279). + +## 12.8.0 + +_Released 03/14/2023_ + +**Features:** + +- The [Debug page](https://docs.cypress.io/guides/cloud/runs#Debug) is now able to show real-time results from in-progress runs. Addresses [#25759](https://github.com/cypress-io/cypress/issues/25759). +- Added the ability to control whether a request is logged to the command log via [`cy.intercept()`](https://docs.cypress.io/api/commands/intercept) by passing `log: false` or `log: true`. Addresses [#7362](https://github.com/cypress-io/cypress/issues/7362). + - This can be used to override Cypress's default behavior of logging all XHRs and fetches, see the [example](https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request). +- It is now possible to control the number of connection attempts to the browser using the `CYPRESS_CONNECT_RETRY_THRESHOLD` Environment Variable. Learn more [here](https://docs.cypress.io/guides/references/advanced-installation#Environment-variables). Addressed in [#25848](https://github.com/cypress-io/cypress/pull/25848). + +**Bugfixes:** + +- Fixed an issue where using `Cypress.require()` would throw the error `Cannot find module 'typescript'`. Fixes [#25885](https://github.com/cypress-io/cypress/issues/25885). +- The [`before:spec`](https://docs.cypress.io/api/plugins/before-spec-api) API was updated to correctly support async event handlers in `run` mode. Fixes [#24403](https://github.com/cypress-io/cypress/issues/24403). +- Updated the Component Testing [community framework](https://docs.cypress.io/guides/component-testing/third-party-definitions) definition detection logic to take into account monorepo structures that hoist dependencies. Fixes [#25993](https://github.com/cypress-io/cypress/issues/25993). +- The onboarding wizard for Component Testing will now detect installed dependencies more reliably. Fixes [#25782](https://github.com/cypress-io/cypress/issues/25782). +- Fixed an issue where Angular components would sometimes be mounted in unexpected DOM locations in component tests. Fixes [#25956](https://github.com/cypress-io/cypress/issues/25956). +- Fixed an issue where Cypress component testing would fail to work with [Next.js](https://nextjs.org/) `13.2.1`. Fixes [#25951](https://github.com/cypress-io/cypress/issues/25951). +- Fixed an issue where migrating a project from a version of Cypress earlier than [10.0.0](#10-0-0) could fail if the project's `testFiles` configuration was an array of globs. Fixes [#25947](https://github.com/cypress-io/cypress/issues/25947). + +**Misc:** + +- Removed "New" badge in the navigation bar for the debug page icon. Addresses [#25925](https://github.com/cypress-io/cypress/issues/25925). +- Removed inline "Connect" buttons within the Specs Explorer. Addresses [#25926](https://github.com/cypress-io/cypress/issues/25926). +- Added an icon for "beta" versions of the Chrome browser. Addresses [#25968](https://github.com/cypress-io/cypress/issues/25968). + +**Dependency Updates:** + +- Upgraded [`mocha-junit-reporter`](https://www.npmjs.com/package/mocha-junit-reporter) from `2.1.0` to `2.2.0` to be able to use [new placeholders](https://github.com/michaelleeallen/mocha-junit-reporter/pull/163) such as `[suiteFilename]` or `[suiteName]` when defining the test report name. Addressed in [#25922](https://github.com/cypress-io/cypress/pull/25922). + +## 12.7.0 + +_Released 02/24/2023_ + +**Features:** + +- It is now possible to set `hostOnly` cookies with [`cy.setCookie()`](https://docs.cypress.io/api/commands/setcookie) for a given domain. Addresses [#16856](https://github.com/cypress-io/cypress/issues/16856) and [#17527](https://github.com/cypress-io/cypress/issues/17527). +- Added a Public API for third party component libraries to define a Framework Definition, embedding their library into the Cypress onboarding workflow. Learn more [here](https://docs.cypress.io/guides/component-testing/third-party-definitions). Implemented in [#25780](https://github.com/cypress-io/cypress/pull/25780) and closes [#25638](https://github.com/cypress-io/cypress/issues/25638). +- Added a Debug Page tutorial slideshow for projects that are not connected to Cypress Cloud. Addresses [#25768](https://github.com/cypress-io/cypress/issues/25768). +- Improved various error message around interactions with the Cypress cloud. Implemented in [#25837](https://github.com/cypress-io/cypress/pull/25837) +- Updated the "new" status badge for the Debug page navigation link to be less noticeable when the navigation is collapsed. Addresses [#25739](https://github.com/cypress-io/cypress/issues/25739). + +**Bugfixes:** + +- Fixed various bugs when recording to the cloud. Fixed in [#25837](https://github.com/cypress-io/cypress/pull/25837) +- Fixed an issue where cookies were being duplicated with the same hostname, but a prepended dot. Fixed an issue where cookies may not be expiring correctly. Fixes [#25174](https://github.com/cypress-io/cypress/issues/25174), [#25205](https://github.com/cypress-io/cypress/issues/25205) and [#25495](https://github.com/cypress-io/cypress/issues/25495). +- Fixed an issue where cookies weren't being synced when the application was stable. Fixed in [#25855](https://github.com/cypress-io/cypress/pull/25855). Fixes [#25835](https://github.com/cypress-io/cypress/issues/25835). +- Added missing TypeScript type definitions for the [`cy.reload()`](https://docs.cypress.io/api/commands/reload) command. Addressed in [#25779](https://github.com/cypress-io/cypress/pull/25779). +- Ensure Angular components are mounted inside the correct element. Fixes [#24385](https://github.com/cypress-io/cypress/issues/24385). +- Fix a bug where files outside the project root in a monorepo are not correctly served when using Vite. Addressed in [#25801](https://github.com/cypress-io/cypress/pull/25801). +- Fixed an issue where using [`cy.intercept`](https://docs.cypress.io/api/commands/intercept)'s `req.continue()` with a non-function parameter would not provide an appropriate error message. Fixed in [#25884](https://github.com/cypress-io/cypress/pull/25884). +- Fixed an issue where Cypress would erroneously launch and connect to multiple browser instances. Fixes [#24377](https://github.com/cypress-io/cypress/issues/24377). + +**Misc:** + +- Made updates to the way that the Debug Page header displays information. Addresses [#25796](https://github.com/cypress-io/cypress/issues/25796) and [#25798](https://github.com/cypress-io/cypress/issues/25798). + +## 12.6.0 + +_Released 02/15/2023_ + +**Features:** + +- Added a new CLI flag, called [`--auto-cancel-after-failures`](https://docs.cypress.io/guides/guides/command-line#Options), that overrides the project-level ["Auto Cancellation"](https://docs.cypress.io/guides/cloud/smart-orchestration#Auto-Cancellation) value when recording to the Cloud. This gives Cloud users on Business and Enterprise plans the flexibility to alter the auto-cancellation value per run. Addressed in [#25237](https://github.com/cypress-io/cypress/pull/25237). +- It is now possible to overwrite query commands using [`Cypress.Commands.overwriteQuery`](https://on.cypress.io/api/custom-queries). Addressed in [#25078](https://github.com/cypress-io/cypress/issues/25078). +- Added [`Cypress.require()`](https://docs.cypress.io/api/cypress-api/require) for including dependencies within the [`cy.origin()`](https://docs.cypress.io/api/commands/origin) callback. This change removed support for using `require()` and `import()` directly within the callback because we found that it impacted performance not only for spec files using them within the [`cy.origin()`](https://docs.cypress.io/api/commands/origin) callback, but even for spec files that did not use them. Addresses [#24976](https://github.com/cypress-io/cypress/issues/24976). +- Added the ability to open the failing test in the IDE from the Debug page before needing to re-run the test. Addressed in [#24850](https://github.com/cypress-io/cypress/issues/24850). + +**Bugfixes:** + +- When a Cloud user is apart of multiple Cloud organizations, the [Connect to Cloud setup](https://docs.cypress.io/guides/cloud/projects#Set-up-a-project-to-record) now shows the correct organizational prompts when connecting a new project. Fixes [#25520](https://github.com/cypress-io/cypress/issues/25520). +- Fixed an issue where Cypress would fail to load any specs if the project `specPattern` included a resource that could not be accessed due to filesystem permissions. Fixes [#24109](https://github.com/cypress-io/cypress/issues/24109). +- Fixed an issue where the Debug page would display a different number of specs for in-progress runs than the in-progress specs reported in Cypress Cloud. Fixes [#25647](https://github.com/cypress-io/cypress/issues/25647). +- Fixed an issue in middleware where error-handling code could itself generate an error and fail to report the original issue. Fixes [#22825](https://github.com/cypress-io/cypress/issues/22825). +- Fixed an regression introduced in Cypress [12.3.0](#12-3-0) where custom browsers that relied on process environment variables were not found on macOS arm64 architectures. Fixed in [#25753](https://github.com/cypress-io/cypress/pull/25753). + +**Misc:** + +- Improved the UI of the Debug page. Addresses [#25664](https://github.com/cypress-io/cypress/issues/25664), [#25669](https://github.com/cypress-io/cypress/issues/25669), [#25665](https://github.com/cypress-io/cypress/issues/25665), [#25666](https://github.com/cypress-io/cypress/issues/25666), and [#25667](https://github.com/cypress-io/cypress/issues/25667). +- Updated the Debug page sidebar badge to to show 0 to 99+ failing tests, increased from showing 0 to 9+ failing tests, to provide better test failure insights. Addresses [#25662](https://github.com/cypress-io/cypress/issues/25662). + +**Dependency Updates:** + +- Upgrade [`debug`](https://www.npmjs.com/package/debug) to `4.3.4`. Addressed in [#25699](https://github.com/cypress-io/cypress/pull/25699). + +## 12.5.1 + +_Released 02/02/2023_ + +**Bugfixes:** + +- Fixed a regression introduced in Cypress [12.5.0](https://docs.cypress.io/guides/references/changelog#12-5-0) where the `runnable` was not included in the [`test:after:run`](https://docs.cypress.io/api/events/catalog-of-events) event. Fixes [#25663](https://github.com/cypress-io/cypress/issues/25663). + +**Dependency Updates:** + +- Upgraded [`simple-git`](https://github.com/steveukx/git-js) from `3.15.0` to `3.16.0` to address this [security vulnerability](https://github.com/advisories/GHSA-9p95-fxvg-qgq2) where Remote Code Execution (RCE) via the clone(), pull(), push() and listRemote() methods due to improper input sanitization was possible. Addressed in [#25603](https://github.com/cypress-io/cypress/pull/25603). + +## 12.5.0 + +_Released 01/31/2023_ + +**Features:** + +- Easily debug failed CI test runs recorded to the Cypress Cloud from your local Cypress app with the new Debug page. Please leave any feedback [here](https://github.com/cypress-io/cypress/discussions/25649). Your feedback will help us make decisions to improve the Debug experience. For more details, see [our blog post](https://on.cypress.io/debug-page-release). Addressed in [#25488](https://github.com/cypress-io/cypress/pull/25488). + +**Performance:** + +- Improved memory consumption in `run` mode by removing reporter logs for successful tests. Fixes [#25230](https://github.com/cypress-io/cypress/issues/25230). + +**Bugfixes:** + +- Fixed an issue where alternative Microsoft Edge Beta, Canary, and Dev binary versions were not being discovered by Cypress. Fixes [#25455](https://github.com/cypress-io/cypress/issues/25455). + +**Dependency Updates:** + +- Upgraded [`underscore.string`](https://github.com/esamattis/underscore.string/blob/HEAD/CHANGELOG.markdown) from `3.3.5` to `3.3.6` to reference rebuilt assets after security patch to fix regular expression DDOS exploit. Addressed in [#25574](https://github.com/cypress-io/cypress/pull/25574). + +## 12.4.1 + +_Released 01/27/2023_ + +**Bugfixes:** + +- Fixed a regression from Cypress [12.4.0](https://docs.cypress.io/guides/references/changelog#12-4-0) where Cypress was not exiting properly when running multiple Component Testing specs in `electron` in `run` mode. Fixes [#25568](https://github.com/cypress-io/cypress/issues/25568). + +**Dependency Updates:** + +- Upgraded [`ua-parser-js`](https://github.com/faisalman/ua-parser-js) from `0.7.24` to `0.7.33` to address this [security vulnerability](https://github.com/faisalman/ua-parser-js/security/advisories/GHSA-fhg7-m89q-25r3) where crafting a very-very-long user-agent string with specific pattern, an attacker can turn the script to get stuck processing for a very long time which results in a denial of service (DoS) condition. Addressed in [#25561](https://github.com/cypress-io/cypress/pull/25561). + +## 12.4.0 + +_Released 1/24/2023_ + +**Features:** + +- Added official support for Vite 4 in component testing. Addresses + [#24969](https://github.com/cypress-io/cypress/issues/24969). +- Added new + [`experimentalMemoryManagement`](/guides/references/experiments#Configuration) + configuration option to improve memory management in Chromium-based browsers. + Enable this option with `experimentalMemoryManagement=true` if you have + experienced "Out of Memory" issues. Addresses + [#23391](https://github.com/cypress-io/cypress/issues/23391). +- Added new + [`experimentalSkipDomainInjection`](/guides/references/experiments#Experimental-Skip-Domain-Injection) + configuration option to disable Cypress from setting `document.domain` on + injection, allowing users to test Salesforce domains. If you believe you are + having `document.domain` issues, please see the + [`experimentalSkipDomainInjection`](/guides/references/experiments#Experimental-Skip-Domain-Injection) + guide. This config option is end-to-end only. Addresses + [#2367](https://github.com/cypress-io/cypress/issues/2367), + [#23958](https://github.com/cypress-io/cypress/issues/23958), + [#24290](https://github.com/cypress-io/cypress/issues/24290), and + [#24418](https://github.com/cypress-io/cypress/issues/24418). +- The [`.as`](/api/commands/as) command now accepts an options argument, + allowing an alias to be stored as type "query" or "static" value. This is + stored as "query" by default. Addresses + [#25173](https://github.com/cypress-io/cypress/issues/25173). +- The `cy.log()` command will now display a line break where the `\n` character + is used. Addresses + [#24964](https://github.com/cypress-io/cypress/issues/24964). +- [`component.specPattern`](/guides/references/configuration#component) now + utilizes a JSX/TSX file extension when generating a new empty spec file if + project contains at least one file with those extensions. This applies only to + component testing and is skipped if + [`component.specPattern`](/guides/references/configuration#component) has been + configured to exclude files with those extensions. Addresses + [#24495](https://github.com/cypress-io/cypress/issues/24495). +- Added support for the `data-qa` selector in the + [Selector Playground](guides/core-concepts/cypress-app#Selector-Playground) in + addition to `data-cy`, `data-test` and `data-testid`. Addresses + [#25305](https://github.com/cypress-io/cypress/issues/25305). + +**Bugfixes:** + +- Fixed an issue where component tests could incorrectly treat new major + versions of certain dependencies as supported. Fixes + [#25379](https://github.com/cypress-io/cypress/issues/25379). +- Fixed an issue where new lines or spaces on new lines in the Command Log were + not maintained. Fixes + [#23679](https://github.com/cypress-io/cypress/issues/23679) and + [#24964](https://github.com/cypress-io/cypress/issues/24964). +- Fixed an issue where Angular component testing projects would fail to + initialize if an unsupported browserslist entry was specified in the project + configuration. Fixes + [#25312](https://github.com/cypress-io/cypress/issues/25312). + +**Misc** + +- Video output link in `cypress run` mode has been added to it's own line to + make the video output link more easily clickable in the terminal. Addresses + [#23913](https://github.com/cypress-io/cypress/issues/23913). diff --git a/cli/__snapshots__/build_spec.js b/cli/__snapshots__/build_spec.js index 4be1b245a7e..2f0a3189ea9 100644 --- a/cli/__snapshots__/build_spec.js +++ b/cli/__snapshots__/build_spec.js @@ -1,36 +1,37 @@ exports['package.json build outputs expected properties 1'] = { - "name": "test", - "engines": "test engines", - "version": "x.y.z", - "buildInfo": "replaced by normalizePackageJson", - "description": "Cypress.io end to end testing tool", - "homepage": "https://github.com/cypress-io/cypress", - "license": "MIT", - "bugs": { - "url": "https://github.com/cypress-io/cypress/issues" + 'name': 'test', + 'engines': 'test engines', + 'version': 'x.y.z', + 'buildInfo': 'replaced by normalizePackageJson', + 'description': 'Cypress is a next generation front end testing tool built for the modern web', + 'homepage': 'https://cypress.io', + 'license': 'MIT', + 'bugs': { + 'url': 'https://github.com/cypress-io/cypress/issues', }, - "repository": { - "type": "git", - "url": "https://github.com/cypress-io/cypress.git" + 'repository': { + 'type': 'git', + 'url': 'https://github.com/cypress-io/cypress.git', }, - "keywords": [ - "automation", - "browser", - "cypress", - "cypress.io", - "e2e", - "end-to-end", - "integration", - "mocks", - "runner", - "spies", - "stubs", - "test", - "testing" + 'keywords': [ + 'automation', + 'browser', + 'cypress', + 'cypress.io', + 'e2e', + 'end-to-end', + 'integration', + 'component', + 'mocks', + 'runner', + 'spies', + 'stubs', + 'test', + 'testing', ], - "types": "types", - "scripts": { - "postinstall": "node index.js --exec install", - "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" - } + 'types': 'types', + 'scripts': { + 'postinstall': 'node index.js --exec install', + 'size': 't="$(npm pack .)"; wc -c "${t}"; tar tvf "${t}"; rm "${t}";', + }, } diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 0e15c004957..6cca25fe06c 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -67,29 +67,32 @@ exports['shows help for run --foo 1'] = ` Runs Cypress tests from the CLI without the GUI Options: - -b, --browser runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path. - --ci-build-id the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers - --component runs component tests - -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}. - -C, --config-file path to script file where configuration values are set. defaults to "cypress.config.{js,ts,mjs,cjs}". - --e2e runs end to end tests - -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs} or cypress.env.json - --group a named group for recorded runs in the Cypress Dashboard - -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. - --headed displays the browser instead of running headlessly - --headless hide the browser instead of running headed (default for cypress run) - --no-exit keep the browser open after tests finish - --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes - -p, --port runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}. - -P, --project path to the project - -q, --quiet run quietly, using only the configured reporter - --record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard. - -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" - -o, --reporter-options options for the mocha reporter. defaults to "null" - -s, --spec runs specific spec file(s). defaults to "all" - -t, --tag named tag(s) for recorded runs in the Cypress Dashboard - --dev runs cypress in development and bypasses binary check - -h, --help display help for command + --auto-cancel-after-failures overrides the project-level Cloud configuration to set the failed test threshold for auto cancellation or to disable auto cancellation when recording to the Cloud + -b, --browser runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path. + --ci-build-id the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers + --component runs component tests + -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}. + -C, --config-file path to script file where configuration values are set. defaults to "cypress.config.{js,ts,mjs,cjs}". + --e2e runs end to end tests + -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs} or cypress.env.json + --group a named group for recorded runs in Cypress Cloud + -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. + --headed displays the browser instead of running headlessly + --headless hide the browser instead of running headed (default for cypress run) + --no-exit keep the browser open after tests finish + --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes + -p, --port runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}. + -P, --project path to the project + -q, --quiet run quietly, using only the configured reporter + --record [bool] records the run. sends test results, screenshots and videos to Cypress Cloud. + -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" + --runner-ui displays the Cypress Runner UI + --no-runner-ui hides the Cypress Runner UI + -o, --reporter-options options for the mocha reporter. defaults to "null" + -s, --spec runs specific spec file(s). defaults to "all" + -t, --tag named tag(s) for recorded runs in Cypress Cloud + --dev runs cypress in development and bypasses binary check + -h, --help display help for command ------- stderr: ------- @@ -217,7 +220,7 @@ exports['cli help command shows help 1'] = ` Commands: help Shows CLI help and exits - version prints Cypress version + version [options] prints Cypress version open [options] Opens Cypress in the interactive GUI. run [options] Runs Cypress tests from the CLI without the GUI open-ct [options] Opens Cypress component testing interactive mode. @@ -257,7 +260,7 @@ exports['cli help command shows help for -h 1'] = ` Commands: help Shows CLI help and exits - version prints Cypress version + version [options] prints Cypress version open [options] Opens Cypress in the interactive GUI. run [options] Runs Cypress tests from the CLI without the GUI open-ct [options] Opens Cypress component testing interactive mode. @@ -297,7 +300,7 @@ exports['cli help command shows help for --help 1'] = ` Commands: help Shows CLI help and exits - version prints Cypress version + version [options] prints Cypress version open [options] Opens Cypress in the interactive GUI. run [options] Runs Cypress tests from the CLI without the GUI open-ct [options] Opens Cypress component testing interactive mode. @@ -338,7 +341,7 @@ exports['cli unknown command shows usage and exits 1'] = ` Commands: help Shows CLI help and exits - version prints Cypress version + version [options] prints Cypress version open [options] Opens Cypress in the interactive GUI. run [options] Runs Cypress tests from the CLI without the GUI open-ct [options] Opens Cypress component testing interactive mode. @@ -407,20 +410,6 @@ Electron version: not found Bundled Node version: not found ` -exports['cli --version no binary version 1'] = ` -Cypress package version: 1.2.3 -Cypress binary version: not installed -Electron version: not found -Bundled Node version: not found -` - -exports['cli -v no binary version 1'] = ` -Cypress package version: 1.2.3 -Cypress binary version: not installed -Electron version: not found -Bundled Node version: not found -` - exports['cli cypress run warns with space-separated --spec 1'] = ` ⚠ Warning: It looks like you're passing --spec a space-separated list of arguments: @@ -465,7 +454,7 @@ exports['cli CYPRESS_INTERNAL_ENV allows and warns when staging environment 1'] Commands: help Shows CLI help and exits - version prints Cypress version + version [options] prints Cypress version open [options] Opens Cypress in the interactive GUI. run [options] Runs Cypress tests from the CLI without the GUI open-ct [options] Opens Cypress component testing interactive mode. diff --git a/cli/__snapshots__/cypress_spec.js b/cli/__snapshots__/cypress_spec.js index 875a36c441b..f603cfb784e 100644 --- a/cli/__snapshots__/cypress_spec.js +++ b/cli/__snapshots__/cypress_spec.js @@ -1,4 +1,4 @@ exports['cypress .run resolves with contents of tmp file 1'] = { - "code": 0, - "failingTests": [] + 'code': 0, + 'failingTests': [], } diff --git a/cli/__snapshots__/download_spec.js b/cli/__snapshots__/download_spec.js index c913289d584..54ec0d54973 100644 --- a/cli/__snapshots__/download_spec.js +++ b/cli/__snapshots__/download_spec.js @@ -60,3 +60,7 @@ https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip exports['desktop url from template with escaped dollar sign wrapped in quote'] = ` https://download.cypress.io/desktop/0.20.2/OS-ARCH/cypress.zip ` + +exports['desktop url from template with multiple replacements'] = ` +https://download.cypress.io/desktop/0.20.2/OS/ARCH/cypress-0.20.2-OS-ARCH.zip?referrer=https://download.cypress.io/desktop/0.20.2&version=0.20.2 +` diff --git a/cli/__snapshots__/errors_spec.js b/cli/__snapshots__/errors_spec.js index 0b0fdd01354..05e1ce7edcb 100644 --- a/cli/__snapshots__/errors_spec.js +++ b/cli/__snapshots__/errors_spec.js @@ -50,8 +50,8 @@ Cypress Version: 1.2.3 ` exports['child kill error object'] = { - "description": "The Test Runner unexpectedly exited via a exit event with signal SIGKILL", - "solution": "Please search Cypress documentation for possible solutions:\n\n https://on.cypress.io\n\nCheck if there is a GitHub issue describing this crash:\n\n https://github.com/cypress-io/cypress/issues\n\nConsider opening a new issue." + 'description': 'The Test Runner unexpectedly exited via a exit event with signal SIGKILL', + 'solution': 'Please search Cypress documentation for possible solutions:\n\n https://on.cypress.io\n\nCheck if there is a GitHub issue describing this crash:\n\n https://github.com/cypress-io/cypress/issues\n\nConsider opening a new issue.', } exports['Error message'] = ` @@ -74,29 +74,29 @@ Cypress Version: 1.2.3 ` exports['errors individual has the following errors 1'] = [ - "CYPRESS_RUN_BINARY", - "binaryNotExecutable", - "childProcessKilled", - "failedDownload", - "failedUnzip", - "failedUnzipWindowsMaxPathLength", - "incompatibleHeadlessFlags", - "incompatibleTestTypeFlags", - "incompatibleTestingTypeAndFlag", - "invalidCacheDirectory", - "invalidConfigFile", - "invalidCypressEnv", - "invalidOS", - "invalidRunProjectPath", - "invalidSmokeTestDisplayError", - "invalidTestingType", - "missingApp", - "missingDependency", - "missingXvfb", - "nonZeroExitCodeXvfb", - "notInstalledCI", - "smokeTestFailure", - "unexpected", - "unknownError", - "versionMismatch" + 'CYPRESS_RUN_BINARY', + 'binaryNotExecutable', + 'childProcessKilled', + 'failedDownload', + 'failedUnzip', + 'failedUnzipWindowsMaxPathLength', + 'incompatibleHeadlessFlags', + 'incompatibleTestTypeFlags', + 'incompatibleTestingTypeAndFlag', + 'invalidCacheDirectory', + 'invalidConfigFile', + 'invalidCypressEnv', + 'invalidOS', + 'invalidRunProjectPath', + 'invalidSmokeTestDisplayError', + 'invalidTestingType', + 'missingApp', + 'missingDependency', + 'missingXvfb', + 'nonZeroExitCodeXvfb', + 'notInstalledCI', + 'smokeTestFailure', + 'unexpected', + 'unknownError', + 'versionMismatch', ] diff --git a/cli/__snapshots__/run_spec.js b/cli/__snapshots__/run_spec.js index 1d9b1bb4bf7..49d3ba1b8ef 100644 --- a/cli/__snapshots__/run_spec.js +++ b/cli/__snapshots__/run_spec.js @@ -1,27 +1,27 @@ exports['exec run .processRunOptions does not remove --record option when using --browser 1'] = [ - "--run-project", + '--run-project', null, - "--browser", - "test browser", - "--record", - "foo" + '--browser', + 'test browser', + '--record', + 'foo', ] exports['exec run .processRunOptions passes --browser option 1'] = [ - "--run-project", + '--run-project', null, - "--browser", - "test browser" + '--browser', + 'test browser', ] exports['exec run .processRunOptions passes --record option 1'] = [ - "--run-project", + '--run-project', null, - "--record", - "my record id" + '--record', + 'my record id', ] exports['exec run .processRunOptions defaults to e2e testingType 1'] = [ - "--run-project", - null + '--run-project', + null, ] diff --git a/cli/__snapshots__/spawn_spec.js b/cli/__snapshots__/spawn_spec.js index 797395f38fd..9af35625fca 100644 --- a/cli/__snapshots__/spawn_spec.js +++ b/cli/__snapshots__/spawn_spec.js @@ -1,18 +1,18 @@ exports['lib/exec/spawn .start forces colors and streams when supported 1'] = { - "FORCE_COLOR": "1", - "DEBUG_COLORS": "1", - "MOCHA_COLORS": "1", - "FORCE_STDIN_TTY": "1", - "FORCE_STDOUT_TTY": "1", - "FORCE_STDERR_TTY": "1" + 'FORCE_COLOR': '1', + 'DEBUG_COLORS': '1', + 'MOCHA_COLORS': '1', + 'FORCE_STDIN_TTY': '1', + 'FORCE_STDOUT_TTY': '1', + 'FORCE_STDERR_TTY': '1', } exports['lib/exec/spawn .start does not force colors and streams when not supported 1'] = { - "FORCE_COLOR": "0", - "DEBUG_COLORS": "0", - "FORCE_STDIN_TTY": "0", - "FORCE_STDOUT_TTY": "0", - "FORCE_STDERR_TTY": "0" + 'FORCE_COLOR': '0', + 'DEBUG_COLORS': '0', + 'FORCE_STDIN_TTY': '0', + 'FORCE_STDOUT_TTY': '0', + 'FORCE_STDERR_TTY': '0', } exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = ` diff --git a/cli/__snapshots__/util_spec.js b/cli/__snapshots__/util_spec.js index 525f9bf06c8..c65580b8953 100644 --- a/cli/__snapshots__/util_spec.js +++ b/cli/__snapshots__/util_spec.js @@ -1,27 +1,27 @@ exports['config_as_object 1'] = { - "config": "{\"baseUrl\":\"http://localhost:2000\",\"watchForFileChanges\":false}" + 'config': '{"baseUrl":"http://localhost:2000","watchForFileChanges":false}', } exports['env_as_object 1'] = { - "env": "{\"foo\":\"bar\",\"magicNumber\":1234,\"host\":\"kevin.dev.local\"}" + 'env': '{"foo":"bar","magicNumber":1234,"host":"kevin.dev.local"}', } exports['env_as_string 1'] = { - "env": "foo=bar" + 'env': 'foo=bar', } exports['others_unchanged 1'] = { - "foo": "bar" + 'foo': 'bar', } exports['reporter_options_as_object 1'] = { - "reporterOptions": "{\"mochaFile\":\"results/my-test-output.xml\",\"toConsole\":true}" + 'reporterOptions': '{"mochaFile":"results/my-test-output.xml","toConsole":true}', } exports['spec_as_array 1'] = { - "spec": "[\"a\",\"b\",\"c\"]" + 'spec': '["a","b","c"]', } exports['spec_as_string 1'] = { - "spec": "x,y,z" + 'spec': 'x,y,z', } diff --git a/cli/index.mjs b/cli/index.mjs index 7b616f65a34..dcf09178f39 100644 --- a/cli/index.mjs +++ b/cli/index.mjs @@ -8,6 +8,8 @@ export default cypress export const defineConfig = cypress.defineConfig +export const defineComponentFramework = cypress.defineComponentFramework + export const run = cypress.run export const open = cypress.open diff --git a/cli/lib/cli.js b/cli/lib/cli.js index 052893ca4c9..2be77ecd394 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -93,6 +93,7 @@ const parseVariableOpts = (fnArgs, args) => { } const descriptions = { + autoCancelAfterFailures: 'overrides the project-level Cloud configuration to set the failed test threshold for auto cancellation or to disable auto cancellation when recording to the Cloud', browser: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.', cacheClear: 'delete all cached binaries', cachePrune: 'deletes all cached binaries except for the version currently in use', @@ -110,7 +111,7 @@ const descriptions = { exit: 'keep the browser open after tests finish', forceInstall: 'force install the Cypress binary', global: 'force Cypress into global mode as if its globally installed', - group: 'a named group for recorded runs in the Cypress Dashboard', + group: 'a named group for recorded runs in Cypress Cloud', headed: 'displays the browser instead of running headlessly', headless: 'hide the browser instead of running headed (default for cypress run)', key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.', @@ -118,11 +119,13 @@ const descriptions = { port: 'runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}.', project: 'path to the project', quiet: 'run quietly, using only the configured reporter', - record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.', + record: 'records the run. sends test results, screenshots and videos to Cypress Cloud.', reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"', reporterOptions: 'options for the mocha reporter. defaults to "null"', + runnerUi: 'displays the Cypress Runner UI', + noRunnerUi: 'hides the Cypress Runner UI', spec: 'runs specific spec file(s). defaults to "all"', - tag: 'named tag(s) for recorded runs in the Cypress Dashboard', + tag: 'named tag(s) for recorded runs in Cypress Cloud', version: 'prints Cypress version', } @@ -153,26 +156,16 @@ const text = (description) => { function includesVersion (args) { return ( - _.includes(args, 'version') || _.includes(args, '--version') || _.includes(args, '-v') ) } -function showVersions (args) { +function showVersions (opts) { debug('printing Cypress version') - debug('additional arguments %o', args) + debug('additional arguments %o', opts) - const versionParser = commander.option( - '--component ', 'component to report version for', - ) - .allowUnknownOption(true) - const parsed = versionParser.parse(args) - const parsedOptions = { - component: parsed.component, - } - - debug('parsed version arguments %o', parsedOptions) + debug('parsed version arguments %o', opts) const reportAllVersions = (versions) => { logger.always('Cypress package version:', versions.package) @@ -214,8 +207,8 @@ function showVersions (args) { return require('./exec/versions') .getVersions() .then((versions = defaultVersions) => { - if (parsedOptions.component) { - reportComponentVersion(parsedOptions.component, versions) + if (opts?.component) { + reportComponentVersion(opts.component, versions) } else { reportAllVersions(versions) } @@ -242,6 +235,7 @@ const addCypressRunCommand = (program) => { .command('run') .usage('[options]') .description('Runs Cypress tests from the CLI without the GUI') + .option('--auto-cancel-after-failures ', text('autoCancelAfterFailures')) .option('-b, --browser ', text('browser')) .option('--ci-build-id ', text('ciBuildId')) .option('--component', text('component')) @@ -260,6 +254,8 @@ const addCypressRunCommand = (program) => { .option('-q, --quiet', text('quiet')) .option('--record [bool]', text('record'), coerceFalse) .option('-r, --reporter ', text('reporter')) + .option('--runner-ui', text('runnerUi')) + .option('--no-runner-ui', text('noRunnerUi')) .option('-o, --reporter-options ', text('reporterOptions')) .option('-s, --spec ', text('spec')) .option('-t, --tag ', text('tag')) @@ -405,7 +401,21 @@ module.exports = { args = process.argv } - const { CYPRESS_INTERNAL_ENV } = process.env + const { CYPRESS_INTERNAL_ENV, CYPRESS_DOWNLOAD_USE_CA } = process.env + + if (process.env.CYPRESS_DOWNLOAD_USE_CA) { + let msg = ` + ${logSymbols.warning} Warning: It looks like you're setting CYPRESS_DOWNLOAD_USE_CA=${CYPRESS_DOWNLOAD_USE_CA} + + The environment variable "CYPRESS_DOWNLOAD_USE_CA" is no longer required to be set. + + You can safely unset this environment variable. + ` + + logger.log() + logger.warn(stripIndent(msg)) + logger.log() + } if (!util.isValidCypressInternalEnvValue(CYPRESS_INTERNAL_ENV)) { debug('invalid CYPRESS_INTERNAL_ENV value', CYPRESS_INTERNAL_ENV) @@ -440,13 +450,19 @@ module.exports = { program.help() }) - program + const handleVersion = (cmd) => { + return cmd + .option('--component ', 'component to report version for') + .action((opts, ...other) => { + showVersions(util.parseOpts(opts)) + }) + } + + handleVersion(program + .storeOptionsAsProperties() .option('-v, --version', text('version')) .command('version') - .description(text('version')) - .action(() => { - showVersions(args) - }) + .description(text('version'))) maybeAddInspectFlags(addCypressOpenCommand(program)) .action((opts) => { @@ -649,7 +665,7 @@ module.exports = { // and now does not understand top level options // .option('-v, --version').command('version') // so we have to manually catch '-v, --version' - return showVersions(args) + handleVersion(program) } debug('program parsing arguments') diff --git a/cli/lib/cypress.js b/cli/lib/cypress.js index 3ecbad4c2dd..ff140859ff2 100644 --- a/cli/lib/cypress.js +++ b/cli/lib/cypress.js @@ -31,6 +31,8 @@ const cypressModuleApi = { options = util.normalizeModuleOptions(options) + tmp.setGracefulCleanup() + return tmp.fileAsync() .then((outputPath) => { options.outputPath = outputPath @@ -85,6 +87,24 @@ const cypressModuleApi = { defineConfig (config) { return config }, + + /** + * Provides automatic code completion for Component Frameworks Definitions. + * While it's not strictly necessary for Cypress to parse your configuration, we + * recommend wrapping your Component Framework Definition object with `defineComponentFramework()` + * @example + * module.exports = defineComponentFramework({ + * type: 'cypress-ct-solid-js' + * // ... + * }) + * + * @see ../types/cypress-npm-api.d.ts + * @param {Cypress.ThirdPartyComponentFrameworkDefinition} config + * @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter + */ + defineComponentFramework (config) { + return config + }, } module.exports = cypressModuleApi diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index e070205901b..965e2f9fe97 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -45,6 +45,10 @@ const processRunOptions = (options = {}) => { const args = ['--run-project', options.project] + if (options.autoCancelAfterFailures || options.autoCancelAfterFailures === 0 || options.autoCancelAfterFailures === false) { + args.push('--auto-cancel-after-failures', options.autoCancelAfterFailures) + } + if (options.browser) { args.push('--browser', options.browser) } @@ -129,6 +133,10 @@ const processRunOptions = (options = {}) => { args.push('--reporter-options', options.reporterOptions) } + if (options.runnerUi != null) { + args.push('--runner-ui', options.runnerUi) + } + // if we have specific spec(s) push that into the args if (options.spec) { args.push('--spec', options.spec) diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index 096458fa502..e132b964e0a 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -4,7 +4,7 @@ const cp = require('child_process') const path = require('path') const Promise = require('bluebird') const debug = require('debug')('cypress:cli') -const debugElectron = require('debug')('cypress:electron') +const debugVerbose = require('debug')('cypress-verbose:cli') const util = require('../util') const state = require('../tasks/state') @@ -44,7 +44,36 @@ const isCertVerifyProcBuiltin = /(^\[.*ERROR:cert_verify_proc_builtin\.cc|^----- // objc[60540]: Class WebSwapCGLLayer is implemented in both /System/Library/Frameworks/WebKit.framework/Versions/A/Frameworks/WebCore.framework/Versions/A/Frameworks/libANGLE-shared.dylib (0x7ffa5a006318) and /{path/to/app}/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libGLESv2.dylib (0x10f8a89c8). One of the two will be used. Which one is undefined. const isMacOSElectronWebSwapCGLLayerWarning = /^objc\[\d+\]: Class WebSwapCGLLayer is implemented in both.*Which one is undefined\./ -const GARBAGE_WARNINGS = [isXlibOrLibudevRe, isHighSierraWarningRe, isRenderWorkerRe, isDbusWarning, isCertVerifyProcBuiltin, isMacOSElectronWebSwapCGLLayerWarning] +/** + * Electron logs benign warnings about Vulkan when run on hosts that do not have a GPU. This is coming from the primary Electron process, + * and not the browser being used for tests. + * Samples: + * Warning: loader_scanned_icd_add: Driver /usr/lib/x86_64-linux-gnu/libvulkan_intel.so supports Vulkan 1.2, but only supports loader interface version 4. Interface version 5 or newer required to support this version of Vulkan (Policy #LDP_DRIVER_7) + * Warning: loader_scanned_icd_add: Driver /usr/lib/x86_64-linux-gnu/libvulkan_lvp.so supports Vulkan 1.1, but only supports loader interface version 4. Interface version 5 or newer required to support this version of Vulkan (Policy #LDP_DRIVER_7) + * Warning: loader_scanned_icd_add: Driver /usr/lib/x86_64-linux-gnu/libvulkan_radeon.so supports Vulkan 1.2, but only supports loader interface version 4. Interface version 5 or newer required to support this verison of Vulkan (Policy #LDP_DRIVER_7) + * Warning: Layer VK_LAYER_MESA_device_select uses API version 1.2 which is older than the application specified API version of 1.3. May cause issues. + */ + +const isHostVulkanDriverWarning = /^Warning:.+(#LDP_DRIVER_7|VK_LAYER_MESA_device_select).+/ + +/** + * Electron logs benign warnings about Vulkan when run in docker containers whose host does not have a GPU. This is coming from the primary + * Electron process, and not the browser being used for tests. + * Sample: + * Warning: vkCreateInstance: Found no drivers! + * Warning: vkCreateInstance failed with VK_ERROR_INCOMPATIBLE_DRIVER + * at CheckVkSuccessImpl (../../third_party/dawn/src/dawn/native/vulkan/VulkanError.cpp:88) + * at CreateVkInstance (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:458) + * at Initialize (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:344) + * at Create (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:266) + * at operator() (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:521) + */ + +const isContainerVulkanDriverWarning = /^Warning: vkCreateInstance/ + +const isContainerVulkanStack = /^\s*at (CheckVkSuccessImpl|CreateVkInstance|Initialize|Create|operator).+(VulkanError|BackendVk).cpp/ + +const GARBAGE_WARNINGS = [isXlibOrLibudevRe, isHighSierraWarningRe, isRenderWorkerRe, isDbusWarning, isCertVerifyProcBuiltin, isMacOSElectronWebSwapCGLLayerWarning, isHostVulkanDriverWarning, isContainerVulkanDriverWarning, isContainerVulkanStack] const isGarbageLineWarning = (str) => { return _.some(GARBAGE_WARNINGS, (re) => { @@ -122,10 +151,9 @@ module.exports = { return new Promise((resolve, reject) => { _.defaults(overrides, { onStderrData: false, - electronLogging: false, }) - const { onStderrData, electronLogging } = overrides + const { onStderrData } = overrides const envOverrides = util.getEnvOverrides(options) const electronArgs = [] const node11WindowsFix = isPlatform('win32') @@ -160,10 +188,6 @@ module.exports = { stdioOptions = _.extend({}, stdioOptions, { windowsHide: false }) } - if (electronLogging) { - stdioOptions.env.ELECTRON_ENABLE_LOGGING = true - } - if (util.isPossibleLinuxWithIncorrectDisplay()) { // make sure we use the latest DISPLAY variable if any debug('passing DISPLAY', process.env.DISPLAY) @@ -236,12 +260,14 @@ module.exports = { // bail if this is warning line garbage if (isGarbageLineWarning(str)) { + debugVerbose(str) + return } - // if we have a callback and this explictly returns + // if we have a callback and this explicitly returns // false then bail - if (onStderrData && onStderrData(str) === false) { + if (onStderrData && onStderrData(str)) { return } @@ -294,13 +320,6 @@ module.exports = { if (util.isBrokenGtkDisplay(str)) { brokenGtkDisplay = true } - - // we should attempt to always slurp up - // the stderr logs unless we've explicitly - // enabled the electron debug logging - if (!debugElectron.enabled) { - return false - } }, }) } diff --git a/cli/lib/tasks/cache.js b/cli/lib/tasks/cache.js index cb653acb730..15a19a841c6 100644 --- a/cli/lib/tasks/cache.js +++ b/cli/lib/tasks/cache.js @@ -33,14 +33,14 @@ const clear = () => { const prune = () => { const cacheDir = state.getCacheDir() - const currentVersion = util.pkgVersion() + const checkedInBinaryVersion = util.pkgVersion() let deletedBinary = false return fs.readdirAsync(cacheDir) .then((versions) => { return Bluebird.all(versions.map((version) => { - if (version !== currentVersion) { + if (version !== checkedInBinaryVersion) { deletedBinary = true const versionDir = join(cacheDir, version) @@ -51,7 +51,7 @@ const prune = () => { }) .then(() => { if (deletedBinary) { - logger.always(`Deleted all binary caches except for the ${currentVersion} binary cache.`) + logger.always(`Deleted all binary caches except for the ${checkedInBinaryVersion} binary cache.`) } else { logger.always(`No binary caches found to prune.`) } diff --git a/cli/lib/tasks/download.js b/cli/lib/tasks/download.js index 6b8cd38b69b..01d0f73bc0a 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -40,13 +40,7 @@ const getBaseUrl = () => { const getCA = () => { return new Promise((resolve) => { - if (!util.getEnv('CYPRESS_DOWNLOAD_USE_CA')) { - resolve() - } - - if (process.env.npm_config_ca) { - resolve(process.env.npm_config_ca) - } else if (process.env.npm_config_cafile) { + if (process.env.npm_config_cafile) { fs.readFile(process.env.npm_config_cafile, 'utf8') .then((cafileContent) => { resolve(cafileContent) @@ -54,6 +48,8 @@ const getCA = () => { .catch(() => { resolve() }) + } else if (process.env.npm_config_ca) { + resolve(process.env.npm_config_ca) } else { resolve() } @@ -68,10 +64,10 @@ const prepend = (arch, urlPath, version) => { return pathTemplate ? ( pathTemplate - .replace(/\\?\$\{endpoint\}/, endpoint) - .replace(/\\?\$\{platform\}/, platform) - .replace(/\\?\$\{arch\}/, arch) - .replace(/\\?\$\{version\}/, version) + .replace(/\\?\$\{endpoint\}/g, endpoint) + .replace(/\\?\$\{platform\}/g, platform) + .replace(/\\?\$\{arch\}/g, arch) + .replace(/\\?\$\{version\}/g, version) ) : `${endpoint}?platform=${platform}&arch=${arch}` } diff --git a/cli/lib/tasks/unzip.js b/cli/lib/tasks/unzip.js index 5993bd2700a..a4c911a5dfa 100644 --- a/cli/lib/tasks/unzip.js +++ b/cli/lib/tasks/unzip.js @@ -84,11 +84,11 @@ const unzip = ({ zipFilePath, installDir, progress }) => { return resolve() }) .catch((err) => { - if (err) { - debug('error %s', err.message) + const error = err || new Error('Unknown error with Node extract tool') - return reject(err) - } + debug('error %s', error.message) + + return reject(error) }) } diff --git a/cli/lib/tasks/verify.js b/cli/lib/tasks/verify.js index 826c35e1654..87e44412b67 100644 --- a/cli/lib/tasks/verify.js +++ b/cli/lib/tasks/verify.js @@ -101,12 +101,11 @@ const runSmokeTest = (binaryDir, options) => { debug('smoke test command:', smokeTestCommand) debug('smoke test timeout %d ms', options.smokeTestTimeout) - const env = _.extend({}, process.env, { - ELECTRON_ENABLE_LOGGING: true, - }) - const stdioOptions = _.extend({}, { - env, + env: { + ...process.env, + FORCE_COLOR: 0, + }, timeout: options.smokeTestTimeout, }) diff --git a/cli/lib/util.js b/cli/lib/util.js index 5604e313c23..653ffdd1af0 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -21,7 +21,6 @@ const isInstalledGlobally = require('is-installed-globally') const logger = require('./logger') const debug = require('debug')('cypress:cli') const fs = require('./fs') -const semver = require('semver') const pkg = require(path.join(__dirname, '..', 'package.json')) @@ -134,7 +133,7 @@ function isValidCypressInternalEnvValue (value) { return true } - // names of config environments, see "packages/server/config/app.yml" + // names of config environments, see "packages/server/config/app.json" const names = ['development', 'test', 'staging', 'production'] return _.includes(names, value) @@ -192,6 +191,7 @@ const dequote = (str) => { const parseOpts = (opts) => { opts = _.pick(opts, + 'autoCancelAfterFailures', 'browser', 'cachePath', 'cacheList', @@ -225,6 +225,7 @@ const parseOpts = (opts) => { 'reporter', 'reporterOptions', 'record', + 'runnerUi', 'runProject', 'spec', 'tag') @@ -257,7 +258,7 @@ const getApplicationDataFolder = (...paths) => { const { env } = process // allow overriding the app_data folder - let folder = env.CYPRESS_KONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development' + let folder = env.CYPRESS_CONFIG_ENV || env.CYPRESS_INTERNAL_ENV || 'development' const PRODUCT_NAME = pkg.productName || pkg.name const OS_DATA_PATH = ospath.data() @@ -304,21 +305,6 @@ const util = { opts.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS } - // https://github.com/cypress-io/cypress/issues/18914 - // Node 17+ ships with OpenSSL 3 by default, so we may need the option - // --openssl-legacy-provider so that webpack@4 can use the legacy MD4 hash - // function. This option doesn't exist on Node <17 or when it is built - // against OpenSSL 1, so we have to detect Node's major version and check - // which version of OpenSSL it was built against before spawning the plugins - // process. - - // To be removed when the Cypress binary pulls in the @cypress/webpack-batteries-included-preprocessor - // version that has been updated to webpack >= 5.61, which no longer relies on - // Node's builtin crypto.hash function. - if (process.versions && semver.satisfies(process.versions.node, '>=17.0.0') && semver.satisfies(process.versions.openssl, '>=3', { includePrerelease: true })) { - opts.ORIGINAL_NODE_OPTIONS = `${opts.ORIGINAL_NODE_OPTIONS || ''} --openssl-legacy-provider` - } - return opts }, @@ -345,7 +331,7 @@ const util = { }, supportsColor () { - // if we've been explictly told not to support + // if we've been explicitly told not to support // color then turn this off if (process.env.NO_COLOR) { return false @@ -533,6 +519,7 @@ const util = { la(is.unemptyString(varName), 'expected environment variable name, not', varName) const configVarName = `npm_config_${varName}` + const configVarNameLower = configVarName.toLowerCase() const packageConfigVarName = `npm_package_config_${varName}` let result @@ -545,6 +532,10 @@ const util = { debug(`Using ${varName} from npm config`) result = process.env[configVarName] + } else if (process.env.hasOwnProperty(configVarNameLower)) { + debug(`Using ${varName.toLowerCase()} from npm config`) + + result = process.env[configVarNameLower] } else if (process.env.hasOwnProperty(packageConfigVarName)) { debug(`Using ${varName} from package.json config`) diff --git a/cli/package.json b/cli/package.json index 0cb51269c79..c918056ac5a 100644 --- a/cli/package.json +++ b/cli/package.json @@ -4,11 +4,12 @@ "private": true, "main": "index.js", "scripts": { + "build-cli": "node ./scripts/build.js && node ./scripts/post-build.js", "clean": "node ./scripts/clean.js", - "prebuild": "yarn postinstall && node ./scripts/start-build.js", - "build": "node ./scripts/build.js", "dtslint": "dtslint types", "postinstall": "patch-package && node ./scripts/post-install.js", + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json,.vue .", + "prebuild": "yarn postinstall && node ./scripts/start-build.js", "size": "t=\"cypress-v0.0.0.tgz\"; yarn pack --filename \"${t}\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", "test": "yarn test-unit", "test-debug": "node --inspect-brk $(yarn bin mocha)", @@ -19,24 +20,23 @@ "unit": "cross-env BLUEBIRD_DEBUG=1 NODE_ENV=test mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json" }, "dependencies": { - "@cypress/request": "^2.88.10", + "@cypress/request": "^3.0.0", "@cypress/xvfb": "^1.2.4", - "@types/node": "^14.14.31", "@types/sinonjs__fake-timers": "8.1.1", "@types/sizzle": "^2.3.2", "arch": "^2.2.0", "blob-util": "^2.0.2", "bluebird": "^3.7.2", - "buffer": "^5.6.0", + "buffer": "^5.7.1", "cachedir": "^2.3.0", "chalk": "^4.1.0", "check-more-types": "^2.24.0", "cli-cursor": "^3.1.0", "cli-table3": "~0.6.1", - "commander": "^5.1.0", + "commander": "^6.2.1", "common-tags": "^1.8.0", "dayjs": "^1.10.4", - "debug": "^4.3.2", + "debug": "^4.3.4", "enquirer": "^2.3.6", "eventemitter2": "6.4.7", "execa": "4.1.0", @@ -45,27 +45,37 @@ "figures": "^3.2.0", "fs-extra": "^9.1.0", "getos": "^3.2.1", - "is-ci": "^3.0.0", + "is-ci": "^3.0.1", "is-installed-globally": "~0.4.0", "lazy-ass": "^1.6.0", "listr2": "^3.8.3", "lodash": "^4.17.21", "log-symbols": "^4.0.0", - "minimist": "^1.2.6", + "minimist": "^1.2.8", "ospath": "^1.2.2", "pretty-bytes": "^5.6.0", + "process": "^0.11.10", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", - "semver": "^7.3.2", + "semver": "^7.5.3", "supports-color": "^8.1.1", - "tmp": "~0.2.1", + "tmp": "~0.2.3", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, "devDependencies": { - "@babel/cli": "7.13.0", - "@babel/preset-env": "7.13.5", + "@babel/cli": "7.24.5", + "@babel/preset-env": "7.24.5", + "@cypress/angular": "0.0.0-development", + "@cypress/angular-signals": "0.0.0-development", + "@cypress/grep": "0.0.0-development", + "@cypress/mount-utils": "0.0.0-development", + "@cypress/react": "0.0.0-development", + "@cypress/react18": "0.0.0-development", "@cypress/sinon-chai": "2.9.1", + "@cypress/svelte": "0.0.0-development", + "@cypress/vue": "0.0.0-development", + "@cypress/vue2": "0.0.0-development", "@packages/root": "0.0.0-development", "@types/bluebird": "3.5.33", "@types/chai": "4.2.15", @@ -85,10 +95,9 @@ "execa-wrap": "1.4.0", "hasha": "5.2.2", "mocha": "6.2.2", - "mock-fs": "5.1.1", + "mock-fs": "5.2.0", "mocked-env": "1.3.2", "nock": "13.2.9", - "postinstall-postinstall": "2.1.0", "proxyquire": "2.1.3", "resolve-pkg": "2.0.0", "shelljs": "0.8.5", @@ -109,63 +118,85 @@ "vue2", "react18", "angular", - "svelte" + "svelte", + "angular-signals" ], "bin": { "cypress": "bin/cypress" }, "engines": { - "node": ">=12.0.0" + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" }, "types": "types", "exports": { ".": { + "types": "./types/index.d.ts", "import": "./index.mjs", - "require": "./index.js", - "types": "./types/index.d.ts" + "require": "./index.js" }, "./vue": { + "types": "./vue/dist/index.d.ts", "import": "./vue/dist/cypress-vue.esm-bundler.js", - "require": "./vue/dist/cypress-vue.cjs.js", - "types": "./vue/dist/index.d.ts" + "require": "./vue/dist/cypress-vue.cjs.js" }, "./vue2": { + "types": "./vue2/dist/index.d.ts", "import": "./vue2/dist/cypress-vue2.esm-bundler.js", - "require": "./vue2/dist/cypress-vue2.cjs.js", - "types": "./vue2/dist/index.d.ts" + "require": "./vue2/dist/cypress-vue2.cjs.js" }, "./package.json": { "import": "./package.json", "require": "./package.json" }, "./react": { + "types": "./react/dist/index.d.ts", "import": "./react/dist/cypress-react.esm-bundler.js", - "require": "./react/dist/cypress-react.cjs.js", - "types": "./react/dist/index.d.ts" + "require": "./react/dist/cypress-react.cjs.js" }, "./react18": { + "types": "./react18/dist/index.d.ts", "import": "./react18/dist/cypress-react.esm-bundler.js", - "require": "./react18/dist/cypress-react.cjs.js", - "types": "./react18/dist/index.d.ts" + "require": "./react18/dist/cypress-react.cjs.js" }, "./mount-utils": { - "require": "./mount-utils/dist/index.js", - "types": "./mount-utils/dist/index.d.ts" + "types": "./mount-utils/dist/index.d.ts", + "require": "./mount-utils/dist/index.js" }, "./angular": { + "types": "./angular/dist/index.d.ts", "import": "./angular/dist/index.js", - "require": "./angular/dist/index.js", - "types": "./angular/dist/index.d.ts" + "require": "./angular/dist/index.js" }, "./svelte": { + "types": "./svelte/dist/index.d.ts", "import": "./svelte/dist/cypress-svelte.esm-bundler.js", - "require": "./svelte/dist/cypress-svelte.cjs.js", - "types": "./svelte/dist/index.d.ts" + "require": "./svelte/dist/cypress-svelte.cjs.js" + }, + "./angular-signals": { + "types": "./angular-signals/dist/index.d.ts", + "import": "./angular-signals/dist/index.js", + "require": "./angular-signals/dist/index.js" } }, "workspaces": { "nohoist": [ "@types/*" ] + }, + "nx": { + "targets": { + "build-cli": { + "dependsOn": [ + "prebuild" + ], + "outputs": [ + "{projectRoot}/types", + "{projectRoot}/build" + ] + } + }, + "implicitDependencies": [ + "@cypress/*" + ] } } diff --git a/cli/patches/@types+sinon+9.0.9.patch b/cli/patches/@types+sinon+9.0.9.patch new file mode 100644 index 00000000000..4c3a0fc1056 --- /dev/null +++ b/cli/patches/@types+sinon+9.0.9.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/@types/sinon/index.d.ts b/node_modules/@types/sinon/index.d.ts +index 07ba706..fd32cbe 100644 +--- a/node_modules/@types/sinon/index.d.ts ++++ b/node_modules/@types/sinon/index.d.ts +@@ -45,7 +45,7 @@ declare namespace Sinon { + * so a call that received the provided arguments (in the same spots) and possibly others as well will return true. + * @param args + */ +- calledWith(...args: Partial>): boolean; ++ calledWith(...args: Partial>[]): boolean; + /** + * Returns true if spy was called at least once with the provided arguments and no others. + */ diff --git a/cli/scripts/build.js b/cli/scripts/build.js index b5fe4fb8c24..a957f3e701b 100644 --- a/cli/scripts/build.js +++ b/cli/scripts/build.js @@ -1,7 +1,6 @@ const _ = require('lodash') const path = require('path') const shell = require('shelljs') - const fs = require('../lib/fs') // grab the current version and a few other properties @@ -24,7 +23,7 @@ function getStdout (cmd) { return shell.exec(cmd).trim() } -function preparePackageForNpmRelease (json) { +function preparePackageForNpmRelease (json, branchName) { // modify the existing package.json // to prepare it for releasing to npm delete json.devDependencies @@ -36,7 +35,7 @@ function preparePackageForNpmRelease (json) { _.extend(json, { version, buildInfo: { - commitBranch: process.env.CIRCLE_BRANCH || getStdout('git branch --show-current'), + commitBranch: branchName || process.env.CIRCLE_BRANCH || getStdout('git branch --show-current'), commitSha: getStdout('git rev-parse HEAD'), commitDate: new Date(getStdout('git show -s --format=%ci')).toISOString(), stable: false, @@ -57,9 +56,9 @@ function preparePackageForNpmRelease (json) { return json } -function makeUserPackageFile () { +function makeUserPackageFile (branchName) { return fs.readJsonAsync(packageJsonSrc) - .then(preparePackageForNpmRelease) + .then((json) => preparePackageForNpmRelease(json, branchName)) .then((json) => { return fs.outputJsonAsync(packageJsonDest, json, { spaces: 2, @@ -71,7 +70,7 @@ function makeUserPackageFile () { module.exports = makeUserPackageFile if (!module.parent) { - makeUserPackageFile() + makeUserPackageFile(process.env.BRANCH) .catch((err) => { /* eslint-disable no-console */ console.error('Could not write user package file') diff --git a/cli/scripts/post-build.js b/cli/scripts/post-build.js index ab853799a74..752578adf98 100644 --- a/cli/scripts/post-build.js +++ b/cli/scripts/post-build.js @@ -13,6 +13,7 @@ const npmModulesToCopy = [ 'vue', 'vue2', 'angular', + 'angular-signals', 'svelte', ] diff --git a/cli/test/lib/cli_spec.js b/cli/test/lib/cli_spec.js index e5d8506fe0a..3e0de35ddfa 100644 --- a/cli/test/lib/cli_spec.js +++ b/cli/test/lib/cli_spec.js @@ -23,11 +23,11 @@ describe('cli', () => { beforeEach(() => { logger.reset() - sinon.stub(process, 'exit') + sinon.stub(process, 'exit').returns(null) os.platform.returns('darwin') - // sinon.stub(util, 'exit') - sinon.stub(util, 'logErrorExit1') + sinon.stub(util, 'logErrorExit1').returns(null) + sinon.stub(util, 'pkgBuildInfo').returns({ stable: true }) this.exec = (args) => { const cliArgs = `node test ${args}`.split(' ') @@ -136,189 +136,169 @@ describe('cli', () => { }) }) - context('cypress version', () => { - let restoreEnv - - afterEach(() => { - if (restoreEnv) { - restoreEnv() - restoreEnv = null - } - }) + ;['--version', '-v', 'version'].forEach((versionCommand) => { + context(`cypress ${versionCommand}`, () => { + let restoreEnv - const binaryDir = '/binary/dir' + afterEach(() => { + if (restoreEnv) { + restoreEnv() + restoreEnv = null + } + }) - beforeEach(() => { - sinon.stub(state, 'getBinaryDir').returns(binaryDir) - }) + const binaryDir = '/binary/dir' - describe('individual package versions', () => { beforeEach(() => { + sinon.stub(state, 'getBinaryDir').returns(binaryDir) + }) + + describe('individual package versions', () => { + beforeEach(() => { + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon + .stub(state, 'getBinaryPkgAsync') + .withArgs(binaryDir) + .resolves({ + version: 'X.Y.Z', + electronVersion: '10.9.8', + electronNodeVersion: '7.7.7', + }) + }) + + it('reports just the package version', (done) => { + this.exec(`${versionCommand} --component package`) + process.exit.callsFake((exitCode) => { + expect(logger.print()).to.equal('1.2.3') + done() + }) + }) + + it('reports just the binary version', (done) => { + this.exec(`${versionCommand} --component binary`) + process.exit.callsFake(() => { + expect(logger.print()).to.equal('X.Y.Z') + done() + }) + }) + + it('reports just the electron version', (done) => { + this.exec(`${versionCommand} --component electron`) + process.exit.callsFake(() => { + expect(logger.print()).to.equal('10.9.8') + done() + }) + }) + + it('reports just the bundled Node version', (done) => { + this.exec(`${versionCommand} --component node`) + process.exit.callsFake(() => { + expect(logger.print()).to.equal('7.7.7') + done() + }) + }) + + it('handles not found bundled Node version', (done) => { + state.getBinaryPkgAsync + .withArgs(binaryDir) + .resolves({ + version: 'X.Y.Z', + electronVersion: '10.9.8', + }) + + this.exec(`${versionCommand} --component node`) + process.exit.callsFake(() => { + expect(logger.print()).to.equal('not found') + done() + }) + }) + }) + + it('reports package version', (done) => { sinon.stub(util, 'pkgVersion').returns('1.2.3') sinon .stub(state, 'getBinaryPkgAsync') .withArgs(binaryDir) .resolves({ version: 'X.Y.Z', - electronVersion: '10.9.8', - electronNodeVersion: '7.7.7', }) - }) - it('reports just the package version', (done) => { - this.exec('version --component package') + this.exec(versionCommand) process.exit.callsFake(() => { - expect(logger.print()).to.equal('1.2.3') + snapshot('cli version and binary version 1', logger.print(), { allowSharedSnapshot: true }) done() }) }) - it('reports just the binary version', (done) => { - this.exec('version --component binary') + it('reports package and binary message', (done) => { + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgAsync').resolves({ version: 'X.Y.Z' }) + + this.exec(versionCommand) process.exit.callsFake(() => { - expect(logger.print()).to.equal('X.Y.Z') + snapshot('cli version and binary version 2', logger.print(), { allowSharedSnapshot: true }) done() }) }) - it('reports just the electron version', (done) => { - this.exec('version --component electron') - process.exit.callsFake(() => { - expect(logger.print()).to.equal('10.9.8') - done() + it('reports electron and node message', (done) => { + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgAsync').resolves({ + version: 'X.Y.Z', + electronVersion: '10.10.88', + electronNodeVersion: '11.10.3', }) - }) - it('reports just the bundled Node version', (done) => { - this.exec('version --component node') + this.exec(versionCommand) process.exit.callsFake(() => { - expect(logger.print()).to.equal('7.7.7') + snapshot('cli version with electron and node 1', logger.print(), { allowSharedSnapshot: true }) done() }) }) - it('handles not found bundled Node version', (done) => { - state.getBinaryPkgAsync - .withArgs(binaryDir) - .resolves({ - version: 'X.Y.Z', - electronVersion: '10.9.8', + it('reports package and binary message with npm log silent', (done) => { + restoreEnv = mockedEnv({ + npm_config_loglevel: 'silent', }) - this.exec('version --component node') + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgAsync').resolves({ version: 'X.Y.Z' }) + + this.exec(versionCommand) process.exit.callsFake(() => { - expect(logger.print()).to.equal('not found') + // should not be empty! + snapshot('cli version and binary version with npm log silent', logger.print(), { allowSharedSnapshot: true }) done() }) }) - }) - - it('reports package version', (done) => { - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon - .stub(state, 'getBinaryPkgAsync') - .withArgs(binaryDir) - .resolves({ - version: 'X.Y.Z', - }) - this.exec('version') - process.exit.callsFake(() => { - snapshot('cli version and binary version 1', logger.print()) - done() - }) - }) - - it('reports package and binary message', (done) => { - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves({ version: 'X.Y.Z' }) - - this.exec('version') - process.exit.callsFake(() => { - snapshot('cli version and binary version 2', logger.print()) - done() - }) - }) - - it('reports electron and node message', (done) => { - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves({ - version: 'X.Y.Z', - electronVersion: '10.10.88', - electronNodeVersion: '11.10.3', - }) - - this.exec('version') - process.exit.callsFake(() => { - snapshot('cli version with electron and node 1', logger.print()) - done() - }) - }) - - it('reports package and binary message with npm log silent', (done) => { - restoreEnv = mockedEnv({ - npm_config_loglevel: 'silent', - }) - - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves({ version: 'X.Y.Z' }) - - this.exec('version') - process.exit.callsFake(() => { - // should not be empty! - snapshot('cli version and binary version with npm log silent', logger.print()) - done() - }) - }) - - it('reports package and binary message with npm log warn', (done) => { - restoreEnv = mockedEnv({ - npm_config_loglevel: 'warn', - }) + it('reports package and binary message with npm log warn', (done) => { + restoreEnv = mockedEnv({ + npm_config_loglevel: 'warn', + }) - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves({ - version: 'X.Y.Z', - }) + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgAsync').resolves({ + version: 'X.Y.Z', + }) - this.exec('version') - process.exit.callsFake(() => { + this.exec(versionCommand) + process.exit.callsFake(() => { // should not be empty! - snapshot('cli version and binary version with npm log warn', logger.print()) - done() - }) - }) - - it('handles non-existent binary version', (done) => { - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves(null) - - this.exec('version') - process.exit.callsFake(() => { - snapshot('cli version no binary version 1', logger.print()) - done() - }) - }) - - it('handles non-existent binary --version', (done) => { - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves(null) - - this.exec('--version') - process.exit.callsFake(() => { - snapshot('cli --version no binary version 1', logger.print()) - done() + snapshot('cli version and binary version with npm log warn', logger.print(), { allowSharedSnapshot: true }) + done() + }) }) - }) - it('handles non-existent binary -v', (done) => { - sinon.stub(util, 'pkgVersion').returns('1.2.3') - sinon.stub(state, 'getBinaryPkgAsync').resolves(null) + it('handles non-existent binary', (done) => { + sinon.stub(util, 'pkgVersion').returns('1.2.3') + sinon.stub(state, 'getBinaryPkgAsync').resolves(null) - this.exec('-v') - process.exit.callsFake(() => { - snapshot('cli -v no binary version 1', logger.print()) - done() + this.exec(versionCommand) + process.exit.callsFake(() => { + snapshot('cli version no binary version 1', logger.print(), { allowSharedSnapshot: true }) + done() + }) }) }) }) @@ -493,6 +473,26 @@ describe('cli', () => { this.exec('run --ci-build-id "123" --group "staging"') expect(run.start).to.be.calledWith({ ciBuildId: '123', group: 'staging' }) }) + + it('calls run with --auto-cancel-after-failures', () => { + this.exec('run --auto-cancel-after-failures 4') + expect(run.start).to.be.calledWith({ autoCancelAfterFailures: '4' }) + }) + + it('calls run with --auto-cancel-after-failures with false', () => { + this.exec('run --auto-cancel-after-failures false') + expect(run.start).to.be.calledWith({ autoCancelAfterFailures: 'false' }) + }) + + it('calls run with --runner-ui', () => { + this.exec('run --runner-ui') + expect(run.start).to.be.calledWith({ runnerUi: true }) + }) + + it('calls run with --no-runner-ui', () => { + this.exec('run --no-runner-ui') + expect(run.start).to.be.calledWith({ runnerUi: false }) + }) }) context('cypress open', () => { @@ -635,7 +635,7 @@ describe('cli', () => { expect(spawn.start.firstCall.args[0]).to.include('component') }) - it('spawns server with correct args for depricated component-testing command', () => { + it('spawns server with correct args for deprecated component-testing command', () => { this.exec('open-ct --dev') expect(spawn.start.firstCall.args[0]).to.include('--testing-type') expect(spawn.start.firstCall.args[0]).to.include('component') @@ -647,7 +647,7 @@ describe('cli', () => { expect(spawn.start.firstCall.args[0]).to.include('component') }) - it('runs server with correct args for depricated component-testing command', () => { + it('runs server with correct args for deprecated component-testing command', () => { this.exec('run-ct --dev') expect(spawn.start.firstCall.args[0]).to.include('--testing-type') expect(spawn.start.firstCall.args[0]).to.include('component') diff --git a/cli/test/lib/cypress_spec.js b/cli/test/lib/cypress_spec.js index 44579d7ac92..b2650e20ca1 100644 --- a/cli/test/lib/cypress_spec.js +++ b/cli/test/lib/cypress_spec.js @@ -100,10 +100,28 @@ describe('cypress', function () { } it('calls run#start, passing in options', () => { - return cypress.run({ spec: 'foo' }) + return cypress.run({ spec: 'foo', autoCancelAfterFailures: 4 }) .then(getStartArgs) .then((args) => { expect(args.spec).to.equal('foo') + expect(args.autoCancelAfterFailures).to.equal(4) + expect(args.runnerUi).to.be.undefined + }) + }) + + it('calls run#start, passing in autoCancelAfterFailures false', () => { + return cypress.run({ autoCancelAfterFailures: false }) + .then(getStartArgs) + .then((args) => { + expect(args.autoCancelAfterFailures).to.equal(false) + }) + }) + + it('calls run#start, passing in autoCancelAfterFailures 0', () => { + return cypress.run({ autoCancelAfterFailures: 0 }) + .then(getStartArgs) + .then((args) => { + expect(args.autoCancelAfterFailures).to.equal(0) }) }) diff --git a/cli/test/lib/exec/.eslintrc.json b/cli/test/lib/exec/.eslintrc.json deleted file mode 100644 index 74e0826b6db..00000000000 --- a/cli/test/lib/exec/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "parserOptions": { - "sourceType": "script" - } -} diff --git a/cli/test/lib/exec/run_spec.js b/cli/test/lib/exec/run_spec.js index a79d1a6e006..959bf65133c 100644 --- a/cli/test/lib/exec/run_spec.js +++ b/cli/test/lib/exec/run_spec.js @@ -216,5 +216,32 @@ describe('exec run', function () { ]) }) }) + + it('spawns with --auto-cancel-after-failures value', function () { + return run.start({ autoCancelAfterFailures: 4 }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--auto-cancel-after-failures', 4, + ]) + }) + }) + + it('spawns with --auto-cancel-after-failures value false', function () { + return run.start({ autoCancelAfterFailures: false }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--auto-cancel-after-failures', false, + ]) + }) + }) + + it('spawns with --runner-ui', function () { + return run.start({ runnerUi: true }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--runner-ui', true, + ]) + }) + }) }) }) diff --git a/cli/test/lib/exec/spawn_spec.js b/cli/test/lib/exec/spawn_spec.js index ce39d1b8fa3..26ad6d6713e 100644 --- a/cli/test/lib/exec/spawn_spec.js +++ b/cli/test/lib/exec/spawn_spec.js @@ -77,6 +77,19 @@ describe('lib/exec/spawn', function () { ERROR: No matching issuer found objc[60540]: Class WebSwapCGLLayer is implemented in both /System/Library/Frameworks/WebKit.framework/Versions/A/Frameworks/WebCore.framework/Versions/A/Frameworks/libANGLE-shared.dylib (0x7ffa5a006318) and /{path/to/app}/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libGLESv2.dylib (0x10f8a89c8). One of the two will be used. Which one is undefined. + + Warning: loader_scanned_icd_add: Driver /usr/lib/x86_64-linux-gnu/libvulkan_intel.so supports Vulkan 1.2, but only supports loader interface version 4. Interface version 5 or newer required to support this version of Vulkan (Policy #LDP_DRIVER_7) + Warning: loader_scanned_icd_add: Driver /usr/lib/x86_64-linux-gnu/libvulkan_lvp.so supports Vulkan 1.1, but only supports loader interface version 4. Interface version 5 or newer required to support this version of Vulkan (Policy #LDP_DRIVER_7) + Warning: loader_scanned_icd_add: Driver /usr/lib/x86_64-linux-gnu/libvulkan_radeon.so supports Vulkan 1.2, but only supports loader interface version 4. Interface version 5 or newer required to support this verison of Vulkan (Policy #LDP_DRIVER_7) + Warning: Layer VK_LAYER_MESA_device_select uses API version 1.2 which is older than the application specified API version of 1.3. May cause issues. + + Warning: vkCreateInstance: Found no drivers! + Warning: vkCreateInstance failed with VK_ERROR_INCOMPATIBLE_DRIVER + at CheckVkSuccessImpl (../../third_party/dawn/src/dawn/native/vulkan/VulkanError.cpp:88) + at CreateVkInstance (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:458) + at Initialize (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:344) + at Create (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:266) + at operator() (../../third_party/dawn/src/dawn/native/vulkan/BackendVk.cpp:521) ` const lines = _ @@ -409,7 +422,7 @@ describe('lib/exec/spawn', function () { }) }) - it('inherits when on linux and xvfb isnt needed', function () { + it('inherits when on linux and xvfb isn\'t needed', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) os.platform.returns('linux') xvfb.isNeeded.returns(false) diff --git a/cli/test/lib/tasks/cache_spec.js b/cli/test/lib/tasks/cache_spec.js index 02d96504ba7..bb74c2444bf 100644 --- a/cli/test/lib/tasks/cache_spec.js +++ b/cli/test/lib/tasks/cache_spec.js @@ -136,14 +136,14 @@ describe('lib/tasks/cache', () => { it('deletes cache binaries for all version but the current one', async () => { await cache.prune() - const currentVersion = util.pkgVersion() + const checkedInBinaryVersion = util.pkgVersion() const files = await fs.readdir('/.cache/Cypress') expect(files.length).to.eq(1) files.forEach((file) => { - expect(file).to.eq(currentVersion) + expect(file).to.eq(checkedInBinaryVersion) }) defaultSnapshot() @@ -155,14 +155,14 @@ describe('lib/tasks/cache', () => { await fs.removeAsync(dir) await cache.prune() - const currentVersion = util.pkgVersion() + const checkedInBinaryVersion = util.pkgVersion() const files = await fs.readdirAsync('/.cache/Cypress') expect(files.length).to.eq(1) files.forEach((file) => { - expect(file).to.eq(currentVersion) + expect(file).to.eq(checkedInBinaryVersion) }) defaultSnapshot() diff --git a/cli/test/lib/tasks/dependency_spec.js b/cli/test/lib/tasks/dependency_spec.js new file mode 100644 index 00000000000..49d00ad99f5 --- /dev/null +++ b/cli/test/lib/tasks/dependency_spec.js @@ -0,0 +1,26 @@ +/** + * as of Webpack 5, dependencies that are polyfilled through the Provide plugin must be defined inside the CLI + * in order to guarantee there is a version of the dependency accessible by the cypress CLI, either in the cypress directory + * or the root of their project. Currently, these two dependencies are 'buffer' and 'process' + */ +describe('dependencies', () => { + it('process dependency exists in package.json and is available', () => { + const { dependencies } = require('../../../package.json') + + expect(dependencies.process).to.be.ok + + const process = require('process') + + expect(typeof process).to.equal('object') + }) + + it('buffer dependency exists in package.json and is available', () => { + const { dependencies } = require('../../../package.json') + + expect(dependencies.buffer).to.be.ok + + const buffer = require('buffer') + + expect(typeof buffer).to.equal('object') + }) +}) diff --git a/cli/test/lib/tasks/download_spec.js b/cli/test/lib/tasks/download_spec.js index 13c1176795b..b808492e038 100644 --- a/cli/test/lib/tasks/download_spec.js +++ b/cli/test/lib/tasks/download_spec.js @@ -81,6 +81,13 @@ describe('lib/tasks/download', function () { snapshot('desktop url from template with version', normalize(url)) }) + it('returns custom url from template with multiple replacements', () => { + process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '${endpoint}/${platform}/${arch}/cypress-${version}-${platform}-${arch}.zip?referrer=${endpoint}&version=${version}' + const url = download.getUrl('ARCH', '0.20.2') + + snapshot('desktop url from template with multiple replacements', normalize(url)) + }) + it('returns custom url from template with escaped dollar sign', () => { process.env.CYPRESS_DOWNLOAD_PATH_TEMPLATE = '\\${endpoint}/\\${platform}-\\${arch}/cypress.zip' const url = download.getUrl('ARCH', '0.20.2') @@ -481,10 +488,6 @@ describe('lib/tasks/download', function () { }) context('architecture detection', () => { - beforeEach(() => { - sinon.stub(os, 'arch') - }) - context('Apple Silicon/M1', () => { function nockDarwinArm64 () { return nock('https://download.cypress.io') @@ -607,7 +610,7 @@ describe('lib/tasks/download', function () { // prevent ambient environment masking of environment variables referenced in this test ;([ - 'CYPRESS_DOWNLOAD_USE_CA', 'NO_PROXY', 'http_proxy', + 'NO_PROXY', 'http_proxy', 'https_proxy', 'npm_config_ca', 'npm_config_cafile', 'npm_config_https_proxy', 'npm_config_proxy', ]).forEach((e) => { @@ -683,7 +686,6 @@ describe('lib/tasks/download', function () { }) it('returns CA from npm_config_ca', () => { - process.env.CYPRESS_DOWNLOAD_USE_CA = 'true' process.env.npm_config_ca = 'foo' return download.getCA().then((ca) => { @@ -692,7 +694,6 @@ describe('lib/tasks/download', function () { }) it('returns CA from npm_config_cafile', () => { - process.env.CYPRESS_DOWNLOAD_USE_CA = 'true' process.env.npm_config_cafile = 'test/fixture/cafile.pem' return download.getCA().then((ca) => { @@ -701,7 +702,6 @@ describe('lib/tasks/download', function () { }) it('returns undefined if failed reading npm_config_cafile', () => { - process.env.CYPRESS_DOWNLOAD_USE_CA = 'true' process.env.npm_config_cafile = 'test/fixture/not-exists.pem' return download.getCA().then((ca) => { diff --git a/cli/test/lib/tasks/unzip_spec.js b/cli/test/lib/tasks/unzip_spec.js index 286439f6701..af42b21fe73 100644 --- a/cli/test/lib/tasks/unzip_spec.js +++ b/cli/test/lib/tasks/unzip_spec.js @@ -132,6 +132,45 @@ describe('lib/tasks/unzip', function () { }) }) + it('can try unzip first then fall back to node unzip and fails with an empty error', async function () { + const zipFilePath = path.join('test', 'fixture', 'example.zip') + + sinon.stub(unzip.utils.unzipTools, 'extract').callsFake(() => { + return new Promise((_, reject) => reject()) + }) + + const unzipChildProcess = new events.EventEmitter() + + unzipChildProcess.stdout = { + on () {}, + } + + unzipChildProcess.stderr = { + on () {}, + } + + sinon.stub(cp, 'spawn').withArgs('unzip').returns(unzipChildProcess) + + setTimeout(() => { + debug('emitting unzip error') + unzipChildProcess.emit('error', new Error('unzip fails badly')) + }, 100) + + try { + await unzip + .start({ + zipFilePath, + installDir, + }) + } catch (err) { + logger.error(err) + expect(err.message).to.include('Unknown error with Node extract tool') + + return + } + throw new Error('should have failed') + }) + it('calls node unzip just once', function (done) { const zipFilePath = path.join('test', 'fixture', 'example.zip') diff --git a/cli/test/lib/tasks/verify_spec.js b/cli/test/lib/tasks/verify_spec.js index b81ff36a55b..356a8f0c478 100644 --- a/cli/test/lib/tasks/verify_spec.js +++ b/cli/test/lib/tasks/verify_spec.js @@ -278,31 +278,40 @@ context('lib/tasks/verify', () => { }) }) - it('sets ELECTRON_ENABLE_LOGGING without mutating process.env', () => { - createfs({ - alreadyVerified: false, - executable: mockfs.file({ mode: 0o777 }), - packageVersion, - }) + describe('FORCE_COLOR', () => { + let previousForceColors - expect(process.env.ELECTRON_ENABLE_LOGGING).to.be.undefined + beforeEach(() => { + previousForceColors = process.env.FORCE_COLOR - util.exec.resolves() - sinon.stub(util, 'stdoutLineMatches').returns(true) + process.env.FORCE_COLOR = true + }) - return verify - .start() - .then(() => { - expect(process.env.ELECTRON_ENABLE_LOGGING).to.be.undefined + afterEach(() => { + process.env.FORCE_COLOR = previousForceColors + }) - const stdioOptions = util.exec.firstCall.args[2] + // @see https://github.com/cypress-io/cypress/issues/28982 + it('sets FORCE_COLOR to 0 when piping stdioOptions to to the smoke test to avoid ANSI in binary smoke test', () => { + createfs({ + alreadyVerified: false, + executable: mockfs.file({ mode: 0o777 }), + packageVersion, + }) - expect(stdioOptions).to.include({ - timeout: verify.VERIFY_TEST_RUNNER_TIMEOUT_MS, + util.exec.resolves({ + stdout: '222', + stderr: '', }) - expect(stdioOptions.env).to.include({ - ELECTRON_ENABLE_LOGGING: true, + return verify.start() + .then(() => { + expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222'], + sinon.match({ + env: { + FORCE_COLOR: 0, + }, + })) }) }) }) diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index 7a347e11294..26110cabc98 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -278,68 +278,6 @@ describe('util', () => { ORIGINAL_NODE_OPTIONS: '--require foo.js', }) }) - - // https://github.com/cypress-io/cypress/issues/18914 - it('includes --openssl-legacy-provider in Node 17+ w/ OpenSSL 3', () => { - sandbox.stub(process.versions, 'node').value('v17.1.0') - sandbox.stub(process.versions, 'openssl').value('3.0.0-quic') - - restoreEnv = mockedEnv({ - NODE_OPTIONS: '--require foo.js', - }) - - let childOptions = util.getOriginalNodeOptions() - - expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq('--require foo.js --openssl-legacy-provider') - - restoreEnv() - restoreEnv = mockedEnv({}) - childOptions = util.getOriginalNodeOptions() - - expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq(' --openssl-legacy-provider') - }) - - // https://github.com/cypress-io/cypress/issues/19320 - it('does not include --openssl-legacy-provider in Node 17+ w/ OpenSSL 1', () => { - sandbox.stub(process.versions, 'node').value('v17.1.0') - sandbox.stub(process.versions, 'openssl').value('1.0.0') - - restoreEnv = mockedEnv({ - NODE_OPTIONS: '--require foo.js', - }) - - let childOptions = util.getOriginalNodeOptions() - - expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq('--require foo.js') - expect(childOptions.ORIGINAL_NODE_OPTIONS).not.to.contain('--openssl-legacy-provider') - - restoreEnv() - restoreEnv = mockedEnv({}) - childOptions = util.getOriginalNodeOptions() - - expect(childOptions.ORIGINAL_NODE_OPTIONS).to.be.undefined - }) - - // https://github.com/cypress-io/cypress/issues/18914 - it('does not include --openssl-legacy-provider in Node <=16', () => { - sandbox.stub(process.versions, 'node').value('v16.14.2') - sandbox.stub(process.versions, 'openssl').value('1.0.0') - - restoreEnv = mockedEnv({}) - - let childOptions = util.getOriginalNodeOptions() - - expect(childOptions.ORIGINAL_NODE_OPTIONS).to.be.undefined - - restoreEnv = mockedEnv({ - NODE_OPTIONS: '--require foo.js', - }) - - childOptions = util.getOriginalNodeOptions() - - expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq('--require foo.js') - expect(childOptions.ORIGINAL_NODE_OPTIONS).not.to.contain('--openssl-legacy-provider') - }) }) context('.exit', () => { @@ -543,6 +481,11 @@ describe('util', () => { expect(util.getEnv('CYPRESS_FOO')).to.eql('') }) + it('npm config set should work', () => { + process.env.npm_config_cypress_foo_foo = 'bazz' + expect(util.getEnv('CYPRESS_FOO_FOO')).to.eql('bazz') + }) + it('throws on non-string name', () => { expect(() => { util.getEnv() diff --git a/cli/test/spec_helper.js b/cli/test/spec_helper.js index 06dd030427b..41ff9d6e8b1 100644 --- a/cli/test/spec_helper.js +++ b/cli/test/spec_helper.js @@ -97,8 +97,11 @@ sinon.stub = function (obj, method) { beforeEach(function () { sinon.stub(os, 'platform') + sinon.stub(os, 'arch') sinon.stub(os, 'release') sinon.stub(util, 'getOsVersionAsync').resolves('Foo-OsVersion') + + os.arch.returns('x64') }) afterEach(function () { diff --git a/cli/types/cy-http.d.ts b/cli/types/cy-http.d.ts deleted file mode 100644 index 3514d80b395..00000000000 --- a/cli/types/cy-http.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * This file should be deleted as soon as the serever - * TODO: delete this file when ResolvedDevServerConfig.server is converted to closeServer - */ - -/// -import * as cyUtilsHttp from 'http' -export = cyUtilsHttp -/** - * namespace created to bridge nodeJs.http typings so that - * we can type http Server in CT - */ -export as namespace cyUtilsHttp diff --git a/cli/types/cypress-eventemitter.d.ts b/cli/types/cypress-eventemitter.d.ts index 86d2587f722..adf6279c79c 100644 --- a/cli/types/cypress-eventemitter.d.ts +++ b/cli/types/cypress-eventemitter.d.ts @@ -3,8 +3,8 @@ type EventEmitter2 = import("eventemitter2").EventEmitter2 interface CyEventEmitter extends Omit { proxyTo: (cy: Cypress.cy) => null - emitMap: (eventName: string, args: any[]) => Array<(...args: any[]) => any> - emitThen: (eventName: string, args: any[]) => Bluebird.BluebirdStatic + emitMap: (eventName: string, ...args: any[]) => Array<(...args: any[]) => any> + emitThen: (eventName: string, ...args: any[]) => Bluebird.BluebirdStatic } // Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/events.d.ts diff --git a/cli/types/cypress-expect.d.ts b/cli/types/cypress-expect.d.ts index 74f71a57de8..bea4c7f566d 100644 --- a/cli/types/cypress-expect.d.ts +++ b/cli/types/cypress-expect.d.ts @@ -1,3 +1,3 @@ // Cypress adds chai expect and assert to global -declare const expect: Chai.ExpectStatic -declare const assert: Chai.AssertStatic +declare var expect: Chai.ExpectStatic +declare var assert: Chai.AssertStatic diff --git a/cli/types/cypress-global-vars.d.ts b/cli/types/cypress-global-vars.d.ts index ddeaa326a9e..e7665012797 100644 --- a/cli/types/cypress-global-vars.d.ts +++ b/cli/types/cypress-global-vars.d.ts @@ -7,7 +7,7 @@ cy.get('button').click() cy.get('.result').contains('Expected text') ``` */ -declare const cy: Cypress.cy & CyEventEmitter +declare var cy: Cypress.cy & CyEventEmitter /** * Global variable `Cypress` holds common utilities and constants. @@ -19,4 +19,4 @@ Cypress.version // => "1.4.0" Cypress._ // => Lodash _ ``` */ -declare const Cypress: Cypress.Cypress & CyEventEmitter +declare var Cypress: Cypress.Cypress & CyEventEmitter diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index 269889d6ffd..2181f864000 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -7,13 +7,6 @@ // but for now describe it as an ambient module declare namespace CypressCommandLine { - type HookName = 'before' | 'beforeEach' | 'afterEach' | 'after' - - interface TestError { - name: string - message: string - stack: string - } /** * All options that one can pass to "cypress.run" * @see https://on.cypress.io/module-api#cypress-run @@ -60,7 +53,7 @@ declare namespace CypressCommandLine { */ headless: boolean /** - * Specify your secret record key + * Specify your secret Record Key */ key: string /** @@ -95,6 +88,14 @@ declare namespace CypressCommandLine { * Specify the specs to run */ spec: string + /** + * Specify the number of failures to cancel a run being recorded to the Cloud or false to disable auto-cancellation. + */ + autoCancelAfterFailures: number | false + /** + * Whether to display the Cypress Runner UI + */ + runnerUi: boolean } /** @@ -170,10 +171,10 @@ declare namespace CypressCommandLine { * Cypress single test result */ interface TestResult { + duration: number title: string[] state: string - body: string - /** + /** * Error string as it's presented in console if the test fails */ displayError: string | null @@ -182,20 +183,6 @@ declare namespace CypressCommandLine { interface AttemptResult { state: string - error: TestError | null - startedAt: dateTimeISO - duration: ms - videoTimestamp: ms - screenshots: ScreenshotInformation[] - } - - /** - * Information about a single "before", "beforeEach", "afterEach" and "after" hook. - */ - interface HookInformation { - hookName: HookName - title: string[] - body: string } /** @@ -212,55 +199,69 @@ declare namespace CypressCommandLine { width: pixels } + interface SpecResult { + /** + * resolved filename of the spec + */ + absolute: string + /** + * file extension like ".js" + */ + fileExtension: string + /** + * file name without extension like "spec" + */ + fileName: string + /** + * filename like "spec.js" + */ + name: string + /** + * name relative to the project root, like "cypress/integration/spec.js" + */ + relative: string + } + /** * Cypress test run result for a single spec. */ interface RunResult { + error: string | null + /** + * Reporter name like "spec" + */ + reporter: string + /** + * This is controlled by the reporter, and Cypress cannot guarantee + * the properties. Usually this object has suites, tests, passes, etc + */ + reporterStats: object + screenshots: ScreenshotInformation[] /** * Accurate test results collected by Cypress. */ stats: { - suites: number - tests: number + duration?: ms + endedAt: dateTimeISO + failures: number passes: number pending: number skipped: number - failures: number startedAt: dateTimeISO - endedAt: dateTimeISO - duration: ms + suites: number + tests: number } /** - * Reporter name like "spec" - */ - reporter: string - /** - * This is controlled by the reporter, and Cypress cannot guarantee - * the properties. Usually this object has suites, tests, passes, etc + * information about the spec test file. */ - reporterStats: object - hooks: HookInformation[] + spec: SpecResult tests: TestResult[] - error: string | null video: string | null - /** - * information about the spec test file. - */ - spec: { - /** - * filename like "spec.js" - */ - name: string - /** - * name relative to the project root, like "cypress/integration/spec.js" - */ - relative: string - /** - * resolved filename of the spec - */ - absolute: string - } - shouldUploadVideo: boolean + } + + type PublicConfig = Omit & { + browsers: Cypress.PublicBrowser[] + cypressInternalEnv: string } /** @@ -268,29 +269,28 @@ declare namespace CypressCommandLine { * @see https://on.cypress.io/module-api */ interface CypressRunResult { - status: 'finished' - startedTestsAt: dateTimeISO + browserName: string + browserPath: string + browserVersion: string + config: PublicConfig + cypressVersion: string endedTestsAt: dateTimeISO - totalDuration: ms - totalSuites: number - totalTests: number + osName: string + osVersion: string + runs: RunResult[] + /** + * If Cypress test run was recorded, full url will be provided. + * @see https://on.cypress.io/cloud-introduction + */ + runUrl?: string + startedTestsAt: dateTimeISO + totalDuration: number totalFailed: number totalPassed: number totalPending: number totalSkipped: number - /** - * If Cypress test run is being recorded, full url will be provided. - * @see https://on.cypress.io/dashboard-introduction - */ - runUrl?: string - runs: RunResult[] - browserPath: string - browserName: string - browserVersion: string - osName: string - osVersion: string - cypressVersion: string - config: Cypress.ResolvedConfigOptions + totalSuites: number + totalTests: number } /** @@ -382,7 +382,7 @@ declare module 'cypress' { * recommend wrapping your config object with `defineConfig()` * @example * module.exports = defineConfig({ - * viewportWith: 400 + * viewportWidth: 400 * }) * * @see ../types/cypress-npm-api.d.ts @@ -390,6 +390,21 @@ declare module 'cypress' { * @returns {Cypress.ConfigOptions} the configuration passed in parameter */ defineConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions + + /** + * Provides automatic code completion for Component Frameworks Definitions. + * While it's not strictly necessary for Cypress to parse your configuration, we + * recommend wrapping your Component Framework Definition object with `defineComponentFramework()` + * @example + * module.exports = defineComponentFramework({ + * type: 'cypress-ct-solid-js' + * }) + * + * @see ../types/cypress-npm-api.d.ts + * @param {Cypress.ThirdPartyComponentFrameworkDefinition} config + * @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter + */ + defineComponentFramework(config: Cypress.ThirdPartyComponentFrameworkDefinition): Cypress.ThirdPartyComponentFrameworkDefinition } // export Cypress NPM module interface diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 8906b353073..cbf7d881962 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /// /// /// @@ -6,7 +7,7 @@ declare namespace Cypress { type FileContents = string | any[] | object type HistoryDirection = 'back' | 'forward' type HttpMethod = string - type RequestBody = string | object + type RequestBody = string | object | boolean | null type ViewportOrientation = 'portrait' | 'landscape' type PrevSubject = keyof PrevSubjectMap type TestingType = 'e2e' | 'component' @@ -49,6 +50,12 @@ declare namespace Cypress { interface CommandFnWithOriginalFnAndSubject { (this: Mocha.Context, originalFn: CommandOriginalFnWithSubject, prevSubject: S, ...args: Parameters): ReturnType | void } + interface QueryFn { + (this: Command, ...args: Parameters): (subject: any) => any + } + interface QueryFnWithOriginalFn { + (this: Command, originalFn: QueryFn, ...args: Parameters): (subject: any) => any + } interface ObjectLike { [key: string]: any } @@ -127,6 +134,71 @@ declare namespace Cypress { unsupportedVersion?: boolean } + /** + * Browser that's exposed in public APIs + */ + interface PublicBrowser { + channel: BrowserChannel + displayName: string + family: string + majorVersion?: string | number | null + name: BrowserName + path: string + version: string + } + + interface Ensure { + /** + * Throws an error if `subject` is not one of the passed in `type`s. + */ + isType(subject: any, type: PrevSubject[], commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a DOM element. + */ + isElement(subject: any, commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a `document`. + */ + isDocument(subject: any, commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a `window`. + */ + isWindow(subject: any, commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a DOM element attached to the application under test. + */ + isAttached(subject: any, commandName: string, cy: Chainable, onFail?: Log): void + + /** + * Throws an error if `subject` is a disabled DOM element. + */ + isNotDisabled(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is a DOM element hidden by any of its parent elements. + */ + isNotHiddenByAncestors(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is a read-only form element. + */ + isNotReadonly(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is a read-only form element. + */ + isScrollable(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is not a DOM element visible in the AUT. + */ + isVisible(subject: any, commandName: string, onFail?: Log): void + } + interface LocalStorage { /** * Called internally to clear `localStorage` in two situations. @@ -141,6 +213,39 @@ declare namespace Cypress { clear: (keys?: string[]) => void } + // TODO: raise minimum required TypeScript version to 3.7 + // and make this recursive + // https://github.com/cypress-io/cypress/issues/24875 + type Storable = + | string + | number + | boolean + | null + | StorableObject + | StorableArray + + interface StorableObject { + [key: string]: Storable + } + + interface StorableArray extends Array { } + + type StorableRecord = Record + + interface OriginStorage { + origin: string + value: StorableRecord + } + + interface Storages { + localStorage: OriginStorage[] + sessionStorage: OriginStorage[] + } + + interface StorageByOrigin { + [key: string]: StorableRecord + } + type IsBrowserMatcher = BrowserName | Partial | Array> interface ViewportPosition extends WindowPosition { @@ -186,10 +291,14 @@ declare namespace Cypress { */ interface Spec { name: string // "config_passing_spec.js" - relative: string // "cypress/integration/config_passing_spec.js" or "__all" if clicked all specs button - absolute: string // "/Users/janelane/app/cypress/integration/config_passing_spec.js" + relative: string // "cypress/e2e/config_passing_spec.cy.js" or "__all" if clicked all specs button + absolute: string // "/Users/janelane/app/cypress/e2e/config_passing_spec.cy.js" specFilter?: string // optional spec filter used by the user specType?: CypressSpecType + baseName?: string // "config_passing_spec.cy.js" + fileExtension?: string // ".js" + fileName?: string // "config_passing_spec.cy" + id?: string // "U3BlYzovVXNlcnMvamFuZWxhbmUvYXBwL2N5cHJlc3MvZTJlL2NvbmZpZ19wYXNzaW5nX3NwZWMuY3kuanM=" } /** @@ -271,6 +380,12 @@ declare namespace Cypress { */ sinon: sinon.SinonStatic + /** + * Utility functions for ensuring various properties about a subject. + * @see https://on.cypress.io/api/custom-queries + */ + ensure: Ensure + /** * Cypress version string. i.e. "1.1.2" * @see https://on.cypress.io/version @@ -325,6 +440,11 @@ declare namespace Cypress { titlePath: string[] } + /** + * Information about current test retry + */ + currentRetry: number + /** * Information about the browser currently running the tests */ @@ -395,7 +515,7 @@ declare namespace Cypress { }) ``` */ - config(Object: ConfigOptions): void + config(Object: TestConfigOverrides): void // no real way to type without generics /** @@ -462,30 +582,98 @@ declare namespace Cypress { */ log(options: Partial): Log - /** - * @see https://on.cypress.io/api/commands - */ Commands: { + /** + * Add a custom command + * @see https://on.cypress.io/api/commands + */ add(name: T, fn: CommandFn): void - add(name: T, options: CommandOptions & {prevSubject: false}, fn: CommandFn): void - add(name: T, options: CommandOptions & {prevSubject: true}, fn: CommandFnWithSubject): void + + /** + * Add a custom parent command + * @see https://on.cypress.io/api/commands#Parent-Commands + */ + add(name: T, options: CommandOptions & { prevSubject: false }, fn: CommandFn): void + + /** + * Add a custom child command + * @see https://on.cypress.io/api/commands#Child-Commands + */ + add(name: T, options: CommandOptions & { prevSubject: true }, fn: CommandFnWithSubject): void + + /** + * Add a custom child or dual command + * @see https://on.cypress.io/api/commands#Validations + */ add( - name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject, + name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject, ): void + + /** + * Add a custom command that allows multiple types as the prevSubject + * @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types + */ add( - name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject[S]>, + name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject[S]>, ): void + + /** + * Add one or more custom commands + * @see https://on.cypress.io/api/commands + */ addAll(fns: CommandFns): void - addAll(options: CommandOptions & {prevSubject: false}, fns: CommandFns): void + + /** + * Add one or more custom parent commands + * @see https://on.cypress.io/api/commands#Parent-Commands + */ + addAll(options: CommandOptions & { prevSubject: false }, fns: CommandFns): void + + /** + * Add one or more custom child commands + * @see https://on.cypress.io/api/commands#Child-Commands + */ addAll(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject): void + + /** + * Add one or more custom commands that validate their prevSubject + * @see https://on.cypress.io/api/commands#Validations + */ addAll( - options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject, + options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject, ): void + + /** + * Add one or more custom commands that allow multiple types as their prevSubject + * @see https://on.cypress.io/api/commands#Allow-Multiple-Types + */ addAll( - options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject[S]>, + options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject[S]>, ): void + + /** + * Overwrite an existing Cypress command with a new implementation + * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands + */ overwrite(name: T, fn: CommandFnWithOriginalFn): void + + /** + * Overwrite an existing Cypress command with a new implementation + * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands + */ overwrite(name: T, fn: CommandFnWithOriginalFnAndSubject): void + + /** + * Add a custom query + * @see https://on.cypress.io/api/custom-queries + */ + addQuery(name: T, fn: QueryFn): void + + /** + * Overwrite an existing Cypress query with a new implementation + * @see https://on.cypress.io/api/custom-queries + */ + overwriteQuery(name: T, fn: QueryFnWithOriginalFn): void } /** @@ -493,16 +681,6 @@ declare namespace Cypress { */ Cookies: { debug(enabled: boolean, options?: Partial): void - /** - * @deprecated Use `cy.session()` instead. - * @see https://on.cypress.io/session - */ - preserveOnce(...names: string[]): void - /** - * @deprecated Use `cy.session()` instead. - * @see https://on.cypress.io/session - */ - defaults(options: Partial): CookieDefaults } /** @@ -580,7 +758,7 @@ declare namespace Cypress { isInputType(element: JQuery | HTMLElement, type: string | string[]): boolean stringify(element: JQuery | HTMLElement, form: string): string getElements(element: JQuery): JQuery | HTMLElement[] - getContainsSelector(text: string, filter?: string): JQuery.Selector + getContainsSelector(text: string, filter?: string, options?: CaseMatchable): JQuery.Selector getFirstDeepestElement(elements: HTMLElement[], index?: number): HTMLElement getWindowByElement(element: JQuery | HTMLElement): JQuery | HTMLElement getReasonIsHidden(element: JQuery | HTMLElement, options?: object): string @@ -601,13 +779,6 @@ declare namespace Cypress { defaults(options: Partial): void } - /** - * @see https://on.cypress.io/api/api-server - */ - Server: { - defaults(options: Partial): void - } - /** * @see https://on.cypress.io/screenshot-api */ @@ -641,23 +812,25 @@ declare namespace Cypress { */ off: Actions + /** + * Used to include dependencies within the cy.origin() callback + * @see https://on.cypress.io/origin + */ + require: (id: string) => T + /** * Trigger action * @private */ - action: (action: string, ...args: any[]) => any[] | void + action: (action: string, ...args: any[]) => T /** - * Load files + * Load files * @private */ onSpecWindow: (window: Window, specList: string[] | Array<() => Promise>) => void } - interface SessionOptions { - validate?: () => false | void - } - type CanReturnChainable = void | Chainable | Promise type ThenReturn = R extends void ? Chainable : @@ -686,29 +859,13 @@ declare namespace Cypress { * @see https://on.cypress.io/variables-and-aliases * @see https://on.cypress.io/get * @example - ``` - // Get the aliased 'todos' elements - cy.get('ul#todos').as('todos') - //...hack hack hack... - // later retrieve the todos - cy.get('@todos') - ``` - */ - as(alias: string): Chainable - - /** - * Select a file with the given element, or drag and drop a file over any DOM subject. + * // Get the aliased 'todos' elements + * cy.get('ul#todos').as('todos') * - * @param {FileReference} files - The file(s) to select or drag onto this element. - * @see https://on.cypress.io/selectfile - * @example - * cy.get('input[type=file]').selectFile(Cypress.Buffer.from('text')) - * cy.get('input[type=file]').selectFile({ - * fileName: 'users.json', - * fileContents: [{name: 'John Doe'}] - * }) + * // later retrieve the todos + * cy.get('@todos') */ - selectFile(files: FileReference | FileReference[], options?: Partial): Chainable + as(alias: string, options?: Partial): Chainable /** * Blur a focused element. This element must currently be in focus. @@ -753,33 +910,88 @@ declare namespace Cypress { /** * Clear the value of an `input` or `textarea`. - * An alias for `.type({selectall}{backspace})` + * An alias for `.type({selectall}{del})` * * @see https://on.cypress.io/clear */ clear(options?: Partial): Chainable /** - * Clear a specific browser cookie. - * Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear a specific cookie inside a single test. + * Clear a specific browser cookie for a domain. + * + * Cypress automatically clears all cookies _before_ each test to prevent + * state from being shared across tests when test isolation is enabled. + * You shouldn't need to use this command unless you're using it to clear + * a specific cookie inside a single test or test isolation is disabled. * * @see https://on.cypress.io/clearcookie */ - clearCookie(name: string, options?: Partial): Chainable + clearCookie(name: string, options?: CookieOptions): Chainable /** - * Clear all browser cookies. - * Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear a specific cookie inside a single test. + * Clear browser cookies for a domain. + * + * Cypress automatically clears all cookies _before_ each test to prevent + * state from being shared across tests when test isolation is enabled. + * You shouldn't need to use this command unless you're using it to clear + * specific cookies inside a single test or test isolation is disabled. * * @see https://on.cypress.io/clearcookies */ - clearCookies(options?: Partial): Chainable + clearCookies(options?: CookieOptions): Chainable /** - * Clear data in local storage. - * Cypress automatically runs this command before each test to prevent state from being - * shared across tests. You shouldn't need to use this command unless you're using it - * to clear localStorage inside a single test. Yields `localStorage` object. + * Clear all browser cookies. + * + * Cypress automatically clears all cookies _before_ each test to prevent + * state from being shared across tests when test isolation is enabled. + * You shouldn't need to use this command unless you're using it to clear + * all cookie inside a single test or test isolation is disabled. + * + * @see https://on.cypress.io/clearallcookies + */ + clearAllCookies(options?: Partial): Chainable + + /** + * Get local storage for all origins. + * + * @see https://on.cypress.io/getalllocalstorage + */ + getAllLocalStorage(options?: Partial): Chainable + + /** + * Clear local storage for all origins. + * + * Cypress automatically clears all local storage _before_ each test to + * prevent state from being shared across tests when test isolation + * is enabled. You shouldn't need to use this command unless you're using it + * to clear localStorage inside a single test or test isolation is disabled. + * + * @see https://on.cypress.io/clearalllocalstorage + */ + clearAllLocalStorage(options?: Partial): Chainable + + /** + * Get session storage for all origins. + * + * @see https://on.cypress.io/getallsessionstorage + */ + getAllSessionStorage(options?: Partial): Chainable + + /** + * Clear session storage for all origins. + * + * @see https://on.cypress.io/clearallsessionstorage + */ + clearAllSessionStorage(options?: Partial): Chainable + + /** + * Clear data in local storage for the current origin. + * + * Cypress automatically clears all local storage _before_ each test to + * prevent state from being shared across tests when test isolation + * is enabled. You shouldn't need to use this command unless you're using it + * to clear localStorage inside a single test or test isolation is disabled. * * @see https://on.cypress.io/clearlocalstorage * @param {string} [key] - name of a particular item to remove (optional). @@ -1070,11 +1282,9 @@ declare namespace Cypress { /** * Save/Restore browser Cookies, LocalStorage, and SessionStorage data resulting from the supplied `setup` function. * - * Only available if the `experimentalSessionAndOrigin` config option is enabled. - * * @see https://on.cypress.io/session */ - session(id: string | object, setup?: SessionOptions['validate'], options?: SessionOptions): Chainable + session(id: string | object, setup: () => void, options?: SessionOptions): Chainable /** * Get the window.document of the page that is currently active. @@ -1237,14 +1447,21 @@ declare namespace Cypress { * * @see https://on.cypress.io/getcookie */ - getCookie(name: string, options?: Partial): Chainable + getCookie(name: string, options?: CookieOptions): Chainable /** - * Get all of the browser cookies. + * Get browser cookies for a domain. * * @see https://on.cypress.io/getcookies */ - getCookies(options?: Partial): Chainable + getCookies(options?: CookieOptions): Chainable + + /** + * Get all browser cookies. + * + * @see https://on.cypress.io/getallcookies + */ + getAllCookies(options?: Partial): Chainable /** * Navigate back or forward to the previous or next URL in the browser's history. @@ -1301,7 +1518,7 @@ declare namespace Cypress { * // Drill into nested properties by using dot notation * cy.wrap({foo: {bar: {baz: 1}}}).its('foo.bar.baz') */ - its(propertyName: K, options?: Partial): Chainable + its(propertyName: K, options?: Partial): Chainable> its(propertyPath: string, options?: Partial): Chainable /** @@ -1393,19 +1610,19 @@ declare namespace Cypress { * * @see https://on.cypress.io/nextuntil */ - nextUntil(selector: K, options?: Partial): Chainable> + nextUntil(selector: K, filter?: string, options?: Partial): Chainable> /** * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * * @see https://on.cypress.io/nextuntil */ - nextUntil(options?: Partial): Chainable> + nextUntil(selector: string, filter?: string, options?: Partial): Chainable> /** * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * * @see https://on.cypress.io/nextuntil */ - nextUntil(selector: string, options?: Partial): Chainable> + nextUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> /** * Filter DOM element(s) from a set of DOM elements. Opposite of `.filter()` @@ -1414,6 +1631,13 @@ declare namespace Cypress { */ not(selector: string, options?: Partial): Chainable + /** + * Invoke a command synchronously, without using the command queue. + * + * @see https://on.cypress.io/api/custom-queries + */ + now(name: string, ...args: any[]): Promise | ((subject: any) => any) + /** * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. * @see https://on.cypress.io/catalog-of-events#App-Events @@ -1571,21 +1795,21 @@ declare namespace Cypress { * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. * - * @see https://on.cypress.io/prevall + * @see https://on.cypress.io/prevuntil */ prevUntil(selector: K, filter?: string, options?: Partial): Chainable> /** * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. * - * @see https://on.cypress.io/prevall + * @see https://on.cypress.io/prevuntil */ prevUntil(selector: string, filter?: string, options?: Partial): Chainable> /** * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. * - * @see https://on.cypress.io/prevall + * @see https://on.cypress.io/prevuntil */ prevUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> @@ -1609,9 +1833,21 @@ declare namespace Cypress { * * @see https://on.cypress.io/reload * @example + * cy.visit('http://localhost:3000/admin') * cy.reload() */ - reload(options?: Partial): Chainable + reload(): Chainable + /** + * Reload the page. + * + * @see https://on.cypress.io/reload + * @param {Partial} options Pass in an options object to modify the default behavior of cy.reload() + * @example + * // Reload the page, do not log it in the command log and timeout after 15s + * cy.visit('http://localhost:3000/admin') + * cy.reload({log: false, timeout: 15000}) + */ + reload(options: Partial): Chainable /** * Reload the page without cache * @@ -1623,6 +1859,18 @@ declare namespace Cypress { * cy.reload(true) */ reload(forceReload: boolean): Chainable + /** + * Reload the page without cache and with log and timeout options + * + * @see https://on.cypress.io/reload + * @param {Boolean} forceReload Whether to reload the current page without using the cache. true forces the reload without cache. + * @param {Partial} options Pass in an options object to modify the default behavior of cy.reload() + * @example + * // Reload the page without using the cache, do not log it in the command log and timeout after 15s + * cy.visit('http://localhost:3000/admin') + * cy.reload(true, {log: false, timeout: 15000}) + */ + reload(forceReload: boolean, options: Partial): Chainable /** * Make an HTTP GET request. @@ -1662,66 +1910,6 @@ declare namespace Cypress { */ root(options?: Partial): Chainable> // can't do better typing unless we ignore the `.within()` case - /** - * @deprecated Use `cy.intercept()` instead. - * - * Use `cy.route()` to manage the behavior of network requests. - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route('https://localhost:7777/users', [{id: 1, name: 'Pat'}]) - */ - route(url: string | RegExp, response?: string | object): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Spy or stub request with specific method and url. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * // spy on POST /todos requests - * cy.route('POST', '/todos').as('add-todo') - */ - route(method: string, url: string | RegExp, response?: string | object): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Set a route by returning an object literal from a callback function. - * Functions that return a Promise will automatically be awaited. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route(() => { - * // your logic here - * // return an appropriate routing object here - * return { - * method: 'POST', - * url: '/comments', - * response: this.commentsFixture - * } - * }) - */ - route(fn: () => RouteOptions): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Spy or stub a given route. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route({ - * method: 'DELETE', - * url: '/users', - * status: 412, - * delay: 1000 - * // and other options, see documentation - * }) - */ - route(options: Partial): Chainable - /** * Take a screenshot of the application under test and the Cypress Command Log. * @@ -1730,15 +1918,17 @@ declare namespace Cypress { * cy.screenshot() * cy.get(".post").screenshot() */ - screenshot(options?: Partial): Chainable + screenshot(options?: Partial): Chainable + /** * Take a screenshot of the application under test and the Cypress Command Log and save under given filename. * * @see https://on.cypress.io/screenshot * @example + * cy.screenshot("post-element") * cy.get(".post").screenshot("post-element") */ - screenshot(fileName: string, options?: Partial): Chainable + screenshot(fileName: string, options?: Partial): Chainable /** * Scroll an element into view. @@ -1768,24 +1958,18 @@ declare namespace Cypress { select(valueOrTextOrIndex: string | number | Array, options?: Partial): Chainable /** - * @deprecated Use `cy.intercept()` instead. - * - * Start a server to begin routing responses to `cy.route()` and `cy.request()`. + * Select a file with the given element, or drag and drop a file over any DOM subject. * + * @param {FileReference} files - The file(s) to select or drag onto this element. + * @see https://on.cypress.io/selectfile * @example - * // start server - * cy.server() - * // get default server options - * cy.server().should((server) => { - * expect(server.delay).to.eq(0) - * expect(server.method).to.eq('GET') - * expect(server.status).to.eq(200) - * // and many others options + * cy.get('input[type=file]').selectFile(Cypress.Buffer.from('text')) + * cy.get('input[type=file]').selectFile({ + * fileName: 'users.json', + * contents: [{name: 'John Doe'}] * }) - * - * @see https://on.cypress.io/server */ - server(options?: Partial): Chainable + selectFile(files: FileReference | FileReference[], options?: Partial): Chainable /** * Set a browser cookie. @@ -2358,8 +2542,8 @@ declare namespace Cypress { type ChainableMethods = { [P in keyof Chainable]: Chainable[P] extends ((...args: any[]) => any) - ? Chainable[P] - : never + ? Chainable[P] + : never } interface SinonSpyAgent
{ @@ -2388,10 +2572,6 @@ declare namespace Cypress { type Agent = SinonSpyAgent & T - interface CookieDefaults { - preserve: string | string[] | RegExp | ((cookie: Cookie) => boolean) - } - interface Failable { /** * Whether to fail on response codes other than 2xx and 3xx @@ -2461,7 +2641,7 @@ declare namespace Cypress { * Time to wait (ms) * * @default defaultCommandTimeout - * @see https://docs.cypress.io/guides/references/configuration.html#Timeouts + * @see https://on.cypress.io/configuration#Timeouts */ timeout: number } @@ -2486,21 +2666,21 @@ declare namespace Cypress { * Time to wait for the request (ms) * * @default {@link Timeoutable#timeout} - * @see https://docs.cypress.io/guides/references/configuration.html#Timeouts + * @see https://on.cypress.io/configuration#Timeouts */ requestTimeout: number /** * Time to wait for the response (ms) * * @default {@link Timeoutable#timeout} - * @see https://docs.cypress.io/guides/references/configuration.html#Timeouts + * @see https://on.cypress.io/configuration#Timeouts */ responseTimeout: number } /** * Options to force an event, skipping Actionability check - * @see https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Actionability + * @see https://on.cypress.io/interacting-with-elements#Actionability */ interface Forceable { /** @@ -2511,11 +2691,13 @@ declare namespace Cypress { force: boolean } + type experimentalCspAllowedDirectives = 'default-src' | 'child-src' | 'frame-src' | 'script-src' | 'script-src-elem' | 'form-action' + type scrollBehaviorOptions = false | 'center' | 'top' | 'bottom' | 'nearest' /** * Options to affect Actionability checks - * @see https://docs.cypress.io/guides/core-concepts/interacting-with-elements.html#Actionability + * @see https://on.cypress.io/interacting-with-elements#Actionability */ interface ActionableOptions extends Forceable { /** @@ -2526,6 +2708,7 @@ declare namespace Cypress { waitForAnimations: boolean /** * The distance in pixels an element must exceed over time to be considered animating + * * @default 5 */ animationDistanceThreshold: number @@ -2537,15 +2720,20 @@ declare namespace Cypress { scrollBehavior: scrollBehaviorOptions } - interface SelectFileOptions extends Loggable, Timeoutable, ActionableOptions { + /** + * Options to affect how an alias is stored + * + * @see https://on.cypress.io/as + */ + interface AsOptions { /** - * Which user action to perform. `select` matches selecting a file while - * `drag-drop` matches dragging files from the operating system into the - * document. + * The type of alias to store, which impacts how the value is retrieved later in the test. + * If an alias should be a 'query' (re-runs all queries leading up to the resulting value so it's alway up-to-date) or a + * 'static' (read once when the alias is saved and is never updated). `type` has no effect when aliasing intercepts, spies, and stubs. * - * @default 'select' + * @default 'query' */ - action: 'select' | 'drag-drop' + type: 'query' | 'static' } interface BlurOptions extends Loggable, Timeoutable, Forceable { } @@ -2618,6 +2806,14 @@ declare namespace Cypress { cmdKey: boolean } + interface CookieOptions extends Partial { + /** + * Domain to set cookies on or get cookies from + * @default hostname of the current app under test + */ + domain?: string + } + interface PEMCert { /** * Path to the certificate file, relative to project root. @@ -2659,6 +2855,30 @@ declare namespace Cypress { certs: PEMCert[] | PFXCert[] } + type RetryStrategyWithModeSpecs = RetryStrategy & { + openMode: boolean; // defaults to false + runMode: boolean; // defaults to true + } + + type RetryStrategy = + | RetryStrategyDetectFlakeAndPassOnThresholdType + | RetryStrategyDetectFlakeButAlwaysFailType + + interface RetryStrategyDetectFlakeAndPassOnThresholdType { + experimentalStrategy: "detect-flake-and-pass-on-threshold" + experimentalOptions?: { + maxRetries: number; // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0 + passesRequired: number; // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0 and <= maxRetries + } + } + + interface RetryStrategyDetectFlakeButAlwaysFailType { + experimentalStrategy: "detect-flake-but-always-fail" + experimentalOptions?: { + maxRetries: number; // defaults to 2 if experimentalOptions is not provided, must be a whole number > 0 + stopIfAnyPassed: boolean; // defaults to false if experimentalOptions is not provided + } + } interface ResolvedConfigOptions { /** * Url used as prefix for [cy.visit()](https://on.cypress.io/visit) or [cy.request()](https://on.cypress.io/request) command's url @@ -2666,12 +2886,12 @@ declare namespace Cypress { */ baseUrl: string | null /** - * Any values to be set as [environment variables](https://docs.cypress.io/guides/guides/environment-variables.html) + * Any values to be set as [environment variables](https://on.cypress.io/environment-variables) * @default {} */ env: { [key: string]: any } /** - * A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using http://globtester.com to test what files would match. + * A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using a tool to test what files would match. * @default "*.hot-update.js" */ excludeSpecPattern: string | string[] @@ -2686,7 +2906,7 @@ declare namespace Cypress { */ port: number | null /** - * The [reporter](https://docs.cypress.io/guides/guides/reporters.html) used when running headlessly or in CI + * The [reporter](https://on.cypress.io/reporters) used when running headlessly or in CI * @default "spec" */ reporter: string @@ -2757,18 +2977,13 @@ declare namespace Cypress { * @default "cypress/downloads" */ downloadsFolder: string - /** - * If set to `system`, Cypress will try to find a `node` executable on your path to use when executing your plugins. Otherwise, Cypress will use the Node version bundled with Cypress. - * @default "bundled" - */ - nodeVersion: 'system' | 'bundled' /** * The application under test cannot redirect more than this limit. * @default 20 */ redirectionLimit: number /** - * If `nodeVersion === 'system'` and a `node` executable is found, this will be the full filesystem path to that executable. + * If a `node` executable is found, this will be the full filesystem path to that executable. * @default null */ resolvedNodePath: string @@ -2793,12 +3008,31 @@ declare namespace Cypress { */ supportFile: string | false /** - * The test isolation level applied to ensure a clean slate between tests. - * - legacy - resets/clears aliases, intercepts, clock, viewport, cookies, and local storage before each test. - * - strict - applies all resets/clears from legacy, plus clears the page by visiting 'about:blank' to ensure clean app state before each test. - * @default "legacy", however, when experimentalSessionAndOrigin=true, the default is "strict" + * The test isolation ensures a clean browser context between tests. + * + * Cypress will always reset/clear aliases, intercepts, clock, and viewport before each test + * to ensure a clean test slate; i.e. this configuration only impacts the browser context. + * + * Note: the [`cy.session()`](https://on.cypress.io/session) command will inherent this value to determine whether + * or not the page is cleared when the command executes. This command is only available in end-to-end testing. + * + * - true - The page is cleared before each test. Cookies, local storage and session storage in all domains are cleared + * before each test. The `cy.session()` command will also clear the page and current browser context when creating + * or restoring the browser session. + * - false - The current browser state will persist between tests. The page does not clear before the test and cookies, local + * storage and session storage will be available in the next test. The `cy.session()` command will only clear the + * current browser context when creating or restoring the browser session - the current page will not clear. + * + * Tradeoffs: + * Turning test isolation off may improve performance of end-to-end tests, however, previous tests could impact the + * browser state of the next test and cause inconsistency when using .only(). Be mindful to write isolated tests when + * test isolation is false. If a test in the suite impacts the state of other tests and it were to fail, you could see + * misleading errors in later tests which makes debugging clunky. See the [documentation](https://on.cypress.io/test-isolation) + * for more information. + * + * @default true */ - testIsolation: 'legacy' | 'strict' + testIsolation: boolean /** * Path to folder where videos will be saved after a headless or CI run * @default "cypress/videos" @@ -2810,22 +3044,20 @@ declare namespace Cypress { */ trashAssetsBeforeRuns: boolean /** - * The quality setting for the video compression, in Constant Rate Factor (CRF). The value can be false to disable compression or a value between 0 and 51, where a lower value results in better quality (at the expense of a higher file size). + * The quality setting for the video compression, in Constant Rate Factor (CRF). + * Enable compression by passing true to use the default CRF of 32. + * Compress at custom CRF by passing a number between 1 and 51, where a lower value results in better quality (at the expense of a higher file size). + * Disable compression by passing false or 0. * @default 32 */ - videoCompression: number | false + videoCompression: number | boolean /** - * Whether Cypress will record a video of the test run when running headlessly. - * @default true + * Whether Cypress will record a video of the test run when executing in run mode. + * @default false */ video: boolean /** - * Whether Cypress will upload the video to the Dashboard even if all tests are passing. This applies only when recording your runs to the Dashboard. Turn this off if you'd like the video uploaded only when there are failing tests. - * @default true - */ - videoUploadOnPasses: boolean - /** - * Whether Chrome Web Security for same-origin policy and insecure mixed content is enabled. Read more about this here + * Whether Chrome Web Security for same-origin policy and insecure mixed content is enabled. Read more about this [here](https://on.cypress.io/web-security#Disabling-Web-Security) * @default true */ chromeWebSecurity: boolean @@ -2855,24 +3087,43 @@ declare namespace Cypress { */ scrollBehavior: scrollBehaviorOptions /** - * Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode. + * Indicates whether Cypress should allow CSP header directives from the application under test. + * - When this option is set to `false`, Cypress will strip the entire CSP header. + * - When this option is set to `true`, Cypress will only to strip directives that would interfere + * with or inhibit Cypress functionality. + * - When this option to an array of allowable directives (`[ 'default-src', ... ]`), the directives + * specified will remain in the response headers. + * + * Please see the documentation for more information. + * @see https://on.cypress.io/experiments#Experimental-CSP-Allow-List * @default false */ - experimentalInteractiveRunEvents: boolean + experimentalCspAllowList: boolean | experimentalCspAllowedDirectives[], /** - * Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands. See https://on.cypress.io/origin and https://on.cypress.io/session. + * Allows listening to the `before:run`, `after:run`, `before:spec`, and `after:spec` events in the plugins file during interactive mode. * @default false */ - experimentalSessionAndOrigin: boolean + experimentalInteractiveRunEvents: boolean /** * Whether Cypress will search for and replace obstructive code in third party .js or .html files. * NOTE: Setting this flag to true removes Subresource Integrity (SRI). * Please see https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity. * This option has no impact on experimentalSourceRewriting and is only used with the * non-experimental source rewriter. - * @see https://on.cypress.io/configuration#experimentalModifyObstructiveThirdPartyCode + * @see https://on.cypress.io/experiments#Configuration */ experimentalModifyObstructiveThirdPartyCode: boolean + /** + * Disables setting document.domain to the applications super domain on injection. + * This experiment is to be used for sites that do not work with setting document.domain + * due to cross-origin issues. Enabling this option no longer allows for default subdomain + * navigations, and will require the use of cy.origin(). This option takes an array of + * strings/string globs. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/domain + * @see https://on.cypress.io/experiments#Experimental-Skip-Domain-Injection + * @default null + */ + experimentalSkipDomainInjection: string[] | null /** * Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm. * @default false @@ -2888,13 +3139,18 @@ declare namespace Cypress { * @default false */ experimentalWebKitSupport: boolean + /** + * Enables support for improved memory management within Chromium-based browsers. + * @default false + */ + experimentalMemoryManagement: boolean /** * Number of times to retry a failed test. * If a number is set, tests will retry in both runMode and openMode. * To enable test retries only in runMode, set e.g. `{ openMode: null, runMode: 2 }` * @default null */ - retries: Nullable, openMode?: Nullable }> + retries: Nullable, openMode?: Nullable }) | RetryStrategyWithModeSpecs> /** * Enables including elements within the shadow DOM when using querying * commands (e.g. cy.get(), cy.find()). Can be set globally in cypress.config.{js,ts,mjs,cjs}, @@ -2939,7 +3195,7 @@ declare namespace Cypress { * Override default config options for E2E Testing runner. * @default {} */ - e2e: Omit + e2e: EndToEndConfigOptions /** * An array of objects defining the certificates @@ -2954,6 +3210,19 @@ declare namespace Cypress { indexHtmlFile: string } + interface EndToEndConfigOptions extends Omit { + /** + * Enables the "Run All Specs" UI feature, allowing the execution of multiple specs sequentially. + * @default false + */ + experimentalRunAllSpecs?: boolean + /** + * Enables support for `Cypress.require()` for including dependencies within the `cy.origin()` callback. + * @default false + */ + experimentalOriginDependencies?: boolean + } + /** * Options appended to config object on runtime. */ @@ -2975,7 +3244,7 @@ declare namespace Cypress { /** * Hosts mappings to IP addresses. */ - hosts: null | {[key: string]: string} + hosts: null | { [key: string]: string } /** * Whether Cypress was launched via 'cypress open' (interactive mode) */ @@ -2993,6 +3262,7 @@ declare namespace Cypress { // Internal or Unlisted at server/lib/config_options namespace: string projectRoot: string + repoRoot: string | null devServerPublicPathRoute: string cypressBinaryRoot: string } @@ -3026,11 +3296,21 @@ declare namespace Cypress { socketIoRoute: string spec: Cypress['spec'] | null specs: Array - xhrRoute: string - xhrUrl: string + protocolEnabled: boolean + hideCommandLog: boolean + hideRunnerUi: boolean } - interface TestConfigOverrides extends Partial>, Partial> { + interface SuiteConfigOverrides extends Partial< + Pick + >, Partial> { + browser?: IsBrowserMatcher | IsBrowserMatcher[] + keystrokeDelay?: number + } + + interface TestConfigOverrides extends Partial< + Pick + >, Partial> { browser?: IsBrowserMatcher | IsBrowserMatcher[] keystrokeDelay?: number } @@ -3058,32 +3338,207 @@ declare namespace Cypress { type PickConfigOpt = T extends keyof DefineDevServerConfig ? DefineDevServerConfig[T] : any + interface DependencyToInstall { + dependency: CypressComponentDependency + satisfied: boolean + detectedVersion: string | null + } + + interface CypressComponentDependency { + /** + * Unique identifier. + * @example 'reactscripts' + */ + type: string + + /** + * Name to display in the user interface. + * @example "React Scripts" + */ + name: string + + /** + * Package name on npm. + * @example react-scripts + */ + package: string + + /** + * Code to run when installing. Version is optional. + * + * Should be @. + * + * @example `react` + * @example `react@18` + * @example `react-scripts` + */ + installer: string + + /** + * Description shown in UI. It is recommended to use the same one the package uses on npm. + * @example 'Create React apps with no build configuration' + */ + description: string + + /** + * Minimum version supported. Should conform to Semantic Versioning as used in `package.json`. + * @see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#dependencies + * @example '^=4.0.0 || ^=5.0.0' + * @example '^2.0.0' + */ + minVersion: string + } + + interface ResolvedComponentFrameworkDefinition { + /** + * A semantic, unique identifier. + * Must begin with `cypress-ct-` or `@org/cypress-ct-` for third party implementations. + * @example 'reactscripts' + * @example 'nextjs' + * @example 'cypress-ct-solid-js' + */ + type: string + + /** + * Used as the flag for `getPreset` for meta framworks, such as finding the webpack config for CRA, Angular, etc. + * It is also the name of the string added to `cypress.config` + * + * @example + * export default { + * component: { + * devServer: { + * framework: 'create-react-app' // can be 'next', 'create-react-app', etc etc. + * } + * } + * } + */ + configFramework: string + + /** + * Library (React, Vue) or template (aka "meta framework") (CRA, Next.js, Angular) + */ + category: 'library' | 'template' + + /** + * Name displayed in Launchpad when doing initial setup. + * @example 'Solid.js' + * @example 'Create React App' + */ + name: string + + /** + * Supported bundlers. + */ + supportedBundlers: Array<'webpack' | 'vite'> + + /** + * Used to attempt to automatically select the correct framework/bundler from the dropdown. + * + * @example + * const SOLID_DETECTOR: Dependency = { + * type: 'solid', + * name: 'Solid.js', + * package: 'solid-js', + * installer: 'solid-js', + * description: 'Solid is a declarative JavaScript library for creating user interfaces', + * minVersion: '^1.0.0', + * } + */ + detectors: CypressComponentDependency[] + + /** + * Array of required dependencies. This could be the bundler and JavaScript library. + */ + dependencies: (bundler: 'webpack' | 'vite', projectPath: string) => Promise + + /** + * This is used interally by Cypress for the "Create From Component" feature. + */ + codeGenFramework?: 'react' | 'vue' | 'svelte' | 'angular' + + /** + * This is used interally by Cypress for the "Create From Component" feature. + * @example '*.{js,jsx,tsx}' + */ + glob?: string + + /** + * This is the path to get mount, eg `import { mount } from , + * @example: `cypress-ct-solidjs/src/mount` + */ + mountModule: (projectPath: string) => Promise + + /** + * Support status. Internally alpha | beta | full. + * Community integrations are "community". + */ + supportStatus: 'alpha' | 'beta' | 'full' | 'community' + + /** + * Function returning string for used for the component-index.html file. + * Cypress provides a default if one isn't specified for third party integrations. + */ + componentIndexHtml?: () => string + + /** + * Used for the Create From Component feature. + * This is currently not supported for third party frameworks. + */ + specPattern?: '**/*.cy.ts' + } + + type ComponentFrameworkDefinition = Omit & { + dependencies: (bundler: 'webpack' | 'vite') => CypressComponentDependency[] + } + + /** + * Certain properties are not supported for third party frameworks right now, + * such as ones related to the "Create From" feature. This is a subset of + * properties that are exposed for public usage. + */ + + type ThirdPartyComponentFrameworkDefinition = Pick & { + /** + * @example `cypress-ct-${string} for third parties. Any string is valid internally. + */ + type: string + + /** + * Raw SVG icon that will be displayed in the Project Setup Wizard. Used for third parties that + * want to render a custom icon. + */ + icon?: string + } + interface AngularDevServerProjectConfig { - root: string, - sourceRoot: string, + root: string + sourceRoot: string buildOptions: Record } type DevServerFn = (cypressDevServerConfig: DevServerConfig, devServerConfig: ComponentDevServerOpts) => ResolvedDevServerConfig | Promise + type ConfigHandler = T + | (() => T | Promise) + type DevServerConfigOptions = { bundler: 'webpack' framework: 'react' | 'vue' | 'vue-cli' | 'nuxt' | 'create-react-app' | 'next' | 'svelte' - webpackConfig?: PickConfigOpt<'webpackConfig'> + webpackConfig?: ConfigHandler> } | { bundler: 'vite' framework: 'react' | 'vue' | 'svelte' - viteConfig?: Omit, undefined>, 'base' | 'root'> + viteConfig?: ConfigHandler, undefined>, 'base' | 'root'>> } | { - bundler: 'webpack', - framework: 'angular', - webpackConfig?: PickConfigOpt<'webpackConfig'>, + bundler: 'webpack' + framework: 'angular' + webpackConfig?: ConfigHandler> options?: { projectConfig: AngularDevServerProjectConfig } } - interface ComponentConfigOptions extends Omit { + interface ComponentConfigOptions extends Omit { devServer: DevServerFn | DevServerConfigOptions devServerConfig?: ComponentDevServerOpts /** @@ -3106,7 +3561,7 @@ declare namespace Cypress { /** * Hosts mappings to IP addresses. */ - hosts?: null | {[key: string]: string} + hosts?: null | { [key: string]: string } } interface PluginConfigOptions extends ResolvedConfigOptions, RuntimeConfigOptions { @@ -3272,41 +3727,118 @@ declare namespace Cypress { interval: number } - /** - * Setting default options for cy.server() - * @see https://on.cypress.io/server - */ - interface ServerOptions { - delay: number - method: HttpMethod - status: number - headers: object - response: any - onRequest(...args: any[]): void - onResponse(...args: any[]): void - onAbort(...args: any[]): void - enable: boolean - force404: boolean - urlMatchingOptions: object - ignore(xhr: Request): void - onAnyRequest(route: RouteOptions, proxy: any): void - onAnyResponse(route: RouteOptions, proxy: any): void - onAnyAbort(route: RouteOptions, proxy: any): void - } - interface Session { - // Clear all saved sessions and re-run the current spec file. + /** + * Clear all sessions saved on the backend, including cached global sessions. + */ clearAllSavedSessions: () => Promise + /** + * Clear all storage and cookie data across all origins associated with the current session. + */ + clearCurrentSessionData: () => Promise + /** + * Get all storage and cookie data across all origins associated with the current session. + */ + getCurrentSessionData: () => Promise + /** + * Get all storage and cookie data saved on the backend associated with the provided session id. + */ + getSession: (id: string) => Promise + } + + type ActiveSessions = Record + + interface SessionData { + id: string + hydrated: boolean + cacheAcrossSpecs: SessionOptions['cacheAcrossSpecs'] + cookies?: Cookie[] | null + localStorage?: OriginStorage[] | null + sessionStorage?: OriginStorage[] | null + setup: () => void + validate?: SessionOptions['validate'] + } + + interface ServerSessionData extends Omit { + setup: string + validate?: string + } + + interface SessionOptions { + /** + * Whether or not to persist the session across all specs in the run. + * @default {false} + */ + cacheAcrossSpecs?: boolean + /** + * Function to run immediately after the session is created and `setup` function runs or + * after a session is restored and the page is cleared. If this returns `false`, throws an + * exception, returns a Promise which resolves to `false` or rejects or contains any failing + * Cypress command, the session is considered invalid. + * + * If validation fails immediately after `setup`, the test will fail. + * If validation fails after restoring a session, `setup` will re-run. + * @default {false} + */ + validate?: () => Promise | void } type SameSiteStatus = 'no_restriction' | 'strict' | 'lax' + interface SelectFileOptions extends Loggable, Timeoutable, ActionableOptions { + /** + * Which user action to perform. `select` matches selecting a file while + * `drag-drop` matches dragging files from the operating system into the + * document. + * + * @default 'select' + */ + action: 'select' | 'drag-drop' + } + + /** + * Options that control how the `cy.setCookie` command + * sets the cookie in the browser. + * @see https://on.cypress.io/setcookie#Arguments + */ interface SetCookieOptions extends Loggable, Timeoutable { + /** + * The path of the cookie. + * @default "/" + */ path: string + /** + * Represents the domain the cookie belongs to (e.g. "docs.cypress.io", "github.com"). + * @default location.hostname + */ domain: string + /** + * Whether a cookie's scope is limited to secure channels, such as HTTPS. + * @default false + */ secure: boolean + /** + * Whether or not the cookie is HttpOnly, meaning the cookie is inaccessible to client-side scripts. + * The Cypress cookie API has access to HttpOnly cookies. + * @default false + */ httpOnly: boolean + /** + * Whether or not the cookie is a host-only cookie, meaning the request's host must exactly match the domain of the cookie. + * @default false + */ + hostOnly: boolean + /** + * The cookie's expiry time, specified in seconds since Unix Epoch. + * The default is expiry is 20 years in the future from current time. + */ expiry: number + /** + * The cookie's SameSite value. If set, should be one of `lax`, `strict`, or `no_restriction`. + * `no_restriction` is the equivalent of `SameSite=None`. Pass `undefined` to use the browser's default. + * Note: `no_restriction` can only be used if the secure flag is set to `true`. + * @default undefined + */ sameSite: SameSiteStatus } @@ -5481,10 +6013,15 @@ declare namespace Cypress { (fn: (currentSubject: Subject) => void): Chainable } - interface BrowserLaunchOptions { + interface AfterBrowserLaunchDetails { + webSocketDebuggerUrl: string + } + + interface BeforeBrowserLaunchOptions { extensions: string[] preferences: { [key: string]: any } args: string[] + env: { [key: string]: any } } interface Dimensions { @@ -5546,6 +6083,7 @@ declare namespace Cypress { specPattern?: string[] system: SystemDetails tag?: string + autoCancelAfterFailures?: number | false } interface DevServerConfig { @@ -5560,12 +6098,13 @@ declare namespace Cypress { } interface PluginEvents { + (action: 'after:browser:launch', fn: (browser: Browser, browserLaunchDetails: AfterBrowserLaunchDetails) => void | Promise): void (action: 'after:run', fn: (results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult) => void | Promise): void (action: 'after:screenshot', fn: (details: ScreenshotDetails) => void | AfterScreenshotReturnObject | Promise): void (action: 'after:spec', fn: (spec: Spec, results: CypressCommandLine.RunResult) => void | Promise): void (action: 'before:run', fn: (runDetails: BeforeRunDetails) => void | Promise): void (action: 'before:spec', fn: (spec: Spec) => void | Promise): void - (action: 'before:browser:launch', fn: (browser: Browser, browserLaunchOptions: BrowserLaunchOptions) => void | BrowserLaunchOptions | Promise): void + (action: 'before:browser:launch', fn: (browser: Browser, afterBrowserLaunchOptions: BeforeBrowserLaunchOptions) => void | Promise | BeforeBrowserLaunchOptions | Promise): void (action: 'file:preprocessor', fn: (file: FileObject) => string | Promise): void (action: 'dev-server:start', fn: (file: DevServerConfig) => Promise): void (action: 'task', tasks: Tasks): void @@ -5712,7 +6251,7 @@ declare namespace Cypress { * Useful for debugging purposes if you're confused about the order in which commands will execute. * @see https://on.cypress.io/catalog-of-events#App-Events */ - (action: 'command:enqueued', fn: (command: EnqueuedCommand) => void): Cypress + (action: 'command:enqueued', fn: (command: EnqueuedCommandAttributes) => void): Cypress /** * Fires when cy begins actually running and executing your command. * Useful for debugging and understanding how the command queue is async. @@ -5746,14 +6285,14 @@ declare namespace Cypress { * Useful to see how internal cypress commands utilize the {% url 'Cypress.log()' cypress-log %} API. * @see https://on.cypress.io/catalog-of-events#App-Events */ - (action: 'log:added', fn: (log: any, interactive: boolean) => void): Cypress + (action: 'log:added', fn: (attributes: ObjectLike, log: any) => void): Cypress /** * Fires whenever a command's attributes changes. * This event is debounced to prevent it from firing too quickly and too often. * Useful to see how internal cypress commands utilize the {% url 'Cypress.log()' cypress-log %} API. * @see https://on.cypress.io/catalog-of-events#App-Events */ - (action: 'log:changed', fn: (log: any, interactive: boolean) => void): Cypress + (action: 'log:changed', fn: (attributes: ObjectLike, log: any) => void): Cypress /** * Fires before the test and all **before** and **beforeEach** hooks run. * @see https://on.cypress.io/catalog-of-events#App-Events @@ -5834,13 +6373,14 @@ declare namespace Cypress { value: string path: string domain: string + hostOnly?: boolean httpOnly: boolean secure: boolean expiry?: number sameSite?: SameSiteStatus } - interface EnqueuedCommand { + interface EnqueuedCommandAttributes { id: string name: string args: any[] @@ -5848,16 +6388,35 @@ declare namespace Cypress { chainerId: string injected: boolean userInvocationStack?: string + query?: boolean fn(...args: any[]): any } + interface Command { + get(attr: K): EnqueuedCommandAttributes[K] + get(): EnqueuedCommandAttributes + set(key: K, value: EnqueuedCommandAttributes[K]): Log + set(options: Partial): Log + } + interface Exec { code: number stdout: string stderr: string } - type FileReference = string | BufferType | FileReferenceObject + type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + + type FileReference = string | BufferType | FileReferenceObject | TypedArray interface FileReferenceObject { /* * Buffers will be used as-is, while strings will be interpreted as an alias or a file path. @@ -5929,28 +6488,6 @@ declare namespace Cypress { viewportHeight: number } - interface WaitXHR { - duration: number - id: string - method: HttpMethod - request: { - body: string | ObjectLike - headers: ObjectLike - } - requestBody: WaitXHR['request']['body'] - requestHeaders: WaitXHR['request']['headers'] - response: { - body: string | ObjectLike - headers: ObjectLike - } - responseBody: WaitXHR['response']['body'] - responseHeaders: WaitXHR['response']['headers'] - status: number - statusMessage: string - url: string - xhr: XMLHttpRequest - } - type Encodings = 'ascii' | 'base64' | 'binary' | 'hex' | 'latin1' | 'utf8' | 'utf-8' | 'ucs2' | 'ucs-2' | 'utf16le' | 'utf-16le' | null type PositionType = 'topLeft' | 'top' | 'topRight' | 'left' | 'center' | 'right' | 'bottomLeft' | 'bottom' | 'bottomRight' type ViewportPreset = 'macbook-16' | 'macbook-15' | 'macbook-13' | 'macbook-11' | 'ipad-2' | 'ipad-mini' | 'iphone-xr' | 'iphone-x' | 'iphone-6+' | 'iphone-se2' | 'iphone-8' | 'iphone-7' | 'iphone-6' | 'iphone-5' | 'iphone-4' | 'iphone-3' | 'samsung-s10' | 'samsung-note9' @@ -6031,7 +6568,7 @@ declare namespace Mocha { * Describe a "suite" with the given `title`, TestOptions, and callback `fn` containing * nested suites. */ - (title: string, config: Cypress.TestConfigOverrides, fn: (this: Suite) => void): Suite + (title: string, config: Cypress.SuiteConfigOverrides, fn: (this: Suite) => void): Suite } interface ExclusiveSuiteFunction { @@ -6039,10 +6576,10 @@ declare namespace Mocha { * Describe a "suite" with the given `title`, TestOptions, and callback `fn` containing * nested suites. Indicates this suite should be executed exclusively. */ - (title: string, config: Cypress.TestConfigOverrides, fn: (this: Suite) => void): Suite + (title: string, config: Cypress.SuiteConfigOverrides, fn: (this: Suite) => void): Suite } interface PendingSuiteFunction { - (title: string, config: Cypress.TestConfigOverrides, fn: (this: Suite) => void): Suite | void + (title: string, config: Cypress.SuiteConfigOverrides, fn: (this: Suite) => void): Suite | void } } diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index 39c93067a16..5167a954f11 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -4,7 +4,7 @@ // Mike Woudenberg // Robbert van Markus // Nicholas Boll -// TypeScript Version: 3.4 +// TypeScript Version: 4.3 // Updated by the Cypress team: https://www.cypress.io/about/ /// diff --git a/cli/types/tests/actions.ts b/cli/types/tests/actions.ts index 831aaee0fe8..af5563597d2 100644 --- a/cli/types/tests/actions.ts +++ b/cli/types/tests/actions.ts @@ -46,7 +46,7 @@ Cypress.on('scrolled', ($el) => { }) Cypress.on('command:enqueued', (command) => { - command // $ExpectType EnqueuedCommand + command // $ExpectType EnqueuedCommandAttributes }) Cypress.on('command:start', (command) => { @@ -61,11 +61,13 @@ Cypress.on('command:retry', (command) => { command // $ExpectType CommandQueue }) -Cypress.on('log:added', (log, interactive: boolean) => { +Cypress.on('log:added', (attributes, log) => { + attributes // $ExpectType ObjectLike log // $ExpectTyped any }) -Cypress.on('log:changed', (log, interactive: boolean) => { +Cypress.on('log:changed', (attributes, log) => { + attributes // $ExpectType ObjectLike log // $ExpectTyped any }) @@ -74,6 +76,11 @@ Cypress.on('test:before:run', (attributes , test) => { test // $ExpectType Test }) +Cypress.on('test:before:run:async', (attributes , test) => { + attributes // $ExpectType ObjectLike + test // $ExpectType Test +}) + Cypress.on('test:after:run', (attributes , test) => { attributes // $ExpectType ObjectLike test // $ExpectType Test diff --git a/cli/types/tests/cypress-npm-api-test.ts b/cli/types/tests/cypress-npm-api-test.ts index 3a0feebd7df..bea7236891c 100644 --- a/cli/types/tests/cypress-npm-api-test.ts +++ b/cli/types/tests/cypress-npm-api-test.ts @@ -1,6 +1,6 @@ // type tests for Cypress NPM module // https://on.cypress.io/module-api -import cypress, { defineConfig } from 'cypress' +import cypress, { defineComponentFramework, defineConfig } from 'cypress' cypress.run // $ExpectType (options?: Partial | undefined) => Promise cypress.open // $ExpectType (options?: Partial | undefined) => Promise @@ -43,18 +43,39 @@ cypress.run({}).then((results) => { // the caller can determine if Cypress ran or failed to launch cypress.run().then(results => { - if (results.status === 'failed') { - results // $ExpectType CypressFailedRunResult - } else { - results // $ExpectType CypressRunResult - results.status // $ExpectType "finished" - } + results // $ExpectType CypressRunResult | CypressFailedRunResult }) const config = defineConfig({ modifyObstructiveCode: true }) +const solid = { + type: 'solid-js', + name: 'Solid.js', + package: 'solid-js', + installer: 'solid-js', + description: 'Solid is a declarative JavaScript library for creating user interfaces', + minVersion: '^1.0.0' +} + +const thirdPartyFrameworkDefinition = defineComponentFramework({ + type: 'cypress-ct-third-party', + name: 'Third Party', + dependencies: (bundler) => [solid], + detectors: [solid], + supportedBundlers: ['vite', 'webpack'], + icon: '...' +}) + +const thirdPartyFrameworkDefinitionInvalidStrings = defineComponentFramework({ + type: 'cypress-ct-third-party', + name: 'Third Party', + dependencies: (bundler) => [], + detectors: [{}], // $ExpectError + supportedBundlers: ['metro', 'webpack'] // $ExpectError +}) + // component options const componentConfigNextWebpack: Cypress.ConfigOptions = { component: { diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index f13753aedc7..d4a38fb6198 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -34,11 +34,12 @@ namespace CypressConfigTests { // setters Cypress.config('baseUrl', '.') // $ExpectType void - Cypress.config({ e2e: { baseUrl: '.' }}) // $ExpectType void - Cypress.config({ e2e: { baseUrl: null }}) // $ExpectType void - Cypress.config({ e2e: { baseUrl: '.', }}) // $ExpectType void + Cypress.config({ e2e: { baseUrl: '.' } }) // $ExpectError + Cypress.config({ e2e: { baseUrl: null } }) // $ExpectError + Cypress.config({ e2e: { baseUrl: '.', } }) // $ExpectError Cypress.config({ component: { baseUrl: '.', devServer: () => ({} as any) } }) // $ExpectError Cypress.config({ e2e: { indexHtmlFile: 'index.html' } }) // $ExpectError + Cypress.config({ testIsolation: false }) // $ExpectError Cypress.config('taskTimeout') // $ExpectType number Cypress.config('includeShadowDom') // $ExpectType boolean @@ -68,6 +69,7 @@ namespace CypressIsCyTests { declare namespace Cypress { interface Chainable { newCommand: (arg: string) => Chainable + newQuery: (arg: string) => Chainable } } @@ -90,7 +92,7 @@ namespace CypressCommandsTests { arg // $ExpectType string return }) - Cypress.Commands.add('newCommand', { prevSubject: false }, (arg) => { + Cypress.Commands.add('newCommand', { prevSubject: false }, (arg: string) => { arg // $ExpectType string return }) @@ -273,7 +275,7 @@ namespace CypressCommandsTests { originalFn // $ExpectedType Chainable['newCommand'] originalFn.apply(this, [arg]) // $ExpectType Chainable }) - Cypress.Commands.overwrite<'type', 'element'>('type', (originalFn, element, text, options?: Partial) => { + Cypress.Commands.overwrite<'type', 'element'>('type', (originalFn, element, text, options?: Partial) => { element // $ExpectType JQueryWithSelector text // $ExpectType string @@ -290,6 +292,32 @@ namespace CypressCommandsTests { return originalFn(element, text, options) }) + Cypress.Commands.overwrite<'screenshot', 'element'>('screenshot', (originalFn, subject, fileName, options) => { + subject // $ExpectType JQueryWithSelector + fileName // $ExpectType string + options // $ExpectType Partial | undefined + }) + + Cypress.Commands.addQuery('newQuery', function(arg) { + this // $ExpectType Command + arg // $ExpectType string + return () => 3 + }) +} + +namespace CypressNowTest { + cy.now('get') // $ExpectType Promise | ((subject: any) => any) +} + +namespace CypressEnsuresTest { + Cypress.ensure.isType('', ['optional', 'element'], 'newQuery', cy) // $ExpectType void + Cypress.ensure.isElement('', 'newQuery', cy) // $ExpectType void + Cypress.ensure.isWindow('', 'newQuery', cy) // $ExpectType void + Cypress.ensure.isDocument('', 'newQuery', cy) // $ExpectType void + + Cypress.ensure.isAttached('', 'newQuery', cy) // $ExpectType void + Cypress.ensure.isNotDisabled('', 'newQuery') // $ExpectType void + Cypress.ensure.isVisible('', 'newQuery') // $ExpectType void } namespace CypressLogsTest { @@ -328,16 +356,16 @@ namespace CypressItsTests { s }) - cy.wrap({foo: 'bar'}).its('foo') // $ExpectType Chainable + cy.wrap({ foo: 'bar' }).its('foo') // $ExpectType Chainable cy.wrap([1, 2]).its(1) // $ExpectType Chainable cy.wrap(['foo', 'bar']).its(1) // $ExpectType Chainable - .then((s: string) => { - s - }) - cy.wrap({baz: { quux: '2' }}).its('baz.quux') // $ExpectType Chainable - cy.wrap({foo: 'bar'}).its('foo', { log: true }) // $ExpectType Chainable - cy.wrap({foo: 'bar'}).its('foo', { timeout: 100 }) // $ExpectType Chainable - cy.wrap({foo: 'bar'}).its('foo', { log: true, timeout: 100 }) // $ExpectType Chainable + .then((s: string) => { + s + }) + cy.wrap({ baz: { quux: '2' } }).its('baz.quux') // $ExpectType Chainable + cy.wrap({ foo: 'bar' }).its('foo', { log: true }) // $ExpectType Chainable + cy.wrap({ foo: 'bar' }).its('foo', { timeout: 100 }) // $ExpectType Chainable + cy.wrap({ foo: 'bar' }).its('foo', { log: true, timeout: 100 }) // $ExpectType Chainable } namespace CypressInvokeTests { @@ -354,13 +382,13 @@ namespace CypressInvokeTests { cy.wrap([returnsString, returnsNumber]).invoke(1) // $ExpectType Chainable // invoke through property path results in any - cy.wrap({ a: { fn: (x: number) => x * x }}).invoke('a.fn', 4) // $ExpectType Chainable + cy.wrap({ a: { fn: (x: number) => x * x } }).invoke('a.fn', 4) // $ExpectType Chainable // examples below are from previous attempt at typing `invoke` // (see https://github.com/cypress-io/cypress/issues/4022) // call methods on arbitrary objects with reasonable return types - cy.wrap({ fn: () => ({a: 1})}).invoke("fn") // $ExpectType Chainable<{ a: number; }> + cy.wrap({ fn: () => ({ a: 1 }) }).invoke("fn") // $ExpectType Chainable<{ a: number; }> // call methods on dom elements with reasonable return types cy.get('.trigger-input-range').invoke('val', 25) // $ExpectType Chainable @@ -446,46 +474,46 @@ describe('then', () => { it('HTMLElement', () => { cy.get('div') - .then(($div) => { - $div // $ExpectType JQuery - return $div[0] - }) - .then(($div) => { - $div // $ExpectType JQuery - }) + .then(($div) => { + $div // $ExpectType JQuery + return $div[0] + }) + .then(($div) => { + $div // $ExpectType JQuery + }) cy.get('div') - .then(($div) => { - $div // $ExpectType JQuery - return [$div[0]] - }) - .then(($div) => { - $div // $ExpectType JQuery - }) + .then(($div) => { + $div // $ExpectType JQuery + return [$div[0]] + }) + .then(($div) => { + $div // $ExpectType JQuery + }) cy.get('p') - .then(($p) => { - $p // $ExpectType JQuery - return $p[0] - }) - .then({timeout: 3000}, ($p) => { - $p // $ExpectType JQuery - }) + .then(($p) => { + $p // $ExpectType JQuery + return $p[0] + }) + .then({ timeout: 3000 }, ($p) => { + $p // $ExpectType JQuery + }) }) // https://github.com/cypress-io/cypress/issues/16669 it('any as default', () => { cy.get('body') - .then(() => ({} as any)) - .then(v => { - v // $ExpectType any - }) + .then(() => ({} as any)) + .then(v => { + v // $ExpectType any + }) }) }) cy.wait(['@foo', '@bar']) .then(([first, second]) => { - first // $ExpectType Interception + first // $ExpectType Interception }) cy.wait(1234) // $ExpectType Chainable @@ -509,13 +537,13 @@ cy.wrap([{ foo: 'bar' }, { foo: 'baz' }]) subject // $ExpectType string }) - cy.wrap([1, 2, 3]).each((num: number, i, array) => { - return new Cypress.Promise((resolve) => { - setTimeout(() => { - resolve() - }, num * 100) - }) +cy.wrap([1, 2, 3]).each((num: number, i, array) => { + return new Cypress.Promise((resolve) => { + setTimeout(() => { + resolve() + }, num * 100) }) +}) cy.get('something').should('have.length', 1) @@ -523,6 +551,8 @@ cy.stub().withArgs('').log(false).as('foo') cy.spy().withArgs('').log(false).as('foo') +cy.get('something').as('foo', { type: 'static' }) + cy.wrap('foo').then(subject => { subject // $ExpectType string return cy.wrap(subject) @@ -636,6 +666,9 @@ namespace CypressFilterTests { } namespace CypressScreenshotTests { + cy.screenshot().then((result) => { + result // $ExpectType undefined + }) cy.screenshot('example-name') cy.screenshot('example', { log: false }) cy.screenshot({ log: false }) @@ -647,6 +680,10 @@ namespace CypressScreenshotTests { log: true, blackout: [] }) + cy.get('#id').screenshot('example-name', { log: false }) + cy.get('#id').screenshot().then((result) => { + result // $ExpectType JQuery + }) } namespace CypressShadowDomTests { @@ -718,7 +755,7 @@ namespace CypressLocationTests { // https://github.com/cypress-io/cypress/issues/17399 namespace CypressUrlTests { - cy.url({decode: true}).should('contain', '사랑') + cy.url({ decode: true }).should('contain', '사랑') } namespace CypressBrowserTests { @@ -730,11 +767,11 @@ namespace CypressBrowserTests { // does not error to allow for user supplied browsers Cypress.isBrowser('safari')// $ExpectType boolean - Cypress.isBrowser({channel: 'stable'})// $ExpectType boolean - Cypress.isBrowser({family: 'chromium'})// $ExpectType boolean - Cypress.isBrowser({name: 'chrome'})// $ExpectType boolean + Cypress.isBrowser({ channel: 'stable' })// $ExpectType boolean + Cypress.isBrowser({ family: 'chromium' })// $ExpectType boolean + Cypress.isBrowser({ name: 'chrome' })// $ExpectType boolean - Cypress.isBrowser({family: 'foo'}) // $ExpectError + Cypress.isBrowser({ family: 'foo' }) // $ExpectError Cypress.isBrowser() // $ExpectError } @@ -767,6 +804,7 @@ namespace CypressDomTests { Cypress.dom.stringify(el, 'foo') // $ExpectType string Cypress.dom.getElements(jel) // $ExpectType JQuery | HTMLElement[] Cypress.dom.getContainsSelector('foo', 'bar') // $ExpectType string + Cypress.dom.getContainsSelector('foo', 'bar', { matchCase: true }) // $ExpectType string Cypress.dom.getFirstDeepestElement([el], 1) // $ExpectType HTMLElement Cypress.dom.getWindowByElement(el) // $ExpectType HTMLElement | JQuery Cypress.dom.getReasonIsHidden(el) // $ExpectType string @@ -802,6 +840,7 @@ namespace CypressDomTests { Cypress.dom.stringify('', 'foo') // $ExpectError Cypress.dom.getElements(el) // $ExpectError Cypress.dom.getContainsSelector(el, 'bar') // $ExpectError + Cypress.dom.getContainsSelector('foo', 'bar', { invalid: false }) // $ExpectError Cypress.dom.getFirstDeepestElement(el, 1) // $ExpectError Cypress.dom.getWindowByElement('') // $ExpectError Cypress.dom.getReasonIsHidden('') // $ExpectError @@ -832,18 +871,18 @@ namespace CypressTestConfigOverridesTests { waitForAnimations: false }, () => { }) it('test', { - browser: {name: 'firefox'} - }, () => {}) + browser: { name: 'firefox' } + }, () => { }) it('test', { - browser: [{name: 'firefox'}, {name: 'chrome'}] - }, () => {}) + browser: [{ name: 'firefox' }, { name: 'chrome' }] + }, () => { }) it('test', { browser: 'firefox', keystrokeDelay: 0 - }, () => {}) + }, () => { }) it('test', { - browser: {foo: 'bar'}, // $ExpectError - }, () => {}) + browser: { foo: 'bar' }, // $ExpectError + }, () => { }) it('test', { retries: null, keystrokeDelay: 0 @@ -866,47 +905,54 @@ namespace CypressTestConfigOverridesTests { it('test', { retries: { run: 3 } // $ExpectError }, () => { }) + it('test', { + testIsolation: false, // $ExpectError + }, () => { }) - it.skip('test', {}, () => {}) - it.only('test', {}, () => {}) - xit('test', {}, () => {}) + it.skip('test', {}, () => { }) + it.only('test', {}, () => { }) + xit('test', {}, () => { }) - specify('test', {}, () => {}) - specify.only('test', {}, () => {}) - specify.skip('test', {}, () => {}) - xspecify('test', {}, () => {}) + specify('test', {}, () => { }) + specify.only('test', {}, () => { }) + specify.skip('test', {}, () => { }) + xspecify('test', {}, () => { }) // set config on a per-suite basis describe('suite', { - browser: {family: 'firefox'}, + browser: { family: 'firefox' }, keystrokeDelay: 0 - }, () => {}) + }, () => { }) - context('suite', {}, () => {}) + describe('suite', { + testIsolation: false, + }, () => { }) + + context('suite', {}, () => { }) describe('suite', { - browser: {family: 'firefox'}, + browser: { family: 'firefox' }, keystrokeDelay: false // $ExpectError foo: 'foo' // $ExpectError - }, () => {}) + }, () => { }) - describe.only('suite', {}, () => {}) - describe.skip('suite', {}, () => {}) - xdescribe('suite', {}, () => {}) + describe.only('suite', {}, () => { }) + describe.skip('suite', {}, () => { }) + xdescribe('suite', {}, () => { }) } namespace CypressShadowTests { cy - .get('.foo') - .shadow() - .find('.bar') - .click() + .get('.foo') + .shadow() + .find('.bar') + .click() cy.get('.foo', { includeShadowDom: true }).click() cy - .get('.foo') - .find('.bar', {includeShadowDom: true}) + .get('.foo') + .find('.bar', { includeShadowDom: true }) } namespace CypressTaskTests { @@ -922,20 +968,21 @@ namespace CypressTaskTests { } namespace CypressSessionsTests { - cy.session('user') - cy.session('user', () => {}) - cy.session({ name: 'bob' }, () => {}) - cy.session('user', () => {}, {}) - cy.session('user', () => {}, { - validate: () => {} + cy.session('user', () => { }) + cy.session({ name: 'bob' }, () => { }) + cy.session('user', () => { }, {}) + cy.session('user', () => { }, { + validate: () => { } }) cy.session() // $ExpectError + cy.session('user') // $ExpectError cy.session(null) // $ExpectError - cy.session('user', () => {}, { + cy.session('user', () => { }, { validate: { foo: true } // $ExpectError }) } + namespace CypressCurrentTest { Cypress.currentTest.title // $ExpectType string Cypress.currentTest.titlePath // $ExpectType string[] @@ -958,19 +1005,251 @@ namespace CypressKeyboardTests { } namespace CypressOriginTests { - cy.origin('example.com', () => {}) - cy.origin('example.com', { args: {}}, (value: object) => {}) - cy.origin('example.com', { args: { one: 1, key: 'value', bool: true } }, (value: { one: number, key: string, bool: boolean}) => {}) - cy.origin('example.com', { args: [1, 'value', true ] }, (value: Array<(number | string | boolean)>) => {}) - cy.origin('example.com', { args : 'value'}, (value: string) => {}) - cy.origin('example.com', { args: 1 }, (value: number) => {}) - cy.origin('example.com', { args: true }, (value: boolean) => {}) + cy.origin('example.com', () => { }) + cy.origin('example.com', { args: {} }, (value: object) => { }) + cy.origin('example.com', { args: { one: 1, key: 'value', bool: true } }, (value: { one: number, key: string, bool: boolean }) => { }) + cy.origin('example.com', { args: [1, 'value', true] }, (value: Array<(number | string | boolean)>) => { }) + cy.origin('example.com', { args: 'value' }, (value: string) => { }) + cy.origin('example.com', { args: 1 }, (value: number) => { }) + cy.origin('example.com', { args: true }, (value: boolean) => { }) cy.origin() // $ExpectError cy.origin('example.com') // $ExpectError cy.origin(true) // $ExpectError cy.origin('example.com', {}) // $ExpectError cy.origin('example.com', {}, {}) // $ExpectError - cy.origin('example.com', { args: ['value'] }, (value: boolean[]) => {}) // $ExpectError - cy.origin('example.com', {}, (value: undefined) => {}) // $ExpectError + cy.origin('example.com', { args: ['value'] }, (value: boolean[]) => { }) // $ExpectError + cy.origin('example.com', {}, (value: undefined) => { }) // $ExpectError +} + +namespace CypressGetCookiesTests { + cy.getCookies().then((cookies) => { + cookies // $ExpectType Cookie[] + }) + cy.getCookies({ log: true }) + cy.getCookies({ timeout: 10 }) + cy.getCookies({ domain: 'localhost' }) + cy.getCookies({ log: true, timeout: 10, domain: 'localhost' }) + + cy.getCookies({ log: 'true' }) // $ExpectError + cy.getCookies({ timeout: '10' }) // $ExpectError + cy.getCookies({ domain: false }) // $ExpectError +} + +namespace CypressGetAllCookiesTests { + cy.getAllCookies().then((cookies) => { + cookies // $ExpectType Cookie[] + }) + cy.getAllCookies({ log: true }) + cy.getAllCookies({ timeout: 10 }) + cy.getAllCookies({ log: true, timeout: 10 }) + + cy.getAllCookies({ log: 'true' }) // $ExpectError + cy.getAllCookies({ timeout: '10' }) // $ExpectError + cy.getAllCookies({ other: true }) // $ExpectError +} + +namespace CypressGetCookieTests { + cy.getCookie('name').then((cookie) => { + cookie // $ExpectType Cookie | null + }) + cy.getCookie('name', { log: true }) + cy.getCookie('name', { timeout: 10 }) + cy.getCookie('name', { domain: 'localhost' }) + cy.getCookie('name', { log: true, timeout: 10, domain: 'localhost' }) + + cy.getCookie('name', { log: 'true' }) // $ExpectError + cy.getCookie('name', { timeout: '10' }) // $ExpectError + cy.getCookie('name', { domain: false }) // $ExpectError +} + +namespace CypressSetCookieTests { + cy.setCookie('name', 'value').then((cookie) => { + cookie // $ExpectType Cookie + }) + cy.setCookie('name', 'value', { log: true }) + cy.setCookie('name', 'value', { timeout: 10 }) + cy.setCookie('name', 'value', { + domain: 'localhost', + path: '/', + secure: true, + httpOnly: false, + expiry: 12345, + sameSite: 'lax', + }) + cy.setCookie('name', 'value', { + domain: 'www.foobar.com', + path: '/', + secure: false, + httpOnly: false, + hostOnly: true, + sameSite: 'lax', + }) + cy.setCookie('name', 'value', { log: true, timeout: 10, domain: 'localhost' }) + + cy.setCookie('name') // $ExpectError + cy.setCookie('name', 'value', { log: 'true' }) // $ExpectError + cy.setCookie('name', 'value', { timeout: '10' }) // $ExpectError + cy.setCookie('name', 'value', { domain: false }) // $ExpectError + cy.setCookie('name', 'value', { foo: 'bar' }) // $ExpectError +} + +namespace CypressClearCookieTests { + cy.clearCookie('name').then((result) => { + result // $ExpectType null + }) + cy.clearCookie('name', { log: true }) + cy.clearCookie('name', { timeout: 10 }) + cy.clearCookie('name', { domain: 'localhost' }) + cy.clearCookie('name', { log: true, timeout: 10, domain: 'localhost' }) + + cy.clearCookie('name', { log: 'true' }) // $ExpectError + cy.clearCookie('name', { timeout: '10' }) // $ExpectError + cy.clearCookie('name', { domain: false }) // $ExpectError +} + +namespace CypressClearCookiesTests { + cy.clearCookies().then((result) => { + result // $ExpectType null + }) + cy.clearCookies({ log: true }) + cy.clearCookies({ timeout: 10 }) + cy.clearCookies({ domain: 'localhost' }) + cy.clearCookies({ log: true, timeout: 10, domain: 'localhost' }) + + cy.clearCookies({ log: 'true' }) // $ExpectError + cy.clearCookies({ timeout: '10' }) // $ExpectError + cy.clearCookies({ domain: false }) // $ExpectError +} + +namespace CypressClearAllCookiesTests { + cy.clearAllCookies().then((cookies) => { + cookies // $ExpectType null + }) + cy.clearAllCookies({ log: true }) + cy.clearAllCookies({ timeout: 10 }) + cy.clearAllCookies({ log: true, timeout: 10 }) + + cy.clearAllCookies({ log: 'true' }) // $ExpectError + cy.clearAllCookies({ timeout: '10' }) // $ExpectError + cy.clearAllCookies({ other: true }) // $ExpectError +} + +namespace CypressLocalStorageTests { + cy.getAllLocalStorage().then((result) => { + result // $ExpectType StorageByOrigin + }) + cy.getAllLocalStorage({ log: false }) + cy.getAllLocalStorage({ log: 'true' }) // $ExpectError + + cy.clearAllLocalStorage().then((result) => { + result // $ExpectType null + }) + cy.clearAllLocalStorage({ log: false }) + cy.clearAllLocalStorage({ log: 'true' }) // $ExpectError + + cy.getAllSessionStorage().then((result) => { + result // $ExpectType StorageByOrigin + }) + cy.getAllSessionStorage({ log: false }) + cy.getAllSessionStorage({ log: 'true' }) // $ExpectError + + cy.clearAllSessionStorage().then((result) => { + result // $ExpectType null + }) + cy.clearAllSessionStorage({ log: false }) + cy.clearAllSessionStorage({ log: 'true' }) // $ExpectError +} + +namespace CypressRetriesSpec { + Cypress.config('retries', { + openMode: 0, + runMode: 1 + }) + + Cypress.config('retries', { + openMode: false, + runMode: false, + experimentalStrategy: "detect-flake-and-pass-on-threshold", + experimentalOptions: { + maxRetries: 2, + passesRequired: 2 + } + }) + + Cypress.config('retries', { + openMode: false, + runMode: false, + experimentalStrategy: "detect-flake-but-always-fail", + experimentalOptions: { + maxRetries: 2, + stopIfAnyPassed: true + } + }) + + Cypress.config('retries', { openMode: false, runMode: true, experimentalStrategy: "detect-flake-and-pass-on-threshold", experimentalOptions: { maxRetries: 2 } }) // $ExpectError + Cypress.config('retries', { openMode: false, runMode: true, experimentalStrategy: "detect-flake-but-always-fail", experimentalOptions: { maxRetries: 2 } }) // $ExpectError + + Cypress.config('retries', { openMode: false, runMode: true, experimentalStrategy: "detect-flake-and-pass-on-threshold", experimentalOptions: { passesRequired: 2 } }) // $ExpectError + Cypress.config('retries', { openMode: false, runMode: true, experimentalStrategy: "detect-flake-but-always-fail", experimentalOptions: { stopIfAnyPassed: true } }) // $ExpectError +} + +namespace CypressTraversalTests { + cy.wrap({}).prevUntil('a') // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem') // $ExpectType Chainable> + cy.wrap({}).prevUntil('span', 'a') // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem', 'a') // $ExpectType Chainable> + cy.wrap({}).prevUntil('div', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem', 'a', { log: 'true' }) // $ExpectError + + cy.wrap({}).nextUntil('a') // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem') // $ExpectType Chainable> + cy.wrap({}).nextUntil('span', 'a') // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem', 'a') // $ExpectType Chainable> + cy.wrap({}).nextUntil('div', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem', 'a', { log: 'true' }) // $ExpectError + + cy.wrap({}).parentsUntil('a') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('span', 'a') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem', 'a') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('div', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem', 'a', { log: 'true' }) // $ExpectError +} + +namespace CypressRequireTests { + Cypress.require('lodash') + + const anydep = Cypress.require('anydep') + anydep // $ExpectType any + + const sinon = Cypress.require('sinon') as typeof import('sinon') + sinon // $ExpectType SinonStatic + + const lodash = Cypress.require<_.LoDashStatic>('lodash') + lodash // $ExpectType LoDashStatic + + Cypress.require() // $ExpectError + Cypress.require({}) // $ExpectError + Cypress.require(123) // $ExpectError +} + +namespace CypressGlobalsTests { + Cypress + cy + expect + assert + + window.Cypress + window.cy + window.expect + window.assert + + globalThis.Cypress + globalThis.cy + globalThis.expect + globalThis.assert } diff --git a/cli/types/tests/kitchen-sink.ts b/cli/types/tests/kitchen-sink.ts index 5f5085ae8b1..7ddd77619ae 100644 --- a/cli/types/tests/kitchen-sink.ts +++ b/cli/types/tests/kitchen-sink.ts @@ -18,14 +18,6 @@ result // $ExpectType boolean Cypress.minimatch('/users/1/comments', '/users/*/comments') // $ExpectType boolean -// check if cy.server() yields default server options -cy.server().should((server) => { - server // $ExpectType ServerOptions - expect(server.delay).to.eq(0) - expect(server.method).to.eq('GET') - expect(server.status).to.eq(200) -}) - cy.visit('https://www.acme.com/', { auth: { username: 'wile', @@ -33,13 +25,6 @@ cy.visit('https://www.acme.com/', { } }) -const serverOptions: Partial = { - delay: 100, - ignore: () => true -} - -cy.server(serverOptions) - Cypress.spec.name // $ExpectType string Cypress.spec.relative // $ExpectType string Cypress.spec.absolute // $ExpectType string diff --git a/cli/types/tests/net-stubbing-tests.ts b/cli/types/tests/net-stubbing-tests.ts new file mode 100644 index 00000000000..0af4230846c --- /dev/null +++ b/cli/types/tests/net-stubbing-tests.ts @@ -0,0 +1,608 @@ +import { + CyHttpMessages, + HttpRequestInterceptor, + HttpResponseInterceptor, + Interception, + Route, + RouteHandler, + RouteHandlerController, + RouteMap, + RouteMatcher, + StringMatcher +} from 'cypress/types/net-stubbing' + +interface CustomRequest { + payload: object +} + +interface CustomResponse { + data: object +} + +describe('net stubbing types', () => { + describe('BaseMessage', () => { + it('has any body by default', () => { + const sut: CyHttpMessages.BaseMessage = undefined! + sut.body // $ExpectType any + }) + + it('has typed body if given', () => { + const sut: CyHttpMessages.BaseMessage = undefined! + sut.body // $ExpectType CustomRequest + }) + }) + + describe('IncomingResponse', () => { + it('has any body by default', () => { + const sut: CyHttpMessages.IncomingResponse = undefined! + sut.body // $ExpectType any + }) + + it('has typed body if given', () => { + const sut: CyHttpMessages.IncomingResponse = undefined! + sut.body // $ExpectType CustomResponse + }) + }) + + describe('IncomingHttpResponse', () => { + it('has any body by default', () => { + const sut: CyHttpMessages.IncomingHttpResponse = undefined! + sut.body // $ExpectType any + }) + + it('has typed body if given', () => { + const sut: CyHttpMessages.IncomingHttpResponse = undefined! + sut.body // $ExpectType CustomResponse + }) + + it('returns the typed body from setDelay', () => { + const sut: CyHttpMessages.IncomingHttpResponse = undefined! + sut.setDelay(0) // $ExpectType IncomingHttpResponse + }) + + it('returns the typed body from setThrottle', () => { + const sut: CyHttpMessages.IncomingHttpResponse = undefined! + sut.setThrottle(0) // $ExpectType IncomingHttpResponse + }) + }) + + describe('IncomingRequest', () => { + it('has any body by default', () => { + const sut: CyHttpMessages.IncomingRequest = undefined! + sut.body // $ExpectType any + }) + + it('has typed body if given', () => { + const sut: CyHttpMessages.IncomingRequest = undefined! + sut.body // $ExpectType CustomRequest + }) + }) + + describe('IncomingHttpRequest', () => { + it('has any body by default', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + sut.body // $ExpectType any + }) + + it('has typed body if given', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + sut.body // $ExpectType CustomRequest + }) + + it('accepts a typed interceptor, of the same expected response type, in continue()', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + const input: HttpResponseInterceptor = undefined! + + sut.continue(input) + }) + + it('accepts a typed interceptor, of the same expected response type, in reply()', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + const input: HttpResponseInterceptor = undefined! + + sut.reply(input) + }) + }) + + describe('ResponseComplete', () => { + it('has any finalResBody by default', () => { + const sut: CyHttpMessages.ResponseComplete = undefined! + sut.finalResBody // $ExpectType any + }) + + it('has typed finalResBody if given', () => { + const sut: CyHttpMessages.ResponseComplete = undefined! + sut.finalResBody // $ExpectType CustomResponse | undefined + }) + }) + + describe('HttpRequestInterceptor', () => { + it('accepts a request with any req/res body by default', () => { + const sut: HttpRequestInterceptor = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut(request) + }) + + it('accepts a request with a typed req/res body if given', () => { + const sut: HttpRequestInterceptor = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut(request) + }) + + it('does not accept a request with a typed req/res body if mismatched', () => { + const sut: HttpRequestInterceptor = undefined! + + // Request and response are flipped, which is incorrect. + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + // @ts-expect-error -- Argument of type ... is not assignable. + sut(request) + }) + }) + + describe('HttpResponseInterceptor', () => { + it('accepts a response with any body by default', () => { + const sut: HttpResponseInterceptor = undefined! + const response: CyHttpMessages.IncomingHttpResponse = undefined! + + sut(response) + }) + + it('accepts a response with a typed body if given', () => { + const sut: HttpResponseInterceptor = undefined! + const response: CyHttpMessages.IncomingHttpResponse = undefined! + + sut(response) + }) + + it('does not accept a response with a typed body if mismatched', () => { + const sut: HttpResponseInterceptor = undefined! + + // Expecting a custom response, but response is just a string. + const response: CyHttpMessages.IncomingHttpResponse = undefined! + + // @ts-expect-error -- Argument of type ... is not assignable. + sut(response) + }) + }) + + describe('RequestEvents (via IncomingHttpRequest)', () => { + it('accepts a response with any body by default, in each on()', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + const cb: HttpResponseInterceptor = undefined! + const cbAfter: (res: CyHttpMessages.IncomingResponse) => void = undefined! + + sut.on('before:response', cb) // $ExpectType IncomingHttpRequest + sut.on('response', cb) // $ExpectType IncomingHttpRequest + sut.on('after:response', cbAfter) // $ExpectType IncomingHttpRequest + }) + + it('accepts a response with a typed body if given', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + const cb: HttpResponseInterceptor = undefined! + const cbAfter: (res: CyHttpMessages.IncomingResponse) => void = undefined! + + sut.on('before:response', cb) // $ExpectType IncomingHttpRequest + sut.on('response', cb) // $ExpectType IncomingHttpRequest + sut.on('after:response', cbAfter) // $ExpectType IncomingHttpRequest + }) + + it('does not accept a response with a typed body if given but mismatched', () => { + const sut: CyHttpMessages.IncomingHttpRequest = undefined! + + // Expecting a custom response, but callbacks just have mismatched string responses. + const cb: HttpResponseInterceptor = undefined! + const cbAfter: (res: CyHttpMessages.IncomingResponse) => void = undefined! + + // @ts-expect-error -- Argument of type ... is not assignable. + sut.on('before:response', cb) + + // @ts-expect-error -- Argument of type ... is not assignable. + sut.on('response', cb) + + // @ts-expect-error -- Argument of type ... is not assignable. + sut.on('after:response', cbAfter) + }) + }) + + describe('Interception', () => { + it('has any req/res body by default', () => { + const sut: Interception = undefined! + + sut.request.body // $ExpectType any + sut.response!.body // $ExpectType any + }) + + it('has typed req/res body if given', () => { + const sut: Interception = undefined! + + sut.request.body // $ExpectType CustomRequest + sut.response!.body // $ExpectType CustomResponse + }) + }) + + describe('Route', () => { + it('has a handler typed as any by default', () => { + const sut: Route = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut.handler // $ExpectType RouteHandler + + if (typeof sut.handler === 'function') { + sut.handler(request) + } + }) + + it('has a typed handler if given', () => { + const sut: Route = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut.handler // $ExpectType RouteHandler + + if (typeof sut.handler === 'function') { + sut.handler(request) + } + }) + + it('does not accept a typed handler if given but mismatched', () => { + const sut: Route = undefined! + + // Request and response are flipped, which is incorrect. + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + if (typeof sut.handler === 'function') { + // @ts-expect-error -- Argument of type ... is not assignable. + sut.handler(request) + } + }) + + it('contains requests with interceptions of any req/res body by default', () => { + const sut: Route = undefined! + sut.requests['r'] // $ExpectType Interception + }) + + it('contains requests with interceptions of typed req/res body if given', () => { + const sut: Route = undefined! + sut.requests['r'] // $ExpectType Interception + }) + + it('contains requests with interceptions, which do not accept mismatches, of typed req/res body if given', () => { + const sut: Route = undefined! + + // Request and response are flipped, which is incorrect. + const interception: Interception = undefined! + + // @ts-expect-error -- Type ... is not assignable. + sut.requests['r'] = interception + }) + }) + + describe('RouteMap', () => { + it('each item has a handler typed as any by default', () => { + const sut: RouteMap = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut['r'] // $ExpectType Route + + if (typeof sut['r'].handler === 'function') { + sut['r'].handler(request) + } + }) + + it('each item has a typed handler if given', () => { + const sut: RouteMap = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut['r'] // $ExpectType Route + + if (typeof sut['r'].handler === 'function') { + sut['r'].handler(request) + } + }) + + it('each item does not accept a typed handler if given but mismatched', () => { + const sut: RouteMap = undefined! + + // Request and response are flipped, which is incorrect. + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + if (typeof sut['r'].handler === 'function') { + // @ts-expect-error -- Argument of type ... is not assignable. + sut['r'].handler(request) + } + }) + + it('each item contains requests with interceptions of any req/res body by default', () => { + const sut: RouteMap = undefined! + + sut['r'] // $ExpectType Route + sut['r'].requests['r'] // $ExpectType Interception + }) + + it('each item contains requests with interceptions of typed req/res body if given', () => { + const sut: RouteMap = undefined! + + sut['r'] // $ExpectType Route + sut['r'].requests['r'] // $ExpectType Interception + }) + + it('each item contains requests with interceptions, which do not accept mismatches, of typed req/res body if given', () => { + const sut: RouteMap = undefined! + + // Request and response are flipped, which is incorrect. + const interception: Interception = undefined! + + // @ts-expect-error -- Type ... is not assignable. + sut['r'].requests['r'] = interception + }) + }) + + describe('RouteHandlerController', () => { + it('accepts a request with any req/res body by default', () => { + const sut: RouteHandlerController = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut(request) + }) + + it('accepts a request with a typed req/res body if given', () => { + const sut: RouteHandlerController = undefined! + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + sut(request) + }) + + it('does not accept a request with a typed req/res body if mismatched', () => { + const sut: RouteHandlerController = undefined! + + // Request and response are flipped, which is incorrect. + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + // @ts-expect-error -- Argument of type ... is not assignable. + sut(request) + }) + }) + + describe('RouteHandler', () => { + it('accepts a request with any req/res body by default', () => { + const sut: RouteHandler = () => { } + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + if (typeof sut === 'function') { + sut(request) + } + }) + + it('accepts a request with a typed req/res body if given', () => { + const sut: RouteHandler = () => { } + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + if (typeof sut === 'function') { + sut(request) + } + }) + + it('does not accept a request with a typed req/res body if mismatched', () => { + const sut: RouteHandler = () => { } + + // Request and response are flipped, which is incorrect. + const request: CyHttpMessages.IncomingHttpRequest = undefined! + + if (typeof sut === 'function') { + // @ts-expect-error -- Argument of type ... is not assignable. + sut(request) + } + }) + }) + + describe('cy.intercept', () => { + describe('for a route matcher URL', () => { + it('accepts any req/res body as response handler by default', () => { + const sut: Cypress.Chainable = undefined! + const url: RouteMatcher = undefined! + + sut.intercept(url, (req) => { + req.body // $ExpectType any + + req.continue((res) => { + res.body // $ExpectType any + }) + }) + }) + + it('accepts typed req/res body as response handler if given', () => { + const sut: Cypress.Chainable = undefined! + const url: RouteMatcher = undefined! + + sut.intercept(url, (req) => { + req.body // $ExpectType CustomRequest + + req.continue((res) => { + res.body // $ExpectType CustomResponse + }) + }) + }) + + it('infers types for req/res body if given', () => { + const sut: Cypress.Chainable = undefined! + const url: RouteMatcher = undefined! + + type Req = CyHttpMessages.IncomingHttpRequest + + sut.intercept(url, (req: Req) => { + req.body // $ExpectType CustomRequest + + req.continue((res) => { + res.body // $ExpectType CustomResponse + }) + }) + }) + }) + + describe('for a method-restricted route matcher URL', () => { + it('accepts any req/res body as response handler by default', () => { + const sut: Cypress.Chainable = undefined! + const url: RouteMatcher = undefined! + + sut.intercept('POST', url, (req) => { + req.body // $ExpectType any + + req.continue((res) => { + res.body // $ExpectType any + }) + }) + }) + + it('accepts typed req/res body as response handler if given', () => { + const sut: Cypress.Chainable = undefined! + const url: RouteMatcher = undefined! + + sut.intercept('POST', url, (req) => { + req.body // $ExpectType CustomRequest + + req.continue((res) => { + res.body // $ExpectType CustomResponse + }) + }) + }) + + it('infers types for req/res body if given', () => { + const sut: Cypress.Chainable = undefined! + const url: RouteMatcher = undefined! + + type Req = CyHttpMessages.IncomingHttpRequest + + sut.intercept('POST', url, (req: Req) => { + req.body // $ExpectType CustomRequest + + req.continue((res) => { + res.body // $ExpectType CustomResponse + }) + }) + }) + }) + + describe('for a string matcher URL', () => { + it('accepts any req/res body as response handler by default', () => { + const sut: Cypress.Chainable = undefined! + const url: StringMatcher = undefined! + + sut.intercept(url, { middleware: true }, (req) => { + req.body // $ExpectType any + + req.continue((res) => { + res.body // $ExpectType any + }) + }) + }) + + it('accepts typed req/res body as response handler if given', () => { + const sut: Cypress.Chainable = undefined! + const url: StringMatcher = undefined! + + sut.intercept(url, { middleware: true }, (req) => { + req.body // $ExpectType CustomRequest + + req.continue((res) => { + res.body // $ExpectType CustomResponse + }) + }) + }) + + it('infers types for req/res body if given', () => { + const sut: Cypress.Chainable = undefined! + const url: StringMatcher = undefined! + + type Req = CyHttpMessages.IncomingHttpRequest + + sut.intercept(url, { middleware: true }, (req: Req) => { + req.body // $ExpectType CustomRequest + + req.continue((res) => { + res.body // $ExpectType CustomResponse + }) + }) + }) + }) + }) + + describe('cy.wait', () => { + describe('with a single alias', () => { + it('accepts any req/res body as response handler by default', () => { + const cy: Cypress.Chainable = undefined! + + cy.wait('@a').then(({ request, response }) => { + request.body // $ExpectType any + response!.body // $ExpectType any + }) + }) + + it('accepts typed req/res body as response handler if given', () => { + const cy: Cypress.Chainable = undefined! + + cy.wait('@a').then(({ request, response }) => { + request.body // $ExpectType CustomRequest + response!.body // $ExpectType CustomResponse + }) + }) + + it('infers types for req/res body if given', () => { + const cy: Cypress.Chainable = undefined! + + cy.wait('@a').then(({ request, response }: Interception) => { + request.body // $ExpectType CustomRequest + response!.body // $ExpectType CustomResponse + }) + }) + }) + + describe('with an array of aliases', () => { + interface AReq { a: CustomRequest } + interface BReq { b: CustomRequest } + interface ARes { a: CustomResponse } + interface BRes { b: CustomResponse } + + it('accepts any req/res body as response handler by default', () => { + const cy: Cypress.Chainable = undefined! + + cy.wait([]).then((interceptions) => { + interceptions // $ExpectType Interception[] + }) + + cy.wait(['@a']).then((interceptions) => { + interceptions.forEach(({ request, response }) => { + request.body // $ExpectType any + response!.body // $ExpectType any + }) + }) + + cy.wait(['@a', '@b']).then((interceptions) => { + interceptions.forEach(({ request, response }) => { + request.body // $ExpectType any + response!.body // $ExpectType any + }) + }) + }) + + it('infers types for req/res body if given', () => { + const cy: Cypress.Chainable = undefined! + + cy.wait(['@a']).then((interceptions: Array>) => { + interceptions.forEach(({ request, response }) => { + request.body // $ExpectType AReq + response!.body // $ExpectType ARes + }) + }) + + cy.wait(['@a', '@b']).then((interceptions: Array>) => { + interceptions.forEach(({ request, response }) => { + request.body // $ExpectType AReq | BReq + response!.body // $ExpectType ARes | BRes + }) + }) + }) + }) + }) +}) diff --git a/cli/types/tests/plugins-config.ts b/cli/types/tests/plugins-config.ts index 2292a8e42fa..8b92a501032 100644 --- a/cli/types/tests/plugins-config.ts +++ b/cli/types/tests/plugins-config.ts @@ -9,7 +9,7 @@ const pluginConfig2: Cypress.PluginConfig = (on, config) => { config.configFile // $ExpectType string config.fixturesFolder // $ExpectType string | false config.screenshotsFolder // $ExpectType string | false - config.videoCompression // $ExpectType number | false + config.videoCompression // $ExpectType number | boolean config.projectRoot // $ExpectType string config.version // $ExpectType string config.testingType // $ExpectType TestingType @@ -19,6 +19,7 @@ const pluginConfig2: Cypress.PluginConfig = (on, config) => { browser.displayName // $ExpectType string options.extensions // $ExpectType string[] options.args // $ExpectType string[] + options.env // $ExpectType { [key: string]: any; } console.log('launching browser', browser.displayName) return options diff --git a/cli/types/tsconfig.json b/cli/types/tsconfig.json index 8bb09ec2444..5e9ac0d8af4 100644 --- a/cli/types/tsconfig.json +++ b/cli/types/tsconfig.json @@ -5,6 +5,7 @@ "dom", "es6" ], + "exactOptionalPropertyTypes": true, "noImplicitAny": true, "noImplicitThis": true, "strictNullChecks": true, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..849089685df --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +version: '3' + +services: + dev: + image: cypress/browsers:latest + ports: + # Share debugging ports + - 5566:5566 + - 5567:5567 + environment: + # Use Hist file from shared volume + HISTFILE: /root/hist/.bash_history + # Setup inspect to use the more permissive address when debugging so + # that we can connect to it from outside the docker container + CYPRESS_DOCKER_DEV_INSPECT_OVERRIDE: '0.0.0.0:5566' + # This disables CI mode which causes cypress to build differently + CI: '' + command: /bin/bash + working_dir: /opt/cypress + volumes: + # Copy Cypress source to docker container + - .:/opt/cypress + - bash-history:/root/hist + watch: + image: cypress/browsers:latest + environment: + # This disables CI mode which causes cypress to build differently + CI: '' + command: yarn watch + working_dir: /opt/cypress + volumes: + # Copy Cypress source to docker container + - .:/opt/cypress + ci: + # This should mirror the image used in workflows.yml + image: cypress/browsers-internal:node18.17.1-chrome118-ff115 + ports: + - 5566:5566 + - 5567:5567 + command: /bin/bash + environment: + HISTFILE: /root/hist/.bash_history + CYPRESS_DOCKER_DEV_INSPECT_OVERRIDE: '0.0.0.0:5566' + working_dir: /opt/cypress + volumes: + - .:/opt/cypress + - bash-history:/root/hist + +# persist terminal history between runs in a virtual volume +volumes: + bash-history: diff --git a/electron-builder.json b/electron-builder.json index bfe10a1acac..91a46582f95 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -16,6 +16,10 @@ "executableName": "Cypress" }, "win": { + "signingHashAlgorithms": [ + "sha256" + ], + "sign": "./scripts/windows-sign.js", "target": "dir" }, "afterPack": "./scripts/after-pack-hook.js", diff --git a/graphql-codegen.yml b/graphql-codegen.yml deleted file mode 100644 index c540da7d569..00000000000 --- a/graphql-codegen.yml +++ /dev/null @@ -1,130 +0,0 @@ -# https://www.graphql-code-generator.com/docs/getting-started/index - -documentFilters: &documentFilters - immutableTypes: true - useTypeImports: true - preResolveTypes: true - onlyOperationTypes: true - avoidOptionals: true - -vueOperations: &vueOperations - schema: './packages/graphql/schemas/schema.graphql' - config: - <<: *documentFilters - plugins: - - add: - content: '/* eslint-disable */' - - 'typescript' - - 'typescript-operations' - - 'typed-document-node': - # Intentionally specified under typed-document-node rather than top level config, - # becuase we don't want it flattening the types for the operations - flattenGeneratedTypes: true - -vueTesting: &vueTesting - schema: './packages/graphql/schemas/schema.graphql' - config: - <<: *documentFilters - plugins: - - add: - content: '/* eslint-disable */' - - 'typescript' - - 'typescript-operations': - # For modifying in mountFragment - immutableTypes: false - - 'typed-document-node' - -overwrite: true -config: - enumsAsTypes: true - declarationKind: 'interface' - strictScalars: true - scalars: - Date: string - DateTime: string - JSON: any -generates: - ### - # Generates types for us to infer the correct "source types" when we mock out on the frontend - # This ensures we have proper type checking when we're using cy.mountFragment in component tests - ### - './packages/frontend-shared/cypress/support/generated/test-graphql-types.gen.ts': - schema: 'packages/graphql/schemas/schema.graphql' - plugins: - - add: - content: '/* eslint-disable */' - - 'typescript': - nonOptionalTypename: true - - 'packages/frontend-shared/script/codegen-type-map.js' - - './packages/graphql/src/gen/test-cloud-graphql-types.gen.ts': - schema: 'packages/graphql/schemas/cloud.graphql' - plugins: - - add: - content: '/* eslint-disable */' - - 'typescript': - nonOptionalTypename: true - - 'packages/frontend-shared/script/codegen-type-map.js' - - './packages/graphql/src/gen/cloud-source-types.gen.ts': - config: - useTypeImports: true - schema: 'packages/graphql/schemas/cloud.graphql' - plugins: - - add: - content: '/* eslint-disable */' - - 'typescript' - - 'typescript-resolvers' - - ### - # Generates types for us to infer the correct keys for graphcache - ### - './packages/data-context/src/gen/graphcache-config.gen.ts': - config: - useTypeImports: true - schema: 'packages/graphql/schemas/schema.graphql' - plugins: - - add: - content: '/* eslint-disable */' - - typescript - - typescript-urql-graphcache - - ### - # All of the GraphQL Query/Mutation documents we import for use in the .{vue,ts,tsx,js,jsx} - # files for useQuery / useMutation, as well as types associated with the fragments - ### - './packages/launchpad/src/generated/graphql.ts': - documents: - - './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}' - - './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}' - <<: *vueOperations - - './packages/app/src/generated/graphql.ts': - documents: - - './packages/app/src/**/*.{vue,ts,tsx,js,jsx}' - - './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}' - <<: *vueOperations - - './packages/frontend-shared/src/generated/graphql.ts': - documents: './packages/frontend-shared/src/{gql-components,graphql}/**/*.{vue,ts,tsx,js,jsx}' - <<: *vueOperations - ### - # All GraphQL documents imported into the .spec.tsx files for component testing. - # Similar to generated/graphql.ts, except it doesn't include the flattening for the document nodes, - # so we can actually use the document in cy.mountFragment - ### - './packages/launchpad/src/generated/graphql-test.ts': - documents: - - './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}' - - './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}' - <<: *vueTesting - - './packages/app/src/generated/graphql-test.ts': - documents: - - './packages/app/src/**/*.{vue,ts,tsx,js,jsx}' - - './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}' - <<: *vueTesting - - './packages/frontend-shared/src/generated/graphql-test.ts': - documents: './packages/frontend-shared/src/gql-components/**/*.{vue,ts,tsx,js,jsx}' - <<: *vueTesting diff --git a/guides/README.md b/guides/README.md index 9264d51e406..ce55622ddae 100644 --- a/guides/README.md +++ b/guides/README.md @@ -17,8 +17,10 @@ For general contributor information, check out [`CONTRIBUTING.md`](../CONTRIBUTI * [Determining the next version of Cypress to be released](./next-version.md) * [E2E Open Mode Testing](./e2e-open-testing.md) * [Error handling](./error-handling.md) +* [GraphQL Subscriptions - Overview and Test Guide](./graphql-subscriptions.md) * [Patching packages](./patch-package.md) * [Release process](./release-process.md) * [Testing other projects](./testing-other-projects.md) * [Testing strategy and style guide (draft)](./testing-strategy-and-styleguide.md) * [Writing cross-platform JavaScript](./writing-cross-platform-javascript.md) +* [Writing the Cypress Changelog](./writing-the-cypress-changelog.md) diff --git a/guides/app-cloud-testing.md b/guides/app-cloud-testing.md new file mode 100644 index 00000000000..dba4043fd4d --- /dev/null +++ b/guides/app-cloud-testing.md @@ -0,0 +1,63 @@ +# App <--> Cloud Testing + +Testing communication between the Cypress App and Cypress Cloud can be complex depending on how much data is going back and forth. For example, we recently completed the Debug page feature which involved writing some e2e tests to verify that the page was functioning correctly with the expected data from Cypress Cloud. In this document, we'll share some of our findings from the process of writing these tests for the Debug page. + +## Working with GraphQL requests + +We use GraphQL to interface with Cypress Cloud, so there are some things to know when writing e2e tests between the App and Cloud. + +### GraphQL requests over fetch + +By default, GraphQL queries use web sockets to fetch data. This doesn't let us intercept these messages and return a mock response using `cy.intercept` to get our app into the desired state for the test. We can set the GraphQL client to use fetch rather than WS by adding this hook at the top of our spec file. + +You can see an example of this in [our spec for the Debug page](/packages/app/cypress/e2e/debug.cy.ts#L4). + +```js +Cypress.on('window:before:load', (win) => { + win.__CYPRESS_GQL_NO_SOCKET__ = 'true' +}) +``` + +Now that the GraphQL requests to Cypress Cloud are happening over fetch, we can use `cy.intercept` to intercept the requests and do whatever we need to do (in most cases, return a JSON fixture as a response to the request). + +Note that this intercepts requests between the App and Server. This means that any logic that we have in the `data-context` layer for this query will not be executed by the test because the query will be intercepted before it runs. + +### Creating JSON fixtures from GraphQL requests + +Another benefit of making our GraphQL requests over fetch is that we can easily see the real responses from Cypress Cloud in the Network tab of our browser developer tools. This is especially useful when we have large responses coming back from Cypress Cloud that we want to mock in our tests. + +We can start our fixture by copying the real response from the dev tools response into our fixture and then modifying individual fields to tweak our app state. Then we can intercept the GraphQL request and return our fixture. + +You can see an example of this in [our spec for the Debug page](/packages/app/cypress/e2e/debug.cy.ts#L35). + +```js +cy.intercept('query-Debug', { + fixture: 'debug-Passing/gql-Debug.json', +}) +``` + +### Intercepting subscriptions and requests from the server + +There are a few situations in which we need to use `cy.remoteGraphQLIntercept` to intercept a request: + +- If the request originates from the server +- If the request is a subscription (these will always be over web sockets, so cy.intercept doesn't work) +- If there is server-side logic in the resolver that we want to cover with our test + +`cy.remoteGraphQLIntercept` intercepts the request at the server level, so we are able to return fixture data here and have the resolver continue on as if this is the data that was returned from Cypress Cloud. + +You can see an example of this in [our spec for the Debug page](/packages/app/cypress/e2e/debug.cy.ts#L23). + +```js +import RelevantRunsDataSource_RunsByCommitShas from '../fixtures/gql-RelevantRunsDataSource_RunsByCommitShas.json' + +beforeEach(() => { + cy.remoteGraphQLIntercept((obj, _testState, options) => { + if (obj.operationName === 'RelevantRunsDataSource_RunsByCommitShas') { + obj.result.data = options.RelevantRunsDataSource_RunsByCommitShas.data + } + + return obj.result + }, { RelevantRunsDataSource_RunsByCommitShas }) +}) +``` \ No newline at end of file diff --git a/guides/building-release-artifacts.md b/guides/building-release-artifacts.md index 544cfa018e2..a8b273e0771 100644 --- a/guides/building-release-artifacts.md +++ b/guides/building-release-artifacts.md @@ -19,7 +19,7 @@ This guide has instructions for building both. Building a new npm package is two commands: 1. Increment the version in the root `package.json` -2. `yarn build --scope cypress` +2. `yarn lerna run build-cli` The steps above: @@ -33,6 +33,14 @@ The steps above: The npm package requires a corresponding binary of the same version. In production, it will try to retrieve the binary from the Cypress CDN if it is not cached locally. -You can build the Cypress binary locally by running `yarn binary-build`. You can use Linux to build the Cypress binary (just like it is in CI) by running `yarn binary-build` inside of `yarn docker`. +You can build the Cypress binary locally by running `yarn binary-build`, then package the binary by running `yarn binary-package`. You can use Linux to build the Cypress binary (just like it is in CI) by running `yarn binary-build` and `yarn binary-package` inside of `yarn docker`. -`yarn binary-zip` can be used to zip the built binary together. \ No newline at end of file +If you're on macOS and building locally, you'll need a code-signing certificate in your keychain, which you can get by following the [instructions on Apple's website](https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html#//apple_ref/doc/uid/TP40005929-CH4-SW30). Also, you'll also most likely want to skip notarization since it requires an Apple Developer Program account - set `SKIP_NOTARIZATION=1` when building locally to do this. [More info about code signing in CI](./code-signing.md). + +`yarn binary-zip` can be used to zip the built binary together. + +### Tips + +If you want to speed up the time it takes to package the binary, set `V8_SNAPSHOT_DISABLE_MINIFY=1` + +If you are on an M1, you need to set `RESET_ADHOC_SIGNATURE=1` in order to be able to actually run the binary after packaging it. diff --git a/guides/code-signing.md b/guides/code-signing.md index db166d937cf..75cb40b39a9 100644 --- a/guides/code-signing.md +++ b/guides/code-signing.md @@ -4,20 +4,37 @@ Code signing is done for the Windows and Mac distributions of Cypress when they `electron-builder` handles code signing during the `create-build-artifacts` jobs. This guide assumes that the reader is already familiar with [`electron-builder`'s Code Signing documentation](https://www.electron.build/code-signing). -## Installing a new Mac code signing key +## Rotating the Mac code signing key -Follow the directions supplied by `electron-builder`: https://www.electron.build/code-signing#travis-appveyor-and-other-ci-servers +1. On a Mac, log in to Xcode using Cypress's Apple developer program identity. +2. Follow Apple's [Create, export, and delete signing certificates](https://help.apple.com/xcode/mac/current/#/dev154b28f09) instructions: + 1. Follow "View signing certificates". + 2. Follow "Create a signing certificate", and choose the type of "Developer ID Application" when prompted. + 3. Follow "Export a signing certificate". Set a strong passphrase when prompted, which will later become `CSC_KEY_PASSWORD`. +3. Upload the exported, encrypted `.p12` file to the [Code Signing folder][code-signing-folder] in Google Drive and obtain a public [direct download link][direct-download]. +4. Within the `test-runner:sign-mac-binary` CircleCI context, set `CSC_LINK` to that direct download URL and set `CSC_KEY_PASSWORD` to the passphrase used to encrypt the `p12` file. -Set the environment variables `CSC_LINK` and `CSC_KEY_PASSWORD` in the `test-runner:sign-mac-binary` CircleCI context. +## Rotating the Windows code signing key -## Installing a new Windows code signing key - -1. Obtain the private key and full certificate chain in ASCII-armored PEM format and store each in a file (`-----BEGIN PRIVATE KEY-----`, `-----BEGIN CERTIFICATE-----`) -2. Using `openssl`, convert the plaintext PEM public and private key to binary PKCS#12/PFX format and encrypt it with a real strong password. +1. Generate a certificate signing request (CSR) file using `openssl`. For example: + ```shell + # generate a new private key + openssl genrsa -out win-code-signing.key 4096 + # create a CSR using the private key + openssl req -new -key win-code-signing.key -out win-code-signing.csr + ``` +2. Obtain a certificate by submitting the CSR to SSL.com using the Cypress SSL.com account. + * If renewing, follow the [renewal instructions](https://www.ssl.com/how-to/renewing-ev-ov-and-iv-certificates/). + * If rotating, contact SSL.com's support to request certificate re-issuance. +3. Obtain the full certificate chain from SSL.com's dashboard in ASCII-armored PEM format and save it as `win-code-signing.crt`. (`-----BEGIN PRIVATE KEY-----`, `-----BEGIN CERTIFICATE-----`) +4. Using `openssl`, convert the plaintext PEM public and private key to binary PKCS#12/PFX format and encrypt it with a strong passphrase, which will later become `CSC_KEY_PASSWORD`. ```shell - ➜ openssl pkcs12 -export -inkey key.pem -in cert.pem -out encrypted.pfx + ➜ openssl pkcs12 -export -inkey win-code-signing.key -in win-code-signing.crt -out encrypted-win-code-signing.pfx Enter Export Password: Verifying - Enter Export Password: ``` -3. Upload the `encrypted.pfx` file to the Cypress App Google Drive and obtain a [direct download link](http://www.syncwithtech.org/p/direct-download-link-generator.html). -4. Within the `test-runner:sign-windows-binary` CircleCI context, set `CSC_LINK` to that URL and `CSC_KEY_PASSWORD` to the password. \ No newline at end of file +5. Upload the `encrypted-win-code-signing.pfx` file to the [Code Signing folder][code-signing-folder] in Google Drive and obtain a public [direct download link][direct-download]. +6. Within the `test-runner:sign-windows-binary` CircleCI context, set `CSC_LINK` to that direct download URL and set `CSC_KEY_PASSWORD` to the passphrase used to encrypt the `pfx` file. + +[direct-download]: https://www.syncwithtech.org/p/direct-download-link-generator.html +[code-signing-folder]: https://drive.google.com/drive/u/1/folders/1CsuoXRDmXvd3ImvFI-sChniAMJBASUW diff --git a/guides/e2e-open-testing.md b/guides/e2e-open-testing.md index ff2e7e36437..fcac1952623 100644 --- a/guides/e2e-open-testing.md +++ b/guides/e2e-open-testing.md @@ -34,6 +34,7 @@ it('should onboard a todos project', () => { it('should open todos in the app', () => { cy.startAppServer() // starts the express server used to run the "app" cy.visitApp() // visits the app page, without launching the browser + cy.specsPageIsVisible() cy.get('[href=#/runs]').click() cy.get('[href=#/settings]').click() }) diff --git a/guides/error-handling.md b/guides/error-handling.md index 3035e33c325..4ae4daf3d02 100644 --- a/guides/error-handling.md +++ b/guides/error-handling.md @@ -68,7 +68,7 @@ CANNOT_TRASH_ASSETS: (arg1: string) => { return errTemplate`\ Warning: We failed to trash the existing run results. - This error will not alter the exit code. + This error will not affect or change the exit code. ${details(arg1)}` }, diff --git a/guides/graphql-subscriptions.md b/guides/graphql-subscriptions.md index 47ba0a66f58..a346d34299a 100644 --- a/guides/graphql-subscriptions.md +++ b/guides/graphql-subscriptions.md @@ -109,7 +109,7 @@ browserStatusChange () { ``` - [API Docs](https://github.com/enisdenjo/graphql-ws/tree/master/docs) -- [Transport layer protcol specification](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) +- [Transport layer protocol specification](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) ### Testing diff --git a/guides/protocol-development.md b/guides/protocol-development.md new file mode 100644 index 00000000000..db5d088935e --- /dev/null +++ b/guides/protocol-development.md @@ -0,0 +1,10 @@ +# Protocol Development + +In production, the capture code used to capture and communicate test data will be retrieved from the Cloud. However, in order to develop the capture code locally, developers will: + +* Clone the `cypress-services` repo + * Run `yarn` + * Run `yarn watch` in `packages/app-capture-protocol` +* Clone the `cypress` repo + * Run `yarn` + * Execute `CYPRESS_LOCAL_PROTOCOL_PATH=path/to/cypress-services/packages/app-capture-protocol/dist/index.js CYPRESS_INTERNAL_ENV=staging yarn cypress:run --record --key --project ` on a project in record mode diff --git a/guides/release-process.md b/guides/release-process.md index 10f52f058f3..2f96cceab66 100644 --- a/guides/release-process.md +++ b/guides/release-process.md @@ -2,7 +2,7 @@ These procedures concern the release process for the Cypress binary and `cypress` npm module. -The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) directory are automatically published to npm (with [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/)) upon being merged into `master`. You can read more about this in [CONTRIBUTING.md](../CONTRIBUTING.md#independent-packages-ci-workflow). +The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) directory are automatically published to npm (with [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/)) upon being merged into `develop`. You can read more about this in [CONTRIBUTING.md](../CONTRIBUTING.md#releases). [Anyone can build the binary and npm package locally](./building-release-artifacts.md), but you can only deploy the Cypress application and publish the npm module `cypress` if you are a member of the `cypress` npm organization. @@ -13,38 +13,43 @@ The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) di - Ensure you have the following permissions set up: - An AWS account with permission to access and write to the AWS S3, i.e. the Cypress CDN. - Permissions for your npm account to publish the `cypress` package. - - Permissions to update releases in ZenHub. - [Set up](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress) an AWS SSO profile with the [Team-CypressApp-Prod](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress#Team-CypressApp-Prod) role. The release scripts assumes the name of your profile is `prod`. Make sure to open the "App Developer" expando for some necessary config values. Your AWS config file should end up looking like the following: ``` - [prod] + [profile prod] sso_start_url = sso_region = - aws_access_key_id = - aws_secret_access_key = - aws_session_token = + sso_account_id = + sso_role_name = + region = + cli_pager = ``` - Set up the following environment variables: - - For the `release-automations` steps, you will need setup the following envs: - - GitHub token - generated yourself in github. - - [ZenHub API token](https://app.zenhub.com/dashboard/tokens) to interact with Zenhub. Found in 1Password. + - For the `release-automations` step, you will need setup the following envs: + - GitHub token - Found in 1Password. - The `cypress-bot` GitHub app credentials. Found in 1Password. ```text GITHUB_TOKEN="..." - ZENHUB_API_TOKEN="..." GITHUB_APP_CYPRESS_INSTALLATION_ID= GITHUB_APP_ID= GITHUB_PRIVATE_KEY= ``` - - For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. + - For purging the Cloudflare cache (needed for the `prepare-release-artifacts` script in step 6), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. ```text CF_ZONEID="..." CF_TOKEN="..." ``` +- Ensure that you have the following repositories checked out locally and ready to contribute to: + - [`cypress-realworld-app`](https://github.com/cypress-io/cypress-realworld-app) + - [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation) + - [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) + - [cypress-io/release-automations][release-automations] + + If you don't have access to 1Password, ask a team member who has done a deploy. Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations. @@ -68,30 +73,36 @@ of Cypress. You can see the progress of the test projects by opening the status In the following instructions, "X.Y.Z" is used to denote the [next version of Cypress being published](./next-version.md). -1. `develop` should contain all of the changes made in `master`. However, this occasionally may not be the case. - - Ensure that `master` does not have any additional commits that are not on `develop`. - - Ensure all auto-generated pull requests designed to merge master into develop have been successfully merged. - - If there are additional commits necessary to merge `master` to `develop`, submit, get approvals on, and merge a new PR - -2. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog. Also ensure that every closed issue in any obsolete releases are moved to the appropriate release in ZehHub. For example, if the open releases are 9.5.5 and 9.6.0, the current release is 9.6.0, then all closed issues marked as 9.5.5 should be moved to 9.6.0. Ensure that there are no commits on `develop` since the last release that are user facing and aren't marked with the current release. - -3. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version. - -4. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/develop-/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-/cypress.tgz`, publishing can proceed. +_Note: It is advisable to notify the team that the `develop` branch is locked down prior to beginning the release process_ -5. Install and test the pre-release version to make sure everything is working. - - Get the pre-release version that matches your system from the latest develop commit. - - Install the new version: `npm install -g ` +1. Install and test the pre-release version to make sure everything is working. See [Install Pre-Release Version docs](https://docs.cypress.io/guides/references/advanced-installation#Install-pre-release-version) for more details. + - Install the new version: + - Globally: `npm install -g ` + - or in a project: `npm i -D cypress@file:` - Run a quick, manual smoke test: - `cypress open` - Go into a project, run a quick test, make sure things look right - Optionally, install the new version into an established project and run the tests there - [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation. - - Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress dashboard repo. + - Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress Cloud repo. -6. Log into AWS SSO with `aws sso login --profile `. If you have setup your credentials under a different profile than `prod`, be sure to set the `AWS_PROFILE` environment variable to that profile name for the remaining steps. For example, if you are using `production` instead of `prod`, do `export AWS_PROFILE=production`. +2. Ensure all changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on) have been merged to `develop` and deployed. -7. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens: +3. Create a Release PR - + Bump, submit, get approvals on, and merge a new PR. This PR should: + - Bump the Cypress `version` in [`package.json`](package.json) + - Bump the [`packages/example`](../packages/example) dependency if there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, and `yarn` to ensure the lockfile is up to date. + - Follow the writing the [Cypress Changelog release steps](./writing-the-cypress-changelog.md#release) to update the [`cli/CHANGELOG.md`](../cli/CHANGELOG.md). + +4. Once the `develop` branch is passing in CI and you have confirmed the `cypress-bot` has commented on the commit with the pre-release versions for `darwin-x64`, `darwin-arm64`, `linux-x64`,`linux-arm64`, and `win32-x64`, publishing can proceed. + Tips for getting a green build: + - If the `windows` workflow is failing with timeout errors, you can retry from the last failed step. + - Sometimes a test can get stuck in a failing state between attempts on the `windows` workflow. In these cases, kicking off a full run of the workflow can help get it into a passing state. + - If the `linux-x64` workflow fails due to a flaky test but percy finalizes the build, you *must* restart the workflow from the failed steps. Restarting the entire workflow after a finalized Percy build can cause Percy to fail the next attempt with a "Build has already been finalized" error, requiring pushing a new commit to start fresh. + +5. Log into AWS SSO with `aws sso login --profile `. If you have setup your credentials under a different profile than `prod`, be sure to set the `AWS_PROFILE` environment variable to that profile name for the remaining steps. For example, if you are using `production` instead of `prod`, do `export AWS_PROFILE=production`. + +6. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens: * the binaries for `` are moved from `beta` to the `desktop` folder for `` in S3 * the Cloudflare cache for this version is purged * the pre-prod `cypress.tgz` NPM package is converted to a stable NPM package ready for release @@ -102,96 +113,88 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy You can pass `--dry-run` to see the commands this would run under the hood. -8. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`. +7. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`. + If you are not already a Cypress package maintainer, contact a team member who is to get you added. -9. Publish the generated npm package under the `dev` tag, using your personal npm account. +8. Publish the generated npm package under the `dev` tag, using your personal npm account. ```shell npm publish /tmp/cypress-prod.tgz --tag dev ``` -10. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output: +9. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output: ```shell dist-tags: dev: 3.4.0 latest: 3.3.2 ``` -11. Test `cypress@X.Y.Z` to make sure everything is working. + **Note**: It may take several minutes for `npm info` to reflect the latest version info. + +10. Test `cypress@X.Y.Z` to make sure everything is working. - Install the new version: `npm install -g cypress@X.Y.Z` - Run a quick, manual smoke test: - `cypress open` - Go into a project, run a quick test, make sure things look right - Install the new version into an established project and run the tests there - [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation. - - Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress dashboard repo. - -12. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. This PR must be merged, built, and deployed before moving to the next step. - - Use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub: - ```shell - cd packages/issues-in-release - yarn do:changelog --release - ``` - - Ensure the changelog is up-to-date and has the correct date. + - Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress Cloud repo. + +11. Review the release-specific documentation and changelog PR in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. + - Copy the changelog content for this version from the release PR above into `/docs/guides/references/changelog.mdx`. Adjust any `docs.cypress.io` links to use host-relative paths. - Merge any release-specific documentation changes into the main release PR. - You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check. -13. Create a PR for a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version. Instructions for updating `cypress-docker-images` can be found [here](https://github.com/cypress-io/cypress-docker-images/blob/master/CONTRIBUTING.md#add-new-included-image). Ensure the docker image is reviewed and has passing tests before preceeding. - +12. Create a new docker image using the new cypress version in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images). Create a PR in which you update [factory/.env](https://github.com/cypress-io/cypress-docker-images/blob/master/factory/.env#L20) to use the new cypress version. Ensure the docker image is reviewed and has passing tests before proceeding. -14. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version: +13. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version: ```shell npm dist-tag add cypress@X.Y.Z ``` -15. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system. +14. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system. ```shell yarn binary-release --version X.Y.Z ``` -16. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on). - -17. Merge the new docker image PR created in step 13 to release the image. - -18. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md). +15. Merge the documentation PR from step 11 and the new docker image PR created in step 12 to release the image. -19. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release): - - Close the current release in ZenHub. - - Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date. - - Move all issues that are still open from the current release to the appropriate future release. - -20. Bump `version` in [`package.json`](package.json), submit, get approvals on, and merge a new PR for the change. After it merges: +16. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md). + - Build `@packages/example` with `yarn workspace @packages/example build` + - Inspect the contents of `./packages/example/build` before deploying, and ensure it looks correct + - Run `yarn workspace @packages/example deploy`. This adds changes from `cypress-example-kitchensink` to a commit in the `gh-pages` branch, which will deploy to production with its own CI. + - Check the deployed site at `https://example.cypress.io` to ensure the new changes deployed correctly. +17. Once the release is complete, create a Github tag off of the release commit which bumped the version: ```shell git checkout develop git pull origin develop git log --pretty=oneline - # copy sha of the previous commit + # copy sha of the version bump commit git tag -a vX.Y.Z -m vX.Y.Z git push origin vX.Y.Z ``` -21. Submit, get approvals on, and merge a new PR that merges `develop` to `master`. **Important**: make sure to use a merge commit, not a squash merge. - -22. Create a new [GitHub release](https://github.com/cypress-io/cypress/releases). Choose the tag you created previously and add contents to match previous releases. +18. Create a new [GitHub release](https://github.com/cypress-io/cypress/releases). Choose the tag you created previously and add contents to match previous releases. -23. Inside of [cypress-io/release-automations][release-automations], run the following to add a comment to each GH issue that has been resolved with the new published version: +19. Add a comment to each GH issue that has been resolved with the new published version. Download the `releaseData.json` artifact from the `verify-release-readiness` CircleCI job and run the following command inside of [cypress-io/release-automations][release-automations]: ```shell - cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z + npm run do:comment -- --release-data ``` -24. Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left +22. Confirm there are no issues from the release with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left. + +23. Notify the team that `develop` is reopen, and post a message to the Releases Slack channel with a link to the changelog. + +24. If utilizing the `SKIP_RELEASE_CHANGELOG_VALIDATION_FOR_BRANCHES` to override and skip changelog validation for this release, change its value as needed or delete it from CircleCI so that subsequent releases and PRs will go through changelog validation. -25. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z` for testing the features or fixes from the newly published version `x.y.z`, update that branch to refer to the newly published NPM version in `package.json`. Then, get the changes approved and merged into that project's `master`. For projects without a `x.y.z` branch, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects: +25. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z` for testing the features or fixes from the newly published version `x.y.z`, update that branch to refer to the newly published NPM version in `package.json`. Then, get the changes approved and merged into that project's main branch. For projects without a `x.y.z` branch, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects: - [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99) - - [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1) - [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app/issues/41) - [cypress-example-recipes](https://github.com/cypress-io/cypress-example-recipes/issues/225) - - [cypress-fiddle](https://github.com/cypress-io/cypress-fiddle/issues/5) - - [cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose) Take a break, you deserve it! 👉😎👉 diff --git a/guides/testing-other-projects.md b/guides/testing-other-projects.md index 41f3516eaa7..4864ecc0d06 100644 --- a/guides/testing-other-projects.md +++ b/guides/testing-other-projects.md @@ -1,6 +1,6 @@ # Testing other projects -In `develop`, `master`, and any other branch configured in [`circle.yml`](../circle.yml), the Cypress binary and npm package are built and uploaded to `cdn.cypress.io`. Then, tests are run, using a variety of real-world example repositories. +In `develop` and any other branch configured in [the CircleCI config](../.circleci/config.yml), the Cypress binary and npm package are built and uploaded to `cdn.cypress.io`. Then, tests are run, using a variety of real-world example repositories. Two main strategies are used to spawn these test projects: @@ -9,7 +9,7 @@ Two main strategies are used to spawn these test projects: ## `test-binary-against-repo` jobs -A number of CI jobs in `circle.yml` clone test projects and run tests as part of `cypress-io/cypress`'s CI pipeline. +A number of CI jobs in `.circleci/config.yml` clone test projects and run tests as part of `cypress-io/cypress`'s CI pipeline. You can find a list of test projects that do this by searching for usage of the `test-binary-against-repo` step. @@ -19,4 +19,4 @@ One advantage to local CI is that it does not require creating commits to anothe ## `binary-system-tests` -System tests in `/system-tests/test-binary` are run against the built Cypress App in CI. For more details, see the [README](../system-tests/README.md). \ No newline at end of file +System tests in `/system-tests/test-binary` are run against the built Cypress App in CI. For more details, see the [README](../system-tests/README.md). diff --git a/guides/v8-snapshots.md b/guides/v8-snapshots.md new file mode 100644 index 00000000000..e80d39bae92 --- /dev/null +++ b/guides/v8-snapshots.md @@ -0,0 +1,69 @@ +# V8 Snapshots + +In order to improve start up time, Cypress uses [electron mksnapshot](https://github.com/electron/mksnapshot) for generating [v8 snapshots](https://v8.dev/blog/custom-startup-snapshots) for both development and production. + +## Snapshot Generation + +At a high level, snapshot generation works by creating a single snapshot JS file out of all Cypress server code. In order to do this some code needs to be translated to be "snapshottable". For specifics on what can and can't be snapshot, see [these requirements](https://github.com/cypress-io/cypress/tree/develop/tooling/v8-snapshot#requirements). The JS file that gets generated then runs through [electron mksnapshot](https://github.com/cypress-io/cypress/tree/develop/tooling/electron-mksnapshot) which creates the actual binary snapshot that can be used to load the entire JS file into memory almost instantaneously. + +Locally, a v8 snapshot is generated in a post install step and set up to only include node modules in the snapshot. In this way, cypress code can be modified without having to regenerate a snapshot. If you do want or need to regenerate the snapshot for development you can run: + +```shell +yarn build-v8-snapshot-dev +``` + +On CI and for binary builds we run: + +```shell +yarn build-v8-snapshot-prod +``` + +which will include both node modules and cypress code in the snapshot. + +## Environment Variables + +* `DISABLE_SNAPSHOT_REQUIRE` - disables snapshot require and the snapshot build process when running `yarn install` +* `V8_SNAPSHOT_DISABLE_MINIFY` - disables the minification process during a V8 snapshot production build. This speeds up the build time greatly. It is useful when building the Cypress binary locally + +## Cache + +Because the V8 snapshot process involves analyzing every file to determine whether it can be properly snapshot or not, this process can be time consuming. In order to make this process faster, there is a cache file that is stored at `tooling/v8-snapshot/cache//snapshot-meta.json`. This file specifies the snapshotability of each file. The snapshot process uses this snapshot meta file as a starting guess as to all of the files snapshot status. It can then detect new problems per file and thus only have to process new or newly problematic files, thus speeding up the overall process. + +This cache should be maintained and updated over time. Rather than having this maintenance be a manual process done by developers, a [github action](https://github.com/cypress-io/cypress/blob/develop/.github/workflows/update_v8_snapshot_cache.yml) is used. It is scheduled to run nightly and issues a PR to develop with any changes to the cache files. Because the iterative snapshot process is good at transitioning files from a healthy status to unhealthy but not the other way, we generate the snapshot from scratch weekly to try and keep the cache as optimal as possible. Generating from scratch is a very lengthy process (on the order of hours) which is why this is only done weekly. + +## Troubleshooting + +### Local Development + +If you're running into problems locally, either with generating the snapshot or at runtime, a good first step is to clean everything and start from scratch. This command will accomplish that (note that it will delete any new unstaged files, so if you want to keep them, either stash them or stage them): + +```shell +git clean -fxd && yarn +``` + +### Generation + +If the build v8 snapshot command is taking a long time to run on Circle CI, the snapshot cache probably needs to be updated. Run the [Update V8 Snapshot Cache](https://github.com/cypress-io/cypress/actions/workflows/update_v8_snapshot_cache.yml) github action against your branch to generate the snapshots for you on all platforms. You can choose to commit directly to your branch or alternatively issue a PR to your branch. + +![Update V8 SnapshotCache](https://user-images.githubusercontent.com/4873279/206541239-1afb1d29-4d66-4593-92a7-5a5961a12137.png) + +If the build v8 snapshot command fails, you can sometimes see which file is causing the problem via the stack trace. Running the build snapshot command with `DEBUG=*snap*` set will give you more information. Sometimes you can narrow the issue down to a specific file. If this is the case, you can try removing that file from the snapshot cache at `tooling/v8-snapshot/cache//snapshot-meta.json`. If this works, check in the changes and the file will get properly updated in the cache during the next automatic update. + +### If a Full Snapshot Rebuild Still Fails + +Occasionally, a full rebuild will still fail. This is typically a sign that there is a problem with the code we are generating, with Electron's `mksnapshot` or even deeper with the pure v8 `mksnapshot`. The first thing to be done is to figure out where the problem actually is. + +To determine if the problem is with the pure v8 `mksnapshot`, [check out](https://v8.dev/docs/source-code), [build](https://v8.dev/docs/build-gn), and run [the v8 `mksnapshot` code](https://github.com/v8/v8/). Then you can run the built `mksnapshot` against the generated snapshot file at `tooling/v8-snapshot/cache/darwin/snapshot.js`. For example on an M1, you would run `tools/dev/gm.py arm64.release` and once that builds, you can run `mksnapshot` via: `out/arm64.release/mksnapshot `. Then, it's just a matter of figuring out where that commit is that introduces the problem. A good strategy is to check out the tag of the release corresponding to the electron version where everything is working and verify it works. Then check out the tag of the release corresponding to the electron version where everything is not working and verify that it is broken. Then use a binary search on the commits in between to find the place where things break. + +To determine if the problem is with Electron's `mksnapshot`, [check out, build](https://www.electronjs.org/docs/latest/development/build-instructions-gn) and run [the electron `mksnapshot` code](https://github.com/electron/electron). Using [Electron's build tools](https://github.com/electron/build-tools) is strongly recommended. An example of what the process looks like would be running `e build mksnapshot` to build the mksnapshot target and then executing `src/out/Release/mksnapshot ` to try and generate the snapshot. A good strategy is to check out the tag of the release corresponding to the electron version where everything is working and verify it works. Then check out the tag of the release corresponding to the electron version where everything is not working and verify that it is broken. Then use a binary search on the commits in between to find the place where things break. If you have determined that the problem is not with the pure v8 `mksnapshot`, then the issue is likely with one of the patches that electron applied on top of v8. + +To determine if the problem is with the code we are generating, run `V8_SNAPSHOT_DISABLE_MINIFY=1 DEBUG=*snap* yarn build-v8-snapshot-prod`. The failure will spell out a command like `node /Users/user1/cypress/tooling/electron-mksnapshot/dist/mksnapshot-bin.js /Users/user1/cypress/tooling/v8-snapshot/cache/darwin/snapshot.js --output_dir /private/var/folders/9z/qzyh61x16tv5y874h_8297m40000gq/T/cy-v8-snapshot-bin`. Running that by itself will then spell out another command like `/private/var/folders/9z/qzyh61x16tv5y874h_8297m40000gq/T/mksnapshot-workdir/mksnapshot /Users/user1/cypress/tooling/v8-snapshot/cache/darwin/snapshot.js --turbo_instruction_scheduling --stress-turbo-late-spilling --target_os=mac --target_arch=arm64 --embedded_src gen/v8/embedded.S --embedded_variant Default --random-seed 314159265 --startup_blob snapshot_blob.bin --no-native-code-counters`. This last command can be used for further iterative troubleshooting. A good approach for troubleshooting involves looking at the snapshot file (`/Users/user1/cypress/tooling/v8-snapshot/cache/darwin/snapshot.js` in the above example) and commenting out various files referenced in the `// tooling/v8-snapshot/cache/darwin/snapshot-entry.js` section. If you can narrow it down to a specific file causing the problem, you can further try and narrow it down to a line of code in that file. If it is a specific line, there are two options: tweak the line in the source code to see if there's a way to cause it not to have a problem or fix the code generation at `https://github.com/cypress-io/esbuild`. + +### Runtime + +If you're experiencing issues during runtime, you can try and narrow down where the problem might be via a few different scenarios: + +* If the problem occurs with the binary, but not in the monorepo, chances are something is being removed during the binary cleanup step that shouldn't be +* If the problem occurs with running `yarn build-v8-snapshot-prod` but not `yarn build-v8-snapshot-dev`, then that means there's a problem with a cypress file and not a node module dependency. Chances are that a file is not being flagged properly (e.g. healthy when it should be deferred or norewrite). +* If the problem occurs with both `yarn build-v8-snapshot-prod` and `yarn build-v8-snapshot-dev` but does not occur when using the `DISABLE_SNAPSHOT_REQUIRE` environment variable, then that means there's a problem with a node module dependency. Chances are that a file is not being flagged properly (e.g. healthy when it should be deferred or norewrite). +* If the problem still occurs when using the `DISABLE_SNAPSHOT_REQUIRE` environment variable, then that means the problem is not snapshot related. diff --git a/guides/writing-cross-platform-javascript.md b/guides/writing-cross-platform-javascript.md index a1058090344..8148bc7d12f 100644 --- a/guides/writing-cross-platform-javascript.md +++ b/guides/writing-cross-platform-javascript.md @@ -7,7 +7,7 @@ Cypress works on Linux, macOS and Windows. This includes both installing from np Throughout the code base, we access the file system in various ways, and need to be conscious of how we do so to ensure Cypress can be used and developed seamlessly on multiple platforms. One thing to keep in mind is file paths and file separators. macOS and Linux systems use `/`, and Windows uses `\`. -As a general rule, we want to use **native paths** where possible. There are a few reasons for this. Whereever we display a file path, we want to use the native file separator, since that is what the user will expect on their platform. In general, we can use the Node.js `path` module to handle this: +As a general rule, we want to use **native paths** where possible. There are a few reasons for this. Wherever we display a file path, we want to use the native file separator, since that is what the user will expect on their platform. In general, we can use the Node.js `path` module to handle this: ```js // on linux-like systems @@ -40,7 +40,7 @@ path.resolve('../', '/../', '../') // 'C:\\Users' on Windows ``` -In general, you want to avoid writing file system code using `/` and `\`, and use Node.js APIs where possible - those are cross platform and guarenteed to work. +In general, you want to avoid writing file system code using `/` and `\`, and use Node.js APIs where possible - those are cross platform and guaranteed to work. ## Use Node.js Scripts diff --git a/guides/writing-the-cypress-changelog.md b/guides/writing-the-cypress-changelog.md new file mode 100644 index 00000000000..f80b2af5cf3 --- /dev/null +++ b/guides/writing-the-cypress-changelog.md @@ -0,0 +1,61 @@ +# Cypress App - Managing the Release Changelog + +Cypress prefers hand tailored release notes over auto generated release notes, primarily, user experience is highly valued at Cypress. åWhile Cypress is a dependency installed via a package manager, the changelog should be more akin to other desktop products like [VS Code](https://code.visualstudio.com/updates/v1_62) or [Notion](https://www.notion.so/What-s-New-157765353f2c4705bd45474e5ba8b46c). + +## When to Add an Entry + +The changelog should include anything that was merged into the develop branch of the cypress repo that are user affecting changes. These include: +- `breaking` - A breaking change that will require a MVB +- `dependency` - A change to a dependency that impact the user +- `deprecation` - A API deprecation notice for users +- `feat` - A new feature +- `fix` - A bug fix or regression fix. +- `misc` - a misc user-facing change, like a UI update which is not a fix or enhancement to how Cypress works +- `perf` - A code change that improves performance + +## Writing Guidelines +1. The changelog is formatted like the following. If there is not a pending changelog for the next release, add these sections. +```md +## + +_Released (PENDING)_ + +** +``` +2. Each changelog entry is written and merged with the associated user-facing code change in [`cli/CHANGELOG.md`](../cli/CHANGELOG.md). +3. The changelog entry should be added the associated change section. The supported change sections for the changelog (that should be listed in the order below) are: + + | change type (by order of impact) | change section | details | + | -- | -- | --| + | -- | Summary | A description of the overall changes. This is usually only provided for **breaking changes** or **large features**. This should be written in coordination with Cypress's marketing and match the language used around the release. It may also link to relevant blogs. [Example](https://docs.cypress.io/guides/references/changelog#7-0-0) | + | `breaking` | Breaking Changes | Link to the Migration Guide (if any) at the beginning of this section. For each one explain the change, how it affects them, and how the can mitigate the effects of the change (unless it's covered in the Migration Guide). [Example](https://docs.cypress.io/guides/references/changelog#6-0-0) | + | `deprecation` | Deprecations | Explain each deprecation and that it will be removed in a future release. [Example](https://docs.cypress.io/guides/references/changelog#6-0-0) | + | `perf` | Performance | [Example](https://docs.cypress.io/guides/references/changelog#7-2-0) | + | `feat` | Features | [Example](https://docs.cypress.io/guides/references/changelog#8-6-0) | + | `fix` | Bugfixes | [Example](https://docs.cypress.io/guides/references/changelog#9-1-0) | + | `misc` | Misc | We don't use this section as much as we used to, but perhaps there was a change that is not necessarily a feature or a bugfix, it would go here. (Like the design of the browser picker changed). [Example](https://docs.cypress.io/guides/references/changelog#6-7-0) | + | `dependency` | Dependency Updates | A list of dependencies that were updated, downgraded, or removed as well as the version it was changed from. [Example](https://docs.cypress.io/guides/references/changelog#7-2-0) | +4. You may have several changes around a feature that make sense to group. Feel free to do so to make more sense to users consuming the changelog. [Example](https://docs.cypress.io/guides/references/changelog#8-7-0) +5. Do not refer to 'we' when writing a changelog item. We want to phrase the changelog in a way that emphasizes how the user is impacted. Additionally 'we' may not have addressed the issue, an outside contributor may have. + - _Example:_ Instead of 'We fixed a situation where a cross-origin errors could incorrectly throw in Chrome' write 'Cross-origin errors will no longer incorrectly throw in Chrome in certain situations'. +6. Be as direct as possible in explaining the changes, but with enough clarity that the user understands the full impact. Users should *never* have to click on the link to the issue/PR to understand the change that happened and *absolutely never* have to look at the code to understand the change. If you cannot yourself understand the change from the Changelog entry, add more context. +7. Order the changelog items in order of impact. The most impactful features/bugfixes should be ordered first. +8. If a changelog item is a regression, the description should start with `Fixed a regression in [9.1.0](#9-1-0)` with a link to the release that introduced it. +9. For each changelog item, there should be a link to the issue(s) it addresses (or the PR that it was addressed in if there is no corresponding issue(s)). See phrasing below + * For bugfixes: + > Fixes [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234)) + 11. For other issues: "Addresses [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234))" + 12. When no issues, but PR: "Addressed in [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234))" + 13. When multiple issues: "Fixes [#12]([https://github.com/cypress-io/cypress/issues/12](https://github.com/cypress-io/cypress/issues/1234)), [#13]([https://github.com/cypress-io/cypress/issues/13](https://github.com/cypress-io/cypress/issues/1234)) and [#14]([https://github.com/cypress-io/cypress/issues/14](https://github.com/cypress-io/cypress/issues/1234))." + +## Release + +At the time of the release, the releaser will: +- remove the `(PENDING)` verbiage next to the perspective release date and adjust the date if needed +- ensure the Changelog is coherent +- ensure the change sections are in the correct order +- ensure that the entries are ordered by impact + +Each Cypress release results in a new changelog file being added to the [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation) repository to be published on the doc site. [Example pull request](https://github.com/cypress-io/cypress-documentation/pull/4141) adding changelog to the repository. \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json index 5f354257acc..8313f340090 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -10,6 +10,7 @@ "**/build/**", "**/dist/**", "**/dist-test/**", - "**/.cy/**" + "**/.cy/**", + "**/v8-snapshot/cache/**" ] } diff --git a/lerna.json b/lerna.json index e541ef2d738..a1cac42c579 100644 --- a/lerna.json +++ b/lerna.json @@ -4,8 +4,11 @@ "cli", "packages/*", "npm/*", - "system-tests" + "tooling/*", + "system-tests", + "scripts" ], "useWorkspaces": true, + "useNx": true, "version": "0.0.0" } diff --git a/npm/README.md b/npm/README.md index 44d7a303b9f..c9fdf732229 100644 --- a/npm/README.md +++ b/npm/README.md @@ -2,5 +2,5 @@ This directory contains packages that are both used internally inside the Cypress monorepo [`packages`](../packages) and also published independently on npm under the Cypress organization using the `@cypress` prefix. For example, `vite-dev-server` is published as `@cypress/vite-dev-server`. -These are automatically released based on [Semantic Version](https://semver.org) commit message prefixes (`feat`, `chore` etc). A package is automatically released when changes are merged into master. You can read more about this process in [`CONTRIBUTING`](../CONTRIBUTING.md#committing-code). +These are automatically released based on [Semantic Version](https://semver.org) commit message prefixes (`feat`, `chore` etc). A package is automatically released when changes are merged into `develop`. You can read more about this process in [`CONTRIBUTING`](../CONTRIBUTING.md#committing-code). diff --git a/npm/angular-signals/.eslintignore b/npm/angular-signals/.eslintignore new file mode 100644 index 00000000000..79afe972da7 --- /dev/null +++ b/npm/angular-signals/.eslintignore @@ -0,0 +1,5 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json +**/cypress/fixtures \ No newline at end of file diff --git a/npm/angular-signals/.eslintrc b/npm/angular-signals/.eslintrc new file mode 100644 index 00000000000..f044f320923 --- /dev/null +++ b/npm/angular-signals/.eslintrc @@ -0,0 +1,8 @@ +{ + "plugins": [ + "cypress" + ], + "extends": [ + "plugin:@cypress/dev/tests" + ] +} diff --git a/npm/angular-signals/.npmignore b/npm/angular-signals/.npmignore new file mode 100644 index 00000000000..d4372c984ed --- /dev/null +++ b/npm/angular-signals/.npmignore @@ -0,0 +1,3 @@ +examples +src +cypress \ No newline at end of file diff --git a/npm/angular-signals/.releaserc.js b/npm/angular-signals/.releaserc.js new file mode 100644 index 00000000000..17d3bb87147 --- /dev/null +++ b/npm/angular-signals/.releaserc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('../../.releaserc'), +} diff --git a/npm/angular-signals/CHANGELOG.md b/npm/angular-signals/CHANGELOG.md new file mode 100644 index 00000000000..e7244aa851c --- /dev/null +++ b/npm/angular-signals/CHANGELOG.md @@ -0,0 +1,6 @@ +# @cypress/angular-signals-v1.0.0 (2024-07-02) + + +### Features + +* add Angular Signals CT Harness for Angular 17.2 and up for users to be able to use Angular Signals within their component tests ([#29621](https://github.com/cypress-io/cypress/issues/29621)) ([f2554f1](https://github.com/cypress-io/cypress/commit/f2554f12d6d1f438db898fbbc10a100ebff733ce)) diff --git a/npm/angular-signals/README.md b/npm/angular-signals/README.md new file mode 100644 index 00000000000..bbe66b417b2 --- /dev/null +++ b/npm/angular-signals/README.md @@ -0,0 +1,11 @@ +# @cypress/angular-signals + +Mount Angular components in the open source [Cypress.io](https://www.cypress.io/) test runner. This package is an extension of `@cypress/angular`, but with [signals](https://angular.dev/guide/signals) support. + +> **Note:** This package is bundled with the `cypress` package and should not need to be installed separately. See the [Angular Component Testing Docs](https://docs.cypress.io/guides/component-testing/angular/overview) for mounting Angular components. Installing and importing `mount` from `@cypress/angular-signals` should only be done for advanced use-cases. + +## Development + +Run `yarn build` to compile and sync packages to the `cypress` cli package. + +## [Changelog](./CHANGELOG.md) diff --git a/npm/angular-signals/package.json b/npm/angular-signals/package.json new file mode 100644 index 00000000000..104c1f5b21b --- /dev/null +++ b/npm/angular-signals/package.json @@ -0,0 +1,74 @@ +{ + "name": "@cypress/angular-signals", + "version": "0.0.0-development", + "description": "Test Angular Components using Signals with Cypress", + "main": "dist/index.js", + "scripts": { + "prebuild": "rimraf dist", + "build": "rollup -c rollup.config.mjs", + "postbuild": "node ../../scripts/sync-exported-npm-with-cli.js", + "check-ts": "tsc --noEmit", + "dev": "rollup -c rollup.config.mjs -w", + "lint": "eslint --ext .js,.ts,.json, ." + }, + "dependencies": {}, + "devDependencies": { + "@angular/common": "^17.2.0", + "@angular/core": "^17.2.0", + "@angular/platform-browser-dynamic": "^17.2.0", + "@cypress/mount-utils": "0.0.0-development", + "typescript": "~5.4.5", + "zone.js": "~0.14.6" + }, + "peerDependencies": { + "@angular/common": ">=17.2", + "@angular/core": ">=17.2", + "@angular/platform-browser-dynamic": ">=17.2", + "rxjs": ">=7.5.0", + "zone.js": ">=0.13.0" + }, + "files": [ + "dist" + ], + "types": "dist/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cypress-io/cypress.git" + }, + "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/angular-signals/#readme", + "bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fangular&template=1-bug-report.md&title=", + "keywords": [ + "angular", + "cypress", + "cypress-io", + "test", + "testing" + ], + "contributors": [ + { + "name": "Bill Glesias", + "social": "@atofstryker" + } + ], + "module": "dist/index.js", + "publishConfig": { + "access": "public" + }, + "nx": { + "targets": { + "build": { + "outputs": [ + "{workspaceRoot}/cli/angular-signals" + ] + } + } + }, + "standard": { + "globals": [ + "Cypress", + "cy", + "expect" + ] + } +} diff --git a/npm/angular-signals/rollup.config.mjs b/npm/angular-signals/rollup.config.mjs new file mode 100644 index 00000000000..4ef324cfd1a --- /dev/null +++ b/npm/angular-signals/rollup.config.mjs @@ -0,0 +1,14 @@ +import { createEntries } from '@cypress/mount-utils/create-rollup-entry.mjs' + +const config = { + external: [ + '@angular/core', + '@angular/core/testing', + '@angular/common', + '@angular/platform-browser-dynamic/testing', + 'zone.js', + 'zone.js/testing', + ], +} + +export default createEntries({ formats: ['es'], input: 'src/index.ts', config }) diff --git a/npm/angular-signals/src/index.ts b/npm/angular-signals/src/index.ts new file mode 100644 index 00000000000..1af962e1d53 --- /dev/null +++ b/npm/angular-signals/src/index.ts @@ -0,0 +1 @@ +export * from './mount' diff --git a/npm/angular-signals/src/mount.ts b/npm/angular-signals/src/mount.ts new file mode 100644 index 00000000000..1c039f2970b --- /dev/null +++ b/npm/angular-signals/src/mount.ts @@ -0,0 +1,554 @@ +import 'zone.js' + +/** + * @hack fixes "Mocha has already been patched with Zone" error. + */ +// @ts-ignore +window.Mocha['__zone_patch__'] = false +import 'zone.js/testing' + +import { CommonModule } from '@angular/common' +import { Component, ErrorHandler, EventEmitter, Injectable, SimpleChange, SimpleChanges, Type, OnChanges, Injector, InputSignal, WritableSignal, signal } from '@angular/core' +import { toObservable } from '@angular/core/rxjs-interop' +import { + ComponentFixture, + getTestBed, + TestModuleMetadata, + TestBed, + TestComponentRenderer, +} from '@angular/core/testing' +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing' +import { + setupHooks, + getContainerEl, +} from '@cypress/mount-utils' +import type { Subscription } from 'rxjs' + +/** + * Additional module configurations needed while mounting the component, like + * providers, declarations, imports and even component @Inputs() + * + * @interface MountConfig + * @see https://angular.io/api/core/testing/TestModuleMetadata + */ +export interface MountConfig extends TestModuleMetadata { + /** + * @memberof MountConfig + * @description flag to automatically create a cy.spy() for every component @Output() property + * @example + * export class ButtonComponent { + * @Output clicked = new EventEmitter() + * } + * + * cy.mount(ButtonComponent, { autoSpyOutputs: true }) + * cy.get('@clickedSpy).should('have.been.called') + */ + autoSpyOutputs?: boolean + + /** + * @memberof MountConfig + * @description flag defaulted to true to automatically detect changes in your components + */ + autoDetectChanges?: boolean + /** + * @memberof MountConfig + * @example + * import { ButtonComponent } from 'button/button.component' + * it('renders a button with Save text', () => { + * cy.mount(ButtonComponent, { componentProperties: { text: 'Save' }}) + * cy.get('button').contains('Save') + * }) + * + * it('renders a button with a cy.spy() replacing EventEmitter', () => { + * cy.mount(ButtonComponent, { + * componentProperties: { + * clicked: cy.spy().as('mySpy) + * } + * }) + * cy.get('button').click() + * cy.get('@mySpy').should('have.been.called') + * }) + */ + // allow InputSignals to be type primitive and WritableSignal for type compliance + componentProperties?: Partial<{ [P in keyof T]: T[P] extends InputSignal ? InputSignal | WritableSignal | V : T[P]}> +} + +let activeFixture: ComponentFixture | null = null +let activeInternalSubscriptions: Subscription[] = [] + +function cleanup () { + // Not public, we need to call this to remove the last component from the DOM + try { + (getTestBed() as any).tearDownTestingModule() + } catch (e) { + const notSupportedError = new Error(`Failed to teardown component. The version of Angular you are using may not be officially supported.`) + + ;(notSupportedError as any).docsUrl = 'https://on.cypress.io/component-framework-configuration' + throw notSupportedError + } + + // clean up internal subscriptions if any exist. We use this for two-way data binding for + // signal() models + activeInternalSubscriptions.forEach((subscription) => { + subscription.unsubscribe() + }) + + getTestBed().resetTestingModule() + activeFixture = null + activeInternalSubscriptions = [] +} + +/** + * Type that the `mount` function returns + * @type MountResponse + */ +export type MountResponse = { + /** + * Fixture for debugging and testing a component. + * + * @memberof MountResponse + * @see https://angular.io/api/core/testing/ComponentFixture + */ + fixture: ComponentFixture + + /** + * The instance of the root component class + * + * @memberof MountResponse + * @see https://angular.io/api/core/testing/ComponentFixture#componentInstance + */ + component: T +}; + +// 'zone.js/testing' is not properly aliasing `it.skip` but it does provide `xit`/`xspecify` +// Written up under https://github.com/angular/angular/issues/46297 but is not seeing movement +// so we'll patch here pending a fix in that library +// @ts-ignore Ignore so that way we can bypass semantic error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. +globalThis.it.skip = globalThis.xit + +@Injectable() +class CypressAngularErrorHandler implements ErrorHandler { + handleError (error: Error): void { + throw error + } +} + +/** + * Bootstraps the TestModuleMetaData passed to the TestBed + * + * @param {Type} component Angular component being mounted + * @param {MountConfig} config TestBed configuration passed into the mount function + * @returns {MountConfig} MountConfig + */ +function bootstrapModule ( + component: Type, + config: MountConfig, +): MountConfig { + const { componentProperties, ...testModuleMetaData } = config + + if (!testModuleMetaData.declarations) { + testModuleMetaData.declarations = [] + } + + if (!testModuleMetaData.imports) { + testModuleMetaData.imports = [] + } + + if (!testModuleMetaData.providers) { + testModuleMetaData.providers = [] + } + + // Replace default error handler since it will swallow uncaught exceptions. + // We want these to be uncaught so Cypress catches it and fails the test + testModuleMetaData.providers.push({ + provide: ErrorHandler, + useClass: CypressAngularErrorHandler, + }) + + // check if the component is a standalone component + if ((component as any).ɵcmp?.standalone) { + testModuleMetaData.imports.push(component) + } else { + testModuleMetaData.declarations.push(component) + } + + if (!testModuleMetaData.imports.includes(CommonModule)) { + testModuleMetaData.imports.push(CommonModule) + } + + return testModuleMetaData +} + +@Injectable() +export class CypressTestComponentRenderer extends TestComponentRenderer { + override insertRootElement (rootElId: string) { + this.removeAllRootElements() + + const rootElement = getContainerEl() + + rootElement.setAttribute('id', rootElId) + } + + override removeAllRootElements () { + getContainerEl().innerHTML = '' + } +} + +/** + * Initializes the TestBed + * + * @param {Type | string} component Angular component being mounted or its template + * @param {MountConfig} config TestBed configuration passed into the mount function + * @returns {Type} componentFixture + */ +function initTestBed ( + component: Type | string, + config: MountConfig, +): Type { + const componentFixture = createComponentFixture(component) as Type + + getTestBed().configureTestingModule({ + ...bootstrapModule(componentFixture, config), + }) + + getTestBed().overrideProvider(TestComponentRenderer, { useValue: new CypressTestComponentRenderer() }) + + return componentFixture +} + +@Component({ selector: 'cy-wrapper-component', template: '' }) +class WrapperComponent { } + +/** + * Returns the Component if Type or creates a WrapperComponent + * + * @param {Type | string} component The component you want to create a fixture of + * @returns {Type | WrapperComponent} + */ +function createComponentFixture ( + component: Type | string, +): Type { + if (typeof component === 'string') { + // getTestBed().overrideTemplate is available in v14+ + // The static TestBed.overrideTemplate is available across versions + TestBed.overrideTemplate(WrapperComponent, component) + + return WrapperComponent + } + + return component +} + +/** + * Creates the ComponentFixture + * + * @param {Type} component Angular component being mounted + * @param {MountConfig} config MountConfig + + * @returns {ComponentFixture} ComponentFixture + */ +function setupFixture ( + component: Type, + config: MountConfig, +): ComponentFixture { + const fixture = getTestBed().createComponent(component) + + setupComponent(config, fixture) + + fixture.whenStable().then(() => { + fixture.autoDetectChanges(config.autoDetectChanges ?? true) + }) + + return fixture +} + +// Best known way to currently detect whether or not a function is a signal is if the signal symbol exists. +// From there, we can take our best guess based on what exists on the object itself. +// @see https://github.com/cypress-io/cypress/issues/29731. +function isSignal (prop: any): boolean { + try { + const symbol = Object.getOwnPropertySymbols(prop).find((symbol) => symbol.toString() === 'Symbol(SIGNAL)') + + return !!symbol + } catch (e) { + // likely a primitive type, object, array, or something else (i.e. not a signal). + // We can return false here. + return false + } +} + +// currently not a great way to detect if a function is an InputSignal. +// @see https://github.com/cypress-io/cypress/issues/29731. +function isInputSignal (prop: any): boolean { + return isSignal(prop) && typeof prop === 'function' && prop['name'] === 'inputValueFn' +} + +// currently not a great way to detect if a function is a Model Signal. +// @see https://github.com/cypress-io/cypress/issues/29731. +function isModelSignal (prop: any): boolean { + return isSignal(prop) && isWritableSignal(prop) && typeof prop.subscribe === 'function' +} + +// currently not a great way to detect if a function is a Writable Signal. +// @see https://github.com/cypress-io/cypress/issues/29731. +function isWritableSignal (prop: any): boolean { + return isSignal(prop) && typeof prop === 'function' && typeof prop.set === 'function' +} + +function convertPropertyToSignalIfApplicable (propValue: any, componentValue: any, injector: Injector) { + const isComponentValueAnInputSignal = isInputSignal(componentValue) + const isComponentValueAModelSignal = isModelSignal(componentValue) + let convertedValueIfApplicable = propValue + + // If the component has the property defined as an InputSignal, we need to detect whether a non signal value or not was passed into the component as a prop + // and attempt to merge the value in correctly. + // We don't want to expose the primitive created signal as it should really be one-way binding from within the component. + // However, to make CT testing easier, a user can technically pass in a signal to an input component and assert on the signal itself and pass in updates + // down to the component as 1 way binding is supported by the test harness + if (isComponentValueAnInputSignal) { + const isPassedInValueNotASignal = !isSignal(propValue) + + if (isPassedInValueNotASignal) { + // Input signals require an injection context to set initial values. + // Because of this, we cannot create them outside the scope of the component. + // Options for input signals also don't allow the passing of an injection contexts, so in order to work around this, + // we convert the non signal input passed into the input to a writable signal + convertedValueIfApplicable = signal(propValue) + } + + // If the component has the property defined as a ModelSignal, we need to detect whether a signal value or not was passed into the component as a prop. + // If a non signal property is passed into the component model (primitive, object, array, etc), we need to set the model to that value and propagate changes of that model through the output spy. + // Since the non signal type likely lives outside the context of Angular, the non signal type will NOT be updated outside of this context. Instead, the output spy will allow you + // to see this change. + // If the value passed into the property is in fact a signal, we need to set up two-way binding between the signals to make sure changes from one propagate to the other. + } else if (isComponentValueAModelSignal) { + const isPassedInValueLikelyARegularSignal = isWritableSignal(propValue) + + // if the value passed into the component is a signal, set up two-way binding + if (isPassedInValueLikelyARegularSignal) { + // update the passed in value with the models updates + componentValue.subscribe((value: any) => { + propValue.set(value) + }) + + // update the model signal with the properties updates + const convertedToObservable = toObservable(propValue, { + injector, + }) + + // push the subscription into an array to be cleaned up at the end of the test + // to prevent a memory leak + activeInternalSubscriptions.push( + convertedToObservable.subscribe((value) => { + componentValue.set(value) + }), + ) + } else { + // it's a non signal type, set it as we only need to handle updating the model signal and emit changes on this through the output spy. + componentValue.set(propValue) + + convertedValueIfApplicable = componentValue + } + } + + return convertedValueIfApplicable +} + +// In the case of signals, if we need to create an output spy, we need to check first whether or not a user has one defined first or has it created through +// autoSpyOutputs. If so, we need to subscribe to the writable signal to push updates into the event emitter. We do NOT observe input signals and output spies will not +// work for input signals. +function detectAndRegisterOutputSpyToSignal (config: MountConfig, component: { [key: string]: any } & Partial, key: string, injector: Injector): void { + if (config.componentProperties) { + const expectedChangeKey = `${key}Change` + let changeKeyIfExists = !!Object.keys(config.componentProperties).find((componentKey) => componentKey === expectedChangeKey) + + // since spies do NOT make change handlers by default, similar to the Output() decorator, we need to create the spy and subscribe to the signal + if (!changeKeyIfExists && config.autoSpyOutputs) { + component[expectedChangeKey] = createOutputSpy(`${expectedChangeKey}Spy`) + changeKeyIfExists = true + } + + if (changeKeyIfExists) { + const componentValue = component[key] + + // if the user passed in a change key or we created one due to config.autoSpyOutputs being set to true for a given signal, + // we will create a subscriber that will emit an event every time the value inside the signal changes. We only do this + // if the signal is writable and not an input signal. + if (isWritableSignal(componentValue) && !isInputSignal(componentValue)) { + toObservable(componentValue, { + injector, + }).subscribe((value) => { + component[expectedChangeKey]?.emit(value) + }) + } + } + } +} + +/** + * Gets the componentInstance and Object.assigns any componentProperties() passed in the MountConfig + * + * @param {MountConfig} config TestBed configuration passed into the mount function + * @param {ComponentFixture} fixture Fixture for debugging and testing a component. + * @returns {T} Component being mounted + */ +function setupComponent ( + config: MountConfig, + fixture: ComponentFixture, +): void { + let component = fixture.componentInstance as unknown as { [key: string]: any } & Partial + const injector = fixture.componentRef.injector + + if (config?.componentProperties) { + // convert primitives to signals if passed in type is a primitive but expected type is signal + // a bit of magic. need to move to another function + Object.keys(component).forEach((key) => { + // only assign props if they are passed into the component + if (config?.componentProperties?.hasOwnProperty(key)) { + // @ts-expect-error + const passedInValue = config?.componentProperties[key] + + const componentValue = component[key] + + // @ts-expect-error + config.componentProperties[key] = convertPropertyToSignalIfApplicable(passedInValue, componentValue, injector) + detectAndRegisterOutputSpyToSignal(config, component, key, injector) + } + }) + + component = Object.assign(component, config.componentProperties) + } + + if (config.autoSpyOutputs) { + Object.keys(component).forEach((key) => { + const property = component[key] + + if (property instanceof EventEmitter) { + component[key] = createOutputSpy(`${key}Spy`) + } + }) + } + + // Manually call ngOnChanges when mounting components using the class syntax. + // This is necessary because we are assigning input values to the class directly + // on mount and therefore the ngOnChanges() lifecycle is not triggered. + if (component.ngOnChanges && config.componentProperties) { + const { componentProperties } = config + + const simpleChanges: SimpleChanges = Object.entries(componentProperties).reduce((acc, [key, value]) => { + acc[key] = new SimpleChange(null, value, true) + + return acc + }, {} as {[key: string]: SimpleChange}) + + if (Object.keys(componentProperties).length > 0) { + component.ngOnChanges(simpleChanges) + } + } +} + +/** + * Mounts an Angular component inside Cypress browser + * + * @param component Angular component being mounted or its template + * @param config configuration used to configure the TestBed + * @example + * import { mount } from '@cypress/angular-signals' + * import { StepperComponent } from './stepper.component' + * import { MyService } from 'services/my.service' + * import { SharedModule } from 'shared/shared.module'; + * it('mounts', () => { + * mount(StepperComponent, { + * providers: [MyService], + * imports: [SharedModule] + * }) + * cy.get('[data-cy=increment]').click() + * cy.get('[data-cy=counter]').should('have.text', '1') + * }) + * + * // or + * + * it('mounts with template', () => { + * mount('', { + * declarations: [StepperComponent], + * }) + * }) + * + * @see {@link https://on.cypress.io/mounting-angular} for more details. + * + * @returns A component and component fixture + */ +export function mount ( + component: Type | string, + config: MountConfig = { }, +): Cypress.Chainable> { + // Remove last mounted component if cy.mount is called more than once in a test + if (activeFixture) { + cleanup() + } + + const componentFixture = initTestBed(component, config) + + activeFixture = setupFixture(componentFixture, config) + + const mountResponse: MountResponse = { + fixture: activeFixture, + component: activeFixture.componentInstance, + } + + const logMessage = typeof component === 'string' ? 'Component' : componentFixture.name + + Cypress.log({ + name: 'mount', + message: logMessage, + consoleProps: () => ({ result: mountResponse }), + }) + + return cy.wrap(mountResponse, { log: false }) +} + +/** + * Creates a new Event Emitter and then spies on it's `emit` method + * + * @param {string} alias name you want to use for your cy.spy() alias + * @returns EventEmitter + * @example + * import { StepperComponent } from './stepper.component' + * import { mount, createOutputSpy } from '@cypress/angular-signals' + * + * it('Has spy', () => { + * mount(StepperComponent, { componentProperties: { change: createOutputSpy('changeSpy') } }) + * cy.get('[data-cy=increment]').click() + * cy.get('@changeSpy').should('have.been.called') + * }) + * + * // Or for use with Angular Signals following the output nomenclature. + * // see https://v17.angular.io/guide/model-inputs#differences-between-model-and-input/ + * + * it('Has spy', () => { + * mount(StepperComponent, { componentProperties: { count: signal(0), countChange: createOutputSpy('countChange') } }) + * cy.get('[data-cy=increment]').click() + * cy.get('@countChange').should('have.been.called') + * }) + */ +export const createOutputSpy = (alias: string) => { + const emitter = new EventEmitter() + + cy.spy(emitter, 'emit').as(alias) + + return emitter as any +} + +// Only needs to run once, we reset before each test +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false }, + }, +) + +setupHooks(cleanup) diff --git a/npm/angular-signals/tsconfig.json b/npm/angular-signals/tsconfig.json new file mode 100644 index 00000000000..a73e01dcecc --- /dev/null +++ b/npm/angular-signals/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "target": "es2020", + "module": "es2020", + "skipLibCheck": true, + "lib": [ + "ESNext", + "DOM" + ], + "allowJs": true, + "declaration": true, + "outDir": "dist", + "strict": true, + "baseUrl": "./", + "types": [ + "cypress" + ], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "moduleResolution": "node", + "noPropertyAccessFromIndexSignature": true, + }, + "include": ["src/**/*.*"], + "exclude": ["src/**/*-spec.*"] +} diff --git a/npm/angular/.eslintignore b/npm/angular/.eslintignore new file mode 100644 index 00000000000..79afe972da7 --- /dev/null +++ b/npm/angular/.eslintignore @@ -0,0 +1,5 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json +**/cypress/fixtures \ No newline at end of file diff --git a/npm/angular/.eslintrc b/npm/angular/.eslintrc index 220212842e1..f044f320923 100644 --- a/npm/angular/.eslintrc +++ b/npm/angular/.eslintrc @@ -4,14 +4,5 @@ ], "extends": [ "plugin:@cypress/dev/tests" - ], - "env": { - "cypress/globals": true - }, - "rules": { - "mocha/no-global-tests": "off", - "no-unused-vars": "off", - "no-console": "off", - "@typescript-eslint/no-unused-vars": "off" - } + ] } diff --git a/npm/angular/.releaserc.js b/npm/angular/.releaserc.js index 7b15992ed70..17d3bb87147 100644 --- a/npm/angular/.releaserc.js +++ b/npm/angular/.releaserc.js @@ -1,6 +1,3 @@ module.exports = { - ...require('../../.releaserc.base'), - branches: [ - { name: 'master', channel: 'latest' }, - ], + ...require('../../.releaserc'), } diff --git a/npm/angular/CHANGELOG.md b/npm/angular/CHANGELOG.md index ef3b3e8a8d7..d10a8ce28ad 100644 --- a/npm/angular/CHANGELOG.md +++ b/npm/angular/CHANGELOG.md @@ -1,3 +1,104 @@ +# [@cypress/angular-v2.1.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.4...@cypress/angular-v2.1.0) (2024-07-02) + + +### Features + +* add Angular Signals CT Harness for Angular 17.2 and up for users to be able to use Angular Signals within their component tests ([#29621](https://github.com/cypress-io/cypress/issues/29621)) ([f2554f1](https://github.com/cypress-io/cypress/commit/f2554f12d6d1f438db898fbbc10a100ebff733ce)) + +# [@cypress/angular-v2.0.4](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.3...@cypress/angular-v2.0.4) (2024-06-07) + + +### Bug Fixes + +* update cypress to Typescript 5 ([#29568](https://github.com/cypress-io/cypress/issues/29568)) ([f3b6766](https://github.com/cypress-io/cypress/commit/f3b67666a5db0438594339c379cf27e1fd1e4abc)) + +# [@cypress/angular-v2.0.3](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.2...@cypress/angular-v2.0.3) (2023-03-14) + + +### Bug Fixes + +* **angular:** mount cy-root in original location ([#25965](https://github.com/cypress-io/cypress/issues/25965)) ([e674f43](https://github.com/cypress-io/cypress/commit/e674f43040ad95d2745601aa0f36eab4703c837e)) + +# [@cypress/angular-v2.0.2](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.1...@cypress/angular-v2.0.2) (2023-02-17) + + +### Bug Fixes + +* mount component in [data-cy-root] ([#25807](https://github.com/cypress-io/cypress/issues/25807)) ([104eef5](https://github.com/cypress-io/cypress/commit/104eef5dfb4b619a748e7ddd59534eadb1044ae7)) + +# [@cypress/angular-v2.0.1](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.0...@cypress/angular-v2.0.1) (2022-11-08) + + +### Bug Fixes + +* make component derived info not throw ([#24571](https://github.com/cypress-io/cypress/issues/24571)) ([838dd4f](https://github.com/cypress-io/cypress/commit/838dd4fa2e0ec56633d0af2faf10a47d190b5594)) + +# [@cypress/angular-v2.0.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.1.2...@cypress/angular-v2.0.0) (2022-11-07) + + +### Bug Fixes + +* possibility to override global services in Angular component tests ([#24394](https://github.com/cypress-io/cypress/issues/24394)) ([54d2853](https://github.com/cypress-io/cypress/commit/54d285321723450920e0f1d50374c4bd0590e72a)) +* remove last mounted component upon subsequent mount calls ([#24470](https://github.com/cypress-io/cypress/issues/24470)) ([f39eb1c](https://github.com/cypress-io/cypress/commit/f39eb1c19e0923bda7ae263168fc6448da942d54)) + + +### BREAKING CHANGES + +* remove last mounted component upon subsequent mount calls of mount + +# [@cypress/angular-v1.1.2](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.1.1...@cypress/angular-v1.1.2) (2022-10-11) + + +### Bug Fixes + +* angular and nuxt ct tests now fail on uncaught exceptions ([#24122](https://github.com/cypress-io/cypress/issues/24122)) ([53eef4f](https://github.com/cypress-io/cypress/commit/53eef4fbd7e1caf32f0183cadbc0e4cf05524c34)) + +# [@cypress/angular-v1.1.1](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.1.0...@cypress/angular-v1.1.1) (2022-10-04) + + +### Bug Fixes + +* **angular:** call ngOnChanges after mount ([#23596](https://github.com/cypress-io/cypress/issues/23596)) ([670d438](https://github.com/cypress-io/cypress/commit/670d43830947c3ea93ef9fdc9c90932a817eb453)) + +# [@cypress/angular-v1.1.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.0.0...@cypress/angular-v1.1.0) (2022-09-28) + + +### Bug Fixes + +* angular 14.2 mount compilation error ([#23593](https://github.com/cypress-io/cypress/issues/23593)) ([2f337db](https://github.com/cypress-io/cypress/commit/2f337dbfa2bb212754c8fa82e3f4548a2f3a07a4)) +* Fix missing `it.skip` function in Angular tests ([#23829](https://github.com/cypress-io/cypress/issues/23829)) ([64c0f45](https://github.com/cypress-io/cypress/commit/64c0f45182456bd43f4b64b2311e816dde615236)) + + +### Features + +* adding svelte component testing support ([#23553](https://github.com/cypress-io/cypress/issues/23553)) ([f6eaad4](https://github.com/cypress-io/cypress/commit/f6eaad40e1836fa9db87c60defa5ae6f390c8fd8)) + +# [@cypress/angular-v1.1.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.0.0...@cypress/angular-v1.1.0) (2022-09-27) + + +### Bug Fixes + +* angular 14.2 mount compilation error ([#23593](https://github.com/cypress-io/cypress/issues/23593)) ([2f337db](https://github.com/cypress-io/cypress/commit/2f337dbfa2bb212754c8fa82e3f4548a2f3a07a4)) +* Fix missing `it.skip` function in Angular tests ([#23829](https://github.com/cypress-io/cypress/issues/23829)) ([64c0f45](https://github.com/cypress-io/cypress/commit/64c0f45182456bd43f4b64b2311e816dde615236)) + + +### Features + +* adding svelte component testing support ([#23553](https://github.com/cypress-io/cypress/issues/23553)) ([f6eaad4](https://github.com/cypress-io/cypress/commit/f6eaad40e1836fa9db87c60defa5ae6f390c8fd8)) + +# [@cypress/angular-v1.1.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.0.0...@cypress/angular-v1.1.0) (2022-09-23) + + +### Bug Fixes + +* angular 14.2 mount compilation error ([#23593](https://github.com/cypress-io/cypress/issues/23593)) ([2f337db](https://github.com/cypress-io/cypress/commit/2f337dbfa2bb212754c8fa82e3f4548a2f3a07a4)) +* Fix missing `it.skip` function in Angular tests ([#23829](https://github.com/cypress-io/cypress/issues/23829)) ([64c0f45](https://github.com/cypress-io/cypress/commit/64c0f45182456bd43f4b64b2311e816dde615236)) + + +### Features + +* adding svelte component testing support ([#23553](https://github.com/cypress-io/cypress/issues/23553)) ([f6eaad4](https://github.com/cypress-io/cypress/commit/f6eaad40e1836fa9db87c60defa5ae6f390c8fd8)) + # [@cypress/angular-v1.1.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.0.0...@cypress/angular-v1.1.0) (2022-08-30) diff --git a/npm/angular/README.md b/npm/angular/README.md index ef4d366a443..2e75bfe830b 100644 --- a/npm/angular/README.md +++ b/npm/angular/README.md @@ -1,85 +1,10 @@ # @cypress/angular -Mount Angular components in the open source [Cypress.io](https://www.cypress.io/) test runner **v7.0.0+** - -> **Note:** This package is bundled with the `cypress` package and should not need to be installed separately. See the [Angular Component Testing Docs](https://docs.cypress.io/guides/component-testing/quickstart-angular#Configuring-Component-Testing) for mounting Angular components. Installing and importing `mount` from `@cypress/angular` should only be used for advanced use-cases. - -## Install - -- Requires Cypress v7.0.0 or later -- Requires [Node](https://nodejs.org/en/) version 12 or above - -```sh -npm install --save-dev @cypress/angular -``` - -## Run - -Open cypress test runner -``` -npx cypress open --component -``` - -If you need to run test in CI -``` -npx cypress run --component -``` - -For more information, please check the official docs for [running Cypress](https://on.cypress.io/guides/getting-started/opening-the-app#Quick-Configuration) and for [component testing](https://on.cypress.io/guides/component-testing/writing-your-first-component-test). - -## API - -- `mount` is the most important function, allows to mount a given Angular component as a mini web application and interact with it using Cypress commands -- `MountConfig` Configuration used to configure your test -- `createOutputSpy` factory function that creates new EventEmitter for your component and spies on it's `emit` method. - -## Examples - -```ts -import { mount } from '@cypress/angular' -import { HelloWorldComponent } from './hello-world.component' - -describe('HelloWorldComponent', () => { - it('works', () => { - mount(HelloWorldComponent) - // now use standard Cypress commands - cy.contains('Hello World!').should('be.visible') - }) -}) -``` - -```ts -import { mount } from '@cypress/angular' -import { HelloWorldComponent } from './hello-world.component' - -describe('HelloWorldComponent', () => { - it('works', () => { - mount('', { - declarations: [HelloWorldComponent] - }) - // now use standard Cypress commands - cy.contains('Hello World!').should('be.visible') - }) -}) -``` - -Look at the examples in [cypress-component-testing-apps](https://github.com/cypress-io/cypress-component-testing-apps) repo. Here in the `angular` and `angular-standalone` folders are the two example applications showing various testing scenarios. - - -## Compatibility - -| @cypress/angular | cypress | -| -------------- | ------- | -| >= v1 | >= v10.5 | +Mount Angular components in the open source [Cypress.io](https://www.cypress.io/) test runner +> **Note:** This package is bundled with the `cypress` package and should not need to be installed separately. See the [Angular Component Testing Docs](https://docs.cypress.io/guides/component-testing/angular/overview) for mounting Angular components. Installing and importing `mount` from `@cypress/angular` should only be done for advanced use-cases. ## Development Run `yarn build` to compile and sync packages to the `cypress` cli package. -## License - -[![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cypress-io/cypress/blob/master/LICENSE) - -This project is licensed under the terms of the [MIT license](/LICENSE). - ## [Changelog](./CHANGELOG.md) diff --git a/npm/angular/package.json b/npm/angular/package.json index 44d04e3989e..6917b8c3035 100644 --- a/npm/angular/package.json +++ b/npm/angular/package.json @@ -7,8 +7,8 @@ "prebuild": "rimraf dist", "build": "rollup -c rollup.config.mjs", "postbuild": "node ../../scripts/sync-exported-npm-with-cli.js", - "build-prod": "yarn build", - "check-ts": "tsc --noEmit" + "check-ts": "tsc --noEmit", + "lint": "eslint --ext .js,.ts,.json, ." }, "dependencies": {}, "devDependencies": { @@ -16,7 +16,7 @@ "@angular/core": "^14.2.0", "@angular/platform-browser-dynamic": "^14.2.0", "@cypress/mount-utils": "0.0.0-development", - "typescript": "^4.7.4", + "typescript": "~5.4.5", "zone.js": "~0.11.4" }, "peerDependencies": { @@ -34,8 +34,7 @@ "type": "git", "url": "https://github.com/cypress-io/cypress.git" }, - "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/angular/#readme", - "author": "Jordan Powell", + "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/angular/#readme", "bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fangular&template=1-bug-report.md&title=", "keywords": [ "angular", @@ -58,6 +57,15 @@ "publishConfig": { "access": "public" }, + "nx": { + "targets": { + "build": { + "outputs": [ + "{workspaceRoot}/cli/angular" + ] + } + } + }, "standard": { "globals": [ "Cypress", diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index bd449e52acb..0c923456a1d 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -8,12 +8,13 @@ window.Mocha['__zone_patch__'] = false import 'zone.js/testing' import { CommonModule } from '@angular/common' -import { Component, EventEmitter, Type } from '@angular/core' +import { Component, ErrorHandler, EventEmitter, Injectable, SimpleChange, SimpleChanges, Type, OnChanges } from '@angular/core' import { ComponentFixture, getTestBed, TestModuleMetadata, TestBed, + TestComponentRenderer, } from '@angular/core/testing' import { BrowserDynamicTestingModule, @@ -21,13 +22,13 @@ import { } from '@angular/platform-browser-dynamic/testing' import { setupHooks, + getContainerEl, } from '@cypress/mount-utils' /** * Additional module configurations needed while mounting the component, like * providers, declarations, imports and even component @Inputs() * - * * @interface MountConfig * @see https://angular.io/api/core/testing/TestModuleMetadata */ @@ -72,6 +73,23 @@ export interface MountConfig extends TestModuleMetadata { componentProperties?: Partial<{ [P in keyof T]: T[P] }> } +let activeFixture: ComponentFixture | null = null + +function cleanup () { + // Not public, we need to call this to remove the last component from the DOM + try { + (getTestBed() as any).tearDownTestingModule() + } catch (e) { + const notSupportedError = new Error(`Failed to teardown component. The version of Angular you are using may not be officially supported.`) + + ;(notSupportedError as any).docsUrl = 'https://on.cypress.io/component-framework-configuration' + throw notSupportedError + } + + getTestBed().resetTestingModule() + activeFixture = null +} + /** * Type that the `mount` function returns * @type MountResponse @@ -94,6 +112,19 @@ export type MountResponse = { component: T }; +// 'zone.js/testing' is not properly aliasing `it.skip` but it does provide `xit`/`xspecify` +// Written up under https://github.com/angular/angular/issues/46297 but is not seeing movement +// so we'll patch here pending a fix in that library +// @ts-ignore Ignore so that way we can bypass semantic error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. +globalThis.it.skip = globalThis.xit + +@Injectable() +class CypressAngularErrorHandler implements ErrorHandler { + handleError (error: Error): void { + throw error + } +} + /** * Bootstraps the TestModuleMetaData passed to the TestBed * @@ -115,8 +146,19 @@ function bootstrapModule ( testModuleMetaData.imports = [] } + if (!testModuleMetaData.providers) { + testModuleMetaData.providers = [] + } + + // Replace default error handler since it will swallow uncaught exceptions. + // We want these to be uncaught so Cypress catches it and fails the test + testModuleMetaData.providers.push({ + provide: ErrorHandler, + useClass: CypressAngularErrorHandler, + }) + // check if the component is a standalone component - if ((component as any).ɵcmp.standalone) { + if ((component as any).ɵcmp?.standalone) { testModuleMetaData.imports.push(component) } else { testModuleMetaData.declarations.push(component) @@ -129,6 +171,21 @@ function bootstrapModule ( return testModuleMetaData } +@Injectable() +export class CypressTestComponentRenderer extends TestComponentRenderer { + override insertRootElement (rootElId: string) { + this.removeAllRootElements() + + const rootElement = getContainerEl() + + rootElement.setAttribute('id', rootElId) + } + + override removeAllRootElements () { + getContainerEl().innerHTML = '' + } +} + /** * Initializes the TestBed * @@ -140,21 +197,13 @@ function initTestBed ( component: Type | string, config: MountConfig, ): Type { - const { providers, ...configRest } = config - const componentFixture = createComponentFixture(component) as Type getTestBed().configureTestingModule({ - ...bootstrapModule(componentFixture, configRest), + ...bootstrapModule(componentFixture, config), }) - if (providers != null) { - getTestBed().overrideComponent(componentFixture, { - add: { - providers, - }, - }) - } + getTestBed().overrideProvider(TestComponentRenderer, { useValue: new CypressTestComponentRenderer() }) return componentFixture } @@ -196,6 +245,8 @@ function setupFixture ( ): ComponentFixture { const fixture = getTestBed().createComponent(component) + setupComponent(config, fixture) + fixture.whenStable().then(() => { fixture.autoDetectChanges(config.autoDetectChanges ?? true) }) @@ -213,15 +264,15 @@ function setupFixture ( function setupComponent ( config: MountConfig, fixture: ComponentFixture, -): T { - let component: T = fixture.componentInstance +): void { + let component = fixture.componentInstance as unknown as { [key: string]: any } & Partial if (config?.componentProperties) { component = Object.assign(component, config.componentProperties) } if (config.autoSpyOutputs) { - Object.keys(component).forEach((key: string, index: number, keys: string[]) => { + Object.keys(component).forEach((key) => { const property = component[key] if (property instanceof EventEmitter) { @@ -230,49 +281,71 @@ function setupComponent ( }) } - return component + // Manually call ngOnChanges when mounting components using the class syntax. + // This is necessary because we are assigning input values to the class directly + // on mount and therefore the ngOnChanges() lifecycle is not triggered. + if (component.ngOnChanges && config.componentProperties) { + const { componentProperties } = config + + const simpleChanges: SimpleChanges = Object.entries(componentProperties).reduce((acc, [key, value]) => { + acc[key] = new SimpleChange(null, value, true) + + return acc + }, {} as {[key: string]: SimpleChange}) + + if (Object.keys(componentProperties).length > 0) { + component.ngOnChanges(simpleChanges) + } + } } /** * Mounts an Angular component inside Cypress browser * - * @param {Type | string} component Angular component being mounted or its template - * @param {MountConfig} config configuration used to configure the TestBed + * @param component Angular component being mounted or its template + * @param config configuration used to configure the TestBed * @example - * import { HelloWorldComponent } from 'hello-world/hello-world.component' + * import { mount } from '@cypress/angular' + * import { StepperComponent } from './stepper.component' * import { MyService } from 'services/my.service' * import { SharedModule } from 'shared/shared.module'; - * import { mount } from '@cypress/angular' - * it('can mount', () => { - * mount(HelloWorldComponent, { - * providers: [MyService], - * imports: [SharedModule] - * }) - * cy.get('h1').contains('Hello World') + * it('mounts', () => { + * mount(StepperComponent, { + * providers: [MyService], + * imports: [SharedModule] + * }) + * cy.get('[data-cy=increment]').click() + * cy.get('[data-cy=counter]').should('have.text', '1') * }) * - * or + * // or * - * it('can mount with template', () => { - * mount('', { - * declarations: [HelloWorldComponent], - * providers: [MyService], - * imports: [SharedModule] - * }) + * it('mounts with template', () => { + * mount('', { + * declarations: [StepperComponent], + * }) * }) - * @returns Cypress.Chainable> + * + * @see {@link https://on.cypress.io/mounting-angular} for more details. + * + * @returns A component and component fixture */ export function mount ( component: Type | string, config: MountConfig = { }, ): Cypress.Chainable> { + // Remove last mounted component if cy.mount is called more than once in a test + if (activeFixture) { + cleanup() + } + const componentFixture = initTestBed(component, config) - const fixture = setupFixture(componentFixture, config) - const componentInstance = setupComponent(config, fixture) + + activeFixture = setupFixture(componentFixture, config) const mountResponse: MountResponse = { - fixture, - component: componentInstance, + fixture: activeFixture, + component: activeFixture.componentInstance, } const logMessage = typeof component === 'string' ? 'Component' : componentFixture.name @@ -291,6 +364,15 @@ export function mount ( * * @param {string} alias name you want to use for your cy.spy() alias * @returns EventEmitter + * @example + * import { StepperComponent } from './stepper.component' + * import { mount, createOutputSpy } from '@cypress/angular' + * + * it('Has spy', () => { + * mount(StepperComponent, { componentProperties: { change: createOutputSpy('changeSpy') } }) + * cy.get('[data-cy=increment]').click() + * cy.get('@changeSpy').should('have.been.called') + * }) */ export const createOutputSpy = (alias: string) => { const emitter = new EventEmitter() @@ -309,8 +391,4 @@ getTestBed().initTestEnvironment( }, ) -setupHooks(() => { - // Not public, we need to call this to remove the last component from the DOM - getTestBed()['tearDownTestingModule']() - getTestBed().resetTestingModule() -}) +setupHooks(cleanup) diff --git a/npm/angular/tsconfig.json b/npm/angular/tsconfig.json index b21ac64dace..a73e01dcecc 100644 --- a/npm/angular/tsconfig.json +++ b/npm/angular/tsconfig.json @@ -11,15 +11,15 @@ "allowJs": true, "declaration": true, "outDir": "dist", - "strict": false, - "noImplicitAny": false, + "strict": true, "baseUrl": "./", "types": [ "cypress" ], "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "moduleResolution": "node" + "moduleResolution": "node", + "noPropertyAccessFromIndexSignature": true, }, "include": ["src/**/*.*"], "exclude": ["src/**/*-spec.*"] diff --git a/npm/create-cypress-tests/.eslintrc b/npm/create-cypress-tests/.eslintrc deleted file mode 100644 index 08df16eaf0d..00000000000 --- a/npm/create-cypress-tests/.eslintrc +++ /dev/null @@ -1,38 +0,0 @@ -{ - "plugins": [ - "cypress", - "@cypress/dev" - ], - "extends": [ - "plugin:@cypress/dev/general", - "plugin:@cypress/dev/tests" - ], - "parser": "@typescript-eslint/parser", - "env": { - "cypress/globals": true - }, - "rules": { - "no-console": "off", - "mocha/no-global-tests": "off", - "@typescript-eslint/no-unused-vars": "off" - }, - "overrides": [ - { - "files": [ - "lib/*" - ], - "rules": { - "no-console": 1 - } - }, - { - "files": [ - "**/*.json" - ], - "rules": { - "quotes": "off", - "comma-dangle": "off" - } - } - ] -} diff --git a/npm/create-cypress-tests/.npmignore b/npm/create-cypress-tests/.npmignore deleted file mode 100644 index c9cfa44194a..00000000000 --- a/npm/create-cypress-tests/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -./src/ -./initial-template/ -scripts/ -__snapshots__/ \ No newline at end of file diff --git a/npm/create-cypress-tests/CHANGELOG.md b/npm/create-cypress-tests/CHANGELOG.md deleted file mode 100644 index adb76ba6db2..00000000000 --- a/npm/create-cypress-tests/CHANGELOG.md +++ /dev/null @@ -1,105 +0,0 @@ -# [create-cypress-tests-v2.0.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.3.0...create-cypress-tests-v2.0.0) (2022-06-13) - - -### Bug Fixes - -* scope config to current testing type ([#20677](https://github.com/cypress-io/cypress/issues/20677)) ([61f7cfc](https://github.com/cypress-io/cypress/commit/61f7cfc59284a2938e0a1c15d74ee75215ba5f8b)) -* support using create-cypress-tests as part of build process ([#18714](https://github.com/cypress-io/cypress/issues/18714)) ([0501452](https://github.com/cypress-io/cypress/commit/0501452fb9e2df954ee871171052ab9f01367b25)) -* **unified-desktop-gui branch:** initial installation on windows ([#18247](https://github.com/cypress-io/cypress/issues/18247)) ([8614e97](https://github.com/cypress-io/cypress/commit/8614e978029bcbf7155b7ae98ac54feb11f2e7f3)) - - -### chore - -* prep npm packages for use with Cypress v10 ([b924d08](https://github.com/cypress-io/cypress/commit/b924d086ee2e2ccc93303731e001b2c9e9d0af17)) - - -### Features - -* Add vue2 package from npm/vue/v2 branch ([#21026](https://github.com/cypress-io/cypress/issues/21026)) ([3aa69e2](https://github.com/cypress-io/cypress/commit/3aa69e2538aae5702bfc48789c54f37263ce08fc)) -* Deprecate run-ct / open-ct, and update all examples to use --ct instead ([#18422](https://github.com/cypress-io/cypress/issues/18422)) ([196e8f6](https://github.com/cypress-io/cypress/commit/196e8f62cc6d27974f235945cb5700624b3dae41)) -* remove testFiles reference ([#20565](https://github.com/cypress-io/cypress/issues/20565)) ([5670344](https://github.com/cypress-io/cypress/commit/567034459089d9d53dfab5556cb9369fb335c3db)) -* update on-links ([#19235](https://github.com/cypress-io/cypress/issues/19235)) ([cc2d734](https://github.com/cypress-io/cypress/commit/cc2d7348185e2a090c60d92d9319ab460d8c7827)) -* Use .config files ([#18578](https://github.com/cypress-io/cypress/issues/18578)) ([081dd19](https://github.com/cypress-io/cypress/commit/081dd19cc6da3da229a7af9c84f62730c85a5cd6)) -* use supportFile by testingType ([#19364](https://github.com/cypress-io/cypress/issues/19364)) ([0366d4f](https://github.com/cypress-io/cypress/commit/0366d4fa8971e5e5189c6fd6450cc3c8d72dcfe1)) - - -### BREAKING CHANGES - -* new version of packages for Cypress v10 - -# [create-cypress-tests-v1.3.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.2.0...create-cypress-tests-v1.3.0) (2021-12-16) - - -### Bug Fixes - -* Restore broken gif ([#18987](https://github.com/cypress-io/cypress/issues/18987)) ([f251681](https://github.com/cypress-io/cypress/commit/f251681b814b102ca374abdef148b777c4e72c67)) - - -### Features - -* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88)) - -# [create-cypress-tests-v1.2.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.3...create-cypress-tests-v1.2.0) (2021-11-10) - - -### Features - -* **deps:** update dependency electron to v15 🌟 ([#18317](https://github.com/cypress-io/cypress/issues/18317)) ([3095d73](https://github.com/cypress-io/cypress/commit/3095d733e92527ffd67344c6899211e058ceefa3)) - -# [create-cypress-tests-v1.1.3](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.2...create-cypress-tests-v1.1.3) (2021-10-29) - - -### Bug Fixes - -* revive type checker ([#18172](https://github.com/cypress-io/cypress/issues/18172)) ([af472b6](https://github.com/cypress-io/cypress/commit/af472b6419ecb2aec1abdb09df99b2fa5f56e033)) - -# [create-cypress-tests-v1.1.2](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.1...create-cypress-tests-v1.1.2) (2021-06-17) - - -### Bug Fixes - -* case issue create cypress tests with `react/plugins/load-webpack` ([#16961](https://github.com/cypress-io/cypress/issues/16961)) ([c37ecea](https://github.com/cypress-io/cypress/commit/c37ecea3ca462015637515b331d1c9828ac1ed29)), closes [#16960](https://github.com/cypress-io/cypress/issues/16960) - -# [create-cypress-tests-v1.1.1](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.0...create-cypress-tests-v1.1.1) (2021-05-10) - - -### Bug Fixes - -* add return config for vitejs templates ([69d9de5](https://github.com/cypress-io/cypress/commit/69d9de581a03dce8e3535917a4cdcea8fa4eb6e9)) -* add return config for vueCli and vueWebpack ([9c12ee6](https://github.com/cypress-io/cypress/commit/9c12ee6d8467c65414ab2d413a9c45b2bbec64e9)) -* remove all of rollup, not supported anymore ([f8a71e7](https://github.com/cypress-io/cypress/commit/f8a71e75ae8208dc628d342cb1054c12f98338e9)) -* typo in the final message (run vs run-ct) ([294db04](https://github.com/cypress-io/cypress/commit/294db04f042dba86b69bb15d847c80a2c4202e80)) -* vueCli and webpack key vue@2 fix when guessing ([89f1bb9](https://github.com/cypress-io/cypress/commit/89f1bb9bc6bd987fbf6679a9d955c3587e69aa61)) - -# [create-cypress-tests-v1.1.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.0.1...create-cypress-tests-v1.1.0) (2021-04-05) - - -### Bug Fixes - -* **component-testing:** Fix webpack-dev-server deps validation crash ([#15708](https://github.com/cypress-io/cypress/issues/15708)) ([254eb47](https://github.com/cypress-io/cypress/commit/254eb47d91c75a9f56162e7493ab83e5be169935)) - - -### Features - -* support ct/e2e specific overrides in cypress.json ([#15526](https://github.com/cypress-io/cypress/issues/15526)) ([43c8ae2](https://github.com/cypress-io/cypress/commit/43c8ae2a7c20ba70a0bb0b45b8f6a086e2782f29)) - -# [create-cypress-tests-v1.0.1](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.0.0...create-cypress-tests-v1.0.1) (2021-03-16) - - -### Bug Fixes - -* add missing script for building wizard ([#15502](https://github.com/cypress-io/cypress/issues/15502)) ([393a8ca](https://github.com/cypress-io/cypress/commit/393a8ca9cac905e0f6d8623bff889b041dd076b6)) - -# create-cypress-tests-v1.0.0 (2021-03-15) - - -### Bug Fixes - -* **runner-ct:** open link in external browser ([#15420](https://github.com/cypress-io/cypress/issues/15420)) ([d291157](https://github.com/cypress-io/cypress/commit/d291157f07ffebe961527fdd85c7ec51056801e7)) - - -### Features - -* **@cypress/react:** Make correct plugins for different adapters/bundlers ([#15337](https://github.com/cypress-io/cypress/issues/15337)) ([fc30118](https://github.com/cypress-io/cypress/commit/fc301182523f0a645bfb17ea3b541644b9732dd0)), closes [#9116](https://github.com/cypress-io/cypress/issues/9116) -* create-cypress-tests installation wizard ([#9563](https://github.com/cypress-io/cypress/issues/9563)) ([c405ee8](https://github.com/cypress-io/cypress/commit/c405ee89ef5321df6151fdeec1e917ac952c0d38)), closes [#9116](https://github.com/cypress-io/cypress/issues/9116) -* create-cypress-tests wizard ([#8857](https://github.com/cypress-io/cypress/issues/8857)) ([21ee591](https://github.com/cypress-io/cypress/commit/21ee591d1e9c4083a0c67f2062ced92708c0cedd)) diff --git a/npm/create-cypress-tests/README.md b/npm/create-cypress-tests/README.md deleted file mode 100644 index b962f78c990..00000000000 --- a/npm/create-cypress-tests/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Create Cypress Tests - -Installs and injects all the required configuration to run cypress tests. - -## Quick overview - -``` -cd my-app -npx create-cypress-tests -npx cypress open -``` - -![demo](./demo.gif) - -## Package manager - -This wizard will automatically determine which package do you use. If `yarn` available as global dependency it will use yarn to install dependencies and create lock file. - -If you need to use `npm` over `yarn` you can do the following - -``` -npx create-cypress-tests --use-npm -``` - -By the way you can use yarn to run the installation wizard 😉 - -``` -yarn create cypress-tests -``` - -## Typescript - -This package will also automatically determine if typescript if available in this project and inject the required typescript configuration for cypress. If you are starting a new project and want to create typescript configuration, please do the following: - -``` -npm init -npm install typescript -npx create-cypress-tests -``` - -## Configuration - -Here is a list of available configuration options: - -`--use-npm` – use npm if yarn available -`--ignore-typescript` – will not create typescript configuration if available -`--ignore-examples` – will create a 1 empty spec file (`cypress/integration/spec.js`) to start with -`--component-tests` – will not ask should setup component testing or not - -## License - -The project is licensed under the terms of [MIT license](../../LICENSE) - -## Changelog - -[Changelog](./CHANGELOG.md) diff --git a/npm/create-cypress-tests/__snapshots__/babel.test.ts.js b/npm/create-cypress-tests/__snapshots__/babel.test.ts.js deleted file mode 100644 index 182ba10a2fb..00000000000 --- a/npm/create-cypress-tests/__snapshots__/babel.test.ts.js +++ /dev/null @@ -1,13 +0,0 @@ -exports['babel installation template correctly generates plugins config 1'] = ` -const injectDevServer = require('@cypress/react/plugins/babel'); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js deleted file mode 100644 index f1ff8aa8b36..00000000000 --- a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js +++ /dev/null @@ -1,43 +0,0 @@ -exports['injects guessed next.js template cypress.config.ts'] = ` -export default { - specPattern: "src/**/*.spec.{js,ts,jsx,tsx}" -}; - -` - -exports['injects guessed next.js template plugins/index.js'] = ` -const injectDevServer = require("@cypress/react/plugins/next"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; - -` - -exports['Injected overridden webpack template cypress.config.ts'] = ` -export default { - specPattern: "cypress/component/**/*.spec.{js,ts,jsx,tsx}" -}; - -` - -exports['Injected overridden webpack template plugins/index.js'] = ` -const injectDevServer = require("@cypress/react/plugins/react-scripts"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; - -` - -exports['Injected overridden webpack template support/component.js'] = ` -import "./commands.js"; -` diff --git a/npm/create-cypress-tests/__snapshots__/next.test.ts.js b/npm/create-cypress-tests/__snapshots__/next.test.ts.js deleted file mode 100644 index 5d8602de1bb..00000000000 --- a/npm/create-cypress-tests/__snapshots__/next.test.ts.js +++ /dev/null @@ -1,13 +0,0 @@ -exports['next.js install template correctly generates plugins config 1'] = ` -const injectDevServer = require('@cypress/react/plugins/next'); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js b/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js deleted file mode 100644 index b960b4be6f6..00000000000 --- a/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js +++ /dev/null @@ -1,13 +0,0 @@ -exports['create-react-app install template correctly generates plugins config 1'] = ` -const injectDevServer = require('@cypress/react/plugins/react-scripts'); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js deleted file mode 100644 index 5e36dd80347..00000000000 --- a/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js +++ /dev/null @@ -1,32 +0,0 @@ -exports['webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` -const injectDevServer = require("@cypress/react/plugins/load-webpack"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config, { - // TODO replace with valid webpack config path - webpackFilename: './webpack.config.js' - }); - } - - return config; // IMPORTANT to return a config -}; -` - -exports['webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` -const injectDevServer = require("@cypress/react/plugins/load-webpack"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config, { - webpackFilename: 'config/webpack.config.js' - }); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/vite.test.ts.js b/npm/create-cypress-tests/__snapshots__/vite.test.ts.js deleted file mode 100644 index 5fc7bc31a4f..00000000000 --- a/npm/create-cypress-tests/__snapshots__/vite.test.ts.js +++ /dev/null @@ -1,17 +0,0 @@ -exports['vue: vite template correctly generates plugins config 1'] = ` -const { - startDevServer -} = require("@cypress/vite-dev-server"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on("dev-server:start", async options => startDevServer({ - options - })); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js deleted file mode 100644 index fa70c0ef2db..00000000000 --- a/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js +++ /dev/null @@ -1,20 +0,0 @@ -exports['vue webpack-file install template correctly generates plugins for vue-cli-service 1'] = ` -const { - startDevServer -} = require("@cypress/webpack-dev-server"); - -const webpackConfig = require("@vue/cli-service/webpack.config.js"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js deleted file mode 100644 index b686b719276..00000000000 --- a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js +++ /dev/null @@ -1,42 +0,0 @@ -exports['vue webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` -const { - startDevServer -} = require("@cypress/webpack-dev-server"); - -const webpackConfig = require("./webpack.config.js"); // TODO replace with valid webpack config path - - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } - - return config; // IMPORTANT to return a config -}; -` - -exports['vue webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` -const { - startDevServer -} = require("@cypress/webpack-dev-server"); - -const webpackConfig = require("build/webpack.config.js"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js deleted file mode 100644 index f384e4b912a..00000000000 --- a/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js +++ /dev/null @@ -1,40 +0,0 @@ -exports['webpack-options template correctly generates plugins config 1'] = ` -const path = require("path"); - -const { - startDevServer -} = require("@cypress/webpack-dev-Server"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - /** @type import("webpack").Configuration */ - const webpackConfig = { - resolve: { - extensions: ['.js', '.ts', '.jsx', '.tsx'] - }, - mode: 'development', - devtool: false, - output: { - publicPath: '/', - chunkFilename: '[name].bundle.js' - }, - // TODO: update with valid configuration for your components - module: { - rules: [{ - test: /\\.(js|jsx|mjs|ts|tsx)$/, - loader: 'babel-loader', - options: { - cacheDirectory: path.resolve(__dirname, '.babel-cache') - } - }] - } - }; - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } -}; -` diff --git a/npm/create-cypress-tests/cypress.config.js b/npm/create-cypress-tests/cypress.config.js deleted file mode 100644 index 4ba52ba2c8d..00000000000 --- a/npm/create-cypress-tests/cypress.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {} diff --git a/npm/create-cypress-tests/demo.gif b/npm/create-cypress-tests/demo.gif deleted file mode 100644 index 344fc47d3fa..00000000000 Binary files a/npm/create-cypress-tests/demo.gif and /dev/null differ diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json deleted file mode 100644 index 7ecf5f176ef..00000000000 --- a/npm/create-cypress-tests/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "create-cypress-tests", - "version": "0.0.0-development", - "description": "Cypress smart installation wizard", - "main": "dist/src/main.js", - "scripts": { - "build": "yarn prepare-example && tsc -p ./tsconfig.json && node scripts/example copy-to ./dist/initial-template && yarn prepare-copy-templates", - "build-prod": "yarn build", - "prepare-example": "node scripts/example copy-to ./initial-template", - "prepare-copy-templates": "node scripts/copy-templates copy-to ./dist/src", - "test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json './src/**/*.test.ts'", - "test:watch": "yarn test -w" - }, - "dependencies": { - "@babel/core": "^7.5.4", - "@babel/plugin-transform-typescript": "^7.2.0", - "@babel/template": "^7.5.4", - "@babel/types": "^7.5.0", - "bluebird": "3.7.2", - "chalk": "4.1.0", - "cli-highlight": "2.1.10", - "commander": "6.1.0", - "fast-glob": "3.2.7", - "find-up": "5.0.0", - "fs-extra": "^9.1.0", - "glob": "^7.1.6", - "inquirer": "7.3.3", - "ora": "^5.1.0", - "recast": "0.20.4", - "semver": "7.3.7" - }, - "devDependencies": { - "@types/babel__core": "^7.1.2", - "@types/inquirer": "7.3.1", - "@types/mock-fs": "4.10.0", - "@types/node": "14.14.31", - "@types/ora": "^3.2.0", - "@types/semver": "7.3.9", - "copy": "0.3.2", - "mocha": "7.1.1", - "mock-fs": "5.1.1", - "shx": "0.3.3", - "snap-shot-it": "7.9.3", - "typescript": "^4.7.4" - }, - "files": [ - "dist", - "bin" - ], - "bin": { - "create-cypress-tests": "dist/src/index.js" - }, - "license": "MIT", - "repository": "https://github.com/cypress-io/cypress.git", - "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/create-cypress-tests/#readme" -} diff --git a/npm/create-cypress-tests/scripts/copy-templates.js b/npm/create-cypress-tests/scripts/copy-templates.js deleted file mode 100644 index 832a816c053..00000000000 --- a/npm/create-cypress-tests/scripts/copy-templates.js +++ /dev/null @@ -1,40 +0,0 @@ -const fg = require('fast-glob') -const fs = require('fs-extra') -const chalk = require('chalk') -const path = require('path') -const program = require('commander') - -program -.command('copy-to [destination]') -.description('copy ./src/**/*.template.js into destination') -.action(async (destination) => { - const srcPath = path.resolve(__dirname, '..', 'src') - const destinationPath = path.resolve(process.cwd(), destination) - - const templates = await fg('**/*.template.js', { - cwd: srcPath, - onlyFiles: true, - unique: true, - }) - - const srcOuput = './src/' - let destinationOuput = destination.replace('/\\/g', '/') - - if (!destinationOuput.endsWith('/')) { - destinationOuput += '/' - } - - const relOutput = (template, forSource) => { - return `${forSource ? srcOuput : destinationOuput}${template}` - } - - const result = await Promise.all(templates.map(async (template) => { - await fs.copy(path.join(srcPath, template), path.join(destinationPath, template)) - - return () => console.log(`✅ ${relOutput(template, true)} successfully copied to ${chalk.cyan(relOutput(template, false))}`) - })) - - result.forEach((r) => r()) -}) - -program.parse(process.argv) diff --git a/npm/create-cypress-tests/scripts/example-tsconfig.json b/npm/create-cypress-tests/scripts/example-tsconfig.json deleted file mode 100644 index 08c029d4441..00000000000 --- a/npm/create-cypress-tests/scripts/example-tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": [ - "es5", - "dom" - ], - "types": [ - "cypress" - ] - }, - "include": [ - "**/*.ts*" - ] -} \ No newline at end of file diff --git a/npm/create-cypress-tests/scripts/example.js b/npm/create-cypress-tests/scripts/example.js deleted file mode 100644 index 49b35fd7ee2..00000000000 --- a/npm/create-cypress-tests/scripts/example.js +++ /dev/null @@ -1,23 +0,0 @@ -const fs = require('fs-extra') -const chalk = require('chalk') -const path = require('path') -const program = require('commander') - -program -.command('copy-to [destination]') -.description('copy cypress/packages/example into destination') -.action(async (destination) => { - const exampleFolder = path.resolve(__dirname, '..', '..', '..', 'packages', 'example', 'cypress') - const destinationPath = path.resolve(process.cwd(), destination) - - await fs.remove(destinationPath) - await fs.copy(exampleFolder, destinationPath, { recursive: true }) - - console.log(`✅ Example was successfully created at ${chalk.cyan(destination)}`) - - await fs.copy(path.join(__dirname, 'example-tsconfig.json'), path.join(destination, 'tsconfig.json')) - - console.log(`✅ tsconfig.json was created for ${chalk.cyan(destination)}`) -}) - -program.parse(process.argv) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts deleted file mode 100644 index 5bc9e1ed33d..00000000000 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/// - -import * as babel from '@babel/core' -import { expect } from 'chai' -import { createTransformPluginsFileBabelPlugin } from './babelTransform' - -describe('babel transform utils', () => { - context('Plugins config babel plugin', () => { - it('injects code into the plugins file based on ast', () => { - const plugin = createTransformPluginsFileBabelPlugin({ - RequireAst: babel.template.ast('require("something")'), - IfComponentTestingPluginsAst: babel.template.ast('yey()'), - }) - - const output = babel.transformSync([ - 'module.exports = (on, config) => {', - 'on("do")', - '}', - ].join('\n'), { - plugins: [plugin], - })?.code - - expect(output).to.equal([ - 'require("something");', - '', - 'module.exports = (on, config) => {', - ' on("do");', - '', - ' if (config.testingType === "component") {', - ' yey();', - ' }', - '};', - ].join(`\n`)) - }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts deleted file mode 100644 index b7089d3cc15..00000000000 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts +++ /dev/null @@ -1,138 +0,0 @@ -import path from 'path' -import * as fs from 'fs-extra' -import * as babel from '@babel/core' -import * as babelTypes from '@babel/types' -import { prettifyCode } from '../../utils' - -type AST = ReturnType - -export type PluginsConfigAst = { - RequireAst: AST - IfComponentTestingPluginsAst: AST - requiresReturnConfig?: true -} - -const sharedBabelOptions = { - // disable user config - configFile: false, - babelrc: false, - presets: [], - root: process.env.BABEL_TEST_ROOT, // for testing -} - -async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) { - try { - const initialCode = await fs.readFile(filePath, { encoding: 'utf-8' }) - - const updatedResult = await babel.transformAsync(initialCode, { - filename: path.basename(filePath), - filenameRelative: path.relative(process.cwd(), filePath), - plugins: [babelPlugin], - ...sharedBabelOptions, - }) - - if (!updatedResult) { - return false - } - - let finalCode = updatedResult.code - - if (finalCode === initialCode) { - return false - } - - finalCode = await prettifyCode(finalCode) - - await fs.writeFile(filePath, finalCode) - - return true - } catch (e) { - return false - } -} - -const returnConfigAst = babel.template.ast('return config; // IMPORTANT to return a config', { preserveComments: true }) - -export function createTransformPluginsFileBabelPlugin (ast: PluginsConfigAst): babel.PluginObj { - return { - visitor: { - Program: (path) => { - path.unshiftContainer('body', ast.RequireAst) - }, - Function: (path) => { - if (!babelTypes.isAssignmentExpression(path.parent)) { - return - } - - const assignment = path.parent.left - - const isModuleExports = - babelTypes.isMemberExpression(assignment) - && babelTypes.isIdentifier(assignment.object) - && assignment.object.name === 'module' - && babelTypes.isIdentifier(assignment.property) - && assignment.property.name === 'exports' - - if (isModuleExports && babelTypes.isFunction(path.parent.right)) { - const paramsLength = path.parent.right.params.length - - if (paramsLength === 0) { - path.parent.right.params.push(babelTypes.identifier('on')) - path.parent.right.params.push(babelTypes.identifier('config')) - } - - if (paramsLength === 1) { - path.parent.right.params.push(babelTypes.identifier('config')) - } - - const statementToInject = Array.isArray(ast.IfComponentTestingPluginsAst) - ? ast.IfComponentTestingPluginsAst - : [ast.IfComponentTestingPluginsAst] - - const ifComponentMode = babelTypes.ifStatement( - babelTypes.binaryExpression( - '===', - babelTypes.identifier('config.testingType'), - babelTypes.stringLiteral('component'), - ), - babelTypes.blockStatement(statementToInject as babelTypes.Statement[] | babelTypes.Statement[]), - ) - - path.get('body').pushContainer('body' as never, ifComponentMode as babel.Node) - - if (ast.requiresReturnConfig) { - path.get('body').pushContainer('body' as never, returnConfigAst) - } - } - }, - }, - } -} - -export async function injectPluginsCode (pluginsFilePath: string, ast: PluginsConfigAst) { - return transformFileViaPlugin(pluginsFilePath, createTransformPluginsFileBabelPlugin(ast)) -} - -export async function getPluginsSourceExample (ast: PluginsConfigAst) { - const exampleCode = [ - 'module.exports = (on, config) => {', - '', - '}', - ].join('\n') - - try { - const babelResult = await babel.transformAsync(exampleCode, { - filename: 'nothing.js', - plugins: [createTransformPluginsFileBabelPlugin(ast)], - ...sharedBabelOptions, - }) - - if (!babelResult?.code) { - throw new Error() - } - - return babelResult.code - } catch (e) { - throw new Error('Can not generate code example for plugins file because of unhandled error. Please update the plugins file manually.') - } -} diff --git a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.test.ts b/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.test.ts deleted file mode 100644 index 8c9e3381e98..00000000000 --- a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.test.ts +++ /dev/null @@ -1,409 +0,0 @@ -/// - -import * as path from 'path' -import { expect } from 'chai' - -import * as fs from 'fs-extra' -import { insertValueInJSString, insertValuesInConfigFile } from './configFileUpdater' -const projectRoot = process.cwd() - -// Test util - if needed outside the tests we can move it to utils -const stripIndent = (strings: any, ...args: any) => { - const parts = [] - - for (let i = 0; i < strings.length; i++) { - parts.push(strings[i]) - - if (i < strings.length - 1) { - parts.push(`<<${i}>>`) - } - } - - const lines = parts.join('').split('\n') - const firstLine = lines[0].length === 0 ? lines[1] : lines[0] - let indentSize = 0 - - for (let i = 0; i < firstLine.length; i++) { - if (firstLine[i] === ' ') { - indentSize++ - continue - } - - break - } - - const strippedLines = lines.map((line) => line.substring(indentSize)) - - let result = strippedLines.join('\n').trimLeft() - - args.forEach((arg: any, i: any) => { - result = result.replace(`<<${i}>>`, `${arg}`) - }) - - return result -} - -describe('lib/util/config-file-updater', () => { - context('with js files', () => { - describe('#insertValueInJSString', () => { - describe('es6 vs es5', () => { - it('finds the object litteral and adds the values to it es6', async () => { - const src = stripIndent`\ - export default { - foo: 42, - } - ` - - const expectedOutput = stripIndent`\ - export default { - projectId: "id1234", - viewportWidth: 400, - foo: 42, - } - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('finds the object litteral and adds the values to it es5', async () => { - const src = stripIndent`\ - module.exports = { - foo: 42, - } - ` - - const expectedOutput = stripIndent`\ - module.exports = { - projectId: "id1234", - viewportWidth: 400, - foo: 42, - } - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('works with and without the quotes around keys', async () => { - const src = stripIndent`\ - export default { - "foo": 42, - } - ` - - const expectedOutput = stripIndent`\ - export default { - projectId: "id1234", - viewportWidth: 400, - "foo": 42, - } - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('defineConfig', () => { - it('skips defineConfig and add to the object inside', async () => { - const src = stripIndent`\ - import { defineConfig } from "cypress" - export default defineConfig({ - foo: 42, - }) - ` - - const expectedOutput = stripIndent`\ - import { defineConfig } from "cypress" - export default defineConfig({ - projectId: "id1234", - viewportWidth: 400, - foo: 42, - }) - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('skips defineConfig even if it renamed in an import (es6)', async () => { - const src = stripIndent`\ - import { defineConfig as cy_defineConfig } from "cypress" - export default cy_defineConfig({ - foo: 42, - }) - ` - - const expectedOutput = stripIndent`\ - import { defineConfig as cy_defineConfig } from "cypress" - export default cy_defineConfig({ - projectId: "id1234", - viewportWidth: 400, - foo: 42, - }) - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('skips defineConfig even if it renamed in a require (es5)', async () => { - const src = stripIndent`\ - const { defineConfig: cy_defineConfig } = require("cypress") - module.exports = cy_defineConfig({ - foo: 42, - }) - ` - - const expectedOutput = stripIndent`\ - const { defineConfig: cy_defineConfig } = require("cypress") - module.exports = cy_defineConfig({ - projectId: "id1234", - viewportWidth: 400, - foo: 42, - }) - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('updates', () => { - it('updates a value if the same value is found in resolved config', async () => { - const src = stripIndent`\ - export default { - foo: 42, - } - ` - const expectedOutput = stripIndent`\ - export default { - foo: 1000, - } - ` - - const output = await insertValueInJSString(src, { foo: 1000 }) - - expect(output).to.equal(expectedOutput) - }) - - it('accepts inline comments', async () => { - const src = stripIndent`\ - export default { - foo: 12, // will do this later - viewportWidth: 800, - } - ` - const expectedOutput = stripIndent`\ - export default { - foo: 1000, // will do this later - viewportWidth: 800, - } - ` - - const output = await insertValueInJSString(src, { foo: 1000 }) - - expect(output).to.equal(expectedOutput) - }) - - it('updates a value even when this value is explicitely undefined', async () => { - const src = stripIndent`\ - export default { - foo: undefined, // will do this later - viewportWidth: 800, - } - ` - const expectedOutput = stripIndent`\ - export default { - foo: 1000, // will do this later - viewportWidth: 800, - } - ` - - const output = await insertValueInJSString(src, { foo: 1000 }) - - expect(output).to.equal(expectedOutput) - }) - - it('updates values and inserts config', async () => { - const src = stripIndent`\ - export default { - foo: 42, - bar: 84, - component: { - devServer() { - return null - } - } - } - ` - - const expectedOutput = stripIndent`\ - export default { - projectId: "id1234", - foo: 1000, - bar: 3000, - component: { - devServer() { - return null - } - } - } - ` - - const output = await insertValueInJSString(src, { foo: 1000, bar: 3000, projectId: 'id1234' }) - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('subkeys', () => { - it('inserts nested values', async () => { - const src = stripIndent`\ - module.exports = { - foo: 42 - } - ` - - const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } }) - - const expectedOutput = stripIndent`\ - module.exports = { - component: { - specPattern: "src/**/*.spec.cy.js", - }, - foo: 42 - } - ` - - expect(output).to.equal(expectedOutput) - }) - - it('inserts nested values into existing keys', async () => { - const src = stripIndent`\ - module.exports = { - component: { - viewportWidth: 800 - }, - foo: 42 - } - ` - - const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } }) - - const expectedOutput = stripIndent`\ - module.exports = { - component: { - specPattern: "src/**/*.spec.cy.js", - viewportWidth: 800 - }, - foo: 42 - } - ` - - expect(output).to.equal(expectedOutput) - }) - - it('updates nested values', async () => { - const src = stripIndent`\ - module.exports = { - foo: 42, - component: { - specPattern: 'components/**/*.spec.cy.js', - foo: 82 - } - }` - - const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } }) - - const expectedOutput = stripIndent`\ - module.exports = { - foo: 42, - component: { - specPattern: "src/**/*.spec.cy.js", - foo: 82 - } - }` - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('failures', () => { - it('fails if not an object litteral', () => { - const src = [ - 'const foo = {}', - 'export default foo', - ].join('\n') - - return insertValueInJSString(src, { bar: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - - it('fails if one of the values to update is not a literal', () => { - const src = [ - 'const bar = 12', - 'export default {', - ' foo: bar', - '}', - ].join('\n') - - return insertValueInJSString(src, { foo: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - - it('fails with inlined values', () => { - const src = stripIndent`\ - const foo = 12 - export default { - foo - } - ` - - return insertValueInJSString(src, { foo: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - - it('fails if there is a spread', () => { - const src = stripIndent`\ - const foo = { bar: 12 } - export default { - bar: 8, - ...foo - } - ` - - return insertValueInJSString(src, { bar: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - }) - }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.ts b/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.ts deleted file mode 100644 index 6d16505c40e..00000000000 --- a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.ts +++ /dev/null @@ -1,295 +0,0 @@ -import _ from 'lodash' -import { parse } from '@babel/parser' -import type { File } from '@babel/types' -import type { NodePath } from 'ast-types/lib/node-path' -import { visit } from 'recast' -import type { namedTypes } from 'ast-types' -import * as fs from 'fs-extra' -import { prettifyCode } from '../../utils' - -export async function insertValuesInConfigFile (filePath: string, obj: Record = {}) { - await insertValuesInJavaScript(filePath, obj) - - return true -} - -export async function insertValuesInJavaScript (filePath: string, obj: Record) { - const fileContents = await fs.readFile(filePath, { encoding: 'utf8' }) - - let finalCode = await insertValueInJSString(fileContents, obj) - - const prettifiedCode = await prettifyCode(finalCode) - - if (prettifiedCode) { - finalCode = prettifiedCode - } - - await fs.writeFile(filePath, finalCode) -} - -export async function insertValueInJSString (fileContents: string, obj: Record): Promise { - const ast = parse(fileContents, { plugins: ['typescript'], sourceType: 'module' }) - - let objectLiteralNode: namedTypes.ObjectExpression | undefined - - function handleExport (nodePath: NodePath | NodePath): void { - if (nodePath.node.type === 'CallExpression' - && nodePath.node.callee.type === 'Identifier') { - const functionName = nodePath.node.callee.name - - if (isDefineConfigFunction(ast, functionName)) { - return handleExport(nodePath.get('arguments', 0)) - } - } - - if (nodePath.node.type === 'ObjectExpression' && !nodePath.node.properties.find((prop) => prop.type !== 'ObjectProperty')) { - objectLiteralNode = nodePath.node - - return - } - - throw new Error('Cypress was unable to add/update values in your configuration file.') - } - - visit(ast, { - visitAssignmentExpression (nodePath) { - if (nodePath.node.left.type === 'MemberExpression') { - if (nodePath.node.left.object.type === 'Identifier' && nodePath.node.left.object.name === 'module' - && nodePath.node.left.property.type === 'Identifier' && nodePath.node.left.property.name === 'exports') { - handleExport(nodePath.get('right')) - } - } - - return false - }, - visitExportDefaultDeclaration (nodePath) { - handleExport(nodePath.get('declaration')) - - return false - }, - }) - - const splicers: Splicer[] = [] - - if (!objectLiteralNode) { - // if the export is no object litteral - throw new Error('Cypress was unable to add/update values in your configuration file.') - } - - setRootKeysSplicers(splicers, obj, objectLiteralNode!, ' ') - setSubKeysSplicers(splicers, obj, objectLiteralNode!, ' ', ' ') - - // sort splicers to keep the order of the original file - const sortedSplicers = splicers.sort((a, b) => a.start === b.start ? 0 : a.start > b.start ? 1 : -1) - - if (!sortedSplicers.length) return fileContents - - let nextStartingIndex = 0 - let resultCode = '' - - sortedSplicers.forEach((splicer) => { - resultCode += fileContents.slice(nextStartingIndex, splicer.start) + splicer.replaceString - nextStartingIndex = splicer.end - }) - - return resultCode + fileContents.slice(nextStartingIndex) -} - -export function isDefineConfigFunction (ast: File, functionName: string): boolean { - let value = false - - visit(ast, { - visitVariableDeclarator (nodePath) { - // if this is a require of cypress - if (nodePath.node.init?.type === 'CallExpression' - && nodePath.node.init.callee.type === 'Identifier' - && nodePath.node.init.callee.name === 'require' - && nodePath.node.init.arguments[0].type === 'StringLiteral' - && nodePath.node.init.arguments[0].value === 'cypress') { - if (nodePath.node.id?.type === 'ObjectPattern') { - const defineConfigFunctionNode = nodePath.node.id.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === 'defineConfig' - }) - - if (defineConfigFunctionNode) { - value = (defineConfigFunctionNode as any).value?.name === functionName - } - } - } - - return false - }, - visitImportDeclaration (nodePath) { - if (nodePath.node.source.type === 'StringLiteral' - && nodePath.node.source.value === 'cypress') { - const defineConfigFunctionNode = nodePath.node.specifiers?.find((specifier) => { - return specifier.type === 'ImportSpecifier' - && specifier.imported.type === 'Identifier' - && specifier.imported.name === 'defineConfig' - }) - - if (defineConfigFunctionNode) { - value = (defineConfigFunctionNode as any).local?.name === functionName - } - } - - return false - }, - }) - - return value -} - -function setRootKeysSplicers ( - splicers: Splicer[], - obj: Record, - objectLiteralNode: namedTypes.ObjectExpression, - lineStartSpacer: string, -) { - const objectLiteralStartIndex = (objectLiteralNode as any).start + 1 - // add values - const objKeys = Object.keys(obj).filter((key) => ['boolean', 'number', 'string'].includes(typeof obj[key])) - - // update values - const keysToUpdate = objKeys.filter((key) => { - return objectLiteralNode.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === key - }) - }) - - keysToUpdate.forEach( - (key) => { - const propertyToUpdate = propertyFromKey(objectLiteralNode, key) - - if (propertyToUpdate) { - setSplicerToUpdateProperty(splicers, propertyToUpdate, obj[key], key, obj) - } - }, - ) - - const keysToInsert = objKeys.filter((key) => !keysToUpdate.includes(key)) - - if (keysToInsert.length) { - const valuesInserted = `\n${lineStartSpacer}${ keysToInsert.map((key) => `${key}: ${JSON.stringify(obj[key])},`).join(`\n${lineStartSpacer}`)}` - - splicers.push({ - start: objectLiteralStartIndex, - end: objectLiteralStartIndex, - replaceString: valuesInserted, - }) - } -} - -function setSubKeysSplicers ( - splicers: Splicer[], - obj: Record, - objectLiteralNode: namedTypes.ObjectExpression, - lineStartSpacer: string, - parentLineStartSpacer: string, -) { - const objectLiteralStartIndex = (objectLiteralNode as any).start + 1 - - const keysToUpdateWithObjects: string[] = [] - - const objSubkeys = Object.keys(obj).filter((key) => typeof obj[key] === 'object').reduce((acc: Array<{parent: string, subkey: string}>, key) => { - keysToUpdateWithObjects.push(key) - Object.entries(obj[key]).forEach(([subkey, value]) => { - if (['boolean', 'number', 'string'].includes(typeof value)) { - acc.push({ parent: key, subkey }) - } - }) - - return acc - }, []) - - // add values where the parent key needs to be created - const subkeysToInsertWithoutKey = objSubkeys.filter(({ parent }) => { - return !objectLiteralNode.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === parent - }) - }) - const keysToInsertForSubKeys: Record = {} - - subkeysToInsertWithoutKey.forEach((keyTuple) => { - const subkeyList = keysToInsertForSubKeys[keyTuple.parent] || [] - - subkeyList.push(keyTuple.subkey) - keysToInsertForSubKeys[keyTuple.parent] = subkeyList - }) - - let subvaluesInserted = '' - - for (const key in keysToInsertForSubKeys) { - subvaluesInserted += `\n${parentLineStartSpacer}${key}: {` - keysToInsertForSubKeys[key].forEach((subkey) => { - subvaluesInserted += `\n${parentLineStartSpacer}${lineStartSpacer}${subkey}: ${JSON.stringify(obj[key][subkey])},` - }) - - subvaluesInserted += `\n${parentLineStartSpacer}},` - } - - if (subkeysToInsertWithoutKey.length) { - splicers.push({ - start: objectLiteralStartIndex, - end: objectLiteralStartIndex, - replaceString: subvaluesInserted, - }) - } - - // add/update values where parent key already exists - keysToUpdateWithObjects.filter((parent) => { - return objectLiteralNode.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === parent - }) - }).forEach((key) => { - const propertyToUpdate = propertyFromKey(objectLiteralNode, key) - - if (propertyToUpdate?.value.type === 'ObjectExpression') { - setRootKeysSplicers(splicers, obj[key], propertyToUpdate.value, parentLineStartSpacer + lineStartSpacer) - } - }) -} - -function setSplicerToUpdateProperty (splicers: Splicer[], - propertyToUpdate: namedTypes.ObjectProperty, - updatedValue: any, - key: string, - obj: Record) { - if (propertyToUpdate && (isPrimitive(propertyToUpdate.value) || isUndefinedOrNull(propertyToUpdate.value))) { - splicers.push({ - start: (propertyToUpdate.value as any).start, - end: (propertyToUpdate.value as any).end, - replaceString: JSON.stringify(updatedValue), - }) - } else { - throw new Error('Cypress was unable to add/update values in your configuration file.') - } -} - -function propertyFromKey (objectLiteralNode: namedTypes.ObjectExpression | undefined, key: string): namedTypes.ObjectProperty | undefined { - return objectLiteralNode?.properties.find((prop) => { - return prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === key - }) as namedTypes.ObjectProperty -} - -function isPrimitive (value: NodePath['node']): value is namedTypes.NumericLiteral | namedTypes.StringLiteral | namedTypes.BooleanLiteral { - return value.type === 'NumericLiteral' || value.type === 'StringLiteral' || value.type === 'BooleanLiteral' -} - -function isUndefinedOrNull (value: NodePath['node']): value is namedTypes.Identifier { - return value.type === 'Identifier' && ['undefined', 'null'].includes(value.name) -} - -interface Splicer{ - start: number - end: number - replaceString: string -} diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts deleted file mode 100644 index 2db6b20baf1..00000000000 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import path from 'path' -import fs from 'fs-extra' -import snapshot from 'snap-shot-it' -import { expect, use } from 'chai' -import sinon, { SinonStub, SinonSpy } from 'sinon' -import chalk from 'chalk' -import mockFs from 'mock-fs' -import { initComponentTesting } from './init-component-testing' -import inquirer from 'inquirer' -import sinonChai from 'sinon-chai' -import childProcess from 'child_process' -import { someOfSpyCallsIncludes } from '../test-utils' - -use(sinonChai) - -describe('init component tests script', () => { - let promptSpy: SinonStub | null = null - let logSpy: SinonSpy | null = null - let processExitStub: SinonStub | null = null - let execStub: SinonStub | null = null - - const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output') - const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.config.ts') - - beforeEach(async () => { - logSpy = sinon.spy(global.console, 'log') - // @ts-ignores - execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) - processExitStub = sinon.stub(process, 'exit').callsFake(() => { - throw new Error(`${chalk.red('process.exit')} should not be called`) - }) - - await fs.remove(e2eTestOutputPath) - await fs.mkdir(e2eTestOutputPath) - - process.env.BABEL_TEST_ROOT = e2eTestOutputPath - }) - - afterEach(() => { - mockFs.restore() - logSpy?.restore() - promptSpy?.restore() - processExitStub?.restore() - execStub?.restore() - }) - - function createTempFiles (tempFiles: Record) { - Object.entries(tempFiles).forEach(([fileName, content]) => { - fs.outputFileSync( - path.join(e2eTestOutputPath, fileName), - content, - ) - }) - } - - function snapshotGeneratedFiles (name: string) { - snapshot( - `${name} cypress.config.ts`, - fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress.config.ts'), - { encoding: 'utf-8' }, - ), - ) - - snapshot( - `${name} plugins/index.js`, - fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), - { encoding: 'utf-8' }, - ), - ) - - const supportFile = fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress', 'support', 'component.js'), - { encoding: 'utf-8' }, - ) - - // Comparing empty snapshot errors. - if (supportFile.length === 0) { - return - } - - snapshot( - `${name} support/component.js`, - fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress', 'support', 'component.js'), - { encoding: 'utf-8' }, - ), - ) - } - - it('determines more presumable configuration to suggest', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/cypress/support/component.js': '', - '/cypress/plugins/index.js': 'module.exports = (on, config) => {}', - // For next.js user will have babel config, but we want to suggest to use the closest config for the application code - '/babel.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { react: '^17.x', next: '^9.2.0' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'next.js', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - const [{ choices }] = (inquirer.prompt as any).args[0][0] - - expect(choices[0]).to.equal('next.js') - snapshotGeneratedFiles('injects guessed next.js template') - }) - - it('automatically suggests to the user which config to use', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/cypress/support/component.js': 'import "./commands.js";', - '/cypress/plugins/index.js': 'module.exports = () => {}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.10.0', - }, - }), - '/webpack.config.js': 'module.exports = { }', - }) - - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - const [{ choices, message }] = (inquirer.prompt as any).args[0][0] - - expect(choices[0]).to.equal('webpack') - expect(message).to.contain( - `Press ${chalk.inverse(' Enter ')} to continue with ${chalk.green( - 'webpack', - )} configuration`, - ) - - snapshotGeneratedFiles('Injected overridden webpack template') - }) - - it('Asks for preferred bundling tool if can not determine the right one', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/webpack.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - framework: 'vue@2', - }) as any) - .onCall(1) - .returns(Promise.resolve({ - chosenTemplateName: 'webpack', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - expect( - someOfSpyCallsIncludes(global.console.log, 'We were unable to automatically determine your framework 😿'), - ).to.be.true - }) - - it('Asks for framework if more than 1 option was auto detected', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/webpack.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { react: '*', vue: '^2.4.5' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - framework: 'vue@3', - }) as any) - .onCall(1) - .returns(Promise.resolve({ - chosenTemplateName: 'webpack', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - expect( - someOfSpyCallsIncludes(global.console.log, `It looks like all these frameworks: ${chalk.yellow('react, vue@2')} are available from this directory.`), - ).to.be.true - }) - - it('installs the right adapter', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/webpack.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { react: '16.4.5' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - chosenTemplateName: 'vite', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - expect(execStub).to.be.calledWith('yarn add @cypress/react --dev') - }) - - it('installs the right adapter for vue 3', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/vite.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { vue: '^3.0.0' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - chosenTemplateName: 'vite', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - expect(execStub).to.be.calledWith('yarn add @cypress/vue --dev') - }) - - it('suggest the right instruction based on user template choice', async () => { - createTempFiles({ - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.0.0', - }, - }), - '/cypress.config.ts': 'export default {}', - }) - - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - expect( - someOfSpyCallsIncludes( - global.console.log, - 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts', - ), - ).to.be.true - }) - - it('suggests right docs example and cypress.config.ts config based on the `componentFolder` answer', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.0.0', - }, - }), - }) - - sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - const injectedCode = require(path.join(e2eTestOutputPath, 'cypress.config.ts')) - - expect(JSON.stringify(injectedCode.default, null, 2)).to.equal(JSON.stringify( - { - specPattern: 'cypress/component/**/*.spec.{js,ts,jsx,tsx}', - }, - null, - 2, - )) - }) - - it('Shows help message if cypress files are not created', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.0.0', - }, - }), - }) - - sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - expect( - someOfSpyCallsIncludes( - global.console.log, - 'was not updated automatically. Please add the following config manually:', - ), - ).to.be.true - }) - - it(`Doesn't affect injected code if user has custom babel.config.js`, async () => { - createTempFiles({ - '/cypress/plugins/index.js': 'module.exports = (on, config) => {}', - '/cypress.config.ts': 'export default {}', - 'babel.config.js': `module.exports = ${JSON.stringify({ - presets: [ - '@babel/preset-env', - ], - })}`, - '/package.json': JSON.stringify({ - dependencies: { - babel: '*', - react: '^16.0.0', - }, - }), - }) - - sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - const babelPluginsOutput = await fs.readFile( - path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), - 'utf-8', - ) - - expect(babelPluginsOutput).not.to.contain('use strict') - expect(babelPluginsOutput).to.contain('module.exports = (on, config) => {') - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts deleted file mode 100644 index a79908088fe..00000000000 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts +++ /dev/null @@ -1,194 +0,0 @@ -import fs from 'fs-extra' -import path from 'path' -import chalk from 'chalk' -import inquirer from 'inquirer' -import highlight from 'cli-highlight' -import { Template } from './templates/Template' -import { guessTemplate } from './templates/guessTemplate' -import { installFrameworkAdapter } from './installFrameworkAdapter' -import { injectPluginsCode, getPluginsSourceExample } from './babel/babelTransform' -import { installDependency } from '../utils' -import { insertValuesInConfigFile } from './config-file-updater/configFileUpdater' - -async function injectOrShowConfigCode (injectFn: () => Promise, { - code, - filePath, - fallbackFileMessage, - language, -}: { - code: string - filePath: string - language: string - fallbackFileMessage: string -}) { - const fileExists = fs.existsSync(filePath) - const readableFilePath = fileExists ? path.relative(process.cwd(), filePath) : fallbackFileMessage - - const printCode = () => { - console.log() - console.log(highlight(code, { language })) - console.log() - } - - const printSuccess = () => { - console.log(`✅ ${chalk.bold.green(readableFilePath)} was updated with the following config:`) - printCode() - } - - const printFailure = () => { - console.log(`❌ ${chalk.bold.red(readableFilePath)} was not updated automatically. Please add the following config manually: `) - printCode() - } - - if (!fileExists) { - printFailure() - - return - } - - // something get completely wrong when using babel or something. Print error message. - const injected = await injectFn().catch(() => false) - - injected ? printSuccess() : printFailure() -} - -async function injectAndShowCypressConfig ( - cypressJsonPath: string, - componentFolder: string, -) { - const configToInject = { - specPattern: `${componentFolder}/**/*.spec.{js,ts,jsx,tsx}`, - } - - await injectOrShowConfigCode(() => insertValuesInConfigFile(cypressJsonPath, configToInject), { - code: JSON.stringify(configToInject, null, 2), - language: 'js', - filePath: cypressJsonPath, - fallbackFileMessage: 'cypress.json config file', - }) -} - -async function injectAndShowPluginConfig (template: Template, { - templatePayload, - pluginsFilePath, - cypressProjectRoot, -}: { - templatePayload: T | null - pluginsFilePath: string - cypressProjectRoot: string -}) { - const ast = template.getPluginsCodeAst(templatePayload, { cypressProjectRoot }) - - await injectOrShowConfigCode(() => injectPluginsCode(pluginsFilePath, ast), { - code: await getPluginsSourceExample(ast), - language: 'js', - filePath: pluginsFilePath, - fallbackFileMessage: 'plugins file (https://on.cypress.io/plugins-file)', - }) -} - -type InitComponentTestingOptions = { - config: Record - cypressConfigPath: string - useYarn: boolean -} - -export async function initComponentTesting ({ config, useYarn, cypressConfigPath }: InitComponentTestingOptions) { - const cypressProjectRoot = path.resolve(cypressConfigPath, '..') - - const framework = await installFrameworkAdapter(cypressProjectRoot, { useYarn }) - const { - possibleTemplates, - defaultTemplate, - defaultTemplateName, - templatePayload, - } = await guessTemplate(framework, cypressProjectRoot) - - const pluginsFilePath = path.resolve( - cypressProjectRoot, - config.pluginsFile ?? './cypress/plugins/index.js', - ) - - const templateChoices = Object.keys(possibleTemplates).sort((key) => { - return key === defaultTemplateName ? -1 : 0 - }) - - const { - chosenTemplateName, - componentFolder, - }: Record = await inquirer.prompt([ - { - type: 'list', - name: 'chosenTemplateName', - choices: templateChoices, - default: defaultTemplate ? 0 : undefined, - message: defaultTemplate?.message - ? `${defaultTemplate?.message}\n\n Press ${chalk.inverse( - ' Enter ', - )} to continue with ${chalk.green( - defaultTemplateName, - )} configuration or select another template from the list:` - : 'We were not able to automatically determine which framework or bundling tool you are using. Please choose one from the list:', - }, - { - type: 'input', - name: 'componentFolder', - filter: (input) => input.trim(), - validate: (input) => { - return input === '' || !/^[a-zA-Z].*/.test(input) - ? `Directory "${input}" is invalid` - : true - }, - message: 'Which folder would you like to use for your component tests?', - default: (answers: { chosenTemplateName: keyof typeof possibleTemplates }) => { - return possibleTemplates[answers.chosenTemplateName].recommendedComponentFolder - }, - }, - ]) - - const chosenTemplate = possibleTemplates[chosenTemplateName] as Template - - console.log() - console.log(`Installing required dependencies`) - console.log() - - for (const dependency of chosenTemplate.dependencies) { - await installDependency(dependency, { useYarn }) - } - - console.log() - console.log(`Let's setup everything for component testing with ${chalk.cyan(chosenTemplateName)}:`) - console.log() - - await injectAndShowCypressConfig(cypressConfigPath, componentFolder) - await injectAndShowPluginConfig(chosenTemplate, { - templatePayload, - pluginsFilePath, - cypressProjectRoot, - }) - - if (chosenTemplate.printHelper) { - chosenTemplate.printHelper() - } - - console.log( - `Find examples of component tests for ${chalk.green( - chosenTemplateName, - )} in ${chalk.underline(chosenTemplate.getExampleUrl({ componentFolder }))}.`, - ) - - if (framework === 'react') { - console.log() - - console.log( - `Docs for different recipes of bundling tools: ${chalk.bold.underline( - 'https://github.com/cypress-io/cypress/tree/develop/npm/react/docs/recipes.md', - )}`, - ) - } - - // render delimiter - console.log() - console.log(new Array(process.stdout.columns).fill('═').join('')) - console.log() -} diff --git a/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts b/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts deleted file mode 100644 index 18e669a02ea..00000000000 --- a/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts +++ /dev/null @@ -1,63 +0,0 @@ -import chalk from 'chalk' -import inquirer from 'inquirer' -import { scanFSForAvailableDependency } from '../findPackageJson' -import { installDependency } from '../utils' - -async function guessOrAskForFramework (cwd: string): Promise<'react' | 'vue@2' | 'vue@3'> { - // please sort this alphabetically - const frameworks = { - react: () => scanFSForAvailableDependency(cwd, { react: '*', 'react-dom': '*' }), - 'vue@2': () => scanFSForAvailableDependency(cwd, { vue: '2.x' }), - 'vue@3': () => scanFSForAvailableDependency(cwd, { vue: '3.x' }), - } - - const guesses = Object.keys(frameworks).filter((framework) => { - return frameworks[framework as keyof typeof frameworks]() - }) as Array<'react' | 'vue@2' | 'vue@3'> - - // found 1 precise guess. Continue - if (guesses.length === 1) { - const framework = guesses[0] - - console.log(`\nThis project is using ${chalk.bold.cyan(framework)}. Let's install the right adapter:`) - - return framework - } - - if (guesses.length === 0) { - console.log(`We were unable to automatically determine your framework 😿. ${chalk.grey('Make sure to run this command from the directory where your components located in order to make smart detection works. Or continue with manual setup:')}`) - } - - if (guesses.length > 0) { - console.log(`It looks like all these frameworks: ${chalk.yellow(guesses.join(', '))} are available from this directory. ${chalk.grey('Make sure to run this command from the directory where your components located in order to make smart detection works. Or continue with manual setup:')}`) - } - - const { framework } = await inquirer.prompt([ - { - type: 'list', - name: 'framework', - choices: Object.keys(frameworks), - message: `Which framework do you use?`, - }, - ]) - - return framework -} - -type InstallAdapterOptions = { - useYarn: boolean -} - -const frameworkDependencies = { - react: '@cypress/react', - 'vue@2': '@cypress/vue2', - 'vue@3': '@cypress/vue', -} - -export async function installFrameworkAdapter (cwd: string, options: InstallAdapterOptions) { - const framework = await guessOrAskForFramework(cwd) - - await installDependency(frameworkDependencies[framework], options) - - return framework -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/Template.ts b/npm/create-cypress-tests/src/component-testing/templates/Template.ts deleted file mode 100644 index 72ea8de09d2..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/Template.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PluginsConfigAst } from '../babel/babelTransform' - -export interface Template { - message: string - getExampleUrl: ({ componentFolder }: { componentFolder: string }) => string - recommendedComponentFolder: string - test(rootPath: string): { success: boolean, payload?: T } - getPluginsCodeAst: ( - payload: T | null, - options: { cypressProjectRoot: string }, - ) => PluginsConfigAst - dependencies: string[] - printHelper?: () => void -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/_shared/index.ts b/npm/create-cypress-tests/src/component-testing/templates/_shared/index.ts deleted file mode 100644 index 58f0af7bbb8..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/_shared/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Template } from '../Template' -import { ViteTemplate } from './vite' - -export const frameworkAgnosticTemplates: Record> = { - vite: ViteTemplate, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.test.ts b/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.test.ts deleted file mode 100644 index 78b557d8609..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ViteTemplate } from './vite' -import { snapshotPluginsAstCode } from '../../../test-utils' - -describe('vue: vite template', () => { - it('correctly generates plugins config', () => snapshotPluginsAstCode(ViteTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.ts b/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.ts deleted file mode 100644 index b9b6f8f93c5..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as babel from '@babel/core' -import { scanFSForAvailableDependency } from '../../../findPackageJson' -import { Template } from '../Template' - -export const ViteTemplate: Template = { - message: - 'It looks like you are using vitejs to run and build an application.', - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/vite', - recommendedComponentFolder: 'src', - dependencies: ['@cypress/vite-dev-server'], - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast( - 'const { startDevServer } = require("@cypress/vite-dev-server");', - ), - IfComponentTestingPluginsAst: babel.template.ast([ - 'on("dev-server:start", async (options) => startDevServer({ options }))', - ].join('\n'), { preserveComments: true }), - } - }, - test: (root) => { - return { - success: scanFSForAvailableDependency(root, { vite: '*' }), - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts b/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts deleted file mode 100644 index 10bcdfaa93a..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Template } from './Template' -import { reactTemplates } from './react' -import { vueTemplates } from './vue' -import { frameworkAgnosticTemplates } from './_shared' - -const frameworkSpecificTemplates = { - react: reactTemplates, - 'vue@2': vueTemplates, - 'vue@3': vueTemplates, -} - -export async function guessTemplate (framework: keyof typeof frameworkSpecificTemplates, cwd: string) { - const templates = { ...frameworkAgnosticTemplates, ...frameworkSpecificTemplates[framework] } - - for (const [name, template] of Object.entries(templates)) { - const typedTemplate = template as Template - const { success, payload } = typedTemplate.test(cwd) - - if (success) { - return { - defaultTemplate: typedTemplate, - defaultTemplateName: name, - templatePayload: payload ?? null, - possibleTemplates: templates, - } - } - } - - return { - templatePayload: null, - defaultTemplate: null, - defaultTemplateName: null, - possibleTemplates: templates, - } -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts deleted file mode 100644 index 1bff7af467a..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { BabelTemplate } from './babel' -import { snapshotPluginsAstCode } from '../../../test-utils' - -describe('babel installation template', () => { - beforeEach(mockFs.restore) - - it('resolves babel.config.json', () => { - mockFs({ - '/babel.config.json': JSON.stringify({ - presets: [], - plugins: [], - }), - }) - - const { success } = BabelTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('resolves babel.config.js', () => { - mockFs({ - '/project/babel.config.js': - 'module.exports = { presets: [], plugins: [] };', - '/project/index/package.json': 'dev/null', - }) - - const { success } = BabelTemplate.test('/project/index') - - expect(success).to.equal(true) - }) - - it('resolves babel config from the deep folder', () => { - mockFs({ - '/some/.babelrc': JSON.stringify({ - presets: [], - plugins: [], - }), - '/some/deep/folder/text.txt': '1', - }) - - const { success } = BabelTemplate.test('/some/deep/folder') - - expect(success).to.equal(true) - }) - - it('fails if no babel config found', () => { - mockFs({ - '/some.txt': '1', - }) - - const { success } = BabelTemplate.test('/') - - expect(success).to.equal(false) - }) - - it('resolves babel.config from package.json', () => { - mockFs({ - '/package.json': JSON.stringify({ - babel: { - presets: [], - }, - }), - }) - - const { success } = BabelTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('correctly generates plugins config', () => snapshotPluginsAstCode(BabelTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts deleted file mode 100644 index 0202d05b440..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts +++ /dev/null @@ -1,44 +0,0 @@ -import chalk from 'chalk' -import findUp from 'find-up' -import * as babel from '@babel/core' -import { Template } from '../Template' -import { createFindPackageJsonIterator } from '../../../findPackageJson' - -export const BabelTemplate: Template = { - message: `It looks like you have babel config defined. We can use it to transpile your components for testing.\n ${chalk.red( - '>>', - )} This is not a replacement for bundling tool. We will use ${chalk.red( - 'webpack', - )} to bundle the components for testing.`, - recommendedComponentFolder: 'cypress/component', - dependencies: ['webpack', '@cypress/webpack-dev-server'], - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/babel', - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/babel\')'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config)', - ].join('\n'), { preserveComments: true }), - } - }, - test: (cwd) => { - const babelConfig = findUp.sync( - ['babel.config.js', 'babel.config.json', '.babelrc', '.babelrc.json'], - { type: 'file', cwd }, - ) - - if (babelConfig) { - return { success: true } - } - - // babel config can also be declared in package.json with `babel` key https://babeljs.io/docs/en/configuration#packagejson - const packageJsonIterator = createFindPackageJsonIterator(cwd) - - return packageJsonIterator.map(({ babel }) => { - return { - success: Boolean(babel), - } - }) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/index.ts b/npm/create-cypress-tests/src/component-testing/templates/react/index.ts deleted file mode 100644 index bbbfca5178f..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Template } from '../Template' -import { NextTemplate } from './next' -import { WebpackTemplate } from './reactWebpackFile' -import { ReactScriptsTemplate } from './react-scripts' -import { BabelTemplate } from './babel' -import { WebpackOptions } from './webpack-options' - -export const reactTemplates: Record> = { - 'create-react-app': ReactScriptsTemplate, - 'next.js': NextTemplate, - webpack: WebpackTemplate, - babel: BabelTemplate, - 'default (webpack options)': WebpackOptions, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts deleted file mode 100644 index 4701791877e..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import sinon, { SinonSpy } from 'sinon' -import { expect, use } from 'chai' -import sinonChai from 'sinon-chai' -import mockFs from 'mock-fs' -import { NextTemplate } from './next' -import { snapshotPluginsAstCode } from '../../../test-utils' - -use(sinonChai) - -describe('next.js install template', () => { - let warnSpy: SinonSpy | null = null - - beforeEach(() => { - warnSpy = sinon.spy(global.console, 'warn') - }) - - afterEach(() => { - mockFs.restore() - warnSpy?.restore() - }) - - it('finds the closest package.json and checks that next is declared as dependency', () => { - mockFs({ - '/package.json': JSON.stringify({ - dependencies: { - next: '^9.2.3', - }, - scripts: { - build: 'next', - }, - }), - }) - - const { success } = NextTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('works if next is declared in the devDependencies as well', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - next: '^9.2.3', - }, - scripts: { - build: 'next', - }, - }), - }) - - const { success } = NextTemplate.test(process.cwd()) - - expect(success).to.equal(true) - }) - - it('warns and fails if version is not supported', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - next: '^8.2.3', - }, - scripts: { - build: 'next', - }, - }), - }) - - const { success } = NextTemplate.test('i/am/in/some/deep/folder') - - console.log(global.console.warn) - expect(success).to.equal(false) - - expect(global.console.warn).to.be.called - }) - - it('correctly generates plugins config', () => snapshotPluginsAstCode(NextTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts deleted file mode 100644 index 2f7ef89adfa..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as babel from '@babel/core' -import { createFindPackageJsonIterator } from '../../../findPackageJson' -import { Template } from '../Template' -import { validateSemverVersion } from '../../../utils' -import { MIN_SUPPORTED_VERSION } from '../../versions' - -export const NextTemplate: Template = { - message: 'It looks like you are using next.js.', - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/nextjs' - }, - recommendedComponentFolder: 'cypress/component', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/next\')'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config)', - ].join('\n'), { preserveComments: true }), - } - }, - test: (cwd) => { - const packageJsonIterator = createFindPackageJsonIterator(cwd) - - return packageJsonIterator.map(({ dependencies, devDependencies }, path) => { - if (!dependencies && !devDependencies) { - return { success: false } - } - - const allDeps = { - ...(devDependencies || {}), - ...(dependencies || {}), - } as Record - - const nextVersion = allDeps['next'] - - if (!nextVersion) { - return { success: false } - } - - if ( - !validateSemverVersion( - nextVersion, - MIN_SUPPORTED_VERSION['next'], - 'next.js', - ) - ) { - return { success: false } - } - - return { success: true } - }) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts deleted file mode 100644 index 7bf01624006..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import sinon, { SinonSpy } from 'sinon' -import { expect, use } from 'chai' -import sinonChai from 'sinon-chai' -import mockFs from 'mock-fs' -import { ReactScriptsTemplate } from './react-scripts' -import { snapshotPluginsAstCode } from '../../../test-utils' - -use(sinonChai) - -describe('create-react-app install template', () => { - let warnSpy: SinonSpy | null = null - - beforeEach(() => { - warnSpy = sinon.spy(global.console, 'warn') - }) - - afterEach(() => { - mockFs.restore() - warnSpy?.restore() - }) - - it('finds the closest package.json and checks that react-scripts is declared as dependency', () => { - mockFs({ - '/package.json': JSON.stringify({ - dependencies: { - 'react-scripts': '^3.2.3', - }, - }), - }) - - const { success } = ReactScriptsTemplate.test(process.cwd()) - - expect(success).to.equal(true) - }) - - it('works if react-scripts is declared in the devDependencies as well', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - 'react-scripts': '^3.2.3', - }, - }), - }) - - const { success } = ReactScriptsTemplate.test(process.cwd()) - - expect(success).to.equal(true) - }) - - it('warns and fails if version is not supported', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - 'react-scripts': '^2.2.3', - }, - }), - }) - - const { success } = ReactScriptsTemplate.test(process.cwd()) - - expect(success).to.equal(false) - expect(global.console.warn).to.be.called - }) - - it('correctly generates plugins config', () => snapshotPluginsAstCode(ReactScriptsTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts deleted file mode 100644 index 860106b74b2..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts +++ /dev/null @@ -1,62 +0,0 @@ -import chalk from 'chalk' -import { createFindPackageJsonIterator } from '../../../findPackageJson' -import { Template } from '../Template' -import { validateSemverVersion } from '../../../utils' -import { MIN_SUPPORTED_VERSION } from '../../versions' -import * as babel from '@babel/core' - -export const ReactScriptsTemplate: Template = { - recommendedComponentFolder: 'src', - message: 'It looks like you are using create-react-app.', - dependencies: ['@cypress/webpack-dev-server'], - getExampleUrl: ({ componentFolder }) => { - return componentFolder === 'src' - ? 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts' - : 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts-folder' - }, - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/react-scripts\')'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config)', - ].join('\n'), { preserveComments: true }), - } - }, - test: () => { - // TODO also determine ejected create react app - const packageJsonIterator = createFindPackageJsonIterator(process.cwd()) - - return packageJsonIterator.map(({ dependencies, devDependencies }) => { - if (dependencies || devDependencies) { - const allDeps = { ...devDependencies, ...dependencies } || {} - - if (!allDeps['react-scripts']) { - return { success: false } - } - - if ( - !validateSemverVersion( - allDeps['react-scripts'], - MIN_SUPPORTED_VERSION['react-scripts'], - ) - ) { - console.warn( - `It looks like you are using ${chalk.green( - 'create-react-app', - )}, but we support only projects with version ${chalk.bold( - MIN_SUPPORTED_VERSION['react-scripts'], - )} of react-scripts.`, - ) - - // yey found the template - return { success: false } - } - - return { success: true } - } - - return { success: false } - }) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts deleted file mode 100644 index 4212fcc04f5..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { snapshotPluginsAstCode } from '../../../test-utils' -import { WebpackTemplate } from './reactWebpackFile' - -describe('webpack-file install template', () => { - afterEach(mockFs.restore) - - it('resolves webpack.config.js', () => { - mockFs({ - '/webpack.config.js': 'module.exports = { }', - }) - - const { success, payload } = WebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/webpack.config.js') - }) - - it('finds the closest package.json and tries to fetch webpack config path from scrips', () => { - mockFs({ - '/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config configs/webpack.js', - }, - }), - }) - - const { success, payload } = WebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/configs/webpack.js') - }) - - it('looks for package.json in the upper folder', () => { - mockFs({ - '/i/am/in/some/deep/folder/withFile': 'test', - '/somewhere/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config somewhere/configs/webpack.js', - }, - }), - }) - - const { success, payload } = WebpackTemplate.test( - 'i/am/in/some/deep/folder', - ) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/somewhere/configs/webpack.js') - }) - - it('returns success:false if cannot find webpack config', () => { - mockFs({ - '/a.js': '1', - '/b.js': '2', - }) - - const { success, payload } = WebpackTemplate.test('/') - - expect(success).to.equal(false) - expect(payload).to.equal(undefined) - }) - - it('correctly generates plugins config when webpack config path is missing', () => { - snapshotPluginsAstCode(WebpackTemplate) - }) - - it('correctly generates plugins config when webpack config path is provided', () => { - snapshotPluginsAstCode(WebpackTemplate, { webpackConfigPath: '/config/webpack.config.js' }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts deleted file mode 100644 index 6bc89f7fe0f..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as babel from '@babel/core' -import path from 'path' -import { Template } from '../Template' -import { findWebpackConfig } from '../templateUtils' - -export const WebpackTemplate: Template<{ webpackConfigPath: string }> = { - message: - 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-file' - }, - recommendedComponentFolder: 'cypress/component', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: (payload, { cypressProjectRoot }) => { - const includeWarnComment = !payload - const webpackConfigPath = payload - ? path.relative(cypressProjectRoot, payload.webpackConfigPath) - : './webpack.config.js' - - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require("@cypress/react/plugins/load-webpack")'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config, {', - includeWarnComment - ? ' // TODO replace with valid webpack config path' - : '', - ` webpackFilename: '${webpackConfigPath}'`, - '})', - ].join('\n'), { preserveComments: true }), - } - }, - test: (root) => { - const webpackConfigPath = findWebpackConfig(root) - - return webpackConfigPath ? { - success: true, - payload: { webpackConfigPath }, - } : { - success: false, - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js deleted file mode 100644 index cffd3bce12f..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @type import("webpack").Configuration */ -const webpackConfig = { - resolve: { - extensions: ['.js', '.ts', '.jsx', '.tsx'], - }, - mode: 'development', - devtool: false, - output: { - publicPath: '/', - chunkFilename: '[name].bundle.js', - }, - // TODO: update with valid configuration for your components - module: { - rules: [ - { - test: /\.(js|jsx|mjs|ts|tsx)$/, - loader: 'babel-loader', - options: { cacheDirectory: path.resolve(__dirname, '.babel-cache') }, - }, - ] - }, -} - -on('dev-server:start', (options) => startDevServer({ options, webpackConfig })) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts deleted file mode 100644 index 8765be52910..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fs from 'fs' -import path from 'path' -import * as babel from '@babel/core' -import chalk from 'chalk' -import { Template } from '../Template' - -export const WebpackOptions: Template = { - // this should never show ideally - message: `Unable to detect where webpack options are.`, - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-options' - }, - test: () => ({ success: false }), - recommendedComponentFolder: 'src', - dependencies: ['webpack', '@cypress/webpack-dev-server'], - getPluginsCodeAst: () => { - return { - RequireAst: babel.template.ast([ - 'const path = require("path")', - 'const { startDevServer } = require("@cypress/webpack-dev-Server")', - ].join('\n')), - IfComponentTestingPluginsAst: babel.template.ast( - fs.readFileSync(path.resolve(__dirname, 'webpack-options-module-exports.template.js'), { encoding: 'utf-8' }), - { preserveComments: true }, - ), - } - }, - printHelper: () => { - console.log( - `${chalk.inverse('Important:')} this configuration is using ${chalk.blue( - 'new webpack configuration', - )} to bundle components. If you are using some framework (e.g. next) or bundling tool (e.g. rollup/vite) consider using them to bundle component specs for cypress. \n`, - ) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts deleted file mode 100644 index c7b7e2e6f9a..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WebpackOptions } from './webpack-options' -import { snapshotPluginsAstCode } from '../../../test-utils' - -describe('webpack-options template', () => { - it('correctly generates plugins config', () => snapshotPluginsAstCode(WebpackOptions)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts b/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts deleted file mode 100644 index 8b1f8e978f3..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import findUp from 'find-up' -import path from 'path' -import { createFindPackageJsonIterator } from '../../findPackageJson' - -export function extractWebpackConfigPathFromScript (script: string) { - if (script.includes('webpack ') || script.includes('webpack-dev-server ')) { - const webpackCliArgs = script.split(' ').map((part) => part.trim()) - const configArgIndex = webpackCliArgs.findIndex((arg) => arg === '--config') - - return configArgIndex === -1 ? null : webpackCliArgs[configArgIndex + 1] - } - - return null -} - -export function findWebpackConfig (root: string) { - const webpackConfigPath = findUp.sync('webpack.config.js', { cwd: root }) - - if (webpackConfigPath) { - return webpackConfigPath - } - - const packageJsonIterator = createFindPackageJsonIterator(root) - - const { success, payload } = packageJsonIterator.map(({ scripts }, packageJsonPath) => { - if (!scripts) { - return { success: false } - } - - for (const script of Object.values(scripts)) { - const webpackConfigRelativePath = extractWebpackConfigPathFromScript( - script, - ) - - if (webpackConfigRelativePath) { - const directoryRoot = path.resolve(packageJsonPath, '..') - const webpackConfigPath = path.resolve( - directoryRoot, - webpackConfigRelativePath, - ) - - return { - success: true, - payload: { webpackConfigPath }, - } - } - } - - return { success: false } - }) - - return success ? payload?.webpackConfigPath : null -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts deleted file mode 100644 index 512f4ff6ee8..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Template } from '../Template' -import { VueCliTemplate } from './vueCli' -import { VueWebpackTemplate } from './vueWebpackFile' - -export const vueTemplates: Record> = { - webpack: VueWebpackTemplate, - 'vue-cli': VueCliTemplate, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts deleted file mode 100644 index 29bb76b76d2..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { snapshotPluginsAstCode } from '../../../test-utils' -import { VueCliTemplate } from './vueCli' - -describe('vue webpack-file install template', () => { - beforeEach(mockFs.restore) - - it('resolves webpack.config.js', () => { - mockFs({ - '/package.json': JSON.stringify({ - 'devDependencies': { - '@vue/cli-plugin-babel': '~4.5.0', - '@vue/cli-plugin-eslint': '~4.5.0', - '@vue/cli-plugin-router': '~4.5.0', - '@vue/cli-service': '~4.5.0', - }, - }), - }) - - const { success } = VueCliTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('returns success:false if vue-cli-service is not installed', () => { - mockFs({ - '/package.json': JSON.stringify({ - 'devDependencies': { - 'webpack': '*', - 'vue': '2.x', - }, - }), - }) - - const { success } = VueCliTemplate.test('/') - - expect(success).to.equal(false) - }) - - it('correctly generates plugins for vue-cli-service', () => { - snapshotPluginsAstCode(VueCliTemplate) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts deleted file mode 100644 index f920c85fa89..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as babel from '@babel/core' -import { scanFSForAvailableDependency } from '../../../findPackageJson' -import { Template } from '../Template' - -export const VueCliTemplate: Template = { - message: - 'It looks like you are using vue-cli-service to run and build an application.', - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', - recommendedComponentFolder: 'src', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast([ - 'const { startDevServer } = require("@cypress/webpack-dev-server")', - `const webpackConfig = require("@vue/cli-service/webpack.config.js")`, - ].join('\n')), - IfComponentTestingPluginsAst: babel.template.ast([ - `on('dev-server:start', (options) => startDevServer({ options, webpackConfig }))`, - ].join('\n'), { preserveComments: true }), - } - }, - test: (root) => { - const hasVueCliService = scanFSForAvailableDependency(root, { '@vue/cli-service': '>=4' }) - - return { - success: hasVueCliService, - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts deleted file mode 100644 index e0b058958ef..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { snapshotPluginsAstCode } from '../../../test-utils' -import { VueWebpackTemplate } from './vueWebpackFile' - -describe('vue webpack-file install template', () => { - beforeEach(mockFs.restore) - - it('resolves webpack.config.js', () => { - mockFs({ - '/webpack.config.js': 'module.exports = { }', - }) - - const { success, payload } = VueWebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/webpack.config.js') - }) - - it('finds the closest package.json and tries to fetch webpack config path from scrips', () => { - mockFs({ - '/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config configs/webpack.js', - }, - }), - }) - - const { success, payload } = VueWebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/configs/webpack.js') - }) - - it('looks for package.json in the upper folder', () => { - mockFs({ - '/some/deep/folder/withFile': 'test', - '/somewhere/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config somewhere/configs/webpack.js', - }, - }), - }) - - const { success, payload } = VueWebpackTemplate.test( - '/some/deep/folder', - ) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/somewhere/configs/webpack.js') - }) - - it('returns success:false if cannot find webpack config', () => { - mockFs({ - '/a.js': '1', - '/b.js': '2', - }) - - const { success, payload } = VueWebpackTemplate.test('/') - - expect(success).to.equal(false) - expect(payload).to.equal(undefined) - }) - - it('correctly generates plugins config when webpack config path is missing', () => { - snapshotPluginsAstCode(VueWebpackTemplate) - }) - - it('correctly generates plugins config when webpack config path is provided', () => { - snapshotPluginsAstCode(VueWebpackTemplate, { webpackConfigPath: '/build/webpack.config.js' }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts deleted file mode 100644 index 106145b4067..00000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as babel from '@babel/core' -import path from 'path' -import { Template } from '../Template' -import { findWebpackConfig } from '../templateUtils' - -export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = { - message: - 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', - recommendedComponentFolder: 'cypress/component', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: (payload, { cypressProjectRoot }) => { - const includeWarnComment = !payload - const webpackConfigPath = payload - ? path.relative(cypressProjectRoot, payload.webpackConfigPath) - : './webpack.config.js' - - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast([ - 'const { startDevServer } = require("@cypress/webpack-dev-server")', - - `const webpackConfig = require("${webpackConfigPath}")`, - includeWarnComment - ? '// TODO replace with valid webpack config path' - : '', - ].join('\n'), { preserveComments: true }), - IfComponentTestingPluginsAst: babel.template.ast([ - `on('dev-server:start', (options) => startDevServer({ options, webpackConfig }))`, - ].join('\n')), - } - }, - test: (root) => { - const webpackConfigPath = findWebpackConfig(root) - - return webpackConfigPath ? { - success: true, - payload: { webpackConfigPath }, - } : { - success: false, - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/versions.ts b/npm/create-cypress-tests/src/component-testing/versions.ts deleted file mode 100644 index 734122ff5fe..00000000000 --- a/npm/create-cypress-tests/src/component-testing/versions.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const MIN_SUPPORTED_VERSION = { - 'react-scripts': '^=3.x || ^=4.x', - next: '^=9.x || ^=10.x', -} diff --git a/npm/create-cypress-tests/src/findPackageJson.ts b/npm/create-cypress-tests/src/findPackageJson.ts deleted file mode 100644 index 81858d319c9..00000000000 --- a/npm/create-cypress-tests/src/findPackageJson.ts +++ /dev/null @@ -1,116 +0,0 @@ -import path from 'path' -import fs from 'fs' -import findUp from 'find-up' -import { validateSemverVersion } from './utils' - -type PackageJsonLike = { - name?: string - scripts?: Record - dependencies?: Record - devDependencies?: Record - [key: string]: unknown -} - -type FindPackageJsonResult = - | { - packageData: PackageJsonLike - filename: string - done: false - } - | { - packageData: undefined - filename: undefined - done: true - } - -/** - * Return the parsed package.json that we find in a parent folder. - * - * @returns {Object} Value, filename and indication if the iteration is done. - */ -export function createFindPackageJsonIterator (rootPath = process.cwd()) { - function scanForPackageJson (cwd: string): FindPackageJsonResult { - const packageJsonPath = findUp.sync('package.json', { cwd }) - - if (!packageJsonPath) { - return { - packageData: undefined, - filename: undefined, - done: true, - } - } - - const packageData = JSON.parse( - fs.readFileSync(packageJsonPath, { - encoding: 'utf-8', - }), - ) - - return { - packageData, - filename: packageJsonPath, - done: false, - } - } - - return { - map: ( - cb: ( - data: PackageJsonLike, - packageJsonPath: string, - ) => { success: boolean, payload?: TPayload }, - ) => { - let stepPathToScan = rootPath - - // eslint-disable-next-line - while (true) { - const result = scanForPackageJson(stepPathToScan) - - if (result.done) { - // didn't find the package.json - return { success: false } - } - - if (result.packageData) { - const cbResult = cb(result.packageData, result.filename) - - if (cbResult.success) { - return { success: true, payload: cbResult.payload } - } - } - - const nextStepPathToScan = path.resolve(stepPathToScan, '..') - - if (nextStepPathToScan === stepPathToScan) { - // we are at the root. Give up - return { success: false } - } - - stepPathToScan = nextStepPathToScan - } - }, - } -} - -export function scanFSForAvailableDependency (cwd: string, lookingForDeps: Record) { - const { success } = createFindPackageJsonIterator(cwd) - .map(({ dependencies, devDependencies }, path) => { - if (!dependencies && !devDependencies) { - return { success: false } - } - - return { - success: Object.entries({ ...dependencies, ...devDependencies }) - .some(([dependency, version]) => { - return ( - Boolean(lookingForDeps[dependency]) - && validateSemverVersion(version, lookingForDeps[dependency] as string, dependency) - ) - }), - } - }) - - return success -} - -export type PackageJsonIterator = ReturnType diff --git a/npm/create-cypress-tests/src/index.ts b/npm/create-cypress-tests/src/index.ts deleted file mode 100644 index 9e6e68e3f4e..00000000000 --- a/npm/create-cypress-tests/src/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node -import { program } from 'commander' -import { main } from './main' -import { version } from '../package.json' - -program -.option('--ignore-examples', 'Ignore generating example tests and fixtures by creating one ready-to-fill spec file') -.option('--use-npm', 'Use npm even if yarn is available') -.option('--ignore-ts', 'Ignore typescript if available') -.option('--component-tests', 'Run component testing installation without asking') - -program.version(version, '-v --version') -program.parse(process.argv) - -main({ - useNpm: program.useNpm, - ignoreTs: program.ignoreTs, - ignoreExamples: Boolean(program.ignoreExamples), - setupComponentTesting: program.componentTests, -}).catch(console.error) diff --git a/npm/create-cypress-tests/src/initialTemplate.ts b/npm/create-cypress-tests/src/initialTemplate.ts deleted file mode 100644 index 332df33e389..00000000000 --- a/npm/create-cypress-tests/src/initialTemplate.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from 'path' -import fs from 'fs-extra' - -const INITIAL_TEMPLATE_PATH = path.resolve(__dirname, '..', 'initial-template') - -export async function getInitialSupportFilesPaths () { - return ( - await fs.readdir(path.join(INITIAL_TEMPLATE_PATH, 'support')) - ).map((filename) => path.join(INITIAL_TEMPLATE_PATH, 'support', filename)) -} - -export function getInitialPluginsFilePath () { - return path.join(INITIAL_TEMPLATE_PATH, 'plugins', 'index.js') -} - -export function getInitialTsConfigPath () { - return path.join(INITIAL_TEMPLATE_PATH, 'tsconfig.json') -} diff --git a/npm/create-cypress-tests/src/installCypress.ts b/npm/create-cypress-tests/src/installCypress.ts deleted file mode 100644 index 9abd21345b5..00000000000 --- a/npm/create-cypress-tests/src/installCypress.ts +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'fs-extra' -import findUp from 'find-up' -import path from 'path' -import { installDependency } from './utils' -import chalk from 'chalk' -import ora from 'ora' -import * as initialTemplate from './initialTemplate' - -type InstallCypressOpts = { - useYarn: boolean - useTypescript: boolean - ignoreExamples: boolean -} - -async function copyFiles ({ ignoreExamples, useTypescript }: InstallCypressOpts) { - let fileSpinner = ora('Creating config files').start() - - await fs.outputFile(path.resolve(process.cwd(), useTypescript ? 'cypress.config.ts' : 'cypress.config.js'), useTypescript ? `export default {}` : `module.exports = {}\n`) - await fs.copy( - initialTemplate.getInitialPluginsFilePath(), - path.resolve('cypress', 'plugins/index.js'), - ) - - const supportFiles: string[] = await initialTemplate.getInitialSupportFilesPaths() - - await Promise.all( - supportFiles.map((supportFilePath) => { - const newSupportFilePath = path.resolve('cypress', 'support', path.basename(supportFilePath)) - - return fs.copy(supportFilePath, newSupportFilePath) - }), - ) - - if (useTypescript) { - await fs.copy(initialTemplate.getInitialTsConfigPath(), path.resolve('cypress', 'tsconfig.json')) - } - - // TODO think about better approach - if (ignoreExamples) { - const dummySpec = [ - 'describe("Spec", () => {', - '', - '})', - '', - ].join('\n') - - const specFileName = useTypescript ? 'spec.cy.ts' : 'spec.cy.js' - const specFileToCreate = path.resolve('cypress', 'e2e', specFileName) - - await fs.outputFile(specFileToCreate, dummySpec) - console.log(`In order to ignore examples a spec file ${chalk.green(path.relative(process.cwd(), specFileToCreate))}.`) - } - - fileSpinner.succeed() -} - -export async function findInstalledOrInstallCypress (options: InstallCypressOpts) { - const configFile = options.useTypescript ? 'cypress.config.ts' : 'cypress.config.js' - let cypressConfigPath = await findUp(configFile) - - if (!cypressConfigPath) { - await installDependency('cypress', options) - await copyFiles(options) - - cypressConfigPath = await findUp(configFile) - } - - if (!cypressConfigPath) { - throw new Error('Unexpected error during cypress installation.') - } - - const config = await import(cypressConfigPath) - - return { - cypressConfigPath, - config: config.default, - } -} diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts deleted file mode 100644 index 93cdaa91a46..00000000000 --- a/npm/create-cypress-tests/src/main.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { expect, use } from 'chai' -import path from 'path' -import sinon, { SinonStub, SinonSpy, SinonSpyCallApi } from 'sinon' -import mockFs from 'mock-fs' -import fsExtra from 'fs-extra' -import { main } from './main' -import sinonChai from 'sinon-chai' -import childProcess from 'child_process' - -use(sinonChai) - -function mockFsWithInitialTemplate (...args: Parameters) { - const [fsConfig, options] = args - - mockFs({ - ...fsConfig, - // @ts-expect-error Load required template files - [path.resolve(__dirname, '..', 'initial-template')]: mockFs.load(path.resolve(__dirname, '..', 'initial-template')), - }, options) -} - -function someOfSpyCallsIncludes (spy: any, logPart: string) { - return spy.getCalls().some( - (spy: SinonSpyCallApi) => { - return spy.args.some((callArg) => typeof callArg === 'string' && callArg.includes(logPart)) - }, - ) -} - -describe('create-cypress-tests', () => { - let promptSpy: SinonStub | null = null - let logSpy: SinonSpy | null = null - let errorSpy: SinonSpy | null = null - let execStub: SinonStub | null = null - let fsCopyStub: SinonStub | null = null - let processExitStub: SinonStub | null = null - - beforeEach(() => { - logSpy = sinon.spy(global.console, 'log') - errorSpy = sinon.spy(global.console, 'error') - // @ts-ignore - execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) - // @ts-ignore - fsCopyStub = sinon.stub(fsExtra, 'copy').returns(Promise.resolve()) - processExitStub = sinon.stub(process, 'exit').callsFake(() => { - throw new Error('process.exit should not be called') - }) - }) - - afterEach(() => { - mockFs.restore() - logSpy?.restore() - promptSpy?.restore() - execStub?.restore() - fsCopyStub?.restore() - processExitStub?.restore() - execStub?.restore() - errorSpy?.restore() - }) - - it('Install cypress if no config found', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect(execStub).calledWith('yarn add cypress --dev') - }) - - it('Uses npm if yarn is not available', async () => { - execStub - ?.onFirstCall().callsFake((command, callback) => callback('yarn is not available')) - ?.onSecondCall().callsFake((command, callback) => callback()) - ?.onThirdCall().callsFake((command, callback) => callback()) - - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - expect(execStub).calledWith('npm install -D cypress') - }) - - it('Uses npm if --use-npm was provided', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect(execStub).calledWith('npm install -D cypress') - }) - - it('Prints correct commands helper for npm', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - expect(someOfSpyCallsIncludes(logSpy, 'npx cypress open')).to.be.true - }) - - it('Prints correct commands helper for yarn', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - expect(someOfSpyCallsIncludes(logSpy, 'yarn cypress open')).to.be.true - }) - - it('Fails if git repository have untracked or uncommited files', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - execStub?.callsFake((_, callback) => callback(null, { stdout: 'test' })) - processExitStub?.callsFake(() => {}) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect( - someOfSpyCallsIncludes(errorSpy, 'This repository has untracked files or uncommmited changes.'), - ).to.equal(true) - - expect(processExitStub).to.be.called - }) - - context('e2e fs tests', () => { - const e2eTestOutputPath = path.resolve(__dirname, 'test-output') - - beforeEach(async () => { - fsCopyStub?.restore() - mockFs.restore() - sinon.stub(process, 'cwd').returns(e2eTestOutputPath) - - await fsExtra.remove(e2eTestOutputPath) - await fsExtra.mkdir(e2eTestOutputPath) - }) - - it('Copies plugins and support files', async () => { - await fsExtra.outputFile( - path.join(e2eTestOutputPath, 'package.json'), - JSON.stringify({ name: 'test' }, null, 2), - ) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'))).to.equal(true) - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'e2e.js'))).to.equal(true) - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'commands.js'))).to.equal(true) - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress.config.ts'))).to.equal(true) - }) - - it('Copies tsconfig if typescript is installed', async () => { - await fsExtra.outputFile( - path.join(e2eTestOutputPath, 'package.json'), - JSON.stringify({ - name: 'test-typescript', - dependencies: { typescript: '^4.0.0' }, - }, null, 2), - ) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) - console.log(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) - }) - }) -}) diff --git a/npm/create-cypress-tests/src/main.ts b/npm/create-cypress-tests/src/main.ts deleted file mode 100644 index 818a58b9432..00000000000 --- a/npm/create-cypress-tests/src/main.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from 'fs' -import findUp from 'find-up' -import chalk from 'chalk' -import util from 'util' -import inquirer from 'inquirer' -import { initComponentTesting } from './component-testing/init-component-testing' -import { exec } from 'child_process' -import { scanFSForAvailableDependency } from './findPackageJson' -import { findInstalledOrInstallCypress } from './installCypress' - -type MainArgv = { - useNpm: boolean - ignoreTs: boolean - ignoreExamples: boolean - setupComponentTesting: boolean -} - -async function getGitStatus () { - const execAsync = util.promisify(exec) - - try { - let { stdout } = await execAsync(`git status --porcelain`) - - console.log(stdout) - - return stdout.trim() - } catch (e) { - return '' - } -} - -async function shouldUseYarn () { - const execAsync = util.promisify(exec) - - return execAsync('yarn --version') - .then(() => true) - .catch(() => false) -} - -function shouldUseTypescript () { - return scanFSForAvailableDependency(process.cwd(), { typescript: '*' }) -} - -async function askForComponentTesting () { - const { shouldSetupComponentTesting } = await inquirer.prompt({ - type: 'confirm', - name: 'shouldSetupComponentTesting', - message: `Do you want to setup ${chalk.cyan('component testing')}? ${chalk.grey('You can do this later by rerunning this command')}.`, - }) - - return shouldSetupComponentTesting -} - -function printCypressCommandsHelper (options: { shouldSetupComponentTesting: boolean, useYarn: boolean }) { - const printCommand = (command: string, description: string) => { - const displayedRunner = options.useYarn ? 'yarn' : 'npx' - - console.log() - console.log(chalk.cyan(` ${displayedRunner} ${command}`)) - console.log(` ${description}`) - } - - printCommand('cypress open', 'Opens cypress local development app.') - printCommand('cypress run', 'Runs tests in headless mode.') - - if (options.shouldSetupComponentTesting) { - printCommand('cypress open --component', 'Opens Cypress component testing interactive mode.') - printCommand('cypress run-ct', 'Runs all Cypress component testing suites.') - } -} - -export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExamples }: MainArgv) { - const rootPackageJsonPath = await findUp('package.json') - const useYarn = useNpm === true ? false : await shouldUseYarn() - const useTypescript = ignoreTs ? false : shouldUseTypescript() - - if (!rootPackageJsonPath) { - console.log(`${chalk.bold.red(`It looks like you are running cypress installation wizard outside of npm module.`)}\nIf you would like to setup a new project for cypress tests please run the ${chalk.inverse(useNpm ? ' npm init ' : ' yarn init ')} first.`) - process.exit(1) - } - - const { name = 'unknown', version = '0.0.0' } = JSON.parse(fs.readFileSync(rootPackageJsonPath).toString()) - - console.log(`Running ${chalk.green('cypress 🌲')} installation wizard for ${chalk.cyan(`${name}@${version}`)}`) - - const gitStatus = await getGitStatus() - - if (gitStatus) { - console.error(`\n${chalk.bold.red('This repository has untracked files or uncommmited changes.')}\nThis command will ${chalk.cyan('make changes in the codebase')}, so please remove untracked files, stash or commit any changes, and try again.`) - process.exit(1) - } - - const { config, cypressConfigPath } = await findInstalledOrInstallCypress({ useYarn, useTypescript, ignoreExamples }) - const shouldSetupComponentTesting = setupComponentTesting ?? await askForComponentTesting() - - if (shouldSetupComponentTesting) { - await initComponentTesting({ config, cypressConfigPath, useYarn }) - } - - console.log(`\n👍 Success! Cypress is installed and ready to run tests.`) - printCypressCommandsHelper({ useYarn, shouldSetupComponentTesting }) - - console.log(`\nHappy testing with ${chalk.green('cypress.io')} 🌲\n`) -} - -export { scanFSForAvailableDependency } diff --git a/npm/create-cypress-tests/src/test-utils.ts b/npm/create-cypress-tests/src/test-utils.ts deleted file mode 100644 index 6021690678b..00000000000 --- a/npm/create-cypress-tests/src/test-utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as babel from '@babel/core' -import snapshot from 'snap-shot-it' -import mockFs from 'mock-fs' -import { SinonSpyCallApi } from 'sinon' -import { createTransformPluginsFileBabelPlugin } from './component-testing/babel/babelTransform' -import { Template } from './component-testing/templates/Template' - -export function someOfSpyCallsIncludes (spy: any, logPart: string) { - return spy.getCalls().some( - (spy: SinonSpyCallApi) => { - return spy.args.some((callArg) => typeof callArg === 'string' && callArg.includes(logPart)) - }, - ) -} - -export function snapshotPluginsAstCode (template: Template, payload?: T) { - mockFs.restore() - const code = [ - 'const something = require("something")', - 'module.exports = (on) => {', - '};', - ].join('\n') - - const babelPlugin = createTransformPluginsFileBabelPlugin(template.getPluginsCodeAst(payload ?? null, { cypressProjectRoot: '/' })) - const output = babel.transformSync(code, { - plugins: [babelPlugin], - }) - - if (!output || !output.code) { - throw new Error('Babel transform output is empty.') - } - - snapshot(output.code) -} diff --git a/npm/create-cypress-tests/src/utils.ts b/npm/create-cypress-tests/src/utils.ts deleted file mode 100644 index e79055e6028..00000000000 --- a/npm/create-cypress-tests/src/utils.ts +++ /dev/null @@ -1,76 +0,0 @@ -import semver from 'semver' -import chalk from 'chalk' -import ora from 'ora' -import util from 'util' -import { exec } from 'child_process' - -/** - * Compare available version range with the provided version from package.json - * @param packageName Package name used to display a helper message to user. - */ -export function validateSemverVersion ( - version: string, - allowedVersionRange: string, - packageName?: string, -) { - let isValid: boolean - - try { - const minAvailableVersion = semver.minVersion(version)?.raw - - isValid = Boolean( - minAvailableVersion && - semver.satisfies(minAvailableVersion, allowedVersionRange), - ) - } catch (e) { - // handle not semver versions like "latest", "git:" or "file:" - isValid = false - } - - if (!isValid && packageName) { - const packageNameSymbol = chalk.green(packageName) - - console.warn( - `It seems like you are using ${packageNameSymbol} with version ${chalk.bold( - version, - )}, however we support only ${packageNameSymbol} projects with version ${chalk.bold( - allowedVersionRange, - )}. \n`, - ) - } - - return isValid -} - -export async function installDependency (name: string, options: { useYarn: boolean}) { - const commandToRun = options.useYarn ? `yarn add ${name} --dev` : `npm install -D ${name}` - let cliSpinner = ora(`Installing ${name} ${chalk.gray(`(${commandToRun})`)}`).start() - - try { - // do this inside function for test stubbing - const execAsync = util.promisify(exec) - - await execAsync(commandToRun) - } catch (e) { - cliSpinner.fail(`Can not install ${name} using ${chalk.inverse(commandToRun)})}`) - console.log(e) - - process.exit(1) - } - - cliSpinner.succeed() -} - -export async function prettifyCode (finalCode?: string | null) { - try { - const maybePrettier = require('prettier') - - if (maybePrettier && maybePrettier.format) { - finalCode = maybePrettier.format(finalCode, { parser: 'babel' }) - } - } catch (e) { - return null - } finally { - return finalCode - } -} diff --git a/npm/create-cypress-tests/tsconfig.json b/npm/create-cypress-tests/tsconfig.json deleted file mode 100644 index 5cf13aaaaa0..00000000000 --- a/npm/create-cypress-tests/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist", - "rootDir": ".", - "esModuleInterop": true, - "allowJs": true, - "allowSyntheticDefaultImports": true, - "declaration": true, - "moduleResolution": "node", - "strict": true, - "strictNullChecks": true, - "resolveJsonModule": true, - "module": "CommonJS", - "target": "ES2018", - "types": [ - "node", - ], - "lib": [ - "ES2018" - ], - "noImplicitAny": true - }, - "exclude": [ - "./src/**/*.test.ts", - "node_modules" - ], - "include": [ - "./src/**/*.ts", - ] -} diff --git a/npm/create-cypress-tests/tsconfig.test.json b/npm/create-cypress-tests/tsconfig.test.json deleted file mode 100644 index df01205d9a4..00000000000 --- a/npm/create-cypress-tests/tsconfig.test.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": true, - "types": [ - "mocha" - ] - }, - "include": [ - "./src/**/*.test.ts" - ] -} \ No newline at end of file diff --git a/npm/cypress-schematic/.eslintignore b/npm/cypress-schematic/.eslintignore new file mode 100644 index 00000000000..79afe972da7 --- /dev/null +++ b/npm/cypress-schematic/.eslintignore @@ -0,0 +1,5 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json +**/cypress/fixtures \ No newline at end of file diff --git a/npm/cypress-schematic/.releaserc.js b/npm/cypress-schematic/.releaserc.js index 7b15992ed70..17d3bb87147 100644 --- a/npm/cypress-schematic/.releaserc.js +++ b/npm/cypress-schematic/.releaserc.js @@ -1,6 +1,3 @@ module.exports = { - ...require('../../.releaserc.base'), - branches: [ - { name: 'master', channel: 'latest' }, - ], + ...require('../../.releaserc'), } diff --git a/npm/cypress-schematic/CHANGELOG.md b/npm/cypress-schematic/CHANGELOG.md index aa22d736a24..f4e8f167c90 100644 --- a/npm/cypress-schematic/CHANGELOG.md +++ b/npm/cypress-schematic/CHANGELOG.md @@ -1,3 +1,50 @@ +# [@cypress/schematic-v2.5.2](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.5.1...@cypress/schematic-v2.5.2) (2024-06-07) + + +### Bug Fixes + +* update cypress to Typescript 5 ([#29568](https://github.com/cypress-io/cypress/issues/29568)) ([f3b6766](https://github.com/cypress-io/cypress/commit/f3b67666a5db0438594339c379cf27e1fd1e4abc)) + +# [@cypress/schematic-v2.5.1](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.5.0...@cypress/schematic-v2.5.1) (2023-09-07) + +# [@cypress/schematic-v2.5.0](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.4.0...@cypress/schematic-v2.5.0) (2023-01-25) + + +### Bug Fixes + +* **cypress-schematic:** do not disable e2e support file ([#25400](https://github.com/cypress-io/cypress/issues/25400)) ([d52f3dc](https://github.com/cypress-io/cypress/commit/d52f3dc8cbf0daf276d1ec5db95e065cf703b070)) + + +### Features + +* Add Angular CT Schematic ([#24374](https://github.com/cypress-io/cypress/issues/24374)) ([af6be6f](https://github.com/cypress-io/cypress/commit/af6be6f27d8547bbfbed60cd3a7524861d842548)), closes [#23645](https://github.com/cypress-io/cypress/issues/23645) [#23673](https://github.com/cypress-io/cypress/issues/23673) [#23696](https://github.com/cypress-io/cypress/issues/23696) + +# [@cypress/schematic-v2.4.0](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.3.0...@cypress/schematic-v2.4.0) (2022-12-02) + + +### Features + +* Queries, Detached DOM, and Retry-Ability ([#24628](https://github.com/cypress-io/cypress/issues/24628)) ([9ae911f](https://github.com/cypress-io/cypress/commit/9ae911f396fa6cac0a1464537d1492d68cbb1898)), closes [#23665](https://github.com/cypress-io/cypress/issues/23665) [#24022](https://github.com/cypress-io/cypress/issues/24022) [#23791](https://github.com/cypress-io/cypress/issues/23791) [#24203](https://github.com/cypress-io/cypress/issues/24203) [#24417](https://github.com/cypress-io/cypress/issues/24417) + +# [@cypress/schematic-v2.3.0](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.2.0...@cypress/schematic-v2.3.0) (2022-11-01) + + +### Features + +* introduce v8 snapshots to improve startup performance ([#24295](https://github.com/cypress-io/cypress/issues/24295)) ([b0c0eaa](https://github.com/cypress-io/cypress/commit/b0c0eaa508bb6dafdc1997bc00fb7ed6f5bcc160)) + +# [@cypress/schematic-v2.2.0](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.1.1...@cypress/schematic-v2.2.0) (2022-10-13) + + +### Bug Fixes + +* Detect user-configured browsers ([#23446](https://github.com/cypress-io/cypress/issues/23446)) ([a75d3ec](https://github.com/cypress-io/cypress/commit/a75d3ec81f3405db6721a89875d89cdca0109013)) + + +### Features + +* _addQuery() ([#23665](https://github.com/cypress-io/cypress/issues/23665)) ([41fc535](https://github.com/cypress-io/cypress/commit/41fc535dca51cda4e40b5d9fc827d8bff534f3d1)) + # [@cypress/schematic-v2.1.1](https://github.com/cypress-io/cypress/compare/@cypress/schematic-v2.1.0...@cypress/schematic-v2.1.1) (2022-08-31) diff --git a/npm/cypress-schematic/README.md b/npm/cypress-schematic/README.md index bd61e482816..71be26beeaa 100644 --- a/npm/cypress-schematic/README.md +++ b/npm/cypress-schematic/README.md @@ -31,7 +31,7 @@ ## Requirements -- Angular 13+ +- Angular 14+ ## Usage ⏯ @@ -49,6 +49,8 @@ To install the schematic via cli arguments (installs both e2e and component test ng add @cypress/schematic --e2e --component ``` +The installation will add this schematic to the [default schematic collections](https://angular.io/guide/workspace-config#angular-cli-configuration-options). This allows you to execute the CLI commands without prefixing them with the package name. + To run Cypress in `open` mode within your project: ```shell script @@ -78,37 +80,43 @@ ng run {project-name}:ct To generate a new e2e spec file: ```shell script -ng generate @cypress/schematic:spec +ng generate spec ``` or (without cli prompt) ```shell script -ng generate @cypress/schematic:spec {name} +ng generate spec {name} ``` To generate a new component spec file: ```shell script -ng generate @cypress/schematic:spec --component +ng generate spec --component ``` or (without cli prompt) ```shell script -ng generate @cypress/schematic:spec {component name} --component +ng generate spec {component name} --component ``` To generate a new component spec file in a specific folder: ```shell script -ng generate @cypress/schematic:spec {component name} --component --path {path relative to project root} +ng generate spec {component name} --component --path {path relative to project root} ``` To generate new component spec files alongside all component files in a project: ```shell script -ng generate @cypress/schematic:specs-ct +ng generate specs-ct +``` + +To generate a new, generic component definition with a component spec file in the given or default project. This wraps the [Angular CLI Component Generator](https://angular.io/cli/generate#component) and supports the same arguments. + +```shell script +ng generate component {component name} ``` ## Builder Options 🛠 @@ -135,9 +143,9 @@ Before running Cypress in `open` mode, ensure that you have started your applica Read our docs to learn more about [launching browsers](https://on.cypress.io/launching-browsers) with Cypress. -### Recording test results to the Cypress Dashboard +### Recording test results to Cypress Cloud -We recommend setting your [Cypress Dashboard](https://on.cypress.io/features-dashboard) recording key as an environment variable and NOT as a builder option when running it in CI. +We recommend setting your [Cypress Cloud](https://on.cypress.io/features-dashboard) recording key as an environment variable and NOT as a builder option when running it in CI. ```json "cypress-run": { @@ -145,7 +153,7 @@ We recommend setting your [Cypress Dashboard](https://on.cypress.io/features-das "options": { "devServerTarget": "{project-name}:serve", "record": true, - "key": "your-cypress-dashboard-recording-key" + "key": "your-cypress-cloud-recording-key" }, "configurations": { "production": { @@ -155,7 +163,7 @@ We recommend setting your [Cypress Dashboard](https://on.cypress.io/features-das } ``` -Read our docs to learn more about [recording test results](https://on.cypress.io/recording-project-runs) to the [Cypress Dashboard](https://on.cypress.io/features-dashboard). +Read our docs to learn more about [recording test results](https://on.cypress.io/recording-project-runs) to [Cypress Cloud](https://on.cypress.io/features-dashboard). ### Specifying a custom config file @@ -166,7 +174,7 @@ It may be useful to have different Cypress configuration files per environment ( "builder": "@cypress/schematic:cypress", "options": { "devServerTarget": "{project-name}:serve", - "configFile": "cypress.production.json" + "configFile": "cypress.production.js" }, "configurations": { "production": { @@ -187,7 +195,7 @@ Read our docs to learn more about all the [configuration options](https://on.cyp "devServerTarget": "{project-name}:serve", "parallel": true, "record": true, - "key": "your-cypress-dashboard-recording-key" + "key": "your-cypress-cloud-recording-key" }, "configurations": { "production": { @@ -226,7 +234,7 @@ Read our docs to learn more about working with [reporters](https://on.cypress.io ### Running the builder with a different baseUrl -You can specify a `baseUrl` that is different than the one in `cypress.json`. There are two ways to do this. +You can specify a `baseUrl` that is different than the one in `cypress.config.js`. There are two ways to do this. 1. Add `baseUrl` to `configurations` like the following: diff --git a/npm/cypress-schematic/package.json b/npm/cypress-schematic/package.json index 85b9e642508..30bf1ceee0e 100644 --- a/npm/cypress-schematic/package.json +++ b/npm/cypress-schematic/package.json @@ -6,37 +6,37 @@ "scripts": { "build": "tsc -p tsconfig.json", "build:watch": "tsc -p tsconfig.json --watch", + "lint": "eslint --ext .ts,.json, .", "test": "mocha -r @packages/ts/register --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json src/**/*.spec.ts" }, "dependencies": { - "@angular-devkit/architect": "^0.1402.1", - "@angular-devkit/core": "^14.2.1", - "@angular-devkit/schematics": "^14.2.1", - "@schematics/angular": "^14.2.1", "jsonc-parser": "^3.0.0", "rxjs": "~6.6.0" }, "devDependencies": { + "@angular-devkit/architect": "^0.1402.1", + "@angular-devkit/core": "^14.2.1", + "@angular-devkit/schematics": "^14.2.1", "@angular-devkit/schematics-cli": "^14.2.1", "@angular/cli": "^14.2.1", + "@schematics/angular": "^14.2.1", "@types/chai-enzyme": "0.6.7", "@types/mocha": "8.0.3", - "@types/node": "^18.0.6", + "@types/node": "^18.17.5", "chai": "4.2.0", "mocha": "3.5.3", - "typescript": "^4.7.4" + "typescript": "^5.4.5" }, "peerDependencies": { - "@angular/cli": ">=12", - "@angular/core": ">=12" + "@angular/cli": ">=14", + "@angular/core": ">=14" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/cypress-io/cypress.git" }, - "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/cypress-schematic#readme", - "author": "Cypress DX Team", + "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/cypress-schematic#readme", "bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fcypress-schematics&template=1-bug-report.md&title=", "keywords": [ "schematics", @@ -53,5 +53,16 @@ "ng-add": { "save": "devDependencies" }, + "nx": { + "targets": { + "build": { + "outputs": [ + "{projectRoot}/src/**/*.js", + "{projectRoot}/src/**/*.d.ts", + "{projectRoot}/src/**/*.js.map" + ] + } + } + }, "schematics": "./src/schematics/collection.json" } diff --git a/npm/cypress-schematic/src/ct.spec.ts b/npm/cypress-schematic/src/ct.spec.ts index 9cf4511c3ac..0e1929dd3be 100644 --- a/npm/cypress-schematic/src/ct.spec.ts +++ b/npm/cypress-schematic/src/ct.spec.ts @@ -9,7 +9,7 @@ const scaffoldAngularProject = async (project: string) => { Fixtures.removeProject(project) await Fixtures.scaffoldProject(project) - await FixturesScaffold.scaffoldProjectNodeModules(project) + await FixturesScaffold.scaffoldProjectNodeModules({ project }) await fs.remove(path.join(projectPath, 'cypress.config.ts')) await fs.remove(path.join(projectPath, 'cypress')) @@ -35,10 +35,10 @@ const copyAngularMount = async (projectPath: string) => { const cypressSchematicPackagePath = path.join(__dirname, '..') -const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-13', 'angular-14'] +const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-14', 'angular-15'] describe('ng add @cypress/schematic / e2e and ct', function () { - this.timeout(1000 * 60 * 4) + this.timeout(1000 * 60 * 5) for (const project of ANGULAR_PROJECTS) { it('should install ct files with option and no component specs', async () => { @@ -49,5 +49,15 @@ describe('ng add @cypress/schematic / e2e and ct', function () { await copyAngularMount(projectPath) await runCommandInProject('yarn ng run angular:ct --watch false --spec src/app/app.component.cy.ts', projectPath) }) + + it('should generate component alongside component spec', async () => { + const projectPath = await scaffoldAngularProject(project) + + await runCommandInProject(`yarn add @cypress/schematic@file:${cypressSchematicPackagePath}`, projectPath) + await runCommandInProject('yarn ng add @cypress/schematic --e2e --component', projectPath) + await copyAngularMount(projectPath) + await runCommandInProject('yarn ng generate c foo', projectPath) + await runCommandInProject('yarn ng run angular:ct --watch false --spec src/app/foo/foo.component.cy.ts', projectPath) + }) } }) diff --git a/npm/cypress-schematic/src/e2e.spec.ts b/npm/cypress-schematic/src/e2e.spec.ts index add1d333e54..bd231bc8ea7 100644 --- a/npm/cypress-schematic/src/e2e.spec.ts +++ b/npm/cypress-schematic/src/e2e.spec.ts @@ -9,7 +9,7 @@ const scaffoldAngularProject = async (project: string) => { Fixtures.removeProject(project) await Fixtures.scaffoldProject(project) - await FixturesScaffold.scaffoldProjectNodeModules(project) + await FixturesScaffold.scaffoldProjectNodeModules({ project }) await fs.remove(path.join(projectPath, 'cypress.config.ts')) await fs.remove(path.join(projectPath, 'cypress')) @@ -24,10 +24,10 @@ const runCommandInProject = (command: string, projectPath: string) => { const cypressSchematicPackagePath = path.join(__dirname, '..') -const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-13', 'angular-14'] +const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-14', 'angular-15'] describe('ng add @cypress/schematic / only e2e', function () { - this.timeout(1000 * 60 * 4) + this.timeout(1000 * 60 * 5) for (const project of ANGULAR_PROJECTS) { it('should install e2e files by default', async () => { diff --git a/npm/cypress-schematic/src/schematics/collection.json b/npm/cypress-schematic/src/schematics/collection.json index 8141b0ec54a..b5f9358df36 100644 --- a/npm/cypress-schematic/src/schematics/collection.json +++ b/npm/cypress-schematic/src/schematics/collection.json @@ -15,6 +15,14 @@ "description": "Create spec files for all Angular components in a project", "factory": "./ng-generate/cypress-ct-tests/index", "schema": "./ng-generate/cypress-ct-tests/schema.json" + }, + "component": { + "description": "Creates a new, generic component definition in the given or default project.", + "aliases": [ + "c" + ], + "factory": "./ng-generate/component/index", + "schema": "./ng-generate/component/schema.json" } } } diff --git a/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress.config.ts.template b/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress.config.ts.template index acfba90c732..e02505b2c17 100644 --- a/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress.config.ts.template +++ b/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress.config.ts.template @@ -3,8 +3,7 @@ import { defineConfig } from 'cypress' export default defineConfig({ <% if (e2e) { %> e2e: { - 'baseUrl': '<%= baseUrl%>', - supportFile: false + 'baseUrl': '<%= baseUrl%>' }, <% } %> <% if (component) { %> diff --git a/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress/e2e/spec.cy.ts.template b/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress/e2e/spec.cy.ts.template index 4067cfd60e2..3e0714918eb 100644 --- a/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress/e2e/spec.cy.ts.template +++ b/npm/cypress-schematic/src/schematics/ng-add/files-core/cypress/e2e/spec.cy.ts.template @@ -1,6 +1,6 @@ describe('My First Test', () => { it('Visits the initial project page', () => { cy.visit('/') - cy.contains('app is running!') + cy.contains('app is running') }) }) diff --git a/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts b/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts index a8374541198..bef3bf33c0e 100644 --- a/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts +++ b/npm/cypress-schematic/src/schematics/ng-add/index.spec.ts @@ -3,6 +3,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing' import { join } from 'path' import { expect } from 'chai' +import { JsonObject } from '@angular-devkit/core' describe('@cypress/schematic: ng-add', () => { const schematicRunner = new SchematicTestRunner( @@ -16,6 +17,7 @@ describe('@cypress/schematic: ng-add', () => { name: 'workspace', newProjectRoot: 'projects', version: '6.0.0', + packageManager: 'yarn', } const appOptions = { @@ -26,6 +28,10 @@ describe('@cypress/schematic: ng-add', () => { skipPackageJson: false, } + const readAngularJson = (tree: UnitTestTree) => { + return tree.readJson('/angular.json') as JsonObject + } + beforeEach(async () => { appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise() appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree).toPromise() @@ -58,4 +64,35 @@ describe('@cypress/schematic: ng-add', () => { expect(files).to.contain('/projects/sandbox/cypress/fixtures/example.json') }) }) + + it('should add @cypress/schematic to the schemaCollections array', async () => { + const tree = await schematicRunner.runSchematicAsync('ng-add', { 'component': true }, appTree).toPromise() + const angularJson = readAngularJson(tree) + const cliOptions = angularJson.cli as JsonObject + + expect(cliOptions).to.eql({ + packageManager: 'yarn', + schematicCollections: ['@cypress/schematic', '@schematics/angular'], + }) + }) + + it('should not overwrite existing schemaCollections array', async () => { + let angularJson = readAngularJson(appTree) + + appTree.overwrite('./angular.json', JSON.stringify({ + ...angularJson, + cli: { + schematicCollections: ['@any/schematic'], + }, + })) + + const tree = await schematicRunner.runSchematicAsync('ng-add', { 'component': true }, appTree).toPromise() + + angularJson = readAngularJson(tree) + const cliOptions = angularJson.cli as JsonObject + + expect(cliOptions).to.eql({ + schematicCollections: ['@cypress/schematic', '@any/schematic', '@schematics/angular'], + }) + }) }) diff --git a/npm/cypress-schematic/src/schematics/ng-add/index.ts b/npm/cypress-schematic/src/schematics/ng-add/index.ts index 8a7953377e9..d3c0dcfd198 100644 --- a/npm/cypress-schematic/src/schematics/ng-add/index.ts +++ b/npm/cypress-schematic/src/schematics/ng-add/index.ts @@ -45,6 +45,7 @@ export default function (_options: any): Rule { addCtSpecs(_options), addCypressTestScriptsToPackageJson(), modifyAngularJson(_options), + addDefaultSchematic(), ])(tree, _context) } } @@ -306,6 +307,33 @@ function modifyAngularJson (options: any): Rule { } } +function addDefaultSchematic (): Rule { + return (tree: Tree, _: SchematicContext) => { + if (tree.exists('./angular.json')) { + const angularJsonVal = getAngularJsonValue(tree) + const angularSchematic = '@schematics/angular' + const cli = { + ...angularJsonVal.cli, + schematicCollections: ['@cypress/schematic', ...angularJsonVal?.cli?.schematicCollections ?? []], + } + + if (cli.schematicCollections.indexOf('@schematics/angular') === -1) { + cli.schematicCollections.push(angularSchematic) + } + + return tree.overwrite( + './angular.json', + JSON.stringify({ + ...angularJsonVal, + cli, + }, null, 2), + ) + } + + throw new SchematicsException('angular.json not found') + } +} + export const getCypressConfigFile = (angularJsonVal: any, projectName: string) => { const project = angularJsonVal.projects[projectName] const tsConfig = project?.architect?.lint?.options?.tsConfig diff --git a/npm/cypress-schematic/src/schematics/ng-generate/component/index.spec.ts b/npm/cypress-schematic/src/schematics/ng-generate/component/index.spec.ts new file mode 100644 index 00000000000..2c60abc2cf6 --- /dev/null +++ b/npm/cypress-schematic/src/schematics/ng-generate/component/index.spec.ts @@ -0,0 +1,60 @@ +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing' +import { expect } from 'chai' +import { join } from 'path' + +describe('ng-generate @cypress/schematic:component', () => { + const schematicRunner = new SchematicTestRunner( + 'schematics', + join(__dirname, '../../collection.json'), + ) + let appTree: UnitTestTree + + const workspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '12.0.0', + } + + const appOptions: Parameters[2] = { + name: 'sandbox', + inlineTemplate: false, + routing: false, + skipTests: false, + skipPackageJson: false, + } + + beforeEach(async () => { + appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'workspace', workspaceOptions).toPromise() + appTree = await schematicRunner.runExternalSchematicAsync('@schematics/angular', 'application', appOptions, appTree).toPromise() + }) + + it('should create cypress ct alongside the generated component', async () => { + const tree = await schematicRunner.runSchematicAsync('component', { name: 'foo', project: 'sandbox' }, appTree).toPromise() + + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.html') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.cy.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/foo.component.css') + expect(tree.files).not.to.contain('/projects/sandbox/src/app/foo/foo.component.spec.ts') + }) + + it('should not generate component which does exist already', async () => { + let tree = await schematicRunner.runSchematicAsync('component', { name: 'foo', project: 'sandbox' }, appTree).toPromise() + + tree = await schematicRunner.runSchematicAsync('component', { name: 'foo', project: 'sandbox' }, appTree).toPromise() + + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.ts').length).to.eq(1) + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.html').length).to.eq(1) + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.cy.ts').length).to.eq(1) + expect(tree.files.filter((f) => f === '/projects/sandbox/src/app/foo/foo.component.css').length).to.eq(1) + }) + + it('should generate component given a component containing a directory', async () => { + const tree = await schematicRunner.runSchematicAsync('component', { name: 'foo/bar', project: 'sandbox' }, appTree).toPromise() + + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.html') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.cy.ts') + expect(tree.files).to.contain('/projects/sandbox/src/app/foo/bar/bar.component.css') + }) +}) diff --git a/npm/cypress-schematic/src/schematics/ng-generate/component/index.ts b/npm/cypress-schematic/src/schematics/ng-generate/component/index.ts new file mode 100644 index 00000000000..5f926c54a65 --- /dev/null +++ b/npm/cypress-schematic/src/schematics/ng-generate/component/index.ts @@ -0,0 +1,27 @@ +import { chain, externalSchematic, noop, Rule, SchematicContext, Tree } from '@angular-devkit/schematics' +import cypressTest from '../cypress-test' +import path = require('path'); + +export default function (options: any): Rule { + return (_: Tree, _context: SchematicContext) => { + return chain([ + externalSchematic('@schematics/angular', 'component', { + ...options, + skipTests: true, + }), + (tree: Tree, _context: SchematicContext) => { + const componentName = path.parse(options.name).name + const componentPath = tree.actions.filter((a) => a.path.includes(`${componentName}.component.ts`)) + .map((a) => path.dirname(a.path)) + .at(0) + + return componentPath ? cypressTest({ + ...options, + component: true, + path: componentPath, + name: componentName, + }) : noop() + }, + ]) + } +} diff --git a/npm/cypress-schematic/src/schematics/ng-generate/component/schema.json b/npm/cypress-schematic/src/schematics/ng-generate/component/schema.json new file mode 100644 index 00000000000..01d8f39925c --- /dev/null +++ b/npm/cypress-schematic/src/schematics/ng-generate/component/schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "cypress-schematics-generate-component", + "title": "Cypress Wrapper for Angular Component Options Schema", + "type": "object", + "description": "Creates a new, generic component definition in the given or default project.", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "format": "path", + "$default": { + "$source": "workingDirectory" + }, + "description": "The path at which to create the component file, relative to the current workspace. Default is a folder with the same name as the component in the project root.", + "visible": false + }, + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + }, + "name": { + "type": "string", + "description": "The name of the component.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use for the component?" + }, + "displayBlock": { + "description": "Specifies if the style will contain `:host { display: block; }`.", + "type": "boolean", + "default": false, + "alias": "b" + }, + "inlineStyle": { + "description": "Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file.", + "type": "boolean", + "default": false, + "alias": "s", + "x-user-analytics": "ep.ng_inline_style" + }, + "inlineTemplate": { + "description": "Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file.", + "type": "boolean", + "default": false, + "alias": "t", + "x-user-analytics": "ep.ng_inline_template" + }, + "standalone": { + "description": "Whether the generated component is standalone.", + "type": "boolean", + "default": false, + "x-user-analytics": "ep.ng_standalone" + }, + "viewEncapsulation": { + "description": "The view encapsulation strategy to use in the new component.", + "enum": [ + "Emulated", + "None", + "ShadowDom" + ], + "type": "string", + "alias": "v" + }, + "changeDetection": { + "description": "The change detection strategy to use in the new component.", + "enum": [ + "Default", + "OnPush" + ], + "type": "string", + "default": "Default", + "alias": "c" + }, + "prefix": { + "type": "string", + "description": "The prefix to apply to the generated component selector.", + "alias": "p", + "oneOf": [ + { + "maxLength": 0 + }, + { + "minLength": 1, + "format": "html-selector" + } + ] + }, + "style": { + "description": "The file extension or preprocessor to use for style files, or 'none' to skip generating the style file.", + "type": "string", + "default": "css", + "enum": [ + "css", + "scss", + "sass", + "less", + "none" + ], + "x-user-analytics": "ep.ng_style" + }, + "type": { + "type": "string", + "description": "Adds a developer-defined type to the filename, in the format \"name.type.ts\".", + "default": "Component" + }, + "skipTests": { + "type": "boolean", + "description": "Do not create \"spec.ts\" test files for the new component.", + "default": false + }, + "flat": { + "type": "boolean", + "description": "Create the new files at the top level of the current project.", + "default": false + }, + "skipImport": { + "type": "boolean", + "description": "Do not import this component into the owning NgModule.", + "default": false + }, + "selector": { + "type": "string", + "format": "html-selector", + "description": "The HTML selector to use for this component." + }, + "skipSelector": { + "type": "boolean", + "default": false, + "description": "Specifies if the component should have a selector or not." + }, + "module": { + "type": "string", + "description": "The declaring NgModule.", + "alias": "m" + }, + "export": { + "type": "boolean", + "default": false, + "description": "The declaring NgModule exports this component." + } + }, + "required": [ + "name", + "project" + ] +} \ No newline at end of file diff --git a/npm/cypress-schematic/src/schematics/utils/jsonFile.ts b/npm/cypress-schematic/src/schematics/utils/jsonFile.ts index f1c14072520..3e0f812b20a 100644 --- a/npm/cypress-schematic/src/schematics/utils/jsonFile.ts +++ b/npm/cypress-schematic/src/schematics/utils/jsonFile.ts @@ -25,7 +25,7 @@ export type JSONPath = (string | number)[]; /** @internal */ export class JSONFile { - content: string; + content: string constructor (private readonly host: Tree, private readonly path: string) { const buffer = this.host.read(this.path) @@ -37,7 +37,7 @@ export class JSONFile { } } - private _jsonAst: Node | undefined; + private _jsonAst: Node | undefined private get JsonAst (): Node | undefined { if (this._jsonAst) { return this._jsonAst diff --git a/npm/eslint-plugin-dev/.eslintrc.json b/npm/eslint-plugin-dev/.eslintrc.json index 6e7d8f7750a..b7e94b45ffe 100644 --- a/npm/eslint-plugin-dev/.eslintrc.json +++ b/npm/eslint-plugin-dev/.eslintrc.json @@ -6,6 +6,5 @@ "extends": [ "plugin:promise/recommended", "plugin:@cypress/dev/general" - ], - "rules": {} + ] } diff --git a/npm/eslint-plugin-dev/CHANGELOG.md b/npm/eslint-plugin-dev/CHANGELOG.md index 4f7d131be47..2b0105718a0 100644 --- a/npm/eslint-plugin-dev/CHANGELOG.md +++ b/npm/eslint-plugin-dev/CHANGELOG.md @@ -1,3 +1,22 @@ +# [@cypress/eslint-plugin-dev-v6.0.0](https://github.com/cypress-io/cypress/compare/@cypress/eslint-plugin-dev-v5.3.3...@cypress/eslint-plugin-dev-v6.0.0) (2024-05-06) + + +### breaking + +* the supported eslint version is 8 for @cypress/eslint-plugin-dev. ([3b799a1](https://github.com/cypress-io/cypress/commit/3b799a158d7af419637d524e811561cd25143c3f)) + + +### BREAKING CHANGES + +* The supported eslint version is 8. @see f14a11aecfbc1e3854daae02b69fb40b4ec801b7 for breaking changes to the plugin. + +# [@cypress/eslint-plugin-dev-v5.3.3](https://github.com/cypress-io/cypress/compare/@cypress/eslint-plugin-dev-v5.3.2...@cypress/eslint-plugin-dev-v5.3.3) (2024-01-12) + + +### Bug Fixes + +* allow for versions greater than 4 for eslint-plugin-mocha to prevent force installing dependencies when eslint-plugin-mocha is bumbed in comsumer packages ([#27944](https://github.com/cypress-io/cypress/issues/27944)) ([bf05978](https://github.com/cypress-io/cypress/commit/bf0597847e71f34303364929f9c34cdd6c0e7ad8)) + # [@cypress/eslint-plugin-dev-v5.3.2](https://github.com/cypress-io/cypress/compare/@cypress/eslint-plugin-dev-v5.3.1...@cypress/eslint-plugin-dev-v5.3.2) (2022-08-15) diff --git a/npm/eslint-plugin-dev/README.md b/npm/eslint-plugin-dev/README.md index 2672a9fe80c..4656cbff22b 100644 --- a/npm/eslint-plugin-dev/README.md +++ b/npm/eslint-plugin-dev/README.md @@ -3,7 +3,7 @@

[Internal] Cypress Developer ESLint Plugin

- +

Common ESLint rules shared by Cypress packages.

@@ -20,6 +20,12 @@ npm install --save-dev @cypress/eslint-plugin-dev ## Usage +> ⚠️ Currently does **not** support ESLint version 9 + +For Eslint 8, use version 6.x.x + +For Eslint 7 and below, use version 5.x.x + 1) install the following `devDependencies`: ```sh @cypress/eslint-plugin-dev @@ -27,14 +33,11 @@ eslint-plugin-json-format @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-mocha - -# if you have coffeescript files -@fellow/eslint-plugin-coffee -babel-eslint +eslint-plugin-import # if you have react/jsx files eslint-plugin-react -babel-eslint +@babel/eslint-parser ``` 2) add the following to your root level `.eslintrc.json`: @@ -44,7 +47,7 @@ babel-eslint "@cypress/dev" ], "extends": [ - "plugin:@cypress/dev/general", + "plugin:@cypress/dev/general" ] } ``` @@ -92,6 +95,7 @@ _Should usually be used at the root of the package._ **requires you to install the following `devDependencies`**: ```sh +eslint-plugin-import eslint-plugin-json-format @typescript-eslint/parser @typescript-eslint/eslint-plugin @@ -112,7 +116,7 @@ React and JSX-specific configuration and rules. **requires you to install the following `devDependencies`**: ```sh -babel-eslint +@babel/eslint-parser eslint-plugin-react ``` @@ -176,11 +180,7 @@ After installing, add the following to your User or Workspace (`.vscode/settings { "language": "json", "autoFix": true - }, - { - "language": "coffeescript", - "autoFix": false - }, + } ], } ``` diff --git a/npm/eslint-plugin-dev/lib/custom-rules/arrow-body-multiline-braces.js b/npm/eslint-plugin-dev/lib/custom-rules/arrow-body-multiline-braces.js index d3b40d6c6bf..c196e3a9bd0 100644 --- a/npm/eslint-plugin-dev/lib/custom-rules/arrow-body-multiline-braces.js +++ b/npm/eslint-plugin-dev/lib/custom-rules/arrow-body-multiline-braces.js @@ -1,5 +1,6 @@ const ruleComposer = require('eslint-rule-composer') -const arrowBodyStyle = require('eslint/lib/rules/arrow-body-style') +const eslint = require('eslint') +const arrowBodyStyle = new eslint.Linter().getRules().get('arrow-body-style') module.exports = ruleComposer.filterReports( arrowBodyStyle, diff --git a/npm/eslint-plugin-dev/lib/custom-rules/no-only.js b/npm/eslint-plugin-dev/lib/custom-rules/no-only.js index ee86d0625be..f4fba53f428 100644 --- a/npm/eslint-plugin-dev/lib/custom-rules/no-only.js +++ b/npm/eslint-plugin-dev/lib/custom-rules/no-only.js @@ -1,10 +1,7 @@ -let astUtils - -try { - astUtils = require('eslint/lib/util/ast-utils') -} catch (e) { - astUtils = require('eslint/lib/shared/ast-utils') -} +// @see https://github.com/eslint/eslint/blob/v8.57.0/lib/shared/ast-utils.js#L12 +// This value is not exported anywhere, but hasn't changed in almost a decade. +// we can directly reference the pattern here +const lineBreakPattern = /\r\n|[\r\n\u2028\u2029]/u module.exports = { meta: { @@ -25,7 +22,7 @@ module.exports = { const sourceCode = context.getSourceCode() function getPropertyText (node) { - const lines = sourceCode.getText(node).split(astUtils.LINEBREAK_MATCHER) + const lines = sourceCode.getText(node).split(lineBreakPattern) return lines[0] } diff --git a/npm/eslint-plugin-dev/lib/index.js b/npm/eslint-plugin-dev/lib/index.js index 8cd28a0572a..eab453c761c 100644 --- a/npm/eslint-plugin-dev/lib/index.js +++ b/npm/eslint-plugin-dev/lib/index.js @@ -238,25 +238,6 @@ module.exports = { '@cypress/dev/arrow-body-multiline-braces': 'off', }, }, - { - files: '*.coffee', - parser: '@fellow/eslint-plugin-coffee', - parserOptions: { - parser: 'babel-eslint', - sourceType: 'module', - ecmaVersion: 2018, - }, - plugins: [ - '@fellow/eslint-plugin-coffee', - ], - rules: { - ...Object.assign({}, ...Object.keys(baseRules).map((key) => ({ [key]: 'off' }))), - '@fellow/coffee/coffeescript-error': [ - 'error', - {}, - ], - }, - }, { files: [ '*.ts', @@ -266,6 +247,7 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', + 'import', ], rules: { 'no-undef': 'off', @@ -273,10 +255,7 @@ module.exports = { 'indent': 'off', 'no-useless-constructor': 'off', 'no-duplicate-imports': 'off', - 'import/no-duplicates': 'off', - '@typescript-eslint/no-duplicate-imports': [ - 'error', - ], + 'import/no-duplicates': 'error', '@typescript-eslint/no-unused-vars': [ 'error', { @@ -344,10 +323,11 @@ module.exports = { env: { browser: true, }, - parser: 'babel-eslint', + parser: '@babel/eslint-parser', parserOptions: { ecmaVersion: 2018, sourceType: 'module', + requireConfigFile: false, ecmaFeatures: { jsx: true, legacyDecorators: true, diff --git a/npm/eslint-plugin-dev/lib/scripts/.eslintrc.json b/npm/eslint-plugin-dev/lib/scripts/.eslintrc.json deleted file mode 100644 index b5ed5206d08..00000000000 --- a/npm/eslint-plugin-dev/lib/scripts/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "plugin:@cypress/dev/tests" - ] -} diff --git a/npm/eslint-plugin-dev/lib/scripts/utils.js b/npm/eslint-plugin-dev/lib/scripts/utils.js index 603c47629d9..d718464f77c 100644 --- a/npm/eslint-plugin-dev/lib/scripts/utils.js +++ b/npm/eslint-plugin-dev/lib/scripts/utils.js @@ -1,3 +1,4 @@ +const path = require('path') const _ = require('lodash') const EE = require('events') const sh = require('shelljs') @@ -75,12 +76,13 @@ module.exports = { const filenamesString = sh.ShellString(filenames.join(' ')) const lintCommand = opts.fix ? - `./node_modules/.bin/eslint --color=true --fix '' ${filenamesString}` - : `./node_modules/.bin/eslint --color=true '' ${filenamesString}` + `npx eslint --color=true --fix ${filenamesString}` + : `npx eslint --color=true ${filenamesString}` + // always run command in the root of the monorepo! return Promise.promisify(sh.exec)( lintCommand, - { silent: false, async: true }, + { silent: false, async: true, cwd: path.resolve(__dirname, '../../../../') }, ) .tapCatch(debugTerse) .return(false) diff --git a/npm/eslint-plugin-dev/package.json b/npm/eslint-plugin-dev/package.json index f01cdbb88b9..c66e8d9d63c 100644 --- a/npm/eslint-plugin-dev/package.json +++ b/npm/eslint-plugin-dev/package.json @@ -7,7 +7,7 @@ "lint": "eslint --ext .js,json,.eslintrc .", "lint-changed": "node ./lib/scripts/lint-changed", "lint-fix": "npm run lint -- --fix", - "test": "jest" + "test": "mocha" }, "dependencies": { "bluebird": "3.5.5", @@ -17,23 +17,23 @@ "shelljs": "0.8.5" }, "devDependencies": { - "eslint": "^7.22.0", + "eslint": "^8.56.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-json-format": "^2.0.0", - "eslint-plugin-mocha": "^8.1.0", + "eslint-plugin-mocha": "^8.2.0", "eslint-plugin-promise": "^4.2.1", - "jest": "^24.9.0", - "jest-cli": "^24.9.0", "sinon": "^7.3.2", "sinon-chai": "^3.3.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": ">= 1.11.0", - "@typescript-eslint/parser": ">= 1.11.0", - "babel-eslint": "^7.2.3", - "eslint": ">= 3.2.1", + "@babel/eslint-parser": "^7.0.0", + "@typescript-eslint/eslint-plugin": ">= 7.0.0", + "@typescript-eslint/parser": ">= 7.0.0", + "eslint": "^= 8.0.0", + "eslint-plugin-import": ">= 2.0.0", "eslint-plugin-json-format": ">= 2.0.0", - "eslint-plugin-mocha": "^4.11.0", - "eslint-plugin-react": "^7.22.0" + "eslint-plugin-mocha": " >= 8.0.0", + "eslint-plugin-react": ">= 7.22.0" }, "bin": { "lint-changed": "./lib/scripts/lint-changed.js", @@ -44,7 +44,7 @@ "type": "git", "url": "https://github.com/cypress-io/cypress.git" }, - "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/eslint-plugin-dev#readme", + "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/eslint-plugin-dev#readme", "bugs": { "url": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Feslint-plugin-dev&template=bug-report.md" }, @@ -53,4 +53,4 @@ "eslint", "eslintplugin" ] -} \ No newline at end of file +} diff --git a/npm/eslint-plugin-dev/test/.eslintrc b/npm/eslint-plugin-dev/test/.eslintrc index 10054c11ede..b5ed5206d08 100644 --- a/npm/eslint-plugin-dev/test/.eslintrc +++ b/npm/eslint-plugin-dev/test/.eslintrc @@ -1,8 +1,5 @@ { "extends": [ "plugin:@cypress/dev/tests" - ], - "env": { - "jest": true - } + ] } diff --git a/npm/eslint-plugin-dev/test/arrow-body-multiline-braces.spec.js b/npm/eslint-plugin-dev/test/arrow-body-multiline-braces.spec.js index b60118d1311..6698ab68db4 100644 --- a/npm/eslint-plugin-dev/test/arrow-body-multiline-braces.spec.js +++ b/npm/eslint-plugin-dev/test/arrow-body-multiline-braces.spec.js @@ -1,57 +1,55 @@ const path = require('path') -const CLIEngine = require('eslint').CLIEngine -const plugin = require('..') +const eslint = require('eslint') +const plugin = require('../lib') const _ = require('lodash') +const { expect } = require('chai') const pluginName = '__plugin__' +const ESLint = eslint.ESLint -function execute (file, options = {}) { - const opts = _.defaultsDeep(options, { +async function execute (file, options = {}) { + const defaultConfig = { fix: true, - config: { + ignore: false, + useEslintrc: false, + baseConfig: { parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, + rules: { + [`${pluginName}/arrow-body-multiline-braces`]: ['error', 'always'], + }, + plugins: [pluginName], }, - - }) - - const cli = new CLIEngine({ - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - rules: { - [`${pluginName}/arrow-body-multiline-braces`]: ['error', 'always'], + plugins: { + [pluginName]: plugin, }, - ...opts, - ignore: false, - useEslintrc: false, - plugins: [pluginName], - }) + } + const opts = _.defaultsDeep(options, defaultConfig) + + const cli = new ESLint(opts) - cli.addPlugin(pluginName, plugin) - const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0] + const results = await cli.lintFiles([path.join(__dirname, file)]) - return results + return results[0] } describe('arrow-body-multiline-braces', () => { it('lint multiline js', async () => { const filename = './fixtures/multiline.js' - const result = execute(filename, { + const result = await execute(filename, { fix: true, }) - expect(result.output).toContain('{') + expect(result.output).to.contain('{') }) it('lint oneline js', async () => { const filename = './fixtures/oneline.js' - const result = execute(filename, { fix: false }) + const result = await execute(filename, { fix: false }) expect(result.output).not.ok - expect(result).toHaveProperty('errorCount', 0) + expect(result.errorCount).eq(0) }) }) diff --git a/npm/eslint-plugin-dev/test/fixtures/.eslintrc b/npm/eslint-plugin-dev/test/fixtures/.eslintrc deleted file mode 100644 index 612fe1bdc9e..00000000000 --- a/npm/eslint-plugin-dev/test/fixtures/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module" - } -} diff --git a/npm/eslint-plugin-dev/test/no-only.spec.js b/npm/eslint-plugin-dev/test/no-only.spec.js index 24671948be3..8162bf14886 100644 --- a/npm/eslint-plugin-dev/test/no-only.spec.js +++ b/npm/eslint-plugin-dev/test/no-only.spec.js @@ -1,54 +1,53 @@ const path = require('path') -const CLIEngine = require('eslint').CLIEngine +const eslint = require('eslint') const plugin = require('..') const _ = require('lodash') +const { expect } = require('chai') const ruleName = 'no-only' const pluginName = '__plugin__' +const ESLint = eslint.ESLint -function execute (file, options = {}) { - const opts = _.defaultsDeep(options, { +async function execute (file, options = {}) { + const defaultConfig = { fix: true, - config: { + ignore: false, + useEslintrc: false, + baseConfig: { parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, + rules: { + [`${pluginName}/${ruleName}`]: ['error'], + }, + plugins: [pluginName], }, - }) - - const cli = new CLIEngine({ - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - rules: { - [`${pluginName}/${ruleName}`]: ['error'], + plugins: { + [pluginName]: plugin, }, - ...opts, - ignore: false, - useEslintrc: false, - plugins: [pluginName], - }) + } + const opts = _.defaultsDeep(options, defaultConfig) + + const cli = new ESLint(opts) - cli.addPlugin(pluginName, plugin) - const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0] + const results = await cli.lintFiles([path.join(__dirname, file)]) - return results + return results[0] } describe('no-only', () => { it('lint js with only', async () => { const filename = './fixtures/with-only.js' - const result = execute(filename, { + const result = await execute(filename, { fix: true, }) - expect(result.errorCount).toBe(3) - expect(result.messages[0].message).toContain('it') - expect(result.messages[1].message).toContain('describe') - expect(result.messages[2].message).toContain('context') + expect(result.errorCount).eq(3) + expect(result.messages[0].message).to.contain('it') + expect(result.messages[1].message).to.contain('describe') + expect(result.messages[2].message).to.contain('context') - expect(result.output).not.toBeTruthy() + expect(result.output).not.exist }) }) diff --git a/npm/eslint-plugin-dev/test/no-return-before.spec.js b/npm/eslint-plugin-dev/test/no-return-before.spec.js index fead3bced53..e53b67d1798 100644 --- a/npm/eslint-plugin-dev/test/no-return-before.spec.js +++ b/npm/eslint-plugin-dev/test/no-return-before.spec.js @@ -1,67 +1,65 @@ const path = require('path') -const CLIEngine = require('eslint').CLIEngine +const eslint = require('eslint') const plugin = require('..') const _ = require('lodash') const { stripIndent } = require('common-tags') +const { expect } = require('chai') const ruleName = 'no-return-before' const pluginName = '__plugin__' +const ESLint = eslint.ESLint -function execute (file, options = {}) { - const opts = _.defaultsDeep(options, { +async function execute (file, options = {}) { + const defaultConfig = { fix: true, - config: { + ignore: false, + useEslintrc: false, + baseConfig: { parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, + rules: { + [`${pluginName}/${ruleName}`]: ['error'], + }, + plugins: [pluginName], }, - }) - - const cli = new CLIEngine({ - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - rules: { - [`${pluginName}/${ruleName}`]: ['error'], + plugins: { + [pluginName]: plugin, }, - ...opts, - ignore: false, - useEslintrc: false, - plugins: [pluginName], + } + const opts = _.defaultsDeep(options, defaultConfig) - }) + const cli = new ESLint(opts) - cli.addPlugin(pluginName, plugin) - const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0] + const results = await cli.lintFiles([path.join(__dirname, file)]) - return results + return results[0] } describe(ruleName, () => { it('pass', async () => { const filename = './fixtures/no-return-before-pass.js' - const result = execute(filename) + const result = await execute(filename) - expect(result.errorCount).toBe(0) + expect(result.errorCount).equal(0) }) it('fail', async () => { const filename = './fixtures/no-return-before-fail.js' - const result = execute(filename, { + const result = await execute(filename, { fix: false, }) - expect(result.errorCount).toBe(4) - expect(result.messages[0].message).toContain(`after 'describe'`) + expect(result.errorCount).equal(4) + expect(result.messages[0].message).to.contain(`after 'describe'`) }) it('fix fail', async () => { const filename = './fixtures/no-return-before-fail.js' - const result = execute(filename) + const result = await execute(filename) - expect(result.output).toEqual(`${stripIndent` + expect(result.output).equal(`${stripIndent` describe('outer', ()=>{ describe('some test', ()=>{ context('some test', ()=>{ @@ -78,22 +76,24 @@ describe(ruleName, () => { describe('config', () => { it('config [tokens]', async () => { const filename = './fixtures/no-return-before-fail.js' - const result = execute(filename, { + const result = await execute(filename, { fix: false, - rules: { - [`${pluginName}/${ruleName}`]: [ - 'error', { - tokens: ['someFn'], - }, - ], + baseConfig: { + rules: { + [`${pluginName}/${ruleName}`]: [ + 'error', { + tokens: ['someFn'], + }, + ], + }, }, }) - expect(result.errorCount).toBe(1) + expect(result.errorCount).equal(1) - expect(result.messages[0].message).toContain('someFn') + expect(result.messages[0].message).to.contain('someFn') - expect(result.output).not.toBeTruthy() + expect(result.output).not.not.exist }) }) }) diff --git a/npm/eslint-plugin-dev/test/skip-comment.spec.js b/npm/eslint-plugin-dev/test/skip-comment.spec.js index 37f24f277b1..10b7008787e 100644 --- a/npm/eslint-plugin-dev/test/skip-comment.spec.js +++ b/npm/eslint-plugin-dev/test/skip-comment.spec.js @@ -1,91 +1,92 @@ const path = require('path') -const CLIEngine = require('eslint').CLIEngine +const eslint = require('eslint') const plugin = require('..') const _ = require('lodash') +const { expect } = require('chai') const ruleName = 'skip-comment' const pluginName = '__plugin__' +const ESLint = eslint.ESLint -function execute (file, options = {}) { - const opts = _.defaultsDeep(options, { +async function execute (file, options = {}) { + const defaultConfig = { fix: true, - config: { + ignore: false, + useEslintrc: false, + baseConfig: { parserOptions: { ecmaVersion: 2018, sourceType: 'module', }, + rules: { + [`${pluginName}/${ruleName}`]: ['error'], + }, + plugins: [pluginName], }, - }) - - const cli = new CLIEngine({ - parserOptions: { - ecmaVersion: 2018, - sourceType: 'module', - }, - rules: { - [`${pluginName}/${ruleName}`]: ['error'], + plugins: { + [pluginName]: plugin, }, - ...opts, - ignore: false, - useEslintrc: false, - plugins: [pluginName], - }) + } + const opts = _.defaultsDeep(options, defaultConfig) + + const cli = new ESLint(opts) - cli.addPlugin(pluginName, plugin) - const results = cli.executeOnFiles([path.join(__dirname, file)]).results[0] + const results = await cli.lintFiles([path.join(__dirname, file)]) - return results + return results[0] } describe('skip-comment', () => { it('skip test with comment', async () => { const filename = './fixtures/skip-comment-pass.js' - const result = execute(filename, { + const result = await execute(filename, { fix: true, }) - expect(result.errorCount).toBe(0) + expect(result.errorCount).equal(0) }) it('skip test without comment', async () => { const filename = './fixtures/skip-comment-fail.js' - const result = execute(filename, { + const result = await execute(filename, { fix: true, }) - expect(result.errorCount).toBe(3) + expect(result.errorCount).equal(3) - expect(result.messages[0].message).toContain('it') - expect(result.messages[0].message).toContain('NOTE:') - expect(result.messages[0].message).toContain('TODO:') - expect(result.messages[1].message).toContain('describe') - expect(result.messages[1].message).toContain('NOTE:') - expect(result.messages[2].message).toContain('context') - expect(result.messages[2].message).toContain('NOTE:') + expect(result.messages[0].message).to.contain('it') + expect(result.messages[0].message).to.contain('NOTE:') + expect(result.messages[0].message).to.contain('TODO:') + expect(result.messages[1].message).to.contain('describe') + expect(result.messages[1].message).to.contain('NOTE:') + expect(result.messages[2].message).to.contain('context') + expect(result.messages[2].message).to.contain('NOTE:') - expect(result.output).not.toBeTruthy() + expect(result.output).not.not.exist }) describe('config', () => { it('skip test without comment', async () => { const filename = './fixtures/skip-comment-config.js' - const result = execute(filename, { + const result = await execute(filename, { fix: true, - rules: { - [`${pluginName}/${ruleName}`]: [ - 'error', { - commentTokens: ['FOOBAR:'], - }, - ], + baseConfig: { + rules: { + [`${pluginName}/${ruleName}`]: [ + 'error', { + commentTokens: ['FOOBAR:'], + }, + ], + }, }, }) - expect(result.errorCount).toBe(1) + expect(result.errorCount).equal(1) - expect(result.messages[0].message).toContain('it') - expect(result.messages[0].message).toContain('FOOBAR:') + expect(result.messages[0].message).to.contain('it') + expect(result.messages[0].message).to.contain('FOOBAR:') - expect(result.output).not.toBeTruthy() + expect(result.output).not.exist }) }) }) diff --git a/npm/grep/.eslintrc b/npm/grep/.eslintrc new file mode 100644 index 00000000000..667197d7aa4 --- /dev/null +++ b/npm/grep/.eslintrc @@ -0,0 +1,16 @@ +{ + "plugins": [ + "cypress" + ], + "extends": [ + "plugin:@cypress/dev/tests" + ], + "env": { + "cypress/globals": true + }, + "rules": { + "mocha/no-global-tests": "off", + "no-console": "off", + "no-restricted-syntax": "off" + } +} diff --git a/npm/grep/.releaserc.js b/npm/grep/.releaserc.js new file mode 100644 index 00000000000..17d3bb87147 --- /dev/null +++ b/npm/grep/.releaserc.js @@ -0,0 +1,3 @@ +module.exports = { + ...require('../../.releaserc'), +} diff --git a/npm/grep/CHANGELOG.md b/npm/grep/CHANGELOG.md new file mode 100644 index 00000000000..068ffa424f4 --- /dev/null +++ b/npm/grep/CHANGELOG.md @@ -0,0 +1,62 @@ +# [@cypress/grep-v4.1.0](https://github.com/cypress-io/cypress/compare/@cypress/grep-v4.0.2...@cypress/grep-v4.1.0) (2024-07-02) + + +### Features + +* **deps:** update dependency find-test-names to ^1.28.18 ([#29672](https://github.com/cypress-io/cypress/issues/29672)) ([c3694a8](https://github.com/cypress-io/cypress/commit/c3694a8835f715c9fb3cd1713dbe60f1b047c2ff)) + +# [@cypress/grep-v4.0.2](https://github.com/cypress-io/cypress/compare/@cypress/grep-v4.0.1...@cypress/grep-v4.0.2) (2024-06-07) + + +### Bug Fixes + +* update cypress to Typescript 5 ([#29568](https://github.com/cypress-io/cypress/issues/29568)) ([f3b6766](https://github.com/cypress-io/cypress/commit/f3b67666a5db0438594339c379cf27e1fd1e4abc)) + +# [@cypress/grep-v4.0.1](https://github.com/cypress-io/cypress/compare/@cypress/grep-v4.0.0...@cypress/grep-v4.0.1) (2023-10-16) + + +### Bug Fixes + +* **grep:** fix options sent to fast glob package - issue 27216 ([#27231](https://github.com/cypress-io/cypress/issues/27231)) ([5a7eee5](https://github.com/cypress-io/cypress/commit/5a7eee573ec196dc0fcd98768ab021828a3f1307)) + +# [@cypress/grep-v4.0.0](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.5...@cypress/grep-v4.0.0) (2023-08-29) + + +* `@cypress/grep-v4.0.0` was inadvertently released and published. There are no breaking changes or any other changes in this release. + +# [@cypress/grep-v3.1.5](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.4...@cypress/grep-v3.1.5) (2023-03-15) + + +### Bug Fixes + +* **grep:** references to cypress-grep ([#26108](https://github.com/cypress-io/cypress/issues/26108)) ([7a18b79](https://github.com/cypress-io/cypress/commit/7a18b79efae64dc1fc32fb5aaa89969e83971c6f)) + +# [@cypress/grep-v3.1.4](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.3...@cypress/grep-v3.1.4) (2023-02-06) + +# [@cypress/grep-v3.1.3](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.2...@cypress/grep-v3.1.3) (2022-12-14) + + +### Bug Fixes + +* **grep:** @cypress/grep types ([#24844](https://github.com/cypress-io/cypress/issues/24844)) ([55058e7](https://github.com/cypress-io/cypress/commit/55058e7783420d0946bd19eeb72a08ccf3f3a86e)) + +# [@cypress/grep-v3.1.2](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.1...@cypress/grep-v3.1.2) (2022-12-09) + + +### Bug Fixes + +* declare used babel dependencies ([#24842](https://github.com/cypress-io/cypress/issues/24842)) ([910f912](https://github.com/cypress-io/cypress/commit/910f912373bf857a196e2a0d1a73606e3ee199be)) + +# [@cypress/grep-v3.1.1](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.0...@cypress/grep-v3.1.1) (2022-12-08) + + +### Bug Fixes + +* fix behavior when only using inverted tags ([#24413](https://github.com/cypress-io/cypress/issues/24413)) ([b2a2e50](https://github.com/cypress-io/cypress/commit/b2a2e508638d5132fc30e01d707de81d22fde359)) + +# [@cypress/grep-v3.1.0](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.0.3...@cypress/grep-v3.1.0) (2022-10-21) + + +### Features + +* **grep:** move cypress-grep to @cypress/grep ([#23887](https://github.com/cypress-io/cypress/issues/23887)) ([d422aad](https://github.com/cypress-io/cypress/commit/d422aadfa10e5aaac17ed0e4dd5e18a73d821490)) diff --git a/npm/grep/README.md b/npm/grep/README.md new file mode 100644 index 00000000000..355ec62f5b5 --- /dev/null +++ b/npm/grep/README.md @@ -0,0 +1,603 @@ +# @cypress/grep + +> Filter tests using substring + +```shell +# run only tests with "hello" in their names +npx cypress run --env grep=hello + + ✓ hello world + - works + - works 2 @tag1 + - works 2 @tag1 @tag2 + + 1 passing (38ms) + 3 pending +``` + +All other tests will be marked pending, see why in the [Cypress test statuses](https://on.cypress.io/writing-and-organizing-tests#Test-statuses) blog post. + +If you have multiple spec files, all specs will be loaded, and every test will be filtered the same way, since the grep is run-time operation and cannot eliminate the spec files without loading them. If you want to run only specific tests, use the built-in [--spec](https://on.cypress.io/command-line#cypress-run-spec-lt-spec-gt) CLI argument. + +Table of Contents + + + +- [Install](#install) + - [Support file](#support-file) + - [Config file](#config-file) +- [Usage Overview](#usage-overview) +- [Filter by test title](#filter-by-test-title) + - [OR substring matching](#or-substring-matching) + - [Test suites](#test-suites) + - [Invert filter](#invert-filter) +- [Filter with tags](#filter-with-tags) + - [Tags in the test config object](#tags-in-the-test-config-object) + - [AND tags](#and-tags) + - [OR tags](#or-tags) + - [Inverted tags](#inverted-tags) + - [NOT tags](#not-tags) + - [Tags in test suites](#tags-in-test-suites) + - [Grep untagged tests](#grep-untagged-tests) +- [Pre-filter specs \(grepFilterSpecs\)](#pre-filter-specs-grepfilterspecs) +- [Omit filtered tests \(grepOmitFiltered\)](#omit-filtered-tests-grepomitfiltered) +- [Disable grep](#disable-grep) +- [Burn \(repeat\) tests](#burn-repeat-tests) +- [TypeScript support](#typescript-support) +- [General advice](#general-advice) +- [DevTools console](#devtools-console) +- [Debugging](#debugging) + - [Log messages](#log-messages) + - [Debugging in the plugin](#debugging-in-the-plugin) + - [Debugging in the browser](#debugging-in-the-browser) +- [Examples](#examples) +- [See also](#see-also) +- [Migration guide](#migration-guide) + - [from v1 to v2](#from-v1-to-v2) + - [from v2 to v3](#from-v2-to-v3) +- [Videos & Blog Posts](#videos--blog-posts) +- [Blog posts](#blog-posts) +- [Small print](#small-print) +- [MIT License](#mit-license) + + + +## Install + +Assuming you have Cypress installed, add this module as a dev dependency. + +```shell +# using NPM +npm i -D @cypress/grep +# using Yarn +yarn add -D @cypress/grep +``` + +**Note**: @cypress/grep only works with Cypress version >= 10. + +### Support file + +**required:** load this module from the [support file](https://on.cypress.io/writing-and-organizing-tests#Support-file) or at the top of the spec file if not using the support file. You import the registration function and then call it: + +```js +// cypress/support/e2e.js +// load and register the grep feature using "require" function +// https://github.com/cypress-io/cypress/tree/develop/npm/grep +const registerCypressGrep = require('@cypress/grep') +registerCypressGrep() + +// if you want to use the "import" keyword +// note: `./index.d.ts` currently extends the global Cypress types and +// does not define `registerCypressGrep` so the import path is directly +// pointed to the `support.js` file +import registerCypressGrep from '@cypress/grep/src/support' +registerCypressGrep() + + +// "import" with `@ts-ignore` +// @see error 2306 https://github.com/microsoft/TypeScript/blob/3fcd1b51a1e6b16d007b368229af03455c7d5794/src/compiler/diagnosticMessages.json#L1635 +// @ts-ignore +import registerCypressGrep from '@cypress/grep' +registerCypressGrep() +``` + +### Config file + +**optional:** load and register this module from the [config file](https://docs.cypress.io/guides/references/configuration#setupNodeEvents): + +```js +// cypress.config.js +{ + e2e: { + setupNodeEvents(on, config) { + require('@cypress/grep/src/plugin')(config); + return config; + }, + } +} +``` + +Installing the plugin via `setupNodeEvents()` is required to enable the [grepFilterSpecs](#grepfilterspecs) feature. + +## Usage Overview + +You can filter tests to run using part of their title via `grep`, and via explicit tags via `grepTags` Cypress environment variables. + +Most likely you will pass these environment variables from the command line. For example, to only run tests with "login" in their title and tagged "smoke", you would run: + +Here are a few examples: + +```shell +# run only the tests with "auth user" in the title +$ npx cypress run --env grep="auth user" +# run tests with "hello" or "auth user" in their titles +# by separating them with ";" character +$ npx cypress run --env grep="hello; auth user" +# run tests tagged @fast +$ npx cypress run --env grepTags=@fast +# run only the tests tagged "smoke" +# that have "login" in their titles +$ npx cypress run --env grep=login,grepTags=smoke +# only run the specs that have any tests with "user" in their titles +$ npx cypress run --env grep=user,grepFilterSpecs=true +# only run the specs that have any tests tagged "@smoke" +$ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true +# run only tests that do not have any tags +# and are not inside suites that have any tags +$ npx cypress run --env grepUntagged=true +``` + +You can use any way to modify the environment values `grep` and `grepTags`, except the run-time `Cypress.env('grep')` (because it is too late at run-time). You can set the `grep` value in the `cypress.json` file to run only tests with the substring `viewport` in their names + +```json +{ + "env": { + "grep": "viewport" + } +} +``` + +You can also set the `env.grep` object in the plugin file, but remember to return the changed config object: + +```js +// cypress/plugin/index.js +module.exports = (on, config) => { + config.env.grep = 'viewport' + return config +} +``` + +You can also set the grep and grepTags from the DevTools console while running Cypress in the interactive mode `cypress open`, see [DevTools Console section](#devtools-console). + +## Filter by test title + +```shell +# run all tests with "hello" in their title +$ npx cypress run --env grep=hello +# run all tests with "hello world" in their title +$ npx cypress run --env grep="hello world" +``` + +### OR substring matching + +You can pass multiple title substrings to match separating them with `;` character. Each substring is trimmed. + +```shell +# run all tests with "hello world" or "auth user" in their title +$ npx cypress run --env grep="hello world; auth user" +``` + +### Test suites + +The filter is also applied to the "describe" blocks. In that case, the tests look up if any of their outer suites are enabled. + +```js +describe('block for config', () => { + it('should run', () => {}) + + it('should also work', () => {}) +}) +``` + +``` +# run any tests in the blocks including "config" +--env grep=config +``` + +**Note:** global function `describe` and `context` are aliases and both supported by this plugin. + +### Invert filter + +```shell +# run all tests WITHOUT "hello world" in their title +$ npx cypress run --env grep="-hello world" +# run tests with "hello", but without "world" in the titles +$ npx cypress run --env grep="hello; -world" +``` + +**Note:** Inverted title filter is not compatible with the `grepFilterSpecs` option + +## Filter with tags + +You can select tests to run or skip using tags by passing `--env grepTags=...` value. + +``` +# enable the tests with tag "one" or "two" +--env grepTags="one two" +# enable the tests with both tags "one" and "two" +--env grepTags="one+two" +# enable the tests with "hello" in the title and tag "smoke" +--env grep=hello,grepTags=smoke +``` + +If you can pass commas in the environment variable `grepTags`, you can use `,` to separate the tags + +``` +# enable the tests with tag "one" or "two" +CYPRESS_grepTags=one,two npx cypress run +``` + +### Tags in the test config object + +Cypress tests can have their own [test config object](https://on.cypress.io/configuration#Test-Configuration), and when using this plugin you can put the test tags there, either as a single tag string or as an array of tags. + +```js +it('works as an array', { tags: ['config', 'some-other-tag'] }, () => { + expect(true).to.be.true +}) + +it('works as a string', { tags: 'config' }, () => { + expect(true).to.be.true +}) +``` + +You can run both of these tests using `--env grepTags=config` string. + +### AND tags + +Use `+` to require both tags to be present + +``` +--env grepTags=@smoke+@fast +``` + +### OR tags + +You can run tests that match one tag or another using spaces. Make sure to quote the grep string! + +``` +# run tests with tags "@slow" or "@critical" in their names +--env grepTags='@slow @critical' +``` + +### Inverted tags + +You can skip running the tests with specific tag using the invert option: prefix the tag with the character `-`. + +``` +# do not run any tests with tag "@slow" +--env grepTags=-@slow +``` + +If you want to run all tests with tag `@slow` but without tag `@smoke`: + +``` +--env grepTags=@slow+-@smoke +``` + +**Note:** Inverted tag filter is not compatible with the `grepFilterSpecs` option + +### NOT tags + +You can skip running the tests with specific tag, even if they have a tag that should run, using the not option: prefix the tag with `--`. + +Note this is the same as appending `+-` to each tag. May be useful with large number of tags. + +If you want to run tests with tags `@slow` or `@regression` but without tag `@smoke` + +``` +--env grepTags='@slow @regression --@smoke' +``` + +which is equivalent to + +``` +--env grepTags='@slow+-@smoke @regression+-@smoke' +``` + +### Tags in test suites + +The tags are also applied to the "describe" blocks. In that case, the tests look up if any of their outer suites are enabled. + +```js +describe('block with config tag', { tags: '@smoke' }, () => {}) +``` + +``` +# run any tests in the blocks having "@smoke" tag +--env grepTags=@smoke +# skip any blocks with "@smoke" tag +--env grepTags=-@smoke +``` + +See the [cypress/integration/describe-tags-spec.js](./cypress/integration/describe-tags-spec.js) file. + +**Note:** global function `describe` and `context` are aliases and both supported by this plugin. + +### Grep untagged tests + +Sometimes you want to run only the tests without any tags, and these tests are inside the describe blocks without any tags. + +``` +$ npx cypress run --env grepUntagged=true +``` + +## Pre-filter specs (grepFilterSpecs) + +By default, when using `grep` and `grepTags` all specs are executed, and inside each the filters are applied. This can be very wasteful, if only a few specs contain the `grep` in the test titles. Thus when doing the positive `grep`, you can pre-filter specs using the `grepFilterSpecs=true` parameter. + +``` +# filter all specs first, and only run the ones with +# suite or test titles containing the string "it loads" +$ npx cypress run --env grep="it loads",grepFilterSpecs=true +# filter all specs files, only run the specs with a tag "@smoke" +$ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true +``` + +**Note 1:** this requires installing this plugin in your project's plugin file, see the [Install](#install). + +**Note 2:** the `grepFilterSpecs` option is only compatible with the positive `grep` and `grepTags` options, not with the negative (inverted) "-..." filter. + +**Note 3:** if there are no files remaining after filtering, the plugin prints a warning and leaves all files unchanged to avoid the test runner erroring with "No specs found". + +**Tip:** you can set this environment variable in the [config file](https://docs.cypress.io/guides/references/configuration) file to enable it by default and skip using the environment variable: + +```js +{ + "env": { + "grepFilterSpecs": true + } +} +``` + +## Omit filtered tests (grepOmitFiltered) + +By default, all filtered tests are made _pending_ using `it.skip` method. If you want to completely omit them, pass the environment variable `grepOmitFiltered=true`. + +Pending filtered tests + +``` +cypress run --env grep="works 2" +``` + +![Pending tests](./images/includes-pending.png) + +Omit filtered tests + +``` +cypress run --env grep="works 2",grepOmitFiltered=true +``` + +![Only running tests remaining](./images/omit-pending.png) + +**Tip:** you can set this environment variable in the config file (usually `cypress.config.js`) file to enable it by default and skip using the environment variable: + +```json +{ + "env": { + "grepOmitFiltered": true + } +} +``` + +## Disable grep + +If you specify the `grep` parameters the [config file](https://docs.cypress.io/guides/references/configuration), you can disable it from the command line + +``` +$ npx cypress run --env grep=,grepTags=,burn= +``` + +## Burn (repeat) tests + +You can burn the filtered tests to make sure they are flake-free + +``` +npx cypress run --env grep="hello world",burn=5 +``` + +You can pass the number of times to run the tests via environment name `burn` or `grepBurn` or `grep-burn`. Note, if a lot of tests match the grep and grep tags, a lot of tests will be burnt! + +If you do not specify the "grep" or "grep tags" option, the "burn" will repeat _every_ test. + +## TypeScript support + +Because the Cypress test config object type definition does not have the `tags` property we are using above, the TypeScript linter will show an error. Just add an ignore comment above the test: + +```js +// @ts-ignore +it('runs on deploy', { tags: 'smoke' }, () => { + ... +}) +``` + +This package comes with [src/index.d.ts](./src/index.d.ts) definition file that adds the property `tags` to the Cypress test overrides interface. Include this file in your specs or TS config settings. For example, you can load it using a reference comment + +```js +// cypress/integration/my-spec.js +/// +``` + +If you have `tsconfig.json` file, add this library to the types list + +```json +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "@cypress/grep"] + }, + "include": ["**/*.ts"] +} +``` + +## General advice + +- keep it simple. +- I like using `@` as tag prefix to make the tags searchable + +```js +// ✅ good practice +describe('auth', { tags: '@critical' }, () => ...) +it('works', { tags: '@smoke' }, () => ...) +it('works quickly', { tags: ['@smoke', '@fast'] }, () => ...) + +// 🚨 NOT GOING TO WORK +// ERROR: treated as a single tag, +// probably want an array instead +it('works', { tags: '@smoke @fast' }, () => ...) +``` + +Grepping the tests + +```shell +# run the tests by title +$ npx cypress run --env grep="works quickly" +# run all tests tagged @smoke +$ npx cypress run --env grepTags=@smoke +# run all tests except tagged @smoke +$ npx cypress run --env grepTags=-@smoke +# run all tests that have tag @fast but do not have tag @smoke +$ npx cypress run --env grepTags=@fast+-@smoke +``` + +I would run all tests by default, and grep tests from the command line. For example, I could run the smoke tests first using grep plugin, and if the smoke tests pass, then run all the tests. See the video [How I organize pull request workflows by running smoke tests first](https://www.youtube.com/watch?v=SFW7Ecj5TNE) and its [pull request workflow file](https://github.com/bahmutov/cypress-grep-example/blob/main/.github/workflows/pr.yml). + +## DevTools console + +You can set the grep string from the DevTools Console. This plugin adds method `Cypress.grep` and `Cypress.grepTags` to set the grep strings and restart the tests + +```js +// filter tests by title substring +Cypress.grep('hello world') +// run filtered tests 100 times +Cypress.grep('hello world', null, 100) +// filter tests by tag string +// in this case will run tests with tag @smoke OR @fast +Cypress.grep(null, '@smoke @fast') +// run tests tagged @smoke AND @fast +Cypress.grep(null, '@smoke+@fast') +// run tests with title containing "hello" and tag @smoke +Cypress.grep('hello', '@smoke') +// run tests with title containing "hello" and tag @smoke 10 times +Cypress.grep('hello', '@smoke', 10) +``` + +- to remove the grep strings enter `Cypress.grep()` + +## Debugging + +When debugging a problem, first make sure you are using the expected version of this plugin, as some features might be only available in the [later releases](https://www.npmjs.com/package/@cypress/grep?activeTab=versions). + +``` +# get the @cypress/grep version using NPM +$ npm ls @cypress/grep +... +└── @cypress/grep@2.10.1 +# get the @cypress/grep version using Yarn +$ yarn why @cypress/grep +... +=> Found "@cypress/grep@3.1.0" +info Has been hoisted to "@cypress/grep" +info This module exists because it's specified in "devDependencies". +... +``` + +Second, make sure you are passing the values to the plugin correctly by inspecting the "Settings" tab in the Cypress Desktop GUI screen. You should see the values you have passed in the "Config" object under the `env` property. For example, if I start the Test Runner with + +```text +$ npx cypress open --env grep=works,grepFilterTests=true +``` + +Then I expect to see the grep string and the "filter tests" flag in the `env` object. + +![Values in the env object](./images/config.png) + +### Log messages + +This module uses [debug](https://github.com/visionmedia/debug#readme) to log verbose messages. You can enable the debug messages in the plugin file (runs when discovering specs to filter), and inside the browser to see how it determines which tests to run and to skip. When opening a new issue, please provide the debug logs from the plugin (if any) and from the browser. + +### Debugging in the plugin + +Start Cypress with the environment variable `DEBUG=@cypress/grep`. You will see a few messages from this plugin in the terminal output: + +``` +$ DEBUG=@cypress/grep npx cypress run --env grep=works,grepFilterSpecs=true +@cypress/grep: tests with "works" in their names +@cypress/grep: filtering specs using "works" in the title +@cypress/grep Cypress config env object: { grep: 'works', grepFilterSpecs: true } + ... + @cypress/grep found 1 spec files +5ms + @cypress/grep [ 'spec.js' ] +0ms + @cypress/grep spec file spec.js +5ms + @cypress/grep suite and test names: [ 'hello world', 'works', 'works 2 @tag1', + 'works 2 @tag1 @tag2', 'works @tag2' ] +0ms + @cypress/grep found "works" in 1 specs +0ms + @cypress/grep [ 'spec.js' ] +0ms +``` + +### Debugging in the browser + +To enable debug console messages in the browser, from the DevTools console set `localStorage.debug='@cypress/grep'` and run the tests again. + +![Debug messages](./images/debug.png) + +To see how to debug this plugin, watch the video [Debug @cypress/grep Plugin](https://youtu.be/4YMAERddHYA). + +## Examples + +- [cypress-grep-example](https://github.com/bahmutov/cypress-grep-example) +- [todo-graphql-example](https://github.com/bahmutov/todo-graphql-example) + +## See also + +- [cypress-select-tests](https://github.com/bahmutov/cypress-select-tests) +- [cypress-skip-test](https://github.com/cypress-io/cypress-skip-test) + +## Migration guide + +### from v1 to v2 + +In v2 we have separated grepping by part of the title string from tags. + +**v1** + +``` +--env grep="one two" +``` + +The above scenario was confusing - did you want to find all tests with title containing "one two" or did you want to run tests tagged `one` or `two`? + +**v2** + +``` +# enable the tests with string "one two" in their titles +--env grep="one two" +# enable the tests with tag "one" or "two" +--env grepTags="one two" +# enable the tests with both tags "one" and "two" +--env grepTags="one+two" +# enable the tests with "hello" in the title and tag "smoke" +--env grep=hello,grepTags=smoke +``` + +### from v2 to v3 + +Version >= 3 of @cypress/grep _only_ supports Cypress >= 10. + +## Small Print + +License: MIT - do anything with the code, but don't blame me if it does not work. + +Support: if you find any problems with this module, email / tweet / +[open issue](https://github.com/cypress-io/cypress/issues) on Github. diff --git a/npm/grep/cypress.config.js b/npm/grep/cypress.config.js new file mode 100644 index 00000000000..6502677a6d5 --- /dev/null +++ b/npm/grep/cypress.config.js @@ -0,0 +1,21 @@ +const { defineConfig } = require('cypress') +const cypressGrepPlugin = require('./src/plugin') + +module.exports = defineConfig({ + e2e: { + defaultCommandTimeout: 1000, + setupNodeEvents (on, config) { + cypressGrepPlugin(config) + + on('task', { + grep (config) { + return cypressGrepPlugin(config) + }, + }) + + return config + }, + specPattern: '**/spec.js', + }, + fixturesFolder: false, +}) diff --git a/npm/grep/cypress/e2e/before-spec.js b/npm/grep/cypress/e2e/before-spec.js new file mode 100644 index 00000000000..facc7b467dc --- /dev/null +++ b/npm/grep/cypress/e2e/before-spec.js @@ -0,0 +1,17 @@ +describe('Runs before and beforeEach when first test is skipped', () => { + let count = 0 + + before(() => { + count++ + }) + + beforeEach(() => { + count++ + }) + + it('A', { tags: ['@core'] }, () => {}) + + it('B', { tags: ['@core', '@staging'] }, () => { + expect(count).to.equal(2) + }) +}) diff --git a/npm/grep/cypress/e2e/burn-spec.js b/npm/grep/cypress/e2e/burn-spec.js new file mode 100644 index 00000000000..22229d48a6e --- /dev/null +++ b/npm/grep/cypress/e2e/burn-spec.js @@ -0,0 +1,9 @@ +/// + +// if we specify just the burn parameter +// then this test will be repeated N times +describe('burning a test N times', () => { + it('repeats', () => {}) + + it('second test', () => {}) +}) diff --git a/npm/grep/cypress/e2e/config-spec.js b/npm/grep/cypress/e2e/config-spec.js new file mode 100644 index 00000000000..d3b9e9c9d5a --- /dev/null +++ b/npm/grep/cypress/e2e/config-spec.js @@ -0,0 +1,7 @@ +// @ts-check +/// +describe('tests that use config object', () => { + it('still works @config', { baseUrl: 'http://localhost:8000' }, () => { + expect(Cypress.config('baseUrl')).to.equal('http://localhost:8000') + }) +}) diff --git a/npm/grep/cypress/e2e/config-tags-spec.js b/npm/grep/cypress/e2e/config-tags-spec.js new file mode 100644 index 00000000000..b0c6b82be3f --- /dev/null +++ b/npm/grep/cypress/e2e/config-tags-spec.js @@ -0,0 +1,16 @@ +// @ts-check +/// +describe('tags in the config object', () => { + it('works as an array', { tags: ['config', 'some-other-tag'] }, () => { + expect(true).to.be.true + }) + + it('works as a string', { tags: 'config' }, () => { + expect(true).to.be.true + }) + + it('does not use tags', () => { + // so it fails + expect(true).to.be.false + }) +}) diff --git a/npm/grep/cypress/e2e/describe-tags-spec.js b/npm/grep/cypress/e2e/describe-tags-spec.js new file mode 100644 index 00000000000..b3ee573351c --- /dev/null +++ b/npm/grep/cypress/e2e/describe-tags-spec.js @@ -0,0 +1,23 @@ +/// + +// @ts-check + +describe('block with no tags', () => { + it('inside describe 1', () => {}) + + it('inside describe 2', () => {}) +}) + +describe('block with tag smoke', { tags: '@smoke' }, () => { + it('inside describe 3', () => {}) + + it('inside describe 4', () => {}) +}) + +describe('block without any tags', () => { + // note the parent suite has no tags + // so this test should run when using --env grepTags=@smoke + it('test with tag smoke', { tags: '@smoke' }, () => {}) +}) + +it('is a test outside any suites', () => {}) diff --git a/npm/grep/cypress/e2e/each-spec.js b/npm/grep/cypress/e2e/each-spec.js new file mode 100644 index 00000000000..5dfc712ea88 --- /dev/null +++ b/npm/grep/cypress/e2e/each-spec.js @@ -0,0 +1,11 @@ +/// + +// https://github.com/bahmutov/cypress-each +import 'cypress-each' + +describe('tests that use .each work', () => { + // creating tests dynamically works with "cypress-grep" + it.each([1, 2, 3])('test for %d', (x) => { + expect(x).to.be.oneOf([1, 2, 3]) + }) +}) diff --git a/npm/grep/cypress/e2e/inherits-tag-spec.js b/npm/grep/cypress/e2e/inherits-tag-spec.js new file mode 100644 index 00000000000..ee805e2abbd --- /dev/null +++ b/npm/grep/cypress/e2e/inherits-tag-spec.js @@ -0,0 +1,7 @@ +/// + +describe('Screen A', { tags: ['@sanity', '@screen-a'] }, () => { + it('loads', { tags: ['@screen-b'] }, () => { + // do something that eventually sends the page to screen b. + }) +}) diff --git a/npm/grep/cypress/e2e/multiple-registrations.js b/npm/grep/cypress/e2e/multiple-registrations.js new file mode 100644 index 00000000000..1a73d675352 --- /dev/null +++ b/npm/grep/cypress/e2e/multiple-registrations.js @@ -0,0 +1,10 @@ +/// + +// register the plugin multiple times +// to simulate including from support and spec files +// https://github.com/cypress-io/cypress-grep/issues/59 +require('../../src/support')() +require('../../src/support')() +require('../../src/support')() + +it('hello world', () => {}) diff --git a/npm/grep/cypress/e2e/nested-describe-spec.js b/npm/grep/cypress/e2e/nested-describe-spec.js new file mode 100644 index 00000000000..8f3fefcea49 --- /dev/null +++ b/npm/grep/cypress/e2e/nested-describe-spec.js @@ -0,0 +1,18 @@ +/// + +// @ts-check +describe('grand', () => { + context('outer', { tags: '@smoke' }, () => { + describe('inner', () => { + it('runs', () => {}) + }) + }) +}) + +describe('top', { tags: '@smoke' }, () => { + describe('middle', () => { + context('bottom', { tags: ['@integration', '@fast'] }, () => { + it('runs too', () => {}) + }) + }) +}) diff --git a/npm/grep/cypress/e2e/omit-and-skip-spec.js b/npm/grep/cypress/e2e/omit-and-skip-spec.js new file mode 100644 index 00000000000..253ad35be4c --- /dev/null +++ b/npm/grep/cypress/e2e/omit-and-skip-spec.js @@ -0,0 +1,11 @@ +/// + +// @ts-check +describe('Page', () => { + describe('List', { tags: ['@us1'] }, () => { + // eslint-disable-next-line @cypress/dev/skip-comment + it.skip('first test', () => {}) + it('second test', () => {}) + it('third test', () => {}) + }) +}) diff --git a/npm/grep/cypress/e2e/skip-spec.js b/npm/grep/cypress/e2e/skip-spec.js new file mode 100644 index 00000000000..3ca63e02df4 --- /dev/null +++ b/npm/grep/cypress/e2e/skip-spec.js @@ -0,0 +1,10 @@ +/* eslint-disable @cypress/dev/skip-comment */ +/// +describe('tests that use .skip', () => { + // use a template literal + it(`works`, () => {}) + + it.skip('is pending', () => {}) + + it.skip('is pending again', () => {}) +}) diff --git a/npm/grep/cypress/e2e/spec.js b/npm/grep/cypress/e2e/spec.js new file mode 100644 index 00000000000..fd4df2f742f --- /dev/null +++ b/npm/grep/cypress/e2e/spec.js @@ -0,0 +1,11 @@ +/// + +it('hello world', () => {}) + +it('works', () => {}) + +it('works 2 @tag1', { tags: '@tag1' }, () => {}) + +it('works 2 @tag1 @tag2', { tags: ['@tag1', '@tag2'] }, () => {}) + +it('works @tag2', { tags: '@tag2' }, () => {}) diff --git a/npm/grep/cypress/e2e/specify-spec.js b/npm/grep/cypress/e2e/specify-spec.js new file mode 100644 index 00000000000..9651d32a3a2 --- /dev/null +++ b/npm/grep/cypress/e2e/specify-spec.js @@ -0,0 +1,13 @@ +/// + +// specify is the same as it() + +specify('hello world', () => {}) + +specify('works', () => {}) + +specify('works 2 @tag1', { tags: '@tag1' }, () => {}) + +specify('works 2 @tag1 @tag2', { tags: ['@tag1', '@tag2'] }, () => {}) + +specify('works @tag2', { tags: '@tag2' }, () => {}) diff --git a/npm/grep/cypress/e2e/tags/test1.spec.js b/npm/grep/cypress/e2e/tags/test1.spec.js new file mode 100644 index 00000000000..5296790e92a --- /dev/null +++ b/npm/grep/cypress/e2e/tags/test1.spec.js @@ -0,0 +1,5 @@ +/// + +it('Test 1', { tags: ['smoke', 'regression'] }, () => { + expect(true).to.be.true +}) diff --git a/npm/grep/cypress/e2e/tags/test2.spec.js b/npm/grep/cypress/e2e/tags/test2.spec.js new file mode 100644 index 00000000000..3044c0ecd1d --- /dev/null +++ b/npm/grep/cypress/e2e/tags/test2.spec.js @@ -0,0 +1,5 @@ +/// + +it('Test 2', { tags: ['high', 'smoke'] }, () => { + expect(true).to.be.true +}) diff --git a/npm/grep/cypress/e2e/tags/test3.spec.js b/npm/grep/cypress/e2e/tags/test3.spec.js new file mode 100644 index 00000000000..7579c453a21 --- /dev/null +++ b/npm/grep/cypress/e2e/tags/test3.spec.js @@ -0,0 +1,5 @@ +/// + +it('Test 3', { tags: ['smoke'] }, () => { + expect(true).to.be.true +}) diff --git a/npm/grep/cypress/e2e/this-spec.js b/npm/grep/cypress/e2e/this-spec.js new file mode 100644 index 00000000000..2b788e22ea2 --- /dev/null +++ b/npm/grep/cypress/e2e/this-spec.js @@ -0,0 +1,11 @@ +/// +describe('this context', () => { + beforeEach(() => { + cy.wrap(42).as('life') + }) + + it('preserves the test context', function () { + expect(this).to.be.an('object') + expect(this.life).to.equal(42) + }) +}) diff --git a/npm/grep/cypress/e2e/ts-spec.ts b/npm/grep/cypress/e2e/ts-spec.ts new file mode 100644 index 00000000000..8c5ed7d6187 --- /dev/null +++ b/npm/grep/cypress/e2e/ts-spec.ts @@ -0,0 +1,31 @@ +describe('TypeScript spec', () => { + it('works', () => { + type Person = { + name: string + } + + const person: Person = { + name: 'Joe', + } + + cy.wrap(person).should('have.property', 'name', 'Joe') + }) + + it('loads', () => { + const n: number = 1 + + cy.wrap(n).should('eq', 1) + }) + + it('loads interfaces', () => { + interface Person { + name: string + } + + const p: Person = { + name: 'Joe', + } + + cy.wrap(p).should('have.property', 'name', 'Joe') + }) +}) diff --git a/npm/grep/cypress/e2e/unit.js b/npm/grep/cypress/e2e/unit.js new file mode 100644 index 00000000000..da61404ccb2 --- /dev/null +++ b/npm/grep/cypress/e2e/unit.js @@ -0,0 +1,472 @@ +/// + +import { + parseGrep, + parseTitleGrep, + parseFullTitleGrep, + parseTagsGrep, + shouldTestRun, + shouldTestRunTags, + shouldTestRunTitle, +} from '../../src/utils' + +describe('utils', () => { + context('parseTitleGrep', () => { + it('grabs the positive title', () => { + const parsed = parseTitleGrep('hello w') + + expect(parsed).to.deep.equal({ + title: 'hello w', + invert: false, + }) + }) + + it('trims the string', () => { + const parsed = parseTitleGrep(' hello w ') + + expect(parsed).to.deep.equal({ + title: 'hello w', + invert: false, + }) + }) + + it('inverts the string', () => { + const parsed = parseTitleGrep('-hello w') + + expect(parsed).to.deep.equal({ + title: 'hello w', + invert: true, + }) + }) + + it('trims the inverted the string', () => { + const parsed = parseTitleGrep(' -hello w ') + + expect(parsed).to.deep.equal({ + title: 'hello w', + invert: true, + }) + }) + + it('returns null for undefined input', () => { + const parsed = parseTitleGrep() + + expect(parsed).to.equal(null) + }) + }) + + context('parseFullTitleGrep', () => { + it('returns list of title greps', () => { + const parsed = parseFullTitleGrep('hello; one; -two') + + expect(parsed).to.deep.equal([ + { title: 'hello', invert: false }, + { title: 'one', invert: false }, + { title: 'two', invert: true }, + ]) + }) + }) + + context('parseTagsGrep', () => { + it('parses AND tags', () => { + // run only the tests with all 3 tags + const parsed = parseTagsGrep('@tag1+@tag2+@tag3') + + expect(parsed).to.deep.equal([ + // single OR part + [ + // with 3 AND parts + { tag: '@tag1', invert: false }, + { tag: '@tag2', invert: false }, + { tag: '@tag3', invert: false }, + ], + ]) + }) + + it('handles dashes in the tag', () => { + const parsed = parseTagsGrep('@smoke+@screen-b') + + expect(parsed).to.deep.equal([ + [ + { tag: '@smoke', invert: false }, + { tag: '@screen-b', invert: false }, + ], + ]) + }) + + it('parses OR tags spaces', () => { + // run tests with tag1 OR tag2 or tag3 + const parsed = parseTagsGrep('@tag1 @tag2 @tag3') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: false }], + [{ tag: '@tag2', invert: false }], + [{ tag: '@tag3', invert: false }], + ]) + }) + + it('parses OR tags commas', () => { + // run tests with tag1 OR tag2 or tag3 + const parsed = parseTagsGrep('@tag1,@tag2,@tag3') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: false }], + [{ tag: '@tag2', invert: false }], + [{ tag: '@tag3', invert: false }], + ]) + }) + + it('parses inverted tag', () => { + const parsed = parseTagsGrep('-@tag1') + + expect(parsed).to.deep.equal([[{ tag: '@tag1', invert: true }]]) + }) + + it('parses tag1 but not tag2 with space', () => { + const parsed = parseTagsGrep('@tag1 -@tag2') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: false }], + [{ tag: '@tag2', invert: true }], + ]) + }) + + it('forgives extra spaces', () => { + const parsed = parseTagsGrep(' @tag1 -@tag2 ') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: false }], + [{ tag: '@tag2', invert: true }], + ]) + }) + + it('parses tag1 but not tag2 with comma', () => { + const parsed = parseTagsGrep('@tag1,-@tag2') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: false }], + [{ tag: '@tag2', invert: true }], + ]) + }) + + it('filters out empty tags', () => { + const parsed = parseTagsGrep(',, @tag1,-@tag2,, ,, ,') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: false }], + [{ tag: '@tag2', invert: true }], + ]) + }) + + // TODO: would need to change the tokenizer + it.skip('parses tag1 but not tag2', () => { + const parsed = parseTagsGrep('@tag1-@tag2') + + expect(parsed).to.deep.equal([ + [ + { tag: '@tag1', invert: false }, + { tag: '@tag2', invert: true }, + ], + ]) + }) + + it('allows all tags to be inverted', () => { + const parsed = parseTagsGrep('--@tag1,--@tag2') + + expect(parsed).to.deep.equal([ + [{ tag: '@tag1', invert: true }, { tag: '@tag2', invert: true }], + ]) + }) + }) + + context('parseGrep', () => { + // no need to exhaustively test the parsing + // since we want to confirm it works via test names + // and not through the implementation details of + // the parsed object + + it('creates just the title grep', () => { + const parsed = parseGrep('hello w') + + expect(parsed).to.deep.equal({ + title: [ + { + title: 'hello w', + invert: false, + }, + ], + tags: [], + }) + }) + + it('creates object from the grep string only', () => { + const parsed = parseGrep('hello w') + + expect(parsed).to.deep.equal({ + title: [ + { + title: 'hello w', + invert: false, + }, + ], + tags: [], + }) + + // check how the parsed grep works against specific tests + expect(shouldTestRun(parsed, 'hello w')).to.equal(true) + expect(shouldTestRun(parsed, 'hello no')).to.equal(false) + }) + + it('matches one of the titles', () => { + // also should trim each title + const parsed = parseGrep(' hello w; work 2 ') + + expect(parsed).to.deep.equal({ + title: [ + { + title: 'hello w', + invert: false, + }, + { + title: 'work 2', + invert: false, + }, + ], + tags: [], + }) + + // check how the parsed grep works against specific tests + expect(shouldTestRun(parsed, 'hello w')).to.equal(true) + expect(shouldTestRun(parsed, 'this work 2 works')).to.equal(true) + expect(shouldTestRun(parsed, 'hello no')).to.equal(false) + }) + + it('creates object from the grep string and tags', () => { + const parsed = parseGrep('hello w', '@tag1+@tag2+@tag3') + + expect(parsed).to.deep.equal({ + title: [ + { + title: 'hello w', + invert: false, + }, + ], + tags: [ + // single OR part + [ + // with 3 AND parts + { tag: '@tag1', invert: false }, + { tag: '@tag2', invert: false }, + { tag: '@tag3', invert: false }, + ], + ], + }) + + // check how the parsed grep works against specific tests + expect(shouldTestRun(parsed, 'hello w'), 'needs tags').to.equal(false) + expect(shouldTestRun(parsed, 'hello no')).to.equal(false) + // not every tag is present + expect(shouldTestRun(parsed, ['@tag1', '@tag2'])).to.equal(false) + expect(shouldTestRun(parsed, ['@tag1', '@tag2', '@tag3'])).to.equal(true) + expect( + shouldTestRun(parsed, ['@tag1', '@tag2', '@tag3', '@tag4']), + ).to.equal(true) + + // title matches, but tags do not + expect(shouldTestRun(parsed, 'hello w', ['@tag1', '@tag2'])).to.equal( + false, + ) + + // tags and title match + expect( + shouldTestRun(parsed, 'hello w', ['@tag1', '@tag2', '@tag3']), + ).to.equal(true) + }) + }) + + context('shouldTestRunTags', () => { + // when the user types "used" string + // and the test has the given tags, make sure + // our parsing and decision logic computes the expected result + const shouldIt = (used, tags, expected) => { + const parsedTags = parseTagsGrep(used) + + expect( + shouldTestRunTags(parsedTags, tags), + `"${used}" against "${tags}"`, + ).to.equal(expected) + } + + it('handles AND tags', () => { + shouldIt('smoke+slow', ['fast', 'smoke'], false) + shouldIt('smoke+slow', ['mobile', 'smoke', 'slow'], true) + shouldIt('smoke+slow', ['slow', 'extra', 'smoke'], true) + shouldIt('smoke+slow', ['smoke'], false) + }) + + it('handles OR tags', () => { + // smoke OR slow + shouldIt('smoke slow', ['fast', 'smoke'], true) + shouldIt('smoke', ['mobile', 'smoke', 'slow'], true) + shouldIt('slow', ['slow', 'extra', 'smoke'], true) + shouldIt('smoke', ['smoke'], true) + shouldIt('smoke', ['slow'], false) + }) + + it('handles invert tag', () => { + // should not run - we are excluding the "slow" + shouldIt('smoke+-slow', ['smoke', 'slow'], false) + shouldIt('mobile+-slow', ['smoke', 'slow'], false) + shouldIt('smoke -slow', ['smoke', 'fast'], true) + shouldIt('-slow', ['smoke', 'slow'], false) + shouldIt('-slow', ['smoke'], true) + // no tags in the test + shouldIt('-slow', [], true) + }) + }) + + context('shouldTestRun', () => { + // a little utility function to parse the given grep string + // and apply the first argument in shouldTestRun + const checkName = (grep, grepTags) => { + const parsed = parseGrep(grep, grepTags) + + expect(parsed).to.be.an('object') + + return (testName, testTags = []) => { + expect(testName, 'test title').to.be.a('string') + expect(testTags, 'test tags').to.be.an('array') + + return shouldTestRun(parsed, testName, testTags) + } + } + + it('simple tag', () => { + const parsed = parseGrep('@tag1') + + expect(shouldTestRun(parsed, 'no tag1 here')).to.be.false + expect(shouldTestRun(parsed, 'has @tag1 in the name')).to.be.true + }) + + it('with invert title', () => { + const t = checkName('-hello') + + expect(t('no greetings')).to.be.true + expect(t('has hello world')).to.be.false + }) + + it('with invert option', () => { + const t = checkName(null, '-@tag1') + + expect(t('no tags here')).to.be.true + expect(t('has tag1', ['@tag1'])).to.be.false + expect(t('has other tags', ['@tag2'])).to.be.true + }) + + it('with AND option', () => { + const t = checkName('', '@tag1+@tag2') + + expect(t('no tag1 here')).to.be.false + expect(t('has only @tag1', ['@tag1'])).to.be.false + expect(t('has only @tag2', ['@tag2'])).to.be.false + expect(t('has both tags', ['@tag1', '@tag2'])).to.be.true + }) + + it('with OR option', () => { + const t = checkName(null, '@tag1 @tag2') + + expect(t('no tag1 here')).to.be.false + expect(t('has only @tag1 in the name', ['@tag1'])).to.be.true + expect(t('has only @tag2 in the name', ['@tag2'])).to.be.true + expect(t('has @tag1 and @tag2 in the name', ['@tag1', '@tag2'])).to.be + .true + }) + + it('OR with AND option', () => { + const t = checkName(null, '@tag1 @tag2+@tag3') + + expect(t('no tag1 here')).to.be.false + expect(t('has only @tag1 in the name', ['@tag1'])).to.be.true + expect(t('has only @tag2 in the name', ['@tag2'])).to.be.false + expect(t('has only @tag2 in the name and also @tag3', ['@tag2', '@tag3'])) + .to.be.true + + expect( + t('has @tag1 and @tag2 and @tag3 in the name', [ + '@tag1', + '@tag2', + '@tag3', + ]), + ).to.be.true + }) + + it('Multiple invert strings and a simple one', () => { + const t = checkName('-name;-hey;number') + + expect(t('number should only be matches without a n-a-m-e')).to.be.true + expect(t('number can\'t be name')).to.be.false + expect(t('The man needs a name')).to.be.false + expect(t('number hey name')).to.be.false + expect(t('numbers hey name')).to.be.false + expect(t('number hsey nsame')).to.be.true + expect(t('This wont match')).to.be.false + }) + + it('Only inverted strings', () => { + const t = checkName('-name;-hey') + + expect(t('I\'m matched')).to.be.true + expect(t('hey! I\'m not')).to.be.false + expect(t('My name is weird')).to.be.false + }) + }) + + context('parseFullTitleGrep', () => { + const shouldIt = (search, testName, expected) => { + const parsed = parseFullTitleGrep(search) + + expect( + shouldTestRunTitle(parsed, testName), + `"${search}" against title "${testName}"`, + ).to.equal(expected) + } + + it('passes for substring', () => { + shouldIt('hello w', 'hello world', true) + shouldIt('-hello w', 'hello world', false) + }) + }) +}) + +describe('plugin', () => { + context('excludeSpecPattern', () => { + it('supports an array value', () => { + cy.task('grep', { + excludeSpecPattern: ['**/test2.spec.js', '**/test3.spec.js'], + specPattern: '**/*.spec.js', + env: { + grepTags: 'smoke', + grepFilterSpecs: true, + }, + }).then((config) => { + expect(config.specPattern.length).to.equal(1) + expect(config.specPattern[0]).to.contain('test1.spec.js') + }) + }) + + it('supports a string value', () => { + cy.task('grep', { + excludeSpecPattern: '**/test2.spec.js', + specPattern: '**/*.spec.js', + env: { + grepTags: 'smoke', + grepFilterSpecs: true, + }, + }).then((config) => { + expect(config.specPattern.length).to.equal(2) + expect(config.specPattern[0]).to.contain('test1.spec.js') + expect(config.specPattern[1]).to.contain('test3.spec.js') + }) + }) + }) +}) diff --git a/npm/grep/cypress/support/e2e.js b/npm/grep/cypress/support/e2e.js new file mode 100644 index 00000000000..36fa4897e90 --- /dev/null +++ b/npm/grep/cypress/support/e2e.js @@ -0,0 +1,8 @@ +// @ts-check +/// + +import cypressGrep from '../../src/support' + +// register the grep feature +// https://github.com/cypress-io/cypress-grep +cypressGrep() diff --git a/npm/grep/cypress/tsconfig.json b/npm/grep/cypress/tsconfig.json new file mode 100644 index 00000000000..d6ad9937055 --- /dev/null +++ b/npm/grep/cypress/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "types": ["cypress"] + }, + "include": ["e2e/**/*.ts"] +} diff --git a/npm/grep/expects/README.md b/npm/grep/expects/README.md new file mode 100644 index 00000000000..61a360f4d65 --- /dev/null +++ b/npm/grep/expects/README.md @@ -0,0 +1,3 @@ +Different JSON files with expected test statuses when running Cypress tests with different grep argument. + +Used via [cypress-expect](https://github.com/bahmutov/cypress-expect) with `--expect` option to run on CI to verify the correct tests were pending or passing. diff --git a/npm/grep/expects/all-pending.json b/npm/grep/expects/all-pending.json new file mode 100644 index 00000000000..87005e83530 --- /dev/null +++ b/npm/grep/expects/all-pending.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/before.json b/npm/grep/expects/before.json new file mode 100644 index 00000000000..cf306f54639 --- /dev/null +++ b/npm/grep/expects/before.json @@ -0,0 +1,6 @@ +{ + "Runs before and beforeEach when first test is skipped": { + "A": "pending", + "B": "passed" + } +} diff --git a/npm/grep/expects/burn-spec.json b/npm/grep/expects/burn-spec.json new file mode 100644 index 00000000000..c0b40216152 --- /dev/null +++ b/npm/grep/expects/burn-spec.json @@ -0,0 +1,14 @@ +{ + "burning a test N times": { + "repeats: burning 1 of 5": "pass", + "repeats: burning 2 of 5": "pass", + "repeats: burning 3 of 5": "pass", + "repeats: burning 4 of 5": "pass", + "repeats: burning 5 of 5": "pass", + "second test: burning 1 of 5": "pass", + "second test: burning 2 of 5": "pass", + "second test: burning 3 of 5": "pass", + "second test: burning 4 of 5": "pass", + "second test: burning 5 of 5": "pass" + } +} diff --git a/npm/grep/expects/config-spec.json b/npm/grep/expects/config-spec.json new file mode 100644 index 00000000000..12de586cf05 --- /dev/null +++ b/npm/grep/expects/config-spec.json @@ -0,0 +1,5 @@ +{ + "tests that use config object": { + "still works @config": "passed" + } +} diff --git a/npm/grep/expects/config-tags-spec.json b/npm/grep/expects/config-tags-spec.json new file mode 100644 index 00000000000..5c742cb494b --- /dev/null +++ b/npm/grep/expects/config-tags-spec.json @@ -0,0 +1,7 @@ +{ + "tags in the config object": { + "works as an array": "passed", + "works as a string": "passed", + "does not use tags": "pending" + } +} diff --git a/npm/grep/expects/describe-tags-invert-spec.json b/npm/grep/expects/describe-tags-invert-spec.json new file mode 100644 index 00000000000..e21542eaff9 --- /dev/null +++ b/npm/grep/expects/describe-tags-invert-spec.json @@ -0,0 +1,14 @@ +{ + "block with no tags": { + "inside describe 1": "passing", + "inside describe 2": "passing" + }, + "block with tag smoke": { + "inside describe 3": "pending", + "inside describe 4": "pending" + }, + "block without any tags": { + "test with tag smoke": "pending" + }, + "is a test outside any suites": "passing" +} diff --git a/npm/grep/expects/describe-tags-spec-untagged.json b/npm/grep/expects/describe-tags-spec-untagged.json new file mode 100644 index 00000000000..e21542eaff9 --- /dev/null +++ b/npm/grep/expects/describe-tags-spec-untagged.json @@ -0,0 +1,14 @@ +{ + "block with no tags": { + "inside describe 1": "passing", + "inside describe 2": "passing" + }, + "block with tag smoke": { + "inside describe 3": "pending", + "inside describe 4": "pending" + }, + "block without any tags": { + "test with tag smoke": "pending" + }, + "is a test outside any suites": "passing" +} diff --git a/npm/grep/expects/describe-tags-spec.json b/npm/grep/expects/describe-tags-spec.json new file mode 100644 index 00000000000..2e15bf74a5b --- /dev/null +++ b/npm/grep/expects/describe-tags-spec.json @@ -0,0 +1,14 @@ +{ + "block with no tags": { + "inside describe 1": "pending", + "inside describe 2": "pending" + }, + "block with tag smoke": { + "inside describe 3": "passed", + "inside describe 4": "passed" + }, + "block without any tags": { + "test with tag smoke": "passed" + }, + "is a test outside any suites": "pending" +} diff --git a/npm/grep/expects/each-spec.json b/npm/grep/expects/each-spec.json new file mode 100644 index 00000000000..6dd57361e17 --- /dev/null +++ b/npm/grep/expects/each-spec.json @@ -0,0 +1,7 @@ +{ + "tests that use .each work": { + "test for 1": "pending", + "test for 2": "passing", + "test for 3": "pending" + } +} diff --git a/npm/grep/expects/grep-filter-specs-tag.json b/npm/grep/expects/grep-filter-specs-tag.json new file mode 100644 index 00000000000..63a106f756b --- /dev/null +++ b/npm/grep/expects/grep-filter-specs-tag.json @@ -0,0 +1,28 @@ +{ + "block with no tags": { + "inside describe 1": "pending", + "inside describe 2": "pending" + }, + "block with tag smoke": { + "inside describe 3": "passing", + "inside describe 4": "passing" + }, + "block without any tags": { + "test with tag smoke": "passing" + }, + "is a test outside any suites": "pending", + "grand": { + "outer": { + "inner": { + "runs": "passing" + } + } + }, + "top": { + "middle": { + "bottom": { + "runs too": "passing" + } + } + } +} diff --git a/npm/grep/expects/grep-filter-specs.json b/npm/grep/expects/grep-filter-specs.json new file mode 100644 index 00000000000..5fd27bfde29 --- /dev/null +++ b/npm/grep/expects/grep-filter-specs.json @@ -0,0 +1,14 @@ +{ + "is a test outside any suites": "passing", + "block with no tags": { + "inside describe 1": "pending", + "inside describe 2": "pending" + }, + "block with tag smoke": { + "inside describe 3": "pending", + "inside describe 4": "pending" + }, + "block without any tags": { + "test with tag smoke": "pending" + } +} diff --git a/npm/grep/expects/grep-untagged.json b/npm/grep/expects/grep-untagged.json new file mode 100644 index 00000000000..f8b91c5f9c1 --- /dev/null +++ b/npm/grep/expects/grep-untagged.json @@ -0,0 +1,7 @@ +{ + "hello world": "passing", + "works": "passing", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/hello-burn.json b/npm/grep/expects/hello-burn.json new file mode 100644 index 00000000000..58bf7c698f7 --- /dev/null +++ b/npm/grep/expects/hello-burn.json @@ -0,0 +1,9 @@ +{ + "hello world: burning 1 of 3": "passed", + "hello world: burning 2 of 3": "passed", + "hello world: burning 3 of 3": "passed", + "works": "pending", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/hello-or-works-2.json b/npm/grep/expects/hello-or-works-2.json new file mode 100644 index 00000000000..f1d0722064f --- /dev/null +++ b/npm/grep/expects/hello-or-works-2.json @@ -0,0 +1,7 @@ +{ + "hello world": "passed", + "works": "pending", + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "passed", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/hello.json b/npm/grep/expects/hello.json new file mode 100644 index 00000000000..c25d99c4f8a --- /dev/null +++ b/npm/grep/expects/hello.json @@ -0,0 +1,7 @@ +{ + "hello world": "passed", + "works": "pending", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/inherits-tag-spec.json b/npm/grep/expects/inherits-tag-spec.json new file mode 100644 index 00000000000..128005751fe --- /dev/null +++ b/npm/grep/expects/inherits-tag-spec.json @@ -0,0 +1,5 @@ +{ + "Screen A": { + "loads": "passed" + } +} diff --git a/npm/grep/expects/invert-tag1.json b/npm/grep/expects/invert-tag1.json new file mode 100644 index 00000000000..dbef7dc4cab --- /dev/null +++ b/npm/grep/expects/invert-tag1.json @@ -0,0 +1,7 @@ +{ + "hello world": "passing", + "works": "passing", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "passing" +} diff --git a/npm/grep/expects/multiple-registrations.json b/npm/grep/expects/multiple-registrations.json new file mode 100644 index 00000000000..7a56ff7ad1a --- /dev/null +++ b/npm/grep/expects/multiple-registrations.json @@ -0,0 +1,5 @@ +{ + "hello world: burning 1 of 3": "passed", + "hello world: burning 2 of 3": "passed", + "hello world: burning 3 of 3": "passed" +} diff --git a/npm/grep/expects/nested-describe-inheriting-names-spec.json b/npm/grep/expects/nested-describe-inheriting-names-spec.json new file mode 100644 index 00000000000..3b406afdfda --- /dev/null +++ b/npm/grep/expects/nested-describe-inheriting-names-spec.json @@ -0,0 +1,16 @@ +{ + "grand": { + "outer": { + "inner": { + "runs": "pending" + } + } + }, + "top": { + "middle": { + "bottom": { + "runs too": "passing" + } + } + } +} diff --git a/npm/grep/expects/nested-describe-inheriting-tags-spec.json b/npm/grep/expects/nested-describe-inheriting-tags-spec.json new file mode 100644 index 00000000000..3b406afdfda --- /dev/null +++ b/npm/grep/expects/nested-describe-inheriting-tags-spec.json @@ -0,0 +1,16 @@ +{ + "grand": { + "outer": { + "inner": { + "runs": "pending" + } + } + }, + "top": { + "middle": { + "bottom": { + "runs too": "passing" + } + } + } +} diff --git a/npm/grep/expects/nested-describe-spec.json b/npm/grep/expects/nested-describe-spec.json new file mode 100644 index 00000000000..990270c975f --- /dev/null +++ b/npm/grep/expects/nested-describe-spec.json @@ -0,0 +1,16 @@ +{ + "grand": { + "outer": { + "inner": { + "runs": "passing" + } + } + }, + "top": { + "middle": { + "bottom": { + "runs too": "passing" + } + } + } +} diff --git a/npm/grep/expects/no-hello-no-works2.json b/npm/grep/expects/no-hello-no-works2.json new file mode 100644 index 00000000000..65a2cf82c05 --- /dev/null +++ b/npm/grep/expects/no-hello-no-works2.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "passed", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "passed" +} diff --git a/npm/grep/expects/no-hello.json b/npm/grep/expects/no-hello.json new file mode 100644 index 00000000000..c70c9c66546 --- /dev/null +++ b/npm/grep/expects/no-hello.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "passed", + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "passed", + "works @tag2": "passed" +} diff --git a/npm/grep/expects/number1.json b/npm/grep/expects/number1.json new file mode 100644 index 00000000000..9032d535dbe --- /dev/null +++ b/npm/grep/expects/number1.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "passed", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/omit-and-skip.json b/npm/grep/expects/omit-and-skip.json new file mode 100644 index 00000000000..10215299cf5 --- /dev/null +++ b/npm/grep/expects/omit-and-skip.json @@ -0,0 +1,9 @@ +{ + "Page": { + "List": { + "first test": "pending", + "second test": "passed", + "third test": "passed" + } + } +} diff --git a/npm/grep/expects/omit-filtered.json b/npm/grep/expects/omit-filtered.json new file mode 100644 index 00000000000..c123168d830 --- /dev/null +++ b/npm/grep/expects/omit-filtered.json @@ -0,0 +1,4 @@ +{ + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "passed" +} diff --git a/npm/grep/expects/pending.json b/npm/grep/expects/pending.json new file mode 100644 index 00000000000..72bbd91f338 --- /dev/null +++ b/npm/grep/expects/pending.json @@ -0,0 +1,7 @@ +{ + "tests that use .skip": { + "works": "pending", + "is pending": "pending", + "is pending again": "pending" + } +} diff --git a/npm/grep/expects/specify.json b/npm/grep/expects/specify.json new file mode 100644 index 00000000000..9032d535dbe --- /dev/null +++ b/npm/grep/expects/specify.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "passed", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/tag1-and-tag2.json b/npm/grep/expects/tag1-and-tag2.json new file mode 100644 index 00000000000..ba8712abe9a --- /dev/null +++ b/npm/grep/expects/tag1-and-tag2.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "passed", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/tag1-without-tag2.json b/npm/grep/expects/tag1-without-tag2.json new file mode 100644 index 00000000000..9b6c08b11a5 --- /dev/null +++ b/npm/grep/expects/tag1-without-tag2.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/tag1.json b/npm/grep/expects/tag1.json new file mode 100644 index 00000000000..9032d535dbe --- /dev/null +++ b/npm/grep/expects/tag1.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "passed", + "works 2 @tag1 @tag2": "passed", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/tag2.json b/npm/grep/expects/tag2.json new file mode 100644 index 00000000000..471c7adcbf0 --- /dev/null +++ b/npm/grep/expects/tag2.json @@ -0,0 +1,6 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "passed" +} diff --git a/npm/grep/expects/tags-and.json b/npm/grep/expects/tags-and.json new file mode 100644 index 00000000000..84c4a522518 --- /dev/null +++ b/npm/grep/expects/tags-and.json @@ -0,0 +1,5 @@ +{ + "Test 1": "pending", + "Test 2": "passing", + "Test 3": "pending" +} diff --git a/npm/grep/expects/tags-or-filter.json b/npm/grep/expects/tags-or-filter.json new file mode 100644 index 00000000000..260fd0cef36 --- /dev/null +++ b/npm/grep/expects/tags-or-filter.json @@ -0,0 +1,4 @@ +{ + "Test 1": "passing", + "Test 2": "passing" +} diff --git a/npm/grep/expects/tags-or.json b/npm/grep/expects/tags-or.json new file mode 100644 index 00000000000..d9a758dd5bd --- /dev/null +++ b/npm/grep/expects/tags-or.json @@ -0,0 +1,5 @@ +{ + "Test 1": "passing", + "Test 2": "passing", + "Test 3": "passing" +} diff --git a/npm/grep/expects/test-npm-module.js b/npm/grep/expects/test-npm-module.js new file mode 100644 index 00000000000..0abade82e82 --- /dev/null +++ b/npm/grep/expects/test-npm-module.js @@ -0,0 +1,31 @@ +// https://github.com/cypress-io/cypress-grep/issues/41 +// shows how to pass grep parameters using Cypress NPM Module API +// https://on.cypress.io/module-api +const cypress = require('cypress') + +cypress +.run({ + env: { + grep: 'works', + grepTags: '@tag2', + }, +}) +.then((results) => { + // TODO use cypress-expects to compare the test results + if (results.totalTests !== 5) { + console.error('expected 5 tests total, got %d', results.totalTests) + process.exit(1) + } + + if (results.totalPassed !== 2) { + console.error('expected 2 tests passed, got %d', results.totalPassed) + process.exit(1) + } + + if (results.totalPending !== 3) { + console.error('expected 3 tests pending, got %d', results.totalPending) + process.exit(1) + } + + console.log(results.runs[0]) +}) diff --git a/npm/grep/expects/this-spec.json b/npm/grep/expects/this-spec.json new file mode 100644 index 00000000000..c07f53bed61 --- /dev/null +++ b/npm/grep/expects/this-spec.json @@ -0,0 +1,7 @@ +{ + "this context": { + "preserves the test context: burning 1 of 3": "passed", + "preserves the test context: burning 2 of 3": "passed", + "preserves the test context: burning 3 of 3": "passed" + } +} diff --git a/npm/grep/expects/ts-spec.json b/npm/grep/expects/ts-spec.json new file mode 100644 index 00000000000..60c25970061 --- /dev/null +++ b/npm/grep/expects/ts-spec.json @@ -0,0 +1,6 @@ +{ + "TypeScript spec": { + "loads": "passing", + "loads interfaces": "passing" + } +} diff --git a/npm/grep/expects/works-2.json b/npm/grep/expects/works-2.json new file mode 100644 index 00000000000..aee1dddd454 --- /dev/null +++ b/npm/grep/expects/works-2.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "passing", + "works 2 @tag1 @tag2": "passing", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/works-and-tag1.json b/npm/grep/expects/works-and-tag1.json new file mode 100644 index 00000000000..aee1dddd454 --- /dev/null +++ b/npm/grep/expects/works-and-tag1.json @@ -0,0 +1,7 @@ +{ + "hello world": "pending", + "works": "pending", + "works 2 @tag1": "passing", + "works 2 @tag1 @tag2": "passing", + "works @tag2": "pending" +} diff --git a/npm/grep/expects/works-hello-no-2.json b/npm/grep/expects/works-hello-no-2.json new file mode 100644 index 00000000000..8572a0d2d47 --- /dev/null +++ b/npm/grep/expects/works-hello-no-2.json @@ -0,0 +1,7 @@ +{ + "hello world": "passed", + "works": "passed", + "works 2 @tag1": "pending", + "works 2 @tag1 @tag2": "pending", + "works @tag2": "pending" +} diff --git a/npm/grep/images/config.png b/npm/grep/images/config.png new file mode 100644 index 00000000000..3370c6f37f1 Binary files /dev/null and b/npm/grep/images/config.png differ diff --git a/npm/grep/images/debug.png b/npm/grep/images/debug.png new file mode 100644 index 00000000000..9e4b179c370 Binary files /dev/null and b/npm/grep/images/debug.png differ diff --git a/npm/grep/images/includes-pending.png b/npm/grep/images/includes-pending.png new file mode 100644 index 00000000000..335681c8f6e Binary files /dev/null and b/npm/grep/images/includes-pending.png differ diff --git a/npm/grep/images/omit-pending.png b/npm/grep/images/omit-pending.png new file mode 100644 index 00000000000..ffa07bcdc42 Binary files /dev/null and b/npm/grep/images/omit-pending.png differ diff --git a/npm/grep/package.json b/npm/grep/package.json new file mode 100644 index 00000000000..d753ff3c2fc --- /dev/null +++ b/npm/grep/package.json @@ -0,0 +1,49 @@ +{ + "name": "@cypress/grep", + "version": "0.0.0-development", + "description": "Filter tests using substring", + "main": "src/support.js", + "scripts": { + "cy:open": "node ../../scripts/cypress.js open --e2e -b electron --config specPattern='**/unit.js'", + "cy:run": "node ../../scripts/cypress.js run --config specPattern='**/unit.js'", + "lint": "eslint . --ext .js,.ts" + }, + "dependencies": { + "debug": "^4.3.4", + "find-test-names": "^1.28.18", + "globby": "^11.0.4" + }, + "devDependencies": { + "cypress-each": "^1.11.0", + "cypress-expect": "^2.5.3", + "typescript": "^5.4.5" + }, + "peerDependencies": { + "cypress": ">=10" + }, + "files": [ + "src" + ], + "types": "src/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/cypress-io/cypress.git" + }, + "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/grep#readme", + "bugs": { + "url": "https://github.com/cypress-io/cypress/issues" + }, + "keywords": [ + "cypress", + "grep" + ], + "publishConfig": { + "access": "public" + }, + "nx": { + "implicitDependencies": [ + "!cypress" + ] + } +} diff --git a/npm/grep/src/index.d.ts b/npm/grep/src/index.d.ts new file mode 100644 index 00000000000..8410b981d84 --- /dev/null +++ b/npm/grep/src/index.d.ts @@ -0,0 +1,31 @@ +/// + +declare namespace Cypress { + interface SuiteConfigOverrides { + /** + * List of tags for this suite + * @example a single tag + * describe('block with config tag', { tags: '@smoke' }, () => {}) + * @example multiple tags + * describe('block with config tag', { tags: ['@smoke', '@slow'] }, () => {}) + */ + tags?: string | string[] + } + + // specify additional properties in the TestConfig object + // in our case we will add "tags" property + interface TestConfigOverrides { + /** + * List of tags for this test + * @example a single tag + * it('logs in', { tags: '@smoke' }, () => { ... }) + * @example multiple tags + * it('works', { tags: ['@smoke', '@slow'] }, () => { ... }) + */ + tags?: string | string[] + } + + interface Cypress { + grep?: (grep?: string, tags?: string, burn?: string) => void + } +} diff --git a/npm/grep/src/plugin.js b/npm/grep/src/plugin.js new file mode 100644 index 00000000000..483b3aa85f1 --- /dev/null +++ b/npm/grep/src/plugin.js @@ -0,0 +1,156 @@ +const debug = require('debug')('@cypress/grep') +const globby = require('globby') +const { getTestNames } = require('find-test-names') +const fs = require('fs') +const { version } = require('../package.json') +const { parseGrep, shouldTestRun } = require('./utils') + +/** + * Prints the @cypress/grep environment values if any. + * @param {Cypress.ConfigOptions} config + */ +function cypressGrepPlugin (config) { + if (!config || !config.env) { + return config + } + + const { env } = config + + if (!config.specPattern) { + throw new Error( + 'Incompatible versions detected, @cypress/grep 3.0.0+ requires Cypress 10.0.0+', + ) + } + + debug('@cypress/grep plugin version %s', version) + debug('Cypress config env object: %o', env) + + const grep = env.grep ? String(env.grep) : undefined + + if (grep) { + console.log('@cypress/grep: tests with "%s" in their names', grep.trim()) + } + + const grepTags = env.grepTags || env['grep-tags'] + + if (grepTags) { + console.log('@cypress/grep: filtering using tag(s) "%s"', grepTags) + const parsedGrep = parseGrep(null, grepTags) + + debug('parsed grep tags %o', parsedGrep.tags) + } + + const grepBurn = env.grepBurn || env['grep-burn'] || env.burn + + if (grepBurn) { + console.log('@cypress/grep: running filtered tests %d times', grepBurn) + } + + const grepUntagged = env.grepUntagged || env['grep-untagged'] + + if (grepUntagged) { + console.log('@cypress/grep: running untagged tests') + } + + const omitFiltered = env.grepOmitFiltered || env['grep-omit-filtered'] + + if (omitFiltered) { + console.log('@cypress/grep: will omit filtered tests') + } + + const { specPattern, excludeSpecPattern } = config + const integrationFolder = env.grepIntegrationFolder || process.cwd() + + const grepFilterSpecs = env.grepFilterSpecs === true + + if (grepFilterSpecs) { + debug('specPattern', specPattern) + debug('excludeSpecPattern', excludeSpecPattern) + debug('integrationFolder', integrationFolder) + const specFiles = globby.sync(specPattern, { + cwd: integrationFolder, + ignore: Array.isArray(excludeSpecPattern) ? excludeSpecPattern : [excludeSpecPattern], + absolute: true, + }) + + debug('found %d spec files', specFiles.length) + debug('%o', specFiles) + let greppedSpecs = [] + + if (grep) { + console.log('@cypress/grep: filtering specs using "%s" in the title', grep) + const parsedGrep = parseGrep(grep) + + debug('parsed grep %o', parsedGrep) + greppedSpecs = specFiles.filter((specFile) => { + const text = fs.readFileSync(specFile, { encoding: 'utf8' }) + + try { + const names = getTestNames(text) + const testAndSuiteNames = names.suiteNames.concat(names.testNames) + + debug('spec file %s', specFile) + debug('suite and test names: %o', testAndSuiteNames) + + return testAndSuiteNames.some((name) => { + const shouldRun = shouldTestRun(parsedGrep, name) + + return shouldRun + }) + } catch (err) { + debug(err.message) + debug(err.stack) + console.error('Could not determine test names in file: %s', specFile) + console.error('Will run it to let the grep filter the tests') + + return true + } + }) + + debug('found grep "%s" in %d specs', grep, greppedSpecs.length) + debug('%o', greppedSpecs) + } else if (grepTags) { + const parsedGrep = parseGrep(null, grepTags) + + debug('parsed grep tags %o', parsedGrep) + greppedSpecs = specFiles.filter((specFile) => { + const text = fs.readFileSync(specFile, { encoding: 'utf8' }) + + try { + const testInfo = getTestNames(text) + + debug('spec file %s', specFile) + debug('test info: %o', testInfo.tests) + + return testInfo.tests.some((info) => { + const shouldRun = shouldTestRun(parsedGrep, null, info.tags) + + return shouldRun + }) + } catch (err) { + console.error('Could not determine test names in file: %s', specFile) + console.error('Will run it to let the grep filter the tests') + + return true + } + }) + + debug('found grep tags "%s" in %d specs', grepTags, greppedSpecs.length) + debug('%o', greppedSpecs) + } + + if (greppedSpecs.length) { + config.specPattern = greppedSpecs + } else { + // hmm, we filtered out all specs, probably something is wrong + console.warn('grep and/or grepTags has eliminated all specs') + grep ? console.warn('grep: %s', grep) : null + grepTags ? console.warn('grepTags: %s', grepTags) : null + console.warn('Will leave all specs to run to filter at run-time') + } + } + + return config +} + +module.exports = cypressGrepPlugin diff --git a/npm/grep/src/support.js b/npm/grep/src/support.js new file mode 100644 index 00000000000..8797d678699 --- /dev/null +++ b/npm/grep/src/support.js @@ -0,0 +1,247 @@ +// @ts-check +/// + +const { parseGrep, shouldTestRun } = require('./utils') +// @ts-ignore +const { version } = require('../package.json') +const debug = require('debug')('@cypress/grep') + +debug.log = console.info.bind(console) + +// preserve the real "it" function +const _it = it +const _describe = describe + +/** + * Wraps the "it" and "describe" functions that support tags. + * @see https://github.com/cypress-io/cypress/tree/develop/npm/grep + */ +function cypressGrep () { + /** @type {string} Part of the test title go grep */ + let grep = Cypress.env('grep') + + if (grep) { + grep = String(grep).trim() + } + + /** @type {string} Raw tags to grep string */ + const grepTags = Cypress.env('grepTags') || Cypress.env('grep-tags') + + const burnSpecified = + Cypress.env('grepBurn') || Cypress.env('grep-burn') || Cypress.env('burn') + + const grepUntagged = + Cypress.env('grepUntagged') || Cypress.env('grep-untagged') + + if (!grep && !grepTags && !burnSpecified && !grepUntagged) { + // nothing to do, the user has no specified the "grep" string + debug('Nothing to grep, version %s', version) + + return + } + + /** @type {number} Number of times to repeat each running test */ + const grepBurn = + Cypress.env('grepBurn') || + Cypress.env('grep-burn') || + Cypress.env('burn') || + 1 + + /** @type {boolean} Omit filtered tests completely */ + const omitFiltered = + Cypress.env('grepOmitFiltered') || Cypress.env('grep-omit-filtered') + + debug('grep %o', { grep, grepTags, grepBurn, omitFiltered, version }) + if (!Cypress._.isInteger(grepBurn) || grepBurn < 1) { + throw new Error(`Invalid grep burn value: ${grepBurn}`) + } + + const parsedGrep = parseGrep(grep, grepTags) + + debug('parsed grep %o', parsedGrep) + + // prevent multiple registrations + // https://github.com/cypress-io/cypress-grep/issues/59 + if (it.name === 'itGrep') { + debug('already registered @cypress/grep') + + return + } + + it = function itGrep (name, options, callback) { + if (typeof options === 'function') { + // the test has format it('...', cb) + callback = options + options = {} + } + + if (!callback) { + // the pending test by itself + return _it(name, options) + } + + let configTags = options && options.tags + + if (typeof configTags === 'string') { + configTags = [configTags] + } + + const nameToGrep = suiteStack + .map((item) => item.name) + .concat(name) + .join(' ') + const tagsToGrep = suiteStack + .flatMap((item) => item.tags) + .concat(configTags) + .filter(Boolean) + + const shouldRun = shouldTestRun( + parsedGrep, + nameToGrep, + tagsToGrep, + grepUntagged, + ) + + if (tagsToGrep && tagsToGrep.length) { + debug( + 'should test "%s" with tags %s run? %s', + name, + tagsToGrep.join(','), + shouldRun, + ) + } else { + debug('should test "%s" run? %s', nameToGrep, shouldRun) + } + + if (shouldRun) { + if (grepBurn > 1) { + // repeat the same test to make sure it is solid + return Cypress._.times(grepBurn, (k) => { + const fullName = `${name}: burning ${k + 1} of ${grepBurn}` + + _it(fullName, options, callback) + }) + } + + return _it(name, options, callback) + } + + if (omitFiltered) { + // omit the filtered tests completely + return + } + + // skip tests without grep string in their names + return _it.skip(name, options, callback) + } + + // list of "describe" suites for the current test + // when we encounter a new suite, we push it to the stack + // when the "describe" function exits, we pop it + // Thus a test can look up the tags from its parent suites + const suiteStack = [] + + describe = function describeGrep (name, options, callback) { + if (typeof options === 'function') { + // the block has format describe('...', cb) + callback = options + options = {} + } + + const stackItem = { name } + + suiteStack.push(stackItem) + + if (!callback) { + // the pending suite by itself + const result = _describe(name, options) + + suiteStack.pop() + + return result + } + + let configTags = options && options.tags + + if (typeof configTags === 'string') { + configTags = [configTags] + } + + if (!configTags || !configTags.length) { + // if the describe suite does not have explicit tags + // move on, since the tests inside can have their own tags + _describe(name, options, callback) + suiteStack.pop() + + return + } + + // when looking at the suite of the tests, I found + // that using the name is quickly becoming very confusing + // and thus we need to use the explicit tags + stackItem.tags = configTags + _describe(name, options, callback) + suiteStack.pop() + + return + } + + // overwrite "context" which is an alias to "describe" + context = describe + + // overwrite "specify" which is an alias to "it" + specify = it + + // keep the ".skip", ".only" methods the same as before + it.skip = _it.skip + it.only = _it.only + // preserve "it.each" method if found + // https://github.com/cypress-io/cypress-grep/issues/72 + if (typeof _it.each === 'function') { + it.each = _it.each + } + + describe.skip = _describe.skip + describe.only = _describe.only + if (typeof _describe.each === 'function') { + describe.each = _describe.each + } +} + +function restartTests () { + setTimeout(() => { + window.top.document.querySelector('.reporter .restart').click() + }, 0) +} + +if (!Cypress.grep) { + /** + * A utility method to set the grep and run the tests from + * the DevTools console. Restarts the test runner + * @example + * // run only the tests with "hello w" in the title + * Cypress.grep('hello w') + * // runs only tests tagged both "@smoke" and "@fast" + * Cypress.grep(null, '@smoke+@fast') + * // runs the grepped tests 100 times + * Cypress.grep('add items', null, 100) + * // remove all current grep settings + * // and run all tests + * Cypress.grep() + * @see "Grep from DevTools console" https://github.com/cypress-io/cypress/tree/develop/npm/grep#devtools-console + */ + Cypress.grep = function grep (grep, tags, burn) { + Cypress.env('grep', grep) + Cypress.env('grepTags', tags) + Cypress.env('grepBurn', burn) + // remove any aliased values + Cypress.env('grep-tags', null) + Cypress.env('grep-burn', null) + Cypress.env('burn', null) + + debug('set new grep to "%o" restarting tests', { grep, tags, burn }) + restartTests() + } +} + +module.exports = cypressGrep diff --git a/npm/grep/src/utils.js b/npm/grep/src/utils.js new file mode 100644 index 00000000000..9ba2dc544cc --- /dev/null +++ b/npm/grep/src/utils.js @@ -0,0 +1,187 @@ +// @ts-check + +// Universal code - should run in Node or in the browser + +/** + * Parses test title grep string. + * The string can have "-" in front of it to invert the match. + * @param {string} s Input substring of the test title + */ +function parseTitleGrep (s) { + if (!s || typeof s !== 'string') { + return null + } + + s = s.trim() + if (s.startsWith('-')) { + return { + title: s.substring(1), + invert: true, + } + } + + return { + title: s, + invert: false, + } +} + +function parseFullTitleGrep (s) { + if (!s || typeof s !== 'string') { + return [] + } + + // separate each title + return s.split(';').map(parseTitleGrep) +} + +/** + * Parses tags to grep for. + * @param {string} s Tags string like "@tag1+@tag2" + */ +function parseTagsGrep (s) { + if (!s) { + return [] + } + + const explicitNotTags = [] + + // top level split - using space or comma, each part is OR + const ORS = s + .split(/[ ,]/) + // remove any empty tags + .filter(Boolean) + .map((part) => { + // now every part is an AND + if (part.startsWith('--')) { + explicitNotTags.push({ + tag: part.slice(2), + invert: true, + }) + + return + } + + const parsed = part.split('+').map((tag) => { + if (tag.startsWith('-')) { + return { + tag: tag.slice(1), + invert: true, + } + } + + return { + tag, + invert: false, + } + }) + + return parsed + }) + + // filter out undefined from explicit not tags + const ORS_filtered = ORS.filter((x) => x !== undefined) + + if (explicitNotTags.length > 0) { + ORS_filtered.forEach((OR, index) => { + ORS_filtered[index] = OR.concat(explicitNotTags) + }) + + if (ORS_filtered.length === 0) { + ORS_filtered[ 0 ] = explicitNotTags + } + } + + return ORS_filtered +} + +function shouldTestRunTags (parsedGrepTags, tags = []) { + if (!parsedGrepTags.length) { + // there are no parsed tags to search for, the test should run + return true + } + + // now the test has tags and the parsed tags are present + + // top levels are OR + const onePartMatched = parsedGrepTags.some((orPart) => { + const everyAndPartMatched = orPart.every((p) => { + if (p.invert) { + return !tags.includes(p.tag) + } + + return tags.includes(p.tag) + }) + // console.log('every part matched %o?', orPart, everyAndPartMatched) + + return everyAndPartMatched + }) + + // console.log('onePartMatched', onePartMatched) + return onePartMatched +} + +function shouldTestRunTitle (parsedGrep, testName) { + if (!testName) { + // if there is no title, let it run + return true + } + + if (!parsedGrep) { + return true + } + + if (!Array.isArray(parsedGrep)) { + console.error('Invalid parsed title grep') + console.error(parsedGrep) + throw new Error('Expected title grep to be an array') + } + + if (!parsedGrep.length) { + return true + } + + const inverted = parsedGrep.filter((g) => g.invert) + const straight = parsedGrep.filter((g) => !g.invert) + + return ( + inverted.every((titleGrep) => !testName.includes(titleGrep.title)) && + (!straight.length || + straight.some((titleGrep) => testName.includes(titleGrep.title))) + ) +} + +// note: tags take precedence over the test name +function shouldTestRun (parsedGrep, testName, tags = [], grepUntagged = false) { + if (grepUntagged) { + return !tags.length + } + + if (Array.isArray(testName)) { + // the caller passed tags only, no test name + tags = testName + testName = undefined + } + + return ( + shouldTestRunTitle(parsedGrep.title, testName) && + shouldTestRunTags(parsedGrep.tags, tags) + ) +} + +function parseGrep (titlePart, tags) { + return { + title: parseFullTitleGrep(titlePart), + tags: parseTagsGrep(tags), + } +} + +module.exports = { + parseGrep, + parseTitleGrep, + parseFullTitleGrep, + parseTagsGrep, + shouldTestRun, + shouldTestRunTags, + shouldTestRunTitle, +} diff --git a/npm/mount-utils/.eslintignore b/npm/mount-utils/.eslintignore new file mode 100644 index 00000000000..79afe972da7 --- /dev/null +++ b/npm/mount-utils/.eslintignore @@ -0,0 +1,5 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json +**/cypress/fixtures \ No newline at end of file diff --git a/npm/mount-utils/CHANGELOG.md b/npm/mount-utils/CHANGELOG.md index 4dab067c7bb..72350d2ab03 100644 --- a/npm/mount-utils/CHANGELOG.md +++ b/npm/mount-utils/CHANGELOG.md @@ -1,3 +1,43 @@ +# [@cypress/mount-utils-v4.1.1](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v4.1.0...@cypress/mount-utils-v4.1.1) (2024-06-07) + + +### Bug Fixes + +* update cypress to Typescript 5 ([#29568](https://github.com/cypress-io/cypress/issues/29568)) ([f3b6766](https://github.com/cypress-io/cypress/commit/f3b67666a5db0438594339c379cf27e1fd1e4abc)) + +# [@cypress/mount-utils-v4.1.0](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v4.0.0...@cypress/mount-utils-v4.1.0) (2024-03-12) + + +### Features + +* supported type of vue@2.7+ ([#28818](https://github.com/cypress-io/cypress/issues/28818)) ([854a649](https://github.com/cypress-io/cypress/commit/854a6497be2315881b8ad9c92674d3c29a76d581)) + +# [@cypress/mount-utils-v4.0.0](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v3.0.0...@cypress/mount-utils-v4.0.0) (2022-12-02) + + +### chore + +* remove experimentalSessionAndOrigin flag ([#24340](https://github.com/cypress-io/cypress/issues/24340)) ([69873ae](https://github.com/cypress-io/cypress/commit/69873ae9884228f15310fd151e42cbc0cb712817)) + + +### BREAKING CHANGES + +* removed experimentalSessionAndOrigin flag. testIsolation defaults to strict + +# [@cypress/mount-utils-v3.0.0](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v2.1.0...@cypress/mount-utils-v3.0.0) (2022-11-07) + + +### Bug Fixes + +* remove dependence on @cypress/ types ([#24415](https://github.com/cypress-io/cypress/issues/24415)) ([58e0ab9](https://github.com/cypress-io/cypress/commit/58e0ab91604618ea6f75932622f7e66e419270e6)) +* remove last mounted component upon subsequent mount calls ([#24470](https://github.com/cypress-io/cypress/issues/24470)) ([f39eb1c](https://github.com/cypress-io/cypress/commit/f39eb1c19e0923bda7ae263168fc6448da942d54)) +* remove some CT functions and props ([#24419](https://github.com/cypress-io/cypress/issues/24419)) ([294985f](https://github.com/cypress-io/cypress/commit/294985f8b3e0fa00ed66d25f88c8814603766074)) + + +### BREAKING CHANGES + +* remove last mounted component upon subsequent mount calls of mount + # [@cypress/mount-utils-v2.1.0](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v2.0.1...@cypress/mount-utils-v2.1.0) (2022-08-30) diff --git a/npm/mount-utils/README.md b/npm/mount-utils/README.md index e6c03a504fe..6ca97dc425f 100644 --- a/npm/mount-utils/README.md +++ b/npm/mount-utils/README.md @@ -2,12 +2,131 @@ > **Note** this package is not meant to be used outside of cypress component testing. -This librares exports some shared types and utility functions designed to build adapters for components frameworks. +This library exports some shared types and utility functions designed to build adapters for components frameworks. It is used in: - [`@cypress/react`](https://github.com/cypress-io/cypress/tree/develop/npm/react) - [`@cypress/vue`](https://github.com/cypress-io/cypress/tree/develop/npm/vue) +- [`@cypress/svelte`](https://github.com/cypress-io/cypress/tree/develop/npm/svelte) +- [`@cypress/angular`](https://github.com/cypress-io/cypress/tree/develop/npm/angular) + +## What is a Mount Adapter? + +All Component Tests require a component to be **mounted**. This is generally done with a custom command, `cy.mount` by default. + + +### Requirements + +All the functionality used to create the first party Mount adapters is available to support third parties adapters. At minimum, a Mount Adapter must: + +- Receive a component as the first argument. This could be class, function etc - depends on the framework. +- Return a Cypress Chainable (for example using `cy.wrap`) that resolves whatever is idiomatic for your framework +- Call `getContainerEl` to access the root DOM element + +In addition, we recommend that Mount Adapters: + +- call `setupHooks` to register the required lifecycle hooks for `@cypress/mount-utils` to work + +### Example Mount Adapter: Web Components + +Here's a simple yet realistic example of Mount Adapter targeting Web Components. It uses utilities from this package (`@cypress/mount-utils`) This is recommended, so your adapter behaves similar to the first party Mount Adapters. + +```ts +import { + ROOT_SELECTOR, + setupHooks, + getContainerEl +} from "@cypress/mount-utils"; + +Cypress.on("run:start", () => { + // Consider doing a check to ensure your adapter only runs in Component Testing mode. + if (Cypress.testingType !== "component") { + return; + } + + Cypress.on("test:before:run", () => { + // Do some cleanup from previous test - for example, clear the DOM. + getContainerEl().innerHTML = ""; + }); +}); + +function maybeRegisterComponent( + name: string, + webComponent: T +) { + // Avoid double-registering a Web Component. + if (window.customElements.get(name)) { + return; + } + + window.customElements.define(name, webComponent); +} + +export function mount( + webComponent: CustomElementConstructor +): Cypress.Chainable { + // Get root selector defined in `cypress/support.component-index.html + const $root = document.querySelector(ROOT_SELECTOR)!; + + // Change to kebab-case to comply with Web Component naming convention + const name = webComponent.name + .replace(/([a-z0–9])([A-Z])/g, "$1-$2") + .toLowerCase(); + + /// Register Web Component + maybeRegisterComponent(name, webComponent); + + // Render HTML containing component. + $root.innerHTML = `<${name} id="root">`; + + // Log a messsage in the Command Log. + Cypress.log({ + name: "mount", + message: [`<${name} ... />`], + }); + + // Return a `Cypress.Chainable` that returns whatever is idiomatic + // in the framework your mount adapter targets. + return cy.wrap(document.querySelector("#root"), { log: false }); +} + +// Setup Cypress lifecycle hooks. +setupHooks(); +``` + +Usage: + +```ts +// User will generally register a `cy.mount` command in `cypress/support/component.js`: + +import { mount } from '@package/cypress-web-components' + +Cypress.Commands.add('mount', mount) + +// Test +export class WebCounter extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.innerHTML = ` +
+ +
`; + } +} + + +describe('web-component.cy.ts', () => { + it('playground', () => { + cy.mount(WebCounter) + }) +}) +``` + +For more robust, production ready examples, check out our first party adapters. ## Compatibility diff --git a/npm/mount-utils/create-rollup-entry.mjs b/npm/mount-utils/create-rollup-entry.mjs index 2db5e478126..05ba2b654ea 100644 --- a/npm/mount-utils/create-rollup-entry.mjs +++ b/npm/mount-utils/create-rollup-entry.mjs @@ -4,6 +4,7 @@ import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import _ from 'lodash' import { readFileSync } from 'fs' +import dts from 'rollup-plugin-dts' const pkg = JSON.parse(readFileSync('./package.json')) @@ -13,6 +14,7 @@ export function createEntries (options) { formats, input, config = {}, + dtsOptions = {}, } = options const banner = ` @@ -33,7 +35,7 @@ export function createEntries (options) { check: format === 'es', tsconfigOverride: { compilerOptions: { - declaration: format === 'es', + declaration: false, target: 'es6', module: format === 'cjs' ? 'es2015' : 'esnext', }, @@ -67,5 +69,19 @@ export function createEntries (options) { console.log(`Building ${format}: ${finalConfig.output.file}`) return finalConfig - }) + }).concat([{ + input, + output: [{ file: 'dist/index.d.ts', format: 'es' }], + plugins: [ + dts({ respectExternal: true, ...dtsOptions }), + { + name: 'cypress-types-reference', + // rollup-plugin-dts does not add '// ' like rollup-plugin-typescript2 did so we add it here. + renderChunk (...[code]) { + return `/// \n\n${code}` + }, + }, + ], + external: config.external || [], + }]) } diff --git a/npm/mount-utils/package.json b/npm/mount-utils/package.json index 04747ef6a3d..dbfa2a1f84c 100644 --- a/npm/mount-utils/package.json +++ b/npm/mount-utils/package.json @@ -6,17 +6,18 @@ "scripts": { "build": "tsc || echo 'built, with type errors'", "postbuild": "node ../../scripts/sync-exported-npm-with-cli.js", - "build-prod": "yarn build", "check-ts": "tsc --noEmit", + "lint": "eslint --ext .js,.ts,.json, .", "watch": "tsc -w" }, "dependencies": {}, "devDependencies": { "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-node-resolve": "^11.1.1", - "rollup": "^2.38.5", + "rollup": "3.7.3", + "rollup-plugin-dts": "5.0.0", "rollup-plugin-typescript2": "^0.29.0", - "typescript": "^4.7.4" + "typescript": "^5.4.5" }, "files": [ "dist" @@ -27,9 +28,19 @@ "type": "git", "url": "https://github.com/cypress-io/cypress.git" }, - "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/mount-utils#readme", + "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/mount-utils#readme", "bugs": "https://github.com/cypress-io/cypress/issues/new?template=1-bug-report.md", "publishConfig": { "access": "public" + }, + "nx": { + "targets": { + "build": { + "outputs": [ + "{workspaceRoot}/cli/mount-utils", + "{projectRoot}/dist" + ] + } + } } } diff --git a/npm/mount-utils/src/index.ts b/npm/mount-utils/src/index.ts index 5593110bd50..4d00f350716 100644 --- a/npm/mount-utils/src/index.ts +++ b/npm/mount-utils/src/index.ts @@ -1,44 +1,10 @@ -/** - * Additional styles to inject into the document. - * A component might need 3rd party libraries from CDN, - * local CSS files and custom styles. - */ -export interface StyleOptions { - /** - * Creates element for each stylesheet - * @alias stylesheet - */ - stylesheets: string | string[] - /** - * Creates element for each stylesheet - * @alias stylesheets - */ - stylesheet: string | string[] - /** - * Creates element and inserts given CSS. - * @alias styles - */ - style: string | string[] - /** - * Creates element for each given CSS text. - * @alias style - */ - styles: string | string[] - /** - * Loads each file and creates a element - * with the loaded CSS - * @alias cssFile - */ - cssFiles: string | string[] - /** - * Single CSS file to load into a element - * @alias cssFile - */ - cssFile: string | string[] -} - export const ROOT_SELECTOR = '[data-cy-root]' +/** + * Gets the root element used to mount the component. + * @returns {HTMLElement} The root element + * @throws {Error} If the root element is not found + */ export const getContainerEl = (): HTMLElement => { const el = document.querySelector(ROOT_SELECTOR) @@ -49,177 +15,85 @@ export const getContainerEl = (): HTMLElement => { throw Error(`No element found that matches selector ${ROOT_SELECTOR}. Please add a root element with data-cy-root attribute to your "component-index.html" file so that Cypress can attach your component to the DOM.`) } -/** - * Remove any style or extra link elements from the iframe placeholder - * left from any previous test - * - */ -export function cleanupStyles () { - const styles = document.body.querySelectorAll('[data-cy=injected-style-tag]') - - styles.forEach((styleElement) => { - if (styleElement.parentElement) { - styleElement.parentElement.removeChild(styleElement) +export function checkForRemovedStyleOptions (mountingOptions: Record) { + for (const key of ['cssFile', 'cssFiles', 'style', 'styles', 'stylesheet', 'stylesheets'] as const) { + if (mountingOptions[key]) { + Cypress.utils.throwErrByPath('mount.removed_style_mounting_options', key) } - }) - - const links = document.body.querySelectorAll('[data-cy=injected-stylesheet]') - - links.forEach((link) => { - if (link.parentElement) { - link.parentElement.removeChild(link) - } - }) + } } /** - * Insert links to external style resources. + * Utility function to register CT side effects and run cleanup code during the "test:before:run" Cypress hook + * @param optionalCallback Callback to be called before the next test runs */ -function insertStylesheets ( - stylesheets: string[], - document: Document, - el: HTMLElement | null, -) { - stylesheets.forEach((href) => { - const link = document.createElement('link') +export function setupHooks (optionalCallback?: Function) { + // We don't want CT side effects to run when e2e + // testing so we early return. + // System test to verify CT side effects do not pollute e2e: system-tests/test/e2e_with_mount_import_spec.ts + if (Cypress.testingType !== 'component') { + return + } - link.type = 'text/css' - link.rel = 'stylesheet' - link.href = href - link.dataset.cy = 'injected-stylesheet' - document.body.insertBefore(link, el) + // When running component specs, we cannot allow "cy.visit" + // because it will wipe out our preparation work, and does not make much sense + // thus we overwrite "cy.visit" to throw an error + Cypress.Commands.overwrite('visit', () => { + throw new Error( + 'cy.visit from a component spec is not allowed', + ) }) -} - -/** - * Inserts a single stylesheet element - */ -function insertStyles (styles: string[], document: Document, el: HTMLElement | null) { - styles.forEach((style) => { - const styleElement = document.createElement('style') - styleElement.dataset.cy = 'injected-style-tag' - styleElement.appendChild(document.createTextNode(style)) - document.body.insertBefore(styleElement, el) + Cypress.Commands.overwrite('session', () => { + throw new Error( + 'cy.session from a component spec is not allowed', + ) }) -} -function insertSingleCssFile ( - cssFilename: string, - document: Document, - el: HTMLElement | null, - log?: boolean, -) { - return cy.readFile(cssFilename, { log }).then((css) => { - const style = document.createElement('style') + Cypress.Commands.overwrite('origin', () => { + throw new Error( + 'cy.origin from a component spec is not allowed', + ) + }) - style.appendChild(document.createTextNode(css)) - document.body.insertBefore(style, el) + // @ts-ignore + Cypress.on('test:before:after:run:async', () => { + optionalCallback?.() }) } /** - * Reads the given CSS file from local file system - * and adds the loaded style text as an element. + * Remove any style or extra link elements from the iframe placeholder + * left from any previous test + * + * Removed as of Cypress 11.0.0 + * @see https://on.cypress.io/migration-11-0-0-component-testing-updates */ -function insertLocalCssFiles ( - cssFilenames: string[], - document: Document, - el: HTMLElement | null, - log?: boolean, -) { - return Cypress.Promise.mapSeries(cssFilenames, (cssFilename) => { - return insertSingleCssFile(cssFilename, document, el, log) - }) +export function cleanupStyles () { + Cypress.utils.throwErrByPath('mount.cleanup_styles') } +/** + * Additional styles to inject into the document. + * A component might need 3rd party libraries from CDN, + * local CSS files and custom styles. + * + * Removed as of Cypress 11.0.0. + * @see https://on.cypress.io/migration-11-0-0-component-testing-updates + */ +export type StyleOptions = unknown + /** * Injects custom style text or CSS file or 3rd party style resources * into the given document. + * + * Removed as of Cypress 11.0.0. + * @see https://on.cypress.io/migration-11-0-0-component-testing-updates */ export const injectStylesBeforeElement = ( options: Partial, document: Document, el: HTMLElement | null, -): HTMLElement => { - if (!el) return - - // first insert all stylesheets as Link elements - let stylesheets: string[] = [] - - if (typeof options.stylesheet === 'string') { - stylesheets.push(options.stylesheet) - } else if (Array.isArray(options.stylesheet)) { - stylesheets = stylesheets.concat(options.stylesheet) - } - - if (typeof options.stylesheets === 'string') { - options.stylesheets = [options.stylesheets] - } - - if (options.stylesheets) { - stylesheets = stylesheets.concat(options.stylesheets) - } - - insertStylesheets(stylesheets, document, el) - - // insert any styles as elements - let styles: string[] = [] - - if (typeof options.style === 'string') { - styles.push(options.style) - } else if (Array.isArray(options.style)) { - styles = styles.concat(options.style) - } - - if (typeof options.styles === 'string') { - styles.push(options.styles) - } else if (Array.isArray(options.styles)) { - styles = styles.concat(options.styles) - } - - insertStyles(styles, document, el) - - // now load any css files by path and add their content - // as elements - let cssFiles: string[] = [] - - if (typeof options.cssFile === 'string') { - cssFiles.push(options.cssFile) - } else if (Array.isArray(options.cssFile)) { - cssFiles = cssFiles.concat(options.cssFile) - } - - if (typeof options.cssFiles === 'string') { - cssFiles.push(options.cssFiles) - } else if (Array.isArray(options.cssFiles)) { - cssFiles = cssFiles.concat(options.cssFiles) - } - - return insertLocalCssFiles(cssFiles, document, el, options.log) -} - -export function setupHooks (optionalCallback?: Function) { - // Consumed by the framework "mount" libs. A user might register their own mount in the scaffolded 'commands.js' - // file that is imported by e2e and component support files by default. We don't want CT side effects to run when e2e - // testing so we early return. - // System test to verify CT side effects do not pollute e2e: system-tests/test/e2e_with_mount_import_spec.ts - if (Cypress.testingType !== 'component') { - return - } - - // When running component specs, we cannot allow "cy.visit" - // because it will wipe out our preparation work, and does not make much sense - // thus we overwrite "cy.visit" to throw an error - Cypress.Commands.overwrite('visit', () => { - throw new Error( - 'cy.visit from a component spec is not allowed', - ) - }) - - // @ts-ignore - Cypress.on('test:before:run', () => { - optionalCallback?.() - cleanupStyles() - }) +) => { + Cypress.utils.throwErrByPath('mount.inject_styles_before_element') } diff --git a/npm/puppeteer/.eslintignore b/npm/puppeteer/.eslintignore new file mode 100644 index 00000000000..9bd1cd76241 --- /dev/null +++ b/npm/puppeteer/.eslintignore @@ -0,0 +1,4 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json diff --git a/npm/create-cypress-tests/.mocharc.json b/npm/puppeteer/.mocharc.json similarity index 100% rename from npm/create-cypress-tests/.mocharc.json rename to npm/puppeteer/.mocharc.json diff --git a/npm/puppeteer/CHANGELOG.md b/npm/puppeteer/CHANGELOG.md new file mode 100644 index 00000000000..ed69fcb2323 --- /dev/null +++ b/npm/puppeteer/CHANGELOG.md @@ -0,0 +1,31 @@ +# [@cypress/puppeteer-v0.1.5](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.1.4...@cypress/puppeteer-v0.1.5) (2024-06-07) + + +### Bug Fixes + +* update cypress to Typescript 5 ([#29568](https://github.com/cypress-io/cypress/issues/29568)) ([f3b6766](https://github.com/cypress-io/cypress/commit/f3b67666a5db0438594339c379cf27e1fd1e4abc)) + +# [@cypress/puppeteer-v0.1.4](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.1.3...@cypress/puppeteer-v0.1.4) (2024-04-02) + +# [@cypress/puppeteer-v0.1.3](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.1.2...@cypress/puppeteer-v0.1.3) (2024-02-20) + + +### Bug Fixes + +* move main tab activation to puppeteer plugin ([#28898](https://github.com/cypress-io/cypress/issues/28898)) ([ed2fc13](https://github.com/cypress-io/cypress/commit/ed2fc1394623f08097d180747712c557d867ee86)) + +# [@cypress/puppeteer-v0.1.2](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.1.1...@cypress/puppeteer-v0.1.2) (2023-12-26) + +# [@cypress/puppeteer-v0.1.1](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.1.0...@cypress/puppeteer-v0.1.1) (2023-11-29) + + +### Bug Fixes + +* Resolve types and dist issues with @cypress/puppeteer ([#28424](https://github.com/cypress-io/cypress/issues/28424)) ([72225db](https://github.com/cypress-io/cypress/commit/72225db03327744844dcfbcc72b40e85de6a2761)) + +# [@cypress/puppeteer-v0.1.0](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.0.1...@cypress/puppeteer-v0.1.0) (2023-11-28) + + +### Features + +* Initial release ([#28370](https://github.com/cypress-io/cypress/issues/28370)) ([b34d145](https://github.com/cypress-io/cypress/commit/b34d14571689a9b36efc707a3a48f27edcb98113)) diff --git a/npm/puppeteer/README.md b/npm/puppeteer/README.md new file mode 100644 index 00000000000..31063f2ae4e --- /dev/null +++ b/npm/puppeteer/README.md @@ -0,0 +1,383 @@ +# @cypress/puppeteer [beta] + +Utilize [Puppeteer's browser API](https://pptr.dev/api) within Cypress with a single command. + +> This plugin is in public beta, so we'd love to get your feedback to improve it. Please leave any feedback you have in [this discussion](https://github.com/cypress-io/cypress/discussions/28410). + +# Table of Contents + +- [Installation](#installation) +- [Compatibility](#compatibility) +- [Usage](#usage) +- [API](#api) +- [Examples](#examples) +- [Contributing](#contributing) +- [Changelog](./CHANGELOG.md) + +# Installation + +## npm + +```sh +npm install --save-dev @cypress/puppeteer +``` + +## yarn + +```sh +yarn add --dev @cypress/puppeteer +``` + +## With TypeScript + +Add the following in `tsconfig.json`: + +```json +{ + "compilerOptions": { + "types": ["cypress", "@cypress/puppeteer/support"] + } +} +``` + +## Compatibility + +`@cypress/puppeteer` requires Cypress version 13.6.0 or greater. + +Only Chromium-based browsers (e.g. Chrome, Chromium, Electron) are supported. + +## Usage + +`@cypress/puppeteer` is set up in your Cypress config and support file, then executed in your spec. See [API](#api) and [Examples](#examples) below for more details. + +While the `cy.puppeteer()` command is executed in the browser, the majority of the Puppeteer execution is run in the Node process via your Cypress config. You pass a string message name to `cy.puppeteer()` that indicates which message handler to execute in the Cypress config. This is similar to how [cy.task()](on.cypress.io/task) operates. + +In your Cypress config (e.g. `cypress.config.ts`): + +```typescript +import { setup } from '@cypress/puppeteer' + +export default defineConfig({ + e2e: { + setupNodeEvents (on) { + setup({ + on, + onMessage: { + async myMessageHander (browser) { + // Utilize the Puppeteer browser instance and the Puppeteer API to interact with and automate the browser + }, + }, + }) + }, + }, +} +``` + +In your support file (e.g. `cypress/support/e2e.ts`): + +```typescript +import '@cypress/puppeteer/support' +``` + +In your spec (e.g. `spec.cy.ts`): + +```typescript + it('switches to and tests a new tab', () => { + cy.visit('/') + cy.get('button').click() // opens a new tab + + cy + .puppeteer('myMessageHander') + .should('equal', 'You said: Hello from Page 1') + }) +``` + +## API + +### Cypress Config - setup + +This sets up `@cypress/puppeteer` message handlers that run Puppeteer browser automation. + +```typescript +setup(options) +``` + +#### Options + +- `on` _required_: The `on` event registration function provided by `setupNodeEvents` +- `onMessage` _required_: An object with string keys and function values (see more details [below](#onmessage)) +- `puppeteer` _optional_: The `puppeteer` library imported from `puppeteer-core`, overriding the default version of `puppeteer-core` used by this plugin + +##### onMessage + +The keys provided in this are used to invoke their corresponding functions by calling `cy.puppeteer(key)` in your Cypress test. + +The functions should contain Puppeteer code for automating the browser. The code is executed within Node.js and not within the browser, so Cypress commands and DOM APIs cannot be utilized. + +The functions receive the following arguments: + +###### browser + +A [puppeteer browser instance](https://pptr.dev/api/puppeteer.browser) connected to the Cypress-launched browser. + +###### ...args + +The rest of the arguments are any de-serialized arguments passed to the `cy.puppeteer()` command from your Cypress test. + +### Cypress Config - retry + +This is a utility function provided to aid in retrying actions that may initially fail. + +```typescript +retry(functionToRetry[, options]) +``` + +#### functionToRetry + +_required_ + +A function that will run and retry if an error is thrown. If an error is not thrown, `retry` will return the value returned by this function. + +The function will continue to run at the default or configured interval until the default or configured timeout, at which point `retry` will throw an error and cease retrying this function. + +#### Options + +_optional_ + +- `timeout` _optional_: The total time in milliseconds during which to attempt retrying the function. Default: `4000ms` +- `delayBetweenTries` _optional_: The time to wait between retries. Default: `200ms` + +### Cypress Spec - cy.puppeteer() + +```typescript +cy.puppeteer(messageName[, ...args]) +``` + +#### messageName + +_required_ + +A string matching one of the keys passed to the `onMessage` option of `setup` in your Cypress config. + +#### ...args + +_optional_ + +Values that will be passed to the message handler. These values must be JSON-serializable. + +Example: + +```typescript +// spec +cy.puppeteer('testNewTab', 'value 1', 42, [true, false]) + +// Cypress config +setup({ + on, + onMessage: { + testNewTab (browser, stringArg, numberArg, arrayOfBooleans) { + // stringArg === 'value 1' + // numberArg === 42 + // arrayOfBooleans[0] === true / arrayOfBooleans[1] === false + } + } +}) +``` + +## Examples + +These examples can be found and run in the [Cypress tests of this package](./cypress) with this project's [cypress.config.ts](./cypress.config.ts). + +While these examples use tabs, they could just as easily apply to windows. Tabs and windows are essentially the same things as far as Puppeteer is concerned and encapsulated by instances of the [Page class](https://pptr.dev/api/puppeteer.page/). + +### Switching to a new tab + +This example demonstrates the following: + +- Switching to a tab opened by an action in the Cypress test +- Getting the page instance via Puppeteer utilizing the `retry` function +- Getting page references and content via puppeteer +- Passing that content back to be asserted on in Cypress + +_spec.cy.ts_ + +```typescript +it('switches to a new tab', () => { + cy.visit('/cypress/fixtures/page-1.html') + cy.get('input').type('Hello from Page 1') + cy.get('button').click() // Triggers a new tab to open + + cy + .puppeteer('switchToTabAndGetContent') + .should('equal', 'You said: Hello from Page 1') +}) +``` + +_cypress.config.ts_ + +```typescript +import { defineConfig } from 'cypress' +import type { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' + +import { setup, retry } from '@cypress/puppeteer' + +export default defineConfig({ + e2e: { + setupNodeEvents (on) { + setup({ + on, + onMessage: { + async switchToTabAndGetContent (browser: PuppeteerBrowser) { + // In this message handler, we utilize the Puppeteer API to interact with the browser and the new tab that our Cypress tests has opened + + // Utilize the retry since the page may not have opened and loaded by the time this runs + const page = await retry>(async () => { + // The browser will (eventually) have 2 tabs open: the Cypress tab and the newly opened tab + // In Puppeteer, tabs and windows are called pages + const pages = await browser.pages() + // Try to find the page we want to interact with + const page = pages.find((page) => page.url().includes('page-2.html')) + + // If we can't find the page, it probably hasn't loaded yet, so throw an error to signal that this function should retry + if (!page) throw new Error('Could not find page') + + // Otherwise, return the page instance and it will be returned by the `retry` function itself + return page + }) + + // Cypress will maintain focus on the Cypress tab within the browser. It's generally a good idea to bring the page to the front to interact with it. + await page.bringToFront() + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + }, + }) + }, + }, +}) +``` + +### Creating a new tab + +This example demonstrates the following: + +- Passing a non-default version of puppeteer to `@cypress/puppeteer` +- Passing arguments from `cy.puppeteer()` to the message handler +- Creating a new tab and visiting a page via Puppeteer +- Getting page references and content via puppeteer +- Passing that content back to be asserted on in Cypress + +_spec.cy.ts_ + +```typescript +it('creates a new tab', () => { + cy.visit('/cypress/fixtures/page-3.html') + // We get a dynamic value from the page and pass it through to the puppeteer + // message handler + cy.get('#message').invoke('text').then((message) => { + cy.puppeteer('createTabAndGetContent', message) + .should('equal', 'I approve this message: Cypress and Puppeteer make a great combo') + }) +}) +``` + +_cypress.config.ts_ + +```typescript +import { defineConfig } from 'cypress' +import puppeteer, { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' + +import { setup, retry } from '@cypress/puppeteer' + +export default defineConfig({ + e2e: { + setupNodeEvents (on) { + setup({ + on, + // Pass in your own version of puppeteer to be used instead of the default one + puppeteer, + onMessage: { + async createTabAndGetContent (browser: PuppeteerBrowser, text: string) { + // In this message handler, we utilize the Puppeteer API to interact with the browser, creating a new tab and getting its content + + // This will create a new tab within the Cypress-launched browser + const page = await browser.newPage() + + // Text comes from the test invocation of `cy.puppeteer()` + await page.goto(`http://localhost:8000/cypress/fixtures/page-4.html?text=${text}`) + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + }, + }) + }, + }, +}) +``` + +## Troubleshooting + +### Error: Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin. + +If you receive this error in your command log, the Puppeteer plugin was unable to communicate with the Cypress extension. This extension is necessary in order to re-activate the main Cypress tab after a Puppeteer command, when running in open mode. + +* Ensure this extension is enabled in the instance of Chrome that Cypress launches by visiting chrome://extensions/ +* Ensure the Cypress extension is allowed by your company's security policy by its extension id, `caljajdfkjjjdehjdoimjkkakekklcck` + +## Contributing + +Build the TypeScript files: + +```shell +yarn build +``` + +Watch the TypeScript files and rebuild on file change: + +```shell +yarn watch +``` + +Open Cypress tests: + +```shell +yarn cypress:open +``` + +Run Cypress tests once: + +```shell +yarn cypress:run +``` + +Run all unit tests once: + +```shell +yarn test +``` + +Run unit tests in watch mode: + +```shell +yarn test-watch +``` + +## [Changelog](./CHANGELOG.md) diff --git a/npm/puppeteer/cypress.config.ts b/npm/puppeteer/cypress.config.ts new file mode 100644 index 00000000000..e0eda55b48f --- /dev/null +++ b/npm/puppeteer/cypress.config.ts @@ -0,0 +1,81 @@ +import { defineConfig } from 'cypress' +import type { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' +import path from 'path' +import express from 'express' + +import { setup, retry } from './src/plugin' + +const serverPort = 8000 + +export default defineConfig({ + e2e: { + baseUrl: `http://localhost:${serverPort}`, + setupNodeEvents (on) { + setup({ + on, + onMessage: { + async switchToTabAndGetContent (browser: PuppeteerBrowser) { + // Utilize the retry since the page may not have opened and loaded by the time this runs + const page = await retry>(async () => { + const pages = await browser.pages() + const page = pages.find((page) => page.url().includes('page-2.html')) + + // If we haven't found the page, throw an error to signal that it should retry + if (!page) throw new Error('Could not find page matching `page-2.html`') + + // Otherwise, return the page instance and it will be returned by the `retry` function itself + return page + }) + + // Cypress will maintain focus on the Cypress tab within the browser. It's generally a good idea to bring the page to the front to interact with it. + await page.bringToFront() + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + + async createTabAndGetContent (browser: PuppeteerBrowser, text: string) { + const page = await browser.newPage() + + // Text comes from the test invocation of `cy.puppeteer()` + await page.goto(`http://localhost:${serverPort}/cypress/fixtures/page-4.html?text=${text}`) + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + }, + }) + + return new Promise((resolve) => { + const app = express() + + app.set('port', serverPort) + app.set('view engine', 'html') + app.use(express.static(path.join(__dirname))) + + app.listen(serverPort, () => { + // eslint-disable-next-line no-console + console.log(`Express server listening on http://localhost:${serverPort}`) + + resolve() + }) + }) + }, + }, +}) diff --git a/npm/puppeteer/cypress/e2e/multi-tab.cy.ts b/npm/puppeteer/cypress/e2e/multi-tab.cy.ts new file mode 100644 index 00000000000..aceb1de32c2 --- /dev/null +++ b/npm/puppeteer/cypress/e2e/multi-tab.cy.ts @@ -0,0 +1,22 @@ +describe('multi-tab testing', () => { + it('switches to a new tab', () => { + cy.visit('/cypress/fixtures/page-1.html') + cy.get('input').type('Hello from Page 1') + cy.get('button').click() // Triggers a new tab to open + + cy + .puppeteer('switchToTabAndGetContent') + .should('equal', 'You said: Hello from Page 1') + }) + + it('creates a new tab', () => { + cy.visit('/cypress/fixtures/page-3.html') + // We get a dynamic value from the page and pass it through to the puppeteer + // message handler + cy.get('#message').invoke('text').then((message) => { + cy + .puppeteer('createTabAndGetContent', message) + .should('equal', 'I approve this message: Cypress and Puppeteer make a great combo') + }) + }) +}) diff --git a/npm/puppeteer/cypress/fixtures/page-1.html b/npm/puppeteer/cypress/fixtures/page-1.html new file mode 100644 index 00000000000..0bdcc597cfd --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/page-1.html @@ -0,0 +1,24 @@ + + + + Page 1 + + + +
+

Page 1

+ + + + +
+ + + + diff --git a/npm/puppeteer/cypress/fixtures/page-2.html b/npm/puppeteer/cypress/fixtures/page-2.html new file mode 100644 index 00000000000..1a52b802f63 --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/page-2.html @@ -0,0 +1,20 @@ + + + + Page 2 + + + +
+

Page 2

+

You said:

+
+ + + + diff --git a/npm/puppeteer/cypress/fixtures/page-3.html b/npm/puppeteer/cypress/fixtures/page-3.html new file mode 100644 index 00000000000..b91ef7c9200 --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/page-3.html @@ -0,0 +1,14 @@ + + + + Page 3 + + + +
+

Page 3

+ +

The message: Cypress and Puppeteer make a great combo

+
+ + diff --git a/npm/puppeteer/cypress/fixtures/page-4.html b/npm/puppeteer/cypress/fixtures/page-4.html new file mode 100644 index 00000000000..1cee09c2932 --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/page-4.html @@ -0,0 +1,20 @@ + + + + Page 4 + + + +
+

Page 4

+

I approve this message:

+
+ + + + diff --git a/npm/puppeteer/cypress/fixtures/styles.css b/npm/puppeteer/cypress/fixtures/styles.css new file mode 100644 index 00000000000..4dbd6b88084 --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/styles.css @@ -0,0 +1,55 @@ +* { + box-sizing: border-box; +} + +html, +body { + background: #e9e9e9; + color: #333; + font-family: sans-serif; + font-size: 18px; + height: 100%; +} + +main { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + margin: 0 auto; + width: 800px; +} + +p { + font-size: 1.4em; + line-height: 1.5 +} + +a, +button { + background: #0a595e; + border: none; + border-radius: 2em; + color: white; + display: block; + font-size: 1em; + text-align: center; + margin: 1em 0; + padding: 1em 2em; + text-decoration: none; + width: 100%; +} + +code { + font-family: monospace; + color: #b2cdcd; +} + +input { + border: solid 1px #c8c8c8; + border-radius: 0.5em; + font-size: 1em; + padding: 1em 1.5em; + width: 100%; +} diff --git a/npm/puppeteer/cypress/support/e2e.ts b/npm/puppeteer/cypress/support/e2e.ts new file mode 100644 index 00000000000..8a7842cd60c --- /dev/null +++ b/npm/puppeteer/cypress/support/e2e.ts @@ -0,0 +1 @@ +import '../../src/support' diff --git a/npm/puppeteer/cypress/tsconfig.json b/npm/puppeteer/cypress/tsconfig.json new file mode 100644 index 00000000000..ea3daa2c318 --- /dev/null +++ b/npm/puppeteer/cypress/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": [ + "cypress" + ] + }, + "include": [ + "../index.d.ts", + "e2e/**/*.ts" + ] +} diff --git a/npm/puppeteer/package.json b/npm/puppeteer/package.json new file mode 100644 index 00000000000..7b78ff56466 --- /dev/null +++ b/npm/puppeteer/package.json @@ -0,0 +1,54 @@ +{ + "name": "@cypress/puppeteer", + "version": "0.0.0-development", + "description": "Plugin for enhancing Cypress tests with Puppeteer", + "private": false, + "main": "dist/plugin/index.js", + "scripts": { + "build": "rimraf dist && tsc || echo 'built, with errors'", + "build-prod": "yarn build", + "check-ts": "tsc --noEmit", + "cypress:open": "node ../../scripts/cypress.js open", + "cypress:run": "node ../../scripts/cypress.js run --browser chrome", + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .", + "semantic-release": "semantic-release", + "test": "mocha test/unit/*.spec.ts", + "test-watch": "yarn test & chokidar '**/*.ts' 'test/unit/*.ts' -c 'yarn test'", + "watch": "rimraf dist && tsc --watch" + }, + "dependencies": { + "lodash": "^4.17.21", + "puppeteer-core": "^21.2.1" + }, + "devDependencies": { + "@types/node": "^18.17.5", + "chai-as-promised": "^7.1.1", + "chokidar": "^3.5.3", + "express": "4.19.2", + "mocha": "^9.2.2", + "rimraf": "^5.0.1", + "semantic-release": "19.0.3", + "sinon": "^13.0.1", + "sinon-chai": "^3.7.0", + "ts-node": "^10.9.2", + "typescript": "5.4.5" + }, + "peerDependencies": { + "cypress": ">=13.6.0" + }, + "files": [ + "dist", + "support" + ], + "types": "dist/plugin/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cypress-io/cypress.git" + }, + "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/puppeteer/#readme", + "bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fpuppeteer&template=1-bug-report.md&title=", + "publishConfig": { + "access": "public" + } +} diff --git a/npm/puppeteer/src/plugin/activateMainTab.ts b/npm/puppeteer/src/plugin/activateMainTab.ts new file mode 100644 index 00000000000..8d435b6fab2 --- /dev/null +++ b/npm/puppeteer/src/plugin/activateMainTab.ts @@ -0,0 +1,46 @@ +/// +import type { Browser } from 'puppeteer-core' + +export const ACTIVATION_TIMEOUT = 2000 + +const sendActivationMessage = (activationTimeout: number) => { + // don't need to worry about tabs for Cy in Cy tests + if (document.defaultView !== top) { + return + } + + let timeout: NodeJS.Timeout + let onMessage: (ev: MessageEvent) => void + + // promise must resolve with a value for chai as promised to test resolution + return new Promise((resolve, reject) => { + onMessage = (ev) => { + if (ev.data.message === 'cypress:extension:main:tab:activated') { + window.removeEventListener('message', onMessage) + clearTimeout(timeout) + resolve() + } + } + + window.addEventListener('message', onMessage) + window.postMessage({ message: 'cypress:extension:activate:main:tab' }) + + timeout = setTimeout(() => { + window.removeEventListener('message', onMessage) + reject() + }, activationTimeout) + }) +} + +export const activateMainTab = async (browser: Browser) => { + // - Only implemented for Chromium right now. Support for Firefox/webkit + // could be added later + // - Electron doesn't have tabs + // - Focus doesn't matter for headless browsers and old headless Chrome + // doesn't run the extension + const [page] = await browser.pages() + + if (page) { + return page.evaluate(sendActivationMessage, ACTIVATION_TIMEOUT) + } +} diff --git a/npm/puppeteer/src/plugin/index.ts b/npm/puppeteer/src/plugin/index.ts new file mode 100644 index 00000000000..d880e3c57a3 --- /dev/null +++ b/npm/puppeteer/src/plugin/index.ts @@ -0,0 +1,3 @@ +export { setup } from './setup' + +export { retry } from './retry' diff --git a/npm/puppeteer/src/plugin/retry.ts b/npm/puppeteer/src/plugin/retry.ts new file mode 100644 index 00000000000..3ee7bd3cdc2 --- /dev/null +++ b/npm/puppeteer/src/plugin/retry.ts @@ -0,0 +1,28 @@ +import { pluginError } from './util' + +function delay (time: number) { + return new Promise((resolve) => { + setTimeout(() => resolve(), time) + }) +} + +export async function retry (functionToRetry: () => T, options?: { timeout?: number, delayBetweenTries?: number }): Promise { + const timeout = options?.timeout !== undefined ? options?.timeout : 4000 + const delayBetweenTries = options?.delayBetweenTries !== undefined ? options?.delayBetweenTries : 200 + + const makeAttempt = async (timeElapsed = 0): Promise => { + try { + return await functionToRetry() + } catch (err: any) { + await delay(delayBetweenTries) + + if (timeElapsed >= timeout) { + throw pluginError(`Failed retrying after ${timeout}ms: ${err.message}`) + } + + return makeAttempt(timeElapsed + delayBetweenTries) + } + } + + return makeAttempt() +} diff --git a/npm/puppeteer/src/plugin/setup.ts b/npm/puppeteer/src/plugin/setup.ts new file mode 100644 index 00000000000..3f0d1f51337 --- /dev/null +++ b/npm/puppeteer/src/plugin/setup.ts @@ -0,0 +1,140 @@ +import isPlainObject from 'lodash/isPlainObject' +import defaultPuppeteer, { Browser, PuppeteerNode } from 'puppeteer-core' +import { pluginError } from './util' +import { activateMainTab } from './activateMainTab' + +export type MessageHandler = (browser: Browser, ...args: any[]) => any | Promise + +interface SetupOptions { + onMessage: Record + on: Cypress.PluginEvents + puppeteer?: PuppeteerNode +} + +function messageHandlerError (err: any) { + const errObject = {} as any + + if (typeof err === 'string') { + errObject.message = err + } else if (typeof err === 'object') { + Object.assign(errObject, { + name: err.name, + message: err.message, + stack: err.stack, + }) + } else { + errObject.message = err + } + + return { + __error__: errObject, + } +} + +export function setup (options: SetupOptions) { + if (!options) { + throw pluginError('Must provide options argument to `setup`.') + } + + if (!isPlainObject(options)) { + throw pluginError('The options argument provided to `setup` must be an object.') + } + + if (!options.on) { + throw pluginError('Must provide `on` function to `setup`.') + } + + if (typeof options.on !== 'function') { + throw pluginError('The `on` option provided to `setup` must be a function.') + } + + if (!options.onMessage) { + throw pluginError('Must provide `onMessage` object to `setup`.') + } + + if (!isPlainObject(options.onMessage)) { + throw pluginError('The `onMessage` option provided to `setup` must be an object.') + } + + const puppeteer = options.puppeteer || defaultPuppeteer + + let cypressBrowser: Cypress.Browser + let debuggerUrl: string + + try { + options.on('after:browser:launch', (browser: Cypress.Browser, options: Cypress.AfterBrowserLaunchDetails) => { + cypressBrowser = browser + debuggerUrl = options.webSocketDebuggerUrl + }) + } catch (err: any) { + throw pluginError(`Could not set up \`after:browser:launch\` task. Ensure you are running Cypress >= 13.6.0. The following error was encountered:\n\n${err.stack}`) + } + + options.on('task', { + async __cypressPuppeteer__ ({ name, args }: { name: string, args: any[] }) { + if (!cypressBrowser) { + return messageHandlerError(pluginError(`Lost the reference to the browser. This usually occurs because the Cypress config was reloaded without the browser re-launching. Close and re-open the browser.`)) + } + + if (cypressBrowser.family !== 'chromium') { + return messageHandlerError(pluginError(`Only browsers in the "Chromium" family are supported. You are currently running a browser with the family: ${cypressBrowser.family}`)) + } + + const messageHandler = options.onMessage[name] + + if (!messageHandler) { + return messageHandlerError(pluginError(`Could not find message handler with the name \`${name}\`. Registered message handler names are: ${Object.keys(options.onMessage).join(', ')}.`)) + } + + const handlerType = typeof messageHandler + + if (handlerType !== 'function') { + return messageHandlerError(pluginError(`Message handlers must be functions, but the message handler for the name \`${name}\` was type \`${handlerType}\`.`)) + } + + let browser: Browser + + try { + browser = await puppeteer.connect({ + browserWSEndpoint: debuggerUrl, + defaultViewport: null, + }) + } catch (err: any) { + return messageHandlerError(err) + } + + let result: any + let error: any + + try { + result = await messageHandler(browser, ...args) + } catch (err: any) { + error = err + } finally { + // - Only implemented for Chromium right now. Support for Firefox/webkit + // could be added later + // - Electron doesn't have tabs + // - Focus doesn't matter for headless browsers and old headless Chrome + // doesn't run the extension + const isHeadedChromium = cypressBrowser.isHeaded && cypressBrowser.family === 'chromium' && cypressBrowser.name !== 'electron' + + if (isHeadedChromium) { + try { + await activateMainTab(browser) + } catch (e) { + return messageHandlerError(pluginError('Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin.')) + } + } + + await browser.disconnect() + } + + if (error) { + return messageHandlerError(error) + } + + // cy.task() errors if `undefined` is returned, so return null in that case + return result === undefined ? null : result + }, + }) +} diff --git a/npm/puppeteer/src/plugin/util.ts b/npm/puppeteer/src/plugin/util.ts new file mode 100644 index 00000000000..3a8fc7c9a83 --- /dev/null +++ b/npm/puppeteer/src/plugin/util.ts @@ -0,0 +1,5 @@ +export const pluginName = '@cypress/puppeteer' + +export function pluginError (message: string) { + return new Error(message) +} diff --git a/npm/puppeteer/src/support/index.ts b/npm/puppeteer/src/support/index.ts new file mode 100644 index 00000000000..5e8a14a63a7 --- /dev/null +++ b/npm/puppeteer/src/support/index.ts @@ -0,0 +1,14 @@ +Cypress.Commands.add('puppeteer', (name, ...args) => { + Cypress.log({ + name: 'puppeteer', + message: name, + }) + + cy.task('__cypressPuppeteer__', { name, args }, { log: false }).then((result: any) => { + if (result && result.__error__) { + throw new Error(`cy.puppeteer() failed with the following error:\n> ${result.__error__.message || result.__error__}`) + } + + return result + }) +}) diff --git a/npm/puppeteer/support/index.d.ts b/npm/puppeteer/support/index.d.ts new file mode 100644 index 00000000000..0c58bc4b161 --- /dev/null +++ b/npm/puppeteer/support/index.d.ts @@ -0,0 +1,7 @@ +/// + +declare namespace Cypress { + interface Chainable { + puppeteer(messageName: string, ...args: any[]): Chainable + } +} diff --git a/npm/puppeteer/support/index.js b/npm/puppeteer/support/index.js new file mode 100644 index 00000000000..cdde21eaaaf --- /dev/null +++ b/npm/puppeteer/support/index.js @@ -0,0 +1 @@ +require('../dist/support') diff --git a/npm/puppeteer/test/unit/activateMainTab.spec.ts b/npm/puppeteer/test/unit/activateMainTab.spec.ts new file mode 100644 index 00000000000..5089e4dd159 --- /dev/null +++ b/npm/puppeteer/test/unit/activateMainTab.spec.ts @@ -0,0 +1,118 @@ +import { expect, use } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' +import type { Browser, Page } from 'puppeteer-core' +import { activateMainTab, ACTIVATION_TIMEOUT } from '../../src/plugin/activateMainTab' + +use(chaiAsPromised) +use(sinonChai) + +describe('activateMainTab', () => { + let clock: sinon.SinonFakeTimers + let prevWin: Window + let prevDoc: Document + let prevTop: Window & typeof globalThis + let window: Partial + let mockDocument: Partial & { + defaultView: Window & typeof globalThis + } + let mockTop: Partial + let mockBrowser: Partial + let mockPage: Partial + + beforeEach(() => { + clock = sinon.useFakeTimers() + + window = { + addEventListener: sinon.stub(), + removeEventListener: sinon.stub(), + + // @ts-ignore sinon gets confused about postMessage type declaration + postMessage: sinon.stub(), + } + + mockDocument = { + defaultView: window as Window & typeof globalThis, + } + + mockTop = mockDocument.defaultView + + // activateMainTab is eval'd in browser context, but the tests exec in a + // node context. We don't necessarily need to do this swap, but it makes the + // tests more portable. + // @ts-ignore + prevWin = global.window + prevDoc = global.document + // @ts-ignore + prevTop = global.top + //@ts-ignore + global.window = window + global.document = mockDocument as Document + //@ts-ignore + global.top = mockTop + + mockPage = { + evaluate: sinon.stub().callsFake((fn, ...args) => fn(...args)), + } + + mockBrowser = { + pages: sinon.stub(), + } + }) + + afterEach(() => { + clock.restore() + // @ts-ignore + global.window = prevWin + // @ts-ignore + global.top = prevTop + global.document = prevDoc + }) + + it('sends a tab activation request to the plugin, and resolves when the ack event is received', async () => { + const pagePromise = Promise.resolve([mockPage]) + + ;(mockBrowser.pages as sinon.SinonStub).returns(pagePromise) + const p = activateMainTab(mockBrowser as Browser) + + await pagePromise + // @ts-ignore + window.addEventListener.withArgs('message').yield({ data: { message: 'cypress:extension:main:tab:activated' } }) + expect(window.postMessage).to.be.calledWith({ message: 'cypress:extension:activate:main:tab' }) + + expect(p).to.eventually.be.true + }) + + it('sends a tab activation request to the plugin, and rejects if it times out', async () => { + const pagePromise = Promise.resolve([mockPage]) + + ;(mockBrowser.pages as sinon.SinonStub).returns(pagePromise) + await pagePromise + + const p = activateMainTab(mockBrowser as Browser) + + clock.tick(ACTIVATION_TIMEOUT + 1) + + expect(p).to.be.rejected + }) + + describe('when cy in cy', () => { + beforeEach(() => { + mockDocument.defaultView = {} as Window & typeof globalThis + }) + + it('does not try to send tab activation message', async () => { + const pagePromise = Promise.resolve([mockPage]) + + ;(mockBrowser.pages as sinon.SinonStub).returns(pagePromise) + + const p = activateMainTab(mockBrowser as Browser) + + await pagePromise + expect(window.postMessage).not.to.be.called + expect(window.addEventListener).not.to.be.called + await p + }) + }) +}) diff --git a/npm/puppeteer/test/unit/retry.spec.ts b/npm/puppeteer/test/unit/retry.spec.ts new file mode 100644 index 00000000000..50bf30832e4 --- /dev/null +++ b/npm/puppeteer/test/unit/retry.spec.ts @@ -0,0 +1,53 @@ +import { expect, use } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' + +import { retry } from '../../src/plugin' + +use(chaiAsPromised) +use(sinonChai) + +describe('#retry', () => { + it('returns result of passing 1st attempt', async () => { + const fn = sinon.stub().returns('passes 1st attempt') + const result = await retry(fn) + + expect(fn).to.be.calledOnce + expect(result).to.equal('passes 1st attempt') + }) + + it('retries after delay and returns result of subsequent passing attempt', async () => { + const fn = sinon.stub() + + fn.onFirstCall().throws('fail') + fn.onSecondCall().returns('passes 2nd attempt') + + const result = await retry(fn, { delayBetweenTries: 1 }) + + expect(fn).to.be.calledTwice + expect(result).to.equal('passes 2nd attempt') + }) + + it('retries up to timeout and returns result of subsequent passing attempt', async () => { + const fn = sinon.stub() + + fn.throws('fail') + fn.onCall(5).returns('passes 5th attempt') + + const result = await retry(fn, { delayBetweenTries: 1 }) + + expect(fn.callCount).to.equal(6) + expect(result).to.equal('passes 5th attempt') + }) + + it('fails if function does not pass before timeout', async () => { + const fn = sinon.stub().callsFake(() => { + throw new Error('fail') + }) + + await expect( + retry(fn, { timeout: 5, delayBetweenTries: 1 }), + ).to.be.rejectedWith('Failed retrying after 5ms: fail') + }) +}) diff --git a/npm/puppeteer/test/unit/setup.spec.ts b/npm/puppeteer/test/unit/setup.spec.ts new file mode 100644 index 00000000000..f33b4dc7f8b --- /dev/null +++ b/npm/puppeteer/test/unit/setup.spec.ts @@ -0,0 +1,310 @@ +import { expect, use } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import type { PuppeteerNode, Browser } from 'puppeteer-core' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' +import { MessageHandler } from '../../src/plugin/setup' +import { setup } from '../../src/plugin' +import * as activateMainTabExport from '../../src/plugin/activateMainTab' + +use(chaiAsPromised) +use(sinonChai) + +type StubbedMessageHandler = sinon.SinonStub, ReturnType> + +describe('#setup', () => { + let mockBrowser: Partial + let mockPuppeteer: Pick + let on: sinon.SinonStub + let onMessage: Record + + const testTask = 'test' + let testTaskHandler: StubbedMessageHandler + + function getTask () { + return on.withArgs('task').lastCall.args[1].__cypressPuppeteer__ + } + + function simulateBrowserLaunch () { + return on.withArgs('after:browser:launch').yield({ family: 'chromium', isHeaded: true }, { webSocketDebuggerUrl: 'ws://debugger' }) + } + + beforeEach(() => { + sinon.stub(activateMainTabExport, 'activateMainTab') + mockBrowser = { + disconnect: sinon.stub().resolves(), + } + + mockPuppeteer = { + connect: sinon.stub().resolves(mockBrowser), + } + + on = sinon.stub() + + testTaskHandler = sinon.stub() + + onMessage = { + [testTask]: testTaskHandler, + } + }) + + afterEach(() => { + sinon.reset() + + ;(activateMainTabExport.activateMainTab as sinon.SinonStub).restore() + }) + + it('registers `after:browser:launch` and `task` handlers', () => { + setup({ on, onMessage }) + + expect(on).to.be.calledWith('after:browser:launch') + expect(on).to.be.calledWith('task') + }) + + it('errors if registering `after:browser:launch` fails', () => { + const error = new Error('Event not registered') + + error.stack = '' + on.throws(error) + + expect(() => setup({ on, onMessage })).to.throw('Could not set up `after:browser:launch` task. Ensure you are running Cypress >= 13.6.0. The following error was encountered:\n\n') + }) + + describe('running message handler', () => { + it('connects puppeteer to browser', async () => { + setup({ + on, + puppeteer: mockPuppeteer as PuppeteerNode, + onMessage, + }) + + simulateBrowserLaunch() + + const task = getTask() + + await task({ name: testTask, args: [] }) + + expect(mockPuppeteer.connect).to.be.calledWith({ + browserWSEndpoint: 'ws://debugger', + defaultViewport: null, + }) + }) + + it('calls the specified message handler with the browser and args', async () => { + setup({ + on, + puppeteer: mockPuppeteer as PuppeteerNode, + onMessage, + }) + + simulateBrowserLaunch() + + const task = getTask() + + await task({ name: testTask, args: ['arg1', 'arg2'] }) + + expect(testTaskHandler).to.be.calledWith(mockBrowser, 'arg1', 'arg2') + }) + + it('disconnects the browser once the message handler is finished', async () => { + setup({ + on, + puppeteer: mockPuppeteer as PuppeteerNode, + onMessage, + }) + + simulateBrowserLaunch() + + const task = getTask() + + await task({ name: testTask, args: ['arg1', 'arg2'] }) + + expect(mockBrowser.disconnect).to.be.called + }) + + it('returns the result of the handler', async () => { + const resolution = 'result' + + onMessage[testTask].resolves(resolution) + + setup({ + on, + puppeteer: mockPuppeteer as PuppeteerNode, + onMessage, + }) + + simulateBrowserLaunch() + + const task = getTask() + const returnValue = await task({ name: testTask, args: ['arg1', 'arg2'] }) + + expect(returnValue).to.equal(resolution) + }) + + it('returns null if message handler returns undefined', async () => { + onMessage[testTask].resolves(undefined) + setup({ + on, + puppeteer: mockPuppeteer as PuppeteerNode, + onMessage, + }) + + simulateBrowserLaunch() + + const task = getTask() + const returnValue = await task({ name: testTask, args: ['arg1', 'arg2'] }) + + expect(returnValue).to.be.null + }) + + it('returns error object if debugger URL reference is lost', async () => { + setup({ on, onMessage }) + + const task = getTask() + const returnValue = await task({ name: 'nonexistent', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Lost the reference to the browser. This usually occurs because the Cypress config was reloaded without the browser re-launching. Close and re-open the browser.', + ) + }) + + it('returns error object if browser is not supported', async () => { + setup({ on, onMessage }) + + on.withArgs('after:browser:launch').yield({ family: 'Firefox' }, {}) + + const task = getTask() + const returnValue = await task({ name: 'nonexistent', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Only browsers in the "Chromium" family are supported. You are currently running a browser with the family: Firefox', + ) + }) + + it('disconnects browser and returns error object if message handler errors', async () => { + testTaskHandler.rejects(new Error('handler error')) + setup({ + on, + puppeteer: mockPuppeteer as PuppeteerNode, + onMessage, + }) + + simulateBrowserLaunch() + + const task = getTask() + const returnValue = await task({ name: testTask, args: ['arg1', 'arg2'] }) + + expect(mockBrowser.disconnect).to.be.called + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal('handler error') + }) + + it('returns error object if message handler with given name cannot be found', async () => { + setup({ on, onMessage }) + + simulateBrowserLaunch() + + const task = getTask() + const returnValue = await task({ name: 'nonexistent', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Could not find message handler with the name `nonexistent`. Registered message handler names are: test.', + ) + }) + + it('returns error object if message handler with given name cannot be found', async () => { + // @ts-expect-error + setup({ on, onMessage: { notAFunction: true } }) + + simulateBrowserLaunch() + const task = getTask() + const returnValue = await task({ name: 'notAFunction', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Message handlers must be functions, but the message handler for the name `notAFunction` was type `boolean`.', + ) + }) + + it('calls activateMainTab if there is a page in the browser', async () => { + (activateMainTabExport.activateMainTab as sinon.SinonStub).withArgs(mockBrowser).resolves() + setup({ on, onMessage, puppeteer: mockPuppeteer as PuppeteerNode }) + const task = getTask() + + simulateBrowserLaunch() + await task({ name: testTask, args: [] }) + + expect(activateMainTabExport.activateMainTab).to.be.calledWith(mockBrowser) + }) + + it('returns an error object if activateMainTab rejects', async () => { + (activateMainTabExport.activateMainTab as sinon.SinonStub).withArgs(mockBrowser).rejects() + + setup({ on, onMessage, puppeteer: mockPuppeteer as PuppeteerNode }) + simulateBrowserLaunch() + + const task = getTask() + + const returnValue = await task({ name: testTask, args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Cannot communicate with the Cypress Chrome extension. Ensure the extension is enabled when using the Puppeteer plugin.', + ) + }) + + it('does not try to activate main tab when the browser is headless', async () => { + setup({ on, onMessage, puppeteer: mockPuppeteer as PuppeteerNode }) + on.withArgs('after:browser:launch').yield({ family: 'chromium', isHeaded: false }, { webSocketDebuggerUrl: 'ws://debugger' }) + const task = getTask() + + await task({ name: testTask, args: [] }) + + expect(activateMainTabExport.activateMainTab).not.to.be.called + }) + + it('does not try to activate main tab when the browser is electron', async () => { + setup({ on, onMessage, puppeteer: mockPuppeteer as PuppeteerNode }) + on.withArgs('after:browser:launch').yield({ family: 'chromium', isHeaded: true, name: 'electron' }, { webSocketDebuggerUrl: 'ws://debugger' }) + const task = getTask() + + await task({ name: testTask, args: [] }) + expect(activateMainTabExport.activateMainTab).not.to.be.called + }) + }) + + describe('validation', () => { + it('errors if options argument is not provided', () => { + // @ts-expect-error + expect(() => setup()).to.throw('Must provide options argument to `setup`.') + }) + + it('errors if options argument is not an object', () => { + // @ts-expect-error + expect(() => setup(true)).to.throw('The options argument provided to `setup` must be an object.') + }) + + it('errors if `on` option is not provided', () => { + // @ts-expect-error + expect(() => setup({})).to.throw('Must provide `on` function to `setup`.') + }) + + it('errors if `on` option is not a function', () => { + // @ts-expect-error + expect(() => setup({ on: 'string' })).to.throw('The `on` option provided to `setup` must be a function.') + }) + + it('errors if `onMessage` option is not provided', () => { + // @ts-expect-error + expect(() => setup({ on: sinon.stub() })).to.throw('Must provide `onMessage` object to `setup`.') + }) + + it('errors if `onMessage` option is not an object', () => { + // @ts-expect-error + expect(() => setup({ on: sinon.stub(), onMessage: () => {} })).to.throw('The `onMessage` option provided to `setup` must be an object.') + }) + }) +}) diff --git a/npm/puppeteer/tsconfig.json b/npm/puppeteer/tsconfig.json new file mode 100644 index 00000000000..faec3a8532d --- /dev/null +++ b/npm/puppeteer/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "./dist", + "skipLibCheck": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "target": "ES2020", + "types": [ + "cypress", + "./support", + "./node_modules/@types/node" + ] + }, + "include": [ + "src/" + ] +} diff --git a/npm/react/.eslintignore b/npm/react/.eslintignore new file mode 100644 index 00000000000..79afe972da7 --- /dev/null +++ b/npm/react/.eslintignore @@ -0,0 +1,5 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json +**/cypress/fixtures \ No newline at end of file diff --git a/npm/react/.eslintrc b/npm/react/.eslintrc index 1740aa1a0b9..ecfd74f652d 100644 --- a/npm/react/.eslintrc +++ b/npm/react/.eslintrc @@ -13,13 +13,9 @@ "cypress/globals": true }, "root": true, - "globals": { - "jest": "readonly" - }, "rules": { "no-console": "off", "mocha/no-global-tests": "off", - "@typescript-eslint/no-unused-vars": "off", "react/jsx-filename-extension": [ "warn", { @@ -30,24 +26,5 @@ ] } ] - }, - "overrides": [ - { - "files": [ - "lib/*" - ], - "rules": { - "no-console": 1 - } - }, - { - "files": [ - "**/*.json" - ], - "rules": { - "quotes": "off", - "comma-dangle": "off" - } - } - ] + } } diff --git a/npm/react/.releaserc.js b/npm/react/.releaserc.js index 7b15992ed70..17d3bb87147 100644 --- a/npm/react/.releaserc.js +++ b/npm/react/.releaserc.js @@ -1,6 +1,3 @@ module.exports = { - ...require('../../.releaserc.base'), - branches: [ - { name: 'master', channel: 'latest' }, - ], + ...require('../../.releaserc'), } diff --git a/npm/react/CHANGELOG.md b/npm/react/CHANGELOG.md index c5457e9baeb..b6f0e0df3ab 100644 --- a/npm/react/CHANGELOG.md +++ b/npm/react/CHANGELOG.md @@ -1,3 +1,58 @@ +# [@cypress/react-v8.0.2](https://github.com/cypress-io/cypress/compare/@cypress/react-v8.0.1...@cypress/react-v8.0.2) (2024-06-07) + + +### Bug Fixes + +* update cypress to Typescript 5 ([#29568](https://github.com/cypress-io/cypress/issues/29568)) ([f3b6766](https://github.com/cypress-io/cypress/commit/f3b67666a5db0438594339c379cf27e1fd1e4abc)) + +# [@cypress/react-v8.0.1](https://github.com/cypress-io/cypress/compare/@cypress/react-v8.0.0...@cypress/react-v8.0.1) (2024-05-06) + +# [@cypress/react-v8.0.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v7.0.3...@cypress/react-v8.0.0) (2023-08-29) + + +* `@cypress/react-v8.0.0` was inadvertently released and published. There are no breaking changes or any other changes in this release. + +# [@cypress/react-v7.0.3](https://github.com/cypress-io/cypress/compare/@cypress/react-v7.0.2...@cypress/react-v7.0.3) (2023-03-20) + + +### Bug Fixes + +* **vite-dev-server:** do not use incremental esbuild option with Vite v4.2.0+ ([#26139](https://github.com/cypress-io/cypress/issues/26139)) ([3a2b2d3](https://github.com/cypress-io/cypress/commit/3a2b2d3323310c68f72f6e42203f5e93afc1cde5)) + +# [@cypress/react-v7.0.2](https://github.com/cypress-io/cypress/compare/@cypress/react-v7.0.1...@cypress/react-v7.0.2) (2022-12-02) + + +### Bug Fixes + +* remove cypress.server.defaults, cy.server and cy.route ([#24411](https://github.com/cypress-io/cypress/issues/24411)) ([2f18a8c](https://github.com/cypress-io/cypress/commit/2f18a8cbd2d1a90fe1f77a29cdc89571bf54109e)) + +# [@cypress/react-v7.0.1](https://github.com/cypress-io/cypress/compare/@cypress/react-v7.0.0...@cypress/react-v7.0.1) (2022-11-08) + + +### Bug Fixes + +* make component derived info not throw ([#24571](https://github.com/cypress-io/cypress/issues/24571)) ([838dd4f](https://github.com/cypress-io/cypress/commit/838dd4fa2e0ec56633d0af2faf10a47d190b5594)) + +# [@cypress/react-v7.0.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v6.2.1...@cypress/react-v7.0.0) (2022-11-07) + + +### Bug Fixes + +* remove last mounted component upon subsequent mount calls ([#24470](https://github.com/cypress-io/cypress/issues/24470)) ([f39eb1c](https://github.com/cypress-io/cypress/commit/f39eb1c19e0923bda7ae263168fc6448da942d54)) +* remove some CT functions and props ([#24419](https://github.com/cypress-io/cypress/issues/24419)) ([294985f](https://github.com/cypress-io/cypress/commit/294985f8b3e0fa00ed66d25f88c8814603766074)) + + +### BREAKING CHANGES + +* remove last mounted component upon subsequent mount calls of mount + +# [@cypress/react-v6.2.1](https://github.com/cypress-io/cypress/compare/@cypress/react-v6.2.0...@cypress/react-v6.2.1) (2022-11-01) + + +### Bug Fixes + +* Hovering over mount in command log does not show component in AUT ([#24346](https://github.com/cypress-io/cypress/issues/24346)) ([355d210](https://github.com/cypress-io/cypress/commit/355d2101d38ea4d1e93b9c571cf77babab2bbbfc)) + # [@cypress/react-v6.2.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v6.1.1...@cypress/react-v6.2.0) (2022-08-30) diff --git a/npm/react/README.md b/npm/react/README.md index c99ab05b6fd..bd5602adfd5 100644 --- a/npm/react/README.md +++ b/npm/react/README.md @@ -1,107 +1,8 @@ # @cypress/react -Mount React components in the open source [Cypress.io](https://www.cypress.io/) test runner **v7.0.0+** - -> **Note:** This package is bundled with the `cypress` package and should not need to be installed separately. See the [React Component Testing Docs](https://docs.cypress.io/guides/component-testing/quickstart-react#Configuring-Component-Testing) for mounting React components. Installing and importing `mount` from `@cypress/react` should only be used for advanced use-cases. - -## Install - -- Requires Cypress v7.0.0 or later -- Requires [Node](https://nodejs.org/en/) version 12 or above - -```sh -npm install --save-dev @cypress/react -``` - -## Run - -Open cypress test runner -``` -npx cypress open --component -``` - -If you need to run test in CI -``` -npx cypress run --component -``` - -For more information, please check the official docs for [running Cypress](https://on.cypress.io/guides/getting-started/opening-the-app#Quick-Configuration) and for [component testing](https://on.cypress.io/guides/component-testing/writing-your-first-component-test). - -## API - -- `mount` is the most important function, allows to mount a given React component as a mini web application and interact with it using Cypress commands -- `createMount` factory function that creates new `mount` function with default options -- `unmount` removes previously mounted component, mostly useful to test how the component cleans up after itself -- `mountHook` mounts a given React Hook in a test component for full testing, see `hooks` example - -## Examples - -```js -import React from 'react' -import { mount } from '@cypress/react' -import { HelloWorld } from './hello-world.jsx' -describe('HelloWorld component', () => { - it('works', () => { - mount() - // now use standard Cypress commands - cy.contains('Hello World!').should('be.visible') - }) -}) -``` - -Look at the examples in [cypress/component](cypress/component) folder. Here is the list of examples showing various testing scenarios. - -## Options - -In most cases, the component already imports its own styles, thus it looks "right" during the test. If you need another CSS, the simplest way is to import it from the spec file: - -```js -// src/Footer.spec.js -import './styles/main.css' -import Footer from './Footer' -it('looks right', () => { - // styles are applied - mount(