diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt index c8b9721d306c..81c26f678bd7 100644 --- a/.circleci/cache-version.txt +++ b/.circleci/cache-version.txt @@ -1,3 +1,3 @@ # Bump this version to force CI to re-create the cache from scratch. -04-19-22 +06-07-23 diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index ec0582d3dc10..289014aa65c5 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -30,7 +30,7 @@ mainBuildFilters: &mainBuildFilters - /^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' - - 'ryanm/feat/unify-cdp-approach-in-electron' + - 'billg/v13-merge/6-8-23' # 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 @@ -41,7 +41,7 @@ macWorkflowFilters: &darwin-workflow-filters - 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: [ 'ryanm/feat/unify-cdp-approach-in-electron', << pipeline.git.branch >> ] + - equal: [ 'billg/v13-merge/6-8-23', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -52,7 +52,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters - 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: [ 'ryanm/feat/unify-cdp-approach-in-electron', << pipeline.git.branch >> ] + - equal: [ 'billg/v13-merge/6-8-23', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -72,7 +72,7 @@ windowsWorkflowFilters: &windows-workflow-filters - 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: [ 'ryanm/feat/unify-cdp-approach-in-electron', << pipeline.git.branch >> ] + - equal: [ 'billg/v13-merge/6-8-23', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -139,7 +139,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "ryanm/feat/unify-cdp-approach-in-electron" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-v8-snapshot-cache-on-develop" && "$CIRCLE_BRANCH" != "billg/v13-merge/6-8-23" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -282,9 +282,6 @@ commands: 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: 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: | @@ -576,14 +573,15 @@ commands: # 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.*" + # 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).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 - 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. @@ -752,7 +750,7 @@ commands: command: description: Test command to run to start Cypress tests type: string - default: "yarn cypress:run" + 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: @@ -822,7 +820,7 @@ commands: name: Run tests using browser "<>" working_directory: /tmp/<>/<> command: | - <> -- --browser <> + <> --browser <> --record false - unless: condition: <> steps: @@ -839,7 +837,7 @@ commands: - run: name: Run tests using browser "<>" working_directory: /tmp/<> - command: <> -- --browser <> + command: <> --browser <> --record false - unless: condition: <> steps: @@ -1944,20 +1942,6 @@ jobs: command: yarn workspace @cypress/mount-utils build - store-npm-logs - npm-xpath: - <<: *defaults - resource_class: small - steps: - - restore_cached_workspace - - run: - name: Run tests - command: yarn workspace @cypress/xpath cy:run - - store_test_results: - path: npm/xpath/test_results - - store_artifacts: - path: npm/xpath/test_results - - store-npm-logs - npm-grep: <<: *defaults resource_class: small @@ -2050,20 +2034,13 @@ jobs: command: rm -rf cypress.json - run: name: Install prod dependencies - command: yarn --production + 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: 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: | @@ -2767,6 +2744,7 @@ linux-x64-workflow: &linux-x64-workflow requires: - create-build-artifacts - test-binary-against-cypress-realworld-app: + context: test-runner:cypress-record-key <<: *mainBuildFilters requires: - create-build-artifacts diff --git a/.eslintrc.js b/.eslintrc.js index 5c469c301940..12aa005137ac 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -57,7 +57,6 @@ module.exports = { rules: { 'no-restricted-properties': 'off', 'no-restricted-syntax': 'off', - 'no-console': 'off', }, }, ], diff --git a/.github/workflows/snyk_sca_scan.yaml b/.github/workflows/snyk_sca_scan.yaml index 15487445ade7..1aa7fcfb5410 100644 --- a/.github/workflows/snyk_sca_scan.yaml +++ b/.github/workflows/snyk_sca_scan.yaml @@ -16,7 +16,7 @@ jobs: matrix: node-version: [16.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setting up Node uses: actions/setup-node@v3 with: @@ -27,6 +27,6 @@ jobs: - 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=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 50ce41e5f14e..d32553086d01 100644 --- a/.github/workflows/snyk_static_analysis_scan.yaml +++ b/.github/workflows/snyk_static_analysis_scan.yaml @@ -13,7 +13,7 @@ jobs: Snyk_SAST_Scan : runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - 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 index 20b4f31b0415..ece421f0aad6 100644 --- a/.github/workflows/stale_issues_and_pr_cleanup.yml +++ b/.github/workflows/stale_issues_and_pr_cleanup.yml @@ -6,6 +6,10 @@ on: 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 @@ -28,7 +32,8 @@ permissions: issues: write pull-requests: write env: - DEFAULT_DEBUG_ONLY: true + 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' @@ -37,7 +42,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v7 + - 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 }} @@ -50,5 +55,6 @@ jobs: 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: 400 #keeping this a bit higher because it processes newest tickets to oldest + 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 c056ccfea74d..11c27119b66d 100644 --- a/.github/workflows/triage_add_to_project.yml +++ b/.github/workflows/triage_add_to_project.yml @@ -6,6 +6,10 @@ on: secrets: ADD_TO_TRIAGE_BOARD_TOKEN: required: true + TRIAGE_BOARD_TOKEN: + required: true + WORKFLOW_DEPLOY_KEY: + required: true issues: types: - opened @@ -41,3 +45,24 @@ jobs: 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@v3 + with: + repository: 'cypress-io/release-automations' + ref: 'master' + ssh-key: ${{ secrets.WORKFLOW_DEPLOY_KEY }} + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: 'lts/*' + - name: Run comment_workflow.js Script + uses: actions/github-script@v6 + 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_handle_new_comments.yml b/.github/workflows/triage_handle_new_comments.yml index f2b2dbce4ac9..0cf253ad0d24 100644 --- a/.github/workflows/triage_handle_new_comments.yml +++ b/.github/workflows/triage_handle_new_comments.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '16' + node-version: 'lts/*' - name: Run comment_workflow.js Script uses: actions/github-script@v6 with: diff --git a/.github/workflows/update-browser-versions.yml b/.github/workflows/update-browser-versions.yml index ba1ea3a28e2f..be0e08ad3f5e 100644 --- a/.github/workflows/update-browser-versions.yml +++ b/.github/workflows/update-browser-versions.yml @@ -10,7 +10,7 @@ jobs: BASE_BRANCH: develop steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} @@ -40,13 +40,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' }} diff --git a/.gitignore b/.gitignore index 65d1854e6057..f67b890a1140 100644 --- a/.gitignore +++ b/.gitignore @@ -78,8 +78,6 @@ system-tests/lib/fixtureDirs.ts # from npm/webpack-dev-server /npm/webpack-dev-server/cypress/videos -# from npm/xpath -/npm/xpath/cypress/videos # from npm/grep /npm/grep/cypress/videos diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe28547aa15..becc1f915c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,4 +14,3 @@ - [`@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) -- [`@cypress/xpath`](https://github.com/cypress-io/cypress/blob/develop/npm/xpath/CHANGELOG.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fedcaa775fc8..09763aa5403e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -197,7 +197,6 @@ Here is a list of the npm packages in this repository: | [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. | - | [xpath](./npm/xpath) | `@cypress/xpath` | Adds XPath command to Cypress.io test runner | 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. @@ -205,7 +204,7 @@ 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.) diff --git a/README.md b/README.md index 8df5cd1a3ef7..9867193933f2 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ npm - - Gitter chat + + Discord chat StackShare diff --git a/browser-versions.json b/browser-versions.json index 641eb70e9240..eb4bb16309d3 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,5 +1,5 @@ { - "chrome:beta": "113.0.5672.63", - "chrome:stable": "112.0.5615.165", + "chrome:beta": "115.0.5790.13", + "chrome:stable": "114.0.5735.106", "chrome:minimum": "64.0.3282.0" } diff --git a/cli/.eslintignore b/cli/.eslintignore index a3f72532311c..8bd0ed4f295f 100644 --- a/cli/.eslintignore +++ b/cli/.eslintignore @@ -1,6 +1,10 @@ **/__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 diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 82e0a27ecdd6..0d58a8464027 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -7,18 +7,82 @@ _Released 03/1/2023 (PENDING)_ - 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). -## 12.11.1 +## 12.14.1 -_Released 05/09/2023 (PENDING)_ +_Released 06/20/2023 (PENDING)_ + +**Features:** + +- 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). + +**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). + +## 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 @@ -57,7 +121,6 @@ _Released 04/17/2023_ - 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). - - Correctly detect and resolve dependencies when configuring Component Testing in projects using Yarn's [Plug'n'Play feature](https://yarnpkg.com/features/pnp). Fixes [#25960](https://github.com/cypress-io/cypress/issues/25960). - 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:** diff --git a/cli/__snapshots__/download_spec.js b/cli/__snapshots__/download_spec.js index c913289d5846..54ec0d549733 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/lib/tasks/download.js b/cli/lib/tasks/download.js index 17b6c733f301..01d0f73bc0ad 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -64,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/package.json b/cli/package.json index ec12f4cbd10d..f968022a5979 100644 --- a/cli/package.json +++ b/cli/package.json @@ -66,7 +66,15 @@ "devDependencies": { "@babel/cli": "7.13.0", "@babel/preset-env": "7.13.5", + "@cypress/angular": "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", @@ -120,47 +128,47 @@ "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" } }, "workspaces": { diff --git a/cli/test/lib/tasks/download_spec.js b/cli/test/lib/tasks/download_spec.js index 7617cdcd4ebe..7560fbf48fee 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') diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index e2311554ab94..804895976be1 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3004,10 +3004,13 @@ 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 diff --git a/cli/types/tests/plugins-config.ts b/cli/types/tests/plugins-config.ts index bbe8fcc71494..8b92a5010320 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 diff --git a/lerna.json b/lerna.json index f40b1af11f4c..a1cac42c5796 100644 --- a/lerna.json +++ b/lerna.json @@ -5,8 +5,10 @@ "packages/*", "npm/*", "tooling/*", - "system-tests" + "system-tests", + "scripts" ], "useWorkspaces": true, + "useNx": true, "version": "0.0.0" } diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index aeeb9c883a1b..8ba0f0dab89a 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -116,6 +116,7 @@ export type MountResponse = { // '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() diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json index e52091e85c9f..d0fa15b5c53a 100644 --- a/npm/create-cypress-tests/package.json +++ b/npm/create-cypress-tests/package.json @@ -21,10 +21,8 @@ "chalk": "4.1.0", "cli-highlight": "2.1.10", "commander": "6.2.1", - "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", @@ -40,7 +38,6 @@ "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" }, diff --git a/npm/create-cypress-tests/scripts/copy-templates.js b/npm/create-cypress-tests/scripts/copy-templates.js index 832a816c053b..89349cfe67a4 100644 --- a/npm/create-cypress-tests/scripts/copy-templates.js +++ b/npm/create-cypress-tests/scripts/copy-templates.js @@ -1,4 +1,4 @@ -const fg = require('fast-glob') +const globby = require('globby') const fs = require('fs-extra') const chalk = require('chalk') const path = require('path') @@ -11,7 +11,7 @@ program const srcPath = path.resolve(__dirname, '..', 'src') const destinationPath = path.resolve(process.cwd(), destination) - const templates = await fg('**/*.template.js', { + const templates = await globby('**/*.template.js', { cwd: srcPath, onlyFiles: true, unique: true, diff --git a/npm/vite-plugin-cypress-esm/CHANGELOG.md b/npm/vite-plugin-cypress-esm/CHANGELOG.md index e69de29bb2d1..bb4139b30369 100644 --- a/npm/vite-plugin-cypress-esm/CHANGELOG.md +++ b/npm/vite-plugin-cypress-esm/CHANGELOG.md @@ -0,0 +1,13 @@ +# [@cypress/vite-plugin-cypress-esm-v1.0.1](https://github.com/cypress-io/cypress/compare/@cypress/vite-plugin-cypress-esm-v1.0.0...@cypress/vite-plugin-cypress-esm-v1.0.1) (2023-05-23) + + +### Bug Fixes + +* Vite esm plugin issues ([#26714](https://github.com/cypress-io/cypress/issues/26714)) ([38b9526](https://github.com/cypress-io/cypress/commit/38b952622e034928df0d6e5d571e83c22d98ce86)) + +# @cypress/vite-plugin-cypress-esm-v1.0.0 (2023-05-04) + + +### Features + +* initial release of cypress/vite-plugin-cypress-esm ([#26663](https://github.com/cypress-io/cypress/issues/26663)) ([42d4f83](https://github.com/cypress-io/cypress/commit/42d4f836cf30069a152cb45ac49af736155e7cc1)) diff --git a/npm/vite-plugin-cypress-esm/README.md b/npm/vite-plugin-cypress-esm/README.md index 89606956ff9c..ea25b21ef836 100644 --- a/npm/vite-plugin-cypress-esm/README.md +++ b/npm/vite-plugin-cypress-esm/README.md @@ -36,9 +36,9 @@ export default defineConfig({ CypressEsm(), ] } - ), + ) } - }, + } } }) ``` @@ -61,6 +61,8 @@ CypressEsm({ }) ``` +This will exclude modules matching the supplied pattern from being processed by this plugin. It is important to note that the actual import of any matching module into other files/modules will still be processed unless those destinations are themselves excluded via the list. + React is known to have some conflicts with the Proxy implementation that cause problems stubbing internal React functionality. Since it is unlikely you want to stub parts of React itself, it's a good idea to add it to the `ignoreList`. ## Known Issues diff --git a/npm/vite-plugin-cypress-esm/cypress.config.ts b/npm/vite-plugin-cypress-esm/cypress.config.ts index 6d9e5ac8e766..47f27ebf5885 100644 --- a/npm/vite-plugin-cypress-esm/cypress.config.ts +++ b/npm/vite-plugin-cypress-esm/cypress.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ jsxRuntime: 'classic', }), CypressEsm({ - ignoreList: ['*Immutable*', '*MyAsync*'], + ignoreList: ['**/*ignoreList.cy.ts', '*MyAsync*'], }), ], } diff --git a/npm/vite-plugin-cypress-esm/cypress/component/assetTypes.cy.ts b/npm/vite-plugin-cypress-esm/cypress/component/assetTypes.cy.ts index c3768fb881e5..3ddaa0a08f0b 100644 --- a/npm/vite-plugin-cypress-esm/cypress/component/assetTypes.cy.ts +++ b/npm/vite-plugin-cypress-esm/cypress/component/assetTypes.cy.ts @@ -1,6 +1,14 @@ -import './fixtures/style.css' -import { add } from './fixtures/add' +import css from './fixtures/style.css?inline' +import svg from './fixtures/image.svg?raw' -it('does not transform non JS assets', () => { - expect(add(1, 2)).to.eq(3) +describe('asset types', () => { + it('does not transform style assets', () => { + expect(css).to.contain('import') + expect(css).not.to.contain('_cypress') + }) + + it('does not transform image assets', () => { + expect(svg).to.contain('import') + expect(svg).not.to.contain('_cypress') + }) }) diff --git a/npm/vite-plugin-cypress-esm/cypress/component/dynamicImport.cy.ts b/npm/vite-plugin-cypress-esm/cypress/component/dynamicImport.cy.ts index 74975ec6b6ed..ee3f26976069 100644 --- a/npm/vite-plugin-cypress-esm/cypress/component/dynamicImport.cy.ts +++ b/npm/vite-plugin-cypress-esm/cypress/component/dynamicImport.cy.ts @@ -74,4 +74,15 @@ describe('dynamic imports', () => { cy.wrap(run()) }) + + it('ignores import-like functions', async () => { + /* + * This test will probably explode due to malformed syntax if the exclusion logic isn't working, + * so the assertion here isn't really necessary but helps mark as a passing test + */ + + const importLike = await import('./fixtures/import-like') + + expect(importLike.custom_import('abc')).to.eql('123abc') + }) }) diff --git a/npm/vite-plugin-cypress-esm/cypress/component/fixtures/image.svg b/npm/vite-plugin-cypress-esm/cypress/component/fixtures/image.svg new file mode 100644 index 000000000000..96d4c710263f --- /dev/null +++ b/npm/vite-plugin-cypress-esm/cypress/component/fixtures/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/npm/vite-plugin-cypress-esm/cypress/component/fixtures/import-like.ts b/npm/vite-plugin-cypress-esm/cypress/component/fixtures/import-like.ts new file mode 100644 index 000000000000..624814a0c90c --- /dev/null +++ b/npm/vite-plugin-cypress-esm/cypress/component/fixtures/import-like.ts @@ -0,0 +1,5 @@ +// Ensure import replacement is not too aggressive +// Should not replace this function name despite invocation looking like `import()` +export const custom_import = (value: string) => { + return `123${value}` +} diff --git a/npm/vite-plugin-cypress-esm/cypress/component/fixtures/style.css b/npm/vite-plugin-cypress-esm/cypress/component/fixtures/style.css index 795629d78285..90a07da9222a 100644 --- a/npm/vite-plugin-cypress-esm/cypress/component/fixtures/style.css +++ b/npm/vite-plugin-cypress-esm/cypress/component/fixtures/style.css @@ -1,3 +1,4 @@ div { + /* Magic comment: import('test') */ background: red; } \ No newline at end of file diff --git a/npm/vite-plugin-cypress-esm/cypress/component/importSyntax.cy.ts b/npm/vite-plugin-cypress-esm/cypress/component/importSyntax.cy.ts index e4cce16495f0..0e3a7795e084 100644 --- a/npm/vite-plugin-cypress-esm/cypress/component/importSyntax.cy.ts +++ b/npm/vite-plugin-cypress-esm/cypress/component/importSyntax.cy.ts @@ -96,4 +96,16 @@ describe('supports every combination of import syntax in a single file', () => { // @ts-expect-error expect(window.sideEffect).to.eq('Side Effect') }) + + describe('import-like syntax', () => { + it('should ignore if not prefixed with whitespace or newline', () => { + /* + * This test will probably explode due to malformed syntax if the exclusion logic isn't working, + * so the assertion here isn't really necessary but helps mark as a passing test + */ + const value = ' abc_import abc from "blah"' + + expect(value).not.to.contain('cypress') + }) + }) }) diff --git a/npm/vite-plugin-cypress-esm/cypress/types.d.ts b/npm/vite-plugin-cypress-esm/cypress/types.d.ts new file mode 100644 index 000000000000..33c35567169e --- /dev/null +++ b/npm/vite-plugin-cypress-esm/cypress/types.d.ts @@ -0,0 +1,2 @@ +declare module '*.css' {} +declare module '*.svg' {} diff --git a/npm/vite-plugin-cypress-esm/src/index.ts b/npm/vite-plugin-cypress-esm/src/index.ts index 6598894e7b55..9704f48f32f4 100644 --- a/npm/vite-plugin-cypress-esm/src/index.ts +++ b/npm/vite-plugin-cypress-esm/src/index.ts @@ -19,14 +19,37 @@ interface CypressEsmOptions { export const CypressEsm = (options?: CypressEsmOptions): Plugin => { const ignoreList = options?.ignoreList ?? [] + const ignoreMatcher = picomatch(ignoreList) - const importOnIgnoreList = (importTarget: string) => { - for (const ignore of ignoreList) { - if (picomatch.matchBase(importTarget, ignore)) { - return true - } - } + /** + * If a module ID is explicitly ignored then bypass our custom mapping on it + * + * @param moduleId + * @returns + */ + const isModuleOnIgnoreList = (moduleId: string) => { + return ignoreMatcher(moduleId) + } + + /** + * If an import target is for a non-JS asset then we don't want to map it + * This is typically a dynamically-imported image or data asset + * + * @param importTarget + * @returns + */ + const isNonJsTarget = (importTarget: string) => { + // Strip quotes & semicolons + const sanitizedImportTarget = importTarget.replace(/["';]/gi, '').trim() + + // Exclude common extensions for: + // - Images + // - Text/Data/Markup/Markdown files + // - Styles + // Other asset types like audio/video are unlikely to be imported into a JS file thus are not considered here + return /\.(svg|png|jpe?g|gif|tiff|webp|json|md|txt|xml|x?html?|css|less|s[c|a]ss?)(\?|$)/gi.test(sanitizedImportTarget) } + /** * Remap static import calls to use the Cypress module cache * @@ -48,19 +71,23 @@ export const CypressEsm = (options?: CypressEsmOptions): Plugin => { const moduleIdentifier = moduleId.replace(/[^a-zA-Z\d]/g, '_') - const importRegex = /import (.+?) from (.*)/g + // Ensure import comes at start of line *or* is prefixed by a space so we don't capture things like + // `Refresh.__hmr_import('') + const importRegex = /(?<=^|\s)import (.+?) from (.*)/g return code.replace( importRegex, (match, importVars: string, importTarget: string) => { debug(`Mapping import ${counter + 1} in module ${moduleId}`) - let replacement = `import * as cypress_${moduleIdentifier}_${++counter} from ${importTarget}` + if (isNonJsTarget(importTarget)) { + debug(`Import ${importTarget} appears to be an asset and will not be re-mapped`) - if (importOnIgnoreList(importTarget)) { return match } + let replacement = `import * as cypress_${moduleIdentifier}_${++counter} from ${importTarget}` + if (!replacement.endsWith(';')) { replacement += ';' } @@ -128,12 +155,14 @@ export const CypressEsm = (options?: CypressEsmOptions): Plugin => { * __cypressDynamicModule(import("./mod_2")).then(mod => mod) */ const mapDynamicImportsToCache = (id: string, code: string) => { - const RE = /(import\((.+?)\))/g + const RE = /((?<=^|\s)import\((.+?)\))/g return code.replace( RE, (match, importVars: string, importTarget: string) => { - if (importOnIgnoreList(importTarget)) { + if (isNonJsTarget(importTarget)) { + debug(`Import ${importTarget} appears to be an asset and will not be re-mapped`) + return match } @@ -149,10 +178,22 @@ export const CypressEsm = (options?: CypressEsmOptions): Plugin => { name: 'cypress:mocks', enforce: 'post', transform (code, id, options) { - // Process all files to remap imports - // TODO: Restrict to JS files only? Any potential for .mjs etc? + if (isModuleOnIgnoreList(id)) { + debug(`Ignoring module ${id} due to ignoreList`) + + return + } + + if (isNonJsTarget(id)) { + debug(`Module ${id} appears to be an asset, ignoring`) + + return + } + + // Process all files to remap static imports let transformedCode = mapImportsToCache(id, code) + // ...and dynamic imports transformedCode = mapDynamicImportsToCache(id, transformedCode) return { diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index c9a57905bb6a..0526ea7f6ec4 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -38,7 +38,6 @@ "cypress": "0.0.0-development", "dependency-check": "2.9.1", "deps-ok": "1.4.1", - "fast-glob": "3.1.1", "find-webpack": "1.5.0", "fs-extra": "^10.1.0", "mocha": "^7.1.0", diff --git a/npm/webpack-preprocessor/test/e2e/e2e.spec.js b/npm/webpack-preprocessor/test/e2e/e2e.spec.js index 3725f5f59ca2..a70a752344f6 100644 --- a/npm/webpack-preprocessor/test/e2e/e2e.spec.js +++ b/npm/webpack-preprocessor/test/e2e/e2e.spec.js @@ -1,11 +1,11 @@ const { runTest } = require('./helpers') const path = require('path') -const glob = require('fast-glob') +const globby = require('globby') describe('can test', async () => { // runs every test in cypress/tests/e2e as its own test // the comment above the test will determine the assertion on the results - glob.sync(path.join(__dirname, '../../cypress/tests/e2e/**/*')) + globby.sync(path.join(__dirname, '../../cypress/tests/e2e/**/*')) .map((v) => { const filename = path.relative(process.cwd(), v) diff --git a/npm/xpath/.releaserc.js b/npm/xpath/.releaserc.js deleted file mode 100644 index 17d3bb871472..000000000000 --- a/npm/xpath/.releaserc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - ...require('../../.releaserc'), -} diff --git a/npm/xpath/CHANGELOG.md b/npm/xpath/CHANGELOG.md deleted file mode 100644 index 36e869b7cb81..000000000000 --- a/npm/xpath/CHANGELOG.md +++ /dev/null @@ -1,13 +0,0 @@ -# [@cypress/xpath-v2.0.3](https://github.com/cypress-io/cypress/compare/@cypress/xpath-v2.0.2...@cypress/xpath-v2.0.3) (2022-10-26) - - -### Bug Fixes - -* **xpath:** update xpath main path ([#24259](https://github.com/cypress-io/cypress/issues/24259)) ([edf99c4](https://github.com/cypress-io/cypress/commit/edf99c41d6031c7a72ad2258f4fd29231823790c)) - -# [@cypress/xpath-v2.0.2](https://github.com/cypress-io/cypress/compare/@cypress/xpath-v2.0.1...@cypress/xpath-v2.0.2) (2022-09-29) - - -### Bug Fixes - -* release svelte ([b86403f](https://github.com/cypress-io/cypress/commit/b86403fcbcc85ce5be1ca96bbf42357dd24c07dd)) diff --git a/npm/xpath/README.md b/npm/xpath/README.md index 339fce4e3500..de39c50328c9 100644 --- a/npm/xpath/README.md +++ b/npm/xpath/README.md @@ -1,82 +1,3 @@ # @cypress/xpath -> Adds XPath command to [Cypress.io](https://www.cypress.io) test runner - -## Install with npm - -```shell -npm install -D @cypress/xpath -``` - -## Install with Yarn - -```shell -yarn add @cypress/xpath --dev -``` - -Then include in your project's [support file](https://on.cypress.io/support-file) - -```js -require('@cypress/xpath'); -``` - -## Use - -After installation your `cy` object will have `xpath` command. - -```js -it('finds list items', () => { - cy.xpath('//ul[@class="todo-list"]//li').should('have.length', 3); -}); -``` - -You can also chain `xpath` off of another command. - -```js -it('finds list items', () => { - cy.xpath('//ul[@class="todo-list"]').xpath('./li').should('have.length', 3); -}); -``` - -As with other cy commands, it is scoped by `cy.within()`. - -```js -it('finds list items', () => { - cy.xpath('//ul[@class="todo-list"]').within(() => { - cy.xpath('./li').should('have.length', 3); - }); -}); -``` - -**note:** you can test XPath expressions from DevTools console using `$x(...)` function, for example `$x('//div')` to find all divs. - -See [cypress/e2e/spec.cy.js](cypress/e2e/spec.cy.js) - -## Beware the XPath // trap - -In XPath the expression // means something very specific, and it might not be what you think. Contrary to common belief, // means "anywhere in the document" not "anywhere in the current context". As an example: - -```js -cy.xpath('//body').xpath('//script'); -``` - -You might expect this to find all script tags in the body, but actually, it finds all script tags in the entire document, not only those in the body! What you're looking for is the .// expression which means "any descendant of the current node": - -```js -cy.xpath('//body').xpath('.//script'); -``` - -The same thing goes for within: - -```js -cy.xpath('//body').within(() => { - cy.xpath('.//script'); -}); -``` - - -For more, see [Intelligent Code Completion](https://on.cypress.io/intellisense) - -## License - -This project is licensed under the terms of the [MIT license](/LICENSE.md). +> @cypress/xpath has been deprecated and is no longer supported. diff --git a/npm/xpath/cypress.config.js b/npm/xpath/cypress.config.js deleted file mode 100644 index e7a2ffb6dcaa..000000000000 --- a/npm/xpath/cypress.config.js +++ /dev/null @@ -1,12 +0,0 @@ -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - excludeSpecPattern: '*.html', - supportFile: 'cypress/support/e2e.js', - }, - component: { - excludeSpecPattern: '*.html', - supportFile: 'cypress/support/e2e.js', - }, -}) diff --git a/npm/xpath/cypress/e2e/index.html b/npm/xpath/cypress/e2e/index.html deleted file mode 100644 index ebe8a49935f3..000000000000 --- a/npm/xpath/cypress/e2e/index.html +++ /dev/null @@ -1,26 +0,0 @@ - -
-

cypress-xpath

- -
- - -
-
- - \ No newline at end of file diff --git a/npm/xpath/cypress/e2e/spec.cy.js b/npm/xpath/cypress/e2e/spec.cy.js deleted file mode 100644 index c88881f93a80..000000000000 --- a/npm/xpath/cypress/e2e/spec.cy.js +++ /dev/null @@ -1,183 +0,0 @@ -/// -/// - -describe('cypress-xpath', () => { - it('adds xpath command', () => { - expect(cy).property('xpath').to.be.a('function') - }) - - context('elements', () => { - beforeEach(() => { - cy.visit('cypress/e2e/index.html') - }) - - it('finds h1', () => { - cy.xpath('//h1').should('have.length', 1) - }) - - it('returns jQuery wrapped elements', () => { - cy.xpath('//h1').then((el$) => { - expect(el$).to.have.property('jquery') - }) - }) - - it('returns primitives as is', () => { - cy.xpath('string(//h1)').then((el$) => { - expect(el$).to.not.have.property('jquery') - }) - }) - - it('provides jQuery wrapped elements to assertions', () => { - cy.xpath('//h1').should((el$) => { - expect(el$).to.have.property('jquery') - }) - }) - - it('gets h1 text', () => { - cy.xpath('//h1/text()') - .its('0.textContent') - .should('equal', 'cypress-xpath') - }) - - it('retries until element is inserted', () => { - // the element will be inserted after 1 second - cy.xpath('string(//*[@id="inserted"])').should('equal', 'inserted text') - }) - - describe('chaining', () => { - it('finds h1 within main', () => { - // first assert that h1 doesn't exist as a child of the implicit document subject - cy.xpath('./h1').should('not.exist') - - cy.xpath('//main').xpath('./h1').should('exist') - }) - - it('finds body outside of main when succumbing to // trap', () => { - // first assert that body doesn't actually exist within main - cy.xpath('//main').xpath('.//body').should('not.exist') - - cy.xpath('//main').xpath('//body').should('exist') - }) - - it('finds h1 within document', () => { - cy.document().xpath('//h1').should('exist') - }) - - it('throws when subject is more than a single element', (done) => { - cy.on('fail', (err) => { - expect(err.message).to.eq( - 'xpath() can only be called on a single element. Your subject contained 2 elements.', - ) - - done() - }) - - cy.get('main, div').xpath('foo') - }) - }) - - describe('within()', () => { - it('finds h1 within within-subject', () => { - // first assert that h1 doesn't exist as a child of the implicit document subject - cy.xpath('./h1').should('not.exist') - - cy.xpath('//main').within(() => { - cy.xpath('./h1').should('exist') - }) - }) - - it('finds body outside of within-subject when succumbing to // trap', () => { - // first assert that body doesn't actually exist within main - cy.xpath('//main').within(() => { - cy.xpath('.//body').should('not.exist') - }) - - cy.xpath('//main').within(() => { - cy.xpath('//body').should('exist') - }) - }) - }) - - describe('primitives', () => { - it('counts h1 elements', () => { - cy.xpath('count(//h1)').should('equal', 1) - }) - - it('returns h1 text content', () => { - cy.xpath('string(//h1)').should('equal', 'cypress-xpath') - }) - - it('returns boolean', () => { - cy.xpath('boolean(//h1)').should('be.true') - cy.xpath('boolean(//h2)').should('be.false') - }) - }) - - describe('typing', () => { - it('works on text input', () => { - cy.xpath('//*[@id="name"]').type('World') - cy.contains('span#greeting', 'Hello, World') - }) - }) - - describe('clicking', () => { - it('on button', () => { - // this button invokes window.alert when clicked - const alert = cy.stub() - - cy.on('window:alert', alert) - cy.xpath('//*[@id="first-button"]') - .click() - .then(() => { - expect(alert).to.have.been.calledOnce - }) - }) - }) - }) - - context('logging', () => { - beforeEach(() => { - cy.visit('cypress/e2e/index.html') - }) - - it('should log by default', () => { - cy.spy(Cypress, 'log').log(false) - - cy.xpath('//h1').then(() => { - expect(Cypress.log).to.be.calledWithMatch({ name: 'xpath' }) - }) - }) - - it('logs the selector when not found', (done) => { - cy.xpath('//h1') // does exist - cy.on('fail', (e) => { - const isExpectedErrorMessage = (message) => { - return message.includes('Timed out retrying') && - message.includes( - 'Expected to find element: `//h2`, but never found it.', - ) - } - - if (!isExpectedErrorMessage(e.message)) { - console.error('Cypress test failed with an unexpected error message') - console.error(e) - - return done(e) - } - - // no errors, the error message for not found selector is correct - done() - }) - - cy.xpath('//h2', { timeout: 100 }) // does not exist - }) - - it('should not log when provided log: false', () => { - cy.spy(Cypress, 'log').log(false) - - cy.xpath('//h1', { log: false }).then(() => { - expect(Cypress.log).to.not.be.calledWithMatch({ name: 'xpath' }) - }) - }) - }) -}) diff --git a/npm/xpath/cypress/fixtures/example.json b/npm/xpath/cypress/fixtures/example.json deleted file mode 100644 index da18d9352a17..000000000000 --- a/npm/xpath/cypress/fixtures/example.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io", - "body": "Fixtures are a great way to mock data for responses to routes" -} \ No newline at end of file diff --git a/npm/xpath/cypress/support/e2e.js b/npm/xpath/cypress/support/e2e.js deleted file mode 100644 index 10ea61f87d67..000000000000 --- a/npm/xpath/cypress/support/e2e.js +++ /dev/null @@ -1,16 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -import '../../src' diff --git a/npm/xpath/images/cypress-xpath-reference.gif b/npm/xpath/images/cypress-xpath-reference.gif deleted file mode 100644 index 97ebfa8f41d1..000000000000 Binary files a/npm/xpath/images/cypress-xpath-reference.gif and /dev/null differ diff --git a/npm/xpath/package.json b/npm/xpath/package.json deleted file mode 100644 index c4b72bb9ea29..000000000000 --- a/npm/xpath/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@cypress/xpath", - "version": "0.0.0-development", - "description": "Adds XPath command to Cypress.io test runner", - "main": "src", - "scripts": { - "lint": "eslint . --ext .ts,.js", - "cy:run": "node ../../scripts/cypress.js run --e2e", - "cy:open": "node ../../scripts/cypress.js open --e2e --project ${PWD}" - }, - "files": [ - "src" - ], - "types": "src", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/cypress-io/cypress.git" - }, - "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/xpath#readme", - "author": "Cypress Tools Team", - "bugs": { - "url": "https://github.com/cypress-io/cypress/issues" - }, - "keywords": [ - "cypress", - "cypress-io", - "xpath" - ], - "publishConfig": { - "access": "public" - } -} diff --git a/npm/xpath/src/index.d.ts b/npm/xpath/src/index.d.ts deleted file mode 100644 index 346cc72a4ed5..000000000000 --- a/npm/xpath/src/index.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/// - -declare namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - /** - * Get one or more DOM elements by an XPath selector. - * **Note:** you can test XPath expressions from DevTools console using $x(...) function, for example $x('//div') to find all divs. - * @see https://github.com/cypress-io/cypress-xpath - * @example - * cy.xpath(`//ul[@class="todo-list"]//li`) - * .should('have.length', 3) - */ - xpath(selector: string, options?: Partial): Chainable> - } -} diff --git a/npm/xpath/src/index.js b/npm/xpath/src/index.js deleted file mode 100644 index 62001bebf9a7..000000000000 --- a/npm/xpath/src/index.js +++ /dev/null @@ -1,165 +0,0 @@ -/* eslint-disable no-redeclare */ -/// - -/** - * Adds XPath support to Cypress using a custom command. - * - * @see https://devhints.io/xpath - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_using_XPath_in_JavaScript - * @example - ```js - it('finds list items', () => { - cy.xpath('//ul[@class="todo-list"]//li') - .should('have.length', 3) - }) - ``` - */ -const xpath = (subject, selector, options = {}) => { - /* global XPathResult */ - const isNumber = (xpathResult) => { - return xpathResult.resultType === XPathResult.NUMBER_TYPE - } - const numberResult = (xpathResult) => xpathResult.numberValue - - const isString = (xpathResult) => { - return xpathResult.resultType === XPathResult.STRING_TYPE - } - const stringResult = (xpathResult) => xpathResult.stringValue - - const isBoolean = (xpathResult) => { - return xpathResult.resultType === XPathResult.BOOLEAN_TYPE - } - const booleanResult = (xpathResult) => xpathResult.booleanValue - - const isPrimitive = (x) => { - return Cypress._.isNumber(x) || Cypress._.isString(x) || Cypress._.isBoolean(x) - } - - // options to log later - const log = { - name: 'xpath', - message: selector, - } - - if (Cypress.dom.isElement(subject) && subject.length > 1) { - throw new Error( - `xpath() can only be called on a single element. Your subject contained ${ - subject.length - } elements.`, - ) - } - - const getValue = () => { - let nodes = [] - let contextNode - let withinSubject = cy.state('withinSubject') - - if (Cypress.dom.isElement(subject)) { - contextNode = subject[0] - } else if (Cypress.dom.isDocument(subject)) { - contextNode = subject - } else if (withinSubject) { - contextNode = withinSubject[0] - } else { - contextNode = cy.state('window').document - } - - let iterator = (contextNode.ownerDocument || contextNode).evaluate( - selector, - contextNode, - ) - - if (isNumber(iterator)) { - const result = numberResult(iterator) - - log.consoleProps = () => { - return { - XPath: selector, - type: 'number', - result, - } - } - - return result - } - - if (isString(iterator)) { - const result = stringResult(iterator) - - log.consoleProps = () => { - return { - XPath: selector, - type: 'string', - result, - } - } - - return result - } - - if (isBoolean(iterator)) { - const result = booleanResult(iterator) - - log.consoleProps = () => { - return { - XPath: selector, - type: 'boolean', - result, - } - } - - return result - } - - try { - let node = iterator.iterateNext() - - while (node) { - nodes.push(node) - node = iterator.iterateNext() - } - - log.consoleProps = () => { - return { - XPath: selector, - result: nodes.length === 1 ? nodes[0] : nodes, - } - } - - return nodes - } catch (e) { - console.error('Document tree modified during iteration', e) - - return null - } - } - - const resolveValue = () => { - return Cypress.Promise.try(getValue).then((value) => { - if (!isPrimitive(value)) { - value = Cypress.$(value) - // Add the ".selector" property because Cypress uses it for error messages - value.selector = selector - } - - return cy.verifyUpcomingAssertions(value, options, { - onRetry: resolveValue, - }) - }) - } - - return resolveValue().then((value) => { - if (options.log !== false) { - // TODO set found elements on the command log? - Cypress.log(log) - } - - return value - }) -} - -Cypress.Commands.add( - 'xpath', - { prevSubject: ['optional', 'element', 'document'] }, - xpath, -) diff --git a/nx.json b/nx.json new file mode 100644 index 000000000000..95b555823a81 --- /dev/null +++ b/nx.json @@ -0,0 +1,48 @@ +{ + "tasksRunnerOptions": { + "default": { + "runner": "nx-cloud", + "options": { + "cacheableOperations": [ + "lint" + ], + "accessToken": "ZmNlNjA0YzAtNTM1NS00MDIwLWFlMWItNWYxYzNiMjQ4N2VkfHJlYWQtb25seQ==" + } + } + }, + "targetDefaults": { + "lint": { + "inputs": [ + "default", + "{workspaceRoot}/.eslintrc.js" + ] + }, + "build": { + "dependsOn": [ + "^build" + ] + }, + "build-prod": { + "dependsOn": [ + "^build-prod" + ] + } + }, + "namedInputs": { + "sharedGlobals": [], + "default": [ + "{projectRoot}/**/*", + "sharedGlobals" + ], + "production": [ + "default", + "!{projectRoot}/**/*.spec.ts", + "!{projectRoot}/**/*.md", + "!{projectRoot}/tsconfig.spec.json", + "!{projectRoot}/.eslintrc.json", + "!{projectRoot}/.mocharc.{js,json}", + "!{projectRoot/cypress.config.{ts,js}", + "!{projectRoot/**/*.cy.ts" + ] + } +} diff --git a/package.json b/package.json index 431178769b15..f8b22384f38d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "12.11.0", + "version": "12.14.0", "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": { @@ -13,7 +13,7 @@ "binary-upload": "node ./scripts/binary.js upload", "binary-zip": "node ./scripts/binary.js zip", "build": "yarn build-npm-modules && lerna run build --stream --no-bail --ignore create-cypress-tests --ignore cypress --ignore \"'@packages/{runner}'\" --ignore \"'@cypress/{angular,react,react18,vue,vue2,mount-utils,svelte}'\" && node ./cli/scripts/post-build.js && lerna run build --stream --scope create-cypress-tests", - "build-npm-modules": "lerna run build --scope cypress --scope @cypress/mount-utils && lerna run build --scope \"'@cypress/{angular,react,react18,vue,vue2,svelte}'\"", + "build-npm-modules": "lerna run build --scope cypress --scope @cypress/mount-utils --scope @cypress/react && lerna run build --scope \"'@cypress/{angular,react18,vue,vue2,svelte}'\"", "build-prod": "lerna run build-prod-ui --stream && lerna run build-prod --stream --ignore create-cypress-tests && node ./cli/scripts/post-build.js && lerna run build-prod --stream --scope create-cypress-tests --scope", "build-v8-snapshot-dev": "node --max-old-space-size=8192 tooling/v8-snapshot/scripts/setup-v8-snapshot-in-cypress.js --env=dev", "build-v8-snapshot-prod": "node --max-old-space-size=8192 tooling/v8-snapshot/scripts/setup-v8-snapshot-in-cypress.js", @@ -41,7 +41,7 @@ "ensure-deps": "./scripts/ensure-dependencies.sh", "get-next-version": "node scripts/get-next-version.js", "postinstall": "node ./scripts/run-postInstall.js", - "lint": "lerna run lint --no-bail --concurrency 2 && eslint --ext .js,.ts,.json, scripts", + "lint": "lerna run lint --no-bail --concurrency 2", "prepare-release-artifacts": "node ./scripts/prepare-release-artifacts.js", "npm-release": "node scripts/npm-release.js", "prestart": "yarn ensure-deps", @@ -85,6 +85,7 @@ "@graphql-tools/delegate": "8.2.1", "@graphql-tools/utils": "8.2.3", "@graphql-tools/wrap": "8.1.1", + "@nrwl/nx-cloud": "16.0.5", "@octokit/auth-app": "3.6.1", "@octokit/core": "3.6.0", "@percy/cli": "1.2.0", @@ -119,7 +120,6 @@ "@typescript-eslint/eslint-plugin": "4.18.0", "@typescript-eslint/parser": "4.18.0", "@urql/introspection": "^0.3.0", - "arg": "4.1.2", "ascii-table": "0.0.9", "autobarrel": "^1.1.0", "aws-sdk": "2.814.0", @@ -158,7 +158,6 @@ "filesize": "4.1.2", "fs-extra": "9.1.0", "getenv": "^1.0.0", - "gift": "0.10.2", "glob": "7.1.6", "got": "11.8.5", "graphql": "^15.5.1", @@ -174,7 +173,7 @@ "inquirer": "3.3.0", "inquirer-confirm": "2.0.3", "lazy-ass": "1.6.0", - "lerna": "3.22.1", + "lerna": "5.4.3", "lint-staged": "11.1.2", "listr2": "3.8.3", "lodash": "^4.17.21", @@ -195,7 +194,6 @@ "semantic-release-monorepo": "7.0.3", "semver": "7.3.2", "shelljs": "0.8.5", - "shx": "0.3.3", "sinon": "7.3.2", "snap-shot-it": "7.9.3", "start-server-and-test": "1.10.8", @@ -246,7 +244,8 @@ "packages/*", "npm/*", "tooling/*", - "system-tests" + "system-tests", + "scripts" ], "nohoist": [ "**/webpack-preprocessor/babel-loader", diff --git a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts index 1971468fd7e9..ed0cb53db0f7 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts @@ -1,7 +1,7 @@ import type { SinonStub } from 'sinon' import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' import { getPathForPlatform } from '../../src/paths' -import { snapshotAUTPanel } from './support/snapshot-aut-panel' +// import { snapshotAUTPanel } from './support/snapshot-aut-panel' describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: 10000 }, () => { context('default config', () => { @@ -24,19 +24,22 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: cy.contains('Canary').should('be.visible') cy.findByTestId('viewport').click() - snapshotAUTPanel('browsers open') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('browsers open') cy.contains('Canary').should('be.hidden') cy.contains('The viewport determines the width and height of your application under test. By default the viewport will be 500px by 500px for component testing.') .should('be.visible') - snapshotAUTPanel('viewport info open') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('viewport info open') cy.get('body').click() cy.findByTestId('playground-activator').click() cy.findByTestId('playground-selector').clear().type('[data-cy-root]') - snapshotAUTPanel('cy.get selector') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('cy.get selector') cy.findByTestId('playground-num-elements').contains('1 match') @@ -50,7 +53,8 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: cy.findByTestId('playground-selector').clear().type('Component Test') - snapshotAUTPanel('cy.contains selector') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('cy.contains selector') cy.findByTestId('playground-num-elements').contains('1 match') diff --git a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts index 937fe12ecb2f..677feff1e164 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts @@ -1,5 +1,5 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' -import { snapshotAUTPanel } from './support/snapshot-aut-panel' +// import { snapshotAUTPanel } from './support/snapshot-aut-panel' import { getPathForPlatform } from '../../src/paths' describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: 10000 }, () => { @@ -28,7 +28,8 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: .focus() .type('{esc}') - snapshotAUTPanel('browsers open') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('browsers open') cy.contains('Canary').should('be.hidden') @@ -36,14 +37,16 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: cy.contains('The viewport determines the width and height of your application under test. By default the viewport will be 1000px by 660px for end-to-end testing.') .should('be.visible') - snapshotAUTPanel('viewport info open') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('viewport info open') cy.get('body').click() cy.findByTestId('playground-activator').click() cy.findByTestId('playground-selector').clear().type('li') - snapshotAUTPanel('cy.get selector') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('cy.get selector') cy.findByTestId('playground-num-elements').contains('3 matches') @@ -64,7 +67,8 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: cy.findByTestId('playground-selector').clear().type('Item 1') - snapshotAUTPanel('cy.contains selector') + // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 + // snapshotAUTPanel('cy.contains selector') cy.findByTestId('playground-num-elements').contains('1 match') diff --git a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts index d49d3bf8aaba..5e553e0e9ddd 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts @@ -395,4 +395,37 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 expect(ctx.actions.project.initializeActiveProject).to.be.called }) }) + + describe('runSpec mutation', () => { + it('should trigger expected spec from POST', () => { + startAtSpecsPage('e2e') + + cy.contains('E2E specs').should('be.visible') + + cy.withCtx(async (ctx) => { + const url = `http://127.0.0.1:${ctx.gqlServerPort}/__launchpad/graphql?` + const payload = `{"query":"mutation{\\nrunSpec(specPath:\\"cypress/e2e/dom-content.spec.js\\"){\\n__typename\\n... on RunSpecResponse{\\ntestingType\\nbrowser{\\nid\\nname\\n}\\nspec{\\nid\\nname\\n}\\n}\\n}\\n}","variables":null}` + + ctx.coreData.app.browserStatus = 'open' + + /* + Note: If this test starts failing, this fetch is the likely culprit. + Validate the GQL payload above is still valid by logging the fetch response JSON + */ + + await ctx.util.fetch( + url, + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: payload, + }, + ) + }) + + cy.contains('Dom Content').should('be.visible') + }) + }) }) diff --git a/packages/app/cypress/e2e/runner/cloud-debug-filter.cy.ts b/packages/app/cypress/e2e/runner/cloud-debug-filter.cy.ts index 38da4f465330..88906b157cd9 100644 --- a/packages/app/cypress/e2e/runner/cloud-debug-filter.cy.ts +++ b/packages/app/cypress/e2e/runner/cloud-debug-filter.cy.ts @@ -55,12 +55,6 @@ describe('cloud debug test filtering', () => { cy.get('@reporterPanel').then((el) => el.width(500)) cy.get('@reporterPanel').percySnapshot('wide') - cy.get('@reporterPanel').then((el) => el.width(350)) - cy.get('@reporterPanel').percySnapshot('medium') - - cy.get('@reporterPanel').then((el) => el.width(250)) - cy.get('@reporterPanel').percySnapshot('narrow') - cy.get('@reporterPanel').then((el) => el.width(150)) cy.get('@reporterPanel').percySnapshot('skinny') }) diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index f9df449949c4..30c1d5e69f87 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -2,6 +2,10 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' import type { SinonStub } from 'sinon' function moveToRunsPage (): void { + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') + }) + cy.findByTestId('sidebar-link-runs-page').click() cy.findByTestId('app-header-bar').findByText('Runs').should('be.visible') cy.findByTestId('runs-container').should('be.visible') @@ -34,6 +38,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.scaffoldProject('component-tests') cy.openProject('component-tests') cy.startAppServer('component') + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') + }) }) it('resolves the runs page', () => { @@ -99,7 +106,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { moveToRunsPage() cy.contains('a', 'OVERLIMIT').click() - cy.withCtx((ctx) => { + cy.withCtx((ctx, o) => { expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.contain('http://dummy.cypress.io/runs/4') }) }) @@ -315,6 +322,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.withCtx(async (ctx, o) => { o.sinon.spy(ctx.cloud, 'executeRemoteGraphQL') + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') const config = await ctx.project.getConfig() expect(config.projectId).to.not.equal('newProjectId') @@ -323,7 +331,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { moveToRunsPage() cy.findByText(defaultMessages.runs.connect.buttonProject).click() cy.contains('button', defaultMessages.runs.connect.modal.selectProject.createProject).click() - cy.findByText(defaultMessages.runs.connectSuccessAlert.title, { timeout: 10000 }).should('be.visible') + cy.findByText(defaultMessages.runs.connectSuccessAlert.title, { timeout: 10000 }).scrollIntoView().should('be.visible') cy.withCtx(async (ctx) => { const config = await ctx.project.getConfig() @@ -577,7 +585,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { }) }) - context('Runs - No Runs', () => { + context('Runs - No Runs', { viewportWidth: 1280 }, () => { it('when no runs and not connected, shows connect to Cypress Cloud button', () => { cy.scaffoldProject('component-tests') cy.openProject('component-tests', ['--config-file', 'cypressWithoutProjectId.config.js']) @@ -602,7 +610,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { it('displays how to record prompt when connected and no runs in Component Testing', () => { scaffoldTestingTypeAndVisitRunsPage('component') cy.contains(defaultMessages.runs.empty.title).should('be.visible') - cy.contains(defaultMessages.runs.empty.description).should('be.visible') cy.findByDisplayValue('npx cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible') }) @@ -610,7 +617,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { scaffoldTestingTypeAndVisitRunsPage('e2e') cy.contains(defaultMessages.runs.empty.title).should('be.visible') - cy.contains(defaultMessages.runs.empty.description).should('be.visible') cy.findByDisplayValue('npx cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible') }) @@ -759,6 +765,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { }) it('should remove the alert warning if the app reconnects to the internet', () => { + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') + }) + cy.loginUser() cy.visitApp() cy.wait(1000) diff --git a/packages/app/cypress/e2e/specs_list_e2e.cy.ts b/packages/app/cypress/e2e/specs_list_e2e.cy.ts index b44e023ed40f..b3309e40c975 100644 --- a/packages/app/cypress/e2e/specs_list_e2e.cy.ts +++ b/packages/app/cypress/e2e/specs_list_e2e.cy.ts @@ -41,7 +41,7 @@ describe('App: Spec List (E2E)', () => { }) cy.visitApp() - cy.contains('E2E specs') + cy.verifyE2ESelected() } const clearSearchAndType = (search: string) => { @@ -67,10 +67,6 @@ describe('App: Spec List (E2E)', () => { cy.findByTestId('app-header-bar').findByText('Specs').should('be.visible') }) - it('shows the "E2E specs" label as the header for the Spec Name column', () => { - cy.findByTestId('specs-testing-type-header').should('contain', 'E2E specs') - }) - it('shows a git status for each spec', () => { cy.findAllByTestId('git-info-row').each((row) => { cy.wrap(row).find('svg').should('have.length', 1) @@ -85,6 +81,18 @@ describe('App: Spec List (E2E)', () => { cy.findAllByTestId('spec-item').should('contain', 'dom-content.spec.js') }) + it('lists files after folders when in same directory', () => { + cy.findAllByTestId('row-directory-depth-2').first().click() + + const rowId = getPathForPlatform('speclist-cypress/e2e/admin_users/').replaceAll('\\', '\\\\') + + cy.get(`[id="${rowId}"]`) + .next() + .should('contain', 'admin.user') + .next() + .should('contain', 'admin_users_list.spec.js') + }) + it('opens the "Create new spec" modal after clicking the "New specs" button', () => { cy.findByTestId('standard-modal').should('not.exist') cy.findByTestId('new-spec-button').click() diff --git a/packages/app/cypress/e2e/specs_list_flaky.cy.ts b/packages/app/cypress/e2e/specs_list_flaky.cy.ts index c6419bb1b0cd..b3c1b34285cb 100644 --- a/packages/app/cypress/e2e/specs_list_flaky.cy.ts +++ b/packages/app/cypress/e2e/specs_list_flaky.cy.ts @@ -93,7 +93,7 @@ describe('App: Spec List - Flaky Indicator', () => { }) cy.visitApp() - cy.contains('E2E specs') + cy.verifyE2ESelected() }) it('shows the "Flaky" badge on specs considered flaky', () => { diff --git a/packages/app/cypress/e2e/specs_list_switcher.cy.ts b/packages/app/cypress/e2e/specs_list_switcher.cy.ts new file mode 100644 index 000000000000..6bade8ff5036 --- /dev/null +++ b/packages/app/cypress/e2e/specs_list_switcher.cy.ts @@ -0,0 +1,95 @@ +describe('App: Spec List Testing Type Switcher', () => { + context('ct unconfigured', () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.openProject('cypress-in-cypress') + + cy.withCtx(async (ctx, o) => { + const config = await ctx.file.readFileInProject('cypress.config.js') + const newCypressConfig = config.replace(`component:`, `_component:`) + + await ctx.actions.file.writeFileInProject('cypress.config.js', newCypressConfig) + }) + + cy.startAppServer('e2e') + + cy.visitApp() + cy.verifyE2ESelected() + }) + + it('switches testing types', () => { + cy.findByTestId('testing-type-switch').within(() => { + cy.findByText('Component').click() + }) + + cy.verifyCtSelected() + + cy.contains('Component testing is not set up for this project') + + cy.findByTestId('testing-type-setup-button').should('be.visible') + }) + }) + + context('e2e unconfigured', () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.openProject('cypress-in-cypress') + + cy.withCtx(async (ctx, o) => { + const config = await ctx.file.readFileInProject('cypress.config.js') + const newCypressConfig = config.replace(`e2e:`, `_e2e:`) + + await ctx.actions.file.writeFileInProject('cypress.config.js', newCypressConfig) + }) + + cy.startAppServer('component') + + cy.visitApp() + cy.verifyCtSelected() + }) + + it('switches testing types', () => { + cy.findByTestId('testing-type-switch').within(() => { + cy.findByText('E2E').click() + }) + + cy.contains('End-to-end testing is not set up for this project') + + cy.findByTestId('testing-type-setup-button').should('be.visible') + }) + }) + + context('both testing types configured', () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.findBrowsers() + cy.openProject('cypress-in-cypress') + + cy.startAppServer('component') + + cy.visitApp() + cy.verifyCtSelected() + }) + + it('displays expected switch content', () => { + cy.findByTestId('unconfigured-icon').should('not.exist') + + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.actions.project, 'setAndLoadCurrentTestingType') + o.sinon.stub(ctx.actions.project, 'reconfigureProject').resolves() + }) + + cy.findByTestId('testing-type-switch').within(() => { + cy.findByText('E2E').click() + }) + + cy.verifyE2ESelected() + + cy.withCtx((ctx) => { + expect(ctx.coreData.app.relaunchBrowser).eq(true) + expect(ctx.actions.project.setAndLoadCurrentTestingType).to.have.been.calledWith('e2e') + expect(ctx.actions.project.reconfigureProject).to.have.been.called + }) + }) + }) +}) diff --git a/packages/app/cypress/e2e/support/e2eSupport.ts b/packages/app/cypress/e2e/support/e2eSupport.ts index 1c9f27434e1a..97620266165a 100644 --- a/packages/app/cypress/e2e/support/e2eSupport.ts +++ b/packages/app/cypress/e2e/support/e2eSupport.ts @@ -16,3 +16,21 @@ beforeEach(() => { // @ts-ignore - dynamically defined during tests using expect(window.top?.getEventManager().autDestroyedCount).to.be.undefined }) + +function e2eTestingTypeIsSelected () { + cy.findByTestId('specs-testing-type-header').within(() => { + cy.findByTestId('testing-type-switch').contains('button', 'E2E').should('have.attr', 'aria-selected', 'true') + cy.findByTestId('testing-type-switch').contains('button', 'Component').should('not.have.attr', 'aria-selected') + }) +} + +Cypress.Commands.add('verifyE2ESelected', e2eTestingTypeIsSelected) + +function ctTestingTypeIsSelected () { + cy.findByTestId('specs-testing-type-header').within(() => { + cy.findByTestId('testing-type-switch').contains('button', 'E2E').should('not.have.attr', 'aria-selected') + cy.findByTestId('testing-type-switch').contains('button', 'Component').should('have.attr', 'aria-selected', 'true') + }) +} + +Cypress.Commands.add('verifyCtSelected', ctTestingTypeIsSelected) diff --git a/packages/app/cypress/e2e/support/execute-spec.ts b/packages/app/cypress/e2e/support/execute-spec.ts index fe0575619fdc..b54013700722 100644 --- a/packages/app/cypress/e2e/support/execute-spec.ts +++ b/packages/app/cypress/e2e/support/execute-spec.ts @@ -18,6 +18,8 @@ declare global { * */ waitForSpecToFinish(expectedResults?: ExpectedResults, timeout?: number): void + verifyE2ESelected(): void + verifyCtSelected(): void } } } diff --git a/packages/app/cypress/e2e/top-nav.cy.ts b/packages/app/cypress/e2e/top-nav.cy.ts index 9cf84d4eede2..46e776dab7fb 100644 --- a/packages/app/cypress/e2e/top-nav.cy.ts +++ b/packages/app/cypress/e2e/top-nav.cy.ts @@ -699,7 +699,7 @@ describe('Growth Prompts Can Open Automatically', () => { ) cy.visitApp() - cy.contains('E2E specs') + cy.verifyE2ESelected() cy.wait(1000) cy.contains('Configure CI').should('be.visible') }) @@ -717,7 +717,7 @@ describe('Growth Prompts Can Open Automatically', () => { ) cy.visitApp() - cy.contains('E2E specs') + cy.verifyE2ESelected() cy.wait(1000) cy.contains('Configure CI').should('not.exist') }) diff --git a/packages/app/package.json b/packages/app/package.json index 36cc45a7f49a..8d76dda2c293 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,8 +20,10 @@ }, "dependencies": {}, "devDependencies": { - "@cypress-design/vue-icon": "0.20.0", - "@cypress-design/vue-statusicon": "0.3.0", + "@cypress-design/vue-button": "^0.10.1", + "@cypress-design/vue-icon": "^0.23.1", + "@cypress-design/vue-statusicon": "^0.4.7", + "@cypress-design/vue-tabs": "^0.5.1", "@graphql-typed-document-node/core": "^3.1.0", "@headlessui/vue": "1.4.0", "@iconify/iconify": "2.1.2", @@ -30,7 +32,7 @@ "@intlify/unplugin-vue-i18n": "0.10.0", "@packages/frontend-shared": "0.0.0-development", "@packages/telemetry": "0.0.0-development", - "@percy/cypress": "^3.1.0", + "@percy/cypress": "^3.1.2", "@popperjs/core": "2.11.6", "@testing-library/cypress": "9.0.0", "@types/faker": "5.5.8", diff --git a/packages/app/src/assets/debug-guide-skeleton-1.png b/packages/app/src/assets/debug-guide-skeleton-1.png deleted file mode 100644 index 2abb50e0bb50..000000000000 Binary files a/packages/app/src/assets/debug-guide-skeleton-1.png and /dev/null differ diff --git a/packages/app/src/assets/debug-guide-skeleton-1.svg b/packages/app/src/assets/debug-guide-skeleton-1.svg new file mode 100644 index 000000000000..8d5c836e53b4 --- /dev/null +++ b/packages/app/src/assets/debug-guide-skeleton-1.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/debug-guide-skeleton-2.png b/packages/app/src/assets/debug-guide-skeleton-2.png deleted file mode 100644 index 44d36ccbe60b..000000000000 Binary files a/packages/app/src/assets/debug-guide-skeleton-2.png and /dev/null differ diff --git a/packages/app/src/assets/debug-guide-skeleton-2.svg b/packages/app/src/assets/debug-guide-skeleton-2.svg new file mode 100644 index 000000000000..611e8f2ef594 --- /dev/null +++ b/packages/app/src/assets/debug-guide-skeleton-2.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/debug-guide-skeleton-3.png b/packages/app/src/assets/debug-guide-skeleton-3.png deleted file mode 100644 index 9b7957130f2f..000000000000 Binary files a/packages/app/src/assets/debug-guide-skeleton-3.png and /dev/null differ diff --git a/packages/app/src/assets/debug-guide-skeleton-3.svg b/packages/app/src/assets/debug-guide-skeleton-3.svg new file mode 100644 index 000000000000..83cc61d919ee --- /dev/null +++ b/packages/app/src/assets/debug-guide-skeleton-3.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/src/assets/debug-guide-text-1.png b/packages/app/src/assets/debug-guide-text-1.png deleted file mode 100644 index 0c3dbeecffda..000000000000 Binary files a/packages/app/src/assets/debug-guide-text-1.png and /dev/null differ diff --git a/packages/app/src/assets/debug-guide-text-2.png b/packages/app/src/assets/debug-guide-text-2.png deleted file mode 100644 index 4a7dd371a1cd..000000000000 Binary files a/packages/app/src/assets/debug-guide-text-2.png and /dev/null differ diff --git a/packages/app/src/assets/debug-guide-text-3.png b/packages/app/src/assets/debug-guide-text-3.png deleted file mode 100644 index 3a1928737037..000000000000 Binary files a/packages/app/src/assets/debug-guide-text-3.png and /dev/null differ diff --git a/packages/app/src/assets/record-guide.svg b/packages/app/src/assets/record-guide.svg new file mode 100644 index 000000000000..d0561697a6d8 --- /dev/null +++ b/packages/app/src/assets/record-guide.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/runs-guide-skeleton-1.svg b/packages/app/src/assets/runs-guide-skeleton-1.svg new file mode 100644 index 000000000000..3ec7a6ad2223 --- /dev/null +++ b/packages/app/src/assets/runs-guide-skeleton-1.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/runs-guide-skeleton-2.svg b/packages/app/src/assets/runs-guide-skeleton-2.svg new file mode 100644 index 000000000000..566ca8059b33 --- /dev/null +++ b/packages/app/src/assets/runs-guide-skeleton-2.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/assets/runs-guide-skeleton-3.svg b/packages/app/src/assets/runs-guide-skeleton-3.svg new file mode 100644 index 000000000000..0d439f56cbaa --- /dev/null +++ b/packages/app/src/assets/runs-guide-skeleton-3.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/app/src/components/Blank.cy.tsx b/packages/app/src/components/Blank.cy.tsx index 8acc0740ca85..8f47bde2f27b 100644 --- a/packages/app/src/components/Blank.cy.tsx +++ b/packages/app/src/components/Blank.cy.tsx @@ -14,19 +14,19 @@ describe('initial', () => { }) describe('testIsolationBlankPage', () => { - it('works', () => { + beforeEach(() => { getContainerEl()!.innerHTML = testIsolationBlankPage() - cy.get('[data-cy="cypress-logo"]') cy.get('[data-cy="text"]').should('have.text', 'Default blank page') cy.get('[data-cy="subtext"]').should('have.text', 'This page was cleared by navigating to about:blank.All active session data (cookies, localStorage and sessionStorage) across all domains are cleared.') + }) + it('works', () => { cy.percySnapshot() }) it('works with small viewport', () => { cy.viewport(200, 500) - getContainerEl()!.innerHTML = testIsolationBlankPage() cy.percySnapshot() }) @@ -35,13 +35,15 @@ describe('testIsolationBlankPage', () => { describe('visitFailure', () => { it('works', () => { getContainerEl()!.innerHTML = visitFailure({ url: 'http://foo.cypress.io' }) - - cy.percySnapshot() }) it('works with details', () => { getContainerEl()!.innerHTML = visitFailure({ url: 'http://foo.cypress.io', status: 404, statusText: 'Not Found', contentType: 'text/html' }) + cy.get('p').contains('Sorry, we could not load:') + cy.get('a').contains('http://foo.cypress.io').should('have.attr', 'href', 'http://foo.cypress.io') + cy.get('p').contains('404 - Not Found (text/html)') + cy.percySnapshot() }) }) diff --git a/packages/app/src/components/Slideshow.vue b/packages/app/src/components/Slideshow.vue index 2d08e039316c..b6a34bb04008 100644 --- a/packages/app/src/components/Slideshow.vue +++ b/packages/app/src/components/Slideshow.vue @@ -10,48 +10,33 @@ leave-active-class="transition duration-300 ease-in absolute" leave-to-class="opacity-0 absolute" > - diff --git a/packages/app/src/components/SpecPatterns.cy.tsx b/packages/app/src/components/SpecPatterns.cy.tsx index ef94d7cac8b1..e19b395aeb80 100644 --- a/packages/app/src/components/SpecPatterns.cy.tsx +++ b/packages/app/src/components/SpecPatterns.cy.tsx @@ -9,8 +9,6 @@ describe('', () => { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.cy.{js,jsx,ts,tsx}') cy.get('[data-cy="file-match-indicator"]').contains('50 matches') - - cy.percySnapshot() }) it('renders component spec pattern', () => { diff --git a/packages/app/src/components/promo/Promo.cy.tsx b/packages/app/src/components/promo/Promo.cy.tsx new file mode 100644 index 000000000000..67f0861726e9 --- /dev/null +++ b/packages/app/src/components/promo/Promo.cy.tsx @@ -0,0 +1,126 @@ +import Promo from './Promo.vue' +import PromoCard from './PromoCard.vue' +import PromoAction from './PromoAction.vue' +import PromoHeader from './PromoHeader.vue' +import { IconActionRestart, IconChevronRightSmall } from '@cypress-design/vue-icon' +import { Promo_PromoSeenDocument } from '../../generated/graphql-test' + +describe('', () => { + const renderPromo = () => { + const recordEvent = cy.stub().as('recordEvent') + + cy.stubMutationResolver(Promo_PromoSeenDocument, (defineResult, args) => { + recordEvent(args) + + return defineResult({ recordEvent: true }) + }) + + cy.mount( + + {{ + header: () => ( + + {{ + description: () => ( +

Description...

+ ), + content: () => ( +

_content_

+ ), + }} +
+ ), + cards: ({ step, goForward, reset }) => { + if (step === 0) { + return ( + + {{ + image: () => ( +
Image
+ ), + action: () => ( + + ), + }} +
+ ) + } + + if (step === 1) { + return ( + + {{ + image: () => ( +
Image
+ ), + action: () => ( + + ), + }} +
+ ) + } + + return null + }, + }} +
, + ) + } + + beforeEach(() => { + renderPromo() + }) + + it('renders properly on narrow viewport', { viewportWidth: 600 }, () => { + cy.percySnapshot() + }) + + it('renders properly on wide viewport', { viewportWidth: 1280 }, () => { + cy.percySnapshot() + }) + + it('records event on initial render', () => { + cy.get('@recordEvent').should('have.been.calledOnce') + cy.get('@recordEvent').should('have.been.calledWith', { + campaign: '_campaign_', + medium: '_medium_', + cohort: null, + messageId: Cypress.sinon.match.string, + }) + }) + + describe('controls', () => { + it('handles `goForward`', () => { + cy.contains('Card #1').should('be.visible') + cy.findByTestId('promo-action-control').click() + cy.contains('Card #2').should('be.visible') + }) + + it('handles `reset`', () => { + cy.findByTestId('promo-action-control').click() + cy.contains('Card #2').should('be.visible') + cy.findByTestId('promo-action-control').should('contain.text', 'Reset') + cy.findByTestId('promo-action-control').click() + cy.contains('Card #1').should('be.visible') + }) + }) +}) diff --git a/packages/app/src/components/promo/Promo.vue b/packages/app/src/components/promo/Promo.vue new file mode 100644 index 000000000000..5d9358403fab --- /dev/null +++ b/packages/app/src/components/promo/Promo.vue @@ -0,0 +1,72 @@ + + + diff --git a/packages/app/src/components/promo/PromoAction.cy.tsx b/packages/app/src/components/promo/PromoAction.cy.tsx new file mode 100644 index 000000000000..c22a1b53ebcb --- /dev/null +++ b/packages/app/src/components/promo/PromoAction.cy.tsx @@ -0,0 +1,43 @@ +import PromoAction from './PromoAction.vue' +import { IconChevronRightSmall, IconActionRestart } from '@cypress-design/vue-icon' + +describe('', () => { + it('left label, right icon', () => { + const action = cy.stub().as('action') + + cy.mount( + , + ) + + cy.get('@action').should('not.have.been.called') + + cy.findByTestId('promo-action-control').click() + + cy.get('@action').should('have.been.calledOnce') + }) + + it('left icon, right label', () => { + const action = cy.stub().as('action') + + cy.mount( + , + ) + + cy.get('@action').should('not.have.been.called') + + cy.findByTestId('promo-action-control').click() + + cy.get('@action').should('have.been.calledOnce') + }) +}) diff --git a/packages/app/src/components/promo/PromoAction.vue b/packages/app/src/components/promo/PromoAction.vue new file mode 100644 index 000000000000..21cad8824d85 --- /dev/null +++ b/packages/app/src/components/promo/PromoAction.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/app/src/components/promo/PromoCard.vue b/packages/app/src/components/promo/PromoCard.vue new file mode 100644 index 000000000000..ce1c13bdf408 --- /dev/null +++ b/packages/app/src/components/promo/PromoCard.vue @@ -0,0 +1,54 @@ + + + diff --git a/packages/app/src/components/promo/PromoHeader.cy.tsx b/packages/app/src/components/promo/PromoHeader.cy.tsx new file mode 100644 index 000000000000..802cab83e5b8 --- /dev/null +++ b/packages/app/src/components/promo/PromoHeader.cy.tsx @@ -0,0 +1,17 @@ +import PromoHeader from './PromoHeader.vue' +import Button from '@cypress-design/vue-button' + +describe('', () => { + it('renders', () => { + cy.mount( +

Description of this header

, + control: () => , + content: () =>
Test Content
, + }} + />, + ) + }) +}) diff --git a/packages/app/src/components/promo/PromoHeader.vue b/packages/app/src/components/promo/PromoHeader.vue new file mode 100644 index 000000000000..01c41a3f0ec2 --- /dev/null +++ b/packages/app/src/components/promo/PromoHeader.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/app/src/composables/useRelevantRun.ts b/packages/app/src/composables/useRelevantRun.ts index 25e402779953..549fbf8acdf7 100644 --- a/packages/app/src/composables/useRelevantRun.ts +++ b/packages/app/src/composables/useRelevantRun.ts @@ -1,6 +1,6 @@ import { gql, useSubscription } from '@urql/vue' import { Debug_RelevantRuns_SubscriptionDocument, Sidebar_RelevantRuns_SubscriptionDocument } from '@packages/app/src/generated/graphql' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' import { computed } from 'vue' import { uniq } from 'lodash' @@ -43,10 +43,10 @@ gql` ` export function useRelevantRun (location: 'SIDEBAR' | 'DEBUG') { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() const shouldPause = computed(() => { - return !loginConnectStore.project.isProjectConnected + return !userProjectStatusStore.project.isProjectConnected }) //Switch the subscription query depending on where it was registered from diff --git a/packages/app/src/composables/useTestingType.cy.tsx b/packages/app/src/composables/useTestingType.cy.tsx new file mode 100644 index 000000000000..289e4cc2b169 --- /dev/null +++ b/packages/app/src/composables/useTestingType.cy.tsx @@ -0,0 +1,119 @@ +import { UseTestingType_ActivateTestingTypeDocument } from '../generated/graphql' +import { useTestingType } from './useTestingType' +import type { FunctionalComponent } from 'vue' + +describe('useTestingType', () => { + type ComposableWrapperProps = { useComposable: () => R, callback: (result: R) => void } + + const ComposableWrapper: FunctionalComponent> = ({ useComposable, callback }: ComposableWrapperProps) => { + const result = useComposable() + + callback(result) + + return
Composable
+ } + + const mountComposable = (composable: () => any) => { + const callback = cy.stub().as('callback') + + cy.mount({ + name: 'composable', + render () { + return ( + + ) + }, + }) + + return cy.get('@callback').should('have.been.called').then((cb) => { + return cy.wrap(callback.getCall(callback.callCount - 1).args[0]).as('result') + }) + } + + beforeEach(() => { + const activateTestingTypeStub = cy.stub().as('activateTestingType') + + cy.stubMutationResolver(UseTestingType_ActivateTestingTypeDocument, (defineResult, args) => { + activateTestingTypeStub() + + return defineResult({ switchTestingTypeAndRelaunch: true }) + }) + + cy.gqlStub.Query.currentProject = { + id: 'abc123', + currentTestingType: 'e2e', + isCTConfigured: false, + isE2EConfigured: false, + } as any + }) + + it('supplies expected query data', () => { + mountComposable(useTestingType).then((value) => { + const result = value as unknown as ReturnType + + expect(result.activeTestingType.value).to.eql('e2e') + expect(result.isCTConfigured.value).to.eql(false) + expect(result.isE2EConfigured.value).to.eql(false) + expect(result.viewedTestingType.value).to.eql('e2e') + }) + }) + + describe('viewTestingType', () => { + context('target mode is not configured', () => { + beforeEach(() => { + cy.gqlStub.Query.currentProject = { + id: 'abc123', + currentTestingType: 'e2e', + isCTConfigured: false, + } as any + }) + + it('should toggle viewed mode', () => { + mountComposable(useTestingType).then((value) => { + const result = value as unknown as ReturnType + + expect(result.viewedTestingType.value).to.eql('e2e') + + result.viewTestingType('component') + + expect(result.viewedTestingType.value).to.eql('component') + }) + }) + }) + + context('target mode is configured', () => { + beforeEach(() => { + cy.gqlStub.Query.currentProject = { + id: 'abc123', + currentTestingType: 'e2e', + isE2EConfigured: true, + isCTConfigured: true, + } as any + }) + + it('should toggle active mode if not active mode', () => { + mountComposable(useTestingType).then((value) => { + const result = value as unknown as ReturnType + + expect(result.viewedTestingType.value).to.eql('e2e') + + result.viewTestingType('component') + }) + + cy.get('@activateTestingType').should('have.been.calledOnce') + }) + + it('should toggle viewed mode if active mode', () => { + mountComposable(useTestingType).then((value) => { + const result = value as unknown as ReturnType + + expect(result.viewedTestingType.value).to.eql('e2e') + + result.viewTestingType('e2e') + }) + + cy.get('@activateTestingType').should('not.have.been.called') + }) + }) + }) +}) diff --git a/packages/app/src/composables/useTestingType.ts b/packages/app/src/composables/useTestingType.ts new file mode 100644 index 000000000000..8785af843695 --- /dev/null +++ b/packages/app/src/composables/useTestingType.ts @@ -0,0 +1,72 @@ +import { useMutation, useQuery, gql } from '@urql/vue' +import { computed, ref, watchEffect } from 'vue' +import { UseTestingType_TestingTypeDocument, UseTestingType_ActivateTestingTypeDocument } from '../generated/graphql' + +gql` +query UseTestingType_TestingType { + currentProject { + id + currentTestingType + isCTConfigured + isE2EConfigured + } +} +` + +gql` +mutation UseTestingType_ActivateTestingType($testingType: TestingTypeEnum!) { + switchTestingTypeAndRelaunch(testingType: $testingType) +} +` + +export function useTestingType () { + const query = useQuery({ query: UseTestingType_TestingTypeDocument }) + const activateTestingTypeMutation = useMutation(UseTestingType_ActivateTestingTypeDocument) + const viewedTestingType = ref<'e2e' | 'component' | null>(null) + + const activeTestingType = computed(() => query.data.value?.currentProject?.currentTestingType) + const isCTConfigured = computed(() => query.data.value?.currentProject?.isCTConfigured) + const isE2EConfigured = computed(() => query.data.value?.currentProject?.isE2EConfigured) + + const showTestingTypePromo = computed(() => { + // confirm required data has resolved from other computed values + if (isE2EConfigured.value === undefined && isCTConfigured.value === undefined) { + return false + } + + return (viewedTestingType.value === 'e2e' && isE2EConfigured.value === false) || + (viewedTestingType.value === 'component' && isCTConfigured.value === false) + }) + + // Initialize 'viewed' testing type when query first resolves + watchEffect(() => { + if (!!activeTestingType.value && !viewedTestingType.value) { + viewedTestingType.value = activeTestingType.value + } + }) + + async function activateTestingType (testingType: 'e2e' | 'component') { + return await activateTestingTypeMutation.executeMutation({ testingType }) + } + + async function viewTestingType (testingType: 'e2e' | 'component') { + const switchingBackToActiveMode = testingType === activeTestingType.value + const targetModeIsConfigured = testingType === 'e2e' && isE2EConfigured.value || testingType === 'component' && isCTConfigured.value + + if (!switchingBackToActiveMode && targetModeIsConfigured) { + await activateTestingType(testingType) + } else { + viewedTestingType.value = testingType + } + } + + return { + activeTestingType, + viewedTestingType, + isCTConfigured, + isE2EConfigured, + showTestingTypePromo, + viewTestingType, + activateTestingType, + } +} diff --git a/packages/app/src/debug/DebugContainer.cy.tsx b/packages/app/src/debug/DebugContainer.cy.tsx index e4002b096bd7..d466b01d8f38 100644 --- a/packages/app/src/debug/DebugContainer.cy.tsx +++ b/packages/app/src/debug/DebugContainer.cy.tsx @@ -1,10 +1,9 @@ import { DebugSpecListGroupsFragment, DebugSpecListSpecFragment, DebugSpecListTestsFragment, DebugSpecsFragment, DebugSpecsFragmentDoc, UseCohorts_DetermineCohortDocument } from '../generated/graphql-test' import DebugContainer from './DebugContainer.vue' import { defaultMessages } from '@cy/i18n' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' import { specsList } from './utils/DebugMapping' import { CloudRunStubs, createCloudRun } from '@packages/graphql/test/stubCloudTypes' -import { DEBUG_SLIDESHOW } from './utils/constants' import type { CloudRun, CloudSpecRun, CloudTestResult } from '@packages/graphql/src/gen/test-cloud-graphql-types.gen' const DebugSpecVariableTypes = { @@ -29,19 +28,12 @@ describe('', () => { describe('empty states', () => { const validateEmptyState = (expectedMessages: string[]) => { cy.stubMutationResolver(UseCohorts_DetermineCohortDocument, (defineResult) => { - return defineResult({ determineCohort: { __typename: 'Cohort', name: DEBUG_SLIDESHOW.id, cohort: 'A' } }) + return defineResult({ determineCohort: { __typename: 'Cohort', name: 'iatr_debug_slideshow', cohort: 'A' } }) }) cy.mountFragment(DebugSpecsFragmentDoc, { variableTypes: DebugSpecVariableTypes, variables: defaultVariables, - onResult: (res) => { - if (res.currentProject) { - res.currentProject.savedState = { - debugSlideshowComplete: true, - } - } - }, render: (gqlVal) => , }) @@ -51,65 +43,92 @@ describe('', () => { } it('shows not logged in', () => { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setHasInitiallyLoaded() + userProjectStatusStore.setHasInitiallyLoaded() + userProjectStatusStore.setProjectFlag('isUsingGit', true) - validateEmptyState([defaultMessages.debugPage.emptyStates.connectToCypressCloud, defaultMessages.debugPage.emptyStates.debugDirectlyInCypress, defaultMessages.debugPage.emptyStates.notLoggedInTestMessage]) + validateEmptyState([defaultMessages.debugPage.emptyStates.connectToCypressCloud, defaultMessages.debugPage.emptyStates.connect.title, defaultMessages.debugPage.emptyStates.connect.description]) cy.findByRole('button', { name: 'Connect to Cypress Cloud' }).should('be.visible') }) it('is logged in with no project', () => { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) - loginConnectStore.setProjectFlag('isProjectConnected', false) - loginConnectStore.setHasInitiallyLoaded() + userProjectStatusStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setProjectFlag('isProjectConnected', false) + userProjectStatusStore.setProjectFlag('isUsingGit', true) + userProjectStatusStore.setHasInitiallyLoaded() - validateEmptyState([defaultMessages.debugPage.emptyStates.debugDirectlyInCypress, defaultMessages.debugPage.emptyStates.reviewRerunAndDebug, defaultMessages.debugPage.emptyStates.noProjectTestMessage]) + validateEmptyState([defaultMessages.debugPage.emptyStates.connect.title, defaultMessages.debugPage.emptyStates.connect.description]) cy.findByRole('button', { name: 'Connect a Cypress Cloud project' }).should('be.visible') }) it('has no runs', () => { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) - loginConnectStore.setProjectFlag('isProjectConnected', true) - loginConnectStore.setHasInitiallyLoaded() + userProjectStatusStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setProjectFlag('isProjectConnected', true) + userProjectStatusStore.setProjectFlag('isUsingGit', true) + userProjectStatusStore.setHasInitiallyLoaded() cy.mountFragment(DebugSpecsFragmentDoc, { variableTypes: DebugSpecVariableTypes, variables: defaultVariables, render: (gqlVal) => , }) - validateEmptyState([defaultMessages.debugPage.emptyStates.recordYourFirstRun, defaultMessages.debugPage.emptyStates.almostThere, defaultMessages.debugPage.emptyStates.noRunsTestMessage]) + validateEmptyState([defaultMessages.debugPage.emptyStates.noRuns.title]) cy.findByDisplayValue('npx cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible') }) - it('errors', () => { - const loginConnectStore = useLoginConnectStore() + it('is not using git', () => { + const userProjectStatusStore = useUserProjectStatusStore() + + userProjectStatusStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setProjectFlag('isProjectConnected', true) + userProjectStatusStore.setProjectFlag('isUsingGit', false) + userProjectStatusStore.setHasInitiallyLoaded() + cy.mountFragment(DebugSpecsFragmentDoc, { + variableTypes: DebugSpecVariableTypes, + variables: defaultVariables, + render: (gqlVal) => , + }) + + cy.findByTestId('debug-empty-title').should('contain.text', 'Git repository not detected') + }) + + it('has no runs for the current branch', () => { + const { setUserFlag, setProjectFlag, cloudStatusMatches, setHasInitiallyLoaded } = useUserProjectStatusStore() + + setUserFlag('isLoggedIn', true) + setUserFlag('isMemberOfOrganization', true) + setProjectFlag('isProjectConnected', true) + setProjectFlag('hasNoRecordedRuns', true) + setProjectFlag('hasNonExampleSpec', true) + setProjectFlag('isConfigLoaded', true) + setProjectFlag('isUsingGit', true) + setHasInitiallyLoaded() - loginConnectStore.setUserFlag('isLoggedIn', true) - loginConnectStore.setProjectFlag('isProjectConnected', true) - loginConnectStore.setHasInitiallyLoaded() cy.mountFragment(DebugSpecsFragmentDoc, { variableTypes: DebugSpecVariableTypes, variables: defaultVariables, - render: (gqlVal) => , + render: (gqlVal) => , }) - cy.findByTestId('debug-empty').should('not.exist') - cy.findByTestId('debug-alert').should('be.visible') + expect(cloudStatusMatches('needsRecordedRun')).equals(true) + + cy.contains('No runs found for your branch') }) }) describe('run states', { viewportWidth: 900 }, () => { beforeEach(() => { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) - loginConnectStore.setProjectFlag('isProjectConnected', true) - loginConnectStore.setHasInitiallyLoaded() + userProjectStatusStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setProjectFlag('isProjectConnected', true) + userProjectStatusStore.setProjectFlag('isUsingGit', true) + userProjectStatusStore.setHasInitiallyLoaded() }) function mountTestRun (runName: string) { @@ -150,10 +169,10 @@ describe('', () => { it('renders', () => { mountTestRun('allSkipped') + cy.findByTestId('collapsible').should('be.visible') + cy.contains('h3', 'Incomplete') cy.contains('The browser server never connected.').should('be.visible') cy.contains('2 of 3 specs skipped').should('be.visible') - - cy.percySnapshot() }) }) @@ -161,9 +180,9 @@ describe('', () => { it('renders', () => { mountTestRun('noTests') + cy.findByTestId('collapsible').should('be.visible') + cy.contains('h3', 'Incomplete') cy.contains('Run has no tests').should('be.visible') - - cy.percySnapshot() }) }) @@ -171,46 +190,37 @@ describe('', () => { it('renders with CI information', () => { mountTestRun('timedOutWithCi') + cy.findByTestId('collapsible').should('be.visible') + cy.contains('h3', 'Incomplete') cy.contains('Circle CI #1234').should('have.attr', 'href', 'https://circleci.com').should('be.visible') cy.contains('Archive this run to remove it').should('be.visible') - - cy.percySnapshot() }) it('renders without CI information', () => { mountTestRun('timedOutWithoutCi') + cy.findByTestId('collapsible').should('be.visible') + cy.contains('h3', 'Incomplete') cy.contains('Circle CI #1234').should('not.exist') cy.contains('Archive this run to remove it').should('be.visible') - - cy.percySnapshot() }) }) context('over limit', () => { it('handled usage exceeded', () => { mountTestRun('overLimit') - cy.findByRole('link', { name: 'Contact admin' }).should('be.visible').should('have.attr', 'href', 'http://localhost:3000?utmMedium=Debug+Tab&utmSource=Binary%3A+Launchpad') - - cy.percySnapshot() }) it('handles retention exceeded', () => { mountTestRun('overLimitRetention') - cy.findByRole('link', { name: 'Contact admin' }).should('be.visible').should('have.attr', 'href', 'http://localhost:3000?utmMedium=Debug+Tab&utmSource=Binary%3A+Launchpad') - - cy.percySnapshot() }) it('does not show passing message if run is hidden', () => { mountTestRun('overLimitPassed') - cy.contains('Well Done!').should('not.exist') - cy.contains('All your tests passed.').should('not.exist') - cy.findByRole('link', { name: 'Contact admin' }).should('be.visible').should('have.attr', 'href', 'http://localhost:3000?utmMedium=Debug+Tab&utmSource=Binary%3A+Launchpad') }) }) @@ -229,14 +239,15 @@ describe('', () => { }) describe('when logged in and connected', () => { - let loginConnectStore + let userProjectStatusStore beforeEach(() => { - loginConnectStore = useLoginConnectStore() + userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) - loginConnectStore.setProjectFlag('isProjectConnected', true) - loginConnectStore.setHasInitiallyLoaded() + userProjectStatusStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setProjectFlag('isProjectConnected', true) + userProjectStatusStore.setProjectFlag('isUsingGit', true) + userProjectStatusStore.setHasInitiallyLoaded() }) it('renders running run', () => { diff --git a/packages/app/src/debug/DebugContainer.vue b/packages/app/src/debug/DebugContainer.vue index e0ef009841fe..7c82b4ce64c4 100644 --- a/packages/app/src/debug/DebugContainer.vue +++ b/packages/app/src/debug/DebugContainer.vue @@ -4,18 +4,22 @@ {{ t('launchpadErrors.noInternet.connectProject') }} - - + + + + { return props.gql?.currentProject?.cloudProject?.__typename === 'CloudProject' diff --git a/packages/app/src/debug/DebugFailedTest.cy.tsx b/packages/app/src/debug/DebugFailedTest.cy.tsx index db8f3a7f42c2..9433597d1e03 100644 --- a/packages/app/src/debug/DebugFailedTest.cy.tsx +++ b/packages/app/src/debug/DebugFailedTest.cy.tsx @@ -123,8 +123,6 @@ describe('', () => { .and('match', /utm_campaign/) .and('match', /utm_source/) }) - - cy.percySnapshot() }) it('contains multiple titleParts segments', { viewportWidth: 1200 }, () => { @@ -139,8 +137,6 @@ describe('', () => { )) assertRowContents(multipleTitleParts) - - cy.percySnapshot() }) it('tests multiple groups', { viewportWidth: 1200 }, () => { diff --git a/packages/app/src/debug/DebugPageHeader.cy.tsx b/packages/app/src/debug/DebugPageHeader.cy.tsx index 66442cc4fb5d..20a5b574ebb2 100644 --- a/packages/app/src/debug/DebugPageHeader.cy.tsx +++ b/packages/app/src/debug/DebugPageHeader.cy.tsx @@ -98,7 +98,6 @@ describe('', { }) cy.findByTestId(`debug-runNumber-${status}`).should('be.visible') - cy.percySnapshot() }) }) diff --git a/packages/app/src/debug/DebugPendingRunCounts.cy.tsx b/packages/app/src/debug/DebugPendingRunCounts.cy.tsx index 590fea7a27d0..d0e59b2e9184 100644 --- a/packages/app/src/debug/DebugPendingRunCounts.cy.tsx +++ b/packages/app/src/debug/DebugPendingRunCounts.cy.tsx @@ -9,8 +9,6 @@ describe('', () => { ) cy.contains('2 of 20').should('be.visible') - - cy.percySnapshot() }) it('renders counts of zeros input is undefined', () => { @@ -21,7 +19,5 @@ describe('', () => { ) cy.contains('0 of 0').should('be.visible') - - cy.percySnapshot() }) }) diff --git a/packages/app/src/debug/DebugPendingRunSplash.cy.tsx b/packages/app/src/debug/DebugPendingRunSplash.cy.tsx index af4c06096d60..845fe7b5ade6 100644 --- a/packages/app/src/debug/DebugPendingRunSplash.cy.tsx +++ b/packages/app/src/debug/DebugPendingRunSplash.cy.tsx @@ -4,9 +4,8 @@ describe('', () => { it('renders as expected', () => { cy.mount() - cy.contains('Failures will be displayed here') - - cy.percySnapshot() + cy.findByTestId('title').contains('Testing in progress...') + cy.findByTestId('splash-subtitle').contains('Failures will be displayed here') }) it('renders scheduled to complete message', () => { @@ -14,7 +13,5 @@ describe('', () => { cy.contains('Scheduled to complete...') cy.findByTestId('splash-subtitle').should('not.exist') - - cy.percySnapshot() }) }) diff --git a/packages/app/src/debug/DebugResults.cy.tsx b/packages/app/src/debug/DebugResults.cy.tsx index 92e98993a31d..70b0ee565706 100644 --- a/packages/app/src/debug/DebugResults.cy.tsx +++ b/packages/app/src/debug/DebugResults.cy.tsx @@ -17,8 +17,6 @@ describe('', () => { cy.get(`[title=${defaultMessages.runs.results.pending}]`).should('contain.text', cloudRun.totalPending) }) }) - - cy.percySnapshot() }) }) diff --git a/packages/app/src/debug/DebugResults.vue b/packages/app/src/debug/DebugResults.vue index 8a2998d82d6d..0d4b2ea042c3 100644 --- a/packages/app/src/debug/DebugResults.vue +++ b/packages/app/src/debug/DebugResults.vue @@ -1,6 +1,6 @@ diff --git a/packages/app/src/pages/Debug.vue b/packages/app/src/pages/Debug.vue index 4df2fd211d93..f3ae1a2a8197 100644 --- a/packages/app/src/pages/Debug.vue +++ b/packages/app/src/pages/Debug.vue @@ -58,7 +58,7 @@ const cachedProject = ref() const query = useQuery({ query: DebugDocument, variables, pause: shouldPauseQuery, requestPolicy: 'network-only' }) const isLoading = computed(() => { - const relevantRunsHaveNotLoaded = !relevantRuns.value + const relevantRunsHaveNotLoaded = !relevantRuns.value.all const queryIsBeingFetched = query.fetching.value const cloudProject = query.data.value?.currentProject?.cloudProject?.__typename === 'CloudProject' @@ -76,7 +76,6 @@ const isLoading = computed(() => { }) watchEffect(() => { - //console.log('query for debug', query.data.value, relevantRuns.value.commitShas) if (query.data.value?.currentProject?.cloudProject?.__typename === 'CloudProject') { const cloudProject = query.data.value.currentProject.cloudProject diff --git a/packages/app/src/runner/SnapshotControls.cy.tsx b/packages/app/src/runner/SnapshotControls.cy.tsx index 86dd662a76e9..870fb7a5d098 100644 --- a/packages/app/src/runner/SnapshotControls.cy.tsx +++ b/packages/app/src/runner/SnapshotControls.cy.tsx @@ -11,10 +11,6 @@ const snapshotControlsSelector = '[data-testid=snapshot-controls]' const unpinButtonSelector = '[data-testid=unpin]' describe('SnapshotControls', { viewportHeight: 200, viewportWidth: 500 }, () => { - afterEach(() => { - cy.wait(100).percySnapshot() - }) - const mountSnapshotControls = ( eventManager = createEventManager(), autIframe = createTestAutIframe(), diff --git a/packages/app/src/runner/SnapshotToggle.cy.tsx b/packages/app/src/runner/SnapshotToggle.cy.tsx index 4261345647bc..730e44c5e536 100644 --- a/packages/app/src/runner/SnapshotToggle.cy.tsx +++ b/packages/app/src/runner/SnapshotToggle.cy.tsx @@ -6,14 +6,12 @@ describe('', () => { cy.mount(() => ()) - cy.percySnapshot('before') .get('body') .findByText('2') .click() .parent() .findByText('1') .click() - .percySnapshot('after') }) it('renders longer text', () => { @@ -21,14 +19,12 @@ describe('', () => { cy.mount(() => ()) - cy.percySnapshot('before') .get('body') .findByText('Request') .click() .parent() .findByText('Response') .click() - .percySnapshot('after') }) it('emits a select event with the active message', () => { diff --git a/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx b/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx index 8682511b6546..6963a7e47365 100644 --- a/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx +++ b/packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx @@ -20,15 +20,18 @@ function renderWithGql (gqlVal: SpecRunnerHeaderFragment) { describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { it('renders', () => { const autStore = useAutStore() + const autUrl = 'http://localhost:4000' - autStore.updateUrl('http://localhost:4000') + autStore.updateUrl(autUrl) cy.mountFragment(SpecRunnerHeaderFragmentDoc, { render: (gqlVal) => { return renderWithGql(gqlVal) }, }) - cy.percySnapshot() + cy.findByTestId('aut-url-input').should('be.visible').should('have.value', autUrl) + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('disabled selector playground button when isRunning is true', () => { @@ -43,7 +46,9 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }) cy.get('[data-cy="playground-activator"]').should('be.disabled') - cy.percySnapshot() + cy.findByTestId('aut-url-input').should('be.visible') + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('disabled selector playground button when isLoading is true', () => { @@ -58,7 +63,9 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }) cy.get('[data-cy="playground-activator"]').should('be.disabled') - cy.percySnapshot() + cy.findByTestId('aut-url-input').should('be.visible') + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('enables selector playground button by default', () => { @@ -69,13 +76,16 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }) cy.get('[data-cy="playground-activator"]').should('not.be.disabled') - cy.percySnapshot() + cy.findByTestId('aut-url-input').should('be.visible') + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('shows url section if currentTestingType is e2e', () => { const autStore = useAutStore() + const autUrl = 'http://localhost:3000' - autStore.updateUrl('http://localhost:3000') + autStore.updateUrl(autUrl) cy.mountFragment(SpecRunnerHeaderFragmentDoc, { onResult: (gql) => { @@ -87,15 +97,18 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }) cy.get('[data-cy="aut-url"]').should('exist') - cy.percySnapshot() + cy.findByTestId('aut-url-input').should('be.visible').should('have.value', autUrl) + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('url section handles long url/small viewport', { viewportWidth: 500, }, () => { const autStore = useAutStore() + const autUrl = 'http://localhost:3000/pretty/long/url.spec.jsx' - autStore.updateUrl('http://localhost:3000/pretty/long/url.spec.jsx') + autStore.updateUrl(autUrl) cy.mountFragment(SpecRunnerHeaderFragmentDoc, { onResult: (gql) => { @@ -107,14 +120,17 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }) cy.get('[data-cy="aut-url"]').should('exist') + cy.findByTestId('aut-url-input').should('be.visible').should('have.value', autUrl) + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') cy.percySnapshot() }) it('links to aut url', () => { const autStore = useAutStore() - const url = 'http://localhost:3000/todo' + const autUrl = 'http://localhost:3000/todo' - autStore.updateUrl(url) + autStore.updateUrl(autUrl) cy.mountFragment(SpecRunnerHeaderFragmentDoc, { onResult: (gql) => { @@ -125,8 +141,9 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }, }) - cy.findByTestId('aut-url-input').invoke('val').should('contain', url) - cy.percySnapshot() + cy.findByTestId('aut-url-input').invoke('val').should('contain', autUrl) + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('does not show url section if currentTestingType is component', () => { @@ -143,9 +160,10 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }, }) - cy.get('[data-cy="playground-activator"]').should('be.visible') - cy.get('[data-cy="aut-url"]').should('not.exist') - cy.percySnapshot() + cy.findByTestId('playground-activator').should('be.visible') + cy.findByTestId('aut-url').should('not.exist') + cy.findByTestId('select-browser').should('be.visible').contains('Electron 73') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('shows current browser and possible browsers', () => { @@ -177,8 +195,11 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }, }) + cy.findByTestId('select-browser').contains('Fake Browser') + cy.get('[data-cy="select-browser"] > button img').should('have.attr', 'src', allBrowsersIcons.generic) - cy.percySnapshot() + + cy.findByTestId('viewport').contains('500x500') }) it('shows current viewport info', () => { @@ -219,10 +240,11 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { .should('be.visible') .should('have.attr', 'href', 'https://on.cypress.io/viewport') + cy.contains('The viewport determines the width and height of your application under test. By default the viewport will be 500px by 500px for end-to-end testing.') cy.contains('Additionally, you can override this value in your cypress.config.ts or via the cy.viewport() command.') .should('be.visible') - cy.percySnapshot() + cy.findByTestId('viewport-docs').should('have.attr', 'href', 'https://on.cypress.io/viewport') }) it('disables browser dropdown button when isRunning is true', () => { @@ -239,8 +261,10 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }, }) + cy.findByTestId('select-browser').should('be.visible').contains('Chrome 78') cy.get('[data-cy="select-browser"] > button').should('be.disabled') - cy.percySnapshot() + cy.findByTestId('aut-url').should('be.visible') + cy.findByTestId('viewport').should('be.visible').contains('500x500') }) it('opens and closes selector playground', () => { @@ -250,12 +274,10 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => { }, }) - cy.get('[data-cy="playground-activator"]').click() + cy.findByTestId('playground-activator').click() cy.get('#selector-playground').should('be.visible') - cy.percySnapshot() - - cy.get('[data-cy="playground-activator"]').click() + cy.findByTestId('playground-activator').click() cy.get('#selector-playground').should('not.exist') }) }) diff --git a/packages/app/src/runner/SpecRunnerHeaderRunMode.cy.tsx b/packages/app/src/runner/SpecRunnerHeaderRunMode.cy.tsx index 2d3e1016dee9..e534f6e83ed2 100644 --- a/packages/app/src/runner/SpecRunnerHeaderRunMode.cy.tsx +++ b/packages/app/src/runner/SpecRunnerHeaderRunMode.cy.tsx @@ -56,8 +56,6 @@ describe('SpecRunnerHeaderRunMode', { viewportHeight: 500 }, () => { cy.contains('The viewport determines').should('not.exist') cy.contains('Chrome 1').click() cy.contains('Firefox').should('not.exist') - - cy.percySnapshot() }) }) @@ -72,9 +70,8 @@ describe('SpecRunnerHeaderRunMode', { viewportHeight: 500 }, () => { cy.mount() + cy.get('[data-cy="select-browser"] > button img').should('have.attr', 'src', allBrowsersIcons.Chrome) cy.get('[data-cy="select-browser"] > button').should('be.disabled') - - cy.percySnapshot() }) }) @@ -90,7 +87,7 @@ describe('SpecRunnerHeaderRunMode', { viewportHeight: 500 }, () => { cy.mount() cy.get('[data-cy="select-browser"] > button img').should('have.attr', 'src', allBrowsersIcons.generic) - cy.percySnapshot() + cy.get('[data-cy="select-browser"] > button').should('be.disabled') }) }) }) diff --git a/packages/app/src/runner/automation/AutomationDisconnected.cy.tsx b/packages/app/src/runner/automation/AutomationDisconnected.cy.tsx index af8353a9b92c..6cc16b47bb44 100644 --- a/packages/app/src/runner/automation/AutomationDisconnected.cy.tsx +++ b/packages/app/src/runner/automation/AutomationDisconnected.cy.tsx @@ -5,7 +5,10 @@ describe('AutomationDisconnected', () => { it('should relaunch browser', () => { cy.mount() - cy.percySnapshot() + cy.findByTestId('collapsible').should('be.visible') + cy.contains('h3', 'The Cypress extension has disconnected.') + cy.contains('p', 'Cypress cannot run tests without this extension.') + cy.get('a').contains('Read more about browser management').should('have.attr', 'href', 'https://on.cypress.io/launching-browsers') const relaunchStub = cy.stub() diff --git a/packages/app/src/runner/automation/AutomationMissing.cy.tsx b/packages/app/src/runner/automation/AutomationMissing.cy.tsx index bbadf16ea2a0..2779fcadbe05 100644 --- a/packages/app/src/runner/automation/AutomationMissing.cy.tsx +++ b/packages/app/src/runner/automation/AutomationMissing.cy.tsx @@ -14,7 +14,10 @@ describe('AutomationMissing', () => { }, }) - cy.percySnapshot() + cy.findByTestId('collapsible').should('be.visible') + cy.contains('h3', 'The Cypress extension is missing.') + cy.contains('p', 'Cypress cannot run tests without this extension. Please choose another browser.') + cy.findByTestId('external').contains('Read more about browser management').should('have.attr', 'href', 'https://on.cypress.io/launching-browsers') const selectBrowserStub = cy.stub() diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index ab929b03b5d3..e8cb2180f81b 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -127,10 +127,6 @@ export class EventManager { runnerUiStore.setAutomationStatus(connected) }) - this.ws.on('change:to:url', (url) => { - window.location.href = url - }) - this.ws.on('update:telemetry:context', (contextString) => { const context = JSON.parse(contextString) @@ -815,7 +811,12 @@ export class EventManager { stop () { this.localBus.removeAllListeners() + + // Grab existing listeners for url change event, we want to preserve them + const urlChangeListeners = this.ws.listeners('change:to:url') + this.ws.off() + urlChangeListeners.forEach((listener) => this.ws.on('change:to:url', listener)) } async teardown (state: MobxRunnerStore, isRerun = false) { diff --git a/packages/app/src/runner/events/telemetry.ts b/packages/app/src/runner/events/telemetry.ts index 37088c959b72..94311bfbe797 100644 --- a/packages/app/src/runner/events/telemetry.ts +++ b/packages/app/src/runner/events/telemetry.ts @@ -36,4 +36,69 @@ export const addTelemetryListeners = (Cypress) => { // TODO: log error when client side debug logging is available } }) + + const commandSpanInfo = (command: Cypress.CommandQueue) => { + const runnable = Cypress.state('runnable') + const runnableType = runnable.type === 'hook' ? runnable.hookName : runnable.type + + return { + name: `${runnableType}: ${command.attributes.name}(${command.attributes.args.join(',')})`, + runnable, + runnableType, + } + } + + Cypress.on('command:start', (command: Cypress.CommandQueue) => { + try { + const test = Cypress.state('test') + + const { name, runnable, runnableType } = commandSpanInfo(command) + + const span = telemetry.startSpan({ + name, + opts: { + attributes: { + spec: runnable.invocationDetails.relativeFile, + test: `test:${test.fullTitle()}`, + 'runnable-type': runnableType, + }, + }, + isVerbose: true, + }) + + span?.setAttribute('command-name', command.attributes.name) + } catch (error) { + // TODO: log error when client side debug logging is available + } + }) + + const onCommandEnd = (command: Cypress.CommandQueue) => { + try { + const span = telemetry.getSpan(commandSpanInfo(command).name) + + span?.setAttribute('state', command.state) + span?.setAttribute('numLogs', command.logs?.length || 0) + span?.end() + } catch (error) { + // TODO: log error when client side debug logging is available + } + } + + Cypress.on('command:end', onCommandEnd) + + Cypress.on('skipped:command:end', onCommandEnd) + + Cypress.on('command:failed', (command: Cypress.CommandQueue, error: Error) => { + try { + const span = telemetry.getSpan(commandSpanInfo(command).name) + + span?.setAttribute('state', command.state) + span?.setAttribute('numLogs', command.logs?.length || 0) + span?.setAttribute('error.name', error.name) + span?.setAttribute('error.message', error.message) + span?.end() + } catch (error) { + // TODO: log error when client side debug logging is available + } + }) } diff --git a/packages/app/src/runner/index.ts b/packages/app/src/runner/index.ts index 4e1dfe0053d7..69dd26557db8 100644 --- a/packages/app/src/runner/index.ts +++ b/packages/app/src/runner/index.ts @@ -37,6 +37,10 @@ export function createWebsocket (config: Cypress.Config) { ws.emit('runner:connected') }) + ws.on('change:to:url', (url) => { + window.location.href = url + }) + return ws } diff --git a/packages/app/src/runner/selector-playground/SelectorPlayground.cy.tsx b/packages/app/src/runner/selector-playground/SelectorPlayground.cy.tsx index f879da4251ef..785e81c7d000 100644 --- a/packages/app/src/runner/selector-playground/SelectorPlayground.cy.tsx +++ b/packages/app/src/runner/selector-playground/SelectorPlayground.cy.tsx @@ -28,8 +28,6 @@ describe('SelectorPlayground', () => { cy.spy(autIframe, 'toggleSelectorHighlight') cy.get('[data-cy="selected-playground-method"]').should('contain', 'cy.get') cy.get('[data-cy="playground-selector"]').should('have.value', 'body') - - cy.percySnapshot() }) it('toggles enabled', () => { diff --git a/packages/app/src/runs/CloudConnectButton.cy.tsx b/packages/app/src/runs/CloudConnectButton.cy.tsx index 35e9c5825ecb..c4361d619a06 100644 --- a/packages/app/src/runs/CloudConnectButton.cy.tsx +++ b/packages/app/src/runs/CloudConnectButton.cy.tsx @@ -1,5 +1,5 @@ import CloudConnectButton from './CloudConnectButton.vue' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' describe('', { viewportHeight: 60, viewportWidth: 400 }, () => { context('not logged in ', () => { @@ -11,12 +11,12 @@ describe('', { viewportHeight: 60, viewportWidth: 400 }, ( }) context('logged in', () => { - let loginConnectStore + let userProjectStatusStore beforeEach(() => { - loginConnectStore = useLoginConnectStore() + userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setUserFlag('isLoggedIn', true) }) it('show project connect if not connected', () => { @@ -26,7 +26,7 @@ describe('', { viewportHeight: 60, viewportWidth: 400 }, ( }) it('uses the store to open the Login Connect modal', () => { - loginConnectStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal') + userProjectStatusStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal') cy.mount(() =>
) cy.contains('button', 'Connect a Cypress Cloud project').click() @@ -35,7 +35,7 @@ describe('', { viewportHeight: 60, viewportWidth: 400 }, ( }) it('uses the store to open the Login Connect modal with utmContent', () => { - loginConnectStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal') + userProjectStatusStore.openLoginConnectModal = cy.spy().as('openLoginConnectModal') cy.mount(() =>
) cy.contains('button', 'Connect a Cypress Cloud project').click() diff --git a/packages/app/src/runs/CloudConnectButton.vue b/packages/app/src/runs/CloudConnectButton.vue index 255d8d389997..ab5a3b25f6aa 100644 --- a/packages/app/src/runs/CloudConnectButton.vue +++ b/packages/app/src/runs/CloudConnectButton.vue @@ -15,9 +15,9 @@ import ChainIcon from '~icons/cy/chain-link_x16.svg' import CypressIcon from '~icons/cy/cypress-logo_x16.svg' import Button from '@cy/components/Button.vue' import { useI18n } from '@cy/i18n' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' -const { openLoginConnectModal, user } = useLoginConnectStore() +const { openLoginConnectModal, user } = useUserProjectStatusStore() const { t } = useI18n() diff --git a/packages/app/src/runs/RunCard.cy.tsx b/packages/app/src/runs/RunCard.cy.tsx index 799044edfc3f..0b1a8f46f81e 100644 --- a/packages/app/src/runs/RunCard.cy.tsx +++ b/packages/app/src/runs/RunCard.cy.tsx @@ -74,8 +74,6 @@ describe('', { viewportHeight: 400 }, () => { cy.contains(CloudRunStubs.allPassing.commitInfo.branch as string) .should('be.visible') - - cy.percySnapshot() }) }) @@ -100,8 +98,6 @@ describe('', { viewportHeight: 400 }, () => { // this is the human readable commit time from the stub cy.contains('an hour ago').should('be.visible') - - cy.percySnapshot() }) }) @@ -122,8 +118,6 @@ describe('', { viewportHeight: 400 }, () => { // this is the human readable commit time from the stub cy.contains('01:01:01').should('be.visible') - - cy.percySnapshot() }) it('displays mm:ss format for run duration if duration is less than an hour', () => { @@ -142,8 +136,6 @@ describe('', { viewportHeight: 400 }, () => { // this is the human readable commit time from the stub cy.contains('01:01').should('be.visible') - - cy.percySnapshot() }) }) @@ -163,8 +155,6 @@ describe('', { viewportHeight: 400 }, () => { }) cy.get('[data-cy="run-tag"]').should('have.length', 2).each(($el, i) => cy.wrap($el).contains(`tag${i}`)) - - cy.percySnapshot() }) it('truncates tags if > 2', () => { @@ -182,8 +172,6 @@ describe('', { viewportHeight: 400 }, () => { }) cy.get('[data-cy="run-tag"]').should('have.length', 3).last().contains('+4') - - cy.percySnapshot() }) }) }) diff --git a/packages/app/src/runs/RunResults.cy.tsx b/packages/app/src/runs/RunResults.cy.tsx index 87ecceea0296..140a9ec5daa2 100644 --- a/packages/app/src/runs/RunResults.cy.tsx +++ b/packages/app/src/runs/RunResults.cy.tsx @@ -17,8 +17,6 @@ describe('', { viewportHeight: 150, viewportWidth: 250 }, () => { cy.get(`[title=${defaultMessages.runs.results.pending}]`).should('contain.text', cloudRun.totalPending) }) }) - - cy.percySnapshot() }) it('renders flaky ribbon', () => { @@ -32,7 +30,5 @@ describe('', { viewportHeight: 150, viewportWidth: 250 }, () => { }) cy.contains('4 Flaky') - - cy.percySnapshot() }) }) diff --git a/packages/app/src/runs/RunsConnect.cy.tsx b/packages/app/src/runs/RunsConnect.cy.tsx index 067f9b01af1b..70614009e14e 100644 --- a/packages/app/src/runs/RunsConnect.cy.tsx +++ b/packages/app/src/runs/RunsConnect.cy.tsx @@ -2,7 +2,7 @@ import RunsConnect from './RunsConnect.vue' describe('', () => { it('show connect button', () => { - cy.mount(() =>
) + cy.mount(() =>
) cy.contains('button', 'Connect to Cypress Cloud').should('be.visible') }) diff --git a/packages/app/src/runs/RunsConnect.vue b/packages/app/src/runs/RunsConnect.vue index 41306e153184..d7935d7548cb 100644 --- a/packages/app/src/runs/RunsConnect.vue +++ b/packages/app/src/runs/RunsConnect.vue @@ -1,50 +1,17 @@ diff --git a/packages/app/src/runs/RunsConnectSuccessAlert.spec.tsx b/packages/app/src/runs/RunsConnectSuccessAlert.spec.tsx index 0dfe111c5d55..da79aaf33eaa 100644 --- a/packages/app/src/runs/RunsConnectSuccessAlert.spec.tsx +++ b/packages/app/src/runs/RunsConnectSuccessAlert.spec.tsx @@ -41,9 +41,6 @@ describe('', { viewportHeight: 400 }, () => { ) }, }) - - cy.viewport(1000, 800) - cy.percySnapshot() }) }) }) diff --git a/packages/app/src/runs/RunsContainer.cy.tsx b/packages/app/src/runs/RunsContainer.cy.tsx index 8dc6c46e1f9b..8439e2a4c17d 100644 --- a/packages/app/src/runs/RunsContainer.cy.tsx +++ b/packages/app/src/runs/RunsContainer.cy.tsx @@ -1,7 +1,7 @@ import RunsContainer from './RunsContainer.vue' import { RunsContainerFragmentDoc } from '../generated/graphql-test' import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' import { defaultMessages } from '@cy/i18n' @@ -16,9 +16,9 @@ describe('', { keystrokeDelay: 0 }, () => { context('when the user is logged in', () => { beforeEach(() => { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setUserFlag('isLoggedIn', true) }) it('renders with expected runs if there is a cloud project id', () => { @@ -56,9 +56,6 @@ describe('', { keystrokeDelay: 0 }, () => { const text = defaultMessages.runs.connect cy.contains(text.title).should('be.visible') - cy.contains(text.smartText).should('be.visible') - cy.contains(text.debugText).should('be.visible') - cy.contains(text.chartText).should('be.visible') cy.contains(text.buttonProject).should('be.visible') cy.percySnapshot() }) @@ -75,14 +72,35 @@ describe('', { keystrokeDelay: 0 }, () => { const text = defaultMessages.runs.connect cy.contains(text.title).should('be.visible') - cy.contains(text.smartText).should('be.visible') - cy.contains(text.debugText).should('be.visible') - cy.contains(text.chartText).should('be.visible') cy.contains(text.buttonUser).should('be.visible') cy.percySnapshot() }) }) + context('when the user has no recorded runs', () => { + it('renders instructions and record prompt', () => { + cy.mountFragment(RunsContainerFragmentDoc, { + onResult (gql) { + gql.cloudViewer = cloudViewer + if (gql.currentProject?.cloudProject?.__typename === 'CloudProject') { + gql.currentProject.cloudProject.runs = { + __typename: 'CloudRunConnection', + pageInfo: null as any, + nodes: [], + } + } + }, + render (gqlVal) { + return + }, + }) + + const text = defaultMessages.runs.empty + + cy.contains(text.title).should('be.visible') + }) + }) + context('with errors', () => { it('renders connection failed', () => { cy.mountFragment(RunsContainerFragmentDoc, { @@ -107,4 +125,104 @@ describe('', { keystrokeDelay: 0 }, () => { cy.percySnapshot() }) }) + + context('when not using git', () => { + it('renders alert message', () => { + const userProjectStatusStore = useUserProjectStatusStore() + + userProjectStatusStore.setProjectFlag('isUsingGit', false) + userProjectStatusStore.setUserFlag('isLoggedIn', true) + + cy.mountFragment(RunsContainerFragmentDoc, { + onResult: (result) => { + result.cloudViewer = cloudViewer + }, + render (gqlVal) { + return + }, + }) + + cy.get('h3').contains(defaultMessages.runs.empty.gitRepositoryNotDetected) + cy.contains(defaultMessages.runs.empty.ensureGitSetupCorrectly) + }) + + it('dismisses the alert', () => { + const userProjectStatusStore = useUserProjectStatusStore() + + userProjectStatusStore.setProjectFlag('isUsingGit', false) + userProjectStatusStore.setUserFlag('isLoggedIn', true) + + cy.mountFragment(RunsContainerFragmentDoc, { + onResult: (result) => { + result.cloudViewer = cloudViewer + }, + render (gqlVal) { + return + }, + }) + + cy.get('h3').contains(defaultMessages.runs.empty.gitRepositoryNotDetected) + cy.contains(defaultMessages.runs.empty.ensureGitSetupCorrectly) + cy.get('[data-cy=alert-suffix-icon]').click() + cy.get('[data-cy=alert-header]').should('not.exist') + }) + }) + + context('when using git but no runs for current branch', () => { + it('renders alert message', () => { + const { setUserFlag, setProjectFlag, cloudStatusMatches } = useUserProjectStatusStore() + + setUserFlag('isLoggedIn', true) + setUserFlag('isMemberOfOrganization', true) + setProjectFlag('isProjectConnected', true) + setProjectFlag('hasNoRecordedRuns', true) + setProjectFlag('hasNonExampleSpec', true) + setProjectFlag('isConfigLoaded', true) + setProjectFlag('isUsingGit', true) + + expect(cloudStatusMatches('needsRecordedRun')).equals(true) + cy.mountFragment(RunsContainerFragmentDoc, { + onResult: (result) => { + result.cloudViewer = cloudViewer + }, + render (gqlVal) { + return + }, + }) + + cy.get('h3').contains(defaultMessages.runs.empty.noRunsFoundForBranch) + cy.get('p').contains(defaultMessages.runs.empty.noRunsForBranchMessage) + // The utm_source will be Binary%3A+App in production`open` mode but we assert using Binary%3A+Launchpad as this is the value in CI + cy.contains(defaultMessages.links.learnMoreButton).should('have.attr', 'href', 'https://on.cypress.io/git-info?utm_source=Binary%3A+Launchpad&utm_medium=Runs+Tab&utm_campaign=No+Runs+Found') + }) + + it('dismisses the alert', () => { + const { setUserFlag, setProjectFlag, cloudStatusMatches } = useUserProjectStatusStore() + + setUserFlag('isLoggedIn', true) + setUserFlag('isMemberOfOrganization', true) + setProjectFlag('isProjectConnected', true) + setProjectFlag('hasNoRecordedRuns', true) + setProjectFlag('hasNonExampleSpec', true) + setProjectFlag('isConfigLoaded', true) + setProjectFlag('isUsingGit', true) + + expect(cloudStatusMatches('needsRecordedRun')).equals(true) + cy.mountFragment(RunsContainerFragmentDoc, { + onResult: (result) => { + result.cloudViewer = cloudViewer + }, + render (gqlVal) { + return + }, + }) + + cy.get('h3').contains(defaultMessages.runs.empty.noRunsFoundForBranch) + cy.get('p').contains(defaultMessages.runs.empty.noRunsForBranchMessage) + // The utm_source will be Binary%3A+App in production`open` mode but we assert using Binary%3A+Launchpad as this is the value in CI + cy.contains(defaultMessages.links.learnMoreButton).should('have.attr', 'href', 'https://on.cypress.io/git-info?utm_source=Binary%3A+Launchpad&utm_medium=Runs+Tab&utm_campaign=No+Runs+Found') + cy.get('[data-cy=alert-suffix-icon]').click() + cy.get('[data-cy=alert-header]').should('not.exist') + }) + }) }) diff --git a/packages/app/src/runs/RunsContainer.vue b/packages/app/src/runs/RunsContainer.vue index f33bc626092f..7e97ed10a568 100644 --- a/packages/app/src/runs/RunsContainer.vue +++ b/packages/app/src/runs/RunsContainer.vue @@ -1,5 +1,5 @@ @@ -135,15 +138,15 @@ import RequestAccessButton from './RequestAccessButton.vue' import { gql } from '@urql/vue' import { SpecsListBannersFragment, SpecsListBanners_CheckCloudOrgMembershipDocument } from '../generated/graphql' import { AllowedState, BannerIds } from '@packages/types' -import { LoginBanner, CreateOrganizationBanner, ConnectProjectBanner, RecordBanner } from './banners' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' +import { LoginBanner, ComponentTestingAvailableBanner, CreateOrganizationBanner, ConnectProjectBanner, RecordBanner } from './banners' +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' import { usePromptManager } from '@packages/frontend-shared/src/gql-components/composables/usePromptManager' import { CohortConfig, CohortOption, useCohorts } from '@packages/frontend-shared/src/gql-components/composables/useCohorts' import { useSubscription } from '../graphql' const route = useRoute() const { t } = useI18n() -const loginConnectStore = useLoginConnectStore() +const userProjectStatusStore = useUserProjectStatusStore() gql` fragment SpecsListBanners on Query { @@ -163,6 +166,21 @@ fragment SpecsListBanners on Query { id projectId savedState + currentTestingType + config + } + machineId + wizard { + framework { + id + name + icon + type + } + bundler { + id + name + } } } ` @@ -203,13 +221,12 @@ const showFetchError = ref(props.isFetchError) const showProjectNotFound = ref(props.isProjectNotFound) const showProjectRequestAccess = ref(props.isProjectUnauthorized) -const isBannerAllowed = ref(false) - const bannerIds = { isLoggedOut: BannerIds.ACI_082022_LOGIN, needsOrgConnect: BannerIds.ACI_082022_CREATE_ORG, needsProjectConnect: BannerIds.ACI_082022_CONNECT_PROJECT, needsRecordedRun: BannerIds.ACI_082022_RECORD, + isComponentTestingCandidate: BannerIds.CT_052023_AVAILABLE, } as const watch( @@ -223,32 +240,30 @@ watch( }, ) -const cloudData = computed(() => ([props.gql.cloudViewer, props.gql.cachedUser, props.gql.currentProject] as const)) -const bannerToShow = computed(() => { +const { getEffectiveBannerState } = usePromptManager() + +const bannerComponentToShow = computed(() => { const componentsByStatus = { isLoggedOut: LoginBanner, needsOrgConnect: CreateOrganizationBanner, needsProjectConnect: ConnectProjectBanner, needsRecordedRun: RecordBanner, + isComponentTestingCandidate: ComponentTestingAvailableBanner, } - return componentsByStatus[loginConnectStore.userStatus] ?? null + const bannerStateToShow = getEffectiveBannerState('specsListBanner') + + return bannerStateToShow ? componentsByStatus[bannerStateToShow] : null }) const hasCurrentBannerBeenShown = computed(() => { + const bannerStateToShow = getEffectiveBannerState('specsListBanner') const bannersState = (props.gql.currentProject?.savedState as AllowedState)?.banners + const bannerId = bannerStateToShow && bannerIds[bannerStateToShow] - return !!bannersState?._disabled || !!bannersState?.[bannerIds[loginConnectStore.userStatus]]?.lastShown + return !!bannersState?._disabled || (!!bannerId && !!bannersState?.[bannerId]?.lastShown) }) -const { isAllowedFeature } = usePromptManager() - -watch(cloudData, () => { - // when cloud data updates, recheck if banner is allowed - isBannerAllowed.value = isAllowedFeature('specsListBanner') -}, -{ immediate: true }) - type BannerKeys = keyof typeof BannerIds type BannerId = typeof BannerIds[BannerKeys] type BannerCohortOptions = Partial> @@ -280,11 +295,21 @@ const getCohortForBanner = (bannerId: BannerId): Ref = } const currentCohortOption = computed(() => { - if (!bannerCohortOptions[bannerIds[loginConnectStore.userStatus]]) { + if (!bannerCohortOptions[bannerIds[userProjectStatusStore.cloudStatus]]) { return { cohort: null } } - return reactive({ cohort: getCohortForBanner(bannerIds[loginConnectStore.userStatus]) }) + return reactive({ cohort: getCohortForBanner(bannerIds[userProjectStatusStore.cloudStatus]) }) +}) + +const ctFramework = computed(() => { + return { + name: props.gql.wizard?.framework?.name, + type: props.gql.wizard?.framework?.type, + icon: props.gql.wizard?.framework?.icon, + } }) +const ctBundler = computed(() => props.gql.wizard?.bundler?.name) + diff --git a/packages/app/src/specs/SpecsListHeader.cy.tsx b/packages/app/src/specs/SpecsListHeader.cy.tsx index 8e19097280ef..dfebe42f731a 100644 --- a/packages/app/src/specs/SpecsListHeader.cy.tsx +++ b/packages/app/src/specs/SpecsListHeader.cy.tsx @@ -79,8 +79,6 @@ describe('', { keystrokeDelay: 0 }, () => { .click() .get('@show-spec-pattern-modal') .should('have.been.called') - - cy.percySnapshot() }) it('shows the count correctly when not searching', () => { @@ -96,17 +94,11 @@ describe('', { keystrokeDelay: 0 }, () => { .should('be.visible') .and('have.attr', 'aria-live', 'polite') - cy.percySnapshot('No matches') - mountWithSpecCount(1) cy.contains('1 match').should('be.visible') - cy.percySnapshot('Singular Match') - mountWithSpecCount(100) cy.contains('100 matches').should('be.visible') - - cy.percySnapshot('Plural Match') }) it('shows the count correctly while searching', () => { @@ -124,7 +116,5 @@ describe('', { keystrokeDelay: 0 }, () => { cy.contains('0 of 1 match').should('be.visible') cy.contains('1 of 1 match').should('be.visible') cy.contains('5 of 22 matches').should('be.visible') - - cy.percySnapshot() }) }) diff --git a/packages/app/src/specs/SpecsListHeader.vue b/packages/app/src/specs/SpecsListHeader.vue index 25b0b8cd2f08..606766b06a1c 100644 --- a/packages/app/src/specs/SpecsListHeader.vue +++ b/packages/app/src/specs/SpecsListHeader.vue @@ -46,12 +46,11 @@
@@ -64,10 +63,10 @@ diff --git a/packages/app/src/specs/banners/ComponentTestingAvailableBanner.cy.tsx b/packages/app/src/specs/banners/ComponentTestingAvailableBanner.cy.tsx new file mode 100644 index 000000000000..64253ee0a95e --- /dev/null +++ b/packages/app/src/specs/banners/ComponentTestingAvailableBanner.cy.tsx @@ -0,0 +1,87 @@ +import { defaultMessages } from '@cy/i18n' +import ComponentTestingAvailableBanner from './ComponentTestingAvailableBanner.vue' +import { TrackedBanner_RecordBannerSeenDocument, TrackedBanner_SetProjectStateDocument } from '../../generated/graphql' +import type Sinon from 'sinon' + +const frameworks = [ + { name: 'React', type: 'react' }, + { name: 'Create React App', type: 'reactscripts' }, + { name: 'Nuxt.js (v2)', type: 'nuxtjs' }, + { name: 'Vue', type: 'vue3' }, + { name: 'Angular', type: 'angular' }, + { name: 'Next.js', type: 'nextjs' }, + { name: 'Svelte.js', type: 'svelte' }, +] + +describe('', { viewportWidth: 1200 }, () => { + it('should render expected content', () => { + cy.mount() + }) + + frameworks.map((framework) => { + it(`should render expected content for ${framework.name}`, () => { + cy.mount( + , + ) + + cy.findByTestId('alert-prefix-icon').should('be.visible') + cy.contains(defaultMessages.specPage.banners.ct.title.replace('{0}', framework.name)).should('be.visible') + }) + }) + + context('events', () => { + beforeEach(() => { + const recordEvent = cy.stub().as('recordEvent') + const setPrefs = cy.stub().as('setPrefs') + + cy.stubMutationResolver(TrackedBanner_RecordBannerSeenDocument, (defineResult, event) => { + recordEvent(event) + + return defineResult({ recordEvent: true }) + }) + + cy.stubMutationResolver(TrackedBanner_SetProjectStateDocument, (defineResult, event) => { + setPrefs(event) + + return defineResult({ __typename: 'Mutation', setPreferences: { __typename: 'Query' } as any }) + }) + }) + + it('should record expected event on mount', () => { + cy.mount( + , + ) + + cy.get('@recordEvent').should('have.been.calledWith', { + campaign: 'CT Available', + medium: 'Specs CT Available Banner', + messageId: Cypress.sinon.match.string, + cohort: null, + }) + }) + + it('should not record event on mount if already shown', () => { + cy.mount( + , + ) + + cy.get('@recordEvent').should('not.have.been.called') + }) + + it('should record dismissal event when clicking survey link', () => { + cy.mount( + , + ) + + cy.findByTestId('survey-link').click() + + cy.get('@setPrefs').should('have.been.calledTwice') + cy.get('@setPrefs').should(($stub) => { + const arg = ($stub as unknown as Sinon.SinonStub).getCall(1).args[0] + + expect(arg.value).to.contain('ct_052023_available') + expect(arg.value).to.contain('dismissed') + }) + }) + }) +}) diff --git a/packages/app/src/specs/banners/ComponentTestingAvailableBanner.vue b/packages/app/src/specs/banners/ComponentTestingAvailableBanner.vue new file mode 100644 index 000000000000..b3b75b72d704 --- /dev/null +++ b/packages/app/src/specs/banners/ComponentTestingAvailableBanner.vue @@ -0,0 +1,145 @@ + + + diff --git a/packages/app/src/specs/banners/ConnectProjectBanner.vue b/packages/app/src/specs/banners/ConnectProjectBanner.vue index ffa6377d6ff5..ed8fc0861ed6 100644 --- a/packages/app/src/specs/banners/ConnectProjectBanner.vue +++ b/packages/app/src/specs/banners/ConnectProjectBanner.vue @@ -37,8 +37,8 @@ import Button from '@cy/components/Button.vue' import TrackedBanner from './TrackedBanner.vue' import type { CohortOption } from '@packages/frontend-shared/src/gql-components/composables/useCohorts' import { BannerIds } from '@packages/types' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' -const { openLoginConnectModal } = useLoginConnectStore() +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' +const { openLoginConnectModal } = useUserProjectStatusStore() defineProps<{ hasBannerBeenShown: boolean diff --git a/packages/app/src/specs/banners/LoginBanner.vue b/packages/app/src/specs/banners/LoginBanner.vue index acb6faa1d8ab..5d0e0853a0d8 100644 --- a/packages/app/src/specs/banners/LoginBanner.vue +++ b/packages/app/src/specs/banners/LoginBanner.vue @@ -37,8 +37,8 @@ import Button from '@cy/components/Button.vue' import TrackedBanner from './TrackedBanner.vue' import type { CohortOption } from '@packages/frontend-shared/src/gql-components/composables/useCohorts' import { BannerIds } from '@packages/types' -import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' -const { openLoginConnectModal } = useLoginConnectStore() +import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store' +const { openLoginConnectModal } = useUserProjectStatusStore() defineProps<{ hasBannerBeenShown: boolean diff --git a/packages/app/src/specs/banners/TrackedBanner.cy.tsx b/packages/app/src/specs/banners/TrackedBanner.cy.tsx index a553a47643b9..655bc41bbd9c 100644 --- a/packages/app/src/specs/banners/TrackedBanner.cy.tsx +++ b/packages/app/src/specs/banners/TrackedBanner.cy.tsx @@ -8,8 +8,6 @@ describe('', () => { cy.findByText('Test Content').should('be.visible') cy.findByTestId('alert-suffix-icon').should('be.visible') - - cy.percySnapshot() }) it('should record when banner is made visible', () => { @@ -126,5 +124,15 @@ describe('', () => { cy.get('@recordEvent').should('not.have.been.called') }) }) + + context('when eventData is undefined', () => { + it('should not record event', () => { + cy.mount({ + render: () => , + }) + + cy.get('@recordEvent').should('not.have.been.called') + }) + }) }) }) diff --git a/packages/app/src/specs/banners/TrackedBanner.vue b/packages/app/src/specs/banners/TrackedBanner.vue index c5bee7a9563b..62daf6e3ead4 100644 --- a/packages/app/src/specs/banners/TrackedBanner.vue +++ b/packages/app/src/specs/banners/TrackedBanner.vue @@ -3,7 +3,10 @@ v-model="isAlertDisplayed" v-bind="$attrs" > - + @@ -25,7 +28,7 @@ type AlertComponentProps = InstanceType['$props'] interface TrackedBannerComponentProps extends AlertComponentProps { bannerId: string hasBannerBeenShown: boolean - eventData: EventData + eventData: EventData | undefined } gql` @@ -71,22 +74,22 @@ watchEffect(() => { } }) -watch(() => isAlertDisplayed.value, (newVal) => { +watch(() => isAlertDisplayed.value, async (newVal) => { if (!newVal) { - updateBannerState('dismissed') + await updateBannerState('dismissed') } }) -onMounted(() => { - updateBannerState('lastShown') +onMounted(async () => { + await updateBannerState('lastShown') }) -function updateBannerState (field: 'lastShown' | 'dismissed') { +async function updateBannerState (field: 'lastShown' | 'dismissed') { const savedBannerState = stateQuery.data.value?.currentProject?.savedState?.banners ?? {} set(savedBannerState, [props.bannerId, field], Date.now()) - setStateMutation.executeMutation({ value: JSON.stringify({ banners: savedBannerState }) }) + await setStateMutation.executeMutation({ value: JSON.stringify({ banners: savedBannerState }) }) } function recordBannerShown ({ campaign, medium, cohort }: EventData): void { @@ -98,4 +101,8 @@ function recordBannerShown ({ campaign, medium, cohort }: EventData): void { }) } +async function dismiss (): Promise { + await updateBannerState('dismissed') +} + diff --git a/packages/app/src/specs/banners/index.ts b/packages/app/src/specs/banners/index.ts index c952e10fddbb..1414b9c752d9 100644 --- a/packages/app/src/specs/banners/index.ts +++ b/packages/app/src/specs/banners/index.ts @@ -5,3 +5,5 @@ export { default as CreateOrganizationBanner } from './CreateOrganizationBanner. export { default as ConnectProjectBanner } from './ConnectProjectBanner.vue' export { default as RecordBanner } from './RecordBanner.vue' + +export { default as ComponentTestingAvailableBanner } from './ComponentTestingAvailableBanner.vue' diff --git a/packages/app/src/specs/flaky-badge/FlakyBadge.cy.tsx b/packages/app/src/specs/flaky-badge/FlakyBadge.cy.tsx index ac170e142170..0dcda8f349b4 100644 --- a/packages/app/src/specs/flaky-badge/FlakyBadge.cy.tsx +++ b/packages/app/src/specs/flaky-badge/FlakyBadge.cy.tsx @@ -10,7 +10,5 @@ describe('', () => { cy.findByTestId(flakyBadgeTestId) .should('have.text', defaultMessages.specPage.flaky.badgeLabel) .and('be.visible') - - cy.percySnapshot() }) }) diff --git a/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx b/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx index 79d41b964678..11b9d68ab7b7 100644 --- a/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx +++ b/packages/app/src/specs/flaky-badge/FlakySpecSummary.cy.tsx @@ -1,66 +1,47 @@ import FlakySpecSummary from './FlakySpecSummary.vue' describe('', () => { - it('low severity', () => { - cy.mount( - , - ) - - cy.percySnapshot() - }) - - it('medium severity', () => { - cy.mount( - , - ) - - cy.percySnapshot() - }) - - it('high severity', () => { - cy.mount( - , - ) - - cy.percySnapshot() - }) - - it('loading state', () => { - // Ensure component handles malformed/incomplete data without blowing up - cy.mount( - , - ) + it('severities', () => { + cy.mount(() => +
+ + + + , +
) cy.findByTestId('flaky-specsummary-loading-1').should('be.visible') - cy.percySnapshot() }) diff --git a/packages/app/src/specs/generators/ExpandableFileList.cy.tsx b/packages/app/src/specs/generators/ExpandableFileList.cy.tsx index 76dc945269a8..cb149bef0e2b 100644 --- a/packages/app/src/specs/generators/ExpandableFileList.cy.tsx +++ b/packages/app/src/specs/generators/ExpandableFileList.cy.tsx @@ -50,7 +50,6 @@ describe('', { viewportHeight: 500, viewportWidth: 400 }, it('correctly formats a difficult file', () => { cy.get('body').contains('[...all]') - cy.percySnapshot() }) }) @@ -74,8 +73,6 @@ describe('', { viewportHeight: 500, viewportWidth: 400 }, )) .get(noResultsSelector).should('be.visible') - cy.percySnapshot() - cy.get('[data-testid=add-file]') .click() .get(noResultsSelector).should('not.exist') diff --git a/packages/app/src/specs/generators/GeneratorSuccess.cy.tsx b/packages/app/src/specs/generators/GeneratorSuccess.cy.tsx index 2b89be603575..b599f125dd75 100644 --- a/packages/app/src/specs/generators/GeneratorSuccess.cy.tsx +++ b/packages/app/src/specs/generators/GeneratorSuccess.cy.tsx @@ -12,7 +12,7 @@ describe('<${spec.baseName} />', () => { it('renders', () => { // https://on.cypress.io/mount mount(<${spec.baseName} />) - }) + }) }) `.trim() @@ -21,8 +21,6 @@ describe('', () => { cy.mount(() => ()) .get('body') .contains(spec.relative) - - cy.percySnapshot() }) it('can be collapsed to hide the content', () => { @@ -34,23 +32,9 @@ describe('', () => { .wait(200) // just to show off the animation .get(targetSelector) .click() - - cy.percySnapshot() - }) - - it('can be expanded to show the content', () => { - cy.mount(() => ()) - .get(targetSelector) - .click() - .click() .get('code .line') .should('be.visible') .should('have.length', content.split('\n').length) - .wait(200) // just to show off the animation - .get(targetSelector) - .click() - - cy.percySnapshot() }) it('handles really long file names and really long content', () => { diff --git a/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx b/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx index 5f3ad5b79ad3..66109c83302d 100644 --- a/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx +++ b/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx @@ -18,8 +18,6 @@ describe('ReactComponentList', () => { cy.mount() cy.contains('No components found').should('be.visible') - - cy.percySnapshot() }) it('renders error state if errored is true', () => { @@ -30,8 +28,6 @@ describe('ReactComponentList', () => { cy.mount() cy.contains('Unable to parse file').should('be.visible') - - cy.percySnapshot() }) it('fetches and displays a list of components', () => { @@ -40,8 +36,6 @@ describe('ReactComponentList', () => { cy.contains('FooBar').should('be.visible') cy.contains('BarFoo').should('be.visible') cy.contains('FooBarBaz').should('be.visible') - - cy.percySnapshot() }) it('calls selectItem on click', () => { diff --git a/packages/app/src/specs/switcher/TestingTypeSwitcher.cy.tsx b/packages/app/src/specs/switcher/TestingTypeSwitcher.cy.tsx new file mode 100644 index 000000000000..87e871d9a203 --- /dev/null +++ b/packages/app/src/specs/switcher/TestingTypeSwitcher.cy.tsx @@ -0,0 +1,56 @@ +import TestingTypeSwitcher from './TestingTypeSwitcher.vue' + +describe('', () => { + beforeEach(() => { + const stub = cy.stub().as('selectTestingType') + + cy.mount() + }) + + it('renders', () => { + cy.percySnapshot() + }) + + it('displays question mark for unconfigured testing type', () => { + cy.findByTestId('unconfigured-icon').should('be.visible') + }) + + it('emits event on switch', () => { + cy.viewport(1200, 700) + cy.findByText('Component specs').click() + cy.get('@selectTestingType').should('have.been.calledOnceWith', 'component') + }) + + it('selects current testing type when it is e2e', () => { + const stub = cy.stub().as('selectTestingType') + + cy.mount() + + cy.findByTestId('testing-type-switch').contains('button', 'E2E').should('have.attr', 'aria-selected', 'true') + cy.findByTestId('testing-type-switch').contains('button', 'Component').should('not.have.attr', 'aria-selected') + }) + + it('selects current testing type when it is component', () => { + const stub = cy.stub().as('selectTestingType') + + cy.mount() + + cy.findByTestId('testing-type-switch').contains('button', 'E2E').should('not.have.attr', 'aria-selected') + cy.findByTestId('testing-type-switch').contains('button', 'Component').should('have.attr', 'aria-selected', 'true') + }) +}) diff --git a/packages/app/src/specs/switcher/TestingTypeSwitcher.vue b/packages/app/src/specs/switcher/TestingTypeSwitcher.vue new file mode 100644 index 000000000000..48bf4be1e029 --- /dev/null +++ b/packages/app/src/specs/switcher/TestingTypeSwitcher.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/packages/app/tailwind.config.cjs b/packages/app/tailwind.config.cjs index de778a97ae2b..8e706b94f7ef 100644 --- a/packages/app/tailwind.config.cjs +++ b/packages/app/tailwind.config.cjs @@ -1,8 +1,5 @@ const config = require('@packages/frontend-shared/tailwind.config.cjs') -config.content.files = [ - './src/**/*.{vue,js,ts,jsx,tsx,scss,css}', // - '../frontend-shared/src/**/*.{vue,js,ts,jsx,tsx,scss,css}', -], +config.content.files.push('../frontend-shared/src/**/*.{vue,js,ts,jsx,tsx,scss,css}') module.exports = config diff --git a/packages/app/vite.config.mjs b/packages/app/vite.config.mjs index 9c0772108b39..a09852b05822 100644 --- a/packages/app/vite.config.mjs +++ b/packages/app/vite.config.mjs @@ -5,7 +5,23 @@ import Copy from 'rollup-plugin-copy' import Legacy from '@vitejs/plugin-legacy' import { resolve } from 'path' -export default makeConfig({}, { +export default makeConfig({ + optimizeDeps: { + include: [ + 'javascript-time-ago', + 'ansi-to-html', + 'fuzzysort', + '@cypress-design/**', + '@cypress-design/vue-button', + 'debug', + 'p-defer', + 'bluebird', + 'events', + '@popperjs/core', + '@opentelemetry/*', + ] + }, +}, { plugins: [ Layouts(), Pages({ extensions: ['vue'] }), diff --git a/packages/config/__snapshots__/validation.spec.ts.js b/packages/config/__snapshots__/validation.spec.ts.js index a713874b3627..ebd24b7fcfa9 100644 --- a/packages/config/__snapshots__/validation.spec.ts.js +++ b/packages/config/__snapshots__/validation.spec.ts.js @@ -1,13 +1,13 @@ exports['missing https protocol'] = { - "key": "clientCertificates[0].url", - "value": "http://url.com", - "type": "an https protocol" + 'key': 'clientCertificates[0].url', + 'value': 'http://url.com', + 'type': 'an https protocol', } exports['invalid url'] = { - "key": "clientCertificates[0].url", - "value": "not *", - "type": "a valid URL" + 'key': 'clientCertificates[0].url', + 'value': 'not *', + 'type': 'a valid URL', } exports['undefined browsers'] = ` @@ -19,208 +19,220 @@ Expected at least one browser ` exports['browsers list with a string'] = { - "key": "name", - "value": "foo", - "type": "a non-empty string", - "list": "browsers" + 'key': 'name', + 'value': 'foo', + 'type': 'a non-empty string', + 'list': 'browsers', } exports['invalid retry value'] = { - "key": "mockConfigKey", - "value": "1", - "type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls" + 'key': 'mockConfigKey', + 'value': '1', + 'type': 'a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls', } exports['invalid retry object'] = { - "key": "mockConfigKey", - "value": { - "fakeMode": 1 + 'key': 'mockConfigKey', + 'value': { + 'fakeMode': 1, }, - "type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls" + 'type': 'a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls', } exports['not qualified url'] = { - "key": "mockConfigKey", - "value": "url.com", - "type": "a fully qualified URL (starting with `http://` or `https://`)" + 'key': 'mockConfigKey', + 'value': 'url.com', + 'type': 'a fully qualified URL (starting with `http://` or `https://`)', } exports['empty string'] = { - "key": "mockConfigKey", - "value": "", - "type": "a fully qualified URL (starting with `http://` or `https://`)" + 'key': 'mockConfigKey', + 'value': '', + 'type': 'a fully qualified URL (starting with `http://` or `https://`)', } exports['not string or array'] = { - "key": "mockConfigKey", - "value": null, - "type": "a string or an array of strings" + 'key': 'mockConfigKey', + 'value': null, + 'type': 'a string or an array of strings', } exports['array of non-strings'] = { - "key": "mockConfigKey", - "value": [ + 'key': 'mockConfigKey', + 'value': [ 1, 2, - 3 + 3, ], - "type": "a string or an array of strings" + 'type': 'a string or an array of strings', } exports['not one of the strings error message'] = { - "key": "test", - "value": "nope", - "type": "one of these values: \"foo\", \"bar\"" + 'key': 'test', + 'value': 'nope', + 'type': 'one of these values: "foo", "bar"', } exports['number instead of string'] = { - "key": "test", - "value": 42, - "type": "one of these values: \"foo\", \"bar\"" + 'key': 'test', + 'value': 42, + 'type': 'one of these values: "foo", "bar"', } exports['null instead of string'] = { - "key": "test", - "value": null, - "type": "one of these values: \"foo\", \"bar\"" + 'key': 'test', + 'value': null, + 'type': 'one of these values: "foo", "bar"', } exports['not one of the numbers error message'] = { - "key": "test", - "value": 4, - "type": "one of these values: 1, 2, 3" + 'key': 'test', + 'value': 4, + 'type': 'one of these values: 1, 2, 3', } exports['string instead of a number'] = { - "key": "test", - "value": "foo", - "type": "one of these values: 1, 2, 3" + 'key': 'test', + 'value': 'foo', + 'type': 'one of these values: 1, 2, 3', } exports['null instead of a number'] = { - "key": "test", - "value": null, - "type": "one of these values: 1, 2, 3" + 'key': 'test', + 'value': null, + 'type': 'one of these values: 1, 2, 3', } exports['config/src/validation .isValidClientCertificatesSet returns error message for certs not passed as an array array 1'] = { - "key": "mockConfigKey", - "value": "1", - "type": "a positive number or null or an object with keys \"openMode\" and \"runMode\" with values of numbers or nulls" + 'key': 'mockConfigKey', + 'value': '1', + 'type': 'a positive number or null or an object with keys "openMode" and "runMode" with values of numbers or nulls', } exports['config/src/validation .isValidClientCertificatesSet returns error message for certs object without url 1'] = { - "key": "clientCertificates[0].url", - "type": "a URL matcher" + 'key': 'clientCertificates[0].url', + 'type': 'a URL matcher', } exports['config/src/validation .isValidBrowser passes valid browsers and forms error messages for invalid ones isValidBrowser 1'] = { - "name": "isValidBrowser", - "behavior": [ + 'name': 'isValidBrowser', + 'behavior': [ { - "given": { - "name": "Chrome", - "displayName": "Chrome Browser", - "family": "chromium", - "path": "/path/to/chrome", - "version": "1.2.3", - "majorVersion": 1 + 'given': { + 'name': 'Chrome', + 'displayName': 'Chrome Browser', + 'family': 'chromium', + 'path': '/path/to/chrome', + 'version': '1.2.3', + 'majorVersion': 1, }, - "expect": true + 'expect': true, }, { - "given": { - "name": "FF", - "displayName": "Firefox", - "family": "firefox", - "path": "/path/to/firefox", - "version": "1.2.3", - "majorVersion": "1" + 'given': { + 'name': 'FF', + 'displayName': 'Firefox', + 'family': 'firefox', + 'path': '/path/to/firefox', + 'version': '1.2.3', + 'majorVersion': '1', }, - "expect": true + 'expect': true, }, { - "given": { - "name": "Electron", - "displayName": "Electron", - "family": "chromium", - "path": "", - "version": "99.101.3", - "majorVersion": 99 + 'given': { + 'name': 'Electron', + 'displayName': 'Electron', + 'family': 'chromium', + 'path': '', + 'version': '99.101.3', + 'majorVersion': 99, }, - "expect": true + 'expect': true, }, { - "given": { - "name": "No display name", - "family": "chromium" + 'given': { + 'name': 'No display name', + 'family': 'chromium', }, - "expect": { - "key": "displayName", - "value": { - "name": "No display name", - "family": "chromium" + 'expect': { + 'key': 'displayName', + 'value': { + 'name': 'No display name', + 'family': 'chromium', }, - "type": "a non-empty string" - } + 'type': 'a non-empty string', + }, }, { - "given": { - "name": "bad family", - "displayName": "Bad family browser", - "family": "unknown family" + 'given': { + 'name': 'bad family', + 'displayName': 'Bad family browser', + 'family': 'unknown family', }, - "expect": { - "key": "family", - "value": { - "name": "bad family", - "displayName": "Bad family browser", - "family": "unknown family" + 'expect': { + 'key': 'family', + 'value': { + 'name': 'bad family', + 'displayName': 'Bad family browser', + 'family': 'unknown family', }, - "type": "either chromium, firefox or webkit" - } - } - ] + 'type': 'either chromium, firefox or webkit', + }, + }, + ], } exports['config/src/validation .isPlainObject returns error message when value is a not an object 1'] = { - "key": "mockConfigKey", - "value": 1, - "type": "a plain object" + 'key': 'mockConfigKey', + 'value': 1, + 'type': 'a plain object', } exports['config/src/validation .isNumber returns error message when value is a not a number 1'] = { - "key": "mockConfigKey", - "value": "string", - "type": "a number" + 'key': 'mockConfigKey', + 'value': 'string', + 'type': 'a number', } exports['config/src/validation .isNumberOrFalse returns error message when value is a not number or false 1'] = { - "key": "mockConfigKey", - "value": null, - "type": "a number or false" + 'key': 'mockConfigKey', + 'value': null, + 'type': 'a number or false', } exports['config/src/validation .isBoolean returns error message when value is a not a string 1'] = { - "key": "mockConfigKey", - "value": 1, - "type": "a string" + 'key': 'mockConfigKey', + 'value': 1, + 'type': 'a string', } exports['config/src/validation .isString returns error message when value is a not a string 1'] = { - "key": "mockConfigKey", - "value": 1, - "type": "a string" + 'key': 'mockConfigKey', + 'value': 1, + 'type': 'a string', } exports['config/src/validation .isArray returns error message when value is a non-array 1'] = { - "key": "mockConfigKey", - "value": 1, - "type": "an array" + 'key': 'mockConfigKey', + 'value': 1, + 'type': 'an array', } exports['config/src/validation .isStringOrFalse returns error message when value is neither string nor false 1'] = { - "key": "mockConfigKey", - "value": null, - "type": "a string or false" + 'key': 'mockConfigKey', + 'value': null, + 'type': 'a string or false', +} + +exports['invalid lower bound'] = { + 'key': 'test', + 'value': -1, + 'type': 'a valid CRF number between 1 & 51, 0 or false to disable compression, or true to use the default compression of 32', +} + +exports['invalid upper bound'] = { + 'key': 'test', + 'value': 52, + 'type': 'a valid CRF number between 1 & 51, 0 or false to disable compression, or true to use the default compression of 32', } diff --git a/packages/config/src/browser.ts b/packages/config/src/browser.ts index 509886eb5dee..94d21f2bf7e6 100644 --- a/packages/config/src/browser.ts +++ b/packages/config/src/browser.ts @@ -2,6 +2,7 @@ import _ from 'lodash' import Debug from 'debug' import { defaultSpecPattern, + defaultExcludeSpecPattern, options, breakingOptions, breakingRootOptions, @@ -17,6 +18,7 @@ import * as validation from './validation' export { defaultSpecPattern, + defaultExcludeSpecPattern, options, breakingOptions, BreakingOption, diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index 2a48153160ba..66eab7fa18f3 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -120,6 +120,11 @@ export const defaultSpecPattern = { component: '**/*.cy.{js,jsx,ts,tsx}', } +export const defaultExcludeSpecPattern = { + e2e: '*.hot-update.js', + component: ['**/__snapshots__/*', '**/__image_snapshots__/*'], +} + // NOTE: // If you add/remove/change a config value, make sure to update the following // - cli/types/index.d.ts (including allowed config options on TestOptions) @@ -271,7 +276,7 @@ const driverConfigOptions: Array = [ requireRestartOnChange: 'server', }, { name: 'excludeSpecPattern', - defaultValue: (options: Record = {}) => options.testingType === 'component' ? ['**/__snapshots__/*', '**/__image_snapshots__/*'] : '*.hot-update.js', + defaultValue: (options: Record = {}) => options.testingType === 'component' ? defaultExcludeSpecPattern.component : defaultExcludeSpecPattern.e2e, validation: validate.isStringOrArrayOfStrings, overrideLevel: 'any', }, { @@ -423,7 +428,7 @@ const driverConfigOptions: Array = [ }, { name: 'videoCompression', defaultValue: 32, - validation: validate.isNumberOrFalse, + validation: validate.isValidCrfOrBoolean, }, { name: 'videosFolder', defaultValue: 'cypress/videos', diff --git a/packages/config/src/validation.ts b/packages/config/src/validation.ts index 8f023db35515..49f2f7eeb420 100644 --- a/packages/config/src/validation.ts +++ b/packages/config/src/validation.ts @@ -301,6 +301,16 @@ export function isNumberOrFalse (key: string, value: any): ErrResult | true { return errMsg(key, value, 'a number or false') } +export function isValidCrfOrBoolean (key: string, value: any): ErrResult | true { + // a valid number that is between 1-51 including 1 or 51 + // or a boolean. false or 0 disables compression and true sets compression to 32 CRF by default. + if (_.isBoolean(value) || (_.isNumber(value) && _.inRange(value, 0, 52))) { + return true + } + + return errMsg(key, value, 'a valid CRF number between 1 & 51, 0 or false to disable compression, or true to use the default compression of 32') +} + export function isStringOrFalse (key: string, value: any): ErrResult | true { if (_.isString(value) || isFalse(value)) { return true diff --git a/packages/config/test/validation.spec.ts b/packages/config/test/validation.spec.ts index fc7b22e5eb31..36bc180d5575 100644 --- a/packages/config/test/validation.spec.ts +++ b/packages/config/test/validation.spec.ts @@ -388,4 +388,44 @@ describe('config/src/validation', () => { return snapshot('null instead of a number', msg) }) }) + + describe('.isValidCrfOrBoolean', () => { + it('validates booleans', () => { + const validate = validation.isValidCrfOrBoolean + + expect(validate).to.be.a('function') + expect(validate('test', false)).to.be.true + expect(validate('test', true)).to.be.true + }) + + it('validates any number between 0 and 51', () => { + const validate = validation.isValidCrfOrBoolean + + const validConfigNumbers = [...Array(51).keys()] + + validConfigNumbers.forEach((num) => { + expect(validate('test', num)).to.be.true + }) + }) + + it('invalidates lower bound', () => { + const validate = validation.isValidCrfOrBoolean + + const lowerBoundMsg = validate('test', -1) + + expect(lowerBoundMsg).to.not.be.true + + return snapshot('invalid lower bound', lowerBoundMsg) + }) + + it('invalidates upper bound', () => { + const validate = validation.isValidCrfOrBoolean + + const upperBoundMsg = validate('test', 52) + + expect(upperBoundMsg).to.not.be.true + + return snapshot('invalid upper bound', upperBoundMsg) + }) + }) }) diff --git a/packages/data-context/src/actions/ElectronActions.ts b/packages/data-context/src/actions/ElectronActions.ts index afce5ab557ad..091e81d54677 100644 --- a/packages/data-context/src/actions/ElectronActions.ts +++ b/packages/data-context/src/actions/ElectronActions.ts @@ -1,4 +1,4 @@ -import type { App, BrowserWindow, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron' +import type { App, BrowserWindow, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue, Notification } from 'electron' import os from 'os' import type { DataContext } from '..' import _ from 'lodash' @@ -13,6 +13,7 @@ export interface ElectronApiShape { copyTextToClipboard(text: string): void isMainWindowFocused(): boolean focusMainWindow(): void + createNotification(title: string, body: string): Notification } export class ElectronActions { @@ -104,4 +105,18 @@ export class ElectronActions { return obj.filePath || null }) } + + showSystemNotification (title: string, body: string, onClick?: () => void) { + const notification = this.ctx.electronApi.createNotification(title, body) + + const defaultOnClick = async () => { + await this.ctx.actions.browser.focusActiveBrowserWindow() + } + + const clickHandler = onClick || defaultOnClick + + notification.on('click', clickHandler) + + notification.show() + } } diff --git a/packages/data-context/src/actions/EventCollectorActions.ts b/packages/data-context/src/actions/EventCollectorActions.ts index 5d9e0e01e0b4..9f1af8f4616b 100644 --- a/packages/data-context/src/actions/EventCollectorActions.ts +++ b/packages/data-context/src/actions/EventCollectorActions.ts @@ -5,11 +5,13 @@ const pkg = require('@packages/root') const debug = Debug('cypress:data-context:actions:EventCollectorActions') -interface CollectableEvent { +interface CollectibleEvent { campaign: string messageId: string medium: string cohort?: string + payload?: object + machineId?: string } /** @@ -23,23 +25,29 @@ export class EventCollectorActions { debug('Using %s environment for Event Collection', cloudEnv) } - async recordEvent (event: CollectableEvent): Promise { + async recordEvent (event: CollectibleEvent, includeMachineId: boolean): Promise { try { const cloudUrl = this.ctx.cloud.getCloudUrl(cloudEnv) + const eventUrl = includeMachineId ? `${cloudUrl}/machine-collect` : `${cloudUrl}/anon-collect` + const headers = { + 'Content-Type': 'application/json', + 'x-cypress-version': pkg.version, + } + + if (includeMachineId) { + event.machineId = (await this.ctx.coreData.machineId) || undefined + } await this.ctx.util.fetch( - `${cloudUrl}/anon-collect`, + eventUrl, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-cypress-version': pkg.version, - }, + headers, body: JSON.stringify(event), }, ) - debug(`Recorded event: %o`, event) + debug(`Recorded %s event: %o`, includeMachineId ? 'machine-linked' : 'anonymous', event) return true } catch (err) { diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index e0f52325c0b1..5834d542c7f7 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -9,9 +9,17 @@ import type { ProjectShape } from '../data/coreDataShape' import type { DataContext } from '..' import { hasNonExampleSpec } from '../codegen' import templates from '../codegen/templates' -import { insertValuesInConfigFile } from '../util' +import { insertValuesInConfigFile, toPosix } from '../util' import { getError } from '@packages/errors' import { resetIssuedWarnings } from '@packages/config' +import type { RunSpecErrorCode } from '@packages/graphql/src/schemaTypes' +import debugLib from 'debug' + +export class RunSpecError extends Error { + constructor (public code: typeof RunSpecErrorCode[number], msg: string) { + super(msg) + } +} export interface ProjectApiShape { /** @@ -46,6 +54,8 @@ export interface ProjectApiShape { isListening: (url: string) => Promise resetBrowserTabsForNextTest(shouldKeepTabOpen: boolean): Promise resetServer(): void + runSpec(spec: Cypress.Spec): Promise + routeToDebug(): void } export interface FindSpecs { @@ -76,6 +86,8 @@ type SetForceReconfigureProjectByTestingType = { testingType?: TestingType } +const debug = debugLib('cypress:data-context:ProjectActions') + export class ProjectActions { constructor (private ctx: DataContext) {} @@ -462,4 +474,173 @@ export class ProjectActions { await this.ctx.actions.wizard.scaffoldTestingType() } } + + async runSpec ({ specPath }: { specPath: string}) { + const waitForBrowserToOpen = async () => { + const browserStatusSubscription = this.ctx.emitter.subscribeTo('browserStatusChange', { sendInitial: false }) + + // Wait for browser to finish launching. Browser is either launched from scratch + // or relaunched when switching testing types - we need to wait in either case + // We wait a maximum of 3 seconds so we don't block indefinitely in case something + // goes sideways with the browser launch process. This is broken up into three + // separate 'waits' in case we have to watch a browser relaunch (close > opening > open) + debug('Waiting for browser to report `open`') + let maxIterations = 3 + + while (this.ctx.coreData.app.browserStatus !== 'open') { + await Promise.race([ + new Promise((resolve) => setTimeout(resolve, 1000)), + browserStatusSubscription.next(), + ]) + + if (--maxIterations === 0) { + break + } + } + + await browserStatusSubscription.return(undefined as any) + } + + try { + if (!this.ctx.currentProject) { + throw new RunSpecError('NO_PROJECT', 'A project must be open prior to attempting to run a spec') + } + + if (!specPath) { + throw new RunSpecError('NO_SPEC_PATH', '`specPath` must be a non-empty string') + } + + let targetTestingType: TestingType + + // Check to see whether input specPath matches the specPattern for one or the other testing type + // If it matches neither then we can't run the spec and we should error + if (await this.ctx.project.matchesSpecPattern(specPath, 'e2e')) { + targetTestingType = 'e2e' + } else if (await this.ctx.project.matchesSpecPattern(specPath, 'component')) { + targetTestingType = 'component' + } else { + throw new RunSpecError('NO_SPEC_PATTERN_MATCH', 'Unable to determine testing type, spec does not match any configured specPattern') + } + + debug(`Spec %s matches '${targetTestingType}' pattern`, specPath) + + const absoluteSpecPath = this.ctx.path.resolve(this.ctx.currentProject, specPath) + + debug('Attempting to launch spec %s', absoluteSpecPath) + + // Look to see if there's actually a file at the target location + // This helps us avoid switching testingType *then* finding out the spec doesn't exist + if (!this.ctx.fs.existsSync(absoluteSpecPath)) { + throw new RunSpecError('SPEC_NOT_FOUND', `No file exists at path ${absoluteSpecPath}`) + } + + // We now know what testingType we need to be in - if we're already there, great + // If not, verify that type is configured then switch (or throw an error if not configured) + if (this.ctx.coreData.currentTestingType !== targetTestingType) { + if (!this.ctx.lifecycleManager.isTestingTypeConfigured(targetTestingType)) { + throw new RunSpecError('TESTING_TYPE_NOT_CONFIGURED', `Input path matched specPattern for '${targetTestingType}' testing type, but it is not configured.`) + } + + debug('Setting testing type to %s', targetTestingType) + + const specChangeSubscription = this.ctx.emitter.subscribeTo('specsChange', { sendInitial: false }) + + const originalTestingType = this.ctx.coreData.currentTestingType + + // Temporarily toggle testing type so the `activeBrowser` can be initialized + // for the targeted testing type. Browser has to be initialized prior to our "relaunch" + // call below - this can be an issue when Cypress is still on the launchpad and no + // browser has been launched yet + this.ctx.lifecycleManager.setCurrentTestingType(targetTestingType) + await this.ctx.lifecycleManager.setInitialActiveBrowser() + this.ctx.lifecycleManager.setCurrentTestingType(originalTestingType) + + // This is the magic sauce - we now have a browser selected, so this will toggle + // the testing type, trigger specs to update, and launch the browser + await this.switchTestingTypesAndRelaunch(targetTestingType) + + await waitForBrowserToOpen() + + // When testing type changes we need to wait for the specWatcher to trigger and load new specs + // otherwise our call to `getCurrentSpecByAbsolute` below will fail + // Wait a maximum of 2 seconds just in case something breaks with the event subscription + // so we don't block indefinitely + debug('Waiting for specs to finish loading') + await Promise.race([ + new Promise((resolve) => setTimeout(resolve, 2000)), + specChangeSubscription.next(), + ]) + + // Close out subscription + await specChangeSubscription.return(undefined) + } else { + debug('Already in %s testing mode', targetTestingType) + } + + // This accounts for an edge case where a testing type has been previously opened, but + // the user then backs out to the testing type selector in launchpad. In that scenario, + // the testingType switch logic above does not trigger the browser to open, so we do it + // manually here + if (this.ctx.coreData.app.browserStatus === 'closed') { + debug('No browser instance, launching...') + await this.ctx.lifecycleManager.setInitialActiveBrowser() + + await this.api.launchProject( + this.ctx.coreData.activeBrowser!, + { + name: '', + absolute: '', + relative: '', + specType: targetTestingType === 'e2e' ? 'integration' : 'component', + }, + ) + + debug('Browser launched') + } else { + debug(`Browser already running, status ${this.ctx.coreData.app.browserStatus}`) + + if (this.ctx.coreData.app.browserStatus !== 'open') { + await waitForBrowserToOpen() + } + } + + // Now that we're in the correct testingType, verify the requested spec actually exists + // We don't have specs available until a testingType is loaded, so even through we validated + // a matching file exists above it may not end up loading as a valid spec so we validate that here + // + // Have to use toPosix here to align windows absolute paths with how the absolute path is storied in the data context + const spec = this.ctx.project.getCurrentSpecByAbsolute(toPosix(absoluteSpecPath)) + + if (!spec) { + debug(`Spec not found with path: ${absoluteSpecPath}`) + throw new RunSpecError('SPEC_NOT_FOUND', `Unable to find matching spec with path ${absoluteSpecPath}`) + } + + const browser = this.ctx.coreData.activeBrowser! + + // Hooray, everything looks good and we're all set up + // Try to launch the requested spec by navigating to it in the browser + await this.api.runSpec(spec) + + return { + testingType: targetTestingType, + browser, + spec, + } + } catch (err) { + if (!(err instanceof RunSpecError)) { + debug('Unexpected error during `runSpec` %o', err) + } + + return { + code: err instanceof RunSpecError ? err.code : 'GENERAL_ERROR', + detailMessage: err.message, + } + } + } + + async debugCloudRun (runNumber: number) { + await this.ctx.relevantRuns.moveToRun(runNumber, this.ctx.git?.currentHashes || []) + this.api.routeToDebug() + } } diff --git a/packages/data-context/src/actions/WizardActions.ts b/packages/data-context/src/actions/WizardActions.ts index 7a3c4aafed63..9aa87d96f3ed 100644 --- a/packages/data-context/src/actions/WizardActions.ts +++ b/packages/data-context/src/actions/WizardActions.ts @@ -100,6 +100,14 @@ export class WizardActions { this.resetWizard() + await this.initializeFramework() + } + + async initializeFramework () { + if (!this.ctx.currentProject) { + return + } + const detected = await detectFramework(this.ctx.currentProject, this.ctx.coreData.wizard.frameworks) debug('detected %o', detected) diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts index 2c1c4a6a9ad7..afac43214656 100644 --- a/packages/data-context/src/data/ProjectLifecycleManager.ts +++ b/packages/data-context/src/data/ProjectLifecycleManager.ts @@ -584,7 +584,7 @@ export class ProjectLifecycleManager { * this sources the config from the various config sources */ async getFullInitialConfig (options: Partial = this.ctx.modeOptions, withBrowsers = true): Promise { - assert(this._configManager, 'Cannot get full config a config manager') + assert(this._configManager, 'Cannot get full config without a config manager') return this._configManager.getFullInitialConfig(options, withBrowsers) } diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index 1dd5322525f0..529311c62f84 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -7,6 +7,7 @@ import type { SocketIONamespace, SocketIOServer } from '@packages/socket' import type { Server } from 'http' import type { ErrorWrapperSource } from '@packages/errors' import type { GitDataSource, LegacyCypressConfigJson } from '../sources' +import { machineId as getMachineId } from 'node-machine-id' export type Maybe = T | null | undefined @@ -123,6 +124,7 @@ export interface CoreDataShape { cliBrowser: string | null cliTestingType: string | null activeBrowser: FoundBrowser | null + machineId: Promise machineBrowsers: Promise | null allBrowsers: Promise | null servers: { @@ -166,6 +168,7 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa servers: {}, cliBrowser: modeOptions.browser ?? null, cliTestingType: modeOptions.testingType ?? null, + machineId: machineId(), machineBrowsers: null, allBrowsers: null, hasInitializedMode: null, @@ -233,4 +236,12 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa testsForRunResults: {}, }, } + + async function machineId (): Promise { + try { + return await getMachineId() + } catch (error) { + return null + } + } } diff --git a/packages/data-context/src/sources/HtmlDataSource.ts b/packages/data-context/src/sources/HtmlDataSource.ts index a8f377e2f1ea..bbdc351245a7 100644 --- a/packages/data-context/src/sources/HtmlDataSource.ts +++ b/packages/data-context/src/sources/HtmlDataSource.ts @@ -120,7 +120,7 @@ export class HtmlDataSource { window.__CYPRESS_CONFIG__ = ${JSON.stringify(serveConfig)}; window.__CYPRESS_TESTING_TYPE__ = '${this.ctx.coreData.currentTestingType}' window.__CYPRESS_BROWSER__ = ${JSON.stringify(this.ctx.coreData.activeBrowser)} - ${telemetry.isEnabled() ? `window.__CYPRESS_TELEMETRY__ = ${JSON.stringify({ context: telemetry.getActiveContextObject(), resources: telemetry.getResources() })}` : ''} + ${telemetry.isEnabled() ? `window.__CYPRESS_TELEMETRY__ = ${JSON.stringify({ context: telemetry.getActiveContextObject(), resources: telemetry.getResources(), isVerbose: telemetry.isVerbose() })}` : ''} ${process.env.CYPRESS_INTERNAL_GQL_NO_SOCKET ? `window.__CYPRESS_GQL_NO_SOCKET__ = 'true';` : ''} `) diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts index b8cea8e9fa63..95284e536cb8 100644 --- a/packages/data-context/src/sources/ProjectDataSource.ts +++ b/packages/data-context/src/sources/ProjectDataSource.ts @@ -7,7 +7,7 @@ import path from 'path' import Debug from 'debug' import commonPathPrefix from 'common-path-prefix' import type { FSWatcher } from 'chokidar' -import { defaultSpecPattern } from '@packages/config' +import { defaultSpecPattern, defaultExcludeSpecPattern } from '@packages/config' import parseGlob from 'parse-glob' import micromatch from 'micromatch' import RandExp from 'randexp' @@ -23,12 +23,20 @@ import type { ProjectShape } from '../data' import type { FindSpecs } from '../actions' import { FileExtension, getDefaultSpecFileName } from './migration/utils' +type SpecPatterns = { + specPattern?: string[] + excludeSpecPattern?: string[] +} + interface MatchedSpecs { projectRoot: string testingType: Cypress.TestingType specAbsolutePaths: string[] specPattern: string | string[] } + +const toArray = (val?: string | string[]) => val ? typeof val === 'string' ? [val] : val : undefined + export function matchedSpecs ({ projectRoot, testingType, @@ -258,12 +266,10 @@ export class ProjectDataSource { this.ctx.coreData.app.relaunchBrowser = relaunchBrowser } - async specPatterns (): Promise<{ - specPattern?: string[] - excludeSpecPattern?: string[] - }> { - const toArray = (val?: string | string[]) => val ? typeof val === 'string' ? [val] : val : undefined - + /** + * Retrieve the applicable spec patterns for the current testing type + */ + async specPatterns (): Promise { const config = await this.getConfig() return { @@ -272,6 +278,26 @@ export class ProjectDataSource { } } + /** + * Retrieve the applicable spec patterns for a given testing type. Can be used to check whether + * a spec satisfies the pattern when outside a given testing type. + */ + async specPatternsByTestingType (testingType: TestingType): Promise { + const configFile = await this.ctx.lifecycleManager.getConfigFileContents() + + if (testingType === 'e2e') { + return { + specPattern: toArray(configFile.e2e?.specPattern ?? defaultSpecPattern.e2e), + excludeSpecPattern: toArray(configFile.e2e?.excludeSpecPattern ?? defaultExcludeSpecPattern.e2e), + } + } + + return { + specPattern: toArray(configFile.component?.specPattern ?? defaultSpecPattern.component), + excludeSpecPattern: toArray(configFile.component?.excludeSpecPattern ?? defaultExcludeSpecPattern.component), + } + } + async findSpecs ({ projectRoot, testingType, @@ -460,14 +486,21 @@ export class ProjectDataSource { }) } - async matchesSpecPattern (specFile: string): Promise { - if (!this.ctx.currentProject || !this.ctx.coreData.currentTestingType) { + /** + * Determines whether a given spec file satisfies the spec pattern *and* does not satisfy any + * exclusionary pattern. By default it will check the spec pattern for the currently-active + * testing type, but a target testing type can be supplied via optional parameter. + */ + async matchesSpecPattern (specFile: string, testingType?: TestingType): Promise { + const targetTestingType = testingType || this.ctx.coreData.currentTestingType + + if (!this.ctx.currentProject || !targetTestingType) { return false } const MINIMATCH_OPTIONS = { dot: true, matchBase: true } - const { specPattern = [], excludeSpecPattern = [] } = await this.ctx.project.specPatterns() + const { specPattern = [], excludeSpecPattern = [] } = await this.ctx.project.specPatternsByTestingType(targetTestingType) for (const pattern of excludeSpecPattern) { if (minimatch(specFile, pattern, MINIMATCH_OPTIONS)) { diff --git a/packages/data-context/src/sources/UtilDataSource.ts b/packages/data-context/src/sources/UtilDataSource.ts index b17a716c21e5..7ccdf22fd9dc 100644 --- a/packages/data-context/src/sources/UtilDataSource.ts +++ b/packages/data-context/src/sources/UtilDataSource.ts @@ -1,6 +1,6 @@ import fetch from 'cross-fetch' import type { DataContext } from '../DataContext' -import { isDependencyInstalled } from '@packages/scaffold-config' +import { isDependencyInstalled, isDependencyInstalledByName } from '@packages/scaffold-config' // Require rather than import since data-context is stricter than network and there are a fair amount of errors in agent. const { agent } = require('@packages/network') @@ -23,4 +23,8 @@ export class UtilDataSource { isDependencyInstalled (dependency: Cypress.CypressComponentDependency, projectPath: string) { return isDependencyInstalled(dependency, projectPath) } + + isDependencyInstalledByName (packageName: string, projectPath: string) { + return isDependencyInstalledByName(packageName, projectPath) + } } diff --git a/packages/data-context/src/sources/VersionsDataSource.ts b/packages/data-context/src/sources/VersionsDataSource.ts index f2bfcf29d1d5..88cc16f7d254 100644 --- a/packages/data-context/src/sources/VersionsDataSource.ts +++ b/packages/data-context/src/sources/VersionsDataSource.ts @@ -3,13 +3,11 @@ import type { DataContext } from '..' import type { TestingType } from '@packages/types' import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types' import Debug from 'debug' -import { WIZARD_DEPENDENCIES } from '@packages/scaffold-config' -import semver from 'semver' +import { dependencyNamesToDetect } from '@packages/scaffold-config' const debug = Debug('cypress:data-context:sources:VersionsDataSource') const pkg = require('@packages/root') -const nmi = require('node-machine-id') interface Version { id: string @@ -131,7 +129,7 @@ export class VersionsDataSource { return pkg.version } - const id = await VersionsDataSource.machineId() + const id = (await this.ctx.coreData.machineId) || undefined const manifestHeaders: HeadersInit = { 'Content-Type': 'application/json', @@ -162,45 +160,43 @@ export class VersionsDataSource { } } - try { - const projectPath = this.ctx.currentProject - - if (projectPath) { - const dependenciesToCheck = WIZARD_DEPENDENCIES - - debug('Checking %d dependencies in project', dependenciesToCheck.length) - // Check all dependencies of interest in parallel - const dependencyResults = await Promise.allSettled( - dependenciesToCheck.map(async (dependency) => { - const result = await this.ctx.util.isDependencyInstalled(dependency, projectPath) - - // If a dependency isn't satisfied then we are no longer interested in it, - // exclude from further processing by rejecting promise - if (!result.satisfied) { - throw new Error('Unsatisfied dependency') - } - - // We only want major version, fallback to `-1` if we couldn't detect version - const majorVersion = result.detectedVersion ? semver.major(result.detectedVersion) : -1 - - // For any satisfied dependencies, build a `package@version` string - return `${result.dependency.package}@${majorVersion}` - }), - ) - // Take any dependencies that were found and combine into comma-separated string - const headerValue = dependencyResults - .filter(this.isFulfilled) - .map((result) => result.value) - .join(',') - - if (headerValue) { - manifestHeaders['x-dependencies'] = headerValue + if (this._initialLaunch) { + try { + const projectPath = this.ctx.currentProject + + if (projectPath) { + debug('Checking %d dependencies in project', dependencyNamesToDetect.length) + // Check all dependencies of interest in parallel + const dependencyResults = await Promise.allSettled( + dependencyNamesToDetect.map(async (dependency) => { + const result = await this.ctx.util.isDependencyInstalledByName(dependency, projectPath) + + if (!result.detectedVersion) { + throw new Error(`Could not resolve dependency version for ${dependency}`) + } + + // For any satisfied dependencies, build a `package@version` string + return `${result.dependency}@${result.detectedVersion}` + }), + ) + + // Take any dependencies that were found and combine into comma-separated string + const headerValue = dependencyResults + .filter(this.isFulfilled) + .map((result) => result.value) + .join(',') + + if (headerValue) { + manifestHeaders['x-dependencies'] = headerValue + } + } else { + debug('No project path, skipping dependency check') } - } else { - debug('No project path, skipping dependency check') + } catch (err) { + debug('Failed to detect project dependencies', err) } - } catch (err) { - debug('Failed to detect project dependencies', err) + } else { + debug('Not initial launch of Cypress, skipping dependency check') } try { @@ -226,14 +222,6 @@ export class VersionsDataSource { } } - private static async machineId (): Promise { - try { - return await nmi.machineId() - } catch (error) { - return undefined - } - } - private isFulfilled (item: PromiseSettledResult): item is PromiseFulfilledResult { return item.status === 'fulfilled' } diff --git a/packages/data-context/test/unit/actions/EventCollectorActions.spec.ts b/packages/data-context/test/unit/actions/EventCollectorActions.spec.ts index e5fe67b19547..0e989ebdcb76 100644 --- a/packages/data-context/test/unit/actions/EventCollectorActions.spec.ts +++ b/packages/data-context/test/unit/actions/EventCollectorActions.spec.ts @@ -24,13 +24,13 @@ describe('EventCollectorActions', () => { }) context('.recordEvent', () => { - it('makes expected request', async () => { + it('makes expected request for anonymous event', async () => { await actions.recordEvent({ campaign: 'abc', medium: 'def', messageId: 'ghi', cohort: '123', - }) + }, false) expect(ctx.util.fetch).to.have.been.calledOnceWith( sinon.match(/anon-collect$/), // Verify URL ends with expected 'anon-collect' path @@ -38,10 +38,26 @@ describe('EventCollectorActions', () => { ) }) + it('makes expected request for machine-linked event', async () => { + ctx.coreData.machineId = Promise.resolve('xyz') + + await actions.recordEvent({ + campaign: 'abc', + medium: 'def', + messageId: 'ghi', + cohort: '123', + }, true) + + expect(ctx.util.fetch).to.have.been.calledOnceWith( + sinon.match(/machine-collect$/), // Verify URL ends with expected 'machine-collect' path + { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-cypress-version': pkg.version }, body: '{"campaign":"abc","medium":"def","messageId":"ghi","cohort":"123","machineId":"xyz"}' }, + ) + }) + it('resolve true if request succeeds', async () => { (ctx.util.fetch as SinonStub).resolves({} as any) - const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }) + const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false) expect(result).to.eql(true) }) @@ -49,7 +65,7 @@ describe('EventCollectorActions', () => { it('resolves false if request fails', async () => { (ctx.util.fetch as SinonStub).rejects({} as any) - const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }) + const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false) expect(result).to.eql(false) }) diff --git a/packages/data-context/test/unit/actions/ProjectActions.spec.ts b/packages/data-context/test/unit/actions/ProjectActions.spec.ts index dc95bf2175a1..6420d8366126 100644 --- a/packages/data-context/test/unit/actions/ProjectActions.spec.ts +++ b/packages/data-context/test/unit/actions/ProjectActions.spec.ts @@ -103,4 +103,178 @@ describe('ProjectActions', () => { }) }) }) + + describe('runSpec', () => { + context('no project', () => { + it('should fail with `NO_PROJECT`', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + code: 'NO_PROJECT', + detailMessage: sinon.match.string, + }) + }) + }) + + context('empty specPath', () => { + beforeEach(() => { + ctx.coreData.currentProject = '/cy-project' + }) + + it('should fail with `NO_SPEC_PATH`', async () => { + const result = await ctx.actions.project.runSpec({ specPath: '' }) + + sinon.assert.match(result, { + code: 'NO_SPEC_PATH', + detailMessage: sinon.match.string, + }) + }) + }) + + context('no specPattern match', () => { + beforeEach(() => { + ctx.coreData.currentProject = '/cy-project' + sinon.stub(ctx.project, 'matchesSpecPattern').resolves(false) + }) + + it('should fail with `NO_SPEC_PATTERN_MATCH`', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + code: 'NO_SPEC_PATTERN_MATCH', + detailMessage: sinon.match.string, + }) + }) + }) + + context('spec file not found', () => { + beforeEach(() => { + ctx.coreData.currentProject = '/cy-project' + sinon.stub(ctx.project, 'matchesSpecPattern').withArgs(sinon.match.string, 'e2e').resolves(true) + sinon.stub(ctx.fs, 'existsSync').returns(false) + }) + + it('should fail with `SPEC_NOT_FOUND`', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + code: 'SPEC_NOT_FOUND', + detailMessage: sinon.match.string, + }) + }) + }) + + context('matched testing type not configured', () => { + beforeEach(() => { + ctx.coreData.currentTestingType = null + ctx.coreData.currentProject = '/cy-project' + sinon.stub(ctx.project, 'matchesSpecPattern').withArgs(sinon.match.string, 'e2e').resolves(true) + sinon.stub(ctx.fs, 'existsSync').returns(true) + sinon.stub(ctx.lifecycleManager, 'isTestingTypeConfigured').withArgs('e2e').returns(false) + }) + + it('should fail with `TESTING_TYPE_NOT_CONFIGURED`', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + code: 'TESTING_TYPE_NOT_CONFIGURED', + detailMessage: sinon.match.string, + }) + }) + }) + + context('spec can be executed', () => { + beforeEach(() => { + ctx.coreData.currentProject = '/cy-project' + sinon.stub(ctx.project, 'matchesSpecPattern').withArgs(sinon.match.string, 'e2e').resolves(true) + sinon.stub(ctx.fs, 'existsSync').returns(true) + sinon.stub(ctx.project, 'getCurrentSpecByAbsolute').returns({ id: 'xyz' } as any) + sinon.stub(ctx.lifecycleManager, 'setInitialActiveBrowser') + ctx.coreData.activeBrowser = { id: 'abc' } as any + sinon.stub(ctx.lifecycleManager, 'setCurrentTestingType') + sinon.stub(ctx.actions.project, 'switchTestingTypesAndRelaunch') + ctx.coreData.app.browserStatus = 'open' + sinon.stub(ctx.emitter, 'subscribeTo').returns({ + next: () => {}, + return: () => {}, + } as any) + }) + + context('no current testing type', () => { + beforeEach(() => { + ctx.coreData.currentTestingType = null + sinon.stub(ctx.lifecycleManager, 'isTestingTypeConfigured').withArgs('e2e').returns(true) + }) + + it('should succeed', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + testingType: 'e2e', + browser: sinon.match.object, + spec: sinon.match.object, + }) + + expect(ctx.lifecycleManager.setCurrentTestingType).to.have.been.calledWith('e2e') + expect(ctx.actions.project.switchTestingTypesAndRelaunch).to.have.been.calledWith('e2e') + expect(ctx._apis.projectApi.runSpec).to.have.been.called + }) + }) + + context('testing type needs to change', () => { + beforeEach(() => { + ctx.coreData.currentTestingType = 'component' + sinon.stub(ctx.lifecycleManager, 'isTestingTypeConfigured').withArgs('e2e').returns(true) + }) + + it('should succeed', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + testingType: 'e2e', + browser: sinon.match.object, + spec: sinon.match.object, + }) + + expect(ctx.lifecycleManager.setCurrentTestingType).to.have.been.calledWith('e2e') + expect(ctx.actions.project.switchTestingTypesAndRelaunch).to.have.been.calledWith('e2e') + expect(ctx._apis.projectApi.runSpec).to.have.been.called + }) + }) + + context('testing type does not need to change', () => { + beforeEach(() => { + ctx.coreData.currentTestingType = 'e2e' + }) + + it('should succeed', async () => { + const result = await ctx.actions.project.runSpec({ specPath: 'e2e/abc.cy.ts' }) + + sinon.assert.match(result, { + testingType: 'e2e', + browser: sinon.match.object, + spec: sinon.match.object, + }) + + expect(ctx.lifecycleManager.setCurrentTestingType).not.to.have.been.called + expect(ctx.actions.project.switchTestingTypesAndRelaunch).not.to.have.been.called + + expect(ctx._apis.projectApi.runSpec).to.have.been.called + }) + }) + }) + }) + + describe('debugCloudRun', () => { + beforeEach(() => { + sinon.stub(ctx.relevantRuns, 'moveToRun') + }) + + it('should call moveToRun and routeToDebug', async () => { + await ctx.actions.project.debugCloudRun(123) + + expect(ctx.relevantRuns.moveToRun).to.have.been.calledWith(123) + expect(ctx._apis.projectApi.routeToDebug).to.have.been.called + }) + }) }) diff --git a/packages/data-context/test/unit/helper.ts b/packages/data-context/test/unit/helper.ts index 417d74167e06..17091f92143c 100644 --- a/packages/data-context/test/unit/helper.ts +++ b/packages/data-context/test/unit/helper.ts @@ -56,6 +56,8 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run', closeActiveProject: sinon.stub(), insertProjectToCache: sinon.stub().resolves(), getProjectRootsFromCache: sinon.stub().resolves([]), + runSpec: sinon.stub(), + routeToDebug: sinon.stub(), } as unknown as ProjectApiShape, electronApi: { isMainWindowFocused: sinon.stub().returns(false), diff --git a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts index d29e5ffe690e..d50d56c4af9d 100644 --- a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts @@ -12,7 +12,7 @@ import { FoundSpec } from '@packages/types' import { DataContext } from '../../../src' import type { FindSpecs } from '../../../src/actions' import { createTestDataContext } from '../helper' -import { defaultSpecPattern } from '@packages/config' +import { defaultExcludeSpecPattern, defaultSpecPattern } from '@packages/config' import FixturesHelper from '@tooling/system-tests' chai.use(sinonChai) @@ -827,6 +827,7 @@ describe('ProjectDataSource', () => { it('yields correct jsx extension if there are jsx files and specPattern allows', async () => { sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: [defaultSpecPattern.component] }) + sinon.stub(ctx.project, 'specPatternsByTestingType').resolves({ specPattern: [defaultSpecPattern.component] }) const defaultSpecFileName = await ctx.project.defaultSpecFileName() @@ -835,6 +836,7 @@ describe('ProjectDataSource', () => { it('yields non-jsx extension if there are jsx files but specPattern disallows', async () => { sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: ['cypress/component/*.cy.js'] }) + sinon.stub(ctx.project, 'specPatternsByTestingType').resolves({ specPattern: ['cypress/component/*.cy.js'] }) const defaultSpecFileName = await ctx.project.defaultSpecFileName() @@ -843,4 +845,59 @@ describe('ProjectDataSource', () => { }) }) }) + + describe('specPatternsByTestingType', () => { + context('when custom patterns configured', () => { + beforeEach(() => { + sinon.stub(ctx.lifecycleManager, 'getConfigFileContents').resolves({ + e2e: { + specPattern: 'abc', + excludeSpecPattern: 'def', + }, + component: { + specPattern: 'uvw', + excludeSpecPattern: 'xyz', + } as any, + }) + }) + + it('should return custom e2e patterns', async () => { + expect(await ctx.project.specPatternsByTestingType('e2e')).to.eql({ + specPattern: ['abc'], + excludeSpecPattern: ['def'], + }) + }) + + it('should return custom component patterns', async () => { + expect(await ctx.project.specPatternsByTestingType('component')).to.eql({ + specPattern: ['uvw'], + excludeSpecPattern: ['xyz'], + }) + }) + }) + + context('when no custom patterns configured', () => { + const wrapInArray = (value: string | string[]): string[] => { + return Array.isArray(value) ? value : [value] + } + + beforeEach(() => { + sinon.stub(ctx.lifecycleManager, 'getConfigFileContents').resolves({}) + }) + + it('should return default e2e patterns', async () => { + expect(await ctx.project.specPatternsByTestingType('e2e')).to.eql({ + specPattern: wrapInArray(defaultSpecPattern.e2e), + excludeSpecPattern: wrapInArray(defaultExcludeSpecPattern.e2e), + }) + }) + + it('should return default component patterns', async () => { + expect(await ctx.project.specPatternsByTestingType('component')).to.eql({ + specPattern: wrapInArray(defaultSpecPattern.component), + excludeSpecPattern: wrapInArray(defaultExcludeSpecPattern.component), + }) + }) + }) + }) }) diff --git a/packages/data-context/test/unit/sources/VersionsDataSource.spec.ts b/packages/data-context/test/unit/sources/VersionsDataSource.spec.ts index 68285554507d..3bb8cdfc8cbe 100644 --- a/packages/data-context/test/unit/sources/VersionsDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/VersionsDataSource.spec.ts @@ -9,14 +9,12 @@ import { createTestDataContext } from '../helper' import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types' const pkg = require('@packages/root') -const nmi = require('node-machine-id') describe('VersionsDataSource', () => { context('.versions', () => { let ctx: DataContext - let nmiStub: sinon.SinonStub let fetchStub: sinon.SinonStub - let isDependencyInstalledStub: sinon.SinonStub + let isDependencyInstalledByNameStub: sinon.SinonStub let mockNow: Date = new Date() let versionsDataSource: VersionsDataSource let currentCypressVersion: string = pkg.version @@ -33,17 +31,17 @@ describe('VersionsDataSource', () => { }, } + ctx.coreData.machineId = Promise.resolve('abcd123') ctx.coreData.currentProject = '/abc' ctx.coreData.currentTestingType = 'e2e' fetchStub = sinon.stub() - isDependencyInstalledStub = sinon.stub() + isDependencyInstalledByNameStub = sinon.stub() }) beforeEach(() => { - nmiStub = sinon.stub(nmi, 'machineId') sinon.stub(ctx.util, 'fetch').callsFake(fetchStub) - sinon.stub(ctx.util, 'isDependencyInstalled').callsFake(isDependencyInstalledStub) + sinon.stub(ctx.util, 'isDependencyInstalledByName').callsFake(isDependencyInstalledByNameStub) sinon.stub(os, 'platform').returns('darwin') sinon.stub(os, 'arch').returns('x64') sinon.useFakeTimers({ now: mockNow }) @@ -54,8 +52,6 @@ describe('VersionsDataSource', () => { }) it('loads the manifest for the latest version with all headers and queries npm for release dates', async () => { - nmiStub.resolves('abcd123') - fetchStub .withArgs(CYPRESS_REMOTE_MANIFEST_URL, { headers: sinon.match({ @@ -107,7 +103,7 @@ describe('VersionsDataSource', () => { it('resets telemetry data triggering a new call to get the latest version', async () => { const currentCypressVersion = pkg.version - nmiStub.rejects('Error while obtaining machine id') + ctx.coreData.machineId = Promise.resolve(null) ctx.coreData.currentTestingType = 'component' fetchStub @@ -140,8 +136,6 @@ describe('VersionsDataSource', () => { }) it('handles errors fetching version data', async () => { - nmiStub.resolves('abcd123') - fetchStub .withArgs(CYPRESS_REMOTE_MANIFEST_URL, { headers: sinon.match({ @@ -167,8 +161,6 @@ describe('VersionsDataSource', () => { }) it('handles invalid response errors', async () => { - nmiStub.resolves('abcd123') - fetchStub .withArgs(CYPRESS_REMOTE_MANIFEST_URL, { headers: sinon.match({ @@ -202,31 +194,41 @@ describe('VersionsDataSource', () => { }) it('generates x-framework, x-bundler, and x-dependencies headers', async () => { - isDependencyInstalledStub.callsFake(async (dependency) => { + isDependencyInstalledByNameStub.callsFake(async (packageName) => { // Should include any resolved dependency with a valid version - if (dependency.package === 'react') { + if (packageName === 'react') { return { - dependency, + dependency: packageName, detectedVersion: '1.2.3', - satisfied: true, } as Cypress.DependencyToInstall } - // Not satisfied dependency should be excluded - if (dependency.package === 'vue') { + if (packageName === 'vue') { return { - dependency, + dependency: packageName, detectedVersion: '4.5.6', - satisfied: false, } } - // Satisfied dependency without resolved version should result in -1 - if (dependency.package === 'typescript') { + if (packageName === '@builder.io/qwik') { + return { + dependency: packageName, + detectedVersion: '1.1.4', + } + } + + if (packageName === '@playwright/experimental-ct-core') { + return { + dependency: packageName, + detectedVersion: '1.33.0', + } + } + + // Dependency without resolved version should be excluded + if (packageName === 'typescript') { return { - dependency, + dependency: packageName, detectedVersion: null, - satisfied: true, } } @@ -246,7 +248,7 @@ describe('VersionsDataSource', () => { headers: sinon.match({ 'x-framework': 'react', 'x-dev-server': 'vite', - 'x-dependencies': 'typescript@-1,react@1', + 'x-dependencies': 'react@1.2.3,vue@4.5.6,@builder.io/qwik@1.1.4,@playwright/experimental-ct-core@1.33.0', }), }, ) diff --git a/packages/driver/cypress/plugins/index.js b/packages/driver/cypress/plugins/index.js index ee2501ed40f7..3968f8e60043 100644 --- a/packages/driver/cypress/plugins/index.js +++ b/packages/driver/cypress/plugins/index.js @@ -15,8 +15,6 @@ async function getWebpackOptions () { const webpackOptions = opts[0] - console.log(webpackOptions) - // set mode to development which overrides // the 'none' value of the base webpack config // https://webpack.js.org/configuration/mode/ diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 9850d6d92ca8..1afd7f2b9444 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -616,6 +616,9 @@ class $Cypress { case 'cy:command:end': return this.emit('command:end', ...args) + case 'cy:command:failed': + return this.emit('command:failed', ...args) + case 'cy:skipped:command:end': return this.emit('skipped:command:end', ...args) diff --git a/packages/driver/src/cypress/command_queue.ts b/packages/driver/src/cypress/command_queue.ts index f442657f63d9..be28d47858cb 100644 --- a/packages/driver/src/cypress/command_queue.ts +++ b/packages/driver/src/cypress/command_queue.ts @@ -541,6 +541,7 @@ export class CommandQueue extends Queue<$Command> { current.fail() } + Cypress.action('cy:command:failed', current, err) this.cleanup() return this.cy.fail(err) diff --git a/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html b/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_ARTIFACTS.html similarity index 94% rename from packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html rename to packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_ARTIFACTS.html index 3068e21d2321..e8d2ceec9ecb 100644 --- a/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_RESULTS.html +++ b/packages/errors/__snapshot-html__/CLOUD_CANNOT_UPLOAD_ARTIFACTS.html @@ -34,7 +34,7 @@ -
Warning: We encountered an error while uploading results from your run.
+    
Warning: We encountered an error while uploading screenshots & videos from your run.
 
 These results will not be recorded.
 
diff --git a/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html b/packages/errors/__snapshot-html__/VIDEO_CAPTURE_FAILED.html
similarity index 78%
rename from packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html
rename to packages/errors/__snapshot-html__/VIDEO_CAPTURE_FAILED.html
index 5f0017b8e6fb..dc8c80bf8db7 100644
--- a/packages/errors/__snapshot-html__/VIDEO_POST_PROCESSING_FAILED.html
+++ b/packages/errors/__snapshot-html__/VIDEO_CAPTURE_FAILED.html
@@ -34,11 +34,11 @@
     
   
     
-    
Warning: We failed processing this video.
+    
Warning: We failed capturing this video.
 
 This error will not affect or change the exit code.
 
 Error: fail whale
     at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
-    at VIDEO_POST_PROCESSING_FAILED (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+    at VIDEO_CAPTURE_FAILED (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/VIDEO_COMPRESSION_FAILED.html b/packages/errors/__snapshot-html__/VIDEO_COMPRESSION_FAILED.html new file mode 100644 index 000000000000..343b4fa4c424 --- /dev/null +++ b/packages/errors/__snapshot-html__/VIDEO_COMPRESSION_FAILED.html @@ -0,0 +1,44 @@ + + + + + + + + + + + +
Warning: We failed compressing this video.
+
+This error will not affect or change the exit code.
+
+Error: fail whale
+    at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+    at VIDEO_COMPRESSION_FAILED (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index bebb87b26e86..c3151f51a5b8 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -75,9 +75,17 @@ export const AllCypressErrors = { ${fmt.stackTrace(arg1)}` }, - VIDEO_POST_PROCESSING_FAILED: (arg1: Error) => { + VIDEO_CAPTURE_FAILED: (arg1: Error) => { return errTemplate`\ - Warning: We failed processing this video. + Warning: We failed capturing this video. + + This error will not affect or change the exit code. + + ${fmt.stackTrace(arg1)}` + }, + VIDEO_COMPRESSION_FAILED: (arg1: Error) => { + return errTemplate`\ + Warning: We failed compressing this video. This error will not affect or change the exit code. @@ -354,6 +362,7 @@ export const AllCypressErrors = { ${fmt.highlightSecondary(`Auto Cancellation`)} is not included under your current billing plan. To enable this service, please visit your billing and upgrade to another plan with Auto Cancellation. + ${fmt.off(arg1.link)}` }, CLOUD_AUTO_CANCEL_MISMATCH: (arg1: {runUrl: string}) => { @@ -371,6 +380,7 @@ export const AllCypressErrors = { })} The first setting of --auto-cancel-after-failures for any given run takes precedent. + https://on.cypress.io/auto-cancellation-mismatch` }, DEPRECATED_BEFORE_BROWSER_LAUNCH_ARGS: () => { @@ -510,9 +520,9 @@ export const AllCypressErrors = { This error will not affect or change the exit code.` }, - CLOUD_CANNOT_UPLOAD_RESULTS: (apiErr: Error) => { + CLOUD_CANNOT_UPLOAD_ARTIFACTS: (apiErr: Error) => { return errTemplate`\ - Warning: We encountered an error while uploading results from your run. + Warning: We encountered an error while uploading screenshots & videos from your run. These results will not be recorded. diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index 829f2ee8cb58..e5f0b656decd 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -325,7 +325,14 @@ describe('visual error templates', () => { default: [err], } }, - VIDEO_POST_PROCESSING_FAILED: () => { + VIDEO_CAPTURE_FAILED: () => { + const err = makeErr() + + return { + default: [err], + } + }, + VIDEO_COMPRESSION_FAILED: () => { const err = makeErr() return { @@ -618,7 +625,7 @@ describe('visual error templates', () => { default: [], } }, - CLOUD_CANNOT_UPLOAD_RESULTS: () => { + CLOUD_CANNOT_UPLOAD_ARTIFACTS: () => { const err = makeApiErr() return { diff --git a/packages/example/package.json b/packages/example/package.json index 7a0e5951f35e..3c3fe4d62437 100644 --- a/packages/example/package.json +++ b/packages/example/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "cross-env": "6.0.3", - "cypress-example-kitchensink": "1.16.0", + "cypress-example-kitchensink": "1.17.1", "gh-pages": "5.0.0", "gulp": "4.0.2", "gulp-clean": "0.4.0", diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts index 06b3435c5d31..fba7edafd867 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts @@ -29,6 +29,7 @@ export const createTestCurrentProject = (title: string, currentProject: Partial< return { ...globalProject, __typename: 'CurrentProject', + savedState: {}, isCTConfigured: true, serveConfig: {}, isE2EConfigured: true, diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index d873f7ce25f2..befa473d1038 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -18,15 +18,15 @@ }, "dependencies": {}, "devDependencies": { - "@antfu/utils": "^0.3.0", - "@cypress-design/css": "^0.11.0", + "@antfu/utils": "^0.7.0", + "@cypress-design/css": "^0.13.3", "@graphql-typed-document-node/core": "^3.1.0", "@headlessui/vue": "1.4.0", "@iconify/json": "1.1.368", "@iconify/vue": "3.0.0-beta.1", "@intlify/unplugin-vue-i18n": "0.10.0", "@percy/core": "^1.0.0-beta.48", - "@percy/cypress": "^3.1.0", + "@percy/cypress": "^3.1.2", "@testing-library/cypress": "9.0.0", "@toycode/markdown-it-class": "1.2.3", "@types/faker": "5.5.8", diff --git a/packages/frontend-shared/src/assets/icons/illustration-chart_x120.svg b/packages/frontend-shared/src/assets/icons/illustration-chart_x120.svg deleted file mode 100644 index 1dfd8689c224..000000000000 --- a/packages/frontend-shared/src/assets/icons/illustration-chart_x120.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/frontend-shared/src/assets/icons/illustration-debug_x120.svg b/packages/frontend-shared/src/assets/icons/illustration-debug_x120.svg deleted file mode 100644 index cd6906b1c22e..000000000000 --- a/packages/frontend-shared/src/assets/icons/illustration-debug_x120.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/packages/frontend-shared/src/assets/icons/illustration-gear_x120.svg b/packages/frontend-shared/src/assets/icons/illustration-gear_x120.svg deleted file mode 100644 index 9400a40d99de..000000000000 --- a/packages/frontend-shared/src/assets/icons/illustration-gear_x120.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/launchpad/src/images/logos/angular.svg b/packages/frontend-shared/src/assets/logos/angular.svg similarity index 100% rename from packages/launchpad/src/images/logos/angular.svg rename to packages/frontend-shared/src/assets/logos/angular.svg diff --git a/packages/launchpad/src/images/logos/nextjs.svg b/packages/frontend-shared/src/assets/logos/nextjs.svg similarity index 100% rename from packages/launchpad/src/images/logos/nextjs.svg rename to packages/frontend-shared/src/assets/logos/nextjs.svg diff --git a/packages/launchpad/src/images/logos/nuxt.svg b/packages/frontend-shared/src/assets/logos/nuxt.svg similarity index 100% rename from packages/launchpad/src/images/logos/nuxt.svg rename to packages/frontend-shared/src/assets/logos/nuxt.svg diff --git a/packages/launchpad/src/images/logos/react.svg b/packages/frontend-shared/src/assets/logos/react.svg similarity index 100% rename from packages/launchpad/src/images/logos/react.svg rename to packages/frontend-shared/src/assets/logos/react.svg diff --git a/packages/launchpad/src/images/logos/svelte.svg b/packages/frontend-shared/src/assets/logos/svelte.svg similarity index 100% rename from packages/launchpad/src/images/logos/svelte.svg rename to packages/frontend-shared/src/assets/logos/svelte.svg diff --git a/packages/launchpad/src/images/logos/vite.svg b/packages/frontend-shared/src/assets/logos/vite.svg similarity index 100% rename from packages/launchpad/src/images/logos/vite.svg rename to packages/frontend-shared/src/assets/logos/vite.svg diff --git a/packages/launchpad/src/images/logos/vue.svg b/packages/frontend-shared/src/assets/logos/vue.svg similarity index 100% rename from packages/launchpad/src/images/logos/vue.svg rename to packages/frontend-shared/src/assets/logos/vue.svg diff --git a/packages/launchpad/src/images/logos/webpack.svg b/packages/frontend-shared/src/assets/logos/webpack.svg similarity index 100% rename from packages/launchpad/src/images/logos/webpack.svg rename to packages/frontend-shared/src/assets/logos/webpack.svg diff --git a/packages/frontend-shared/src/components/Alert.cy.tsx b/packages/frontend-shared/src/components/Alert.cy.tsx index 0107736697cf..57f9c9133d21 100644 --- a/packages/frontend-shared/src/components/Alert.cy.tsx +++ b/packages/frontend-shared/src/components/Alert.cy.tsx @@ -38,18 +38,6 @@ const prefixIcon = () => const suffixIcon = () => describe('', () => { - describe('classes', () => { - it('can change the text and background color for the alert', () => { - cy.mount(() => - (
- test - test -
)) - - cy.percySnapshot() - }) - }) - describe('title', () => { it('can accept slot as title slot', () => { cy.mount(() => (', () => { }) }) -describe('playground', () => { - it('renders', () => { +describe('', () => { + it('renders various alerts with customizations', () => { const { modelValue, methods } = makeDismissibleProps() cy.mount(() => { @@ -259,11 +247,12 @@ describe('playground', () => { {...methods}>Close me, please! A notice. Default alert + test + test ) }) - cy.contains('Coffee, please').should('be.visible') cy.percySnapshot() }) }) diff --git a/packages/frontend-shared/src/components/Alert.vue b/packages/frontend-shared/src/components/Alert.vue index 9bd1e6fe8a9f..75f678035cd8 100644 --- a/packages/frontend-shared/src/components/Alert.vue +++ b/packages/frontend-shared/src/components/Alert.vue @@ -23,7 +23,7 @@ :title="title" :header-class="`${props.headerClass} ${canCollapse ? 'group-hocus:underline' : ''}`" :prefix-icon="prefix?.icon" - :prefix-icon-class="open ? prefix?.classes + ' rotate-180' : prefix?.classes" + :prefix-icon-class="(open && collapsible) ? prefix?.classes + ' rotate-180' : prefix?.classes" :suffix-icon-aria-label="props.dismissible ? t('components.alert.dismissAriaLabel') : ''" :suffix-icon="props.dismissible ? DeleteIcon : null" :suffix-button-class="classes.suffixButtonClass" @@ -73,7 +73,7 @@ diff --git a/packages/frontend-shared/src/gql-components/HeaderBarContent.cy.tsx b/packages/frontend-shared/src/gql-components/HeaderBarContent.cy.tsx index b4e46bea0903..de8c012a7faa 100644 --- a/packages/frontend-shared/src/gql-components/HeaderBarContent.cy.tsx +++ b/packages/frontend-shared/src/gql-components/HeaderBarContent.cy.tsx @@ -2,7 +2,7 @@ import { HeaderBar_HeaderBarContentFragmentDoc } from '../generated/graphql-test import HeaderBarContent from './HeaderBarContent.vue' import { defaultMessages } from '@cy/i18n' import { CloudUserStubs } from '@packages/graphql/test/stubCloudTypes' -import { useLoginConnectStore } from '../store/login-connect-store' +import { useUserProjectStatusStore } from '../store/user-project-status-store' const text = defaultMessages.topNav @@ -22,8 +22,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( render: (gqlVal) =>
, }) - cy.percySnapshot('before browsers open') - cy.findByTestId('top-nav-active-browser') .should('be.visible') .click() @@ -63,10 +61,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( }) describe('breadcrumbs', () => { - afterEach(() => { - cy.percySnapshot() - }) - context('with current project', () => { const currentProject = { title: 'app', @@ -94,8 +88,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( cy.get('.truncate').contains('application-program/hard-drive-parse').should('be.visible') - cy.percySnapshot() - cy.get('.truncate').realHover() cy.get('.v-popper__popper--shown').contains('application-program/hard-drive-parse') }) @@ -134,7 +126,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( mountFragmentWithData() cy.findByTestId('top-nav-active-browser').should('not.exist') - cy.percySnapshot() cy.contains('button', text.docsMenu.docsHeading).click() cy.wrap(Object.values(text.docsMenu)).each((menuItem) => { @@ -143,15 +134,15 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( } }) - cy.percySnapshot() cy.get('body').click() cy.contains('a', text.docsMenu.firstTest).should('not.be.visible') - cy.percySnapshot('after click') }) it('docs menu has expected links with no current project', () => { mountFragmentWithData({ currentProject: null }) + cy.contains('a', 'Projects').should('not.exist') + // we render without a current project to validate ciSetup and fasterTests links // because outside of global mode, those are buttons that trigger popups // so this way we can assert all links at once. @@ -189,10 +180,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( }) }) - afterEach(() => { - cy.percySnapshot() - }) - // https://github.com/cypress-io/cypress/issues/21842 it('shows docs menu correctly on small viewports', () => { // Simulate the small viewport. @@ -203,6 +190,8 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( // docs menu flex direction is column when viewport width is small cy.findByTestId('docs-menu-container').should('have.css', 'flex-direction', 'column') + + cy.percySnapshot() }) it('shows docs menu correctly on wider viewports', () => { @@ -214,6 +203,7 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( // docs menu flex direction is row when viewport width is big enough. cy.findByTestId('docs-menu-container').should('have.css', 'flex-direction', 'row') + cy.percySnapshot() }) }) @@ -237,7 +227,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( }) cy.contains('a', '8.7.0').should('be.visible').and('have.attr', 'href', 'https://on.cypress.io/changelog#8-7-0') - cy.percySnapshot() }) it('shows hint and modal to upgrade to latest version of cypress', () => { @@ -278,9 +267,7 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( ), }) - cy.contains('v8.6.0 • Upgrade').should('be.visible') - cy.percySnapshot('before upgrade click') - cy.contains('v8.6.0 • Upgrade').click() + cy.contains('v8.6.0 • Upgrade').should('be.visible').click() cy.findByTestId('latest-version').contains('8.7.0') cy.findByTestId('current-version').contains('8.6.0') cy.findByTestId('update-hint').should('be.visible') @@ -290,16 +277,17 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( cy.contains(`${defaultMessages.topNav.updateCypress.title} 8.7.0`).should('be.visible') cy.contains('test-project').should('be.visible') cy.findByDisplayValue('yarn add -D cypress@8.7.0').should('be.visible') - cy.percySnapshot('after upgrade modal open') + cy.get('p').contains('You are currently running Version 8.6.0 of Cypress. To upgrade to the latest version for your project, first close this app, then paste the command below into your terminal:') cy.get('body').type('{esc}') // dismiss modal with keyboard cy.contains(`${defaultMessages.topNav.updateCypress.title} 8.7.0`).should('not.exist') + cy.percySnapshot() }) it('the logged in state is correctly presented in header', () => { - const loginConnectStore = useLoginConnectStore() + const userProjectStatusStore = useUserProjectStatusStore() - loginConnectStore.setUserFlag('isLoggedIn', true) + userProjectStatusStore.setUserFlag('isLoggedIn', true) const cloudViewer = { ...CloudUserStubs.me, @@ -313,7 +301,7 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( fullName: 'Tester Test', } - loginConnectStore.setUserData(cloudViewer) + userProjectStatusStore.setUserData(cloudViewer) cy.mountFragment(HeaderBar_HeaderBarContentFragmentDoc, { onResult: (result) => { @@ -339,14 +327,9 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( cy.contains('Project').should('not.exist') cy.contains('Test Page').should('be.visible') - cy.percySnapshot() }) describe('prompts', () => { - afterEach(() => { - cy.percySnapshot() - }) - describe('the CI prompt', () => { context('opens on click', () => { beforeEach(() => { @@ -360,7 +343,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( it('opens on menu item click', () => { cy.contains(defaultMessages.topNav.docsMenu.prompts.ci1.description).should('be.visible') - cy.percySnapshot() }) it('is dismissible from X icon', () => { @@ -486,7 +468,6 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( }) it('opens on menu item click', () => { - cy.percySnapshot() cy.contains(defaultMessages.topNav.docsMenu.prompts.orchestration1.title).should('be.visible') cy.contains('Getting Started').should('not.exist') }) diff --git a/packages/frontend-shared/src/gql-components/HeaderBarContent.vue b/packages/frontend-shared/src/gql-components/HeaderBarContent.vue index a9f91d5b430e..90532a9fdd6d 100644 --- a/packages/frontend-shared/src/gql-components/HeaderBarContent.vue +++ b/packages/frontend-shared/src/gql-components/HeaderBarContent.vue @@ -94,18 +94,18 @@ @clear-force-open="isForceOpenAllowed = false" > -
+