diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt index 1275f06c8a72..948de64c5a39 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. -5-14-2025 +6-9-2025 diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 6d100d6e6828..5a11f23ba7a8 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -38,7 +38,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' - - 'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40' + - 'feat/add_built_ins_minimal' # 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 @@ -49,11 +49,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: - [ - 'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40', - << pipeline.git.branch >> - ] + - equal: [ 'feat/add_built_ins_minimal', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -64,11 +60,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: - [ - 'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40', - << pipeline.git.branch >> - ] + - equal: [ 'feat/add_built_ins_minimal', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -91,11 +83,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: - [ - 'update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40', - << pipeline.git.branch >> - ] + - equal: [ 'feat/add_built_ins_minimal', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -169,7 +157,7 @@ commands: name: Set environment variable to determine whether or not to persist artifacts command: | echo "Setting SHOULD_PERSIST_ARTIFACTS variable" - echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "update-chrome-stable-from-136.0.7103.113-beta-from-137.0.7151.40" ]]; then + echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "feat/add_built_ins_minimal" ]]; then export SHOULD_PERSIST_ARTIFACTS=true fi' >> "$BASH_ENV" # You must run `setup_should_persist_artifacts` command and be using bash before running this command @@ -688,11 +676,6 @@ commands: description: run subset of tests with injectDocumentDomain config enabled type: boolean default: false - is-firefox-cdp: - description: whether or not the group should be associated to the firefox CDP - run or not. This is determined by the browser version. - type: boolean - default: false steps: - restore_cached_workspace @@ -733,9 +716,6 @@ commands: if << parameters.inject-document-domain >> ; then YARN_CMD="cypress:run:inject-document-domain" PARALLEL="--parallel --group 5x-driver-inject-document-domain-<>" - elif << parameters.is-firefox-cdp >> ; then - YARN_CMD="cypress:run" - PARALLEL="--parallel --group 5x-driver-cdp-<>" else YARN_CMD="cypress:run" PARALLEL="--parallel --group 5x-driver-<>" @@ -1201,23 +1181,6 @@ commands: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm install --legacy-peer-deps ~/cypress/cypress.tgz fi working_directory: /tmp/<> - - run: - name: Scaffold new config file - working_directory: /tmp/<> - environment: - CYPRESS_INTERNAL_FORCE_SCAFFOLD: "1" - command: | - if [[ -f cypress.json ]]; then - rm -rf cypress.json - echo 'module.exports = { e2e: {} }' > cypress.config.js - fi - - run: - name: Rename support file - working_directory: /tmp/<> - command: | - if [[ -f cypress/support/index.js ]]; then - mv cypress/support/index.js cypress/support/e2e.js - fi - run: name: Print Cypress version working_directory: /tmp/<> @@ -2170,18 +2133,6 @@ jobs: - run-driver-integration-tests: browser: firefox - # Runs the driver tests using firefox 134, which does NOT use WebDriver BiDi - # This is to test and make sure there aren't regressions with the old CDP driver - driver-integration-tests-firefox-cdp: - <<: *defaults - resource_class: medium+ - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: firefox - firefox-version: "134.0.2" - is-firefox-cdp: true - driver-integration-tests-electron: <<: *defaults parallelism: 5 @@ -2922,7 +2873,6 @@ linux-x64-workflow: &linux-x64-workflow - run-webpack-dev-server-integration-tests - run-vite-dev-server-integration-tests - driver-integration-tests-firefox - - driver-integration-tests-firefox-cdp - driver-integration-tests-chrome - driver-integration-tests-chrome-inject-document-domain - driver-integration-tests-chrome-beta-inject-document-domain @@ -2998,10 +2948,6 @@ linux-x64-workflow: &linux-x64-workflow context: test-runner:cypress-record-key requires: - build - - driver-integration-tests-firefox-cdp: - context: test-runner:cypress-record-key - requires: - - build - driver-integration-tests-electron: context: test-runner:cypress-record-key requires: @@ -3144,7 +3090,6 @@ linux-x64-workflow: &linux-x64-workflow - linux-lint - percy-finalize - driver-integration-tests-firefox - - driver-integration-tests-firefox-cdp - driver-integration-tests-chrome - driver-integration-tests-chrome-beta - driver-integration-tests-chrome-inject-document-domain @@ -3400,10 +3345,6 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow context: test-runner:cypress-record-key requires: - contributor-pr - - driver-integration-tests-firefox-cdp: - context: test-runner:cypress-record-key - requires: - - contributor-pr - driver-integration-tests-electron: context: test-runner:cypress-record-key requires: @@ -3545,7 +3486,6 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow - linux-lint - percy-finalize - driver-integration-tests-firefox - - driver-integration-tests-firefox-cdp - driver-integration-tests-chrome - driver-integration-tests-chrome-beta - driver-integration-tests-electron diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index c8523a765f7d..606e77bfbff3 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,4 +1,30 @@ +## 15.0.0 + +_Released 07/01/2025 (PENDING)_ + +**Breaking Changes:** + +- Removed support for Angular 17. The minimum supported version is now `18.0.0`. Addresses [#31303](https://github.com/cypress-io/cypress/issues/31303). +- `@cypress/angular` now requires a minimum of `zone.js` `0.14.0`. Addresses [#31582](https://github.com/cypress-io/cypress/issues/31582). +- Removed support for Node.js 18 and Node.js 23. Addresses [#31302](https://github.com/cypress-io/cypress/issues/31302). +- Removed support for [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol) with the [firefox](https://www.mozilla.org/) browser. Addresses [#31189](https://github.com/cypress-io/cypress/issues/31189). +- Removed support of the deprecated 3 argument signature of `cy.stub`. Use `cy.stub(object, name).callsFake(fn)` instead. Addresses [#31346](https://github.com/cypress-io/cypress/issues/31346). +- The Cypress configuration wizard for Component Testing supports TypeScript 5.0 or greater. Addresses [#31187](https://github.com/cypress-io/cypress/issues/31187). +- `@cypress/webpack-dev-server` and `@cypress/webpack-preprocessor` no longer support `webpack` version 4. Addresses [#31344](https://github.com/cypress-io/cypress/issues/31344). If you still need to use `webpack` version 4, please see our [migration guide](https://docs.cypress.io/app/references/migration-guide#Migrating-to-Cypress-150). +- `@cypress/webpack-dev-server` no longer supports `webpack-dev-server` version 4. Addresses [#31605](https://github.com/cypress-io/cypress/issues/31605). If you still need to use `webpack-dev-server` version 4, please see our [migration guide](https://docs.cypress.io/app/references/migration-guide#Migrating-to-Cypress-150). +- In order to better align with best practices, `@cypress/webpack-batteries-included-preprocessor` no longer includes certain browser built-ins that were automatically provided by Webpack 4. The removed built-ins are `assert`, `constants`, `crypto`, `domain`, `events`, `http`, `https`, `punycode`, `querystring`, `string_decoder`, `sys`, `timers`, `tty`, `url`, `util`, `vm`, and `zlib`. However, we know that certain built-ins are popular, given that many users have files that are shared between their Cypress tests and node context. Because of this, `@cypress/webpack-batteries-included-preprocessor` will ship with built-in support for `buffer`, `path`, `process`, `os`, and `stream`. If there is a built-in that isn't supported be default and you need to add support, please refer to the Webpack [resolve.fallback](https://webpack.js.org/configuration/resolve/#resolvefallback) documentation and the [`@cypress/webpack-batteries-included-preprocessor` README](../npm/webpack-batteries-included-preprocessor/README.md). Addresses [#31039](https://github.com/cypress-io/cypress/issues/31039). +- The application under test's `pagehide` event in Chromium browsers will no longer trigger Cypress's `window:unload` event. Addressed in [#31853](https://github.com/cypress-io/cypress/pull/31853). + +**Features:** + +- [`tsx`](https://tsx.is/) is now used in all cases to run the Cypress config, replacing [ts-node](https://github.com/TypeStrong/ts-node) for TypeScript and Node for commonjs/ESM. This should allow for more interoperability for users who are using any variant of ES Modules. Addresses [#8090](https://github.com/cypress-io/cypress/issues/8090), [#15724](https://github.com/cypress-io/cypress/issues/15724), [#21805](https://github.com/cypress-io/cypress/issues/21805), [#22273](https://github.com/cypress-io/cypress/issues/22273), [#22747](https://github.com/cypress-io/cypress/issues/22747), [#23141](https://github.com/cypress-io/cypress/issues/23141), [#25958](https://github.com/cypress-io/cypress/issues/25958), [#25959](https://github.com/cypress-io/cypress/issues/25959), [#26606](https://github.com/cypress-io/cypress/issues/26606), [#27359](https://github.com/cypress-io/cypress/issues/27359), [#27450](https://github.com/cypress-io/cypress/issues/27450), [#28442](https://github.com/cypress-io/cypress/issues/28442), [#30318](https://github.com/cypress-io/cypress/issues/30318), [#30718](https://github.com/cypress-io/cypress/issues/30718), [#30907](https://github.com/cypress-io/cypress/issues/30907), [#30915](https://github.com/cypress-io/cypress/issues/30915), [#30925](https://github.com/cypress-io/cypress/issues/30925), [#30954](https://github.com/cypress-io/cypress/issues/30954) and [#31185](https://github.com/cypress-io/cypress/issues/31185). +- [`cy.url()`](https://docs.cypress.io/api/commands/url), [`cy.hash()`](https://docs.cypress.io/api/commands/hash), [`cy.go()`](https://docs.cypress.io/api/commands/go), [`cy.reload()`](https://docs.cypress.io/api/commands/reload), [`cy.title()`](https://docs.cypress.io/api/commands/title), and [`cy.location()`](https://docs.cypress.io/api/commands/location) now use the automation client (CDP for Chromium browsers and WebDriver BiDi for Firefox) to return the appropriate values from the commands to the user instead of the window object. This is to avoid cross origin issues with [`cy.origin()`](https://docs.cypress.io/api/commands/origin) so these commands can be invoked anywhere inside a Cypress test without having to worry about origin access issues. Experimental Webkit still will use the window object to retrieve these values. Also, [`cy.window()`](https://docs.cypress.io/api/commands/window) will always return the current window object, regardless of origin restrictions. Not every property from the window object will be accessible depending on the origin context. Addresses [#31196](https://github.com/cypress-io/cypress/issues/31196). + +**Misc:** + +- Migration helpers and related errors are no longer shown when upgrading from Cypress versions earlier than 10.0.0. To migrate from a pre-10.0.0 version, upgrade one major version at a time to receive the appropriate guidance. Addresses [#31345](https://github.com/cypress-io/cypress/issues/31345). Addressed in [https://github.com/cypress-io/cypress/pull/31629/](https://github.com/cypress-io/cypress/pull/31629/). + ## 14.5.0 _Released 6/17/2025_ diff --git a/cli/package.json b/cli/package.json index 6bc9c1b46f54..5e10cdf6e4bb 100644 --- a/cli/package.json +++ b/cli/package.json @@ -120,7 +120,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "types": "types", "exports": { diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 65f51f96722e..11c8df64e6ce 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -2126,12 +2126,6 @@ declare namespace Cypress { * expect(o.toString).to.have.been.calledOnce */ stub(obj: T, method: keyof T): Agent - /** - * Stubs a method on an object - * - * @deprecated Use `cy.stub(object, name).callsFake(fn)` instead - */ - stub(obj: T, method: keyof T, func: (...args: any[]) => any): Agent /** * Submit a form. diff --git a/cli/types/tests/plugins-config.ts b/cli/types/tests/plugins-config.ts index 8b92a5010320..14966c217838 100644 --- a/cli/types/tests/plugins-config.ts +++ b/cli/types/tests/plugins-config.ts @@ -1,4 +1,4 @@ -// checking types passed to cypress/plugins/index.js file +// checking types passed through setupNodeEvents // does nothing const pluginConfig: Cypress.PluginConfig = (on, config) => {} diff --git a/guides/error-handling.md b/guides/error-handling.md index 4ae4daf3d021..24ca4d3578fa 100644 --- a/guides/error-handling.md +++ b/guides/error-handling.md @@ -4,7 +4,7 @@ Clear, consistent, errors are one of the important parts of the Cypress experien ### @packages/errors -All error related logic for the server should be added to `@packages/errors`. This logic has been separated out from the `@packages/server` to enable strict type checking & use in other packages we have added in the `10.0-release` branch. +All error related logic for the server should be added to `@packages/errors`. ### Errors Development Workflow @@ -78,15 +78,15 @@ In this case, `arg1` will be highlighted in yellow when printed to the terminal. ```ts -PLUGINS_FILE_ERROR: (arg1: string, arg2: Error) => { +FAKE_ERROR: (arg1: string, arg2: Error) => { return errTemplate`\ - The plugins file is missing or invalid. + The fake file is missing or invalid. - Your \`pluginsFile\` is set to ${arg1}, but either the file is missing, it contains a syntax error, or threw an error when required. The \`pluginsFile\` must be a \`.js\`, \`.ts\`, or \`.coffee\` file. + Your \`fakeFile\` is set to ${arg1}, but either the file is missing, it contains a syntax error, or threw an error when required. The \`fakeFile\` must be a \`.js\`, \`.ts\`, or \`.coffee\` file. - Or you might have renamed the extension of your \`pluginsFile\`. If that's the case, restart the test runner. + Or you might have renamed the extension of your \`fakeFile\`. If that's the case, restart the test runner. - Please fix this, or set \`pluginsFile\` to \`false\` if a plugins file is not necessary for your project. + Please fix this, or set \`fakeFile\` to \`false\` if a plugins file is not necessary for your project. ${details(arg2)} ` diff --git a/npm/angular/README.md b/npm/angular/README.md index 0ca548725828..988a2663bb8b 100644 --- a/npm/angular/README.md +++ b/npm/angular/README.md @@ -6,7 +6,7 @@ Mount Angular components in the open source [Cypress.io](https://www.cypress.io/ ## Requirements -- Angular 17.2.0+ (Cypress 13 and under supports Angular 13 - 16) +- Angular 18.0.0+ (`@cypress/angular@2` supports Angular 13 - 16 and `@cypress/angular@3` supports Angular 17) ## Development diff --git a/npm/angular/package.json b/npm/angular/package.json index a2b02d0cecf4..fa6006740099 100644 --- a/npm/angular/package.json +++ b/npm/angular/package.json @@ -13,20 +13,20 @@ }, "dependencies": {}, "devDependencies": { - "@angular/common": "^17.2.0", - "@angular/core": "^17.2.0", - "@angular/platform-browser-dynamic": "^17.2.0", + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/platform-browser-dynamic": "^18.0.0", "@cypress/mount-utils": "0.0.0-development", "rollup": "^4.24.4", "typescript": "~5.4.5", "zone.js": "~0.14.6" }, "peerDependencies": { - "@angular/common": ">=17.2", - "@angular/core": ">=17.2", - "@angular/platform-browser-dynamic": ">=17.2", + "@angular/common": ">=18.0.0", + "@angular/core": ">=18.0.0", + "@angular/platform-browser-dynamic": ">=18.0.0", "rxjs": ">=7.5.0", - "zone.js": ">=0.13.0" + "zone.js": ">=0.14.0" }, "files": [ "dist" diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index 982c3200adee..fbc23cb160dd 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -123,12 +123,6 @@ export type MountResponse = { component: T }; -// 'zone.js/testing' is not properly aliasing `it.skip` but it does provide `xit`/`xspecify` -// Written up under https://github.com/angular/angular/issues/46297 but is not seeing movement -// so we'll patch here pending a fix in that library -// @ts-ignore Ignore so that way we can bypass semantic error TS7017: Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature. -globalThis.it.skip = globalThis.xit - @Injectable() class CypressAngularErrorHandler implements ErrorHandler { handleError (error: Error): void { diff --git a/npm/cypress-schematic/README.md b/npm/cypress-schematic/README.md index f0c64ba2b929..383de371dcc9 100644 --- a/npm/cypress-schematic/README.md +++ b/npm/cypress-schematic/README.md @@ -31,8 +31,7 @@ ## Requirements -- Angular 17.2.0+ (Cypress 13 and under supports Angular 13 - 16) - +- Angular 18.0.0+ (`@cypress/schematic@2` supports Angular 13 - 16 and `@cypress/schematic@3` supports Angular 17) ## Usage ⏯ ### Adding E2E and Component Testing diff --git a/npm/cypress-schematic/package.json b/npm/cypress-schematic/package.json index 2af4628482e2..f8c62042b509 100644 --- a/npm/cypress-schematic/package.json +++ b/npm/cypress-schematic/package.json @@ -27,8 +27,8 @@ "vitest": "2.1.4" }, "peerDependencies": { - "@angular/cli": ">=17.2", - "@angular/core": ">=17.2" + "@angular/cli": ">=18.0.0", + "@angular/core": ">=18.0.0" }, "license": "MIT", "repository": { diff --git a/npm/grep/README.md b/npm/grep/README.md index e28117773d7a..ae9d4607d0f9 100644 --- a/npm/grep/README.md +++ b/npm/grep/README.md @@ -143,14 +143,16 @@ $ npx cypress run --env grepTags=@smoke,grepFilterSpecs=true $ npx cypress run --env grepUntagged=true ``` -You can use any way to modify the environment values `grep` and `grepTags`, except the run-time `Cypress.env('grep')` (because it is too late at run-time). You can set the `grep` value in the `cypress.json` file to run only tests with the substring `viewport` in their names +You can use any way to modify the environment values `grep` and `grepTags`, except the run-time `Cypress.env('grep')` (because it is too late at run-time). You can set the `grep` value in the `cypress.config.js` file to run only tests with the substring `viewport` in their names -```json -{ - "env": { - "grep": "viewport" - } -} +```js +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + env: { + grep: "viewport" + }, +}) ``` You can also set the `env.grep` object in the plugin file, but remember to return the changed config object: diff --git a/npm/react/cypress/component/advanced/mocking-imports/spec.cy.jsx b/npm/react/cypress/component/advanced/mocking-imports/spec.cy.jsx index d2597f10f0a9..8af1dbaaa227 100644 --- a/npm/react/cypress/component/advanced/mocking-imports/spec.cy.jsx +++ b/npm/react/cypress/component/advanced/mocking-imports/spec.cy.jsx @@ -18,7 +18,7 @@ describe('Mocking ES6 import', () => { // stub property on the loaded ES6 module using cy.stub // which will be restored after the test automatically - cy.stub(GreetingModule, 'greeting', 'test greeting') + cy.stub(GreetingModule, 'greeting').returns('test greeting') mount() cy.contains('h1', 'test greeting').should('be.visible') }) diff --git a/npm/vite-dev-server/tsconfig.json b/npm/vite-dev-server/tsconfig.json index bce39021eb1d..5e937d27bdda 100644 --- a/npm/vite-dev-server/tsconfig.json +++ b/npm/vite-dev-server/tsconfig.json @@ -4,6 +4,7 @@ "resolveJsonModule": true, "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "moduleResolution": "node", "lib": [ "es2015", "dom" diff --git a/npm/webpack-batteries-included-preprocessor/README.md b/npm/webpack-batteries-included-preprocessor/README.md index 728ea4e17a7b..58a01b040d80 100644 --- a/npm/webpack-batteries-included-preprocessor/README.md +++ b/npm/webpack-batteries-included-preprocessor/README.md @@ -14,7 +14,7 @@ This preprocessor is a wrapper for [@cypress/webpack-preprocessor](https://githu Note that installing [@cypress/webpack-preprocessor](https://github.com/cypress-io/cypress-webpack-preprocessor) is also required. This allows you to update its version separately from this wrapper. -For webpack `v5`, use `@cypress/webpack-batteries-included-preprocessor@3.x.x`. For webpack `v4`, use `@cypress/webpack-batteries-included-preprocessor@2.x.x`. +For webpack `v5`, use `@cypress/webpack-batteries-included-preprocessor@3.x.x` and up. For webpack `v4`, use `@cypress/webpack-batteries-included-preprocessor@2.x.x`. ```sh npm install --save-dev @cypress/webpack-batteries-included-preprocessor @cypress/webpack-preprocessor @@ -22,7 +22,7 @@ npm install --save-dev @cypress/webpack-batteries-included-preprocessor @cypress ## Usage -In your project's [plugins file](https://on.cypress.io/guides/tooling/plugins-guide.html): +In your project's [cypress.config.js file](https://on.cypress.io/guides/tooling/plugins-guide.html): ```javascript const webpackPreprocessor = require('@cypress/webpack-batteries-included-preprocessor') @@ -44,6 +44,29 @@ module.exports = (on) => { } ``` +As of version `4.x.x`, `@cypress/webpack-batteries-included-preprocessor` only includes the `buffer`, `path`, `process`, `os`, and `stream` built-ins. If your project requires built-ins not provided, you can retrieve the preprocessor's default Webpack options and decorate them as needed. + +```javascript +const webpackPreprocessor = require('@cypress/webpack-batteries-included-preprocessor') + +function getWebpackOptions () { + const options = webpackPreprocessor.getFullWebpackOptions() + + // add built-ins as needed + options.resolve.fallback.zlib = require.resolve('browserify-zlib') + + return options +} + +module.exports = (on) => { + on('file:preprocessor', webpackPreprocessor({ + webpackOptions: getWebpackOptions() + })) +} +``` + +Please see [resolve.fallback](see https://webpack.js.org/configuration/resolve/#resolvefallback) for more information on what built-ins can be shimmed. + Other than the `typescript` option, this preprocessor supports the same options as [@cypress/webpack-preprocessor](https://github.com/cypress-io/cypress/tree/develop/npm/webpack-preprocessor#readme), so see its [README](https://github.com/cypress-io/cypress/tree/develop/npm/webpack-preprocessor#readme) for more information. ## Debugging diff --git a/npm/webpack-batteries-included-preprocessor/index.js b/npm/webpack-batteries-included-preprocessor/index.js index 46ffecef9f0f..9c594205e100 100644 --- a/npm/webpack-batteries-included-preprocessor/index.js +++ b/npm/webpack-batteries-included-preprocessor/index.js @@ -1,12 +1,20 @@ const path = require('path') -const webpack = require('webpack') const Debug = require('debug') +const getTsConfig = require('get-tsconfig') +const webpack = require('webpack') const webpackPreprocessor = require('@cypress/webpack-preprocessor') const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const debug = Debug('cypress:webpack-batteries-included-preprocessor') const WBADebugNamespace = 'cypress-verbose:webpack-batteries-included-preprocessor:bundle-analyzer' +class TsConfigNotFoundError extends Error { + constructor () { + super('No tsconfig.json found, but typescript is installed. ts-loader needs a tsconfig.json file to work. Please add one to your project in either the root or the cypress directory.') + this.name = 'TsConfigNotFoundError' + } +} + const hasTsLoader = (rules) => { return rules.some((rule) => { if (!rule.use || !Array.isArray(rule.use)) return false @@ -18,6 +26,17 @@ const hasTsLoader = (rules) => { } const addTypeScriptConfig = (file, options) => { + // returns null if tsconfig cannot be found in the path/parent hierarchy + const configFile = getTsConfig.getTsconfig(file.filePath) + + if (!configFile && typescriptExtensionRegex.test(file.filePath)) { + debug('no user tsconfig.json found. Throwing TsConfigNotFoundError') + // @see https://github.com/cypress-io/cypress/issues/18938 + throw new TsConfigNotFoundError() + } + + debug(`found user tsconfig.json at ${configFile?.path} with compilerOptions: ${JSON.stringify(configFile?.config?.compilerOptions)}`) + // shortcut if we know we've already added typescript support if (options.__typescriptSupportAdded) return options @@ -37,13 +56,6 @@ const addTypeScriptConfig = (file, options) => { const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin') // node will try to load a projects tsconfig.json instead of the node - const getTsConfig = require('get-tsconfig') - - // returns null if tsconfig cannot be found in the path/parent hierarchy - const configFile = getTsConfig.getTsconfig(file.filePath) - - configFile ? debug(`found user tsconfig.json at ${configFile?.path} with compilerOptions: ${JSON.stringify(configFile?.config?.compilerOptions)}`) : debug('no user tsconfig.json found') - webpackOptions.module.rules.push({ test: /\.tsx?$/, exclude: [/node_modules/], @@ -52,6 +64,10 @@ const addTypeScriptConfig = (file, options) => { loader: require.resolve('ts-loader'), options: { compiler: options.typescript, + // pass in the resolved compiler options from the tsconfig file into ts-loader to most accurately transpile the code + ...(configFile ? { + compilerOptions: configFile.config.compilerOptions, + } : {}), logLevel: 'error', silent: true, transpileOnly: true, @@ -146,20 +162,20 @@ const getDefaultWebpackOptions = () => { resolve: { extensions: ['.js', '.json', '.jsx', '.mjs', '.coffee'], fallback: { - assert: require.resolve('assert/'), + assert: false, buffer: require.resolve('buffer/'), child_process: false, cluster: false, console: false, - constants: require.resolve('constants-browserify'), - crypto: require.resolve('crypto-browserify'), + constants: false, + crypto: false, dgram: false, dns: false, - domain: require.resolve('domain-browser'), - events: require.resolve('events/'), + domain: false, + events: false, fs: false, - http: require.resolve('stream-http'), - https: require.resolve('https-browserify'), + http: false, + https: false, http2: false, inspector: false, module: false, @@ -167,21 +183,21 @@ const getDefaultWebpackOptions = () => { os: require.resolve('os-browserify/browser'), path: require.resolve('path-browserify'), perf_hooks: false, - punycode: require.resolve('punycode/'), + punycode: false, process: require.resolve('process/browser.js'), - querystring: require.resolve('querystring-es3'), + querystring: false, readline: false, repl: false, stream: require.resolve('stream-browserify'), - string_decoder: require.resolve('string_decoder/'), - sys: require.resolve('util/'), - timers: require.resolve('timers-browserify'), + string_decoder: false, + sys: false, + timers: false, tls: false, - tty: require.resolve('tty-browserify'), - url: require.resolve('url/'), - util: require.resolve('util/'), - vm: require.resolve('vm-browserify'), - zlib: require.resolve('browserify-zlib'), + tty: false, + url: false, + util: false, + vm: false, + zlib: false, }, plugins: [], }, diff --git a/npm/webpack-batteries-included-preprocessor/package.json b/npm/webpack-batteries-included-preprocessor/package.json index e70eaba40cba..522da8281305 100644 --- a/npm/webpack-batteries-included-preprocessor/package.json +++ b/npm/webpack-batteries-included-preprocessor/package.json @@ -15,35 +15,19 @@ "@babel/preset-env": "^7.25.3", "@babel/preset-react": "^7.24.7", "@babel/runtime": "^7.25.0", - "assert": "^2.0.0", "babel-loader": "^10.0.0", "babel-plugin-add-module-exports": "^1.0.2", - "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "coffee-loader": "^4.0.0", "coffeescript": "2.6.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.12.0", "debug": "^4.3.4", - "domain-browser": "^4.22.0", - "events": "^3.3.0", "get-tsconfig": "^4.10.0", - "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "^1.0.1", "process": "^0.11.10", - "punycode": "^2.3.0", - "querystring-es3": "^0.2.1", "stream-browserify": "^3.0.0", - "stream-http": "^3.2.0", - "string_decoder": "1.3.0", - "timers-browserify": "^2.0.12", "ts-loader": "9.5.2", "tsconfig-paths-webpack-plugin": "^3.5.2", - "tty-browserify": "^0.0.1", - "url": "^0.11.1", - "util": "^0.12.5", - "vm-browserify": "^1.1.2", "webpack": "^5.88.2", "webpack-bundle-analyzer": "4.10.2" }, @@ -60,7 +44,7 @@ "typescript": "~5.4.5" }, "peerDependencies": { - "@cypress/webpack-preprocessor": "^5.4.4" + "@cypress/webpack-preprocessor": "^6.0.4" }, "files": [ "index.js", diff --git a/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js b/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js index ff5e436ed771..e97082c2bf7e 100644 --- a/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js +++ b/npm/webpack-batteries-included-preprocessor/test/e2e/features.spec.js @@ -60,10 +60,6 @@ describe('webpack-batteries-included-preprocessor features', () => { await runAndEval('node_shim_spec.js') }) - it('shims node builtins', async () => { - await runAndEval('node_builtins_spec.js') - }) - it('outputs inline source map', async () => { const outputPath = await run('es_features_spec.js') const contents = await fs.readFile(outputPath) diff --git a/npm/webpack-batteries-included-preprocessor/test/fixtures/node_builtins_spec.js b/npm/webpack-batteries-included-preprocessor/test/fixtures/node_builtins_spec.js deleted file mode 100644 index f68fcea839b0..000000000000 --- a/npm/webpack-batteries-included-preprocessor/test/fixtures/node_builtins_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -expect(require('assert')).to.be.a('function').and.have.property('ok') -expect(require('buffer')).to.be.an('object').and.have.property('Buffer') -expect(require('child_process')).to.be.eql({}) -expect(require('cluster')).to.be.eql({}) -expect(require('console')).to.be.eql({}) -expect(require('constants')).to.be.an('object').and.have.property('SIGINT') -expect(require('crypto')).to.be.an('object').and.have.property('Hash') -expect(require('dgram')).to.be.eql({}) -expect(require('dns')).to.be.eql({}) -expect(require('domain')).to.be.an('object').and.have.property('create') -expect(require('events')).to.be.a('function').and.have.property('EventEmitter') -expect(require('fs')).to.be.eql({}) -expect(require('http')).to.be.an('object').and.have.property('request') -expect(require('https')).to.be.an('object').and.have.property('request') -expect(require('http2')).to.be.eql({}) -expect(require('inspector')).to.be.eql({}) -expect(require('module')).to.be.eql({}) -expect(require('net')).to.be.eql({}) -expect(require('os')).to.be.an('object').and.have.property('platform') -expect(require('path')).to.be.an('object').and.have.property('join') -expect(require('perf_hooks')).to.eql({}) -const punycode = require('punycode') - -expect(typeof punycode).to.equal('object') -expect(punycode).to.have.property('encode') -expect(require('querystring')).to.be.an('object').and.have.property('parse') -expect(require('readline')).to.be.eql({}) -expect(require('repl')).to.be.eql({}) -expect(require('stream')).to.be.a('function').and.have.property('Readable') -expect(require('string_decoder')).to.be.an('object').and.have.property('StringDecoder') -expect(require('sys')).to.be.an('object').and.have.property('inspect') -expect(require('timers')).to.be.an('object').and.have.property('setTimeout') -expect(require('tls')).to.be.eql({}) -expect(require('tty')).to.be.an('object').and.have.property('ReadStream') -expect(require('url')).to.be.an('object').and.have.property('resolve') -expect(require('util')).to.be.an('object').and.have.property('format') -expect(require('vm')).to.be.an('object').and.have.property('runInContext') -expect(require('zlib')).to.be.an('object').and.have.property('gzip') diff --git a/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js b/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js index 3cb62fc88308..6d7dee57118f 100644 --- a/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js +++ b/npm/webpack-batteries-included-preprocessor/test/unit/index.spec.js @@ -82,8 +82,16 @@ describe('webpack-batteries-included-preprocessor', () => { mock.stop('@cypress/webpack-preprocessor') }) - it('always returns loader options even if there is an error discovering the user\'s tsconfig.json', () => { - getTsConfigMock.returns(null) + it('correctly passes the options in the user\'s tsconfig.json options into ts-loader', () => { + getTsConfigMock.returns({ + config: { + compilerOptions: { + module: 'ESNext', + moduleResolution: 'Bundler', + }, + path: '/foo/tsconfig.json', + }, + }) const preprocessorCB = preprocessor({ typescript: true, @@ -104,8 +112,28 @@ describe('webpack-batteries-included-preprocessor', () => { expect(tsLoader.options.silent).to.be.true expect(tsLoader.options.transpileOnly).to.be.true - // compilerOptions are set by `@cypress/webpack-preprocessor` if ts-loader is present - expect(tsLoader.options.compilerOptions).to.be.undefined + // compilerOptions are overridden (sourceMap=true) by `@cypress/webpack-preprocessor` if ts-loader is present + expect(tsLoader.options.compilerOptions).to.deep.equal({ + module: 'ESNext', + moduleResolution: 'Bundler', + }) + }) + + // @see https://github.com/cypress-io/cypress/issues/18938. ts-loader needs a tsconfig.json file to work. + it('throws an error if the user\'s tsconfig.json is not found', () => { + getTsConfigMock.returns(null) + + const preprocessorCB = preprocessor({ + typescript: true, + webpackOptions, + }) + + expect(() => { + return preprocessorCB({ + filePath: 'foo.ts', + outputPath: '.js', + }) + }).to.throw('No tsconfig.json found, but typescript is installed. ts-loader needs a tsconfig.json file to work. Please add one to your project in either the root or the cypress directory.') }) }) }) diff --git a/npm/webpack-dev-server/__snapshots__/makeWebpackConfig.spec.ts.js b/npm/webpack-dev-server/__snapshots__/makeWebpackConfig.spec.ts.js index e65e4276b373..8a1eaaad34da 100644 --- a/npm/webpack-dev-server/__snapshots__/makeWebpackConfig.spec.ts.js +++ b/npm/webpack-dev-server/__snapshots__/makeWebpackConfig.spec.ts.js @@ -1,29 +1,3 @@ -exports['makeWebpackConfig ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v4 1'] = { - 'output': { - 'publicPath': '/test-public-path/', - 'filename': '[name].js', - }, - 'devServer': { - 'client': { - 'progress': false, - 'overlay': false, - }, - }, - 'optimization': { - 'emitOnErrors': true, - 'sideEffects': false, - 'splitChunks': { - 'chunks': 'all', - }, - }, - 'devtool': 'inline-source-map', - 'mode': 'development', - 'plugins': [ - 'HtmlWebpackPlugin', - 'CypressCTWebpackPlugin', - ], -} - exports['makeWebpackConfig ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v5 1'] = { 'output': { 'publicPath': '/test-public-path/', diff --git a/npm/webpack-dev-server/cypress/e2e/angular.cy.ts b/npm/webpack-dev-server/cypress/e2e/angular.cy.ts index 81cd000a18db..29be03d5a45c 100644 --- a/npm/webpack-dev-server/cypress/e2e/angular.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/angular.cy.ts @@ -3,7 +3,6 @@ import type { ProjectFixtureDir } from '@tooling/system-tests/lib/fixtureDirs' const WEBPACK_ANGULAR: ProjectFixtureDir[] = [ - 'angular-17', 'angular-18', 'angular-19', ] diff --git a/npm/webpack-dev-server/cypress/e2e/react.cy.ts b/npm/webpack-dev-server/cypress/e2e/react.cy.ts index 641d9f518834..6214d19255ef 100644 --- a/npm/webpack-dev-server/cypress/e2e/react.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/react.cy.ts @@ -4,7 +4,7 @@ import dedent from 'dedent' type ProjectDirs = typeof fixtureDirs -const WEBPACK_REACT: ProjectDirs[number][] = ['webpack4_wds4-react', 'webpack5_wds4-react', 'webpack5_wds5-react'] +const WEBPACK_REACT: ProjectDirs[number][] = ['webpack5_wds5-react'] // Add to this list to focus on a particular permutation const ONLY_PROJECTS: ProjectDirs[number][] = [] diff --git a/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts b/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts index 20aeb097a76a..87a86b52417b 100644 --- a/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts @@ -2,8 +2,8 @@ describe('Config options', () => { it('supports supportFile = false', () => { - cy.scaffoldProject('webpack5_wds4-react') - cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-no-support.config.ts', '--component']) + cy.scaffoldProject('webpack5_wds5-react') + cy.openProject('webpack5_wds5-react', ['--config-file', 'cypress-webpack-no-support.config.ts', '--component']) cy.startAppServer('component') cy.visitApp() @@ -26,8 +26,8 @@ describe('Config options', () => { }) it('supports @cypress/webpack-dev-server', () => { - cy.scaffoldProject('webpack5_wds4-react') - cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-dev-server-function.config.ts', '--component']) + cy.scaffoldProject('webpack5_wds5-react') + cy.openProject('webpack5_wds5-react', ['--config-file', 'cypress-webpack-dev-server-function.config.ts', '--component']) cy.startAppServer('component') cy.visitApp() @@ -38,8 +38,8 @@ describe('Config options', () => { }) it('supports webpackConfig as an async function', () => { - cy.scaffoldProject('webpack5_wds4-react') - cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-dev-server-async-config.config.ts', '--component']) + cy.scaffoldProject('webpack5_wds5-react') + cy.openProject('webpack5_wds5-react', ['--config-file', 'cypress-webpack-dev-server-async-config.config.ts', '--component']) cy.startAppServer('component') cy.visitApp() @@ -56,8 +56,8 @@ describe('Config options', () => { }) it('recompiles with new spec and custom indexHtmlFile', () => { - cy.scaffoldProject('webpack5_wds4-react') - cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-dev-server-custom-index.config.ts', '--component']) + cy.scaffoldProject('webpack5_wds5-react') + cy.openProject('webpack5_wds5-react', ['--config-file', 'cypress-webpack-dev-server-custom-index.config.ts', '--component']) cy.startAppServer('component') cy.visitApp() diff --git a/npm/webpack-dev-server/package.json b/npm/webpack-dev-server/package.json index ea8d2fba94f9..dba36f0b23db 100644 --- a/npm/webpack-dev-server/package.json +++ b/npm/webpack-dev-server/package.json @@ -19,7 +19,6 @@ "dependencies": { "find-up": "6.3.0", "fs-extra": "9.1.0", - "html-webpack-plugin-4": "npm:html-webpack-plugin@^4", "html-webpack-plugin-5": "npm:html-webpack-plugin@^5", "local-pkg": "0.4.1", "semver": "^7.7.1", @@ -42,12 +41,10 @@ "sinon": "^13.0.1", "snap-shot-it": "^7.9.10", "ts-node": "^10.9.2", - "webpack": "npm:webpack@^5", - "webpack-4": "npm:webpack@^4", - "webpack-dev-server-4": "npm:webpack-dev-server@^4" + "webpack": "npm:webpack@^5" }, "peerDependencies": { - "cypress": ">=14.0.0" + "cypress": ">=15.0.0" }, "files": [ "dist" diff --git a/npm/webpack-dev-server/src/createWebpackDevServer.ts b/npm/webpack-dev-server/src/createWebpackDevServer.ts index 96930de28b49..c52c6f764853 100644 --- a/npm/webpack-dev-server/src/createWebpackDevServer.ts +++ b/npm/webpack-dev-server/src/createWebpackDevServer.ts @@ -1,6 +1,5 @@ import debugLib from 'debug' import type { Configuration as WebpackDevServer5Configuration } from 'webpack-dev-server' -import type { Configuration as WebpackDevServer4Configuration } from 'webpack-dev-server-4' import type { WebpackDevServerConfig } from './devServer' import type { SourceRelativeWebpackResult } from './helpers/sourceRelativeWebpackModules' import { makeWebpackConfig } from './makeWebpackConfig' @@ -52,12 +51,6 @@ export async function createWebpackDevServer ( return webpackDevServer5(config, webpackCompiler, finalWebpackConfig) } - if (webpackDevServerMajorVersion === 4) { - debug('using webpack-dev-server v4') - - return webpackDevServer4(config, webpackCompiler, finalWebpackConfig) - } - throw new Error(`Unsupported webpackDevServer version ${webpackDevServerMajorVersion}`) } @@ -99,37 +92,3 @@ function webpackDevServer5 ( compiler, } } - -function webpackDevServer4 ( - config: CreateFinalWebpackConfig, - compiler: object, - finalWebpackConfig: Record, -) { - const { devServerConfig: { cypressConfig: { devServerPublicPathRoute } } } = config - const isOpenMode = !config.devServerConfig.cypressConfig.isTextTerminal - const WebpackDevServer = config.sourceWebpackModulesResult.webpackDevServer.module - const webpackDevServerConfig: WebpackDevServer4Configuration = { - host: '127.0.0.1', - port: 'auto', - // @ts-ignore - ...finalWebpackConfig?.devServer, - devMiddleware: { - publicPath: devServerPublicPathRoute, - stats: finalWebpackConfig.stats ?? 'minimal', - ...(isWebpackBundleAnalyzerEnabled() ? { - // the bundle needs to be written to disk in order to determine source map sizes - writeToDisk: true, - } : {}), - }, - hot: false, - // Only enable file watching & reload when executing tests in `open` mode - liveReload: isOpenMode, - } - - const server = new WebpackDevServer(webpackDevServerConfig, compiler) - - return { - server, - compiler, - } -} diff --git a/npm/webpack-dev-server/src/devServer.ts b/npm/webpack-dev-server/src/devServer.ts index b220b8ccb9a6..750ca6b40856 100644 --- a/npm/webpack-dev-server/src/devServer.ts +++ b/npm/webpack-dev-server/src/devServer.ts @@ -1,8 +1,6 @@ /// import type WebpackDevServer5 from 'webpack-dev-server' -import type WebpackDevServer4 from 'webpack-dev-server-4' - import type { Compiler, Configuration } from 'webpack' import { createWebpackDevServer } from './createWebpackDevServer' @@ -40,10 +38,6 @@ export type WebpackDevServerConfig = { * @internal */ type DevServerCreateResult = { - version: 4 - server: WebpackDevServer4 - compiler: Compiler -} | { version: 5 server: WebpackDevServer5 compiler: Compiler diff --git a/npm/webpack-dev-server/src/helpers/sourceRelativeWebpackModules.ts b/npm/webpack-dev-server/src/helpers/sourceRelativeWebpackModules.ts index 627212aaaae0..dbf45db727c6 100644 --- a/npm/webpack-dev-server/src/helpers/sourceRelativeWebpackModules.ts +++ b/npm/webpack-dev-server/src/helpers/sourceRelativeWebpackModules.ts @@ -5,13 +5,6 @@ import debugFn from 'debug' const debug = debugFn('cypress:webpack-dev-server:sourceRelativeWebpackModules') -class CypressWebpackDevServerError extends Error { - constructor (message: string) { - super(message) - this.name = 'CypressWebpackDevServerError' - } -} - export type ModuleClass = typeof Module & { _load(id: string, parent: Module, isMain: boolean): any _resolveFilename(request: string, parent: Module, isMain: boolean, options?: { paths: string[] }): string @@ -147,7 +140,7 @@ export function sourceWebpack (config: WebpackDevServerConfig, framework: Source webpack.importPath = path.dirname(webpackJsonPath) webpack.packageJson = require(webpackJsonPath) webpack.module = require(webpack.importPath) - webpack.majorVersion = getMajorVersion(webpack.packageJson, [4, 5]) + webpack.majorVersion = getMajorVersion(webpack.packageJson, [5]) debug('Webpack: Successfully sourced webpack - %o', webpack) @@ -211,25 +204,17 @@ export function sourceWebpackDevServer (config: WebpackDevServerConfig, webpackM webpackDevServer.importPath = path.dirname(webpackDevServerJsonPath) webpackDevServer.packageJson = require(webpackDevServerJsonPath) webpackDevServer.module = require(webpackDevServer.importPath) - webpackDevServer.majorVersion = getMajorVersion(webpackDevServer.packageJson, [4, 5]) + webpackDevServer.majorVersion = getMajorVersion(webpackDevServer.packageJson, [5]) debug('WebpackDevServer: Successfully sourced webpack-dev-server - %o', webpackDevServer) - if (webpackMajorVersion < 5 && webpackDevServer.majorVersion === 5) { - const json = webpackDevServer.packageJson - - throw new CypressWebpackDevServerError( - `Incompatible major versions of webpack and webpack-dev-server! - webpack-dev-server major version ${webpackDevServer.majorVersion} only works with major versions of webpack 5 - saw webpack-dev-server version ${json.version}. - If using webpack major version 4, please install webpack-dev-server version 4 to be used with @cypress/webpack-dev-server or upgrade to webpack 5.`, - ) - } return webpackDevServer } // Source the html-webpack-plugin module from the provided framework or projectRoot. // If none is found, we fallback to the version bundled with this package dependent on the major version of webpack. -// We ship both v4 and v5 of 'html-webpack-plugin' by aliasing the package with the major version (check package.json). +// We ship v5 of 'html-webpack-plugin' by aliasing the package with the major version (check package.json). This allows +// us to support newer major versions of 'html-webpack-plugin' easily'. export function sourceHtmlWebpackPlugin (config: WebpackDevServerConfig, framework: SourcedDependency | null, webpack: SourcedWebpack): SourcedHtmlWebpackPlugin { const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot @@ -244,9 +229,9 @@ export function sourceHtmlWebpackPlugin (config: WebpackDevServerConfig, framewo }) htmlWebpackPlugin.packageJson = require(htmlWebpackPluginJsonPath) - // Check that they're not using v3 of html-webpack-plugin. Since we should be the only consumer of it, + // Check that they're not using v3 or v4 of html-webpack-plugin. Since we should be the only consumer of it, // we shouldn't be concerned with using our own copy if they've shipped w/ an earlier version - htmlWebpackPlugin.majorVersion = getMajorVersion(htmlWebpackPlugin.packageJson, [4, 5]) + htmlWebpackPlugin.majorVersion = getMajorVersion(htmlWebpackPlugin.packageJson, [5]) } catch (e) { const err = e as Error & {code?: string} diff --git a/npm/webpack-dev-server/test/devServer-unit.spec.ts b/npm/webpack-dev-server/test/devServer-unit.spec.ts index 2380252585ac..5240419d9a0b 100644 --- a/npm/webpack-dev-server/test/devServer-unit.spec.ts +++ b/npm/webpack-dev-server/test/devServer-unit.spec.ts @@ -15,50 +15,6 @@ const cypressConfig = { describe('devServer', function () { this.timeout(10 * 1000) - it('creates a new devServer webpack4, webpackDevServer4', async () => { - const { devServer } = proxyquire('../src/devServer', { - './helpers/sourceRelativeWebpackModules': { - sourceDefaultWebpackDependencies: () => { - return createModuleMatrixResult({ - webpack: 4, - webpackDevServer: 4, - }) - } }, - }) as typeof import('../src/devServer') - - const result = await devServer.create({ - specs: [], - cypressConfig, - webpackConfig: {}, - devServerEvents: new EventEmitter(), - }) - - expect(result.server).to.be.instanceOf(require('webpack-dev-server-4')) - expect(result.version).to.eq(4) - }) - - it('creates a new devServer webpack5, webpackDevServer4', async () => { - const { devServer } = proxyquire('../src/devServer', { - './helpers/sourceRelativeWebpackModules': { - sourceDefaultWebpackDependencies: () => { - return createModuleMatrixResult({ - webpack: 5, - webpackDevServer: 4, - }) - } }, - }) as typeof import('../src/devServer') - - const result = await devServer.create({ - specs: [], - cypressConfig, - webpackConfig: {}, - devServerEvents: new EventEmitter(), - }) - - expect(result.server).to.be.instanceOf(require('webpack-dev-server-4')) - expect(result.version).to.eq(4) - }) - it('creates a new devServer webpack5, webpackDevServer5', async () => { const { devServer } = proxyquire('../src/devServer', { './helpers/sourceRelativeWebpackModules': { @@ -84,7 +40,7 @@ describe('devServer', function () { // Writing to disk includes the correct source map size, where the difference will be made up from stat size vs parsed size // This is critical if a user is trying to debug to determine if they have large source maps or other large files in their dev-server under test describe('writes to disk if DEBUG=cypress-verbose:webpack-dev-server:bundle-analyzer is set', async () => { - const WEBPACK_DEV_SERVER_VERSIONS: (4 | 5)[] = [4, 5] + const WEBPACK_DEV_SERVER_VERSIONS: (5)[] = [5] beforeEach(() => { debug.enable('cypress-verbose:webpack-dev-server:bundle-analyzer') diff --git a/npm/webpack-dev-server/test/handlers/angularHandler.spec.ts b/npm/webpack-dev-server/test/handlers/angularHandler.spec.ts index 7d64f5661656..11365c1c7d29 100644 --- a/npm/webpack-dev-server/test/handlers/angularHandler.spec.ts +++ b/npm/webpack-dev-server/test/handlers/angularHandler.spec.ts @@ -22,8 +22,8 @@ chai.use(chaiPromise) describe('angularHandler', function () { this.timeout(1000 * 60) - it('sources the config from angular-17', async () => { - const projectRoot = await scaffoldMigrationProject('angular-17') + it('sources the config from angular-18', async () => { + const projectRoot = await scaffoldMigrationProject('angular-18') process.chdir(projectRoot) const devServerConfig = { diff --git a/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts b/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts index d760aa00d4e6..1724c8a34815 100644 --- a/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts +++ b/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts @@ -14,55 +14,9 @@ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' Chai.use(SinonChai) -const WEBPACK_DEV_SERVER_VERSIONS: (4 | 5)[] = [4, 5] +const WEBPACK_DEV_SERVER_VERSIONS: (5)[] = [5] describe('makeWebpackConfig', () => { - it('ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v4', async () => { - const devServerConfig: WebpackDevServerConfig = { - specs: [], - cypressConfig: { - isTextTerminal: false, - projectRoot: '.', - supportFile: '/support.js', - devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts - } as Cypress.PluginConfigOptions, - webpackConfig: { - output: { - publicPath: '/this-will-be-ignored', - }, - devServer: { - client: { - progress: false, - overlay: true, // This will be overridden by makeWebpackConfig.ts - }, - }, - optimization: { - emitOnErrors: false, // This will be overridden by makeWebpackConfig.ts - }, - devtool: 'eval', // This will be overridden by makeWebpackConfig.ts - }, - devServerEvents: new EventEmitter(), - } - const actual = await makeWebpackConfig({ - devServerConfig, - sourceWebpackModulesResult: createModuleMatrixResult({ - webpack: 5, - webpackDevServer: 4, - }), - }) - - // plugins contain circular deps which cannot be serialized in a snapshot. - // instead just compare the name and order of the plugins. - ;(actual as any).plugins = actual.plugins.map((p) => p.constructor.name) - - // these will include paths from the user's local file system, so we should not include them the snapshot - delete actual.output.path - delete actual.entry - - expect(actual.output.publicPath).to.eq('/test-public-path/') - snapshot(actual) - }) - it('ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v5', async () => { const devServerConfig: WebpackDevServerConfig = { specs: [], @@ -300,68 +254,6 @@ describe('makeWebpackConfig', () => { } }) - describe('webpack-dev-server v3', () => { - beforeEach(() => { - sourceWebpackModulesResult = createModuleMatrixResult({ - webpack: 4, - webpackDevServer: 4, - }) - }) - - it('is disabled in run mode', async () => { - devServerConfig.cypressConfig.isTextTerminal = true - - const actual = await makeWebpackConfig({ - devServerConfig, - sourceWebpackModulesResult, - }) - - expect(actual.watchOptions.ignored).to.eql('**/*') - }) - - it('uses defaults in open mode', async () => { - devServerConfig.cypressConfig.isTextTerminal = false - - const actual = await makeWebpackConfig({ - devServerConfig, - sourceWebpackModulesResult, - }) - - expect(actual.watchOptions?.ignored).to.be.undefined - }) - }) - - describe('webpack-dev-server v4', () => { - beforeEach(() => { - sourceWebpackModulesResult = createModuleMatrixResult({ - webpack: 5, - webpackDevServer: 4, - }) - }) - - it('is disabled in run mode', async () => { - devServerConfig.cypressConfig.isTextTerminal = true - - const actual = await makeWebpackConfig({ - devServerConfig, - sourceWebpackModulesResult, - }) - - expect(actual.watchOptions.ignored).to.eql('**/*') - }) - - it('uses defaults in open mode', async () => { - devServerConfig.cypressConfig.isTextTerminal = false - - const actual = await makeWebpackConfig({ - devServerConfig, - sourceWebpackModulesResult, - }) - - expect(actual.watchOptions?.ignored).to.be.undefined - }) - }) - describe('webpack-dev-server v5', () => { beforeEach(() => { sourceWebpackModulesResult = createModuleMatrixResult({ @@ -398,17 +290,9 @@ describe('makeWebpackConfig', () => { let devServerConfig: WebpackDevServerConfig const WEBPACK_MATRIX: { - webpack: 4 | 5 - wds: 4 | 5 + webpack: 5 + wds: 5 }[] = [ - { - webpack: 4, - wds: 4, - }, - { - webpack: 5, - wds: 4, - }, { webpack: 5, wds: 5, @@ -457,7 +341,7 @@ describe('makeWebpackConfig', () => { // Gives users a diagnostic output with webpack-bundle-analyzer to get a visible representation of their webpack bundle, which they can send to us // to give us an idea what issues they may be experiencing describe('enables webpack-bundle-analyzer if DEBUG=cypress-verbose:webpack-dev-server:bundle-analyzer is set', async () => { - const WEBPACK_VERSIONS: (4 | 5)[] = [4, 5] + const WEBPACK_VERSIONS: (5)[] = [5] beforeEach(() => { debug.enable('cypress-verbose:webpack-dev-server:bundle-analyzer') diff --git a/npm/webpack-dev-server/test/sourceRelativeWebpackModules.spec.ts b/npm/webpack-dev-server/test/sourceRelativeWebpackModules.spec.ts index 94c5663aafee..fc17c727e6eb 100644 --- a/npm/webpack-dev-server/test/sourceRelativeWebpackModules.spec.ts +++ b/npm/webpack-dev-server/test/sourceRelativeWebpackModules.spec.ts @@ -18,16 +18,6 @@ const WEBPACK_REACT: Partial> = { - 'webpack4_wds4-react': { - webpack: 4, - webpackDevServer: 4, - htmlWebpackPlugin: 4, - }, - 'webpack5_wds4-react': { - webpack: 5, - webpackDevServer: 4, - htmlWebpackPlugin: 5, - }, 'webpack5_wds5-react': { webpack: 5, webpackDevServer: 5, diff --git a/npm/webpack-dev-server/test/test-helpers/createModuleMatrixResult.ts b/npm/webpack-dev-server/test/test-helpers/createModuleMatrixResult.ts index 01518f415aa6..1083068a42d4 100644 --- a/npm/webpack-dev-server/test/test-helpers/createModuleMatrixResult.ts +++ b/npm/webpack-dev-server/test/test-helpers/createModuleMatrixResult.ts @@ -5,23 +5,20 @@ type ModulesDef = {[K in keyof ModuleMatrixResultOptions]: Record { describe(`Angular ${angularVersion}`, { diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index 7c06c2cbf394..9e4694fd25bd 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -651,6 +651,8 @@ describe('App: Specs', () => { .and('have.attr', 'href', 'https://on.cypress.io/styling-components') cy.log('should not contain the link if you navigate away and back') + // A bit of a hack, but our cy-in-cy test needs to wait for the reporter to fully render before pressing the "f" key to expand the "Search specs" menu. + // Otherwise, the "f" keypress happens before the event is registered, which causes the "Search Specs" menu to not expand. cy.get('[data-cy="runnable-header"]').should('be.visible') cy.get('body').type('f') cy.get('[data-cy=spec-file-item]').first().click() diff --git a/packages/config/__snapshots__/index.spec.ts.js b/packages/config/__snapshots__/index.spec.ts.js index bece4725456e..54fe87d2933e 100644 --- a/packages/config/__snapshots__/index.spec.ts.js +++ b/packages/config/__snapshots__/index.spec.ts.js @@ -1,20 +1,7 @@ exports['config/src/index .getBreakingKeys returns list of breaking config keys 1'] = [ - 'blacklistHosts', - 'componentFolder', - 'experimentalComponentTesting', - 'experimentalGetCookiesSameSite', 'experimentalJustInTimeCompile', - 'experimentalNetworkStubbing', - 'experimentalRunEvents', - 'experimentalSessionSupport', 'experimentalSessionAndOrigin', - 'experimentalShadowDomSupport', 'experimentalSkipDomainInjection', - 'firefoxGcInterval', - 'ignoreTestFiles', - 'integrationFolder', - 'pluginsFile', - 'testFiles', 'videoUploadOnPasses', ] diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index e86be8c01354..8f266c397c94 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -9,26 +9,15 @@ import type { TestingType } from '@packages/types' import * as validate from './validation' const BREAKING_OPTION_ERROR_KEY: Readonly = [ - 'COMPONENT_FOLDER_REMOVED', - 'INTEGRATION_FOLDER_REMOVED', 'CONFIG_FILE_INVALID_ROOT_CONFIG', 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E', 'CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT', 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT', 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E', - 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED', - 'EXPERIMENTAL_SAMESITE_REMOVED', - 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED', - 'EXPERIMENTAL_RUN_EVENTS_REMOVED', - 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED', 'EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED', 'EXPERIMENTAL_SINGLE_TAB_RUN_MODE', - 'EXPERIMENTAL_SHADOW_DOM_REMOVED', - 'FIREFOX_GC_INTERVAL_REMOVED', - 'PLUGINS_FILE_CONFIG_OPTION_REMOVED', 'VIDEO_UPLOAD_ON_PASSES_REMOVED', 'RENAMED_CONFIG_OPTION', - 'TEST_FILES_RENAMED', ] as const type ValidationOptions = { @@ -617,74 +606,17 @@ export const options: Array = [ */ export const breakingOptions: Readonly = [ { - name: 'blacklistHosts', - errorKey: 'RENAMED_CONFIG_OPTION', - newName: 'blockHosts', - isWarning: false, - }, { - name: 'componentFolder', - errorKey: 'COMPONENT_FOLDER_REMOVED', - isWarning: false, - }, { - name: 'experimentalComponentTesting', - errorKey: 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED', - isWarning: false, - }, { - name: 'experimentalGetCookiesSameSite', - errorKey: 'EXPERIMENTAL_SAMESITE_REMOVED', - isWarning: true, - }, { name: 'experimentalJustInTimeCompile', errorKey: 'EXPERIMENTAL_JIT_COMPILE_REMOVED', isWarning: true, - }, - { - name: 'experimentalNetworkStubbing', - errorKey: 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED', - isWarning: true, - }, { - name: 'experimentalRunEvents', - errorKey: 'EXPERIMENTAL_RUN_EVENTS_REMOVED', - isWarning: true, - }, { - name: 'experimentalSessionSupport', - errorKey: 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED', - isWarning: true, }, { name: 'experimentalSessionAndOrigin', errorKey: 'EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED', isWarning: true, - }, { - name: 'experimentalShadowDomSupport', - errorKey: 'EXPERIMENTAL_SHADOW_DOM_REMOVED', - isWarning: true, }, { name: 'experimentalSkipDomainInjection', errorKey: 'EXPERIMENTAL_SKIP_DOMAIN_INJECTION_REMOVED', isWarning: false, - }, { - name: 'firefoxGcInterval', - errorKey: 'FIREFOX_GC_INTERVAL_REMOVED', - isWarning: true, - }, { - name: 'ignoreTestFiles', - errorKey: 'TEST_FILES_RENAMED', - newName: 'excludeSpecPattern', - isWarning: false, - }, { - name: 'integrationFolder', - errorKey: 'INTEGRATION_FOLDER_REMOVED', - isWarning: false, - }, { - name: 'pluginsFile', - errorKey: 'PLUGINS_FILE_CONFIG_OPTION_REMOVED', - isWarning: false, - }, - { - name: 'testFiles', - errorKey: 'TEST_FILES_RENAMED', - newName: 'specPattern', - isWarning: false, }, { name: 'videoUploadOnPasses', errorKey: 'VIDEO_UPLOAD_ON_PASSES_REMOVED', diff --git a/packages/config/test/index.spec.ts b/packages/config/test/index.spec.ts index 60ee518fd2fd..a62852715a10 100644 --- a/packages/config/test/index.spec.ts +++ b/packages/config/test/index.spec.ts @@ -14,14 +14,14 @@ describe('config/src/index', () => { it('returns filter config only containing allowed keys', () => { const keys = configUtil.allowed({ 'baseUrl': 'https://url.com', - 'blacklistHosts': 'breaking option', + 'videoUploadOnPasses': true, 'devServerPublicPathRoute': 'internal key', 'random': 'not a config option', }) expect(keys).to.deep.eq({ 'baseUrl': 'https://url.com', - 'blacklistHosts': 'breaking option', + 'videoUploadOnPasses': true, }) }) }) @@ -30,7 +30,7 @@ describe('config/src/index', () => { it('returns list of breaking config keys', () => { const breakingKeys = configUtil.getBreakingKeys() - expect(breakingKeys).to.include('blacklistHosts') + expect(breakingKeys).to.include('videoUploadOnPasses') snapshot(breakingKeys) }) }) @@ -154,12 +154,12 @@ describe('config/src/index', () => { const errorFn = sinon.spy() configUtil.validateNoBreakingConfig({ - 'experimentalNetworkStubbing': 'should break', + 'experimentalSessionAndOrigin': 'should break', configFile: 'config.js', }, warningFn, errorFn, 'e2e') - expect(warningFn).to.have.been.calledOnceWith('EXPERIMENTAL_NETWORK_STUBBING_REMOVED', { - name: 'experimentalNetworkStubbing', + expect(warningFn).to.have.been.calledOnceWith('EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED', { + name: 'experimentalSessionAndOrigin', newName: undefined, value: undefined, testingType: 'e2e', @@ -174,14 +174,14 @@ describe('config/src/index', () => { const errorFn = sinon.spy() configUtil.validateNoBreakingConfig({ - 'blacklistHosts': 'should break', + experimentalSkipDomainInjection: true, configFile: 'config.js', }, warningFn, errorFn, 'e2e') expect(warningFn).to.have.been.callCount(0) - expect(errorFn).to.have.been.calledOnceWith('RENAMED_CONFIG_OPTION', { - name: 'blacklistHosts', - newName: 'blockHosts', + expect(errorFn).to.have.been.calledOnceWith('EXPERIMENTAL_SKIP_DOMAIN_INJECTION_REMOVED', { + name: 'experimentalSkipDomainInjection', + newName: undefined, value: undefined, testingType: 'e2e', configFile: 'config.js', diff --git a/packages/config/test/project/utils.spec.ts b/packages/config/test/project/utils.spec.ts index ab47b455d667..b707d6a1805b 100644 --- a/packages/config/test/project/utils.spec.ts +++ b/packages/config/test/project/utils.spec.ts @@ -958,17 +958,6 @@ describe('config/src/project/utils', () => { }) }) - // @see https://github.com/cypress-io/cypress/issues/6892 - it('warns if experimentalGetCookiesSameSite is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalGetCookiesSameSite', true, { - experimentalGetCookiesSameSite: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_SAMESITE_REMOVED') - }) - it('warns if experimentalJustInTimeCompile is passed', async function () { const warning = sinon.spy(errors, 'warning') @@ -979,16 +968,6 @@ describe('config/src/project/utils', () => { expect(warning).to.be.calledWith('EXPERIMENTAL_JIT_COMPILE_REMOVED') }) - it('warns if experimentalSessionSupport is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalSessionSupport', true, { - experimentalSessionSupport: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_SESSION_SUPPORT_REMOVED') - }) - it('warns if experimentalSessionAndOrigin is passed', async function () { const warning = sinon.spy(errors, 'warning') @@ -999,47 +978,6 @@ describe('config/src/project/utils', () => { expect(warning).to.be.calledWith('EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED') }) - it('warns if experimentalShadowDomSupport is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalShadowDomSupport', true, { - experimentalShadowDomSupport: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_SHADOW_DOM_REMOVED') - }) - - it('warns if experimentalRunEvents is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalRunEvents', true, { - experimentalRunEvents: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_RUN_EVENTS_REMOVED') - }) - - // @see https://github.com/cypress-io/cypress/pull/9185 - it('warns if experimentalNetworkStubbing is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('experimentalNetworkStubbing', true, { - experimentalNetworkStubbing: true, - }) - - expect(warning).to.be.calledWith('EXPERIMENTAL_NETWORK_STUBBING_REMOVED') - }) - - it('warns if firefoxGcInterval is passed', async function () { - const warning = sinon.spy(errors, 'warning') - - await this.defaults('firefoxGcInterval', true, { - firefoxGcInterval: true, - }) - - expect(warning).to.be.calledWith('FIREFOX_GC_INTERVAL_REMOVED') - }) - describe('.resolved', () => { it('sets reporter and port to cli', () => { const obj = { diff --git a/packages/data-context/__snapshots__/codegen.spec.ts.js b/packages/data-context/__snapshots__/codegen.spec.ts.js deleted file mode 100644 index f6bc863c2b9d..000000000000 --- a/packages/data-context/__snapshots__/codegen.spec.ts.js +++ /dev/null @@ -1,266 +0,0 @@ -exports['cypress.config.js generation should create a string when passed only a global option 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - viewportWidth: 300, - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation should create a string when passed only a e2e options 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - baseUrl: 'localhost:3000', - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation should create a string when passed only a component options 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - }, - component: { - setupNodeEvents(on, config) {}, - retries: 2, - }, -}) - -` - -exports['cypress.config.js generation should create a string for a config with global, component, and e2e options 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - viewportWidth: 300, - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - retries: 2, - baseUrl: 'localhost:300', - slowTestThreshold: 500, - }, - component: { - setupNodeEvents(on, config) {}, - retries: 1, - slowTestThreshold: 500, - }, -}) - -` - -exports['cypress.config.js generation should create a string when passed an empty object 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation should handle export default in plugins file 1'] = ` -import { defineConfig } from 'cypress' - -export default defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.ts').default(on, config) - }, - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation should exclude fields that are no longer valid 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - }, - component: { - setupNodeEvents(on, config) {}, - specPattern: 'path/to/component/folder/**/*.cy.{js,jsx,ts,tsx}', - }, -}) - -` - -exports['cypress.config.js generation should create only a component entry when no e2e specs are detected 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation should create only an e2e entry when no component specs are detected 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - }, -}) - -` - -exports['cypress.config.js generation should maintain both root level and nested non-breaking options during migration 1'] = ` -import { defineConfig } from 'cypress' - -export default defineConfig({ - viewportWidth: 1200, - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - viewportWidth: 1600, - }, - component: { - setupNodeEvents(on, config) {}, - viewportWidth: 400, - }, -}) - -` - -exports['cypress.config.js generation should add custom specPattern if project has projectId 1'] = ` -import { defineConfig } from 'cypress' - -export default defineConfig({ - projectId: 'abc1234', - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - baseUrl: 'http://localhost:3000', - specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation should not add custom specPattern if project has projectId and integrationFolder 1'] = ` -import { defineConfig } from 'cypress' - -export default defineConfig({ - projectId: 'abc1234', - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - baseUrl: 'http://localhost:3000', - specPattern: 'cypress/custom/e2e/**/*.{js,jsx,ts,tsx}', - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation generates correct config for component testing migration with custom testFiles glob 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - component: { - setupNodeEvents(on, config) {}, - specPattern: './**/*.spec.cy.{js,ts,jsx,tsx}', - }, -}) - -` - -exports['cypress.config.js generation should create a string when passed an empty object for an ECMA Script project 1'] = ` -import { defineConfig } from 'cypress' - -export default defineConfig({ - e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config) - }, - }, - component: { - setupNodeEvents(on, config) {}, - }, -}) - -` - -exports['cypress.config.js generation generates correct config for component testing migration with custom testFiles array of glob 1'] = ` -const { defineConfig } = require('cypress') - -module.exports = defineConfig({ - e2e: { - setupNodeEvents(on, config) {}, - specPattern: ['cypress/e2e/**/*.spec.js', 'cypress/e2e/**/*.test.js'], - }, -}) - -` diff --git a/packages/data-context/package.json b/packages/data-context/package.json index ddadd7f8805e..32263a0e7289 100644 --- a/packages/data-context/package.json +++ b/packages/data-context/package.json @@ -39,6 +39,7 @@ "execa": "1.0.0", "front-matter": "^4.0.2", "fs-extra": "8.1.0", + "get-tsconfig": "4.10.0", "getenv": "1.0.0", "globby": "^11.0.1", "graphql": "^15.5.1", @@ -60,6 +61,7 @@ "server-destroy": "1.0.1", "simple-git": "^3.27.0", "stringify-object": "^3.0.0", + "tsx": "4.19.3", "underscore.string": "^3.3.6", "wonka": "^4.0.15" }, diff --git a/packages/data-context/src/DataActions.ts b/packages/data-context/src/DataActions.ts index 145b71c970f7..9579e6d000f6 100644 --- a/packages/data-context/src/DataActions.ts +++ b/packages/data-context/src/DataActions.ts @@ -6,7 +6,6 @@ import { FileActions, ProjectActions, WizardActions, - MigrationActions, BrowserActions, DevActions, AuthActions, @@ -30,7 +29,6 @@ export class DataActions { private _wizard: WizardActions private _project: ProjectActions private _electron: ElectronActions - private _migration: MigrationActions private _browser: BrowserActions private _servers: ServersActions private _versions: VersionsActions @@ -50,7 +48,6 @@ export class DataActions { this._wizard = new WizardActions(this.ctx) this._project = new ProjectActions(this.ctx) this._electron = new ElectronActions(this.ctx) - this._migration = new MigrationActions(this.ctx) this._browser = new BrowserActions(this.ctx) this._servers = new ServersActions(this.ctx) this._versions = new VersionsActions(this.ctx) @@ -97,10 +94,6 @@ export class DataActions { return this._electron } - get migration () { - return this._migration - } - get browser () { return this._browser } diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts index f5f87c286e14..f05b32919928 100644 --- a/packages/data-context/src/DataContext.ts +++ b/packages/data-context/src/DataContext.ts @@ -25,7 +25,6 @@ import { HtmlDataSource, UtilDataSource, BrowserApiShape, - MigrationDataSource, RelevantRunsDataSource, RelevantRunSpecsDataSource, VersionsDataSource, @@ -101,7 +100,6 @@ export class DataContext { private _html: HtmlDataSource private _error: ErrorDataSource private _util: UtilDataSource - private _migration: MigrationDataSource readonly lifecycleManager: ProjectLifecycleManager @@ -136,7 +134,6 @@ export class DataContext { this._html = new HtmlDataSource(this) this._error = new ErrorDataSource(this) this._util = new UtilDataSource(this) - this._migration = new MigrationDataSource(this) // the lifecycle manager needs to be initialized last as it needs properties instantiated on the DataContext object this.lifecycleManager = new ProjectLifecycleManager(this) } @@ -236,10 +233,6 @@ export class DataContext { return this._util } - get migration () { - return this._migration - } - /** * This will be replaced with Immer, for immutable state updates. */ diff --git a/packages/data-context/src/actions/MigrationActions.ts b/packages/data-context/src/actions/MigrationActions.ts deleted file mode 100644 index ab40842bd032..000000000000 --- a/packages/data-context/src/actions/MigrationActions.ts +++ /dev/null @@ -1,442 +0,0 @@ -/* eslint-disable no-dupe-class-members */ -import path from 'path' -import debugLib from 'debug' -import { fork } from 'child_process' -import fs from 'fs-extra' -import semver from 'semver' -import type { ForkOptions } from 'child_process' -import assert from 'assert' -import _ from 'lodash' -import type { DataContext } from '..' -import { getError } from '@packages/errors' -import { - cleanUpIntegrationFolder, - formatConfig, - LegacyCypressConfigJson, - moveSpecFiles, - NonStandardMigrationError, - SpecToMove, -} from '../sources' -import { - tryGetDefaultLegacyPluginsFile, - supportFilesForMigration, - hasSpecFile, - getStepsForMigration, - getIntegrationFolder, - isDefaultTestFiles, - getComponentTestFilesGlobs, - getComponentFolder, - getIntegrationTestFilesGlobs, - getSpecPattern, - legacyOptions, - legacyIntegrationFolder, - getLegacyPluginsCustomFilePath, -} from '../sources/migration' -import { makeCoreData } from '../data' -import { LegacyPluginsIpc } from '../data/LegacyPluginsIpc' -import { hasTypeScriptInstalled, toPosix } from '../util' - -const debug = debugLib('cypress:data-context:MigrationActions') - -const tsNode = toPosix(require.resolve('@packages/server/lib/plugins/child/register_ts_node')) - -export function getConfigWithDefaults (legacyConfig: any) { - const newConfig = _.cloneDeep(legacyConfig) - - legacyOptions.forEach(({ defaultValue, name }) => { - if (defaultValue !== undefined && legacyConfig[name] === undefined) { - newConfig[name] = typeof defaultValue === 'function' ? defaultValue() : defaultValue - } - }) - - return newConfig -} - -export function getDiff (oldConfig: any, newConfig: any) { - // get all the values updated - const result: any = _.reduce(oldConfig, (acc: any, value, key) => { - // ignore values that have been removed - if (newConfig[key] && !_.isEqual(value, newConfig[key])) { - acc[key] = newConfig[key] - } - - return acc - }, {}) - - // get all the values added - return _.reduce(newConfig, (acc: any, value, key) => { - // their key is in the new config but not in the old config - if (!oldConfig.hasOwnProperty(key)) { - acc[key] = value - } - - return acc - }, result) -} - -export async function processConfigViaLegacyPlugins (projectRoot: string, legacyConfig: LegacyCypressConfigJson): Promise { - const pluginFile = legacyConfig.pluginsFile - ? await getLegacyPluginsCustomFilePath(projectRoot, legacyConfig.pluginsFile) - : await tryGetDefaultLegacyPluginsFile(projectRoot) - - debug('found legacy pluginsFile at %s', pluginFile) - - return new Promise((resolve, reject) => { - // couldn't find a pluginsFile - // just bail with initial config - if (!pluginFile) { - return resolve(legacyConfig) - } - - const cwd = path.join(projectRoot, pluginFile) - - const childOptions: ForkOptions = { - stdio: 'inherit', - cwd: path.dirname(cwd), - env: _.omit(process.env, 'CYPRESS_INTERNAL_E2E_TESTING_SELF'), - } - - const configProcessArgs = ['--projectRoot', projectRoot, '--file', cwd] - const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child') - - // use ts-node if they've got typescript installed - // this matches the 9.x behavior, which is what we want for - // processing legacy pluginsFile (we never supported `"type": "module") in 9.x. - if (hasTypeScriptInstalled(projectRoot)) { - const tsNodeLoader = `--require "${tsNode}"` - - if (!childOptions.env) { - childOptions.env = {} - } - - if (childOptions.env.NODE_OPTIONS) { - childOptions.env.NODE_OPTIONS += ` ${tsNodeLoader}` - } else { - childOptions.env.NODE_OPTIONS = tsNodeLoader - } - } - - const childProcess = fork(CHILD_PROCESS_FILE_PATH, configProcessArgs, childOptions) - const ipc = new LegacyPluginsIpc(childProcess) - - childProcess.on('error', (error) => { - error = getError('LEGACY_CONFIG_ERROR_DURING_MIGRATION', cwd, error) - - reject(error) - ipc.killChildProcess() - }) - - const legacyConfigWithDefaults = getConfigWithDefaults(legacyConfig) - - ipc.on('ready', () => { - debug('legacyConfigIpc:ready') - ipc.send('loadLegacyPlugins', legacyConfigWithDefaults) - }) - - ipc.on('loadLegacyPlugins:reply', (modifiedLegacyConfig) => { - debug('loadLegacyPlugins:reply') - const diff = getDiff(legacyConfigWithDefaults, modifiedLegacyConfig) - - // if env is updated by plugins, avoid adding it to the config file - if (diff.env) { - delete diff.env - } - - const legacyConfigWithChanges = _.merge(legacyConfig, diff) - - resolve(legacyConfigWithChanges) - ipc.killChildProcess() - }) - - ipc.on('loadLegacyPlugins:error', (error) => { - debug('loadLegacyPlugins:error') - error = getError('LEGACY_CONFIG_ERROR_DURING_MIGRATION', cwd, error) - - reject(error) - ipc.killChildProcess() - }) - - ipc.on('childProcess:unhandledError', (error) => { - debug('childProcess:unhandledError') - reject(error) - ipc.killChildProcess() - }) - }) -} - -export class MigrationActions { - constructor (private ctx: DataContext) { } - - async initialize (config: LegacyCypressConfigJson) { - const legacyConfigForMigration = await this.setLegacyConfigForMigration(config) - - this.reset(legacyConfigForMigration) - - if (!this.ctx.currentProject || !legacyConfigForMigration) { - throw Error('cannot do migration without currentProject!') - } - - if (this.ctx.coreData.app.isGlobalMode) { - const version = await this.locallyInstalledCypressVersion(this.ctx.currentProject) - - if (!version) { - // Could not resolve Cypress. Unlikely, but they are using a - // project with Cypress that is nested more deeply than - // another project, which has a `cypress.json` but has not had - // it's node_modules installed, or it relies on a global version - // of Cypress that is missing for whatever reason. - return this.ctx.onError(getError('MIGRATION_CYPRESS_NOT_FOUND')) - } - - const currentVersion = (await this.ctx.versions.versionData()).current.version - - // Validate that the project being migrated has a version of Cypress compatible with the version being executed. - // This handles situations where Cypress is launched in global mode to migrate a project with an older version of - // Cypress as a dependency which could break the project when launched directly. - // For example: - // Local: 9.6.0 Global: 10.0.0 FAIL - // Local: 10.0.1 Global: 10.0.0 PASS - // Local: 12.0.0 Global: 12.0.1 FAIL - if (!semver.satisfies(version, `^${currentVersion}`)) { - return this.ctx.onError(getError('MIGRATION_MISMATCHED_CYPRESS_VERSIONS', version, currentVersion)) - } - } - - await this.initializeFlags() - - const legacyConfigFileExist = this.ctx.migration.legacyConfigFileExists() - const filteredSteps = await getStepsForMigration(this.ctx.currentProject, legacyConfigForMigration, Boolean(legacyConfigFileExist)) - - this.ctx.update((coreData) => { - if (!filteredSteps[0]) { - throw Error(`Impossible to initialize a migration. No steps fit the configuration of this project.`) - } - - coreData.migration.filteredSteps = filteredSteps - coreData.migration.step = filteredSteps[0] - }) - } - - async locallyInstalledCypressVersion (currentProject: string) { - try { - const localCypressPkgJsonPath = require.resolve(path.join('cypress', 'package.json'), { - paths: [currentProject], - }) - const localCypressPkgJson = await fs.readJson(path.join(localCypressPkgJsonPath)) as { version: string } - - return localCypressPkgJson?.version ?? undefined - } catch (e) { - // node_modules was not found, or some other unexpected error - // return undefined and surface the correct error. - return undefined - } - } - - /** - * Figure out all the data required for the migration UI. - * This drives which migration steps need be shown and performed. - */ - private async initializeFlags () { - const legacyConfigForMigration = this.ctx.coreData.migration.legacyConfigForMigration - - if (!this.ctx.currentProject || !legacyConfigForMigration) { - throw Error('Need currentProject to do migration') - } - - const integrationFolder = getIntegrationFolder(legacyConfigForMigration) - const integrationTestFiles = getIntegrationTestFilesGlobs(legacyConfigForMigration) - - const hasCustomIntegrationFolder = getIntegrationFolder(legacyConfigForMigration) !== legacyIntegrationFolder - const hasCustomIntegrationTestFiles = !isDefaultTestFiles(legacyConfigForMigration, 'integration') - - const shouldAddCustomE2ESpecPattern = Boolean(this.ctx.migration.legacyConfigProjectId) - - let hasE2ESpec = integrationFolder - ? await hasSpecFile(this.ctx.currentProject, integrationFolder, integrationTestFiles) - : false - - // if we don't find specs in the 9.X scope, - // let's check already migrated files. - // this allows users to stop migration halfway, - // then to pick up where they left migration off - if (!hasE2ESpec && (!hasCustomIntegrationTestFiles || !hasCustomIntegrationFolder)) { - const newE2eSpecPattern = getSpecPattern(legacyConfigForMigration, 'e2e', shouldAddCustomE2ESpecPattern) - - hasE2ESpec = await hasSpecFile(this.ctx.currentProject, '', newE2eSpecPattern) - } - - const componentFolder = getComponentFolder(legacyConfigForMigration) - const componentTestFiles = getComponentTestFilesGlobs(legacyConfigForMigration) - - const hasCustomComponentFolder = componentFolder !== 'cypress/component' - const hasCustomComponentTestFiles = !isDefaultTestFiles(legacyConfigForMigration, 'component') - - // A user is considered to "have" component testing if either - // 1. they have a default component folder (cypress/component) with at least 1 spec file - // OR - // 2. they have configured a non-default componentFolder (even if it doesn't have any specs.) - const hasSpecInDefaultComponentFolder = await hasSpecFile(this.ctx.currentProject, componentFolder, componentTestFiles) - const hasComponentTesting = (hasCustomComponentFolder || hasSpecInDefaultComponentFolder) ?? false - - this.ctx.update((coreData) => { - coreData.migration.flags = { - hasCustomIntegrationFolder, - hasCustomIntegrationTestFiles, - hasCustomComponentFolder, - hasCustomComponentTestFiles, - hasCustomSupportFile: false, - hasComponentTesting, - hasE2ESpec, - hasPluginsFile: true, - shouldAddCustomE2ESpecPattern, - } - }) - } - - get configFileNameAfterMigration () { - return this.ctx.migration.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`) - } - - async createConfigFile () { - const config = await this.ctx.migration.createConfigString() - - this.ctx.lifecycleManager.setConfigFilePath(this.configFileNameAfterMigration) - - await this.ctx.fs.writeFile(this.ctx.lifecycleManager.configFilePath, config).catch((error) => { - throw error - }) - - await this.ctx.actions.file.removeFileInProject(this.ctx.migration.legacyConfigFile).catch((error) => { - throw error - }) - - if (this.ctx.modeOptions.configFile) { - // @ts-ignore configFile needs to be updated with the new one, so it finds the correct one - // with the new file, instead of the deleted one which is not supported anymore - this.ctx.modeOptions.configFile = this.ctx.migration.configFileNameAfterMigration - } - } - - async setLegacyConfigForMigration (config: LegacyCypressConfigJson) { - assert(this.ctx.currentProject) - const legacyConfigForMigration = await processConfigViaLegacyPlugins(this.ctx.currentProject, config) - - this.ctx.update((coreData) => { - coreData.migration.legacyConfigForMigration = legacyConfigForMigration - }) - - return legacyConfigForMigration - } - - async renameSpecsFolder () { - if (!this.ctx.currentProject) { - throw Error('Need to set currentProject before you can rename specs folder') - } - - const projectRoot = this.ctx.path.join(this.ctx.currentProject) - const from = path.join(projectRoot, 'cypress', 'integration') - const to = path.join(projectRoot, 'cypress', 'e2e') - - this.ctx.update((coreData) => { - coreData.migration.flags = { - ...coreData.migration.flags, - shouldAddCustomE2ESpecPattern: true, - } - }) - - await this.ctx.fs.move(from, to) - } - - async renameSpecFiles (beforeSpecs: string[], afterSpecs: string[]) { - if (!this.ctx.currentProject) { - throw Error('Need to set currentProject before you can rename files') - } - - const specsToMove: SpecToMove[] = [] - - for (let i = 0; i < beforeSpecs.length; i++) { - const from = beforeSpecs[i] - const to = afterSpecs[i] - - if (!from || !to) { - throw Error(`Must have matching to and from. Got from: ${from} and to: ${to}`) - } - - specsToMove.push({ from, to }) - } - - const projectRoot = this.ctx.path.join(this.ctx.currentProject) - - await moveSpecFiles(projectRoot, specsToMove) - await cleanUpIntegrationFolder(this.ctx.currentProject) - } - - async renameSupportFile () { - if (!this.ctx.currentProject) { - throw Error(`Need current project before starting migration!`) - } - - const result = await supportFilesForMigration(this.ctx.currentProject) - - const beforeRelative = result.before.relative - const afterRelative = result.after.relative - - if (!beforeRelative || !afterRelative) { - throw new NonStandardMigrationError('support') - } - - await this.ctx.fs.rename( - path.join(this.ctx.currentProject, beforeRelative), - path.join(this.ctx.currentProject, afterRelative), - ) - } - - async finishReconfigurationWizard () { - this.ctx.lifecycleManager.refreshMetaState() - await this.ctx.lifecycleManager.refreshLifecycle() - } - - async nextStep () { - const filteredSteps = this.ctx.coreData.migration.filteredSteps - const index = filteredSteps.indexOf(this.ctx.coreData.migration.step) - - if (index === -1) { - throw new Error('Invalid step') - } - - const nextIndex = index + 1 - - if (nextIndex < filteredSteps.length) { - const nextStep = filteredSteps[nextIndex] - - if (nextStep) { - this.ctx.update((coreData) => { - coreData.migration.step = nextStep - }) - } - } else { - await this.finishReconfigurationWizard() - } - } - - async closeManualRenameWatcher () { - await this.ctx.migration.closeManualRenameWatcher() - } - - async assertSuccessfulConfigMigration (migratedConfigFile: string = 'cypress.config.js') { - const actual = formatConfig(await this.ctx.file.readFileInProject(migratedConfigFile)) - - const configExtension = path.extname(migratedConfigFile) - const expected = formatConfig(await this.ctx.file.readFileInProject(`expected-cypress.config${configExtension}`)) - - if (actual !== expected) { - throw Error(`Expected ${actual} to equal ${expected}`) - } - } - - reset (config?: LegacyCypressConfigJson) { - this.ctx.update((coreData) => { - coreData.migration = { ...makeCoreData().migration, legacyConfigForMigration: config } - }) - } -} diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index fb9a8fa2acb0..f2120ec5a567 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -122,7 +122,6 @@ export class ProjectActions { // Also clear any data associated with the linked cloud project this.ctx.actions.cloudProject.clearCloudProject() - this.ctx.actions.migration.reset() await this.ctx.lifecycleManager.clearCurrentProject() resetIssuedWarnings() await this.api.closeActiveProject() diff --git a/packages/data-context/src/actions/index.ts b/packages/data-context/src/actions/index.ts index 9616fd729e19..b5bae1d19219 100644 --- a/packages/data-context/src/actions/index.ts +++ b/packages/data-context/src/actions/index.ts @@ -14,7 +14,6 @@ export * from './ErrorActions' export * from './EventCollectorActions' export * from './FileActions' export * from './LocalSettingsActions' -export * from './MigrationActions' export * from './NotificationActions' export * from './ProjectActions' export * from './ServersActions' diff --git a/packages/data-context/src/codegen/spec-options.ts b/packages/data-context/src/codegen/spec-options.ts index e4b08eb8ec7b..18bda803ce05 100644 --- a/packages/data-context/src/codegen/spec-options.ts +++ b/packages/data-context/src/codegen/spec-options.ts @@ -3,7 +3,7 @@ import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen' import fs from 'fs-extra' import { uniq, upperFirst } from 'lodash' import path from 'path' -import { FileExtension, getDefaultSpecFileName } from '../sources/migration/utils' +import { FileExtension, getDefaultSpecFileName } from '../util/files' import { toPosix } from '../util' import type { FoundSpec } from '@packages/types' diff --git a/packages/data-context/src/data/LegacyPluginsIpc.ts b/packages/data-context/src/data/LegacyPluginsIpc.ts deleted file mode 100644 index 7687e4bc403e..000000000000 --- a/packages/data-context/src/data/LegacyPluginsIpc.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* eslint-disable no-dupe-class-members */ -import type { ChildProcess } from 'child_process' -import EventEmitter from 'events' -import type { CypressError } from '@packages/errors' -import type { LegacyCypressConfigJson } from '../sources' - -export class LegacyPluginsIpc extends EventEmitter { - constructor (readonly childProcess: ChildProcess) { - super() - childProcess.on('message', (msg: { event: string, args: any[] }) => { - this.emit(msg.event, ...msg.args) - }) - - childProcess.once('disconnect', () => { - this.emit('disconnect') - }) - } - - send(event: 'loadLegacyPlugins', legacyConfig: LegacyCypressConfigJson): boolean - send (event: string, ...args: any[]) { - if (this.childProcess.killed || !this.childProcess.connected) { - return false - } - - return this.childProcess.send({ event, args }) - } - - on(event: 'ready', listener: () => void): this - on(event: 'loadLegacyPlugins:error', listener: (error: CypressError) => void): this - on(event: 'childProcess:unhandledError', listener: (legacyConfig: LegacyCypressConfigJson) => void): this - on(event: 'loadLegacyPlugins:reply', listener: (legacyConfig: LegacyCypressConfigJson) => void): this - on (evt: string, listener: (...args: any[]) => void) { - return super.on(evt, listener) - } - - killChildProcess () { - this.childProcess.kill() - this.childProcess.stdout?.removeAllListeners() - this.childProcess.stderr?.removeAllListeners() - this.childProcess.removeAllListeners() - - this.removeAllListeners() - } -} diff --git a/packages/data-context/src/data/ProjectConfigIpc.ts b/packages/data-context/src/data/ProjectConfigIpc.ts index 32a186229f81..417213ed38ab 100644 --- a/packages/data-context/src/data/ProjectConfigIpc.ts +++ b/packages/data-context/src/data/ProjectConfigIpc.ts @@ -3,13 +3,12 @@ import { CypressError, getError } from '@packages/errors' import type { FullConfig, TestingType } from '@packages/types' import { ChildProcess, fork, ForkOptions, spawn } from 'child_process' import EventEmitter from 'events' -import fs from 'fs-extra' import path from 'path' import inspector from 'inspector' import debugLib from 'debug' +import { getTsconfig } from 'get-tsconfig' import { autoBindDebug, hasTypeScriptInstalled, toPosix } from '../util' import _ from 'lodash' -import { pathToFileURL } from 'url' import os from 'os' import semver from 'semver' import type { OTLPTraceExporterCloud } from '@packages/telemetry' @@ -17,11 +16,12 @@ import { telemetry, encodeTelemetryContext } from '@packages/telemetry' const pkg = require('@packages/root') const debug = debugLib(`cypress:lifecycle:ProjectConfigIpc`) +const debugVerbose = debugLib(`cypress-verbose:lifecycle:ProjectConfigIpc`) const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child') -const tsNodeEsm = pathToFileURL(require.resolve('ts-node/esm/transpile-only')).href -const tsNode = toPosix(require.resolve('@packages/server/lib/plugins/child/register_ts_node')) +// NOTE: need the file:// prefix to avoid https://nodejs.org/api/errors.html#err_unsupported_esm_url_scheme on windows +const tsx = os.platform() === 'win32' ? `file://${toPosix(require.resolve('tsx'))}` : toPosix(require.resolve('tsx')) export type IpcHandler = (ipc: ProjectConfigIpc) => void @@ -262,10 +262,8 @@ export class ProjectConfigIpc extends EventEmitter { private forkConfigProcess () { const configProcessArgs = ['--projectRoot', this.projectRoot, '--file', this.configFilePath] - // allow the use of ts-node in subprocesses tests by removing the env constant from it - // without this line, packages/ts/register.js never registers the ts-node module for config and - // run_plugins can't use the config module. - const env = _.omit(process.env, 'CYPRESS_INTERNAL_E2E_TESTING_SELF') + // we do NOT want telemetry enabled within our cy-in-cy tests as it isn't configured to handled it + const env = _.omit(process.env, 'CYPRESS_INTERNAL_E2E_TESTING_SELF', 'CYPRESS_INTERNAL_ENABLE_TELEMETRY') env.NODE_OPTIONS = process.env.ORIGINAL_NODE_OPTIONS || '' @@ -279,86 +277,72 @@ export class ProjectConfigIpc extends EventEmitter { if (inspector.url()) { childOptions.execArgv = _.chain(process.execArgv.slice(0)) .remove('--inspect-brk') + // NOTE: The IDE in which you are working likely will not let attach to this process until it is running if using the --inspect option + // If needing to debug the child process (webpack-dev-server/vite-dev-server/webpack-preprocessor(s)/config loading), you may want to use --inspect-brk instead + // as it will NOT execute that process until you attach the debugger to it. .push(`--inspect=${process.debugPort + 1}`) .value() } - debug('fork child process %o', { CHILD_PROCESS_FILE_PATH, configProcessArgs, childOptions: _.omit(childOptions, 'env') }) - - let isProjectUsingESModules = false + /** + * Before the introduction of tsx, Cypress used ts-node (@see https://github.com/TypeStrong/ts-node) with native node to try and load the user's cypress.config.ts file. + * This presented problems because the Cypress node runtime runs in commonjs, which may not be compatible with the user's cypress.config.ts and tsconfig.json. + * To mitigate the aforementioned runtime incompatibility, we used to force TypeScript options for the user in order to load their config inside the our node context + * via a child process, which lead to clashes and issues (outlined in the comments below). + * This is best explained historically in our docs which a screenshot can be see in @see https://github.com/cypress-io/cypress/issues/30426#issuecomment-2805204540 and can be seen + * in an older version of the Cypress codebase (@see https://github.com/cypress-io/cypress/blob/v14.3.0/packages/server/lib/plugins/child/ts_node.js#L24) + * + * Attempted workarounds with ts-node and node: @see https://github.com/cypress-io/cypress/pull/28709 + * Example continued end user issues: @see https://github.com/cypress-io/cypress/issues/30954 and @see https://github.com/cypress-io/cypress/issues/30925 + * Spike into ts-node alternatives (a lot of useful comments on tsx): @see https://github.com/cypress-io/cypress/issues/30426 + * feature issue to replace ts-node as our end user TypeScript loader: @see https://github.com/cypress-io/cypress/issues/31185 + * + * tsx (@see https://tsx.is/) is able to work with both CommonJS and ESM at the same time ( @see https://tsx.is/#seamless-cjs-%E2%86%94-esm-imports), which solves the problem of interoperability that + * Cypress faced with ts-node and really just node itself. We no longer need experimental node flags and ts-node permutations to load the user's config file. + * We can use tsx to load just about anything, including JavaScript files (@see https://github.com/privatenumber/ts-runtime-comparison)! + */ - try { - // TODO: convert this to async FS methods - // eslint-disable-next-line no-restricted-syntax - const pkgJson = fs.readJsonSync(path.join(this.projectRoot, 'package.json')) - - isProjectUsingESModules = pkgJson.type === 'module' - } catch (e) { - // project does not have `package.json` or it was not found - // reasonable to assume not using es modules - } + debug('fork child process %o', { CHILD_PROCESS_FILE_PATH, configProcessArgs, childOptions: _.omit(childOptions, 'env') }) if (!childOptions.env) { childOptions.env = {} } - // If they've got TypeScript installed, we can use - // ts-node for CommonJS - // ts-node/esm for ESM - if (hasTypeScriptInstalled(this.projectRoot)) { + /** + * use --import for node versions + * 20.6.0 and above for 20.x.x as --import is supported + * use --loader for node under 20.6.0 for 20.x.x + * @see https://tsx.is/dev-api/node-cli#node-js-cli + */ + let tsxLoader = this.nodeVersion && semver.lt(this.nodeVersion, '20.6.0') ? `--loader ${tsx}` : `--import ${tsx}` + + // If they've got TypeScript installed, we can use tsx for CommonJS and ESM. + // @see https://tsx.is/dev-api/node-cli#node-js-cli + const userHasTypeScriptInstalled = hasTypeScriptInstalled(this.projectRoot) + + if (userHasTypeScriptInstalled) { debug('found typescript in %s', this.projectRoot) - if (isProjectUsingESModules) { - debug(`using --experimental-specifier-resolution=node with --loader ${tsNodeEsm}`) - // Use the ts-node/esm loader so they can use TypeScript with `"type": "module". - // The loader API is experimental and will change. - // The same can be said for the other alternative, esbuild, so this is the - // best option that leverages the existing modules we bundle in the binary. - // @see ts-node esm loader https://typestrong.org/ts-node/docs/usage/#node-flags-and-other-tools - // @see Node.js Loader API https://nodejs.org/api/esm.html#customizing-esm-specifier-resolution-algorithm - let tsNodeEsmLoader = `--experimental-specifier-resolution=node --loader ${tsNodeEsm}` - - // starting in nodejs 20.19.0 and 22.7.0, the --experimental-detect-module option is now enabled by default. - // We need to disable it with the --no-experimental-detect-module flag. - // @see https://github.com/cypress-io/cypress/issues/30084 - if (this.nodeVersion && (semver.gte(this.nodeVersion, '22.7.0') || semver.satisfies(this.nodeVersion, '>= 20.19.0 < 21.0.0'))) { - debug(`detected node version ${this.nodeVersion}, adding --no-experimental-detect-module option to child_process NODE_OPTIONS.`) - tsNodeEsmLoader = `${tsNodeEsmLoader} --no-experimental-detect-module` - } - - // starting in nodejs 20.19.0 and 22.12.0, the --experimental-require-module option is now enabled by default. - // We need to disable it with the --no-experimental-require-module flag. - // @see https://github.com/cypress-io/cypress/issues/30715 - if (this.nodeVersion && (semver.gte(this.nodeVersion, '22.12.0') || semver.satisfies(this.nodeVersion, '>= 20.19.0 < 21.0.0'))) { - debug(`detected node version ${this.nodeVersion}, adding --no-experimental-require-module option to child_process NODE_OPTIONS.`) - tsNodeEsmLoader = `${tsNodeEsmLoader} --no-experimental-require-module` - } - - if (childOptions.env.NODE_OPTIONS) { - childOptions.env.NODE_OPTIONS += ` ${tsNodeEsmLoader}` - } else { - childOptions.env.NODE_OPTIONS = tsNodeEsmLoader - } + + // TODO: get the tsconfig.json that applies to the users cypress.config.ts file + // right now, we are just using the tsconfig.json we find in the project root + const tsConfig = getTsconfig(this.projectRoot) + + if (tsConfig) { + debug(`tsconfig.json found at ${tsConfig.path}`) + childOptions.env.TSX_TSCONFIG_PATH = tsConfig.path + + debugVerbose(`tsconfig.json parsed as follows: %o`, tsConfig.config) } else { - // Not using ES Modules (via "type": "module"), - // so we just register the standard ts-node module - // to handle TypeScript that is compiled to CommonJS. - // We do NOT use the `--loader` flag because we have some additional - // custom logic for ts-node when used with CommonJS that needs to be evaluated - // so we need to load and evaluate the hook first using the `--require` module API. - const tsNodeLoader = `--require "${tsNode}"` - - debug(`using cjs with --require ${tsNode}`) - - if (childOptions.env.NODE_OPTIONS) { - childOptions.env.NODE_OPTIONS += ` ${tsNodeLoader}` - } else { - childOptions.env.NODE_OPTIONS = tsNodeLoader - } + debug(`No tsconfig.json found! Attempting to parse file without tsconfig.json.`) } + } + + debug(`using generic ${tsxLoader} for esm and cjs ${userHasTypeScriptInstalled ? 'with TypeScript' : ''}.`) + + if (childOptions.env.NODE_OPTIONS) { + childOptions.env.NODE_OPTIONS += ` ${tsxLoader}` } else { - // Just use Node's built-in ESM support. - // TODO: Consider using userland `esbuild` with Node's --loader API to handle ESM. - debug(`no typescript found, just use regular Node.js`) + childOptions.env.NODE_OPTIONS = tsxLoader } const telemetryCtx = encodeTelemetryContext({ context: telemetry.getActiveContextObject(), version: pkg.version }) diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts index 86bd1c9e7aae..84c14cd83f59 100644 --- a/packages/data-context/src/data/ProjectLifecycleManager.ts +++ b/packages/data-context/src/data/ProjectLifecycleManager.ts @@ -8,7 +8,6 @@ */ import path from 'path' import _ from 'lodash' -import resolve from 'resolve' import fs from 'fs' import { getError, CypressError, ConfigValidationFailureInfo } from '@packages/errors' @@ -16,7 +15,7 @@ import type { DataContext } from '..' import assert from 'assert' import type { AllModeOptions, FoundBrowser, FullConfig, TestingType } from '@packages/types' import { autoBindDebug } from '../util/autoBindDebug' -import { EventCollectorSource, GitDataSource, LegacyCypressConfigJson } from '../sources' +import { EventCollectorSource, GitDataSource } from '../sources' import { OnFinalConfigLoadedOptions, ProjectConfigManager } from './ProjectConfigManager' import pDefer from 'p-defer' import { EventRegistrar } from './EventRegistrar' @@ -56,23 +55,19 @@ export interface InjectedConfigApi { export interface ProjectMetaState { isUsingTypeScript: boolean - hasLegacyCypressJson: boolean hasCypressEnvFile: boolean hasValidConfigFile: boolean hasSpecifiedConfigViaCLI: false | string allFoundConfigFiles: string[] - needsCypressJsonMigration: boolean isProjectUsingESModules: boolean } const PROJECT_META_STATE: ProjectMetaState = { isUsingTypeScript: false, - hasLegacyCypressJson: false, allFoundConfigFiles: [], hasCypressEnvFile: false, hasSpecifiedConfigViaCLI: false, hasValidConfigFile: false, - needsCypressJsonMigration: false, isProjectUsingESModules: false, } @@ -108,11 +103,6 @@ export class ProjectLifecycleManager { async getProjectId (): Promise { try { - // No need to kick off config initialization if we need to migrate - if (this.ctx.migration.needsCypressJsonMigration()) { - return null - } - const contents = await this.ctx.project.getConfig() return contents.projectId ?? null @@ -514,37 +504,13 @@ export class ProjectLifecycleManager { * @returns true if we can initialize and false if not */ private readyToInitialize (projectRoot: string): boolean { - const { needsCypressJsonMigration } = this.refreshMetaState() - - const legacyConfigPath = path.join(projectRoot, this.ctx.migration.legacyConfigFile) - - if (needsCypressJsonMigration && !this.ctx.isRunMode && this.ctx.fs.existsSync(legacyConfigPath)) { - return false - } - - this.legacyPluginGuard() - + // This calls a lot of methods that are necessary to check config-wise upfront + this.refreshMetaState() this.configFileWarningCheck() return this.metaState.hasValidConfigFile } - async legacyMigration () { - try { - const legacyConfigPath = path.join(this.projectRoot, this.ctx.migration.legacyConfigFile) - // we run the legacy plugins/index.js in a child process - // and mutate the config based on the return value for migration - // only used in open mode (cannot migrate via terminal) - const legacyConfig = await this.ctx.fs.readJson(legacyConfigPath) as LegacyCypressConfigJson - - // should never throw, unless there existing pluginsFile errors out, - // in which case they are attempting to migrate an already broken project. - await this.ctx.actions.migration.initialize(legacyConfig) - } catch (error) { - this.onLoadError(error) - } - } - get runModeExitEarly () { return this._runModeExitEarly } @@ -658,19 +624,6 @@ export class ProjectLifecycleManager { return this._eventRegistrar.executeNodeEvent(event, args) } - private legacyPluginGuard () { - // test and warn for incompatible plugin - try { - const retriesPluginPath = path.dirname(resolve.sync('cypress-plugin-retries/package.json', { - basedir: this.projectRoot, - })) - - this.ctx.onWarning(getError('INCOMPATIBLE_PLUGIN_RETRIES', path.relative(this.projectRoot, retriesPluginPath))) - } catch (e) { - // noop, incompatible plugin not installed - } - } - /** * Find all information about the project we need to know to prompt different * onboarding screens, suggestions in the onboarding wizard, etc. @@ -679,7 +632,6 @@ export class ProjectLifecycleManager { const configFile = this.ctx.modeOptions.configFile const metaState: ProjectMetaState = { ...PROJECT_META_STATE, - hasLegacyCypressJson: this.ctx.migration.legacyConfigFileExists(), hasCypressEnvFile: fs.existsSync(this._pathToFile('cypress.env.json')), } @@ -696,7 +648,6 @@ export class ProjectLifecycleManager { projectRoot: this.projectRoot, customConfigFile: configFile, pkgJson, - isMigrating: metaState.hasLegacyCypressJson, }) === 'ts' } catch { // No need to handle @@ -704,23 +655,10 @@ export class ProjectLifecycleManager { if (configFile) { metaState.hasSpecifiedConfigViaCLI = this._pathToFile(configFile) - if (configFile.endsWith('.json')) { - metaState.needsCypressJsonMigration = true - - const configFileNameAfterMigration = configFile.replace('.json', `.config.${metaState.isUsingTypeScript ? 'ts' : 'js'}`) - if (this.ctx.fs.existsSync(this._pathToFile(configFileNameAfterMigration))) { - if (this.ctx.fs.existsSync(this._pathToFile(configFile))) { - this.ctx.onError(getError('LEGACY_CONFIG_FILE', configFileNameAfterMigration, this.projectRoot, configFile)) - } else { - this.ctx.onError(getError('MIGRATION_ALREADY_OCURRED', configFileNameAfterMigration, configFile)) - } - } - } else { - this.setConfigFilePath(configFile) - if (fs.existsSync(this.configFilePath)) { - metaState.hasValidConfigFile = true - } + this.setConfigFilePath(configFile) + if (fs.existsSync(this.configFilePath)) { + metaState.hasValidConfigFile = true } this._projectMetaState = metaState @@ -758,10 +696,6 @@ export class ProjectLifecycleManager { configFilePathSet = true } - if (metaState.hasLegacyCypressJson && !metaState.hasValidConfigFile) { - metaState.needsCypressJsonMigration = true - } - this._projectMetaState = metaState return metaState @@ -824,7 +758,7 @@ export class ProjectLifecycleManager { return } - if (testingType === 'e2e' && !this.ctx.migration.needsCypressJsonMigration()) { + if (testingType === 'e2e') { // E2E doesn't have a wizard, so if we have a testing type on load we just create/update their cypress.config.js. await this.ctx.actions.wizard.scaffoldTestingType() } else if (testingType === 'component') { @@ -864,17 +798,9 @@ export class ProjectLifecycleManager { this.onLoadError(getError('CONFIG_FILE_NOT_FOUND', path.basename(this.metaState.hasSpecifiedConfigViaCLI), path.dirname(this.metaState.hasSpecifiedConfigViaCLI))) } - if (this.metaState.hasLegacyCypressJson && !this.metaState.hasValidConfigFile && this.ctx.isRunMode) { - this.onLoadError(getError('CONFIG_FILE_MIGRATION_NEEDED', this.projectRoot)) - } - if (this.metaState.allFoundConfigFiles.length > 1) { this.onLoadError(getError('CONFIG_FILES_LANGUAGE_CONFLICT', this.projectRoot, this.metaState.allFoundConfigFiles)) } - - if (this.metaState.hasValidConfigFile && this.metaState.hasLegacyCypressJson) { - this.onLoadError(getError('LEGACY_CONFIG_FILE', path.basename(this.configFilePath), this.projectRoot)) - } } /** diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index 73613a8233f1..c23ec5ec8bbb 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -1,4 +1,4 @@ -import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, StudioLifecycleManagerShape } from '@packages/types' +import type { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, StudioLifecycleManagerShape } from '@packages/types' import { WizardBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition, ErroredFramework } from '@packages/scaffold-config' import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' // tslint:disable-next-line no-implicit-dependencies - electron dep needs to be defined @@ -7,7 +7,7 @@ import type { ChildProcess } from 'child_process' import type { SocketIONamespace, SocketIOServer } from '@packages/socket' import type { Server } from 'http' import type { ErrorWrapperSource } from '@packages/errors' -import type { EventCollectorSource, GitDataSource, LegacyCypressConfigJson } from '../sources' +import type { EventCollectorSource, GitDataSource } from '../sources' import { machineId as getMachineId } from 'node-machine-id' import type { CDPSocketServer } from '@packages/socket/lib/cdp-socket' @@ -83,26 +83,6 @@ export interface WizardDataShape { erroredFrameworks: ErroredFramework[] } -export interface MigrationDataShape { - // TODO: have the model of migration here - step: MigrationStep - legacyConfigForMigration?: LegacyCypressConfigJson | null - filteredSteps: MigrationStep[] - flags: { - hasCustomIntegrationFolder: boolean - hasCustomIntegrationTestFiles: boolean - - hasCustomComponentFolder: boolean - hasCustomComponentTestFiles: boolean - - hasCustomSupportFile: boolean - hasComponentTesting: boolean - hasE2ESpec: boolean - hasPluginsFile: boolean - shouldAddCustomE2ESpecPattern: boolean - } -} - export interface ElectronShape { app: App | null browserWindow: BrowserWindow | null @@ -150,7 +130,6 @@ export interface CoreDataShape { currentTestingType: TestingType | null diagnostics: Diagnostics wizard: WizardDataShape - migration: MigrationDataShape user: AuthenticatedUserShape | null electron: ElectronShape authState: AuthStateShape @@ -215,22 +194,6 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa frameworks: CT_FRAMEWORKS.map((framework) => resolveComponentFrameworkDefinition(framework)), erroredFrameworks: [], }, - migration: { - step: 'renameAuto', - legacyConfigForMigration: null, - filteredSteps: [...MIGRATION_STEPS], - flags: { - hasCustomIntegrationFolder: false, - hasCustomIntegrationTestFiles: false, - hasCustomComponentFolder: false, - hasCustomComponentTestFiles: false, - hasCustomSupportFile: false, - hasComponentTesting: true, - hasE2ESpec: true, - hasPluginsFile: true, - shouldAddCustomE2ESpecPattern: false, - }, - }, activeBrowser: null, user: null, electron: { diff --git a/packages/data-context/src/data/index.ts b/packages/data-context/src/data/index.ts index 5b0716252f86..218bed9a2c0f 100644 --- a/packages/data-context/src/data/index.ts +++ b/packages/data-context/src/data/index.ts @@ -3,7 +3,6 @@ export * from './CypressEnv' export * from './EventRegistrar' -export * from './LegacyPluginsIpc' export * from './ProjectConfigIpc' export * from './ProjectConfigManager' export * from './ProjectLifecycleManager' diff --git a/packages/data-context/src/sources/ErrorDataSource.ts b/packages/data-context/src/sources/ErrorDataSource.ts index 965b84b1534b..1bf2c0a15c47 100644 --- a/packages/data-context/src/sources/ErrorDataSource.ts +++ b/packages/data-context/src/sources/ErrorDataSource.ts @@ -2,9 +2,13 @@ import { ErrorWrapperSource, stackUtils } from '@packages/errors' import path from 'path' import _ from 'lodash' import { codeFrameColumns } from '@babel/code-frame' - +import os from 'os' import type { DataContext } from '..' +const tsxCodeFrameFilter = '/node_modules/tsx/dist/register' +const windowsTsxCodeFrameFilter = tsxCodeFrameFilter.replaceAll('/', '\\') +const isWindows = os.platform() === 'win32' + export interface CodeFrameShape { line: number column: number @@ -24,7 +28,7 @@ export class ErrorDataSource { return null } - // If we saw a TSError, or a esbuild error we will extract the error location from the message + // If we saw a TransformError, or a esbuild error we will extract the error location from the message const compilerErrorLocation = source.cypressError.originalError?.compilerErrorLocation let line: number | null | undefined @@ -38,7 +42,9 @@ export class ErrorDataSource { } else { // Skip any stack trace lines which come from node:internal code const stackLines = stackUtils.getStackLines(source.cypressError.stack ?? '') - const filteredStackLines = stackLines.filter((stackLine) => !stackLine.includes('node:electron') && !stackLine.includes('node:internal') && !stackLine.includes('source-map-support')) + + // we want to filter out any tsx transformation code in the stack to help identify the error. Windows stack can have both posix paths and dos paths so we need to filter both (last line is a no-op on posix as its the same thing) + const filteredStackLines = stackLines.filter((stackLine) => !stackLine.includes('node:electron') && !stackLine.includes('node:internal') && !stackLine.includes('source-map-support') && !stackLine.includes(tsxCodeFrameFilter) && !(isWindows && stackLine.includes(windowsTsxCodeFrameFilter))) const parsedLine = stackUtils.parseStackLine(filteredStackLines[0] ?? '') if (parsedLine) { diff --git a/packages/data-context/src/sources/MigrationDataSource.ts b/packages/data-context/src/sources/MigrationDataSource.ts deleted file mode 100644 index 0d2d67790aa3..000000000000 --- a/packages/data-context/src/sources/MigrationDataSource.ts +++ /dev/null @@ -1,299 +0,0 @@ -import type { TestingType } from '@packages/types' -import type chokidar from 'chokidar' -import type { DataContext } from '..' -import { - createConfigString, - initComponentTestingMigration, - ComponentTestingMigrationStatus, - supportFilesForMigration, - getSpecs, - applyMigrationTransform, - shouldShowRenameSupport, - getIntegrationFolder, - isDefaultTestFiles, - getComponentTestFilesGlobs, - getComponentFolder, -} from './migration' -import _ from 'lodash' - -import type { FilePart } from './migration/format' -import Debug from 'debug' -import path from 'path' - -const debug = Debug('cypress:data-context:sources:MigrationDataSource') - -export type LegacyCypressConfigJson = Partial<{ - component: Omit - e2e: Omit - pluginsFile: string | false - supportFile: string | false - slowTestThreshold: number - componentFolder: string | false - integrationFolder: string - testFiles: string | string[] - ignoreTestFiles: string | string[] - env: { [key: string]: any } - [index: string]: any -}> - -export interface MigrationFile { - testingType: TestingType - before: { - relative: string - parts: FilePart[] - } - after: { - relative: string - parts: FilePart[] - } -} - -export class MigrationDataSource { - private componentTestingMigrationWatcher: chokidar.FSWatcher | null = null - componentTestingMigrationStatus?: ComponentTestingMigrationStatus - - constructor (private ctx: DataContext) { } - - get legacyConfig () { - if (!this.ctx.coreData.migration.legacyConfigForMigration) { - throw Error(`Expected _legacyConfig to be set. Did you forget to call MigrationDataSource#initialize?`) - } - - return this.ctx.coreData.migration.legacyConfigForMigration - } - - get legacyConfigProjectId () { - return this.legacyConfig.projectId || this.legacyConfig.e2e?.projectId - } - - get shouldMigratePreExtension () { - return !this.legacyConfigProjectId - } - - get legacyConfigFile () { - if (this.ctx.modeOptions.configFile && this.ctx.modeOptions.configFile.endsWith('.json')) { - return this.ctx.modeOptions.configFile - } - - return 'cypress.json' - } - - legacyConfigFileExists (): boolean { - // If we aren't in a current project we definitely don't have a legacy config file - if (!this.ctx.currentProject) { - return false - } - - const configFilePath = path.isAbsolute(this.legacyConfigFile) ? this.legacyConfigFile : path.join(this.ctx.currentProject, this.legacyConfigFile) - const legacyConfigFileExists = this.ctx.fs.existsSync(configFilePath) - - return Boolean(legacyConfigFileExists) - } - - needsCypressJsonMigration (): boolean { - const legacyConfigFileExists = this.legacyConfigFileExists() - - return this.ctx.lifecycleManager.metaState.needsCypressJsonMigration && Boolean(legacyConfigFileExists) - } - - async getComponentTestingMigrationStatus () { - debug('getComponentTestingMigrationStatus: start') - if (!this.legacyConfig || !this.ctx.currentProject) { - throw Error('Need currentProject and config to continue') - } - - const componentFolder = getComponentFolder(this.legacyConfig) - - // no component folder, so no specs to migrate - // this should never happen since we never show the - // component specs migration step ("renameManual") - if (componentFolder === false) { - return null - } - - debug('getComponentTestingMigrationStatus: componentFolder', componentFolder) - - if (!this.componentTestingMigrationWatcher) { - debug('getComponentTestingMigrationStatus: initializing watcher') - const onFileMoved = async (status: ComponentTestingMigrationStatus) => { - this.componentTestingMigrationStatus = status - debug('getComponentTestingMigrationStatus: file moved %O', status) - - if (status.completed) { - await this.componentTestingMigrationWatcher?.close() - this.componentTestingMigrationWatcher = null - } - - // TODO(lachlan): is this the right place to use the emitter? - this.ctx.emitter.toLaunchpad() - } - - const { status, watcher } = await initComponentTestingMigration( - this.ctx.currentProject, - componentFolder, - getComponentTestFilesGlobs(this.legacyConfig), - onFileMoved, - ) - - this.componentTestingMigrationStatus = status - this.componentTestingMigrationWatcher = watcher - debug('getComponentTestingMigrationStatus: watcher initialized. Status: %o', status) - } - - if (!this.componentTestingMigrationStatus) { - throw Error(`Status should have been assigned by the watcher. Something is wrong`) - } - - return this.componentTestingMigrationStatus - } - - async supportFilesForMigrationGuide (): Promise { - if (!this.ctx.currentProject) { - throw Error('Need this.ctx.currentProject') - } - - debug('supportFilesForMigrationGuide: config %O', this.legacyConfig) - if (!await shouldShowRenameSupport(this.ctx.currentProject, this.legacyConfig)) { - return null - } - - if (!this.ctx.currentProject) { - throw Error(`Need this.ctx.projectRoot!`) - } - - try { - const supportFiles = await supportFilesForMigration(this.ctx.currentProject) - - debug('supportFilesForMigrationGuide: supportFiles %O', supportFiles) - - return supportFiles - } catch (err) { - debug('supportFilesForMigrationGuide: err %O', err) - - return null - } - } - - async getSpecsForMigrationGuide (): Promise { - if (!this.ctx.currentProject) { - throw Error(`Need this.ctx.projectRoot!`) - } - - const specs = await getSpecs(this.ctx.currentProject, this.legacyConfig) - - const e2eMigrationOptions = { - // If the configFile has projectId, we do not want to change the preExtension - // so, we can keep the cloud history - shouldMigratePreExtension: this.shouldMigratePreExtension, - } - - const canBeAutomaticallyMigrated: MigrationFile[] = specs.integration.map((s) => applyMigrationTransform(s, e2eMigrationOptions)).filter((spec) => spec.before.relative !== spec.after.relative) - - const defaultComponentPattern = isDefaultTestFiles(this.legacyConfig, 'component') - - // Can only migration component specs if they use the default testFiles pattern. - if (defaultComponentPattern) { - canBeAutomaticallyMigrated.push(...specs.component.map((s) => applyMigrationTransform(s)).filter((spec) => spec.before.relative !== spec.after.relative)) - } - - return this.checkAndUpdateDuplicatedSpecs(canBeAutomaticallyMigrated) - } - - async createConfigString () { - if (!this.ctx.currentProject) { - throw Error('Need currentProject!') - } - - const { isUsingTypeScript } = this.ctx.lifecycleManager.metaState - - return createConfigString(this.legacyConfig, { - hasComponentTesting: this.ctx.coreData.migration.flags.hasComponentTesting, - hasE2ESpec: this.ctx.coreData.migration.flags.hasE2ESpec, - hasPluginsFile: this.ctx.coreData.migration.flags.hasPluginsFile, - projectRoot: this.ctx.currentProject, - isUsingTypeScript, - isProjectUsingESModules: this.ctx.lifecycleManager.metaState.isProjectUsingESModules, - shouldAddCustomE2ESpecPattern: this.ctx.coreData.migration.flags.shouldAddCustomE2ESpecPattern, - }) - } - - async integrationFolder () { - return getIntegrationFolder(this.legacyConfig) - } - - async componentFolder () { - return getComponentFolder(this.legacyConfig) - } - - async closeManualRenameWatcher () { - if (this.componentTestingMigrationWatcher) { - await this.componentTestingMigrationWatcher.close() - this.componentTestingMigrationWatcher = null - } - } - - get configFileNameAfterMigration () { - return this.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`) - } - - private checkAndUpdateDuplicatedSpecs (specs: MigrationFile[]) { - const updatedSpecs: MigrationFile[] = [] - - const sortedSpecs = this.sortSpecsByExtension(specs) - - sortedSpecs.forEach((spec) => { - const specExist = _.find(updatedSpecs, (x) => x.after.relative === spec.after.relative) - - if (specExist) { - const beforeParts: FilePart[] = JSON.parse(JSON.stringify(spec.before.parts)) - const preExtensionBefore = beforeParts.find((part) => part.group === 'preExtension') - - if (preExtensionBefore) { - preExtensionBefore.highlight = false - } - - const afterParts: FilePart[] = JSON.parse(JSON.stringify(spec.after.parts)) - const fileNameAfter = afterParts.find((part) => part.group === 'fileName') - - if (fileNameAfter && preExtensionBefore) { - const beforePreExtension = preExtensionBefore?.text?.replace('.', '') - - fileNameAfter.text = `${fileNameAfter.text}${beforePreExtension}` - } - - spec.before.parts = beforeParts - spec.after.parts = afterParts - spec.after.relative = afterParts.map((x) => x.text).join('') - } - - updatedSpecs.push(spec) - }) - - return updatedSpecs - } - - private sortSpecsByExtension (specs: MigrationFile[]) { - const sortedExtensions = ['.spec.', '.Spec.', '_spec.', '_Spec.', '-spec.', '-Spec.', '.test.', '.Test.', '_test.', '_Test.', '-test.', '-Test.'] - - return specs.sort(function (a, b) { - function getExtIndex (spec: string) { - let index = -1 - - // Sort the specs based on the extension, giving priority to .spec - sortedExtensions.some((c, i) => { - if (~spec.indexOf(c)) { - index = i - - return true - } - - return false - }) - - return index - } - - return getExtIndex(a.before.relative) - getExtIndex(b.before.relative) - }) - } -} diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts index 8476c15c6f2c..77bd5bea9ffb 100644 --- a/packages/data-context/src/sources/ProjectDataSource.ts +++ b/packages/data-context/src/sources/ProjectDataSource.ts @@ -21,7 +21,7 @@ import { toPosix } from '../util/file' import type { FilePartsShape } from '@packages/graphql/src/schemaTypes/objectTypes/gql-FileParts' import type { ProjectShape } from '../data' import type { FindSpecs } from '../actions' -import { FileExtension, getDefaultSpecFileName } from './migration/utils' +import { FileExtension, getDefaultSpecFileName } from '../util/files' type SpecPatterns = { specPattern?: string[] diff --git a/packages/data-context/src/sources/index.ts b/packages/data-context/src/sources/index.ts index 7a9585f47350..c7ab850f1bd1 100644 --- a/packages/data-context/src/sources/index.ts +++ b/packages/data-context/src/sources/index.ts @@ -10,7 +10,6 @@ export * from './FileDataSource' export * from './GitDataSource' export * from './GraphQLDataSource' export * from './HtmlDataSource' -export * from './MigrationDataSource' export * from './ProjectDataSource' export * from './RelevantRunSpecsDataSource' export * from './RelevantRunsDataSource' @@ -18,4 +17,3 @@ export * from './RemoteRequestDataSource' export * from './UtilDataSource' export * from './VersionsDataSource' export * from './WizardDataSource' -export * from './migration/' diff --git a/packages/data-context/src/sources/migration/autoRename.ts b/packages/data-context/src/sources/migration/autoRename.ts deleted file mode 100644 index 9d2df337277a..000000000000 --- a/packages/data-context/src/sources/migration/autoRename.ts +++ /dev/null @@ -1,174 +0,0 @@ -import globby from 'globby' -import type { TestingType } from '@packages/types' -import { - defaultTestFilesGlob, - FilePart, - formatMigrationFile, - getComponentFolder, - getComponentTestFilesGlobs, - getIntegrationFolder, - getIntegrationTestFilesGlobs, - isDefaultTestFiles, - legacyIntegrationFolder, - regexps, -} from '.' -import type { MigrationFile } from '../MigrationDataSource' -import type { LegacyCypressConfigJson } from '..' - -export interface MigrationSpec { - relative: string - usesDefaultFolder: boolean - usesDefaultTestFiles: boolean - testingType: TestingType -} - -interface GetSpecs { - component: MigrationSpec[] - integration: MigrationSpec[] -} - -export type MigrationTransformOptions = { - shouldMigratePreExtension: boolean -} - -export const defaultMigrationTransformOptions = { - shouldMigratePreExtension: true, -} - -export function substitute (part: FilePart, options: MigrationTransformOptions = defaultMigrationTransformOptions): FilePart { - // nothing to substitute, just a regular - // part of the file - if (!('group' in part)) { - return part - } - - // cypress/integration -> cypress/e2e - if (part.group === 'folder' && part.text === 'integration') { - return { ...part, text: 'e2e' } - } - - // basic.spec.js -> basic.cy.js - if (part.group === 'preExtension' && options.shouldMigratePreExtension) { - return { ...part, text: '.cy.' } - } - - // support/index.js -> support/e2e.js - if (part.group === 'supportFileName' && part.text === 'index') { - return { ...part, text: 'e2e' } - } - - return part -} - -export function applyMigrationTransform ( - spec: MigrationSpec, - options: MigrationTransformOptions = defaultMigrationTransformOptions, -): MigrationFile { - let regexp: RegExp - - if (spec.testingType === 'e2e' && spec.usesDefaultFolder && spec.usesDefaultTestFiles) { - // e2e, cypress/integration, **/* (default testFiles) - regexp = new RegExp(regexps.e2e.before.defaultFolderDefaultTestFiles) - } else if (spec.testingType === 'e2e' && !spec.usesDefaultFolder && spec.usesDefaultTestFiles) { - // e2e, custom-folder, **/* (default testFiles) - regexp = new RegExp(regexps.e2e.before.customFolderDefaultTestFiles) - } else if (spec.testingType === 'e2e' && spec.usesDefaultFolder && !spec.usesDefaultTestFiles) { - // e2e, cypress/integration , **/*.spec.ts (custom testFiles) - regexp = new RegExp(regexps.e2e.before.defaultFolderCustomTestFiles) - } else if (spec.testingType === 'component' && spec.usesDefaultFolder && spec.usesDefaultTestFiles) { - // component, cypress/component , (default testFiles) - regexp = new RegExp(regexps.component.before.defaultFolderDefaultTestFiles) - } else if (spec.testingType === 'component' && !spec.usesDefaultFolder && spec.usesDefaultTestFiles) { - // component, cypress/custom-component , (default testFiles) - regexp = new RegExp(regexps.component.before.customFolderDefaultTestFiles) - } else { - // custom folder AND test files pattern - // should be impossible, we should not call this function in the first place. - throw Error(`Cannot use applyMigrationTransform on a project with a custom folder and custom testFiles.`) - } - - const partsBeforeMigration = formatMigrationFile(spec.relative, regexp, options) - const partsAfterMigration = partsBeforeMigration.map((part) => { - // avoid re-renaming files with the right preExtension - // it would make a myFile.cy.cy.js file - if (part.highlight - && part.group === 'preExtension' - && /\.cy\.([j|t]s[x]?|coffee)$/.test(spec.relative)) { - return part - } - - return substitute(part, options) - }) - - return { - testingType: spec.testingType, - before: { - relative: spec.relative, - parts: partsBeforeMigration, - }, - after: { - relative: partsAfterMigration.map((x) => x.text).join(''), - parts: partsAfterMigration, - }, - } -} - -export async function getSpecs (projectRoot: string, config: LegacyCypressConfigJson): Promise { - const integrationFolder = getIntegrationFolder(config) - const integrationTestFiles = getIntegrationTestFilesGlobs(config) - - const componentFolder = getComponentFolder(config) - const componentTestFiles = getComponentTestFilesGlobs(config) - - let integrationSpecs: MigrationSpec[] = [] - let componentSpecs: MigrationSpec[] = [] - - const globs = integrationFolder - ? integrationFolder === legacyIntegrationFolder - ? [defaultTestFilesGlob].map((glob) => `${integrationFolder}/${glob}`) - : integrationTestFiles.map((glob) => `${integrationFolder}/${glob}`) - : [] - - let specs = integrationFolder - ? (await globby(globs, { onlyFiles: true, cwd: projectRoot })) - : [] - - const fullyCustom = integrationFolder !== legacyIntegrationFolder && !isDefaultTestFiles(config, 'integration') - - // we cannot do a migration if either integrationFolder is false, - // or if both the integrationFolder and testFiles are custom. - if (fullyCustom) { - integrationSpecs = [] - } else { - integrationSpecs = specs.map((relative) => { - return { - relative, - usesDefaultFolder: integrationFolder === legacyIntegrationFolder, - usesDefaultTestFiles: isDefaultTestFiles(config, 'integration'), - testingType: 'e2e', - } - }) - } - - if (componentFolder === false || !isDefaultTestFiles(config, 'component')) { - componentSpecs = [] - } else { - const globs = componentTestFiles.map((glob) => { - return `${componentFolder}/${glob}` - }) - - componentSpecs = (await globby(globs, { onlyFiles: true, cwd: projectRoot })).map((relative) => { - return { - relative, - usesDefaultFolder: componentFolder === 'cypress/component', - usesDefaultTestFiles: isDefaultTestFiles(config, 'component'), - testingType: 'component', - } - }) - } - - return { - component: componentSpecs, - integration: integrationSpecs, - } -} diff --git a/packages/data-context/src/sources/migration/codegen.ts b/packages/data-context/src/sources/migration/codegen.ts deleted file mode 100644 index b0eb3ddb3a2d..000000000000 --- a/packages/data-context/src/sources/migration/codegen.ts +++ /dev/null @@ -1,623 +0,0 @@ -import chokidar from 'chokidar' -import fs from 'fs-extra' -import path from 'path' -import globby from 'globby' -import type { TestingType } from '@packages/types' -import { formatMigrationFile } from './format' -import { substitute } from './autoRename' -import { supportFileRegexps } from './regexps' -import type { MigrationFile } from '../MigrationDataSource' -import { toPosix } from '../../util' -import Debug from 'debug' -import dedent from 'dedent' -import { hasDefaultExport } from './parserUtils' -import { isDefaultSupportFile, LegacyCypressConfigJson, legacyIntegrationFolder } from '..' -import { parse } from '@babel/parser' -import generate from '@babel/generator' -import _ from 'lodash' -import { defineConfigAvailable, getBreakingKeys } from '@packages/config' - -const debug = Debug('cypress:data-context:sources:migration:codegen') - -type ConfigOptions = { - global: Record - e2e: Record - component: Record -} - -type ResolvedConfigOptions = Cypress.ResolvedConfigOptions & { - testFiles: string | string[] - ignoreTestFiles: string | string[] -} - -export class NonStandardMigrationError extends Error { - constructor (fileType: 'support' | 'config') { - super() - this.message = `Failed to find default ${fileType}. Bailing automation migration.` - } -} - -export interface CreateConfigOptions { - hasE2ESpec: boolean - hasPluginsFile: boolean - hasComponentTesting: boolean - projectRoot: string - isUsingTypeScript: boolean - isProjectUsingESModules: boolean - shouldAddCustomE2ESpecPattern: boolean -} - -export async function createConfigString (cfg: LegacyCypressConfigJson, options: CreateConfigOptions) { - const newConfig = reduceConfig(cfg, options) - const relativePluginPath = await getPluginRelativePath(cfg, options.projectRoot) - - debug('creating cypress.config from newConfig %o relativePluginPath %s options %o', newConfig, relativePluginPath, options) - - return createCypressConfig(newConfig, relativePluginPath, options) -} - -interface FileToBeMigratedManually { - relative: string - moved: boolean -} - -export interface ComponentTestingMigrationStatus { - files: Map - completed: boolean -} - -export async function initComponentTestingMigration ( - projectRoot: string, - componentFolder: string, - testFiles: string[], - onFileMoved: (status: ComponentTestingMigrationStatus) => void, -): Promise<{ - status: ComponentTestingMigrationStatus - watcher: chokidar.FSWatcher | null -}> { - debug('initComponentTestingMigration %O', { projectRoot, componentFolder, testFiles }) - const watchPaths = testFiles.map((glob) => { - return `${componentFolder}/${glob}` - }) - - const watcher = chokidar.watch( - watchPaths, { - cwd: projectRoot, - ignorePermissionErrors: true, - }, - ) - - debug('watchPaths %o', watchPaths) - - let filesToBeMoved: Map = (await globby(watchPaths, { - cwd: projectRoot, - })).reduce>((acc, relative) => { - acc.set(relative, { relative, moved: false }) - - return acc - }, new Map()) - - debug('files to be moved manually %o', filesToBeMoved) - if (filesToBeMoved.size === 0) { - // this should not happen as the step should be hidden in this case - // but files can have been moved manually before clicking next - return { - status: { - files: filesToBeMoved, - completed: true, - }, - watcher: null, - } - } - - watcher.on('unlink', (unlinkedPath) => { - const posixUnlinkedPath = toPosix(unlinkedPath) - const file = filesToBeMoved.get(posixUnlinkedPath) - - if (!file) { - throw Error(`Watcher incorrectly triggered ${posixUnlinkedPath} - while watching ${Array.from(filesToBeMoved.keys()).join(', ')} - projectRoot: ${projectRoot}`) - } - - file.moved = true - - const completed = Array.from(filesToBeMoved.values()).every((value) => value.moved === true) - - onFileMoved({ - files: filesToBeMoved, - completed, - }) - }) - - return new Promise((resolve, reject) => { - watcher.on('ready', () => { - debug('watcher ready') - resolve({ - status: { - files: filesToBeMoved, - completed: false, - }, - watcher, - }) - }).on('error', (err) => { - reject(err) - }) - }) -} - -async function getPluginRelativePath (cfg: LegacyCypressConfigJson, projectRoot: string): Promise { - return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot) -} - -async function createCypressConfig (config: ConfigOptions, pluginPath: string | undefined, options: CreateConfigOptions): Promise { - const globalString = Object.keys(config.global).length > 0 ? `${formatObjectForConfig(config.global)},` : '' - const componentString = options.hasComponentTesting ? createComponentTemplate(config.component) : '' - const e2eString = options.hasE2ESpec - ? await createE2ETemplate(pluginPath, options, config.e2e) - : '' - - if (defineConfigAvailable(options.projectRoot)) { - if (options.isUsingTypeScript || options.isProjectUsingESModules) { - return formatConfig(dedent` - import { defineConfig } from 'cypress' - - export default defineConfig({ - ${globalString} - ${e2eString} - ${componentString} - })`) - } - - return formatConfig(dedent` - const { defineConfig } = require('cypress') - - module.exports = defineConfig({ - ${globalString} - ${e2eString} - ${componentString} - })`) - } - - if (options.isUsingTypeScript || options.isProjectUsingESModules) { - return formatConfig(`export default {${globalString}${e2eString}${componentString}}`) - } - - return formatConfig(`module.exports = {${globalString}${e2eString}${componentString}}`) -} - -function formatObjectForConfig (obj: Record) { - return JSON.stringify(obj, null, 2).replace(/^[{]|[}]$/g, '') // remove opening and closing {} -} - -// Returns path of `pluginsFile` relative to projectRoot -// Considers cases of: -// 1. `pluginsFile` pointing to a directory containing an index file -// 2. `pluginsFile` pointing to a file -// -// Example: -// - projectRoot -// --- cypress -// ----- plugins -// -------- index.js -// Both { "pluginsFile": "cypress/plugins"} and { "pluginsFile": "cypress/plugins/index.js" } are valid. -// -// Will return `cypress/plugins/index.js` for both cases. -export async function getLegacyPluginsCustomFilePath (projectRoot: string, pluginPath: string): Promise { - debug('looking for pluginPath %s in projectRoot %s', pluginPath, projectRoot) - - const pluginLoc = path.join(projectRoot, pluginPath) - - debug('fs.stats on %s', pluginLoc) - - let stats: fs.Stats - - try { - stats = await fs.stat(pluginLoc) - } catch (e) { - throw Error(`Looked for pluginsFile at ${pluginPath}, but it was not found.`) - } - - if (stats.isFile()) { - debug('found pluginsFile %s', pluginLoc) - - return pluginPath - } - - if (stats.isDirectory()) { - // Although you are supposed to pass a file to `pluginsFile`, we also supported - // passing a directory containing an `index` file. - // If pluginsFile is a directory, see if there is an index.{js,ts} and grab that. - // { - // "pluginsFile": "plugins" - // } - // Where cypress/plugins contains an `index.{js,ts,coffee...}` but NOT `index.d.ts`. - const ls = await fs.readdir(pluginLoc) - const indexFile = ls.find((file) => file.startsWith('index.') && !file.endsWith('.d.ts')) - - debug('pluginsFile was a directory containing %o, looks like we want %s', ls, indexFile) - - if (indexFile) { - const pathToIndex = path.join(pluginPath, indexFile) - - debug('found pluginsFile %s', pathToIndex) - - return pathToIndex - } - } - - debug('error, could not find path to pluginsFile!') - - throw Error(`Could not find pluginsFile. Received projectRoot ${projectRoot} and pluginPath: ${pluginPath}`) -} - -async function createE2ETemplate (pluginPath: string | undefined, createConfigOptions: CreateConfigOptions, options: Record) { - if (createConfigOptions.shouldAddCustomE2ESpecPattern && !options.specPattern) { - options.specPattern = 'cypress/e2e/**/*.{js,jsx,ts,tsx}' - } - - if (!createConfigOptions.hasPluginsFile || !pluginPath) { - return dedent` - e2e: { - setupNodeEvents(on, config) {},${formatObjectForConfig(options)} - }, - ` - } - - let relPluginsPath: string - - const startsWithDotSlash = new RegExp(/^.\//) - - if (startsWithDotSlash.test(pluginPath)) { - relPluginsPath = `'${pluginPath}'` - } else { - relPluginsPath = `'./${pluginPath}'` - } - - const legacyPluginFileLoc = await getLegacyPluginsCustomFilePath(createConfigOptions.projectRoot, pluginPath) - const pluginFile = await fs.readFile(path.join(createConfigOptions.projectRoot, legacyPluginFileLoc), 'utf8') - - const requirePlugins = hasDefaultExport(pluginFile) - ? `return require(${relPluginsPath}).default(on, config)` - : `return require(${relPluginsPath})(on, config)` - - const setupNodeEvents = dedent` - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. - setupNodeEvents(on, config) { - ${requirePlugins} - }` - - return dedent` - e2e: { - ${setupNodeEvents},${formatObjectForConfig(options)} - },` -} - -function createComponentTemplate (options: Record) { - return `component: { - setupNodeEvents(on, config) {},${formatObjectForConfig(options)} - },` -} - -export interface RelativeSpec { - relative: string -} - -/** - * Checks that at least one spec file exist for testing type - * - * NOTE: this is what we use to see if CT/E2E is set up - */ -export async function hasSpecFile (projectRoot: string, folder: string | false, glob: string | string[]): Promise { - if (!folder) { - return false - } - - return (await globby(glob, { - cwd: path.join(projectRoot, folder), - onlyFiles: true, - })).length > 0 -} - -export async function tryGetDefaultLegacyPluginsFile (projectRoot: string) { - const files = await globby('cypress/plugins/index.*', { cwd: projectRoot, ignore: ['cypress/plugins/index.d.ts'] }) - - return files[0] -} - -export async function tryGetDefaultLegacySupportFile (projectRoot: string) { - const files = await globby('cypress/support/index.*', { cwd: projectRoot, ignore: ['cypress/support/index.d.ts'] }) - - debug('tryGetDefaultLegacySupportFile: files %O', files) - - return files[0] -} - -export async function getDefaultLegacySupportFile (projectRoot: string) { - const defaultSupportFile = await tryGetDefaultLegacySupportFile(projectRoot) - - if (!defaultSupportFile) { - throw new NonStandardMigrationError('support') - } - - return defaultSupportFile -} - -export async function supportFilesForMigration (projectRoot: string): Promise { - debug('Checking for support files in %s', projectRoot) - const defaultOldSupportFile = await getDefaultLegacySupportFile(projectRoot) - const defaultNewSupportFile = renameSupportFilePath(defaultOldSupportFile) - - const afterParts = formatMigrationFile( - defaultOldSupportFile, - new RegExp(supportFileRegexps.e2e.beforeRegexp), - ).map((part) => substitute(part)) - - return { - testingType: 'e2e', - before: { - relative: defaultOldSupportFile, - parts: formatMigrationFile(defaultOldSupportFile, new RegExp(supportFileRegexps.e2e.beforeRegexp)), - }, - after: { - relative: defaultNewSupportFile, - parts: afterParts, - }, - } -} - -export interface SpecToMove { - from: string - to: string -} - -export async function moveSpecFiles (projectRoot: string, specs: SpecToMove[]) { - await Promise.all(specs.map((spec) => { - const from = path.join(projectRoot, spec.from) - const to = path.join(projectRoot, spec.to) - - if (from === to) { - return - } - - return fs.move(from, to) - })) -} - -export async function cleanUpIntegrationFolder (projectRoot: string) { - const integrationPath = path.join(projectRoot, 'cypress', 'integration') - const e2ePath = path.join(projectRoot, 'cypress', 'e2e') - - try { - await fs.copy(integrationPath, e2ePath, { recursive: true }) - await fs.remove(integrationPath) - } catch (e: any) { - // only throw if the folder exists - if (e.code !== 'ENOENT') { - throw e - } - } -} - -export function renameSupportFilePath (relative: string) { - const res = new RegExp(supportFileRegexps.e2e.beforeRegexp).exec(relative) - - if (!res?.groups?.supportFileName) { - throw new NonStandardMigrationError('support') - } - - return relative.slice(0, res.index) + relative.slice(res.index).replace(res.groups.supportFileName, 'e2e') -} - -export function reduceConfig (cfg: LegacyCypressConfigJson, options: CreateConfigOptions): ConfigOptions { - return Object.entries(cfg).reduce((acc, [key, val]) => { - switch (key) { - case 'pluginsFile': - case '$schema': - return acc - - case 'e2e': - case 'component': { - const value = val as ResolvedConfigOptions - - if (!value) { - return acc - } - - const { testFiles, ignoreTestFiles, ...rest } = value - - // don't include if it's the default! No need. - const specPattern = getSpecPattern(cfg, key, options.shouldAddCustomE2ESpecPattern) - const ext = '**/*.cy.{js,jsx,ts,tsx}' - const isDefaultE2E = key === 'e2e' && specPattern === `cypress/e2e/${ext}` - const isDefaultCT = key === 'component' && specPattern === ext - - const breakingKeys = getBreakingKeys() - const restWithoutBreakingKeys = _.omit(rest, breakingKeys) - const existingWithoutBreakingKeys = _.omit(acc[key], breakingKeys) - - if (isDefaultE2E || isDefaultCT) { - return { - ...acc, [key]: { - ...restWithoutBreakingKeys, - ...existingWithoutBreakingKeys, - }, - } - } - - return { - ...acc, [key]: { - ...restWithoutBreakingKeys, - ...existingWithoutBreakingKeys, - specPattern, - }, - } - } - case 'integrationFolder': - // If the integration folder is set, but the value is the same as the default legacy one - // we do not want to update the config value, we keep using the new default. - if (val === legacyIntegrationFolder) { - return acc - } - - return { - ...acc, - e2e: { ...acc.e2e, specPattern: getSpecPattern(cfg, 'e2e', options.shouldAddCustomE2ESpecPattern) }, - } - case 'componentFolder': - return { - ...acc, - component: { ...acc.component, specPattern: getSpecPattern(cfg, 'component') }, - } - case 'testFiles': - return { - ...acc, - e2e: { ...acc.e2e, specPattern: getSpecPattern(cfg, 'e2e', options.shouldAddCustomE2ESpecPattern) }, - component: { ...acc.component, specPattern: getSpecPattern(cfg, 'component') }, - } - case 'ignoreTestFiles': - return { - ...acc, - e2e: { ...acc.e2e, excludeSpecPattern: val }, - component: { ...acc.component, excludeSpecPattern: val }, - } - case 'supportFile': - // If the supportFile is set, but is the same value as the default one; where - // we migrate it, we do not want to put the legacy value in the migrated config. - // It can be .ts or .js - if (isDefaultSupportFile(val)) { - return acc - } - - return { - ...acc, - e2e: { ...acc.e2e, supportFile: val }, - } - case 'baseUrl': - return { - ...acc, - e2e: { ...acc.e2e, [key]: val }, - } - case 'slowTestThreshold': - return { - ...acc, - component: { ...acc.component, [key]: val }, - e2e: { ...acc.e2e, [key]: val }, - } - default: - return { ...acc, global: { ...acc.global, [key]: val } } - } - }, { global: {}, e2e: {}, component: {} }) -} - -function propOrArrayProp (val: T[]): T | T[] { - if (val[0] && val.length === 1) { - return val[0] - } - - return val -} - -export function getSpecPattern (cfg: LegacyCypressConfigJson, testingType: TestingType, shouldAddCustomE2ESpecPattern: boolean = false) { - let _specPattern = cfg[testingType]?.testFiles ?? cfg.testFiles ?? (testingType === 'e2e' && shouldAddCustomE2ESpecPattern ? '**/*.{js,jsx,ts,tsx}' : '**/*.cy.{js,jsx,ts,tsx}') - const specPattern = _.castArray(_specPattern) - - const customComponentFolder = cfg.component?.componentFolder ?? cfg.componentFolder ?? null - - if (testingType === 'component' && customComponentFolder) { - return propOrArrayProp(specPattern.map((pattern) => `${customComponentFolder}/${pattern}`)) - } - - if (testingType === 'e2e') { - const customIntegrationFolder = cfg.e2e?.integrationFolder ?? cfg.integrationFolder ?? null - - if (customIntegrationFolder && customIntegrationFolder !== legacyIntegrationFolder) { - return propOrArrayProp(specPattern.map((pattern) => `${customIntegrationFolder}/${pattern}`)) - } - - return propOrArrayProp(specPattern.map((pattern) => `cypress/e2e/${pattern}`)) - } - - return propOrArrayProp(specPattern) -} - -function formatWithBundledBabel (config: string) { - const ast = parse(config) - - // @ts-ignore - transitive babel types have a minor conflict - readonly vs non readonly. - let { code } = generate(ast, {}, config) - // By default babel generates imports like this: - // const { - // defineConfig - // } = require('cypress'); - // So we replace them with a one-liner, since we know this will never - // be more than one import. - // - // Babel also adds empty lines, for example: - // - // export default defineConfig({ - // component: { - // }, - // <===== empty line - // e2e: { - // - // } - // }) - // Which we don't want, so we change those to single carriage returns. - const replacers = [ - { - from: dedent` - const { - defineConfig - } = require('cypress'); - `, - to: dedent` - const { defineConfig } = require('cypress'); - `, - }, - { - - from: dedent` - import { - defineConfig - } from 'cypress'; - `, - to: dedent` - import { defineConfig } from 'cypress'; - `, - }, - { - from: `,\n\n`, - to: `,\n`, - }, - ] - - for (const rep of replacers) { - if (code.includes(rep.from)) { - code = code.replaceAll(rep.from, rep.to) - } - } - - return code -} - -export function formatConfig (config: string): string { - try { - const prettier = require('prettier') as typeof import('prettier') - - return prettier.format(config, { - semi: false, - singleQuote: true, - endOfLine: 'lf', - parser: 'babel', - }) - } catch (e) { - // If they do not have prettier - // We do a basic format using babel, which we - // bundle as part of the binary. - // We don't ship a fully fledged formatter like - // prettier, since it's massively bloats the bundle. - return formatWithBundledBabel(config) - } -} diff --git a/packages/data-context/src/sources/migration/format.ts b/packages/data-context/src/sources/migration/format.ts deleted file mode 100644 index ec819405889f..000000000000 --- a/packages/data-context/src/sources/migration/format.ts +++ /dev/null @@ -1,62 +0,0 @@ -import dedent from 'dedent' -import { MigrationTransformOptions, defaultMigrationTransformOptions } from './autoRename' - -export type FilePart = { - text: string - group?: 'folder' | 'preExtension' | 'supportFileName' | 'fileName' - highlight: boolean -} - -export function formatMigrationFile (file: string, regexp: RegExp, options: MigrationTransformOptions = defaultMigrationTransformOptions): FilePart[] { - const match = regexp.exec(file) - - if (!match?.groups) { - throw new Error(dedent` - Expected groups main,ext or file in ${file} using ${regexp} when matching ${file} - Perhaps this isn't a spec file, or it is an unexpected format?`) - } - - const { folder, fileName, preExtension, extension, supportFileName } = match.groups - - if (supportFileName && extension) { - return [{ - text: `${file.slice(0, match.index)}cypress/support/`, // user/cypress/support/index.js -> user/cypress/support/ - highlight: false, - }, { - text: supportFileName, // user/cypress/support/index.js -> index - highlight: true, - group: 'supportFileName', - }, - { - text: extension, // user/cypress/support/index.js -> .js - highlight: false, - }] - } - - return [{ - text: file.slice(0, match.index), // user/cypress/integration/file.spec.tsx -> user/ - highlight: false, - }, - { - text: folder ? 'cypress/' : '', // empty when using a custom integration folder or in component - highlight: false, - }, - { - text: folder || '', // user/cypress/integration/file.spec.tsx -> integration - highlight: true, - group: 'folder', - }, - { - text: (folder ? '/' : '') + fileName, // user/cypress/integration/file.spec.tsx -> /file - highlight: false, - group: 'fileName', - }, - { - text: preExtension || '', // user/cypress/integration/file.spec.tsx -> .spec. - highlight: options.shouldMigratePreExtension, - group: 'preExtension', - }, { - text: extension || '', // user/cypress/integration/file.spec.tsx -> tsx - highlight: false, - }].filter((f) => f.text.length) as FilePart[] -} diff --git a/packages/data-context/src/sources/migration/index.ts b/packages/data-context/src/sources/migration/index.ts deleted file mode 100644 index a3b7b675ba3d..000000000000 --- a/packages/data-context/src/sources/migration/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable padding-line-between-statements */ -// created by autobarrel, do not modify directly - -export * from './autoRename' -export * from './codegen' -export * from './format' -export * from './legacyOptions' -export * from './parserUtils' -export * from './regexps' -export * from './shouldShowSteps' -export * from './utils' diff --git a/packages/data-context/src/sources/migration/legacyOptions.ts b/packages/data-context/src/sources/migration/legacyOptions.ts deleted file mode 100644 index c0cd4ba8da2a..000000000000 --- a/packages/data-context/src/sources/migration/legacyOptions.ts +++ /dev/null @@ -1,296 +0,0 @@ -interface ResolvedConfigOption { - name: string - defaultValue?: any - isFolder?: boolean - isExperimental?: boolean - /** - * Can be mutated with Cypress.config() or test-specific configuration overrides - */ - canUpdateDuringTestTime?: boolean -} - -interface RuntimeConfigOption { - name: string - defaultValue: any - isInternal?: boolean - /** - * Can be mutated with Cypress.config() or test-specific configuration overrides - */ - canUpdateDuringTestTime?: boolean -} - -export const legacyIntegrationFolder = 'cypress/integration' -// 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) -// - cypress.schema.json -// -// Add options in alphabetical order for better readability - -// TODO - add boolean attribute to indicate read-only / static vs mutable options -// that can be updated during test executions -const resolvedOptions: Array = [ - { - name: 'animationDistanceThreshold', - defaultValue: 5, - canUpdateDuringTestTime: true, - }, { - name: 'baseUrl', - defaultValue: null, - canUpdateDuringTestTime: true, - }, { - name: 'blockHosts', - defaultValue: null, - canUpdateDuringTestTime: true, - }, { - name: 'chromeWebSecurity', - defaultValue: true, - canUpdateDuringTestTime: false, - }, { - name: 'clientCertificates', - defaultValue: [], - canUpdateDuringTestTime: false, - }, { - name: 'component', - // runner-ct overrides - defaultValue: {}, - canUpdateDuringTestTime: false, - }, { - name: 'componentFolder', - defaultValue: 'cypress/component', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'defaultCommandTimeout', - defaultValue: 4000, - canUpdateDuringTestTime: true, - }, { - name: 'downloadsFolder', - defaultValue: 'cypress/downloads', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'e2e', - // e2e runner overrides - defaultValue: {}, - canUpdateDuringTestTime: false, - }, { - name: 'env', - defaultValue: {}, - canUpdateDuringTestTime: true, - }, { - name: 'execTimeout', - defaultValue: 60000, - canUpdateDuringTestTime: true, - }, { - name: 'exit', - defaultValue: true, - canUpdateDuringTestTime: false, - }, { - name: 'experimentalInteractiveRunEvents', - defaultValue: false, - isExperimental: true, - canUpdateDuringTestTime: false, - }, { - name: 'experimentalSourceRewriting', - defaultValue: false, - isExperimental: true, - canUpdateDuringTestTime: false, - }, { - name: 'experimentalStudio', - defaultValue: false, - isExperimental: true, - canUpdateDuringTestTime: false, - }, { - name: 'fileServerFolder', - defaultValue: '', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'fixturesFolder', - defaultValue: 'cypress/fixtures', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'ignoreTestFiles', - defaultValue: '*.hot-update.js', - canUpdateDuringTestTime: true, - }, { - name: 'includeShadowDom', - defaultValue: false, - canUpdateDuringTestTime: true, - }, { - name: 'integrationFolder', - defaultValue: legacyIntegrationFolder, - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'keystrokeDelay', - defaultValue: 0, - canUpdateDuringTestTime: true, - }, { - name: 'modifyObstructiveCode', - defaultValue: true, - canUpdateDuringTestTime: false, - }, { - name: 'nodeVersion', - canUpdateDuringTestTime: false, - }, { - name: 'numTestsKeptInMemory', - defaultValue: 50, - canUpdateDuringTestTime: true, - }, { - name: 'pageLoadTimeout', - defaultValue: 60000, - canUpdateDuringTestTime: true, - }, { - name: 'pluginsFile', - defaultValue: 'cypress/plugins', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'port', - defaultValue: null, - canUpdateDuringTestTime: true, - }, { - name: 'projectId', - defaultValue: null, - canUpdateDuringTestTime: true, - }, { - name: 'redirectionLimit', - defaultValue: 20, - canUpdateDuringTestTime: true, - }, { - name: 'reporter', - defaultValue: 'spec', - canUpdateDuringTestTime: true, - }, { - name: 'reporterOptions', - defaultValue: null, - canUpdateDuringTestTime: true, - }, { - name: 'requestTimeout', - defaultValue: 5000, - canUpdateDuringTestTime: true, - }, { - name: 'resolvedNodePath', - defaultValue: null, - canUpdateDuringTestTime: false, - }, { - name: 'resolvedNodeVersion', - defaultValue: null, - canUpdateDuringTestTime: false, - }, { - name: 'responseTimeout', - defaultValue: 30000, - canUpdateDuringTestTime: true, - }, { - name: 'retries', - defaultValue: { - runMode: 0, - openMode: 0, - }, - canUpdateDuringTestTime: true, - }, { - name: 'screenshotOnRunFailure', - defaultValue: true, - canUpdateDuringTestTime: true, - }, { - name: 'screenshotsFolder', - defaultValue: 'cypress/screenshots', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'slowTestThreshold', - defaultValue: (options: Record = {}) => options.testingType === 'component' ? 250 : 10000, - canUpdateDuringTestTime: true, - }, { - name: 'scrollBehavior', - defaultValue: 'top', - canUpdateDuringTestTime: true, - }, { - name: 'supportFile', - defaultValue: 'cypress/support', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'supportFolder', - defaultValue: false, - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'taskTimeout', - defaultValue: 60000, - canUpdateDuringTestTime: true, - }, { - name: 'testFiles', - defaultValue: '**/*.*', - canUpdateDuringTestTime: false, - }, { - name: 'trashAssetsBeforeRuns', - defaultValue: true, - canUpdateDuringTestTime: false, - }, { - name: 'userAgent', - defaultValue: null, - canUpdateDuringTestTime: false, - }, { - name: 'video', - defaultValue: false, - canUpdateDuringTestTime: false, - }, { - name: 'videoCompression', - defaultValue: 32, - canUpdateDuringTestTime: false, - }, { - name: 'videosFolder', - defaultValue: 'cypress/videos', - isFolder: true, - canUpdateDuringTestTime: false, - }, { - name: 'videoUploadOnPasses', - defaultValue: true, - canUpdateDuringTestTime: false, - }, { - name: 'viewportHeight', - defaultValue: 660, - canUpdateDuringTestTime: true, - }, { - name: 'viewportWidth', - defaultValue: 1000, - canUpdateDuringTestTime: true, - }, { - name: 'waitForAnimations', - defaultValue: true, - canUpdateDuringTestTime: true, - }, { - name: 'watchForFileChanges', - defaultValue: true, - canUpdateDuringTestTime: false, - }, -] - -const runtimeOptions: Array = [ - { - name: 'browsers', - defaultValue: [], - canUpdateDuringTestTime: false, - }, { - name: 'hosts', - defaultValue: null, - canUpdateDuringTestTime: false, - }, { - name: 'isInteractive', - defaultValue: true, - canUpdateDuringTestTime: false, - }, { - name: 'modifyObstructiveCode', - defaultValue: true, - canUpdateDuringTestTime: false, - }, -] - -export const legacyOptions: Array = [ - ...resolvedOptions, - ...runtimeOptions, -] diff --git a/packages/data-context/src/sources/migration/parserUtils.ts b/packages/data-context/src/sources/migration/parserUtils.ts deleted file mode 100644 index 7cdc987f8159..000000000000 --- a/packages/data-context/src/sources/migration/parserUtils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { parse, ParserOptions } from '@babel/parser' -import { visit } from 'recast' -import type * as bt from '@babel/types' - -const babelParserOptions: ParserOptions = { - sourceType: 'module', - strictMode: false, - tokens: true, - plugins: [ - 'decorators-legacy', - 'doExpressions', - 'objectRestSpread', - 'classProperties', - 'classPrivateProperties', - 'classPrivateMethods', - 'exportDefaultFrom', - 'exportNamespaceFrom', - 'asyncGenerators', - 'functionBind', - 'functionSent', - 'dynamicImport', - 'numericSeparator', - 'optionalChaining', - 'importMeta', - 'bigInt', - 'optionalCatchBinding', - 'throwExpressions', - 'nullishCoalescingOperator', - 'typescript', - ], -} - -export function hasDefaultExport (src: string): boolean { - const ast = parse(src, babelParserOptions) as bt.File - - let hasDefault = false - - visit(ast, { - visitExportDefaultDeclaration () { - hasDefault = true - - return false - }, - }) - - return hasDefault -} diff --git a/packages/data-context/src/sources/migration/regexps.ts b/packages/data-context/src/sources/migration/regexps.ts deleted file mode 100644 index b160ab1fce84..000000000000 --- a/packages/data-context/src/sources/migration/regexps.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * This partial regular expression is used to extract - * the extension from a spec file name: - * - * matches - * - file.spec.tsx -> ext=".spec." - * - file_Spec.jsx -> ext="_Spec." - * - file-spec.js -> ext="-spec." - * - spec.jsx -> ext="." - * - * The final objective being to be able to replace "ext" with ".cy." - */ -const specExtRe = '(?:[._-]?(?:[s|S]pec|[T|t]est))?[.])(?(?:[j|t]s[x]?|coffee|cjsx)' - -export const regexps = { - e2e: { - before: { - defaultFolderDefaultTestFiles: `cypress\/(?integration)\/(?.+?)(?${specExtRe})$`, - defaultFolderCustomTestFiles: `cypress\/(?integration)\/(?.+)$`, - customFolderDefaultTestFiles: `(?.+?)(?${specExtRe})$`, - }, - }, - component: { - before: { - defaultFolderDefaultTestFiles: `(?cypress\/component\/.+?)(?${specExtRe})`, - customFolderDefaultTestFiles: `(?.+?)(?${specExtRe})`, - }, - }, -} as const - -export const supportFileRegexps = { - e2e: { - beforeRegexp: 'cypress[\\\\/]support[\\\\/](?index)(?\.(?:[j|t]sx?|coffee))', - afterRegexp: 'cypress[\\\\/]support[\\\\/](?e2e)(?\.(?:[j|t]sx?|coffee))', - }, -} as const diff --git a/packages/data-context/src/sources/migration/shouldShowSteps.ts b/packages/data-context/src/sources/migration/shouldShowSteps.ts deleted file mode 100644 index 0d292f32574b..000000000000 --- a/packages/data-context/src/sources/migration/shouldShowSteps.ts +++ /dev/null @@ -1,194 +0,0 @@ -import globby from 'globby' -import path from 'path' -import { MIGRATION_STEPS } from '@packages/types' -import { applyMigrationTransform, getSpecs, isDefaultSupportFile, legacyIntegrationFolder, tryGetDefaultLegacySupportFile } from '.' -import type { LegacyCypressConfigJson } from '..' - -export const defaultTestFilesGlob = '**/*.{js,ts,jsx,tsx,coffee,cjsx}' - -function getTestFilesGlobs (config: LegacyCypressConfigJson, type: 'component' | 'integration'): string[] { - // super awkward how we call it integration tests, but the key to override - // the config is `e2e` - const k = type === 'component' ? 'component' : 'e2e' - - const glob = config[k]?.testFiles ?? config.testFiles - - if (glob) { - return ([] as string[]).concat(glob) - } - - return [defaultTestFilesGlob] -} - -export function getIntegrationTestFilesGlobs (config: LegacyCypressConfigJson): string[] { - return getTestFilesGlobs(config, 'integration') -} - -export function getComponentTestFilesGlobs (config: LegacyCypressConfigJson): string[] { - return getTestFilesGlobs(config, 'component') -} - -export function isDefaultTestFiles (config: LegacyCypressConfigJson, type: 'component' | 'integration') { - const testFiles = type === 'component' - ? getComponentTestFilesGlobs(config) - : getIntegrationTestFilesGlobs(config) - - return testFiles.length === 1 && testFiles[0] === defaultTestFilesGlob -} - -export function getPluginsFile (config: LegacyCypressConfigJson) { - if (config.e2e?.pluginsFile === false || config.pluginsFile === false) { - return false - } - - return config.e2e?.pluginsFile ?? config.pluginsFile ?? 'cypress/plugins/index.js' -} - -export function getIntegrationFolder (config: LegacyCypressConfigJson) { - return config.e2e?.integrationFolder ?? config.integrationFolder ?? legacyIntegrationFolder -} - -export function getComponentFolder (config: LegacyCypressConfigJson): false | string { - if (config.component?.componentFolder === false || config.componentFolder === false) { - return false - } - - return config.component?.componentFolder ?? config.componentFolder ?? 'cypress/component' -} - -async function hasSpecFiles (projectRoot: string, dir: string, testFilesGlob: string[]): Promise { - const f = await globby(testFilesGlob, { cwd: path.join(projectRoot, dir) }) - - return f.length > 0 -} - -export async function shouldShowAutoRenameStep (projectRoot: string, config: LegacyCypressConfigJson) { - const specsToAutoMigrate = await getSpecs(projectRoot, config) - - const e2eMigrationOptions = { - // If the configFile has projectId, we do not want to change the preExtension - // so, we can keep the cloud history - shouldMigratePreExtension: !config.projectId && !config.e2e?.projectId, - } - - const integrationCleaned = specsToAutoMigrate.integration.filter((spec) => { - const transformed = applyMigrationTransform(spec, e2eMigrationOptions) - - return transformed.before.relative !== transformed.after.relative - }) - - const componentCleaned = specsToAutoMigrate.component.filter((spec) => { - const transformed = applyMigrationTransform(spec) - - return transformed.before.relative !== transformed.after.relative - }) - - // if we have at least one spec to auto migrate in either Ct or E2E, we return true. - return integrationCleaned.length > 0 || componentCleaned.length > 0 -} - -async function anyComponentSpecsExist (projectRoot: string, config: LegacyCypressConfigJson) { - const componentFolder = getComponentFolder(config) - - if (componentFolder === false) { - return false - } - - const componentTestFiles = getComponentTestFilesGlobs(config) - - return hasSpecFiles(projectRoot, componentFolder, componentTestFiles) -} - -async function anyIntegrationSpecsExist (projectRoot: string, config: LegacyCypressConfigJson) { - const integrationFolder = getIntegrationFolder(config) - - const integrationTestFiles = getIntegrationTestFilesGlobs(config) - - return hasSpecFiles(projectRoot, integrationFolder, integrationTestFiles) -} - -// we only show rename support file if they are using the default -// if they have anything set in their config, we will not try to rename it. -// Also, if there are no **no** integration specs, we are doing a CT only migration, -// in which case we don't migrate the supportFile - they'll make a new support/component.js -// when they set CT up. -export async function shouldShowRenameSupport (projectRoot: string, config: LegacyCypressConfigJson) { - if (!await anyIntegrationSpecsExist(projectRoot, config)) { - return false - } - - let supportFile = config.e2e?.supportFile ?? config.supportFile - - if (supportFile === undefined) { - const foundDefaultSupportFile = await tryGetDefaultLegacySupportFile(projectRoot) - - if (foundDefaultSupportFile) { - supportFile = foundDefaultSupportFile - } - } - - // if the support file is set to false, we don't show the rename step - // if the support file does not exist (value is undefined), we don't show the rename step - if (!supportFile) { - return false - } - - // if the support file is custom, we don't show the rename step - // only if the support file matches the default do we show the rename step - return isDefaultSupportFile(supportFile) -} - -// if they have component testing configured using the defaults, they will need to -// rename/move their specs. -async function shouldShowRenameManual (projectRoot: string, config: LegacyCypressConfigJson) { - const componentFolder = getComponentFolder(config) - - const usingAllDefaults = componentFolder === 'cypress/component' && isDefaultTestFiles(config, 'component') - - if (componentFolder === false || !usingAllDefaults) { - return false - } - - return anyComponentSpecsExist(projectRoot, config) -} - -// All projects must move from cypress.json to cypress.config.js! -export function shouldShowConfigFileStep (config: LegacyCypressConfigJson) { - return true -} - -export type Step = typeof MIGRATION_STEPS[number] - -export async function getStepsForMigration ( - projectRoot: string, - config: LegacyCypressConfigJson, - configFileExists: boolean, -): Promise { - const steps: Step[] = [] - - for (const step of MIGRATION_STEPS) { - if (step === 'renameAuto' && await shouldShowAutoRenameStep(projectRoot, config)) { - steps.push(step) - } - - if (step === 'renameManual' && await shouldShowRenameManual(projectRoot, config)) { - steps.push(step) - } - - if (step === 'renameSupport' && await shouldShowRenameSupport(projectRoot, config)) { - steps.push(step) - } - - if (step === 'configFile' && configFileExists) { - steps.push(step) - } - - // if we are showing rename manual, this implies - // component testing is configured. - if (step === 'setupComponent' && await anyComponentSpecsExist(projectRoot, config)) { - steps.push(step) - } - } - - return steps -} diff --git a/packages/data-context/src/sources/migration/utils.ts b/packages/data-context/src/util/files.ts similarity index 96% rename from packages/data-context/src/sources/migration/utils.ts rename to packages/data-context/src/util/files.ts index ff2e8e669312..5390e77009b6 100644 --- a/packages/data-context/src/sources/migration/utils.ts +++ b/packages/data-context/src/util/files.ts @@ -3,7 +3,7 @@ import type { TestingType, FoundSpec } from '@packages/types' import Debug from 'debug' import _ from 'lodash' import path from 'path' -import { getPathFromSpecPattern, getLongestCommonPrefixFromPaths } from '../ProjectDataSource' +import { getPathFromSpecPattern, getLongestCommonPrefixFromPaths } from '../sources/ProjectDataSource' export const isDefaultSupportFile = (supportFile: string) => { if (_.isNil(supportFile) || !_.isBoolean(supportFile) && supportFile.match(/(^|\.+\/)cypress\/support($|\/index($|\.(ts|js|coffee)$))/)) { @@ -19,7 +19,7 @@ export async function getDefaultSpecFileName ( { currentProject, testingType, fileExtensionToUse, specPattern, specs = [], name }: { currentProject: string | null, testingType: TestingType | null, fileExtensionToUse: FileExtension, specPattern: string[], specs?: FoundSpec[], name?: string }, ): Promise { - const debug = Debug('cypress:data-context:sources:migration:utils') + const debug = Debug('cypress:data-context:util:files') const defaultFilename = `${name ? name : testingType === 'e2e' ? 'spec' : 'ComponentName'}.cy.${fileExtensionToUse}` const defaultPathname = path.join('cypress', testingType ?? 'e2e', defaultFilename) diff --git a/packages/data-context/src/util/index.ts b/packages/data-context/src/util/index.ts index 564e2f7b9bce..fb634a8c81ff 100644 --- a/packages/data-context/src/util/index.ts +++ b/packages/data-context/src/util/index.ts @@ -5,6 +5,7 @@ export * from './DocumentNodeBuilder' export * from './autoBindDebug' export * from './config-file-updater' export * from './file' +export * from './files' export * from './hasTypescript' export * from './pluginHandlers' export * from './testCounts' diff --git a/packages/data-context/src/util/urqlCacheKeys.ts b/packages/data-context/src/util/urqlCacheKeys.ts index d4a7644adabf..a992c05a3a91 100644 --- a/packages/data-context/src/util/urqlCacheKeys.ts +++ b/packages/data-context/src/util/urqlCacheKeys.ts @@ -22,11 +22,8 @@ export const urqlCacheKeys: Partial = { keys: { DevState: (data) => data.__typename, Wizard: (data) => data.__typename, - Migration: (data) => data.__typename, CloudRunCommitInfo: () => null, GitInfo: () => null, - MigrationFile: () => null, - MigrationFilePart: () => null, CodeFrame: () => null, ProjectPreferences: (data) => data.__typename, VersionData: () => null, diff --git a/packages/data-context/test/unit/actions/MigrationActions.spec.ts b/packages/data-context/test/unit/actions/MigrationActions.spec.ts deleted file mode 100644 index c591a2ff398e..000000000000 --- a/packages/data-context/test/unit/actions/MigrationActions.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import chai, { expect } from 'chai' -import chaiAsPromised from 'chai-as-promised' -import { getConfigWithDefaults, getDiff } from '../../../src/actions/MigrationActions' -import fs from 'fs-extra' -import Fixtures from '@tooling/system-tests' -import { createTestDataContext, scaffoldMigrationProject } from '../helper' -import path from 'path' - -chai.use(chaiAsPromised) - -describe('MigrationActions', () => { - context('getConfigWithDefaults', () => { - it('returns a config with defaults without touching the original', () => { - const config = { - foo: 'bar', - } - - expect(getConfigWithDefaults(config)).to.have.property('env') - expect(getConfigWithDefaults(config)).to.have.property('browsers') - expect(config).not.to.have.property('env') - expect(config).not.to.have.property('browsers') - }) - }) - - context('getDiff', () => { - it('returns all the updated values', () => { - const oldConfig = { - foo: 'bar', - other: 'config', - removed: 'value', - updated: 'oldValue', - } - - const newConfig = { - foo: 'hello', - other: 'config', - updated: 'newValue', - } - - const diff = getDiff(oldConfig, newConfig) - - expect(diff).to.have.property('foo', 'hello') - expect(diff).to.have.property('updated', 'newValue') - expect(diff).not.to.have.property('removed') - }) - }) - - describe('#initialize', () => { - let currentProject: string - - beforeEach(async () => { - Fixtures.clearFixtureNodeModules('migration') - currentProject = await scaffoldMigrationProject('migration') - }) - - // simulate having a specific version of cypress installed - // in a project's local node_modules - function mockLocallyInstalledCypress (projectRoot: string, version: string) { - const mockPkgJson = { - version, - main: 'index.js', - } - const mockCypressDir = path.join(projectRoot, 'node_modules', 'cypress') - - fs.mkdirSync(mockCypressDir, { recursive: true }) - fs.createFileSync(path.join(mockCypressDir, 'index.js')) - fs.writeJsonSync(path.join(mockCypressDir, 'package.json'), mockPkgJson) - } - - it('errors when local cypress version is <10', async () => { - mockLocallyInstalledCypress(currentProject, '9.5.0') - const ctx = createTestDataContext() - - ctx.update((coreData) => { - coreData.currentProject = currentProject - coreData.currentTestingType = 'e2e' - coreData.app.isGlobalMode = true - }) - - const currentVersion = (await ctx.versions.versionData()).current.version - - return ( - expect(ctx.actions.migration.initialize({})).to.eventually.be.rejectedWith( - `You are running Cypress version ${currentVersion} in global mode, but you are attempting to migrate a project where Cypress version 9.5.0 is installed`, - ) - ) - }) - - it('does not error when local cypress version is 10', () => { - mockLocallyInstalledCypress(currentProject, '10.0.0') - const ctx = createTestDataContext() - - ctx.update((coreData) => { - coreData.currentProject = currentProject - coreData.currentTestingType = 'e2e' - }) - - return ( - expect(ctx.actions.migration.initialize({})).to.eventually.not.be.rejected - ) - }) - }) -}) diff --git a/packages/data-context/test/unit/data/ProjectConfigIpc.spec.ts b/packages/data-context/test/unit/data/ProjectConfigIpc.spec.ts index 80e45d2c03b4..a785968762c7 100644 --- a/packages/data-context/test/unit/data/ProjectConfigIpc.spec.ts +++ b/packages/data-context/test/unit/data/ProjectConfigIpc.spec.ts @@ -38,10 +38,9 @@ describe('ProjectConfigIpc', () => { context('forkChildProcess', () => { // some of these node versions may not exist, but we want to verify // the experimental flags are correctly disabled for future versions - const NODE_VERSIONS = ['18.20.4', '20.17.0', '20.19.0', '22.0.0', '22.7.0', '22.11.4', '22.12.0', '22.15.0'] - const experimentalDetectModuleIntroduced = '22.7.0' - const experimentalRequireModuleIntroduced = '22.12.0' - const minorPatchExperimentalModuleIntroduced = '>= 20.19.0 < 21.0.0' + const NODE_VERSIONS = ['20.5.1', '20.6.0', '20.19.1', '22.15.0'] + + const lastVersionWithDeprecatedLoaderOption = '20.5.1' let projectConfigIpc let forkSpy @@ -59,14 +58,16 @@ describe('ProjectConfigIpc', () => { context('typescript', () => { [...NODE_VERSIONS].forEach((nodeVersion) => { + const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node` + const MOCK_NODE_VERSION = nodeVersion + context(`node v${nodeVersion}`, () => { - context('ESM', () => { - it('passes the correct experimental flags if ESM is being used with typescript', async () => { - // @ts-expect-error - const projectPath = await scaffoldProject('config-cjs-and-esm/config-with-ts-module') + const PROJECTS = ['config-cjs-and-esm/config-with-ts-module', 'config-cjs-and-esm/config-with-module-resolution-bundler', 'config-cjs-and-esm/config-with-js-module', 'config-cjs-and-esm/config-with-cjs'] - const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node` - const MOCK_NODE_VERSION = nodeVersion + PROJECTS.forEach((project) => { + it(`${project}: tsx generic loader (esm/commonjs/typescript)`, async () => { + // @ts-expect-error ignoring due to nested directories in the system-test project directory not being included in the type. + const projectPath = await scaffoldProject(project) projectConfigIpc = new ProjectConfigIpc( MOCK_NODE_PATH, @@ -79,70 +80,40 @@ describe('ProjectConfigIpc', () => { () => {}, ) - expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ - env: { - NODE_OPTIONS: sinon.match('--experimental-specifier-resolution=node --loader'), - }, - })) - - if (semver.gte(nodeVersion, experimentalDetectModuleIntroduced) || semver.satisfies(nodeVersion, minorPatchExperimentalModuleIntroduced)) { + // make sure that we use tsx for every file, regardless of typescript, esm, or commonjs + if (semver.lte(nodeVersion, lastVersionWithDeprecatedLoaderOption)) { + // For node 20.5.1 and down, we need use the --loader flag expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ env: { - NODE_OPTIONS: sinon.match('--no-experimental-detect-module'), + NODE_OPTIONS: sinon.match(/--loader .*cypress\/node_modules\/tsx\/dist\/loader.mjs/), }, })) - } - - if (semver.gte(nodeVersion, experimentalRequireModuleIntroduced) || semver.satisfies(nodeVersion, minorPatchExperimentalModuleIntroduced)) { + } else { + // For node 20.6.0 and up, we need use the --import flag expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ env: { - NODE_OPTIONS: sinon.match('--no-experimental-require-module'), + NODE_OPTIONS: sinon.match(/--import .*cypress\/node_modules\/tsx\/dist\/loader.mjs/), }, })) } - if (semver.eq(nodeVersion, '22.0.0')) { - expect(forkSpy).to.not.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ + if (project.includes('config-with-ts-module') || project.includes('config-with-module-resolution-bundler')) { + // these projects have typescript installed and have a tsconfig, so the TSX_TSCONFIG_PATH should be set to the project path + expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ env: { - NODE_OPTIONS: sinon.match('--no-experimental-detect-module'), + TSX_TSCONFIG_PATH: sinon.match(`/cy-projects/${project}/tsconfig.json`), }, })) - - expect(forkSpy).to.not.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ + } else { + // non typescript projects that do NOT have a tsconfig, so the TSX_TSCONFIG_PATH should be undefined + expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ env: { - NODE_OPTIONS: sinon.match('--no-experimental-require-module'), + TSX_TSCONFIG_PATH: undefined, }, })) } }) }) - - context('CommonJS', () => { - it('uses the ts_node commonjs loader if CommonJS is being used with typescript', async () => { - // @ts-expect-error - const projectPath = await scaffoldProject('config-cjs-and-esm/config-with-module-resolution-bundler') - - const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node` - const MOCK_NODE_VERSION = nodeVersion - - projectConfigIpc = new ProjectConfigIpc( - MOCK_NODE_PATH, - MOCK_NODE_VERSION, - projectPath, - 'cypress.config.js', - false, - (error) => {}, - () => {}, - () => {}, - ) - - expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({ - env: { - NODE_OPTIONS: sinon.match('--require'), - }, - })) - }) - }) }) }) }) diff --git a/packages/data-context/test/unit/sources/migration/autoRename.spec.ts b/packages/data-context/test/unit/sources/migration/autoRename.spec.ts deleted file mode 100644 index 0569f98c85c0..000000000000 --- a/packages/data-context/test/unit/sources/migration/autoRename.spec.ts +++ /dev/null @@ -1,577 +0,0 @@ -import { - getSpecs, - applyMigrationTransform, - MigrationSpec, -} from '../../../../src/sources/migration/autoRename' -import { expect } from 'chai' -import path from 'path' -import fs from 'fs-extra' -import { MigrationFile } from '../../../../src/sources' -import { scaffoldMigrationProject } from '../../helper' - -describe('getSpecs', () => { - it('handles custom folders', async () => { - // CASE 1: E2E + CT, custom folders, default test files - // We want to rename specs, but keep current folders. - const cwd = await scaffoldMigrationProject('migration-e2e-component-default-test-files') - const json = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getSpecs(cwd, json) - - expect(actual.integration).to.eql([ - { - relative: 'cypress/custom-integration/foo.spec.ts', - usesDefaultFolder: false, - usesDefaultTestFiles: true, - testingType: 'e2e', - }, - ]) - - expect(actual.component).to.eql([ - { - relative: 'cypress/custom-component/button.spec.js', - usesDefaultFolder: false, - usesDefaultTestFiles: true, - testingType: 'component', - }, - ]) - }) - - it('handles default folder and custom testFiles', async () => { - // CASE 1: E2E + CT, custom folders, default test files - // We want to rename specs, but keep current folders. - const cwd = await scaffoldMigrationProject('migration') - const json = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getSpecs(cwd, json) - - expect(actual.integration).to.eql([ - { - 'relative': 'cypress/integration/app_spec.js', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - { - relative: 'cypress/integration/bar.spec.js', - usesDefaultFolder: true, - usesDefaultTestFiles: false, - testingType: 'e2e', - }, - { - 'relative': 'cypress/integration/blog-post-spec.ts', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - { - 'relative': 'cypress/integration/company.js', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - { - 'relative': 'cypress/integration/homeSpec.js', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - { - 'relative': 'cypress/integration/sign-up.js', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - { - 'relative': 'cypress/integration/spectacleBrowser.ts', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - { - 'relative': 'cypress/integration/someDir/someFile.js', - 'testingType': 'e2e', - 'usesDefaultFolder': true, - 'usesDefaultTestFiles': false, - }, - ]) - - // expect(actual.component).to.eql([ - // { - // relative: 'src/Radio.spec.js', - // usesDefaultFolder: false, - // usesDefaultTestFiles: false, - // testingType: 'component', - // }, - // ]) - }) - - it('handles default folders', async () => { - // CASE 1: E2E + CT, custom folders, default test files - // We want to rename specs, but keep current folders. - const cwd = await scaffoldMigrationProject('migration-e2e-component-default-everything') - const json = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getSpecs(cwd, json) - - expect(actual.integration).to.eql([ - { - relative: 'cypress/integration/foo.spec.ts', - usesDefaultFolder: true, - usesDefaultTestFiles: true, - testingType: 'e2e', - }, - { - relative: 'cypress/integration/spec.ts', - usesDefaultFolder: true, - usesDefaultTestFiles: true, - testingType: 'e2e', - }, - ]) - - expect(actual.component).to.eql([ - { - relative: 'cypress/component/button.spec.js', - usesDefaultFolder: true, - usesDefaultTestFiles: true, - testingType: 'component', - }, - ]) - }) -}) - -describe('applyMigrationTransform', () => { - describe('e2e spec', () => { - it('handles default folders and extensions', async () => { - const input: MigrationSpec = { - relative: 'cypress/integration/button.spec.js', - usesDefaultFolder: true, - usesDefaultTestFiles: true, - testingType: 'e2e', - } - - const expected: MigrationFile = { - testingType: 'e2e', - before: { - relative: 'cypress/integration/button.spec.js', - parts: [ - { - 'highlight': false, - 'text': 'cypress/', - }, - { - group: 'folder', - 'highlight': true, - 'text': 'integration', - }, - { - 'highlight': false, - 'text': '/button', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.spec.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - after: { - relative: 'cypress/e2e/button.cy.js', - parts: [ - { - 'highlight': false, - 'text': 'cypress/', - }, - { - 'highlight': true, - group: 'folder', - 'text': 'e2e', - }, - { - 'highlight': false, - 'text': '/button', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.cy.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - } - - const result = applyMigrationTransform(input) - - expect(result.before).to.eql(expected.before) - expect(result.after).to.eql(expected.after) - }) - - it('handles custom folder, default extension', async () => { - const input: MigrationSpec = { - relative: 'custom-folder/button.spec.js', - usesDefaultFolder: false, - usesDefaultTestFiles: true, - testingType: 'e2e', - } - - const expected: MigrationFile = { - testingType: 'e2e', - before: { - relative: 'custom-folder/button.spec.js', - parts: [ - { - 'highlight': false, - 'text': 'custom-folder/button', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.spec.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - after: { - relative: 'custom-folder/button.cy.js', - parts: [ - { - 'highlight': false, - 'text': 'custom-folder/button', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.cy.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - } - - const result = applyMigrationTransform(input) - - expect(result.before).to.eql(expected.before) - expect(result.after).to.eql(expected.after) - }) - - it('handles default folder, custom extension', async () => { - const input: MigrationSpec = { - relative: 'cypress/integration/foo.bar', - usesDefaultFolder: true, - usesDefaultTestFiles: false, - testingType: 'e2e', - } - - const expected: MigrationFile = { - testingType: 'e2e', - before: { - relative: 'cypress/integration/foo.bar', - parts: [ - { - 'highlight': false, - 'text': 'cypress/', - }, - { - 'highlight': true, - group: 'folder', - 'text': 'integration', - }, - { - 'highlight': false, - 'text': '/foo.bar', - 'group': 'fileName', - }, - ], - }, - after: { - relative: 'cypress/e2e/foo.bar', - parts: [ - { - 'highlight': false, - 'text': 'cypress/', - }, - { - 'highlight': true, - group: 'folder', - 'text': 'e2e', - }, - { - 'highlight': false, - 'text': '/foo.bar', - 'group': 'fileName', - }, - ], - }, - } - - const result = applyMigrationTransform(input) - - expect(result.before).to.eql(expected.before) - expect(result.after).to.eql(expected.after) - }) - - it('handles a spec named spec', () => { - const input: MigrationSpec = { - relative: 'cypress/integration/spec.js', - usesDefaultFolder: true, - usesDefaultTestFiles: true, - testingType: 'e2e', - } - - const expected: MigrationFile = { - testingType: 'e2e', - before: { - relative: 'cypress/integration/spec.js', - parts: [ - { - 'highlight': false, - 'text': 'cypress/', - }, - { - 'highlight': true, - group: 'folder', - 'text': 'integration', - }, - { - 'highlight': false, - 'text': '/spec', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - after: { - relative: 'cypress/e2e/spec.cy.js', - parts: [ - { - 'highlight': false, - 'text': 'cypress/', - }, - { - 'highlight': true, - group: 'folder', - 'text': 'e2e', - }, - { - 'highlight': false, - 'text': '/spec', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.cy.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - } - - const result = applyMigrationTransform(input) - - expect(result.before).to.eql(expected.before) - expect(result.after).to.eql(expected.after) - }) - - it('handles .test files', () => { - const result = applyMigrationTransform( - { - relative: 'cypress/tests/api-bankaccounts.test.js', - usesDefaultFolder: false, - usesDefaultTestFiles: true, - testingType: 'e2e', - }, - ) - - const expected: MigrationFile = { - testingType: 'e2e', - before: { - relative: 'cypress/tests/api-bankaccounts.test.js', - parts: [ - { - 'highlight': false, - 'text': 'cypress/tests/api-bankaccounts', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.test.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - after: { - relative: 'cypress/tests/api-bankaccounts.cy.js', - parts: [ - { - 'highlight': false, - 'text': 'cypress/tests/api-bankaccounts', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.cy.', - }, - { - 'highlight': false, - 'text': 'js', - }, - ], - }, - } - - expect(result.before).to.eql(expected.before) - expect(result.after).to.eql(expected.after) - }) - }) - - describe('component spec', () => { - it('handles default folders and extensions', async () => { - const input: MigrationSpec = { - relative: 'cypress/component/button.spec.tsx', - usesDefaultFolder: true, - usesDefaultTestFiles: true, - testingType: 'component', - } - - const expected: MigrationFile = { - testingType: 'component', - before: { - relative: 'cypress/component/button.spec.tsx', - parts: [ - { - 'highlight': false, - 'text': 'cypress/component/button', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.spec.', - }, - { - 'highlight': false, - 'text': 'tsx', - }, - ], - }, - after: { - relative: 'cypress/component/button.cy.tsx', - parts: [ - { - 'highlight': false, - 'text': 'cypress/component/button', - 'group': 'fileName', - }, - { - 'highlight': true, - group: 'preExtension', - 'text': '.cy.', - }, - { - 'highlight': false, - 'text': 'tsx', - }, - ], - }, - } - - const result = applyMigrationTransform(input) - - expect(result.before).to.eql(expected.before) - expect(result.after).to.eql(expected.after) - }) - }) - - describe('component with custom folder, default testFiles', () => { - it('handles custom folders and default extensions', async () => { - const input: MigrationSpec = { - relative: 'cypress/custom-component/button.spec.js', - usesDefaultFolder: false, - usesDefaultTestFiles: true, - testingType: 'component', - } - - const expected: MigrationFile = { - 'testingType': 'component', - 'before': { - 'relative': 'cypress/custom-component/button.spec.js', - 'parts': [ - { - 'text': 'cypress/custom-component/button', - 'highlight': false, - 'group': 'fileName', - }, - { - 'text': '.spec.', - 'highlight': true, - 'group': 'preExtension', - }, - { - 'text': 'js', - 'highlight': false, - }, - ], - }, - 'after': { - 'relative': 'cypress/custom-component/button.cy.js', - 'parts': [ - { - 'text': 'cypress/custom-component/button', - 'highlight': false, - 'group': 'fileName', - }, - { - 'text': '.cy.', - 'highlight': true, - 'group': 'preExtension', - }, - { - 'text': 'js', - 'highlight': false, - }, - ], - }, - } - - const actual = applyMigrationTransform(input) - - expect(actual.before).to.eql(expected.before) - expect(actual.after).to.eql(expected.after) - }) - }) -}) diff --git a/packages/data-context/test/unit/sources/migration/codegen.spec.ts b/packages/data-context/test/unit/sources/migration/codegen.spec.ts deleted file mode 100644 index bd4714f4d2b1..000000000000 --- a/packages/data-context/test/unit/sources/migration/codegen.spec.ts +++ /dev/null @@ -1,561 +0,0 @@ -import snapshot from 'snap-shot-it' -import path from 'path' -import fs from 'fs-extra' -import { - createConfigString, - initComponentTestingMigration, - ComponentTestingMigrationStatus, - NonStandardMigrationError, - supportFilesForMigration, - reduceConfig, - renameSupportFilePath, -} from '../../../../src/sources/migration' -import { expect } from 'chai' -import { MigrationFile } from '../../../../src/sources' -import { scaffoldMigrationProject, getSystemTestProject } from '../../helper' - -const projectRoot = getSystemTestProject('migration-e2e-defaults') - -describe('cypress.config.js generation', () => { - it('generates correct config for component testing migration with custom testFiles glob', async () => { - const config = { - component: { - testFiles: '**/*.spec.cy.{js,ts,jsx,tsx}', - componentFolder: '.', - }, - } - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: false, - hasComponentTesting: true, - hasPluginsFile: false, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('generates correct config for component testing migration with custom testFiles array of glob', async () => { - const config = { - e2e: { - testFiles: ['**/*.spec.js', '**/*.test.js'], - }, - } - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: false, - hasPluginsFile: false, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: true, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create a string when passed only a global option', async () => { - const config: Partial = { - viewportWidth: 300, - } - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create a string when passed only a e2e options', async () => { - const config: Partial = { - e2e: { - baseUrl: 'localhost:3000', - }, - } - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create a string when passed only a component options', async () => { - const generatedConfig = await createConfigString({ - component: { - retries: 2, - }, - }, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create only a component entry when no e2e specs are detected', async () => { - const generatedConfig = await createConfigString({}, { - hasE2ESpec: false, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create only an e2e entry when no component specs are detected', async () => { - const generatedConfig = await createConfigString({}, { - hasE2ESpec: true, - hasComponentTesting: false, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create a string for a config with global, component, and e2e options', async () => { - const config = { - viewportWidth: 300, - baseUrl: 'localhost:300', - slowTestThreshold: 500, - e2e: { - retries: 2, - }, - component: { - retries: 1, - }, - } - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create a string when passed an empty object', async () => { - const config = {} - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should create a string when passed an empty object for an ECMA Script project', async () => { - const config = {} - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - isProjectUsingESModules: true, - shouldAddCustomE2ESpecPattern: false, - }) - - snapshot(generatedConfig) - }) - - it('should exclude fields that are no longer valid', async () => { - const config = { - '$schema': 'http://someschema.com', - pluginsFile: './cypress/plugins/index.js', - componentFolder: 'path/to/component/folder', - } - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: false, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should handle export default in plugins file', async () => { - const projectRoot = getSystemTestProject('migration-e2e-export-default') - const config = fs.readJsonSync(path.join(projectRoot, 'cypress.json')) - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: true, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should maintain both root level and nested non-breaking options during migration', async () => { - const projectRoot = getSystemTestProject('migration-e2e-component-default-everything') - const config = await fs.readJson(path.join(projectRoot, 'cypress.json')) - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: true, - shouldAddCustomE2ESpecPattern: false, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should add custom specPattern if project has projectId', async () => { - const projectRoot = getSystemTestProject('migration-e2e-defaults-with-projectId') - const config = await fs.readJson(path.join(projectRoot, 'cypress.json')) - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: true, - shouldAddCustomE2ESpecPattern: true, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) - - it('should not add custom specPattern if project has projectId and integrationFolder', async () => { - const projectRoot = getSystemTestProject('migration-e2e-defaults-with-projectId') - const config = await fs.readJson(path.join(projectRoot, 'cypress.json')) - - config['integrationFolder'] = 'cypress/custom/e2e' - - const generatedConfig = await createConfigString(config, { - hasE2ESpec: true, - hasComponentTesting: true, - hasPluginsFile: true, - projectRoot, - isUsingTypeScript: true, - shouldAddCustomE2ESpecPattern: true, - isProjectUsingESModules: false, - }) - - snapshot(generatedConfig) - }) -}) - -describe('supportFilesForMigrationGuide', () => { - it('finds and represents correct supportFile migration guide', async () => { - const cwd = await scaffoldMigrationProject('migration') - const actual = await supportFilesForMigration(cwd) - - const expected: MigrationFile = { - testingType: 'e2e', - before: { - relative: 'cypress/support/index.js', - parts: [ - { - 'text': 'cypress/support/', - 'highlight': false, - }, - { - 'text': 'index', - 'highlight': true, - group: 'supportFileName', - }, - { - 'text': '.js', - 'highlight': false, - }, - ], - }, - after: { - relative: 'cypress/support/e2e.js', - parts: [ - { - 'text': 'cypress/support/', - 'highlight': false, - }, - { - 'text': 'e2e', - 'highlight': true, - group: 'supportFileName', - }, - { - 'text': '.js', - 'highlight': false, - }, - ], - }, - } - - // expect(actual.before).to.eql(expected.before) - expect(actual.after).to.eql(expected.after) - }) -}) - -describe('renameSupportFilePath', () => { - it('renames and keeps correct js extension', () => { - const p = 'cypress/support/index.js' - const actual = renameSupportFilePath(p) - - expect(actual).to.eq('cypress/support/e2e.js') - }) - - it('renames and keeps correct tsx extension', () => { - const p = 'cypress/support/index.tsx' - const actual = renameSupportFilePath(p) - - expect(actual).to.eq('cypress/support/e2e.tsx') - }) - - it('errors on non standard path', () => { - const p = 'cypress/support/something-else.tsx' - - expect(() => renameSupportFilePath(p)).to.throw(NonStandardMigrationError) - }) -}) - -describe('initComponentTestingMigration', () => { - it('calls callback with status each time file is removed', async () => { - const cwd = await scaffoldMigrationProject('migration-component-testing-customized') - - const delay = () => new Promise((res) => setTimeout(res, 250)) - - let updatedStatus: ComponentTestingMigrationStatus - - const onFileMoved = (_status: ComponentTestingMigrationStatus) => { - updatedStatus = _status - } - - const { status, watcher } = await initComponentTestingMigration( - cwd, - 'src', - ['**/*.{js,tsx}'], - onFileMoved, - ) - - expect(status.completed).to.be.false - expect(status.files).to.eql(new Map([ - ['src/button.spec.js', { moved: false, - relative: 'src/button.spec.js', - }], - ['src/input-spec.tsx', { - moved: false, - relative: 'src/input-spec.tsx', - }], - ])) - - fs.moveSync( - path.join(cwd, 'src', 'input-spec.tsx'), - path.join(cwd, 'src', 'input.cy.tsx'), - ) - - // give watcher time to trigger - await delay() - - expect(updatedStatus).to.eql({ - files: new Map([ - ['src/button.spec.js', { moved: false, relative: 'src/button.spec.js' }], - ['src/input-spec.tsx', { moved: true, relative: 'src/input-spec.tsx' }], - ]), - completed: false, - }) - - fs.moveSync( - path.join(cwd, 'src', 'button.spec.js'), - path.join(cwd, 'src', 'button.cy.js'), - ) - - // give watcher time to trigger - await delay() - - expect(updatedStatus).to.eql({ - files: new Map([ - ['src/button.spec.js', { moved: true, relative: 'src/button.spec.js' }], - ['src/input-spec.tsx', { moved: true, relative: 'src/input-spec.tsx' }], - ]), - completed: true, - }) - - await watcher.close() - }) -}) - -describe('reduceConfig', () => { - const options = { - hasComponentTesting: false, - hasE2ESpec: false, - hasPluginsFile: false, - projectRoot: '', - isUsingTypeScript: false, - isProjectUsingESModules: false, - shouldAddCustomE2ESpecPattern: false, - } - - it('should move the testFiles field to e2e and component', () => { - const config = { testFiles: '**/**.cy.js' } - const newConfig = reduceConfig(config, options) - - expect(newConfig.e2e.specPattern).to.eq('cypress/e2e/**/**.cy.js') - expect(newConfig.component.specPattern).to.eq('**/**.cy.js') - }) - - it('should update integration folder for e2e when is set to default', () => { - const config = { testFiles: '*.spec.js', integrationFolder: 'cypress/integration' } - const newConfig = reduceConfig(config, options) - - expect(newConfig.e2e.specPattern).to.eq(`cypress/e2e/${config.testFiles}`) - }) - - it('should combine componentFolder and integrationFolder with testFiles field in component', () => { - const config = { testFiles: '**/**.cy.js', componentFolder: 'src', integrationFolder: 'cypress/src' } - const newConfig = reduceConfig(config, options) - - expect(newConfig.component.specPattern).to.eq('src/**/**.cy.js') - expect(newConfig.e2e.specPattern).to.eq(`${config.integrationFolder}/${config.testFiles}`) - }) - - it('should combine nested componentFolder and integrationFolder with testFiles field in component', () => { - const config = { - testFiles: '**/**.cy.js', - component: { - componentFolder: 'src', - }, - e2e: { - integrationFolder: 'cypress/src', - }, - } - const newConfig = reduceConfig(config, options) - - expect(newConfig.component.componentFolder).to.not.exist - expect(newConfig.component.specPattern).to.eq('src/**/**.cy.js') - expect(newConfig.e2e.specPattern).to.eq(`${config.e2e.integrationFolder}/${config.testFiles}`) - }) - - it('should add custom integrationFolder to default testFiles if testFiles is not present', () => { - const config = { integrationFolder: 'cypress/custom-integration' } - const newConfig = reduceConfig(config, options) - - expect(newConfig.e2e.specPattern).to.eq(`${config.integrationFolder}/**/*.cy.{js,jsx,ts,tsx}`) - }) - - it('should add custom integrationFolder to default testFiles if testFiles is not present and shouldAddCustomE2ESpecPattern is true', () => { - const config = { integrationFolder: 'cypress/custom-integration' } - const newConfig = reduceConfig(config, { ...options, shouldAddCustomE2ESpecPattern: true }) - - expect(newConfig.e2e.specPattern).to.eq(`${config.integrationFolder}/**/*.{js,jsx,ts,tsx}`) - }) - - it('should combine testFiles with highest specificity', () => { - const config = { - testFiles: '**/**.cy.js', - componentFolder: 'lower/specificity', - integrationFolder: 'lower/specificity', - component: { - componentFolder: 'higher/specificity', - }, - e2e: { - integrationFolder: 'higher/specificity', - }, - } - const newConfig = reduceConfig(config, options) - - expect(newConfig.component.specPattern).to.eq(`higher/specificity/**/**.cy.js`) - expect(newConfig.e2e.specPattern).to.eq(`${config.e2e.integrationFolder}/${config.testFiles}`) - }) - - it('should exclude integrationFolder and componentFolder', () => { - const config = { - componentFolder: 'src', - integrationFolder: 'cypress/integration', - } - - const newConfig = reduceConfig(config, options) - - // @ts-ignore field not on ConfigOptions type - expect(newConfig.global.componentFolder).to.not.exist - // @ts-ignore field not on ConfigOptions type - expect(newConfig.global.integrationFolder).to.not.exist - }) - - it('should rename ignoreTestFiles to excludeSpecPattern', () => { - const config = { ignoreTestFiles: 'path/to/**/*.js' } - const newConfig = reduceConfig(config, options) - - expect(newConfig.e2e.excludeSpecPattern).to.eq(config.ignoreTestFiles) - expect(newConfig.component.excludeSpecPattern).to.eq(config.ignoreTestFiles) - }) - - it('should nest supportFile under component and e2e', () => { - const config = { supportFile: 'cypress/support/mySupportFile.js' } - const newConfig = reduceConfig(config, options) - - expect(newConfig.e2e.supportFile).to.eq(config.supportFile) - }) - - it('should not add supportFile if it is the default one', () => { - expect(reduceConfig({ supportFile: null }, options).e2e.supportFile).to.not.exist - expect(reduceConfig({ supportFile: undefined }, options).e2e.supportFile).to.not.exist - expect(reduceConfig({ supportFile: 'cypress/support' }, options).e2e.supportFile).to.not.exist - expect(reduceConfig({ supportFile: 'cypress/support/index' }, options).e2e.supportFile).to.not.exist - expect(reduceConfig({ supportFile: 'cypress/support/index.js' }, options).e2e.supportFile).to.not.exist - expect(reduceConfig({ supportFile: './cypress/support/index.js' }, options).e2e.supportFile).to.not.exist - expect(reduceConfig({ supportFile: '../cypress/support/index.js' }, options).e2e.supportFile).to.not.exist - }) - - it('should exclude the pluginsFile', () => { - const config = { pluginsFile: 'cypress/plugins/index.js' } - const newConfig = reduceConfig(config, options) - - // @ts-ignore field not on ConfigOptions type - expect(newConfig.global.pluginsFile).to.not.exist - }) -}) diff --git a/packages/data-context/test/unit/sources/migration/format.spec.ts b/packages/data-context/test/unit/sources/migration/format.spec.ts deleted file mode 100644 index 133afa07adb6..000000000000 --- a/packages/data-context/test/unit/sources/migration/format.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { expect } from 'chai' -import { - formatMigrationFile, -} from '../../../../src/sources/migration/format' -import { regexps, supportFileRegexps } from '../../../../src/sources/migration/regexps' - -describe('formatMigrationFile', () => { - describe('e2e - defaultFolderDefaultTestFiles', () => { - it('breaks pre-migration spec into parts', () => { - const spec = 'cypress/integration/app.spec.js' - const re = new RegExp(regexps.e2e.before.defaultFolderDefaultTestFiles) - const actual = formatMigrationFile(spec, re, { shouldMigratePreExtension: true }) - - expect(actual).to.eql([ - { text: 'cypress/', highlight: false }, - { text: 'integration', highlight: true, group: 'folder' }, - { text: '/app', highlight: false, group: 'fileName' }, - { text: '.spec.', highlight: true, group: 'preExtension' }, - { text: 'js', highlight: false }, - ]) - }) - - it('do not highlight the preExtension when migratePreExtension is false', () => { - const spec = 'cypress/integration/app.spec.js' - const re = new RegExp(regexps.e2e.before.defaultFolderDefaultTestFiles) - const actual = formatMigrationFile(spec, re, { shouldMigratePreExtension: false }) - - expect(actual).to.eql([ - { text: 'cypress/', highlight: false }, - { text: 'integration', highlight: true, group: 'folder' }, - { text: '/app', highlight: false, group: 'fileName' }, - { text: '.spec.', highlight: false, group: 'preExtension' }, - { text: 'js', highlight: false }, - ]) - }) - }) - - ;['js', 'ts'].forEach((ext) => { - it(`handles e2e support pre file migration [${ext}]`, () => { - const file = `cypress/support/index.${ext}` - const re = new RegExp(supportFileRegexps.e2e.beforeRegexp) - const actual = formatMigrationFile(file, re, { shouldMigratePreExtension: true }) - - expect(actual).to.eql([ - { text: 'cypress/support/', highlight: false }, - { text: 'index', highlight: true, group: 'supportFileName' }, - { text: `.${ext}`, highlight: false }, - ]) - }) - - it(`handles e2e support post file migration [${ext}]`, () => { - const file = `cypress/support/e2e.${ext}` - const re = new RegExp(supportFileRegexps.e2e.afterRegexp) - const actual = formatMigrationFile(file, re, { shouldMigratePreExtension: true }) - - expect(actual).to.eql([ - { text: 'cypress/support/', highlight: false }, - { text: 'e2e', highlight: true, group: 'supportFileName' }, - { text: `.${ext}`, highlight: false }, - ]) - }) - }) -}) diff --git a/packages/data-context/test/unit/sources/migration/resolveLegacyConfig.spec.ts b/packages/data-context/test/unit/sources/migration/resolveLegacyConfig.spec.ts deleted file mode 100644 index b39b2489249a..000000000000 --- a/packages/data-context/test/unit/sources/migration/resolveLegacyConfig.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { expect } from 'chai' -import fs from 'fs-extra' -import path from 'path' -import { processConfigViaLegacyPlugins } from '../../../../src/actions' -import { getSystemTestProject } from '../../helper' - -describe('processConfigViaLegacyPlugins', () => { - it('executes legacy plugins and returns modified config', async () => { - const projectRoot = getSystemTestProject('migration-e2e-plugins-modify-config') - const result = await processConfigViaLegacyPlugins(projectRoot, {}) - - expect(result).to.eql({ - 'component': { - 'testFiles': '**/*.spec.ts', - }, - 'e2e': { - 'testFiles': '**/*.js', - }, - 'integrationFolder': 'tests/e2e', - 'retries': { - 'openMode': 0, - 'runMode': 1, - }, - 'testFiles': '**/*.spec.js', - }) - }) - - it('executes legacy plugins and returns without change if pluginsFile returns nothing', async () => { - const projectRoot = getSystemTestProject('migration-e2e-defaults') - const configFile = fs.readJsonSync(path.join(projectRoot, 'cypress.json')) - const result = await processConfigViaLegacyPlugins(projectRoot, configFile) - - expect(result).to.eql(configFile) - }) - - it('works with cypress/plugins/index.ts and export default', async () => { - const projectRoot = getSystemTestProject('migration-e2e-export-default') - const result = await processConfigViaLegacyPlugins(projectRoot, { - retries: 10, - viewportWidth: 8888, - }) - - expect(result).to.eql({ - retries: 10, - viewportWidth: 1111, // mutated in plugins file - }) - }) - - it('catches error', (done) => { - const projectRoot = getSystemTestProject('migration-e2e-legacy-plugins-throws-error') - - processConfigViaLegacyPlugins(projectRoot, {}) - .catch((e) => { - expect(e.originalError.message).to.eq('Uh oh, there was an error!') - done() - }) - }) - - it('handles pluginsFile: false', async () => { - const projectRoot = getSystemTestProject('launchpad') - const result = await processConfigViaLegacyPlugins(projectRoot, { - retries: 10, - viewportWidth: 8888, - }) - - expect(result).to.eql({ - retries: 10, - viewportWidth: 8888, - }) - }) -}) diff --git a/packages/data-context/test/unit/sources/migration/shouldShowSteps.spec.ts b/packages/data-context/test/unit/sources/migration/shouldShowSteps.spec.ts deleted file mode 100644 index 02705b573cc3..000000000000 --- a/packages/data-context/test/unit/sources/migration/shouldShowSteps.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { scaffoldMigrationProject } from '../../helper' -import path from 'path' -import fs from 'fs-extra' -import { - getStepsForMigration, - shouldShowAutoRenameStep, - Step, -} from '../../../../src/sources/migration/shouldShowSteps' -import { expect } from 'chai' - -describe('shouldShowAutoRenameStep', () => { - it('true when testFiles is custom, but default integration folder', async () => { - const cwd = await scaffoldMigrationProject('migration') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.true - }) - - it('true when testFiles is custom, but default integration folder', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-component-default-test-files') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.true - }) - - it('false when integrationFolder and testFiles are custom', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-fully-custom') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.false - }) - - it('true when integrationFolder custom and testFiles default', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-custom-integration') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.true - }) - - it('true when integrationFolder default and testFiles custom', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-custom-test-files') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.true - }) - - it('true when integrationFolder and testFiles default and spec exists', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-defaults') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.true - }) - - it('false when integrationFolder and testFiles default by no spec to migrate', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-defaults-no-specs') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - const actual = await shouldShowAutoRenameStep(cwd, config) - - expect(actual).to.be.false - }) -}) - -describe('getStepsForMigration', () => { - it('only returns configFile step for highly custom project', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-fully-custom') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getStepsForMigration(cwd, config, true) - const expected: Step[] = ['configFile'] - - expect(actual).to.eql(expected) - }) - - it('returns all e2e steps for project with all defaults', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-defaults') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getStepsForMigration(cwd, config, true) - const expected: Step[] = ['renameAuto', 'renameSupport', 'configFile'] - - expect(actual).to.eql(expected) - }) - - it('returns all e2e steps for project with all defaults + custom testFiles', async () => { - const cwd = await scaffoldMigrationProject('migration-e2e-custom-test-files') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getStepsForMigration(cwd, config, true) - const expected: Step[] = ['renameAuto', 'renameSupport', 'configFile'] - - expect(actual).to.eql(expected) - }) - - it('returns all steps for default integrationFolder, custom testFiles', async () => { - const cwd = await scaffoldMigrationProject('migration') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getStepsForMigration(cwd, config, true) - const expected: Step[] = ['renameAuto', 'renameSupport', 'configFile', 'setupComponent'] - - expect(actual).to.eql(expected) - }) - - it('returns all steps except supportFile for default CT project', async () => { - const cwd = await scaffoldMigrationProject('migration-component-testing-defaults') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getStepsForMigration(cwd, config, true) - const expected: Step[] = ['renameAuto', 'renameManual', 'configFile', 'setupComponent'] - - expect(actual).to.eql(expected) - }) - - it('returns component steps for component testing project (no e2e)', async () => { - const cwd = await scaffoldMigrationProject('migration-component-testing-customized') - const config = fs.readJsonSync(path.join(cwd, 'cypress.json')) - - const actual = await getStepsForMigration(cwd, config, true) - const expected: Step[] = ['configFile', 'setupComponent'] - - expect(actual).to.eql(expected) - }) -}) diff --git a/packages/driver/cypress/e2e/commands/actions/focus.cy.ts b/packages/driver/cypress/e2e/commands/actions/focus.cy.ts index 4f78c0f1dcbb..dca51c1519be 100644 --- a/packages/driver/cypress/e2e/commands/actions/focus.cy.ts +++ b/packages/driver/cypress/e2e/commands/actions/focus.cy.ts @@ -457,7 +457,7 @@ describe('src/cy/commands/actions/focus', () => { // // now.apply(@, arguments) // - // cy.stub(cy, "now", nowFn) + // cy.stub(cy, "now").callsFake(nowFn) const $first = cy.$$('input:first') // Preserved for later use. diff --git a/packages/driver/cypress/e2e/commands/agents.cy.ts b/packages/driver/cypress/e2e/commands/agents.cy.ts index 253bf9ca4fae..15e2d9e57333 100644 --- a/packages/driver/cypress/e2e/commands/agents.cy.ts +++ b/packages/driver/cypress/e2e/commands/agents.cy.ts @@ -62,42 +62,6 @@ describe('src/cy/commands/agents', () => { }) }) - describe('.stub(obj, \'method\', replacerFn)', () => { - beforeEach(function () { - this.originalCalled = false - this.obj = { - bar: 'bar', - foo: () => { - this.originalCalled = true - }, - } - - // @ts-expect-error TODO: deprecated: Figure out how to handle types here - cy.stub(this.obj, 'bar', 'baz') - - this.replacementCalled = false - this.stub = cy.stub(this.obj, 'foo', () => { - this.replacementCalled = true - }) - }) - - it('proxies sinon stub', function () { - this.obj.foo() - - expect(this.stub.callCount).to.eq(1) - }) - - it('replaces method with replacement', function () { - this.obj.foo() - expect(this.originalCalled).to.be.false - expect(this.replacementCalled).to.be.true - }) - - it('replaces values', function () { - expect(this.obj.bar).to.eq('baz') - }) - }) - describe('.resolves', () => { beforeEach(function () { this.obj = { foo () {} } diff --git a/packages/driver/cypress/e2e/commands/location.cy.js b/packages/driver/cypress/e2e/commands/location.cy.js index 1f6ec80699a3..1bbd5f93b83b 100644 --- a/packages/driver/cypress/e2e/commands/location.cy.js +++ b/packages/driver/cypress/e2e/commands/location.cy.js @@ -23,12 +23,15 @@ describe('src/cy/commands/location', () => { cy.url().should('match', /baz/).and('eq', 'http://localhost:3500/foo/bar/baz.html') }) - it('catches thrown errors', () => { - cy.stub(Cypress.utils, 'locToString') - .onFirstCall().throws(new Error) - .onSecondCall().returns('http://localhost:3500/baz.html') + it('propagates thrown errors from CDP', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('CDP was unable to find the AUT iframe') + done() + }) + + cy.stub(Cypress, 'automation').withArgs('get:aut:url').rejects(new Error('CDP was unable to find the AUT iframe')) - cy.url().should('include', '/baz.html') + cy.url() }) // https://github.com/cypress-io/cypress/issues/17399 @@ -380,7 +383,16 @@ describe('src/cy/commands/location', () => { context('#location', () => { it('returns the location object', () => { cy.location().then((loc) => { - expect(loc).to.have.keys(['auth', 'authObj', 'hash', 'href', 'host', 'hostname', 'pathname', 'port', 'protocol', 'search', 'origin', 'superDomainOrigin', 'superDomain', 'toString']) + expect(loc).to.have.property('hash') + expect(loc).to.have.property('host') + expect(loc).to.have.property('hostname') + expect(loc).to.have.property('href') + expect(loc).to.have.property('origin') + expect(loc).to.have.property('pathname') + expect(loc).to.have.property('port') + expect(loc).to.have.property('protocol') + expect(loc).to.have.property('search') + expect(loc).to.have.property('searchParams') }) }) @@ -402,15 +414,13 @@ describe('src/cy/commands/location', () => { // https://github.com/cypress-io/cypress/issues/16463 it('eventually returns a given key', function () { - cy.stub(cy, 'getRemoteLocation') - .onFirstCall().returns('') - .onSecondCall().returns({ - pathname: '/my/path', - }) + cy.stub(Cypress, 'automation').withArgs('get:aut:url') + .onFirstCall().resolves('http://localhost:3500') + .onSecondCall().resolves('http://localhost:3500/my/path') cy.location('pathname').should('equal', '/my/path') .then(() => { - expect(cy.getRemoteLocation).to.have.been.calledTwice + expect(Cypress.automation).to.have.been.calledTwice }) }) @@ -614,7 +624,8 @@ describe('src/cy/commands/location', () => { expect(_.keys(consoleProps)).to.deep.eq(['name', 'type', 'props']) expect(consoleProps.name).to.eq('location') expect(consoleProps.type).to.eq('command') - expect(_.keys(consoleProps.props.Yielded)).to.deep.eq(['auth', 'authObj', 'hash', 'href', 'host', 'hostname', 'origin', 'pathname', 'port', 'protocol', 'search', 'superDomainOrigin', 'superDomain', 'toString']) + + expect(_.keys(consoleProps.props.Yielded)).to.deep.eq(['hash', 'host', 'hostname', 'href', 'origin', 'pathname', 'port', 'protocol', 'search', 'searchParams']) }) }) }) diff --git a/packages/driver/cypress/e2e/commands/navigation.cy.js b/packages/driver/cypress/e2e/commands/navigation.cy.js index 433e8826004a..3b9e9de6a223 100644 --- a/packages/driver/cypress/e2e/commands/navigation.cy.js +++ b/packages/driver/cypress/e2e/commands/navigation.cy.js @@ -11,35 +11,23 @@ describe('src/cy/commands/navigation', () => { }) it('calls into window.location.reload', () => { - const locReload = cy.spy(Cypress.utils, 'locReload') - - cy.reload().then(() => { - expect(locReload).to.be.calledWith(false) + cy.on('fail', () => { + expect(Cypress.automation).to.be.calledWith('reload:aut:frame', { forceReload: false }) }) - }) - it('can pass forceReload', () => { - const locReload = cy.spy(Cypress.utils, 'locReload') + cy.stub(Cypress, 'automation').withArgs('reload:aut:frame', { forceReload: false }).resolves() - cy.reload(true).then(() => { - expect(locReload).to.be.calledWith(true) - }) + cy.reload({ timeout: 1000 }) }) it('can pass forceReload + options', () => { - const locReload = cy.spy(Cypress.utils, 'locReload') - - cy.reload(true, {}).then(() => { - expect(locReload).to.be.calledWith(true) + cy.on('fail', () => { + expect(Cypress.automation).to.be.calledWith('reload:aut:frame', { forceReload: true }) }) - }) - it('can pass just options', () => { - const locReload = cy.spy(Cypress.utils, 'locReload') + cy.stub(Cypress, 'automation').withArgs('reload:aut:frame', { forceReload: true }).resolves() - cy.reload({}).then(() => { - expect(locReload).to.be.calledWith(false) - }) + cy.reload(true, { timeout: 1000 }) }) it('returns the window object', () => { @@ -97,15 +85,13 @@ describe('src/cy/commands/navigation', () => { }) it('removes listeners', () => { - cy.log(Cypress.browser) - const unloadEvent = Cypress.browser.family === 'chromium' ? 'pagehide' : 'unload' const win = cy.state('window') const rel = cy.stub(win, 'removeEventListener') cy.reload().then(() => { expect(rel).to.be.calledWith('beforeunload') - expect(rel).to.be.calledWith(unloadEvent) + expect(rel).to.be.calledWith('unload') }) }) @@ -415,10 +401,8 @@ describe('src/cy/commands/navigation', () => { const rel = cy.stub(win, 'removeEventListener') cy.go('back').then(() => { - const unloadEvent = cy.browser.family === 'chromium' ? 'pagehide' : 'unload' - expect(rel).to.be.calledWith('beforeunload') - expect(rel).to.be.calledWith(unloadEvent) + expect(rel).to.be.calledWith('unload') }) }) }) @@ -600,14 +584,15 @@ describe('src/cy/commands/navigation', () => { const { lastLog } = this beforeunload = true - expect(lastLog.get('snapshots').length).to.eq(1) + expect(lastLog.get('snapshots').length).to.eq(2) expect(lastLog.get('snapshots')[0].name).to.eq('before') expect(lastLog.get('snapshots')[0].body).to.be.an('object') return undefined }) - cy.go('back').then(function () { + // wait for the beforeunload event to be fired after the history navigation + cy.go('back').wait(100).then(function () { const { lastLog } = this expect(beforeunload).to.be.true diff --git a/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts b/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts index 25d08e915d4b..ac9394d132af 100644 --- a/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts +++ b/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts @@ -777,17 +777,6 @@ describe('network stubbing', { retries: 15 }, function () { }) }) - it('errors on matchUrlAgainstPath usage', function (done) { - testFail((err) => { - expect(err.message).to.include('`matchUrlAgainstPath` was removed in Cypress 7.0.0') - - done() - }) - - // @ts-ignore - cy.intercept({ matchUrlAgainstPath: true }) - }) - it('errors on unknown prop', function (done) { testFail((err) => { expect(err.message).to.include('An unknown \`RouteMatcher\` property was passed: `wrong`') @@ -1023,7 +1012,7 @@ describe('network stubbing', { retries: 15 }, function () { context('cors preflight', function () { // a different domain from the page own domain // NOTE: this domain is redirected back to the local host test server - // using "hosts" setting in the "cypress.json" file + // using "hosts" setting in the "cypress.config.js" file let corsUrl = 'http://diff.foobar.com:3501/no-cors' beforeEach(() => { diff --git a/packages/driver/cypress/e2e/dom/coordinates.cy.ts b/packages/driver/cypress/e2e/dom/coordinates.cy.ts index abcc50232e02..16873d3d0bcb 100644 --- a/packages/driver/cypress/e2e/dom/coordinates.cy.ts +++ b/packages/driver/cypress/e2e/dom/coordinates.cy.ts @@ -280,7 +280,7 @@ this is some long text with a single const $el = cy.$$('#multiple') const el = $el[0] - cy.stub(el, 'getClientRects', () => { + cy.stub(el, 'getClientRects').callsFake(() => { return [ { top: 100, diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts index 3210ddda03cc..ca90333bdc44 100644 --- a/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/commands/actions.cy.ts @@ -188,101 +188,103 @@ context('cy.origin actions', { browser: '!webkit' }, () => { }) }) - context('cross-origin AUT errors', () => { - // We only need to check .get here because the other commands are chained off of it. - // the exceptions are window(), document(), title(), url(), hash(), location(), go(), reload(), and scrollTo() - const assertOriginFailure = (err: Error, done: () => void) => { - expect(err.message).to.include(`The command was expected to run against origin \`http://localhost:3500\` but the application is at origin \`http://www.foobar.com:3500\`.`) - expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`) - expect(err.message).to.include(`Using \`cy.origin()\` to wrap the commands run on \`http://www.foobar.com:3500\` will likely fix this issue.`) - expect(err.message).to.include(`cy.origin('http://www.foobar.com:3500', () => {\`\n\` \`\n\`})`) - - // make sure that the secondary origin failures do NOT show up as spec failures or AUT failures - expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`) - expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`) - done() - } - - it('.get()', { defaultCommandTimeout: 50 }, (done) => { - cy.on('fail', (err) => { - expect(err.message).to.include(`Timed out retrying after 50ms:`) - assertOriginFailure(err, done) - }) - - cy.get('a[data-cy="dom-link"]').click() - cy.get('#button') - }) - + // With Cypress 15, window() will work always without cy.origin(). + // However, users may not have access to the AUT window object, so cy.window() yielded window objects + // may return cross-origin errors. + context('cross-origin AUT commands working with cy.origin()', () => { it('.window()', (done) => { - cy.on('fail', (err) => { - assertOriginFailure(err, done) - }) - cy.get('a[data-cy="dom-link"]').click() - cy.window() - }) - - it('.document()', (done) => { - cy.on('fail', (err) => { - assertOriginFailure(err, done) + cy.window().then((win) => { + // The window is in a cross-origin state, but users are able to yield the command + // as well as basic accessible properties + expect(win.length).to.equal(2) + try { + // but cannot access cross-origin properties + win[0].location.href + } catch (e) { + expect(e.name).to.equal('SecurityError') + if (Cypress.isBrowser('firefox')) { + expect(e.message).to.include('Permission denied to get property "href" on cross-origin object') + } else { + expect(e.message).to.include('Blocked a frame with origin "http://localhost:3500" from accessing a cross-origin frame.') + } + + done() + } }) + }) + it('.reload()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.document() + cy.reload() }) - it('.title()', (done) => { - cy.on('fail', (err) => { - assertOriginFailure(err, done) - }) - + it('.url()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.title() + cy.url().then((url) => { + expect(url).to.equal('http://www.foobar.com:3500/fixtures/dom.html') + }) }) - it('.url()', (done) => { - cy.on('fail', (err) => { - assertOriginFailure(err, done) + it('.hash()', () => { + cy.get('a[data-cy="dom-link"]').click() + cy.hash().then((hash) => { + expect(hash).to.equal('') }) + }) + it('.location()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.url() + cy.location().then((loc) => { + expect(loc.href).to.equal('http://www.foobar.com:3500/fixtures/dom.html') + }) }) - it('.hash()', (done) => { - cy.on('fail', (err) => { - assertOriginFailure(err, done) + it('.title()', () => { + cy.get('a[data-cy="dom-link"]').click() + cy.title().then((title) => { + expect(title).to.equal('DOM Fixture') }) + }) + it('.go()', () => { cy.get('a[data-cy="dom-link"]').click() - cy.hash() + cy.go('back') }) + }) - it('.location()', (done) => { - cy.on('fail', (err) => { - assertOriginFailure(err, done) - }) + context('cross-origin AUT errors', () => { + // We only need to check .get here because the other commands are chained off of it. + // the exceptions are document() and scrollTo() + const assertOriginFailure = (err: Error, done: () => void) => { + expect(err.message).to.include(`The command was expected to run against origin \`http://localhost:3500\` but the application is at origin \`http://www.foobar.com:3500\`.`) + expect(err.message).to.include(`This commonly happens when you have either not navigated to the expected origin or have navigated away unexpectedly.`) + expect(err.message).to.include(`Using \`cy.origin()\` to wrap the commands run on \`http://www.foobar.com:3500\` will likely fix this issue.`) + expect(err.message).to.include(`cy.origin('http://www.foobar.com:3500', () => {\`\n\` \`\n\`})`) - cy.get('a[data-cy="dom-link"]').click() - cy.location() - }) + // make sure that the secondary origin failures do NOT show up as spec failures or AUT failures + expect(err.message).not.to.include(`The following error originated from your test code, not from Cypress`) + expect(err.message).not.to.include(`The following error originated from your application code, not from Cypress`) + done() + } - it('.go()', (done) => { + it('.get()', { defaultCommandTimeout: 50 }, (done) => { cy.on('fail', (err) => { + expect(err.message).to.include(`Timed out retrying after 50ms:`) assertOriginFailure(err, done) }) cy.get('a[data-cy="dom-link"]').click() - cy.go('back') + cy.get('#button') }) - it('.reload()', (done) => { + it('.document()', (done) => { cy.on('fail', (err) => { assertOriginFailure(err, done) }) cy.get('a[data-cy="dom-link"]').click() - cy.reload() + cy.document() }) it('.scrollTo()', (done) => { diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/location.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/location.cy.ts index e410f56723bc..9325a2d6ea3b 100644 --- a/packages/driver/cypress/e2e/e2e/origin/commands/location.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/commands/location.cy.ts @@ -62,19 +62,16 @@ context('cy.origin location', { browser: '!webkit' }, () => { expect(consoleProps.name).to.equal('location') expect(consoleProps.type).to.equal('command') - expect(consoleProps.props.Yielded).to.have.property('auth').that.is.a('string') - expect(consoleProps.props.Yielded).to.have.property('authObj').that.is.undefined expect(consoleProps.props.Yielded).to.have.property('hash').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('host').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('hostname').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('href').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('origin').that.is.a('string') - expect(consoleProps.props.Yielded).to.have.property('superDomainOrigin').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('pathname').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('port').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('protocol').that.is.a('string') expect(consoleProps.props.Yielded).to.have.property('search').that.is.a('string') - expect(consoleProps.props.Yielded).to.have.property('superDomain').that.is.a('string') + expect(consoleProps.props.Yielded).to.have.property('searchParams').that.is.an('object') }) }) diff --git a/packages/driver/package.json b/packages/driver/package.json index a033a1eb5e91..8a6b08746e2c 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -95,7 +95,7 @@ "vite": "^5.4.18", "vitest": "^2.1.8", "webpack": "^5.88.2", - "zone.js": "0.9.0" + "zone.js": "0.15.0" }, "files": [ "patches" diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index c6af33aad03c..f0434639b93f 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -220,13 +220,10 @@ const attachToWindow = (autWindow: Window) => { Cypress.specBridgeCommunicator.toPrimary('window:load', { url: remoteLocation.href }) cy.isStable(true, 'load') }, - onPageHide (e) { + onUnload (e) { cy.state('window', undefined) cy.state('document', undefined) - // unload is being actively deprecated/removed by chrome, so for - // compatibility, we are using `window`'s `pagehide` event as a proxy - // for the `window:unload` event that we emit. See: https://github.com/cypress-io/cypress/pull/29525 return Cypress.action('app:window:unload', e) }, onNavigation (...args) { diff --git a/packages/driver/src/cy/commands/agents.ts b/packages/driver/src/cy/commands/agents.ts index 2f35f88f2046..839e768047e7 100644 --- a/packages/driver/src/cy/commands/agents.ts +++ b/packages/driver/src/cy/commands/agents.ts @@ -267,20 +267,10 @@ export default function (Commands, Cypress, cy, state) { return wrap(this, 'spy', theSpy, obj, method) } - const stub = function (obj, method: string, replacerFnOrValue) { + const stub = function (obj, method: string) { // TODO: make the code below work with `packages/runner` type check without casting to `never`. let theStub = sandbox.stub.call(sandbox, obj, method as never) - // sinon 2 changed the stub signature - // this maintains the 3-argument signature so it's not breaking - if (arguments.length === 3) { - if (_.isFunction(replacerFnOrValue)) { - theStub = theStub.callsFake(replacerFnOrValue) - } else { - theStub = theStub.value(replacerFnOrValue) - } - } - return wrap(this, 'stub', theStub, obj, method) } diff --git a/packages/driver/src/cy/commands/helpers/location.ts b/packages/driver/src/cy/commands/helpers/location.ts new file mode 100644 index 000000000000..e82da1c25987 --- /dev/null +++ b/packages/driver/src/cy/commands/helpers/location.ts @@ -0,0 +1,72 @@ +export class UrlNotYetAvailableError extends Error { + constructor () { + const message = 'URL is not yet available' + + super(message) + this.name = 'UrlNotYetAvailableError' + } +} + +export function getUrlFromAutomation (Cypress: Cypress.Cypress, options: Partial = {}) { + const timeout = options.timeout ?? Cypress.config('defaultCommandTimeout') as number + + this.set('timeout', timeout) + + let fullUrlObj: any = null + let automationPromise: Promise | null = null + // need to set a valid type on this + let mostRecentError = new UrlNotYetAvailableError() + + const getUrlFromAutomation = () => { + if (automationPromise) { + return automationPromise + } + + fullUrlObj = null + + automationPromise = Cypress.automation('get:aut:url', {}) + .timeout(timeout) + .then((url) => { + const fullUrlObject = new URL(url) + + fullUrlObj = { + hash: fullUrlObject.hash, + host: fullUrlObject.host, + hostname: fullUrlObject.hostname, + href: fullUrlObject.href, + origin: fullUrlObject.origin, + pathname: fullUrlObject.pathname, + port: fullUrlObject.port, + protocol: fullUrlObject.protocol, + search: fullUrlObject.search, + searchParams: fullUrlObject.searchParams, + } + }) + .catch((err) => mostRecentError = err) + // Pass or fail, we always clear the automationPromise, so future retries know there's no live request to the server. + .finally(() => automationPromise = null) + + return automationPromise + } + + this.set('onFail', (err) => { + // if we are actively retrying or the assertion failed, we want to retry + if (err.name === 'UrlNotYetAvailableError' || err.name === 'AssertionError') { + // tslint:disable-next-line no-floating-promises + getUrlFromAutomation() + } else { + throw err + } + }) + + return () => { + if (fullUrlObj) { + return fullUrlObj + } + + // tslint:disable-next-line no-floating-promises + getUrlFromAutomation() + + throw mostRecentError + } +} diff --git a/packages/driver/src/cy/commands/helpers/window.ts b/packages/driver/src/cy/commands/helpers/window.ts new file mode 100644 index 000000000000..6ae8c15fd8a5 --- /dev/null +++ b/packages/driver/src/cy/commands/helpers/window.ts @@ -0,0 +1,59 @@ +export class TitleNotYetAvailableError extends Error { + constructor () { + const message = 'document.title is not yet available' + + super(message) + this.name = 'TitleNotYetAvailableError' + } +} + +export function getTitleFromAutomation (Cypress: Cypress.Cypress, options: Partial = {}) { + const timeout = options.timeout ?? Cypress.config('defaultCommandTimeout') as number + + this.set('timeout', timeout) + + let documentTitle: any = null + let automationPromise: Promise | null = null + // need to set a valid type on this + let mostRecentError = new TitleNotYetAvailableError() + + const getTitleFromAutomation = () => { + if (automationPromise) { + return automationPromise + } + + documentTitle = null + + automationPromise = Cypress.automation('get:aut:title', {}) + .timeout(timeout) + .then((returnedDocumentTitle) => { + documentTitle = returnedDocumentTitle + }) + .catch((err) => mostRecentError = err) + // Pass or fail, we always clear the automationPromise, so future retries know there's no live request to the server. + .finally(() => automationPromise = null) + + return automationPromise + } + + this.set('onFail', (err, timedOut) => { + // if we are actively retrying or the assertion failed, we want to retry + if (err.name === 'TitleNotYetAvailableError' || err.name === 'AssertionError') { + // tslint:disable-next-line no-floating-promises + getTitleFromAutomation() + } else { + throw err + } + }) + + return () => { + if (documentTitle !== null) { + return documentTitle + } + + // tslint:disable-next-line no-floating-promises + getTitleFromAutomation() + + throw mostRecentError + } +} diff --git a/packages/driver/src/cy/commands/location.ts b/packages/driver/src/cy/commands/location.ts index d68d93eb59aa..43fdeb905423 100644 --- a/packages/driver/src/cy/commands/location.ts +++ b/packages/driver/src/cy/commands/location.ts @@ -1,68 +1,126 @@ import _ from 'lodash' import $errUtils from '../../cypress/error_utils' +import { getUrlFromAutomation } from './helpers/location' -export default (Commands, Cypress, cy) => { - Commands.addQuery('url', function url (options: Partial = {}) { +export function urlQueryCommand (Cypress: Cypress.Cypress, cy: Cypress.Cypress, options: Partial = {}) { + Cypress.log({ message: '', hidden: options.log === false, timeout: options.timeout }) + + // Since webkit doesn't have an automation client and doesn't support cy.origin(), we need to use the legacy method to get the url + if (Cypress.isBrowser('webkit')) { // Make sure the url command can communicate with the AUT. // otherwise, it yields an empty string + // @ts-expect-error Cypress.ensure.commandCanCommunicateWithAUT(cy) this.set('timeout', options.timeout) - Cypress.log({ message: '', hidden: options.log === false, timeout: options.timeout }) - return () => { + // @ts-expect-error const href = cy.getRemoteLocation('href') return options.decode ? decodeURI(href) : href } - }) + } + + const fn = getUrlFromAutomation.bind(this)(Cypress, options) + + return () => { + const fullUrlObj = fn() + + if (fullUrlObj) { + const href = fullUrlObj.href + + return options.decode ? decodeURI(href) : href + } + } +} + +export function hashQueryCommand (Cypress: Cypress.Cypress, cy: Cypress.Cypress, options: Partial = {}) { + Cypress.log({ message: '', hidden: options.log === false, timeout: options.timeout }) - Commands.addQuery('hash', function url (options: Partial = {}) { + // Since webkit doesn't have an automation client and doesn't support cy.origin(), we need to use the legacy method to get the hash + if (Cypress.isBrowser('webkit')) { // Make sure the hash command can communicate with the AUT. + // @ts-expect-error Cypress.ensure.commandCanCommunicateWithAUT(cy) this.set('timeout', options.timeout) - Cypress.log({ message: '', hidden: options.log === false, timeout: options.timeout }) - + // @ts-expect-error return () => cy.getRemoteLocation('hash') + } + + const fn = getUrlFromAutomation.bind(this)(Cypress, options) + + return () => { + const fullUrlObj = fn() + + if (fullUrlObj) { + return fullUrlObj.hash + } + } +} + +export function locationQueryCommand (Cypress: Cypress.Cypress, cy: Cypress.Cypress, key: string, options: Partial = {}) { + // normalize arguments allowing key + options to be undefined + // key can represent the options + + // Make sure the location command can communicate with the AUT. + // otherwise the command just yields 'null' and the reason may be unclear to the user. + if (_.isObject(key)) { + options = key + } + + Cypress.log({ + message: _.isString(key) ? key : '', + hidden: options.log === false, + timeout: options.timeout, }) - Commands.addQuery('location', function location (key, options: Partial = {}) { + // Since webkit doesn't have an automation client and doesn't support cy.origin(), we need to use the legacy method to get the location + if (Cypress.isBrowser('webkit')) { // normalize arguments allowing key + options to be undefined // key can represent the options // Make sure the location command can communicate with the AUT. // otherwise the command just yields 'null' and the reason may be unclear to the user. + //@ts-expect-error Cypress.ensure.commandCanCommunicateWithAUT(cy) - if (_.isObject(key)) { - options = key - } this.set('timeout', options.timeout) + } - Cypress.log({ - message: _.isString(key) ? key : '', - hidden: options.log === false, - timeout: options.timeout, - }) + //@ts-expect-error + const fn = Cypress.isBrowser('webkit') ? cy.getRemoteLocation : getUrlFromAutomation.bind(this)(Cypress, options) - return () => { - const location = cy.getRemoteLocation() - - if (location === '') { - // maybe the page's domain is "invisible" to us - // and we cannot get the location. Return null - // so the command keeps retrying, maybe there is - // a redirect that puts us on the domain we can access - return null - } - - return _.isString(key) - // use existential here because we only want to throw - // on null or undefined values (and not empty strings) - ? location[key] ?? $errUtils.throwErrByPath('location.invalid_key', { args: { key } }) - : location + return () => { + const location = fn() + + if (location === '') { + // maybe the page's domain is "invisible" to us + // and we cannot get the location. Return null + // so the command keeps retrying, maybe there is + // a redirect that puts us on the domain we can access + return null } + + return _.isString(key) + // use existential here because we only want to throw + // on null or undefined values (and not empty strings) + ? location[key] ?? $errUtils.throwErrByPath('location.invalid_key', { args: { key } }) + : location + } +} + +export default (Commands, Cypress, cy) => { + Commands.addQuery('url', function (options: Partial = {}) { + return urlQueryCommand.call(this, Cypress, cy, options) + }) + + Commands.addQuery('hash', function (options: Partial = {}) { + return hashQueryCommand.call(this, Cypress, cy, options) + }) + + Commands.addQuery('location', function (key: string, options: Partial = {}) { + return locationQueryCommand.call(this, Cypress, cy, key, options) }) } diff --git a/packages/driver/src/cy/commands/navigation.ts b/packages/driver/src/cy/commands/navigation.ts index 496d672409c0..4ff70ec2f6a9 100644 --- a/packages/driver/src/cy/commands/navigation.ts +++ b/packages/driver/src/cy/commands/navigation.ts @@ -9,10 +9,9 @@ import { bothUrlsMatchAndOneHasHash } from '../navigation' import { $Location, LocationObject } from '../../cypress/location' import { isRunnerAbleToCommunicateWithAut } from '../../util/commandAUTCommunication' import { whatIsCircular } from '../../util/what-is-circular' - -import type { RunState } from '@packages/types' - import debugFn from 'debug' +import type { RunState } from '@packages/types' +import type { StateFunc } from '../../cypress/state' const debug = debugFn('cypress:driver:navigation') let id = null @@ -444,6 +443,218 @@ interface InternalVisitOptions extends Partial { hasAlreadyVisitedUrl: boolean } +export const reload = (Cypress: Cypress.Cypress, cy: Cypress.Cypress, state: StateFunc, config: Cypress.Config, args: any[]) => { + let forceReload + let userOptions + const throwArgsErr = () => { + $errUtils.throwErrByPath('reload.invalid_arguments') + } + + switch (args.length) { + case 0: + forceReload = false + userOptions = {} + break + + case 1: + if (_.isObject(args[0])) { + userOptions = args[0] + } else { + forceReload = args[0] + } + + break + + case 2: + forceReload = args[0] + userOptions = args[1] + break + + default: + throwArgsErr() + } + + // clear the current timeout + // @ts-expect-error + cy.clearTimeout('reload') + + let cleanup: (() => any) | null = null + const options = _.defaults({}, userOptions, { + log: true, + // @ts-expect-error + timeout: config('pageLoadTimeout'), + }) + + const reload = () => { + return new Promise((resolve) => { + forceReload = forceReload || false + userOptions = userOptions || {} + + if (!_.isObject(userOptions)) { + throwArgsErr() + } + + if (!_.isBoolean(forceReload)) { + throwArgsErr() + } + + options._log = Cypress.log({ timeout: options.timeout, hidden: options.log === false }) + + options._log?.snapshot('before', { next: 'after' }) + + cleanup = () => { + knownCommandCausedInstability = false + + return cy.removeListener('window:load', resolve) + } + + knownCommandCausedInstability = true + + cy.once('window:load', resolve) + + // Since webkit doesn't have an automation client and doesn't support cy.origin(), we need to use the legacy method to reload the page + return Cypress.isBrowser('webkit') ? $utils.locReload(forceReload, state('window')) : Cypress.automation('reload:aut:frame', { + forceReload, + }) + }) + } + + return reload() + .timeout(options.timeout, 'reload') + .catch(Promise.TimeoutError, () => { + return timedOutWaitingForPageLoad(options.timeout, options._log) + }) + .finally(() => { + if (typeof cleanup === 'function') { + cleanup() + } + + if (Cypress.isBrowser('webkit')) { + // Make sure the reload command can communicate with the AUT. + // if we failed for any other reason, we need to display the correct error to the user. + // @ts-expect-error + Cypress.ensure.commandCanCommunicateWithAUT(cy) + } + + return null + }) +} + +export const go = (Cypress: Cypress.Cypress, cy: Cypress.Cypress, state: StateFunc, config: Cypress.Config, numberOrString: number | string, userOptions = {}) => { + const options: Record = _.defaults({}, userOptions, { + log: true, + // @ts-expect-error + timeout: config('pageLoadTimeout'), + }) + + options._log = Cypress.log({ timeout: options.timeout, hidden: options.log === false }) + + const goNumber = (num) => { + if (num === 0) { + $errUtils.throwErrByPath('go.invalid_number', { onFail: options._log }) + } + + let cleanup: (() => any) | null = null + + if (options._log) { + options._log.snapshot('before', { next: 'after' }) + } + + const go = () => { + return Promise.try(() => { + let didUnload = false + + const beforeUnload = () => { + didUnload = true + } + + // clear the current timeout + // @ts-expect-error + cy.clearTimeout() + + cy.once('window:before:unload', beforeUnload) + + const didLoad = new Promise((resolve) => { + cleanup = function () { + cy.removeListener('window:load', resolve) + + return cy.removeListener('window:before:unload', beforeUnload) + } + + return cy.once('window:load', resolve) + }) + + knownCommandCausedInstability = true + + // need to set the attributes of 'go' + // consoleProps here with win + // make sure we resolve our go function + // with the remove window (just like cy.visit) + const retWin = () => state('window') + + // Since webkit doesn't have an automation client and doesn't support cy.origin(), we need to use the legacy method to navigate the history + Cypress.isBrowser('webkit') ? state('window').history.go(num) : Cypress.automation('navigate:aut:history', { historyNumber: num }) + + Promise + .delay(100) + .then(() => { + knownCommandCausedInstability = false + + // if we've didUnload then we know we're + // doing a full page refresh and we need + // to wait until + if (didUnload) { + return didLoad.then(retWin) + } + + return retWin() + }) + }) + } + + return go() + .timeout(options.timeout, 'go') + .catch(Promise.TimeoutError, () => { + return timedOutWaitingForPageLoad(options.timeout, options._log) + }).finally(() => { + if (typeof cleanup === 'function') { + cleanup() + } + + if (Cypress.isBrowser('webkit')) { + // Make sure the reload command can communicate with the AUT. + // if we failed for any other reason, we need to display the correct error to the user. + // @ts-expect-error + Cypress.ensure.commandCanCommunicateWithAUT(cy) + } + + return null + }) + } + + const goString = (str) => { + switch (str) { + case 'forward': return goNumber(1) + case 'back': return goNumber(-1) + default: + return $errUtils.throwErrByPath('go.invalid_direction', { + onFail: options._log, + args: { str }, + }) + } + } + + if (_.isFinite(numberOrString)) { + return goNumber(numberOrString) + } + + if (_.isString(numberOrString)) { + return goString(numberOrString) + } + + return $errUtils.throwErrByPath('go.invalid_argument', { onFail: options._log }) +} + export default (Commands, Cypress, cy, state, config) => { reset() @@ -536,202 +747,11 @@ export default (Commands, Cypress, cy, state, config) => { Commands.addAll({ reload (...args) { - let forceReload - let userOptions - const throwArgsErr = () => { - $errUtils.throwErrByPath('reload.invalid_arguments') - } - - switch (args.length) { - case 0: - forceReload = false - userOptions = {} - break - - case 1: - if (_.isObject(args[0])) { - userOptions = args[0] - } else { - forceReload = args[0] - } - - break - - case 2: - forceReload = args[0] - userOptions = args[1] - break - - default: - throwArgsErr() - } - - // clear the current timeout - cy.clearTimeout('reload') - - let cleanup: (() => any) | null = null - const options = _.defaults({}, userOptions, { - log: true, - timeout: config('pageLoadTimeout'), - }) - - const reload = () => { - return new Promise((resolve) => { - forceReload = forceReload || false - userOptions = userOptions || {} - - if (!_.isObject(userOptions)) { - throwArgsErr() - } - - if (!_.isBoolean(forceReload)) { - throwArgsErr() - } - - options._log = Cypress.log({ timeout: options.timeout, hidden: options.log === false }) - - options._log?.snapshot('before', { next: 'after' }) - - cleanup = () => { - knownCommandCausedInstability = false - - return cy.removeListener('window:load', resolve) - } - - knownCommandCausedInstability = true - - cy.once('window:load', resolve) - - return $utils.locReload(forceReload, state('window')) - }) - } - - return reload() - .timeout(options.timeout, 'reload') - .catch(Promise.TimeoutError, () => { - return timedOutWaitingForPageLoad(options.timeout, options._log) - }) - .finally(() => { - if (typeof cleanup === 'function') { - cleanup() - } - - // Make sure the reload command can communicate with the AUT. - // if we failed for any other reason, we need to display the correct error to the user. - Cypress.ensure.commandCanCommunicateWithAUT(cy) - - return null - }) + return reload.call(this, Cypress, cy, state, config, args) }, go (numberOrString, userOptions = {}) { - const options: Record = _.defaults({}, userOptions, { - log: true, - timeout: config('pageLoadTimeout'), - }) - - options._log = Cypress.log({ timeout: options.timeout, hidden: options.log === false }) - - const win = state('window') - - const goNumber = (num) => { - if (num === 0) { - $errUtils.throwErrByPath('go.invalid_number', { onFail: options._log }) - } - - let cleanup: (() => any) | null = null - - if (options._log) { - options._log.snapshot('before', { next: 'after' }) - } - - const go = () => { - return Promise.try(() => { - let didUnload = false - - const beforeUnload = () => { - didUnload = true - } - - // clear the current timeout - cy.clearTimeout() - - cy.once('window:before:unload', beforeUnload) - - const didLoad = new Promise((resolve) => { - cleanup = function () { - cy.removeListener('window:load', resolve) - - return cy.removeListener('window:before:unload', beforeUnload) - } - - return cy.once('window:load', resolve) - }) - - knownCommandCausedInstability = true - - win.history.go(num) - - // need to set the attributes of 'go' - // consoleProps here with win - // make sure we resolve our go function - // with the remove window (just like cy.visit) - const retWin = () => state('window') - - return Promise - .delay(100) - .then(() => { - knownCommandCausedInstability = false - - // if we've didUnload then we know we're - // doing a full page refresh and we need - // to wait until - if (didUnload) { - return didLoad.then(retWin) - } - - return retWin() - }) - }) - } - - return go() - .timeout(options.timeout, 'go') - .catch(Promise.TimeoutError, () => { - return timedOutWaitingForPageLoad(options.timeout, options._log) - }).finally(() => { - if (typeof cleanup === 'function') { - cleanup() - } - - // Make sure the go command can communicate with the AUT. - Cypress.ensure.commandCanCommunicateWithAUT(cy) - - return null - }) - } - - const goString = (str) => { - switch (str) { - case 'forward': return goNumber(1) - case 'back': return goNumber(-1) - default: - return $errUtils.throwErrByPath('go.invalid_direction', { - onFail: options._log, - args: { str }, - }) - } - } - - if (_.isFinite(numberOrString)) { - return goNumber(numberOrString) - } - - if (_.isString(numberOrString)) { - return goString(numberOrString) - } - - return $errUtils.throwErrByPath('go.invalid_argument', { onFail: options._log }) + return go.call(this, Cypress, cy, state, config, numberOrString, userOptions) }, visit (url, userOptions: Partial = {}) { diff --git a/packages/driver/src/cy/commands/window.ts b/packages/driver/src/cy/commands/window.ts index a42ef1b98472..5af100d26d78 100644 --- a/packages/driver/src/cy/commands/window.ts +++ b/packages/driver/src/cy/commands/window.ts @@ -3,6 +3,9 @@ import Promise from 'bluebird' import $errUtils from '../../cypress/error_utils' import type { Log } from '../../cypress/log' +import { getTitleFromAutomation } from './helpers/window' +import type { StateFunc } from '../../cypress/state' +import type { $Cy } from '../../cypress/cy' const viewports = { 'macbook-16': '1536x960', @@ -40,6 +43,24 @@ interface InternalViewportOptions extends Partial { _log?: Log } +export function getTitleQueryCommand (Cypress: Cypress.Cypress, cy: $Cy, state: StateFunc, options: Partial = {}) { + Cypress.log({ timeout: options.timeout, hidden: options.log === false }) + + // Since webkit doesn't have an automation client and doesn't support cy.origin(), we need to use the legacy method to get the title + if (Cypress.isBrowser('webkit')) { + this.set('timeout', options.timeout) + + // Make sure the window command can communicate with the AUT. + // otherwise, it yields an empty string + //@ts-expect-error + Cypress.ensure.commandCanCommunicateWithAUT(cy) + + return () => (state('document')?.title || '') + } + + return getTitleFromAutomation.bind(this)(Cypress, options) +} + export default (Commands, Cypress, cy, state) => { const defaultViewport: CurrentViewport = _.pick(Cypress.config() as Cypress.Config, 'viewportWidth', 'viewportHeight') @@ -88,19 +109,12 @@ export default (Commands, Cypress, cy, state) => { }) } - Commands.addQuery('title', function title (options: Partial = {}) { - // Make sure the window command can communicate with the AUT. - // otherwise, it yields an empty string - Cypress.ensure.commandCanCommunicateWithAUT(cy) - this.set('timeout', options.timeout) - Cypress.log({ timeout: options.timeout, hidden: options.log === false }) - - return () => (state('document')?.title || '') + Commands.addQuery('title', function (options: Partial = {}) { + return getTitleQueryCommand.call(this, Cypress, cy, state, options) }) Commands.addQuery('window', function windowFn (options: Partial = {}) { // Make sure the window command can communicate with the AUT. - Cypress.ensure.commandCanCommunicateWithAUT(cy) this.set('timeout', options.timeout) Cypress.log({ hidden: options.log === false, diff --git a/packages/driver/src/cy/listeners.ts b/packages/driver/src/cy/listeners.ts index f12ced5e36f0..c31f6456fab0 100644 --- a/packages/driver/src/cy/listeners.ts +++ b/packages/driver/src/cy/listeners.ts @@ -61,7 +61,7 @@ type BoundCallbacks = { onSubmit: (e) => any onLoad: (e) => any onBeforeUnload: (e) => undefined | Promise - onPageHide: (e: PageTransitionEvent) => any + onUnload: (e: BeforeUnloadEvent) => any onNavigation: (...args) => any onAlert: (str) => any onConfirm: (str) => boolean @@ -93,18 +93,12 @@ export const bindToListeners = (contentWindow, callbacks: BoundCallbacks) => { await callbacks.onBeforeUnload(e) }) - // While we must move to pagehide for Chromium, it does not work for our - // needs in Firefox. Until that is addressed, only Chromium uses the pagehide - // event as a proxy for AUT unloads. - - const unloadEvent = Cypress.browser.family === 'chromium' ? 'pagehide' : 'unload' - - addListener(contentWindow, unloadEvent, (e) => { + addListener(contentWindow, 'unload', (e) => { // when we unload we need to remove all of the event listeners removeAllListeners() // else we know to proceed onwards! - callbacks.onPageHide(e) + callbacks.onUnload(e) }) addListener(contentWindow, 'hashchange', (e) => { diff --git a/packages/driver/src/cy/net-stubbing/add-command.ts b/packages/driver/src/cy/net-stubbing/add-command.ts index 3237753c399d..4284288d7e3b 100644 --- a/packages/driver/src/cy/net-stubbing/add-command.ts +++ b/packages/driver/src/cy/net-stubbing/add-command.ts @@ -158,11 +158,6 @@ function validateRouteMatcherOptions (routeMatcher: RouteMatcherOptions): { isVa } } - // @ts-ignore - if (routeMatcher.matchUrlAgainstPath) { - return err(`\`matchUrlAgainstPath\` was removed in Cypress 7.0.0 and should be removed from your tests. Your tests will run the same. For more information, visit https://on.cypress.io/migration-guide`) - } - for (const prop in routeMatcher) { if (!allRouteMatcherFields.includes(prop)) { return err(`An unknown \`RouteMatcher\` property was passed: \`${String(prop)}\`\n\nValid \`RouteMatcher\` properties are: ${allRouteMatcherFields.join(', ')}`) diff --git a/packages/driver/src/cypress/cy.ts b/packages/driver/src/cypress/cy.ts index 516e74436d8f..3b85f5cb5565 100644 --- a/packages/driver/src/cypress/cy.ts +++ b/packages/driver/src/cypress/cy.ts @@ -1130,10 +1130,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert // doesn't trigger a confirmation dialog return undefined }, - onPageHide (e) { - // unload is being actively deprecated/removed by chrome, so for - // compatibility, we are using `window`'s `pagehide` event as a proxy - // for the `window:unload` event that we emit. See: https://github.com/cypress-io/cypress/pull/29525 + onUnload (e) { return cy.Cypress.action('app:window:unload', e) }, onNavigation (...args) { diff --git a/packages/driver/test/unit/cy/commands/helpers/location.spec.ts b/packages/driver/test/unit/cy/commands/helpers/location.spec.ts new file mode 100644 index 000000000000..8165017c71a2 --- /dev/null +++ b/packages/driver/test/unit/cy/commands/helpers/location.spec.ts @@ -0,0 +1,276 @@ +/** + * @vitest-environment jsdom + */ +import { vi, describe, it, expect, beforeEach, Mock, MockedObject } from 'vitest' +import { getUrlFromAutomation, UrlNotYetAvailableError } from '../../../../../src/cy/commands/helpers/location' +import Bluebird from 'bluebird' + +const flushPromises = () => { + return new Promise((resolve) => { + setTimeout(resolve) + }) +} + +describe('cy/commands/helpers/location', () => { + let log: Mock + let mockCypress: MockedObject + let mockLogReturnValue: Cypress.Log + let mockContext: MockedObject + + beforeEach(() => { + log = vi.fn() + + mockCypress = { + // The overloads for `log` don't get applied correctly here + log, + automation: vi.fn(), + // @ts-expect-error - Mock Cypress config object doesn't have all required properties + config: vi.fn(), + } + + mockLogReturnValue = { + id: 'log_id', + end: vi.fn(), + error: vi.fn(), + finish: vi.fn(), + get: vi.fn(), + set: vi.fn(), + snapshot: vi.fn(), + _hasInitiallyLogged: false, + groupEnd: vi.fn(), + } + + mockCypress.log.mockReturnValue(mockLogReturnValue) + + mockContext = { + set: vi.fn(), + } + }) + + describe('getUrlFromAutomation', () => { + describe('options', () => { + it('sets correct timeout option if passed in', async () => { + getUrlFromAutomation.call(mockContext, mockCypress, { + timeout: 2000, + }) + + expect(mockContext.set).toHaveBeenCalledWith('timeout', 2000) + }) + + it('otherwise sets timeout to defaultCommandTimeout', async () => { + mockCypress.config.mockImplementation((key) => { + // @ts-expect-error + if (key === 'defaultCommandTimeout') { + return 1000 + } + + return undefined + }) + + getUrlFromAutomation.call(mockContext, mockCypress, {}) + + expect(mockCypress.config).toHaveBeenCalledWith('defaultCommandTimeout') + expect(mockContext.set).toHaveBeenCalledWith('timeout', 1000) + }) + }) + + describe('leveraging the automation client', () => { + let mockOptions: Cypress.Loggable & Cypress.Timeoutable + + beforeEach(() => { + mockOptions = { + timeout: 1000, + log: false, + } + }) + + it('throws an error when the automation promise has not yet resolved', async () => { + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => undefined) + }) + + const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow('URL is not yet available') + + expect(mockCypress.automation).toHaveBeenCalledWith('get:aut:url', {}) + }) + + it('returns the url object when the automation promise is resolved', async () => { + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve('https://www.example.com#foobar')) + }) + + const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + const url = fn() + + expect(url).toEqual({ + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + searchParams: expect.any(Object), + }) + }) + + it('throws an error when the automation promise is rejected and propagates the error', async () => { + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve, reject) => reject(new Error('The automation client threw an error'))) + }) + + const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + fn() + }).toThrow('The automation client threw an error') + }) + + describe('onFail', () => { + it('retries when the onFail handler is called with a UrlNotYetAvailableError error', async () => { + // when calling the onFail handler with acceptable errors, we will be retrying the automation client + // for this test, the automation client will be called twice + let automationCallCount = 0 + + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + automationCallCount++ + + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve()) + }) + + let onFailHandler: any + + mockContext.set.mockImplementation((key, value) => { + if (key === 'onFail') { + onFailHandler = value + } + }) + + const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + onFailHandler(new UrlNotYetAvailableError()) + }).not.toThrow() + + expect(automationCallCount).toBe(2) + }) + + it('retries when the onFail handler is called with a UrlNotYetAvailableError error', async () => { + // when calling the onFail handler with acceptable errors, we will be retrying the automation client + // for this test, the automation client will be called twice + let automationCallCount = 0 + + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + automationCallCount++ + + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve()) + }) + + let onFailHandler: any + + mockContext.set.mockImplementation((key, value) => { + if (key === 'onFail') { + onFailHandler = value + } + }) + + const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + const mockAssertionError = new Error('The assertion failed') + + mockAssertionError.name = 'AssertionError' + + onFailHandler(mockAssertionError) + }).not.toThrow() + + expect(automationCallCount).toBe(2) + }) + + it('fails when the onFail handler is called with an error that is not a UrlNotYetAvailableError or AssertionError', async () => { + // when calling the onFail handler with unacceptable errrors, we will not be retrying the automation client + // for this test, the automation client will be called once + let automationCallCount = 0 + + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + automationCallCount++ + + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve()) + }) + + let onFailHandler: any + + mockContext.set.mockImplementation((key, value) => { + if (key === 'onFail') { + onFailHandler = value + } + }) + + const fn = getUrlFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + const mockAssertionError = new Error('Something else') + + onFailHandler(mockAssertionError) + }).toThrow('Something else') + + expect(automationCallCount).toBe(1) + }) + }) + }) + }) +}) diff --git a/packages/driver/test/unit/cy/commands/helpers/window.spec.ts b/packages/driver/test/unit/cy/commands/helpers/window.spec.ts new file mode 100644 index 000000000000..8a2794d1dda1 --- /dev/null +++ b/packages/driver/test/unit/cy/commands/helpers/window.spec.ts @@ -0,0 +1,265 @@ +/** + * @vitest-environment jsdom + */ +import { vi, describe, it, expect, beforeEach, Mock, MockedObject } from 'vitest' +import { getTitleFromAutomation, TitleNotYetAvailableError } from '../../../../../src/cy/commands/helpers/window' +import Bluebird from 'bluebird' + +const flushPromises = () => { + return new Promise((resolve) => { + setTimeout(resolve) + }) +} + +describe('cy/commands/helpers/windows', () => { + let log: Mock + let mockCypress: MockedObject + let mockLogReturnValue: Cypress.Log + let mockContext: MockedObject + + beforeEach(() => { + log = vi.fn() + + mockCypress = { + // The overloads for `log` don't get applied correctly here + log, + automation: vi.fn(), + // @ts-expect-error - Mock Cypress config object doesn't have all required properties + config: vi.fn(), + } + + mockLogReturnValue = { + id: 'log_id', + end: vi.fn(), + error: vi.fn(), + finish: vi.fn(), + get: vi.fn(), + set: vi.fn(), + snapshot: vi.fn(), + _hasInitiallyLogged: false, + groupEnd: vi.fn(), + } + + mockCypress.log.mockReturnValue(mockLogReturnValue) + + mockContext = { + set: vi.fn(), + } + }) + + describe('getTitleFromAutomation', () => { + describe('options', () => { + it('sets correct timeout option if passed in', async () => { + getTitleFromAutomation.call(mockContext, mockCypress, { + timeout: 2000, + }) + + expect(mockContext.set).toHaveBeenCalledWith('timeout', 2000) + }) + + it('otherwise sets timeout to defaultCommandTimeout', async () => { + mockCypress.config.mockImplementation((key) => { + // @ts-expect-error + if (key === 'defaultCommandTimeout') { + return 1000 + } + + return undefined + }) + + getTitleFromAutomation.call(mockContext, mockCypress, {}) + + expect(mockCypress.config).toHaveBeenCalledWith('defaultCommandTimeout') + expect(mockContext.set).toHaveBeenCalledWith('timeout', 1000) + }) + }) + + describe('leveraging the automation client', () => { + let mockOptions: Cypress.Loggable & Cypress.Timeoutable + + beforeEach(() => { + mockOptions = { + timeout: 1000, + log: false, + } + }) + + it('throws an error when the automation promise has not yet resolved', async () => { + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => undefined) + }) + + const fn = getTitleFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow('document.title is not yet available') + + expect(mockCypress.automation).toHaveBeenCalledWith('get:aut:title', {}) + }) + + it('returns the document\'s title when the automation promise is resolved', async () => { + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve('This is the frame title')) + }) + + const fn = getTitleFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + const title = fn() + + expect(title).toEqual('This is the frame title') + }) + + it('throws an error when the automation promise is rejected and propagates the error', async () => { + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve, reject) => reject(new Error('The automation client threw an error'))) + }) + + const fn = getTitleFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + fn() + }).toThrow('The automation client threw an error') + }) + + describe('onFail', () => { + it('retries when the onFail handler is called with a TitleNotYetAvailableError error', async () => { + // when calling the onFail handler with acceptable errors, we will be retrying the automation client + // for this test, the automation client will be called twice + let automationCallCount = 0 + + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + automationCallCount++ + + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve()) + }) + + let onFailHandler: any + + mockContext.set.mockImplementation((key, value) => { + if (key === 'onFail') { + onFailHandler = value + } + }) + + const fn = getTitleFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + onFailHandler(new TitleNotYetAvailableError()) + }).not.toThrow() + + expect(automationCallCount).toBe(2) + }) + + it('retries when the onFail handler is called with a TitleNotYetAvailableError error', async () => { + // when calling the onFail handler with acceptable errors, we will be retrying the automation client + // for this test, the automation client will be called twice + let automationCallCount = 0 + + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + automationCallCount++ + + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve()) + }) + + let onFailHandler: any + + mockContext.set.mockImplementation((key, value) => { + if (key === 'onFail') { + onFailHandler = value + } + }) + + const fn = getTitleFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + const mockAssertionError = new Error('The assertion failed') + + mockAssertionError.name = 'AssertionError' + + onFailHandler(mockAssertionError) + }).not.toThrow() + + expect(automationCallCount).toBe(2) + }) + + it('fails when the onFail handler is called with an error that is not a TitleNotYetAvailableError or AssertionError', async () => { + // when calling the onFail handler with unacceptable errrors, we will not be retrying the automation client + // for this test, the automation client will be called once + let automationCallCount = 0 + + // @ts-expect-error + mockCypress.automation.mockImplementation(() => { + automationCallCount++ + + // no-op promise to simulate the waiting for the automation client + return new Bluebird.Promise((resolve) => resolve()) + }) + + let onFailHandler: any + + mockContext.set.mockImplementation((key, value) => { + if (key === 'onFail') { + onFailHandler = value + } + }) + + const fn = getTitleFromAutomation.call(mockContext, mockCypress, mockOptions) + + expect(() => { + fn() + }).toThrow() + + // flush the microtask queue so we have a url value next time we call fn() + await flushPromises() + + expect(() => { + const mockAssertionError = new Error('Something else') + + onFailHandler(mockAssertionError) + }).toThrow('Something else') + + expect(automationCallCount).toBe(1) + }) + }) + }) + }) +}) diff --git a/packages/driver/test/unit/cy/commands/location.spec.ts b/packages/driver/test/unit/cy/commands/location.spec.ts new file mode 100644 index 000000000000..8546e0dd9991 --- /dev/null +++ b/packages/driver/test/unit/cy/commands/location.spec.ts @@ -0,0 +1,374 @@ +/** + * @vitest-environment jsdom + */ +import { vi, describe, it, expect, beforeEach, MockedObject } from 'vitest' +import { urlQueryCommand, hashQueryCommand, locationQueryCommand } from '../../../../src/cy/commands/location' +import { getUrlFromAutomation } from '../../../../src/cy/commands/helpers/location' +import type { $Cy } from '../../../../src/cypress/cy' + +vi.mock('../../../../src/cy/commands/helpers/location', async () => { + return { + getUrlFromAutomation: vi.fn(), + } +}) + +describe('cy/commands/location', () => { + let mockCypress: MockedObject + let mockCy: MockedObject<$Cy> + let mockContext: MockedObject + + beforeEach(() => { + mockCypress = { + log: vi.fn(), + automation: vi.fn(), + isBrowser: vi.fn(), + ensure: { + // @ts-expect-error + commandCanCommunicateWithAUT: vi.fn(), + }, + // @ts-expect-error + config: vi.fn(), + } + + // @ts-expect-error + mockCy = { + getRemoteLocation: vi.fn(), + } + + mockContext = { + set: vi.fn(), + } + + //@ts-expect-error + getUrlFromAutomation.mockReset() + }) + + describe('url', () => { + describe('chromium/firefox', () => { + it('returns the url href from the automation client', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return { + href: 'https://www.example.com/#foobar', + } + }) + + const url = urlQueryCommand.call(mockContext, mockCypress, mockCy, {})() + + expect(url).toBe('https://www.example.com/#foobar') + + expect(getUrlFromAutomation).toHaveBeenCalledOnce() + + expect(mockCy.getRemoteLocation).not.toHaveBeenCalled() + }) + + it('supports the decode option', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return { + href: 'https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B', + } + }) + + const url = urlQueryCommand.call(mockContext, mockCypress, mockCy, { + decode: true, + })() + + expect(url).toBe('https://mozilla.org/?x=шеллы') + + expect(getUrlFromAutomation).toHaveBeenCalledOnce() + }) + }) + + describe('webkit', () => { + beforeEach(() => { + mockCypress.isBrowser.mockImplementation((browserName) => { + if (browserName === 'webkit') { + return true + } + + return false + }) + }) + + it('does not use the automation client if the browser is webkit', () => { + mockCy.getRemoteLocation.mockImplementation(() => { + return 'https://www.example.com/#foobar' + }) + + const url = urlQueryCommand.call(mockContext, mockCypress, mockCy, { timeout: 10000 })() + + expect(url).toBe('https://www.example.com/#foobar') + + // @ts-expect-error + expect(mockCypress.ensure.commandCanCommunicateWithAUT).toHaveBeenCalledOnce() + + expect(mockContext.set).toHaveBeenCalledWith('timeout', 10000) + + expect(getUrlFromAutomation).not.toHaveBeenCalled() + + expect(mockCy.getRemoteLocation).toHaveBeenCalledWith('href') + }) + + it('supports the decode option', () => { + mockCy.getRemoteLocation.mockImplementation(() => { + return 'https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B' + }) + + const url = urlQueryCommand.call(mockContext, mockCypress, mockCy, { + decode: true, + })() + + expect(url).toBe('https://mozilla.org/?x=шеллы') + + // @ts-expect-error + expect(mockCypress.ensure.commandCanCommunicateWithAUT).toHaveBeenCalledOnce() + + expect(getUrlFromAutomation).not.toHaveBeenCalled() + + expect(mockCy.getRemoteLocation).toHaveBeenCalledWith('href') + }) + }) + }) + + describe('hash', () => { + describe('chromium/firefox', () => { + it('returns the hash of the url from the automation client', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return { + hash: 'foobar', + } + }) + + const hash = hashQueryCommand.call(mockContext, mockCypress, mockCy, {})() + + expect(hash).toBe('foobar') + + expect(getUrlFromAutomation).toHaveBeenCalledOnce() + + expect(mockCy.getRemoteLocation).not.toHaveBeenCalled() + }) + }) + + describe('webkit', () => { + beforeEach(() => { + mockCypress.isBrowser.mockImplementation((browserName) => { + if (browserName === 'webkit') { + return true + } + + return false + }) + }) + + it('does not use the automation client if the browser is webkit', () => { + mockCy.getRemoteLocation.mockImplementation(() => { + return 'foobar' + }) + + const hash = hashQueryCommand.call(mockContext, mockCypress, mockCy, { timeout: 10000 })() + + expect(hash).toBe('foobar') + + // @ts-expect-error + expect(mockCypress.ensure.commandCanCommunicateWithAUT).toHaveBeenCalledOnce() + + // @ts-expect-error + expect(mockContext.set).toHaveBeenCalledWith('timeout', 10000) + + expect(getUrlFromAutomation).not.toHaveBeenCalled() + + expect(mockCy.getRemoteLocation).toHaveBeenCalledWith('hash') + }) + }) + }) + + describe('location', () => { + describe('chromium/firefox', () => { + it('returns the location of the url from the automation client', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return { + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + searchParams: expect.any(Object), + } + }) + + const urlObj = locationQueryCommand.call(mockContext, mockCypress, mockCy, undefined, {})() + + expect(urlObj).toEqual({ + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + searchParams: expect.any(Object), + }) + + expect(getUrlFromAutomation).toHaveBeenCalledOnce() + + expect(mockCypress.log).toHaveBeenCalledWith({ + message: '', + hidden: false, + timeout: undefined, + }) + + expect(mockCy.getRemoteLocation).not.toHaveBeenCalled() + }) + + it('works with a string key', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return { + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + searchParams: expect.any(Object), + } + }) + + const hash = locationQueryCommand.call(mockContext, mockCypress, mockCy, 'hash', {})() + + expect(hash).toEqual('#foobar') + + expect(getUrlFromAutomation).toHaveBeenCalledOnce() + + expect(mockCypress.log).toHaveBeenCalledWith({ + message: 'hash', + hidden: false, + timeout: undefined, + }) + + expect(mockCy.getRemoteLocation).not.toHaveBeenCalled() + }) + + it('returns null if the location is empty', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return '' + }) + + const urlObj = locationQueryCommand.call(mockContext, mockCypress, mockCy, undefined, {})() + + expect(urlObj).toEqual(null) + + expect(getUrlFromAutomation).toHaveBeenCalledOnce() + + expect(mockCypress.log).toHaveBeenCalledWith({ + message: '', + hidden: false, + timeout: undefined, + }) + + expect(mockCy.getRemoteLocation).not.toHaveBeenCalled() + }) + + it('throws if the string key is invalid', () => { + // @ts-expect-error + getUrlFromAutomation.mockReturnValue(() => { + return { + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + searchParams: expect.any(Object), + } + }) + + expect(() => { + locationQueryCommand.call(mockContext, mockCypress, mockCy, 'doesnotexist', {})() + }).toThrow('Location object does not have key: `doesnotexist`') + }) + }) + + describe('webkit', () => { + beforeEach(() => { + mockCypress.isBrowser.mockImplementation((browserName) => { + if (browserName === 'webkit') { + return true + } + + return false + }) + }) + + it('does not use the automation client if the browser is webkit', () => { + mockCy.getRemoteLocation.mockImplementation(() => { + // NOTE: this is the legacy API of remote location, which is fairly close to that of the automation client + return { + auth: '', + authObj: '', + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + superDomainOrigin: 'example.com', + superDomain: 'example.com', + } + }) + + const urlLegacyObj = locationQueryCommand.call(mockContext, mockCypress, mockCy, undefined, { timeout: 10000 })() + + expect(urlLegacyObj).toEqual({ + auth: '', + authObj: '', + protocol: 'https:', + host: 'www.example.com', + hostname: 'www.example.com', + hash: '#foobar', + search: '', + pathname: '/', + port: '', + origin: 'https://www.example.com', + href: 'https://www.example.com/#foobar', + superDomainOrigin: 'example.com', + superDomain: 'example.com', + }) + + // @ts-expect-error + expect(mockCypress.ensure.commandCanCommunicateWithAUT).toHaveBeenCalledOnce() + + expect(mockContext.set).toHaveBeenCalledWith('timeout', 10000) + + expect(mockCypress.log).toHaveBeenCalledWith({ + message: '', + hidden: false, + timeout: 10000, + }) + + expect(getUrlFromAutomation).not.toHaveBeenCalled() + + expect(mockCy.getRemoteLocation).toHaveBeenCalledWith() + }) + }) + }) +}) diff --git a/packages/driver/test/unit/cy/commands/navigation.spec.ts b/packages/driver/test/unit/cy/commands/navigation.spec.ts new file mode 100644 index 000000000000..34944fe30230 --- /dev/null +++ b/packages/driver/test/unit/cy/commands/navigation.spec.ts @@ -0,0 +1,181 @@ +/** + * @vitest-environment jsdom + */ +import { vi, describe, it, expect, beforeEach, MockedObject } from 'vitest' +import { go, reload } from '../../../../src/cy/commands/navigation' +import $utils from '../../../../src/cypress/utils' +import type{ $Cy } from '../../../../src/cypress/cy' + +vi.mock('../../../../src/cypress/utils', async () => { + const original = await vi.importActual('../../../../src/cypress/utils') + + return { + default: { + // @ts-expect-error + ...original.default, + locReload: vi.fn(), + }, + } +}) + +describe('cy/commands/navigation', () => { + let mockCypress: MockedObject + let mockCy: MockedObject<$Cy> + let mockContext: MockedObject + let mockState: MockedObject + + beforeEach(() => { + mockCypress = { + log: vi.fn(), + automation: vi.fn(), + isBrowser: vi.fn(), + ensure: { + // @ts-expect-error + commandCanCommunicateWithAUT: vi.fn(), + }, + // @ts-expect-error + config: vi.fn(), + } + + mockCy = { + clearTimeout: vi.fn(), + // @ts-expect-error + once: vi.fn(), + // @ts-expect-error + removeListener: vi.fn(), + } + + mockState = vi.fn() + + mockContext = { + set: vi.fn(), + } + + mockCypress.config.mockImplementation((key) => { + //@ts-expect-error + if (key === 'pageLoadTimeout') { + return 10000 + } + }) + + //@ts-expect-error + $utils.locReload.mockReset() + }) + + describe('reload', () => { + describe('chromium/firefox', () => { + it('sends the reload:aut:frame event to the backend via the automation client', () => { + reload.call(mockContext, mockCypress, mockCy, mockState, mockCypress.config, [true]) + + expect(mockCypress.automation).toHaveBeenCalledWith('reload:aut:frame', { + forceReload: true, + }) + + expect(mockCypress.log).toHaveBeenCalledWith({ + hidden: false, + timeout: 10000, + }) + + expect($utils.locReload).not.toHaveBeenCalled() + }) + + describe('webkit', () => { + beforeEach(() => { + mockCypress.isBrowser.mockImplementation((browserName) => { + if (browserName === 'webkit') { + return true + } + + return false + }) + }) + + it('does not use the automation client if the browser is webkit', () => { + let mockWindow = {} + + mockState.mockImplementation((key) => { + if (key === 'window') { + return mockWindow + } + }) + + reload.call(mockContext, mockCypress, mockCy, mockState, mockCypress.config, [true]) + + expect(mockCypress.log).toHaveBeenCalledWith({ + hidden: false, + timeout: 10000, + }) + + expect(mockCypress.automation).not.toHaveBeenCalled() + + expect(mockCypress.log).toHaveBeenCalledWith({ + hidden: false, + timeout: 10000, + }) + + expect($utils.locReload).toHaveBeenCalledWith(true, mockWindow) + }) + }) + }) + }) + + describe('go', () => { + let mockWindow + + beforeEach(() => { + mockWindow = { + history: { + go: vi.fn(), + }, + } + + mockState.mockImplementation((key) => { + if (key === 'window') { + return mockWindow + } + }) + }) + + describe('chromium/firefox', () => { + it('sends the navigate:aut:history event to the backend via the automation client', () => { + go.call(mockContext, mockCypress, mockCy, mockState, mockCypress.config, -1, {}) + + expect(mockCypress.automation).toHaveBeenCalledWith('navigate:aut:history', { + historyNumber: -1, + }) + + expect(mockCypress.log).toHaveBeenCalledWith({ + hidden: false, + timeout: 10000, + }) + + expect(mockWindow.history.go).not.toHaveBeenCalled() + }) + + describe('webkit', () => { + beforeEach(() => { + mockCypress.isBrowser.mockImplementation((browserName) => { + if (browserName === 'webkit') { + return true + } + + return false + }) + }) + + it('does not use the automation client if the browser is webkit', () => { + go.call(mockContext, mockCypress, mockCy, mockState, mockCypress.config, -1, {}) + + expect(mockCypress.log).toHaveBeenCalledWith({ + hidden: false, + timeout: 10000, + }) + + expect(mockCypress.automation).not.toHaveBeenCalled() + + expect(mockWindow.history.go).toHaveBeenCalledWith(-1) + }) + }) + }) + }) +}) diff --git a/packages/driver/test/unit/cy/commands/window.spec.ts b/packages/driver/test/unit/cy/commands/window.spec.ts new file mode 100644 index 000000000000..1fbc25367b86 --- /dev/null +++ b/packages/driver/test/unit/cy/commands/window.spec.ts @@ -0,0 +1,101 @@ +/** + * @vitest-environment jsdom + */ +import { vi, describe, it, expect, beforeEach, MockedObject } from 'vitest' +import { getTitleQueryCommand } from '../../../../src/cy/commands/window' +import { getTitleFromAutomation } from '../../../../src/cy/commands/helpers/window' +import type { StateFunc } from '../../../../src/cypress/state' +import type { $Cy } from '../../../../src/cypress/cy' + +vi.mock('../../../../src/cy/commands/helpers/window', async () => { + return { + getTitleFromAutomation: vi.fn(), + } +}) + +describe('cy/commands/window', () => { + let mockCypress: MockedObject + let mockState: MockedObject + let mockContext: MockedObject + let mockCy: MockedObject<$Cy> + + beforeEach(() => { + mockCypress = { + log: vi.fn(), + automation: vi.fn(), + isBrowser: vi.fn(), + ensure: { + // @ts-expect-error + commandCanCommunicateWithAUT: vi.fn(), + }, + // @ts-expect-error + config: vi.fn(), + } + + // @ts-expect-error + mockCy = {} + + // @ts-expect-error + mockState = vi.fn() + + mockContext = { + set: vi.fn(), + } + + //@ts-expect-error + getTitleFromAutomation.mockReset() + }) + + describe('title', () => { + describe('chromium/firefox', () => { + it('returns the title from the automation client', () => { + // @ts-expect-error + getTitleFromAutomation.mockReturnValue(() => 'This is the frame title') + + const title = getTitleQueryCommand.call(mockContext, mockCypress, mockCy, mockState, {})() + + expect(title).toBe('This is the frame title') + + expect(getTitleFromAutomation).toHaveBeenCalledOnce() + + expect(mockState).not.toHaveBeenCalled() + }) + }) + + describe('webkit', () => { + beforeEach(() => { + mockCypress.isBrowser.mockImplementation((browserName) => { + if (browserName === 'webkit') { + return true + } + + return false + }) + }) + + it('does not use the automation client if the browser is webkit', () => { + // @ts-expect-error + mockState.mockImplementation((key) => { + if (key === 'document') { + return { title: 'This is the frame title' } + } + }) + + const title = getTitleQueryCommand.call(mockContext, mockCypress, mockCy, mockState, { + timeout: 10000, + })() + + expect(title).toBe('This is the frame title') + + // @ts-expect-error + expect(mockCypress.ensure.commandCanCommunicateWithAUT).toHaveBeenCalledOnce() + + expect(mockContext.set).toHaveBeenCalledWith('timeout', 10000) + + expect(getTitleFromAutomation).not.toHaveBeenCalled() + + expect(mockState).toHaveBeenCalledWith('document') + }) + }) + }) +}) diff --git a/packages/errors/__snapshot-html__/BROWSER_NOT_FOUND_BY_NAME - canary.html b/packages/errors/__snapshot-html__/BROWSER_NOT_FOUND_BY_NAME - canary.html index f3176b9f8ef1..8fc4dce04743 100644 --- a/packages/errors/__snapshot-html__/BROWSER_NOT_FOUND_BY_NAME - canary.html +++ b/packages/errors/__snapshot-html__/BROWSER_NOT_FOUND_BY_NAME - canary.html @@ -60,9 +60,5 @@ - edge - edge:beta - edge:canary - - edge:dev - -Note: In Cypress version 4.0.0, Canary must be launched as chrome:canary, not canary. - -See https://on.cypress.io/migration-guide for more information on breaking changes in 4.0.0. + - edge:dev \ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CANNOT_RECORD_NO_PROJECT_ID.html b/packages/errors/__snapshot-html__/CANNOT_RECORD_NO_PROJECT_ID.html index c682601b3f0d..b41d0bb1156a 100644 --- a/packages/errors/__snapshot-html__/CANNOT_RECORD_NO_PROJECT_ID.html +++ b/packages/errors/__snapshot-html__/CANNOT_RECORD_NO_PROJECT_ID.html @@ -36,7 +36,7 @@
You passed the --record flag but this project has not been setup to record.
 
-This project is missing the projectId inside of: /path/to/cypress.json
+This project is missing the projectId inside of: /path/to/cypress.config.js
 
 We cannot uniquely identify this project without this id.
 
diff --git a/packages/errors/__snapshot-html__/CDP_FIREFOX_DEPRECATED.html b/packages/errors/__snapshot-html__/CDP_FIREFOX_DEPRECATED.html
deleted file mode 100644
index ba2e10ca512a..000000000000
--- a/packages/errors/__snapshot-html__/CDP_FIREFOX_DEPRECATED.html
+++ /dev/null
@@ -1,38 +0,0 @@
-
-    
-    
-      
-      
-    
-    
-    
-    
-  
-    
-    
Since Firefox 129, Chrome DevTools Protocol (CDP) has been deprecated in Firefox. In Firefox 135 and above, Cypress defaults to automating the Firefox browser with WebDriver BiDi. Cypress will no longer support CDP within Firefox in the future and is planned for removal in Cypress 15.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html b/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html index aa52d1cbda9d..dd4b50111fc6 100644 --- a/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html +++ b/packages/errors/__snapshot-html__/CLOUD_PROJECT_NOT_FOUND.html @@ -36,7 +36,7 @@
We could not find a Cypress Cloud project with the projectId: project-id-123
 
-This projectId came from your /path/to/cypress.json file or an environment variable.
+This projectId came from your /path/to/cypress.config.js file or an environment variable.
 
 Please log into Cypress Cloud and find your project.
 
diff --git a/packages/errors/__snapshot-html__/COMPONENT_FOLDER_REMOVED.html b/packages/errors/__snapshot-html__/COMPONENT_FOLDER_REMOVED.html
deleted file mode 100644
index 5a67e5f799a6..000000000000
--- a/packages/errors/__snapshot-html__/COMPONENT_FOLDER_REMOVED.html
+++ /dev/null
@@ -1,48 +0,0 @@
-
-    
-    
-      
-      
-    
-    
-    
-    
-  
-    
-    
The componentFolder configuration option is now invalid when set on the config object in Cypress version 10.0.0.
-
-It is now renamed to specPattern and configured separately as a component testing property: component.specPattern
-
-{
-  component: {
-    specPattern: '...',
-  },
-}
-
-https://on.cypress.io/migration-guide
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_DEV_START_EVENT.html b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_DEV_START_EVENT.html deleted file mode 100644 index cd88c8945491..000000000000 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_DEV_START_EVENT.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - -
To run component tests, Cypress needs you to configure the dev-server:start event.
-
-Please update this file: /path/to/plugins/file.js
-
-module.exports = (on, config) => {
-  on('dev-server:start', () => {
-    // start dev server here
-    return startDevServer(...)
-  }
-}
-
-https://on.cypress.io/component-testing
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG.html b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG.html index 27000687e8c4..e4fd80194d83 100644 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG.html +++ b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG.html @@ -34,9 +34,9 @@ -
The specPattern configuration option is now invalid when set from the root of the config object in Cypress version 10.0.0.
+    
The specPattern configuration option is invalid when set from the root of the config object.
 
-It is now configured separately as a testing type property: e2e.specPattern and component.specPattern
+Set it within a testing type property: e2e.specPattern and component.specPattern
 
 {
   e2e: {
@@ -45,7 +45,5 @@
   component: {
     specPattern: '...',
   },
-}
-
-https://on.cypress.io/migration-guide
+}
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT.html b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT.html index a2e8b2fe1365..05a70bb0cdb7 100644 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT.html +++ b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT.html @@ -34,15 +34,13 @@ -
The indexHtmlFile configuration option is now invalid when set from the root of the config object in Cypress version 10.0.0.
+    
The indexHtmlFile configuration option is invalid when set from the root of the config object.
 
-It is now configured separately as a testing type property: component.indexHtmlFile
+Set it within a testing type property: component.indexHtmlFile
 
 {
   component: {
     indexHtmlFile: '...',
   }
-}
-
-https://on.cypress.io/migration-guide
+}
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_E2E.html b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_E2E.html index a1453f31d8a6..376eb5f580f9 100644 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_E2E.html +++ b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_ROOT_CONFIG_E2E.html @@ -34,15 +34,13 @@ -
The baseUrl configuration option is now invalid when set from the root of the config object in Cypress version 10.0.0.
+    
The baseUrl configuration option is invalid when set from the root of the config object.
 
-It is now configured separately as a testing type property: e2e.baseUrl
+Set it within a testing type property: e2e.baseUrl
 
 {
   e2e: {
     baseUrl: '...',
   }
-}
-
-https://on.cypress.io/migration-guide
+}
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT.html b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT.html index bbcb84ee39bf..13fd6906b4c8 100644 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT.html +++ b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT.html @@ -42,7 +42,5 @@ e2e: { baseUrl: '...', } -} - -https://on.cypress.io/migration-guide
+}
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E.html b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E.html index 1dfb573ebd80..7878f398fc0f 100644 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E.html +++ b/packages/errors/__snapshot-html__/CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E.html @@ -42,7 +42,5 @@ e2e: { indexHtmlFile: '...', } -} - -https://on.cypress.io/migration-guide
+}
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_MIGRATION_NEEDED.html b/packages/errors/__snapshot-html__/CONFIG_FILE_MIGRATION_NEEDED.html deleted file mode 100644 index 384c9e6a36b9..000000000000 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_MIGRATION_NEEDED.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - -
There is a cypress.json file at the path: /path/to/projectRoot
-
-Cypress version 10.0.0 no longer supports cypress.json.
-
-Please run cypress open to launch the migration tool to migrate to cypress.config.{js,ts,mjs,cjs}.
-
-https://on.cypress.io/migration-guide
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_FILE_NOT_FOUND.html b/packages/errors/__snapshot-html__/CONFIG_FILE_NOT_FOUND.html index f2cf222723ba..fae8c30497f5 100644 --- a/packages/errors/__snapshot-html__/CONFIG_FILE_NOT_FOUND.html +++ b/packages/errors/__snapshot-html__/CONFIG_FILE_NOT_FOUND.html @@ -36,5 +36,5 @@
Could not find a Cypress configuration file.
 
-We looked but did not find a cypress.json file in this folder: /path/to/project/root
+We looked but did not find a cypress.config.js file in this folder: /path/to/project/root
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidArray.html b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidArray.html index d9c965fe506b..80078cd1b65b 100644 --- a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidArray.html +++ b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidArray.html @@ -34,7 +34,7 @@ -
Your configFile at cypress.json set an invalid value:
+    
Your configFile at cypress.config.js set an invalid value:
 
 Expected defaultCommandTimeout to be a number.
 
diff --git a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidObject.html b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidObject.html
index c512125bd392..7bc565be889a 100644
--- a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidObject.html	
+++ b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidObject.html	
@@ -34,7 +34,7 @@
     
   
     
-    
Your configFile at cypress.json set an invalid value:
+    
Your configFile at cypress.config.js set an invalid value:
 
 Expected defaultCommandTimeout to be a number.
 
diff --git a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidString.html b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidString.html
index cf809bf98794..683e42e09095 100644
--- a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidString.html	
+++ b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - invalidString.html	
@@ -34,7 +34,7 @@
     
   
     
-    
Your configFile at cypress.json set an invalid value:
+    
Your configFile at cypress.config.js set an invalid value:
 
 Expected defaultCommandTimeout to be a number.
 
diff --git a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - list.html b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - list.html
index 8bb8df4611ed..5f9ae2353e01 100644
--- a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - list.html	
+++ b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR - list.html	
@@ -34,7 +34,7 @@
     
   
     
-    
Your configFile at cypress.json set an invalid value:
+    
Your configFile at cypress.config.js set an invalid value:
 
 The error occurred while validating the browsers list.
 
diff --git a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR.html b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR.html
index 93838e9fc1b7..bffbd09b96ad 100644
--- a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR.html
+++ b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_ERROR.html
@@ -34,7 +34,7 @@
     
   
     
-    
Your configFile at cypress.json set an invalid value:
+    
Your configFile at cypress.config.js set an invalid value:
 
 Expected defaultCommandTimeout to be a number.
 
diff --git a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_MSG_ERROR.html b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_MSG_ERROR.html
index bd6b08ad784e..6fce3bfc58d1 100644
--- a/packages/errors/__snapshot-html__/CONFIG_VALIDATION_MSG_ERROR.html
+++ b/packages/errors/__snapshot-html__/CONFIG_VALIDATION_MSG_ERROR.html
@@ -34,7 +34,7 @@
     
   
     
-    
Your configFile as cypress.json set an invalid value:
+    
Your configFile as cypress.config.js set an invalid value:
 
 `something` was not right
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_COMPONENT_TESTING_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_COMPONENT_TESTING_REMOVED.html deleted file mode 100644 index 349eec66a283..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_COMPONENT_TESTING_REMOVED.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - -
The experimentalComponentTesting configuration option was removed in Cypress version 7.0.0.
-
-Please remove this flag from: /path/to/cypress.config.js
-
-Component Testing is now a supported testing type. You can run your component tests with:
-
-  $ cypress open --component
-
-https://on.cypress.io/migration-guide
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_NETWORK_STUBBING_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_NETWORK_STUBBING_REMOVED.html deleted file mode 100644 index c127cab16de2..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_NETWORK_STUBBING_REMOVED.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - -
The experimentalNetworkStubbing configuration option was removed in Cypress version 6.0.0.
-
-It is no longer necessary for using cy.intercept(). You can safely remove this option from your config.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_RUN_EVENTS_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_RUN_EVENTS_REMOVED.html deleted file mode 100644 index d16b743fd4c9..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_RUN_EVENTS_REMOVED.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - -
The experimentalRunEvents configuration option was removed in Cypress version 6.7.0. It is no longer necessary when listening to run events in the plugins file.
-
-You can safely remove this option from your config.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_SAMESITE_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_SAMESITE_REMOVED.html deleted file mode 100644 index 1aa0127b6c23..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_SAMESITE_REMOVED.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - -
The experimentalGetCookiesSameSite configuration option was removed in Cypress version 5.0.0.
-
-Returning the sameSite property is now the default behavior of the cy.cookie commands.
-
-You can safely remove this option from your config.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html deleted file mode 100644 index 4c61605dc56c..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_SESSION_SUPPORT_REMOVED.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - -
The experimentalSessionSupport configuration option was removed in Cypress version 9.6.0.
-
-You can safely remove this option from your config.
-
-https://on.cypress.io/session
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_SHADOW_DOM_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_SHADOW_DOM_REMOVED.html deleted file mode 100644 index 383ed62ad20b..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_SHADOW_DOM_REMOVED.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - -
The experimentalShadowDomSupport configuration option was removed in Cypress version 5.2.0. It is no longer necessary when utilizing the includeShadowDom option.
-
-You can safely remove this option from your config.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html deleted file mode 100644 index cfc2257dc362..000000000000 --- a/packages/errors/__snapshot-html__/EXPERIMENTAL_STUDIO_REMOVED.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - -
We're ending the experimental phase of Cypress Studio in Cypress version 10.0.0.
-
-If you don't think you can live without Studio or you'd like to learn about how to work around its removal, please join the discussion here: http://on.cypress.io/studio-removal
-
-Your feedback will help us factor in product decisions that may see Studio return in a future release.
-
-You can safely remove the experimentalStudio configuration option from your config.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/FIREFOX_GC_INTERVAL_REMOVED.html b/packages/errors/__snapshot-html__/FIREFOX_GC_INTERVAL_REMOVED.html deleted file mode 100644 index 19925cc65c91..000000000000 --- a/packages/errors/__snapshot-html__/FIREFOX_GC_INTERVAL_REMOVED.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - -
The firefoxGcInterval configuration option was removed in Cypress version 8.0.0. It was introduced to work around a bug in Firefox 79 and below.
-
-Since Cypress no longer supports Firefox 85 and below in Cypress Cypress version 8.0.0, this option was removed.
-
-You can safely remove this option from your config.
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/INCOMPATIBLE_PLUGIN_RETRIES.html b/packages/errors/__snapshot-html__/INCOMPATIBLE_PLUGIN_RETRIES.html deleted file mode 100644 index 6b1a26de534d..000000000000 --- a/packages/errors/__snapshot-html__/INCOMPATIBLE_PLUGIN_RETRIES.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - -
We've detected that the incompatible plugin cypress-plugin-retries is installed at: ./path/to/cypress-plugin-retries
-
-Test retries is now natively supported in Cypress version 5.0.0.
-
-Remove the plugin from your dependencies to silence this warning.
-
-https://on.cypress.io/test-retries
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/INTEGRATION_FOLDER_REMOVED.html b/packages/errors/__snapshot-html__/INTEGRATION_FOLDER_REMOVED.html deleted file mode 100644 index 03cdbee6c59f..000000000000 --- a/packages/errors/__snapshot-html__/INTEGRATION_FOLDER_REMOVED.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - -
The integrationFolder configuration option is now invalid when set on the config object in Cypress version 10.0.0.
-
-It is now renamed to specPattern and configured separately as a end to end testing property: e2e.specPattern
-
-{
-  e2e: {
-    specPattern: '...',
-  },
-}
-
-https://on.cypress.io/migration-guide
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/LEGACY_CONFIG_ERROR_DURING_MIGRATION.html b/packages/errors/__snapshot-html__/LEGACY_CONFIG_ERROR_DURING_MIGRATION.html deleted file mode 100644 index 9e5ce57be9f7..000000000000 --- a/packages/errors/__snapshot-html__/LEGACY_CONFIG_ERROR_DURING_MIGRATION.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - -
Your cypress/plugins/index.js file threw an error. 
-
-Please ensure your pluginsFile is valid and relaunch the migration tool to migrate to Cypress version 10.0.0.
-
-
-Error: fail whale
-    at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
-    at LEGACY_CONFIG_ERROR_DURING_MIGRATION (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/LEGACY_CONFIG_FILE.html b/packages/errors/__snapshot-html__/LEGACY_CONFIG_FILE.html deleted file mode 100644 index 2b8e82ac8afe..000000000000 --- a/packages/errors/__snapshot-html__/LEGACY_CONFIG_FILE.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - -
There is both a cypress.json and a cypress.json file at the location below:
-
-/path/to/projectRoot
-
-Cypress no longer supports cypress.json, please remove it from your project.
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/MIGRATION_ALREADY_OCURRED.html b/packages/errors/__snapshot-html__/MIGRATION_ALREADY_OCURRED.html deleted file mode 100644 index f54683c0ab1c..000000000000 --- a/packages/errors/__snapshot-html__/MIGRATION_ALREADY_OCURRED.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - -
You are attempting to use Cypress with an older config file: custom.json
-When you upgraded to Cypress v10.0 the config file was updated and moved to a new location: custom.config.js
-
-You may need to update any CLI scripts to ensure that they are referring the new version. This would typically look something like:
-"cypress open --config-file=custom.config.js"
-
-https://on.cypress.io/migration-guide
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/MIGRATION_CYPRESS_NOT_FOUND.html b/packages/errors/__snapshot-html__/MIGRATION_CYPRESS_NOT_FOUND.html deleted file mode 100644 index 8f4e67f89715..000000000000 --- a/packages/errors/__snapshot-html__/MIGRATION_CYPRESS_NOT_FOUND.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - -
You are running Cypress 10+ in global mode and attempting to open or migrate a project where an install of cypress cannot be found.
-
-Ensure that cypress@10 or greater is installed in the project you are attempting to open or migrate.
-
-https://on.cypress.io/migration-guide
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html b/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html deleted file mode 100644 index ce18942189c2..000000000000 --- a/packages/errors/__snapshot-html__/MIGRATION_MISMATCHED_CYPRESS_VERSIONS.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - -
You are running Cypress version 10.0.0 in global mode, but you are attempting to migrate a project where Cypress version 9.6.0 is installed.
-
-Ensure the project you are migrating has Cypress version Cypress version 10.0.0 installed.
-
-https://on.cypress.io/migration-guide
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/NO_PROJECT_ID.html b/packages/errors/__snapshot-html__/NO_PROJECT_ID.html index 971dabfdcf6a..cd58b21179e0 100644 --- a/packages/errors/__snapshot-html__/NO_PROJECT_ID.html +++ b/packages/errors/__snapshot-html__/NO_PROJECT_ID.html @@ -34,5 +34,5 @@ -
Can't find projectId in the config file: /path/to/project/cypress.json
+    
Can't find projectId in the config file: /path/to/project/cypress.config.js
 
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/PLUGINS_FILE_CONFIG_OPTION_REMOVED.html b/packages/errors/__snapshot-html__/PLUGINS_FILE_CONFIG_OPTION_REMOVED.html deleted file mode 100644 index 425114647680..000000000000 --- a/packages/errors/__snapshot-html__/PLUGINS_FILE_CONFIG_OPTION_REMOVED.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - -
The pluginsFile configuration option you have supplied has been replaced with setupNodeEvents.
-
-This new option is not a one-to-one correlation and it must be configured separately as a testing type property: e2e.setupNodeEvents and component.setupNodeEvents
-
-{
-  e2e: {
-    setupNodeEvents()
-  },
-  component: {
-    setupNodeEvents()
-  },
-}
-
-https://on.cypress.io/migration-guide
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER.html b/packages/errors/__snapshot-html__/SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER.html deleted file mode 100644 index d1596bcbf224..000000000000 --- a/packages/errors/__snapshot-html__/SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - -
Your configFile is invalid: /path/to/project/cypress.config.js
-
-Binding to the on('dev-server:start') event is no longer necessary.
-
-Please update your code to use the component.devServer() function.
-
-{
-  component: {
-    devServer (cypressDevServerConfig, devServerConfig) {
-      // start dev server here
-  }
-}
-
-Learn more: https://on.cypress.io/dev-server
-
-
\ No newline at end of file diff --git a/packages/errors/__snapshot-html__/TEST_FILES_RENAMED.html b/packages/errors/__snapshot-html__/TEST_FILES_RENAMED.html deleted file mode 100644 index 51a9f06917e9..000000000000 --- a/packages/errors/__snapshot-html__/TEST_FILES_RENAMED.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - -
The testFiles configuration option is now invalid when set on the config object in Cypress version 10.0.0.
-
-It is now renamed to specPattern and configured separately as a testing type property: e2e.specPattern or component.specPattern
-
-{
-  e2e: {
-    specPattern: '...',
-  },
-  component: {
-    specPattern: '...',
-  },
-}
-
-https://on.cypress.io/migration-guide
-
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index ee189cb9cc55..a81c9adc8c26 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -7,7 +7,7 @@ import path from 'path' import stripAnsi from 'strip-ansi' import type { BreakingErrResult, TestingType } from '@packages/types' import { humanTime, logError, parseResolvedPattern, pluralize } from './errorUtils' -import { errPartial, errTemplate, fmt, theme, PartialErr } from './errTemplate' +import { errPartial, errTemplate, fmt, theme } from './errTemplate' import { stackWithoutMessage } from './stackUtils' import type { ClonedError, ConfigValidationFailureInfo, CypressError, ErrTemplateResult, ErrorLike } from './errorTypes' import { normalizeNetworkErrorMessage } from './normalizeNetworkErrorMessage' @@ -109,16 +109,6 @@ export const AllCypressErrors = { ${fmt.listItems(options)}` }, BROWSER_NOT_FOUND_BY_NAME: (browser: string, foundBrowsersStr: string[]) => { - let canarySuffix: PartialErr | null = null - - if (browser === 'canary') { - canarySuffix = errPartial`\ - ${fmt.off('\n\n')} - Note: In ${fmt.cypressVersion(`4.0.0`)}, Canary must be launched as ${fmt.highlightSecondary(`chrome:canary`)}, not ${fmt.highlightSecondary(`canary`)}. - - See https://on.cypress.io/migration-guide for more information on breaking changes in 4.0.0.` - } - return errTemplate`\ Can't run because you've entered an invalid browser name. @@ -130,7 +120,7 @@ export const AllCypressErrors = { You can also use a custom browser: https://on.cypress.io/customize-browsers Available browsers found on your system are: - ${fmt.listItems(foundBrowsersStr)}${canarySuffix}` + ${fmt.listItems(foundBrowsersStr)}` }, BROWSER_NOT_FOUND_BY_PATH: (arg1: string, arg2: string) => { return errTemplate`\ @@ -895,7 +885,7 @@ export const AllCypressErrors = { Fix the error in your code and re-run your tests.` }, - // happens when there is an error in configuration file like "cypress.json" + // happens when there is an error in configuration file like "cypress.config.js" // TODO: make this relative path, not absolute CONFIG_VALIDATION_MSG_ERROR: (fileType: 'configFile' | null, fileName: string | null, validationMsg: string) => { if (!fileType) { @@ -1163,9 +1153,6 @@ export const AllCypressErrors = { CDP_RETRYING_CONNECTION: (attempt: string | number, browserName: string, connectRetryThreshold: number) => { return errTemplate`Still waiting to connect to ${fmt.off(_.capitalize(browserName))}, retrying in 1 second ${fmt.meta(`(attempt ${attempt}/${connectRetryThreshold})`)}` }, - CDP_FIREFOX_DEPRECATED: () => { - return errTemplate`Since Firefox 129, Chrome DevTools Protocol (CDP) has been deprecated in Firefox. In Firefox 135 and above, Cypress defaults to automating the Firefox browser with WebDriver BiDi. Cypress will no longer support CDP within Firefox in the future and is planned for removal in Cypress 15.` - }, BROWSER_PROCESS_CLOSED_UNEXPECTEDLY: (browserName: string) => { return errTemplate`\ We detected that the ${fmt.highlight(browserName)} browser process closed unexpectedly. @@ -1219,42 +1206,12 @@ export const AllCypressErrors = { If you don't require screenshots or videos to be stored you can safely ignore this warning.` }, - EXPERIMENTAL_SAMESITE_REMOVED: () => { - return errTemplate`\ - The ${fmt.highlight(`experimentalGetCookiesSameSite`)} configuration option was removed in ${fmt.cypressVersion(`5.0.0`)}. - - Returning the ${fmt.highlightSecondary(`sameSite`)} property is now the default behavior of the ${fmt.highlightSecondary(`cy.cookie`)} commands. - - You can safely remove this option from your config.` - }, EXPERIMENTAL_JIT_COMPILE_REMOVED: () => { return errTemplate`\ The ${fmt.highlight(`experimentalJustInTimeCompile`)} configuration option was removed in ${fmt.cypressVersion(`14.0.0`)}. A new ${fmt.highlightSecondary(`justInTimeCompile`)} configuration option is available and is now ${fmt.highlightSecondary(`true`)} by default. You can safely remove this option from your config.` }, - // TODO: verify configFile is absolute path - // TODO: make this relative path, not absolute - EXPERIMENTAL_COMPONENT_TESTING_REMOVED: (arg1: {configFile: string}) => { - return errTemplate`\ - The ${fmt.highlight('experimentalComponentTesting')} configuration option was removed in ${fmt.cypressVersion(`7.0.0`)}. - - Please remove this flag from: ${fmt.path(arg1.configFile)} - - Component Testing is now a supported testing type. You can run your component tests with: - - ${fmt.terminal(`cypress open --component`)} - - https://on.cypress.io/migration-guide` - }, - EXPERIMENTAL_SESSION_SUPPORT_REMOVED: () => { - return errTemplate`\ - The ${fmt.highlight(`experimentalSessionSupport`)} configuration option was removed in ${fmt.cypressVersion(`9.6.0`)}. - - You can safely remove this option from your config. - - https://on.cypress.io/session` - }, EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED: () => { return errTemplate`\ The ${fmt.highlight(`experimentalSessionAndOrigin`)} configuration option was removed in ${fmt.cypressVersion(`12.0.0`)}. @@ -1264,34 +1221,6 @@ export const AllCypressErrors = { https://on.cypress.io/session https://on.cypress.io/origin` }, - EXPERIMENTAL_SHADOW_DOM_REMOVED: () => { - return errTemplate`\ - The ${fmt.highlight(`experimentalShadowDomSupport`)} configuration option was removed in ${fmt.cypressVersion(`5.2.0`)}. It is no longer necessary when utilizing the ${fmt.highlightSecondary(`includeShadowDom`)} option. - - You can safely remove this option from your config.` - }, - EXPERIMENTAL_NETWORK_STUBBING_REMOVED: () => { - return errTemplate`\ - The ${fmt.highlight(`experimentalNetworkStubbing`)} configuration option was removed in ${fmt.cypressVersion(`6.0.0`)}. - - It is no longer necessary for using ${fmt.highlightSecondary(`cy.intercept()`)}. You can safely remove this option from your config.` - }, - EXPERIMENTAL_RUN_EVENTS_REMOVED: () => { - return errTemplate`\ - The ${fmt.highlight(`experimentalRunEvents`)} configuration option was removed in ${fmt.cypressVersion(`6.7.0`)}. It is no longer necessary when listening to run events in the plugins file. - - You can safely remove this option from your config.` - }, - EXPERIMENTAL_STUDIO_REMOVED: () => { - return errTemplate`\ - We're ending the experimental phase of Cypress Studio in ${fmt.cypressVersion(`10.0.0`)}. - - If you don't think you can live without Studio or you'd like to learn about how to work around its removal, please join the discussion here: http://on.cypress.io/studio-removal - - Your feedback will help us factor in product decisions that may see Studio return in a future release. - - You can safely remove the ${fmt.highlight(`experimentalStudio`)} configuration option from your config.` - }, EXPERIMENTAL_SINGLE_TAB_RUN_MODE: () => { return errTemplate`\ The ${fmt.highlight(`experimentalSingleTabRunMode`)} experiment is currently only supported for Component Testing. @@ -1359,26 +1288,6 @@ export const AllCypressErrors = { Read the documentation for the injectDocumentDomain configuration option: https://on.cypress.io/inject-document-domain-configuration ` }, - FIREFOX_GC_INTERVAL_REMOVED: () => { - return errTemplate`\ - The ${fmt.highlight(`firefoxGcInterval`)} configuration option was removed in ${fmt.cypressVersion(`8.0.0`)}. It was introduced to work around a bug in Firefox 79 and below. - - Since Cypress no longer supports Firefox 85 and below in Cypress ${fmt.cypressVersion(`8.0.0`)}, this option was removed. - - You can safely remove this option from your config.` - }, - // TODO: make this relative path, not absolute - INCOMPATIBLE_PLUGIN_RETRIES: (arg1: string) => { - return errTemplate`\ - We've detected that the incompatible plugin ${fmt.highlight(`cypress-plugin-retries`)} is installed at: ${fmt.path(arg1)} - - Test retries is now natively supported in ${fmt.cypressVersion(`5.0.0`)}. - - Remove the plugin from your dependencies to silence this warning. - - https://on.cypress.io/test-retries - ` - }, INVALID_CONFIG_OPTION: (arg1: string[]) => { const phrase = arg1.length > 1 ? 'options are' : 'option is' @@ -1398,31 +1307,11 @@ export const AllCypressErrors = { ${fmt.stackTrace(arg2)}` }, - CONFIG_FILE_INVALID_DEV_START_EVENT: (pluginsFilePath: string) => { - const code = errPartial` - module.exports = (on, config) => { - on('dev-server:start', () => { - ${fmt.comment('// start dev server here')} - return startDevServer(...) - } - }` - - return errTemplate`\ - To run component tests, Cypress needs you to configure the ${fmt.highlight(`dev-server:start`)} event. - - Please update this file: ${fmt.path(pluginsFilePath)} - - ${fmt.code(code)} - - https://on.cypress.io/component-testing` - }, UNSUPPORTED_BROWSER_VERSION: (errorMsg: string) => { return errTemplate`${fmt.off(errorMsg)}` }, - // V10 Added: - MULTIPLE_SUPPORT_FILES_FOUND: (arg1: string, arg2: string[]) => { return errTemplate`\ There were multiple support files found matching your ${fmt.highlightSecondary(`supportFile`)} pattern. @@ -1436,80 +1325,6 @@ export const AllCypressErrors = { Please remove or combine the support files into a single file.` }, - CONFIG_FILE_MIGRATION_NEEDED: (projectRoot: string) => { - return errTemplate` - There is a ${fmt.highlight(`cypress.json`)} file at the path: ${fmt.path(projectRoot)} - - ${fmt.cypressVersion('10.0.0')} no longer supports ${fmt.highlight(`cypress.json`)}. - - Please run ${fmt.highlightTertiary('cypress open')} to launch the migration tool to migrate to ${fmt.highlightSecondary('cypress.config.{js,ts,mjs,cjs}')}. - - https://on.cypress.io/migration-guide - ` - }, - - LEGACY_CONFIG_ERROR_DURING_MIGRATION: (file: string, error: Error) => { - return errTemplate` - Your ${fmt.highlight(file)} file threw an error. ${fmt.stackTrace(error)} - - Please ensure your pluginsFile is valid and relaunch the migration tool to migrate to ${fmt.cypressVersion('10.0.0')}. - ` - }, - - LEGACY_CONFIG_FILE: (baseFileName: string, projectRoot: string, legacyConfigFile: string = 'cypress.json') => { - return errTemplate` - There is both a ${fmt.highlight(baseFileName)} and a ${fmt.highlight(legacyConfigFile)} file at the location below: - - ${fmt.path(projectRoot)} - - Cypress no longer supports ${fmt.off(legacyConfigFile)}, please remove it from your project. - ` - }, - - SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER: (configFilePath: string) => { - const code = errPartial` - { - component: { - devServer (cypressDevServerConfig, devServerConfig) { - ${fmt.comment(`// start dev server here`) - } - } - }` - - return errTemplate`\ - Your ${fmt.highlightSecondary(`configFile`)} is invalid: ${fmt.path(configFilePath)} - - Binding to the ${fmt.highlightSecondary(`on('dev-server:start')`)} event is no longer necessary. - - Please update your code to use the ${fmt.highlight(`component.devServer()`)} function. - - ${fmt.code(code)} - - Learn more: https://on.cypress.io/dev-server - ` - }, - - PLUGINS_FILE_CONFIG_OPTION_REMOVED: (_errShape: BreakingErrResult) => { - const code = errPartial` - { - e2e: { - setupNodeEvents() - }, - component: { - setupNodeEvents() - }, - }` - - return errTemplate`\ - The ${fmt.highlight('pluginsFile')} configuration option you have supplied has been replaced with ${fmt.highlightSecondary('setupNodeEvents')}. - - This new option is not a one-to-one correlation and it must be configured separately as a testing type property: ${fmt.highlightSecondary('e2e.setupNodeEvents')} and ${fmt.highlightSecondary('component.setupNodeEvents')} - - ${fmt.code(code)} - - https://on.cypress.io/migration-guide` - }, - VIDEO_UPLOAD_ON_PASSES_REMOVED: (_errShape: BreakingErrResult) => { return errTemplate`\ The ${fmt.highlight(`videoUploadOnPasses`)} configuration option was removed in ${fmt.cypressVersion(`13.0.0`)}. @@ -1531,13 +1346,11 @@ export const AllCypressErrors = { }` return errTemplate`\ - The ${fmt.highlight(errShape.name)} configuration option is now invalid when set from the root of the config object in ${fmt.cypressVersion(`10.0.0`)}. + The ${fmt.highlight(errShape.name)} configuration option is invalid when set from the root of the config object. - It is now configured separately as a testing type property: ${fmt.highlightSecondary(`e2e.${errShape.name}`)} and ${fmt.highlightSecondary(`component.${errShape.name}`)} - - ${fmt.code(code)} + Set it within a testing type property: ${fmt.highlightSecondary(`e2e.${errShape.name}`)} and ${fmt.highlightSecondary(`component.${errShape.name}`)} - https://on.cypress.io/migration-guide` + ${fmt.code(code)}` }, CONFIG_FILE_INVALID_ROOT_CONFIG_E2E: (errShape: BreakingErrResult) => { @@ -1549,13 +1362,11 @@ export const AllCypressErrors = { }` return errTemplate`\ - The ${fmt.highlight(errShape.name)} configuration option is now invalid when set from the root of the config object in ${fmt.cypressVersion(`10.0.0`)}. + The ${fmt.highlight(errShape.name)} configuration option is invalid when set from the root of the config object. - It is now configured separately as a testing type property: ${fmt.highlightSecondary(`e2e.${errShape.name}`)} + Set it within a testing type property: ${fmt.highlightSecondary(`e2e.${errShape.name}`)} - ${fmt.code(code)} - - https://on.cypress.io/migration-guide` + ${fmt.code(code)}` }, CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT: (errShape: BreakingErrResult) => { @@ -1567,13 +1378,11 @@ export const AllCypressErrors = { }` return errTemplate`\ - The ${fmt.highlight(errShape.name)} configuration option is now invalid when set from the root of the config object in ${fmt.cypressVersion(`10.0.0`)}. - - It is now configured separately as a testing type property: ${fmt.highlightSecondary(`component.${errShape.name}`)} + The ${fmt.highlight(errShape.name)} configuration option is invalid when set from the root of the config object. - ${fmt.code(code)} + Set it within a testing type property: ${fmt.highlightSecondary(`component.${errShape.name}`)} - https://on.cypress.io/migration-guide` + ${fmt.code(code)}` }, // TODO: add path to config file @@ -1590,9 +1399,7 @@ export const AllCypressErrors = { Please remove this option or add this as an e2e testing type property: ${fmt.highlightSecondary(`e2e.${errShape.name}`)} - ${fmt.code(code)} - - https://on.cypress.io/migration-guide` + ${fmt.code(code)}` }, CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E: (errShape: BreakingErrResult) => { @@ -1608,9 +1415,7 @@ export const AllCypressErrors = { Please remove this option or add this as a component testing type property: ${fmt.highlightSecondary(`component.${errShape.name}`)} - ${fmt.code(code)} - - https://on.cypress.io/migration-guide` + ${fmt.code(code)}` }, CONFIG_FILE_DEV_SERVER_IS_NOT_VALID: (configFilePath: string, setupNodeEvents: any) => { @@ -1684,120 +1489,6 @@ export const AllCypressErrors = { ` }, - MIGRATION_ALREADY_OCURRED: (configFile: string, legacyConfigFile: string) => { - return errTemplate` - You are attempting to use Cypress with an older config file: ${fmt.highlight(legacyConfigFile)} - When you upgraded to Cypress v10.0 the config file was updated and moved to a new location: ${fmt.highlight(configFile)} - - You may need to update any CLI scripts to ensure that they are referring the new version. This would typically look something like: - "${fmt.highlight(`cypress open --config-file=${configFile}`)}" - - https://on.cypress.io/migration-guide - ` - }, - - TEST_FILES_RENAMED: (errShape: BreakingErrResult, err?: Error) => { - const stackTrace = err ? fmt.stackTrace(err) : null - - const newName = errShape.newName || '' - - const testingTypedHelpMessage = errShape.testingType - ? errPartial`${fmt.highlightSecondary(`${errShape.testingType}.${newName}`)}` - : errPartial`${fmt.highlightSecondary(`e2e.${newName}`)} or ${fmt.highlightSecondary(`component.${newName}`)}` - - const code = errShape.testingType - ? errPartial` - { - ${fmt.off(errShape.testingType)}: { - specPattern: '...', - }, - }` - : errPartial` - { - e2e: { - specPattern: '...', - }, - component: { - specPattern: '...', - }, - }` - - return errTemplate`\ - The ${fmt.highlight(errShape.name)} configuration option is now invalid when set on the config object in ${fmt.cypressVersion(`10.0.0`)}. - - It is now renamed to ${fmt.highlight(newName)} and configured separately as a testing type property: ${testingTypedHelpMessage} - ${fmt.code(code)} - - https://on.cypress.io/migration-guide - - ${stackTrace} - ` - }, - - COMPONENT_FOLDER_REMOVED: (errShape: BreakingErrResult, err?: Error) => { - const stackTrace = err ? fmt.stackTrace(err) : null - - const code = errPartial` - { - component: { - specPattern: '...', - }, - }` - - return errTemplate`\ - The ${fmt.highlight(errShape.name)} configuration option is now invalid when set on the config object in ${fmt.cypressVersion(`10.0.0`)}. - - It is now renamed to ${fmt.highlight('specPattern')} and configured separately as a component testing property: ${fmt.highlightSecondary('component.specPattern')} - ${fmt.code(code)} - - https://on.cypress.io/migration-guide - - ${stackTrace} - ` - }, - - INTEGRATION_FOLDER_REMOVED: (errShape: BreakingErrResult, err?: Error) => { - const stackTrace = err ? fmt.stackTrace(err) : null - - const code = errPartial` - { - e2e: { - specPattern: '...', - }, - }` - - return errTemplate`\ - The ${fmt.highlight(errShape.name)} configuration option is now invalid when set on the config object in ${fmt.cypressVersion(`10.0.0`)}. - - It is now renamed to ${fmt.highlight('specPattern')} and configured separately as a end to end testing property: ${fmt.highlightSecondary('e2e.specPattern')} - ${fmt.code(code)} - - https://on.cypress.io/migration-guide - - ${stackTrace} - ` - }, - - MIGRATION_MISMATCHED_CYPRESS_VERSIONS: (version: string, currentVersion: string) => { - return errTemplate` - You are running ${fmt.cypressVersion(currentVersion)} in global mode, but you are attempting to migrate a project where ${fmt.cypressVersion(version)} is installed. - - Ensure the project you are migrating has Cypress version ${fmt.cypressVersion(currentVersion)} installed. - - https://on.cypress.io/migration-guide - ` - }, - - MIGRATION_CYPRESS_NOT_FOUND: () => { - return errTemplate` - You are running Cypress 10+ in global mode and attempting to open or migrate a project where an install of ${fmt.code('cypress')} cannot be found. - - Ensure that ${fmt.code('cypress@10')} or greater is installed in the project you are attempting to open or migrate. - - https://on.cypress.io/migration-guide - ` - }, - DEV_SERVER_CONFIG_FILE_NOT_FOUND: (devServer: 'vite' | 'webpack', root: string, searchedFor: string[]) => { const devServerConfigFile = `${devServer}Config` diff --git a/packages/errors/test/unit/errors_spec.ts b/packages/errors/test/unit/errors_spec.ts index 724cf2d01a70..2fa5fc56efe0 100644 --- a/packages/errors/test/unit/errors_spec.ts +++ b/packages/errors/test/unit/errors_spec.ts @@ -49,13 +49,13 @@ describe('lib/errors', () => { }) it('logs err.message', () => { - const err = errors.getError('NO_PROJECT_ID', '/path/to/project/cypress.json') + const err = errors.getError('NO_PROJECT_ID', '/path/to/project/cypress.config.js') const ret = errors.log(err) expect(ret).to.be.undefined - expect(console.log).to.be.calledWithMatch('/path/to/project/cypress.json') + expect(console.log).to.be.calledWithMatch('/path/to/project/cypress.config.js') }) it('logs err.details', () => { diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index 81dcb59463aa..cc7ca5c95309 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -304,13 +304,6 @@ describe('visual error templates', () => { // testVisualErrors('CANNOT_RECORD_NO_PROJECT_ID', { testVisualErrors(errorType, { - LEGACY_CONFIG_ERROR_DURING_MIGRATION: () => { - const err = makeErr() - - return { - default: ['cypress/plugins/index.js', err], - } - }, CANNOT_TRASH_ASSETS: () => { const err = makeErr() @@ -603,7 +596,7 @@ describe('visual error templates', () => { }, CANNOT_RECORD_NO_PROJECT_ID: () => { return { - default: ['/path/to/cypress.json'], + default: ['/path/to/cypress.config.js'], } }, PROJECT_ID_AND_KEY_BUT_MISSING_RECORD_OPTION: () => { @@ -744,12 +737,12 @@ describe('visual error templates', () => { }, CLOUD_PROJECT_NOT_FOUND: () => { return { - default: ['project-id-123', '/path/to/cypress.json'], + default: ['project-id-123', '/path/to/cypress.config.js'], } }, NO_PROJECT_ID: () => { return { - default: ['/path/to/project/cypress.json'], + default: ['/path/to/project/cypress.config.js'], } }, NO_PROJECT_FOUND_AT_PROJECT_ROOT: () => { @@ -869,28 +862,28 @@ describe('visual error templates', () => { }, CONFIG_VALIDATION_ERROR: () => { return { - default: ['configFile', 'cypress.json', { + default: ['configFile', 'cypress.config.js', { key: 'defaultCommandTimeout', type: 'a number', value: false, }], - list: ['configFile', 'cypress.json', { + list: ['configFile', 'cypress.config.js', { key: 'displayName', type: 'a non-empty string', value: { name: 'chrome', version: '1.2.3', displayName: null }, list: 'browsers', }], - invalidString: ['configFile', 'cypress.json', { + invalidString: ['configFile', 'cypress.config.js', { key: 'defaultCommandTimeout', type: 'a number', value: '1234', }], - invalidObject: ['configFile', 'cypress.json', { + invalidObject: ['configFile', 'cypress.config.js', { key: 'defaultCommandTimeout', type: 'a number', value: { foo: 'bar' }, }], - invalidArray: ['configFile', 'cypress.json', { + invalidArray: ['configFile', 'cypress.config.js', { key: 'defaultCommandTimeout', type: 'a number', value: [1, 2, 3], @@ -904,7 +897,7 @@ describe('visual error templates', () => { }, CONFIG_VALIDATION_MSG_ERROR: () => { return { - default: ['configFile', 'cypress.json', '`something` was not right'], + default: ['configFile', 'cypress.config.js', '`something` was not right'], noFileType: [null, null, '`something` was not right'], } }, @@ -963,7 +956,7 @@ describe('visual error templates', () => { }, CONFIG_FILE_NOT_FOUND: () => { return { - default: ['cypress.json', '/path/to/project/root'], + default: ['cypress.config.js', '/path/to/project/root'], } }, INVOKED_BINARY_OUTSIDE_NPM_MODULE: () => { @@ -1084,11 +1077,6 @@ describe('visual error templates', () => { default: [1, 'chrome', 62], } }, - CDP_FIREFOX_DEPRECATED: () => { - return { - default: [], - } - }, BROWSER_PROCESS_CLOSED_UNEXPECTEDLY: () => { return { default: ['chrome'], @@ -1123,81 +1111,16 @@ describe('visual error templates', () => { default: ['/path/to/folder'], } }, - EXPERIMENTAL_SAMESITE_REMOVED: () => { - return { - default: [], - } - }, EXPERIMENTAL_JIT_COMPILE_REMOVED: () => { return { default: [], } }, - EXPERIMENTAL_COMPONENT_TESTING_REMOVED: () => { - return { - default: [{ configFile: '/path/to/cypress.config.js' }], - } - }, - EXPERIMENTAL_SESSION_SUPPORT_REMOVED: () => { - return { - default: [], - } - }, EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED: () => { return { default: [], } }, - EXPERIMENTAL_SHADOW_DOM_REMOVED: () => { - return { - default: [], - } - }, - EXPERIMENTAL_NETWORK_STUBBING_REMOVED: () => { - return { - default: [], - } - }, - EXPERIMENTAL_RUN_EVENTS_REMOVED: () => { - return { - default: [], - } - }, - EXPERIMENTAL_STUDIO_REMOVED: () => { - return { - default: [], - } - }, - FIREFOX_GC_INTERVAL_REMOVED: () => { - return { - default: [], - } - }, - INCOMPATIBLE_PLUGIN_RETRIES: () => { - return { - default: ['./path/to/cypress-plugin-retries'], - } - }, - CONFIG_FILE_MIGRATION_NEEDED: () => { - return { - default: ['/path/to/projectRoot'], - } - }, - LEGACY_CONFIG_FILE: () => { - return { - default: ['cypress.json', '/path/to/projectRoot'], - } - }, - SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER: () => { - return { - default: ['/path/to/project/cypress.config.js'], - } - }, - CONFIG_FILE_INVALID_DEV_START_EVENT: () => { - return { - default: ['/path/to/plugins/file.js'], - } - }, CONFIG_FILE_DEV_SERVER_INVALID_RETURN: () => { return { default: [{}], @@ -1224,11 +1147,6 @@ describe('visual error templates', () => { default: ['spec.{ts,js}', ['support.ts', 'support.js']], } }, - PLUGINS_FILE_CONFIG_OPTION_REMOVED: () => { - return { - default: [{ name: 'pluginsFile', configFile: '/path/to/cypress.config.js.ts' }], - } - }, VIDEO_UPLOAD_ON_PASSES_REMOVED: () => { return { default: [{ name: 'videoUploadOnPasses', configFile: '/path/to/cypress.config.js.ts' }], @@ -1284,36 +1202,6 @@ describe('visual error templates', () => { default: [makeErr()], } }, - MIGRATION_ALREADY_OCURRED: () => { - return { - default: ['custom.config.js', 'custom.json'], - } - }, - TEST_FILES_RENAMED: () => { - return { - default: [{ name: 'testFiles', newName: 'specPattern', configFile: '/path/to/cypress.config.js.ts' }], - } - }, - COMPONENT_FOLDER_REMOVED: () => { - return { - default: [{ name: 'componentFolder', configFile: '/path/to/cypress.config.js.ts' }], - } - }, - INTEGRATION_FOLDER_REMOVED: () => { - return { - default: [{ name: 'integrationFolder', configFile: '/path/to/cypress.config.js.ts' }], - } - }, - MIGRATION_MISMATCHED_CYPRESS_VERSIONS: () => { - return { - default: ['9.6.0', '10.0.0'], - } - }, - MIGRATION_CYPRESS_NOT_FOUND: () => { - return { - default: [], - } - }, DEV_SERVER_CONFIG_FILE_NOT_FOUND: () => { return { default: ['vite', '/dev/project', ['vite.config.js', 'vite.config.ts']], diff --git a/packages/extension/app/v2/background.js b/packages/extension/app/v2/background.js index 7fd063288933..c7d91c5acc1c 100644 --- a/packages/extension/app/v2/background.js +++ b/packages/extension/app/v2/background.js @@ -1,25 +1,9 @@ const get = require('lodash/get') -const map = require('lodash/map') -const pick = require('lodash/pick') const once = require('lodash/once') const Promise = require('bluebird') const browser = require('webextension-polyfill') -const { cookieMatches } = require('@packages/server/lib/automation/util') const client = require('./client') -const util = require('../../lib/util') - -const COOKIE_PROPS = ['url', 'name', 'path', 'secure', 'domain'] -const GET_ALL_PROPS = COOKIE_PROPS.concat(['session', 'storeId']) -// https://developer.chrome.com/extensions/cookies#method-set -const SET_PROPS = COOKIE_PROPS.concat(['value', 'httpOnly', 'expirationDate', 'sameSite']) - -const httpRe = /^http/ - -// normalize into null when empty array -const firstOrNull = (cookies) => { - return cookies[0] != null ? cookies[0] : null -} const checkIfFirefox = async () => { if (!browser || !get(browser, 'runtime.getBrowserInfo')) { @@ -31,23 +15,6 @@ const checkIfFirefox = async () => { return name === 'Firefox' } -// this check only applies to firefox versioning! -const isBiDiEnabled = async (config) => { - if (!browser || !get(browser, 'runtime.getBrowserInfo') || config.IS_CDP_FORCED_FOR_FIREFOX) { - return false - } - - const { version } = await browser.runtime.getBrowserInfo() - - if (version) { - const [majorVersion] = version.split('.').map(Number) - - return majorVersion >= 135 - } - - return false -} - const connect = function (host, path, extraOpts) { const listenToCookieChanges = once(() => { return browser.cookies.onChanged.addListener((info) => { @@ -84,30 +51,6 @@ const connect = function (host, path, extraOpts) { }) }) - const listenToOnBeforeHeaders = once(() => { - // adds a header to the request to mark it as a request for the AUT frame - // itself, so the proxy can utilize that for injection purposes - browser.webRequest.onBeforeSendHeaders.addListener((details) => { - if ( - // parentFrameId: 0 means the parent is the top-level, so if it isn't - // 0, it's nested inside the AUT and can't be the AUT itself - details.parentFrameId !== 0 - // is the spec frame, not the AUT - || details.url.includes('__cypress') - ) return - - return { - requestHeaders: [ - ...details.requestHeaders, - { - name: 'X-Cypress-Is-AUT-Frame', - value: 'true', - }, - ], - } - }, { urls: [''], types: ['sub_frame'] }, ['blocking', 'requestHeaders']) - }) - const fail = (id, err) => { return ws.emit('automation:response', id, { __error: err.message, @@ -132,29 +75,8 @@ const connect = function (host, path, extraOpts) { ws.on('automation:request', (id, msg, data) => { switch (msg) { - case 'get:cookies': - return invoke('getCookies', id, data) - case 'get:cookie': - return invoke('getCookie', id, data) - case 'set:cookie': - return invoke('setCookie', id, data) - case 'set:cookies': - case 'add:cookies': - return invoke('setCookies', id, data) - case 'clear:cookies': - return invoke('clearCookies', id, data) - case 'clear:cookie': - return invoke('clearCookie', id, data) - case 'is:automation:client:connected': - return invoke('verify', id, data) - case 'focus:browser:window': - return invoke('focus', id) - case 'take:screenshot': - return invoke('takeScreenshot', id) case 'reset:browser:state': return invoke('resetBrowserState', id) - case 'reset:browser:tabs:for:next:spec': - return invoke('resetBrowserTabsForNextSpec', id) default: return fail(id, { message: `No handler registered for: '${msg}'` }) } @@ -167,13 +89,6 @@ const connect = function (host, path, extraOpts) { if (isFirefox) { // Non-Firefox browsers use CDP for this instead listenToDownloads() - // if BiDi is enabled, BiDi will handle the network interception. - // Otherwise, CDP does not support it for Firefox and we need to listen for it here. - const isBiDiTurnedOn = await isBiDiEnabled(config) - - if (!isBiDiTurnedOn) { - listenToOnBeforeHeaders() - } } }) @@ -184,207 +99,15 @@ const connect = function (host, path, extraOpts) { return ws } -const setOneCookie = (props) => { - // only get the url if its not already set - if (props.url == null) { - props.url = util.getCookieUrl(props) - } - - if (props.hostOnly) { - // If the hostOnly prop is available, delete the domain. - // This will wind up setting a hostOnly cookie based on the calculated cookieURL above. - delete props.domain - } - - if (props.domain === 'localhost') { - delete props.domain - } - - props = pick(props, SET_PROPS) - - return Promise.try(() => { - return browser.cookies.set(props) - }) -} - -const clearOneCookie = (cookie = {}) => { - const url = util.getCookieUrl(cookie) - const props = { url, name: cookie.name } - - const throwError = function (err) { - throw (err != null ? err : new Error(`Removing cookie failed for: ${JSON.stringify(props)}`)) - } - - return Promise.try(() => { - if (!cookie.name) { - throw new Error(`Removing cookie failed for: ${JSON.stringify(cookie)}. Cookie did not include a name`) - } - - return browser.cookies.remove(props) - }).then((details) => { - return cookie - }).catch(throwError) -} - -const clearAllCookies = (cookies) => { - return Promise.mapSeries(cookies, clearOneCookie) -} - const automation = { connect, - getAll (filter = {}) { - filter = pick(filter, GET_ALL_PROPS) - - // Firefox's filtering doesn't match the behavior we want, so we do it - // ourselves. for example, getting { domain: example.com } cookies will - // return cookies for example.com and all subdomains, whereas we want an - // exact match for only "example.com". - return Promise.try(() => { - return browser.cookies.getAll({ url: filter.url }) - .then((cookies) => { - return cookies.filter((cookie) => { - return cookieMatches(cookie, filter) - }) - }) - }) - }, - - getCookies (filter, fn) { - return this.getAll(filter) - .then(fn) - }, - - getCookie (filter, fn) { - return this.getAll(filter) - .then(firstOrNull) - .then(fn) - }, - - setCookie (props = {}, fn) { - return setOneCookie(props) - .then(fn) - }, - - setCookies (propsArr = [], fn) { - return Promise.mapSeries(propsArr, setOneCookie) - .then(fn) - }, - - clearCookie (filter, fn) { - return this.getCookie(filter) - .then((cookie) => { - if (!cookie) return null - - return clearOneCookie(cookie) - }) - .then(fn) - }, - - clearCookies (cookies, fn) { - return clearAllCookies(cookies) - .then(fn) - }, - - focus (fn) { - // lets just make this simple and whatever is the current - // window bring that into focus - // - // TODO: if we REALLY want to be nice its possible we can - // figure out the exact window that's running Cypress but - // that's too much work with too little value at the moment - return Promise.try(() => { - return browser.windows.getCurrent() - }).then((window) => { - return browser.windows.update(window.id, { focused: true }) - }).then(fn) - }, - resetBrowserState (fn) { // We remove browser data. Firefox goes through this path, while chrome goes through cdp automation // Note that firefox does not support fileSystems or serverBoundCertificates // (https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browsingData/DataTypeSet). return browser.browsingData.remove({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).then(fn) }, - - resetBrowserTabsForNextSpec (callback) { - return Promise.try(() => { - return browser.windows.getCurrent({ populate: true }) - }).then(async (currentWindowInfo) => { - const windows = await browser.windows.getAll().catch(() => []) - - for (const window of windows) { - // remove/close the window if it's not the current window - if (window.id !== currentWindowInfo.id) { - // tslint:disable-next-line:no-empty - await browser.windows.remove(window.id).catch(() => {}) - } - } - - return currentWindowInfo - }).then(async (currentWindowInfo) => { - let newTabId = null - - try { - // in versions of Firefox 124 and up, firefox no longer creates a new tab for us when we close all tabs in the browser. - // to keep change minimal and backwards compatible, we are creating an 'about:blank' tab here to keep the behavior consistent. - // this works in previous versions as well since one tab is left, hence one will not be created for us in Firefox 123 and below - const newAboutBlankTab = await browser.tabs.create({ url: 'about:blank', active: false }) - - newTabId = newAboutBlankTab.id - } catch (e) { - undefined - } - - return browser.tabs.remove(currentWindowInfo.tabs.map((tab) => tab.id).filter((tab) => tab.id !== newTabId)) - }).then(callback) - }, - - query (data) { - const code = `var s; (s = document.getElementById('${data.element}')) && s.textContent` - - const queryTab = (tab) => { - return Promise.try(() => { - return browser.tabs.executeScript(tab.id, { code }) - }).then((results) => { - if (!results || (results[0] !== data.randomString)) { - throw new Error('Executed script did not return result') - } - }) - } - - return Promise.try(() => { - return browser.tabs.query({ windowType: 'normal' }) - }).filter((tab) => { - // the tab's url must begin with - // http or https so that we filter out - // about:blank and chrome:// urls - // which will throw errors! - return httpRe.test(tab.url) - }).then((tabs) => { - // generate array of promises - return map(tabs, queryTab) - }).any() - }, - - verify (data, fn) { - return this.query(data) - .then(fn) - }, - - lastFocusedWindow () { - return Promise.try(() => { - return browser.windows.getLastFocused() - }) - }, - - takeScreenshot (fn) { - return this.lastFocusedWindow() - .then((win) => { - return browser.tabs.captureVisibleTab(win.id, { format: 'png' }) - }) - .then(fn) - }, } module.exports = automation diff --git a/packages/extension/test/integration/v2/background_spec.js b/packages/extension/test/integration/v2/background_spec.js index 11c1fc7ea595..eef57bda3771 100644 --- a/packages/extension/test/integration/v2/background_spec.js +++ b/packages/extension/test/integration/v2/background_spec.js @@ -3,15 +3,11 @@ require('../../spec_helper') const _ = require('lodash') const http = require('http') const socket = require('@packages/socket') -const Promise = require('bluebird') const mockRequire = require('mock-require') const client = require('../../../app/v2/client') const browser = { cookies: { - set () {}, - getAll () {}, - remove () {}, onChanged: { addListener () {}, }, @@ -24,29 +20,10 @@ const browser = { addListener () {}, }, }, - windows: { - getAll () {}, - getCurrent () {}, - getLastFocused () {}, - remove () {}, - update () {}, - }, runtime: {}, - tabs: { - create () {}, - query () {}, - executeScript () {}, - captureVisibleTab () {}, - remove () {}, - }, browsingData: { remove () {}, }, - webRequest: { - onBeforeSendHeaders: { - addListener () {}, - }, - }, } mockRequire('webextension-polyfill', browser) @@ -56,69 +33,6 @@ const { expect } = require('chai') const PORT = 12345 -const tab1 = { - 'active': false, - 'audible': false, - 'favIconUrl': 'http://localhost:2020/__cypress/static/img/favicon.ico', - 'height': 553, - 'highlighted': false, - 'id': 1, - 'incognito': false, - 'index': 0, - 'mutedInfo': { - 'muted': false, - }, - 'pinned': false, - 'selected': false, - 'status': 'complete', - 'title': 'foobar', - 'url': 'http://localhost:2020/__/#tests', - 'width': 1920, - 'windowId': 1, -} - -const tab2 = { - 'active': true, - 'audible': false, - 'favIconUrl': 'http://localhost:2020/__cypress/static/img/favicon.ico', - 'height': 553, - 'highlighted': true, - 'id': 2, - 'incognito': false, - 'index': 1, - 'mutedInfo': { - 'muted': false, - }, - 'pinned': false, - 'selected': true, - 'status': 'complete', - 'title': 'foobar', - 'url': 'https://localhost:2020/__/#tests', - 'width': 1920, - 'windowId': 1, -} - -const tab3 = { - 'active': true, - 'audible': false, - 'favIconUrl': 'http://localhost:2020/__cypress/static/img/favicon.ico', - 'height': 553, - 'highlighted': true, - 'id': 2, - 'incognito': false, - 'index': 1, - 'mutedInfo': { - 'muted': false, - }, - 'pinned': false, - 'selected': true, - 'status': 'complete', - 'title': 'foobar', - 'url': 'about:blank', - 'width': 1920, - 'windowId': 1, -} - describe('app/background', () => { beforeEach(function (done) { global.window = {} @@ -293,233 +207,6 @@ describe('app/background', () => { }) }) - context('add header to aut iframe requests', () => { - beforeEach(() => { - browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox', version: '135.0.1' }) - }) - - it('allows for CDP to be used as an escape hatch if BiDi would otherwise be enabled', async function () { - sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect({ - IS_CDP_FORCED_FOR_FIREFOX: true, - }) - - expect(browser.webRequest.onBeforeSendHeaders.addListener).to.be.called - }) - - context('BiDi enabled', () => { - it('does not attach onBeforeSendHeaders listener if BiDi is enabled', async function () { - sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect() - - expect(browser.webRequest.onBeforeSendHeaders.addListener).not.to.be.called - }) - }) - - context('CDP enabled', () => { - beforeEach(() => { - browser.runtime.getBrowserInfo = sinon.stub().resolves({ name: 'Firefox', version: '134' }) - }) - - it('does not add header if it is the top frame', async function () { - const details = { - parentFrameId: -1, - } - - sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect() - - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - - expect(result).to.be.undefined - }) - - it('does not add header if it is a nested frame', async function () { - const details = { - parentFrameId: 12345, - } - - sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect() - - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - - expect(result).to.be.undefined - }) - - it('does not add header if it is a spec frame request', async function () { - const details = { - parentFrameId: 0, - type: 'sub_frame', - url: '/__cypress/integration/spec.js', - } - - sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect() - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - - expect(result).to.be.undefined - }) - - it('appends X-Cypress-Is-AUT-Frame header to AUT iframe request', async function () { - const details = { - parentFrameId: 0, - type: 'sub_frame', - url: 'http://localhost:3000/index.html', - requestHeaders: [ - { name: 'X-Foo', value: 'Bar' }, - ], - } - - sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect() - const result = browser.webRequest.onBeforeSendHeaders.addListener.lastCall.args[0](details) - - expect(result).to.deep.equal({ - requestHeaders: [ - { - name: 'X-Foo', - value: 'Bar', - }, - { - name: 'X-Cypress-Is-AUT-Frame', - value: 'true', - }, - ], - }) - }) - - it('does not add before-headers listener if in non-Firefox browser', async function () { - browser.runtime.getBrowserInfo = undefined - - const onBeforeSendHeaders = sinon.stub(browser.webRequest.onBeforeSendHeaders, 'addListener') - - await this.connect() - - expect(onBeforeSendHeaders).not.to.be.called - }) - }) - }) - - context('.getAll', () => { - it('resolves with specific cookie properties', () => { - sinon.stub(browser.cookies, 'getAll').resolves([ - { name: 'key1', value: 'value1', path: '/', domain: 'localhost', secure: true, httpOnly: true, expirationDate: 123 }, - { name: 'key2', value: 'value2', path: '/', domain: 'localhost', secure: false, httpOnly: false, expirationDate: 456 }, - { name: 'key3', value: 'value3', path: '/', domain: 'foobar.com', secure: false, httpOnly: false, expirationDate: 456 }, - { name: 'key4', value: 'value4', path: '/', domain: 'www.foobar.com', secure: false, httpOnly: false, expirationDate: 456 }, - ]) - - return background.getAll({ domain: 'foobar.com' }) - .then((cookies) => { - expect(cookies).to.deep.eq([ - { name: 'key3', value: 'value3', path: '/', domain: 'foobar.com', secure: false, httpOnly: false, expirationDate: 456 }, - ]) - }) - }) - }) - - context('.query', () => { - beforeEach(function () { - this.code = 'var s; (s = document.getElementById(\'__cypress-string\')) && s.textContent' - }) - - it('resolves on the 1st tab', function () { - sinon.stub(browser.tabs, 'query') - .withArgs({ windowType: 'normal' }) - .resolves([tab1]) - - sinon.stub(browser.tabs, 'executeScript') - .withArgs(tab1.id, { code: this.code }) - .resolves(['1234']) - - return background.query({ - randomString: '1234', - element: '__cypress-string', - }) - }) - - it('resolves on the 2nd tab', function () { - sinon.stub(browser.tabs, 'query') - .withArgs({ windowType: 'normal' }) - .resolves([tab1, tab2]) - - sinon.stub(browser.tabs, 'executeScript') - .withArgs(tab1.id, { code: this.code }) - .resolves(['foobarbaz']) - .withArgs(tab2.id, { code: this.code }) - .resolves(['1234']) - - return background.query({ - randomString: '1234', - element: '__cypress-string', - }) - }) - - it('filters out tabs that don\'t start with http', () => { - sinon.stub(browser.tabs, 'query') - .resolves([tab3]) - - return background.query({ - string: '1234', - element: '__cypress-string', - }) - .then(() => { - throw new Error('should have failed') - }).catch((err) => { - // we good if this hits - expect(err).to.be.instanceof(Promise.RangeError) - }) - }) - - it('rejects if no tab matches', function () { - sinon.stub(browser.tabs, 'query') - .withArgs({ windowType: 'normal' }) - .resolves([tab1, tab2]) - - sinon.stub(browser.tabs, 'executeScript') - .withArgs(tab1.id, { code: this.code }) - .resolves(['foobarbaz']) - .withArgs(tab2.id, { code: this.code }) - .resolves(['foobarbaz2']) - - return background.query({ - string: '1234', - element: '__cypress-string', - }) - .then(() => { - throw new Error('should have failed') - }).catch((err) => { - // we good if this hits - expect(err.length).to.eq(2) - - expect(err).to.be.instanceof(Promise.AggregateError) - }) - }) - - it('rejects if no tabs were found', () => { - sinon.stub(browser.tabs, 'query') - .resolves([]) - - return background.query({ - string: '1234', - element: '__cypress-string', - }) - .then(() => { - throw new Error('should have failed') - }).catch((err) => { - // we good if this hits - expect(err).to.be.instanceof(Promise.RangeError) - }) - }) - }) - context('integration', () => { beforeEach(function (done) { done = _.once(done) @@ -535,329 +222,6 @@ describe('app/background', () => { this.client = background.connect(`http://localhost:${PORT}`, '/__socket') }) - describe('get:cookies', () => { - beforeEach(() => { - sinon.stub(browser.cookies, 'getAll').resolves([ - { cookie: '1', domain: 'example.com' }, - { cookie: '2', domain: 'www.example.com' }, - ]) - }) - - it('returns cookies that match filter', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq([{ cookie: '1', domain: 'example.com' }]) - - done() - }) - - this.server.emit('automation:request', 123, 'get:cookies', { domain: 'example.com' }) - }) - - it('returns all cookies if there is no filter', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq([ - { cookie: '1', domain: 'example.com' }, - { cookie: '2', domain: 'www.example.com' }, - ]) - - done() - }) - - this.server.emit('automation:request', 123, 'get:cookies', {}) - }) - }) - - describe('get:cookie', () => { - beforeEach(() => { - sinon.stub(browser.cookies, 'getAll').resolves([ - { name: 'session', value: 'key', path: '/login', domain: 'example.com', secure: true, httpOnly: true, expirationDate: 123 }, - ]) - }) - - it('returns a specific cookie by name', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq({ name: 'session', value: 'key', path: '/login', domain: 'example.com', secure: true, httpOnly: true, expirationDate: 123 }) - - done() - }) - - this.server.emit('automation:request', 123, 'get:cookie', { domain: 'example.com', name: 'session' }) - }) - - it('returns null when no cookie by name is found', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.be.null - - done() - }) - - this.server.emit('automation:request', 123, 'get:cookie', { domain: 'example.com', name: 'doesNotExist' }) - }) - }) - - describe('set:cookie', () => { - beforeEach(() => { - browser.runtime.lastError = { message: 'some error' } - - return sinon.stub(browser.cookies, 'set') - .withArgs({ domain: 'example.com', name: 'session', value: 'key', path: '/', secure: false, url: 'http://example.com/' }) - .resolves( - { name: 'session', value: 'key', path: '/', domain: 'example', secure: false, httpOnly: false }, - ) - .withArgs({ url: 'https://www.example.com', name: 'session', value: 'key' }) - .resolves( - { name: 'session', value: 'key', path: '/', domain: 'example.com', secure: true, httpOnly: false }, - ) - // 'domain' cannot not set when it's localhost - .withArgs({ name: 'foo', value: 'bar', secure: true, path: '/foo', url: 'https://localhost/foo' }) - .rejects({ message: 'some error' }) - }) - - it('resolves with the cookie details', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq({ name: 'session', value: 'key', path: '/', domain: 'example', secure: false, httpOnly: false }) - - done() - }) - - this.server.emit('automation:request', 123, 'set:cookie', { domain: 'example.com', name: 'session', secure: false, value: 'key', path: '/' }) - }) - - it('does not set url when already present', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq({ name: 'session', value: 'key', path: '/', domain: 'example.com', secure: true, httpOnly: false }) - - done() - }) - - this.server.emit('automation:request', 123, 'set:cookie', { url: 'https://www.example.com', name: 'session', value: 'key' }) - }) - - it('rejects with error', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.__error).to.eq('some error') - - done() - }) - - this.server.emit('automation:request', 123, 'set:cookie', { name: 'foo', value: 'bar', domain: 'localhost', secure: true, path: '/foo' }) - }) - }) - - describe('clear:cookies', () => { - beforeEach(() => { - browser.runtime.lastError = { message: 'some error' } - - return sinon.stub(browser.cookies, 'remove') - .callsFake(function () { - // eslint-disable-next-line no-console - console.log('unstubbed browser.cookies.remove', ...arguments) - }) - .withArgs({ url: 'https://example.com', name: 'foo' }) - .resolves( - { name: 'session', url: 'https://example.com/', storeId: '123' }, - ) - .withArgs({ name: 'foo', url: 'http://example.com/foo' }) - .resolves( - { name: 'foo', url: 'https://example.com/foo', storeId: '123' }, - ) - .withArgs({ name: 'noDetails', url: 'http://no.details' }) - .resolves(null) - .withArgs({ name: 'shouldThrow', url: 'http://should.throw' }) - .rejects({ message: 'some error' }) - }) - - it('resolves with array of removed cookies', function (done) { - const cookieArr = [{ domain: 'example.com', name: 'foo', secure: true }] - - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq(cookieArr) - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookies', cookieArr) - }) - - it('rejects when no cookie.name', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.__error).to.contain('did not include a name') - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookies', [{ domain: 'should.throw' }]) - }) - - it('rejects with error thrown in browser.cookies.remove', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.__error).to.eq('some error') - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookies', [{ domain: 'should.throw', name: 'shouldThrow' }]) - }) - - it('doesnt fail when no found cookie', function (done) { - const cookieArr = [{ domain: 'no.details', name: 'noDetails' }] - - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq(cookieArr) - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookies', cookieArr) - }) - }) - - describe('clear:cookie', () => { - beforeEach(() => { - browser.runtime.lastError = { message: 'some error' } - - sinon.stub(browser.cookies, 'getAll').resolves([ - { name: 'session', value: 'key', path: '/', domain: 'example.com', secure: true, httpOnly: true, expirationDate: 123 }, - ]) - - return sinon.stub(browser.cookies, 'remove') - .withArgs({ name: 'session', url: 'https://example.com/' }) - .resolves( - { name: 'session', url: 'https://example.com/', storeId: '123' }, - ) - .withArgs({ name: 'shouldThrow', url: 'http://cdn.github.com/assets' }) - .rejects({ message: 'some error' }) - }) - - it('resolves single removed cookie', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.deep.eq( - { name: 'session', value: 'key', path: '/', domain: 'example.com', secure: true, httpOnly: true, expirationDate: 123 }, - ) - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookie', { domain: 'example.com', name: 'session' }) - }) - - it('returns null when no cookie by name is found', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.be.null - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookie', { domain: 'example.com', name: 'doesNotExist' }) - }) - - it('rejects with error', function (done) { - browser.cookies.getAll.resolves([ - { name: 'shouldThrow', value: 'key', path: '/assets', domain: 'cdn.github.com', secure: false, httpOnly: true, expirationDate: 123 }, - ]) - - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.__error).to.eq('some error') - - done() - }) - - this.server.emit('automation:request', 123, 'clear:cookie', { domain: 'cdn.github.com', name: 'shouldThrow' }) - }) - }) - - describe('is:automation:client:connected', () => { - beforeEach(() => { - return sinon.stub(browser.tabs, 'query') - .withArgs({ url: 'CHANGE_ME_HOST/*', windowType: 'normal' }) - .resolves([]) - }) - - it('queries url and resolve', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.be.undefined - - done() - }) - - this.server.emit('automation:request', 123, 'is:automation:client:connected') - }) - }) - - describe('focus:browser:window', () => { - beforeEach(() => { - sinon.stub(browser.windows, 'getCurrent').resolves({ id: '10' }) - sinon.stub(browser.windows, 'update').withArgs('10', { focused: true }).resolves() - }) - - it('focuses the current window', function (done) { - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.be.undefined - - expect(browser.windows.getCurrent).to.be.called - expect(browser.windows.update).to.be.called - - done() - }) - - this.server.emit('automation:request', 123, 'focus:browser:window') - }) - }) - - describe('take:screenshot', () => { - beforeEach(() => { - return sinon.stub(browser.windows, 'getLastFocused').resolves({ id: 1 }) - }) - - afterEach(() => { - return delete browser.runtime.lastError - }) - - it('resolves with screenshot', function (done) { - sinon.stub(browser.tabs, 'captureVisibleTab') - .withArgs(1, { format: 'png' }) - .resolves('foobarbaz') - - this.socket.on('automation:response', (id, obj = {}) => { - expect(id).to.eq(123) - expect(obj.response).to.eq('foobarbaz') - - done() - }) - - this.server.emit('automation:request', 123, 'take:screenshot') - }) - - it('rejects with browser.runtime.lastError', function (done) { - sinon.stub(browser.tabs, 'captureVisibleTab').withArgs(1, { format: 'png' }).rejects(new Error('some error')) - - this.socket.on('automation:response', (id, obj) => { - expect(id).to.eq(123) - expect(obj.__error).to.eq('some error') - - done() - }) - - this.server.emit('automation:request', 123, 'take:screenshot') - }) - }) - describe('reset:browser:state', () => { beforeEach(() => { sinon.stub(browser.browsingData, 'remove').withArgs({}, { cache: true, cookies: true, downloads: true, formData: true, history: true, indexedDB: true, localStorage: true, passwords: true, pluginData: true, serviceWorkers: true }).resolves() @@ -876,84 +240,5 @@ describe('app/background', () => { this.server.emit('automation:request', 123, 'reset:browser:state') }) }) - - describe('reset:browser:tabs:for:next:spec', () => { - beforeEach(() => { - sinon.stub(browser.windows, 'getCurrent').withArgs({ populate: true }).resolves({ id: '10', tabs: [{ id: '1' }, { id: '2' }, { id: '3' }] }) - sinon.stub(browser.tabs, 'remove').withArgs(['1', '2', '3']).resolves() - sinon.stub(browser.tabs, 'create').withArgs({ url: 'about:blank', active: false }).resolves({ - id: 'new-tab', - }) - }) - - // @see https://github.com/cypress-io/cypress/issues/29172 for Firefox versions 124 and up - it('closes the tabs in the current browser window and creates a new "about:blank" tab', function (done) { - sinon.stub(browser.windows, 'getAll').resolves([{ id: '10' }]) - - this.socket.on('automation:response', (id, obj) => { - expect(id).to.eq(123) - expect(obj.response).to.be.undefined - - expect(browser.windows.getCurrent).to.be.called - expect(browser.tabs.remove).to.be.calledWith(['1', '2', '3']) - expect(browser.tabs.create).to.be.calledWith({ url: 'about:blank', active: false }) - - done() - }) - - this.server.emit('automation:request', 123, 'reset:browser:tabs:for:next:spec') - }) - - it('closes any extra windows', function (done) { - sinon.stub(browser.windows, 'getAll').resolves([{ id: '9' }, { id: '10' }, { id: '11' }]) - sinon.stub(browser.windows, 'remove').resolves() - - this.socket.on('automation:response', (id, obj) => { - expect(id).to.eq(123) - expect(obj.response).to.be.undefined - - expect(browser.windows.remove).to.be.calledWith('9') - expect(browser.windows.remove).to.be.calledWith('11') - expect(browser.windows.remove).not.to.be.calledWith('10') - - done() - }) - - this.server.emit('automation:request', 123, 'reset:browser:tabs:for:next:spec') - }) - - it('does not fail if we are unable to close the window', function (done) { - sinon.stub(browser.windows, 'getAll').resolves([{ id: '9' }, { id: '10' }, { id: '11' }]) - sinon.stub(browser.windows, 'remove').rejects() - - this.socket.on('automation:response', (id, obj) => { - expect(id).to.eq(123) - expect(obj.response).to.be.undefined - - expect(browser.windows.remove).to.be.calledWith('9') - expect(browser.windows.remove).to.be.calledWith('11') - - expect(browser.windows.remove).not.to.be.calledWith('10') - done() - }) - - this.server.emit('automation:request', 123, 'reset:browser:tabs:for:next:spec') - }) - - it('does not fail if we are unable to retrieve the windows', function (done) { - sinon.stub(browser.windows, 'getAll').rejects() - sinon.stub(browser.windows, 'remove') - - this.socket.on('automation:response', (id, obj) => { - expect(id).to.eq(123) - expect(obj.response).to.be.undefined - - expect(browser.windows.remove).not.to.be.called - done() - }) - - this.server.emit('automation:request', 123, 'reset:browser:tabs:for:next:spec') - }) - }) }) }) diff --git a/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts b/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts index 0b5a9cdcd790..55519ef85fe1 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/clientTestContext.ts @@ -31,7 +31,6 @@ export interface ClientTestContext { allBundlers: WizardBundler[] warnings: [] } - migration: {} user: AuthenticatedUserShape | null cloudTypes: typeof cloudTypes __mockPartial: any @@ -104,7 +103,6 @@ export function makeClientTestContext (): ClientTestContext { }, ], }, - migration: {}, __mockPartial: {}, } } diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts deleted file mode 100644 index 38e429b7bbe9..000000000000 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { MIGRATION_STEPS } from '@packages/types' -import type { Migration } from '../generated/test-graphql-types.gen' -import type { MaybeResolver } from './clientTestUtils' - -let _id = 0 - -const id = () => { - _id++ - - return _id.toString() -} - -export const stubMigration: MaybeResolver = { - __typename: 'Migration', - filteredSteps: MIGRATION_STEPS.map((name, index) => { - return { - id: (index + 1).toString(), - index: index + 1, - isCompleted: false, - isCurrentStep: name === 'renameAuto', - __typename: 'MigrationStep', - name, - } - }), - specFiles: [ - { - __typename: 'MigrationFile', - testingType: 'e2e', - before: { - __typename: 'MigrationFileData', - id: id(), - relative: 'cypress/integration/app.spec.js', - parts: [ - { id: id(), __typename: 'MigrationFilePart', text: 'cypress/', highlight: false }, - { id: id(), __typename: 'MigrationFilePart', text: 'integration', highlight: true }, - { id: id(), __typename: 'MigrationFilePart', text: '/app', highlight: false }, - { id: id(), __typename: 'MigrationFilePart', text: '.spec.', highlight: true }, - { id: id(), __typename: 'MigrationFilePart', text: 'js', highlight: false }, - ], - }, - after: { - __typename: 'MigrationFileData', - id: id(), - relative: 'cypress/integration/app.spec.js', - parts: [ - { id: id(), __typename: 'MigrationFilePart', text: 'cypress/', highlight: false }, - { id: id(), __typename: 'MigrationFilePart', text: 'integration', highlight: true }, - { id: id(), __typename: 'MigrationFilePart', text: '/app', highlight: false }, - { id: id(), __typename: 'MigrationFilePart', text: '.cy.', highlight: true }, - { id: id(), __typename: 'MigrationFilePart', text: 'js', highlight: false }, - ], - }, - }, - ], - manualFiles: { - id: id(), - __typename: 'ManualMigration', - completed: false, - files: [ - { - id: id(), - __typename: 'ManualMigrationFile', - moved: false, - relative: 'cypress/component/button-spec.js', - }, - { - id: id(), - __typename: 'ManualMigrationFile', - moved: true, - relative: 'cypress/component/hello.spec.tsx', - }, - ], - }, - configBeforeCode: `{ - "baseUrl": "http://localhost:1234/", - "retries": 2 - }`, - configAfterCode: `const { defineConfig } = require('cypress') - - module.exports = defineConfig({ - retries: 2, - e2e: { - // End-to-end config overrides go here - baseUrl: "http://localhost:1234/" - - setupNodeEvents (on, config) { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these directly - return require('cypress/plugins/index.js')(on, config) } - } - }, - })`, - integrationFolder: 'cypress/integration', - componentFolder: 'cypress/component', - supportFiles: - { - __typename: 'MigrationFile', - testingType: 'e2e', - before: { - id: id(), - relative: 'cypress/support/index.js', - __typename: 'MigrationFileData', - parts: [ - { - id: id(), - __typename: 'MigrationFilePart', - text: 'cypress/support/', - highlight: false, - }, - { - id: id(), - __typename: 'MigrationFilePart', - text: 'index', - highlight: true, - }, - { - id: id(), - __typename: 'MigrationFilePart', - text: '.js', - highlight: false, - }, - ], - }, - after: { - id: id(), - relative: 'cypress/support/e2e.js', - __typename: 'MigrationFileData', - parts: [ - { - id: id(), - __typename: 'MigrationFilePart', - text: 'cypress/support/', - highlight: false, - }, - { - id: id(), - __typename: 'MigrationFilePart', - text: 'e2e', - highlight: true, - }, - { - id: id(), - __typename: 'MigrationFilePart', - text: '.js', - highlight: false, - }, - ], - }, - }, - hasComponentTesting: true, - hasCustomComponentFolder: false, - hasCustomComponentTestFiles: false, - hasCustomIntegrationFolder: false, - hasCustomIntegrationTestFiles: false, - configFileNameAfter: 'cypress.config.js', - configFileNameBefore: 'cypress.json', - shouldMigratePreExtension: true, -} diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts index 14dabf0b80a0..648a126b03f7 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Query.ts @@ -13,9 +13,6 @@ export const stubQuery: MaybeResolver = { wizard (source, args, ctx) { return ctx.wizard }, - migration (source, args, ctx) { - return ctx.migration - }, currentProject (source, args, ctx) { return ctx.currentProject }, diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Registry.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Registry.ts index 384862cb8687..3dec22ae3216 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Registry.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Registry.ts @@ -4,7 +4,6 @@ import { stubMutation } from './stubgql-Mutation' import { stubQuery } from './stubgql-Query' import { stubGlobalProject, stubProject } from './stubgql-Project' import { CloudOrganizationStubs, CloudProjectStubs, CloudRecordKeyStubs, CloudRunStubs, CloudUserStubs } from '@packages/graphql/test/stubCloudTypes' -import { stubMigration } from './stubgql-Migration' import type { CodegenTypeMap } from '../generated/test-graphql-types.gen' import { StubErrorWrapper } from './stubgql-ErrorWrapper' @@ -15,7 +14,6 @@ export const GQLStubRegistry = { ProjectLike: stubProject, GlobalProject: stubGlobalProject, CurrentProject: stubProject, - Migration: stubMigration, Mutation: stubMutation, Query: stubQuery, CloudOrganization: CloudOrganizationStubs.cyOrg, diff --git a/packages/frontend-shared/src/gql-components/HeaderBarContent.vue b/packages/frontend-shared/src/gql-components/HeaderBarContent.vue index 0820e23a9c0c..64c575848bb6 100644 --- a/packages/frontend-shared/src/gql-components/HeaderBarContent.vue +++ b/packages/frontend-shared/src/gql-components/HeaderBarContent.vue @@ -221,10 +221,6 @@ mutation GlobalPageHeader_clearCurrentProject { currentProject { id } - # This ensures the cache is updated with null after clearing project - migration { - configFileNameBefore - } } } ` diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 15f7658290ec..fc7466b6b3ea 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -909,97 +909,6 @@ "link": "install Cypress" } }, - "migration": { - "before": "Before", - "after": "After", - "heresWhy": "here's why:", - "renameAuto": { - "title": "We recommend automatically renaming your specs in this step", - "changeButton": "change", - "changedSpecFolder": "We've changed the default spec folder from:", - "changedSpecExt": "We've changed the default spec file extension from:", - "changedSpecPatternExplain": "We've changed the default spec file extension to {0} in order to avoid conflicts with any existing testing frameworks.", - "optedOutMessage": "You've opted not to rename your spec file extension. You may need to change your specPattern later so we can still find your spec files.", - "folderRenameMessage": "You've opted not to rename your spec file extension, we'll only rename the folder", - "modal": { - "title": "Change the existing spec file extension", - "warning": "We recommend using the default extension to avoid inconsistencies, framework conflicts, and confusion with your team.", - "line1": "Cypress now supports the ability to create new spec files from within the UI for both E2E and component specs.", - "line2": "All new spec files created within Cypress will use the default pattern of: ", - "line3": "We want to rename your existing specs so that they have a consistent filename pattern for both E2E and component testing.", - "line4": "All documentation and example code will be using: ", - "line5": "We've changed the placement of component specs to be next to their source files (e.g. src/Button.jsx and src/Button.cy.jsx)", - "line6": "The new default pattern of {0} prevents targeting conflicts with other testing frameworks. (e.g. Jest)", - "label": "Choose from the following filename patterns:", - "option1": "{0} (recommended)", - "option2": "Don't rename anything — keep what I have.", - "option3": "Rename folder only.", - "optOutAdditional": "I may need to change my {0} later if I don't use the recommended filename extension.", - "buttonSave": "Save changes", - "buttonCancel": "Cancel" - } - }, - "renameManual": { - "title": "We need you to move your component specs manually", - "componentFolderRemoved": "We've removed the {0} options from the Cypress config.", - "addedSpecPattern": "We've added a new {0} option in the Cypress config that tells us where to find your component specs.", - "cannotAuto": "We can't automatically migrate your existing component spec files. We recommend that you move the following component spec files next to your source component files (e.g. {0})", - "ifSkipNote": "If you skip this step, Cypress will still be able to find them, but any new specs that you create will automatically be created next to your component files." - }, - "renameSupport": { - "title": "We'll automatically rename your existing E2E support file in this step", - "serveDifferentTypes": "We now serve different support files for E2E and Component Testing.", - "changedSupportFile": "We've renamed the E2E support file from:" - }, - "configFile": { - "title": "We need to migrate to the new Cypress configuration file", - "changedDefault": "We've changed the default Cypress config file from:", - "customOptions": "We've set a custom {specPattern} option based on your {options}.", - "willConvert": "We'll automatically create a new {jsFile} file and seed it with your options from your existing {jsonFile}." - }, - "setupComponent": { - "title": "You need to reconfigure Cypress for component testing", - "line1": "We've detected that you are currently using the experimental version of component testing.", - "line2": "Your existing configuration is no longer compatible with new component testing configuration options.", - "line3": "In a previous step, we renamed your component specs, but can't automatically migrate your existing component testing configuration.", - "line4": "In the next screen, you'll be able to reconfigure component testing in a new guided configuration wizard." - }, - "wizard": { - "title": "Migrating to Cypress {version}", - "description": "Your project requires updates to work with this version.", - "typicalMigrationLabel": "Typical migration:", - "typicalMigrationTime": "5-10 minutes", - "step1": { - "title": "Migrate existing specs", - "description": "In this step, we'll automatically rename and/or move your existing spec files as needed.", - "button": "Rename these specs for me", - "buttonSkip": "Skip renaming specs", - "buttonRenameFolder": "Rename the folder for me" - }, - "step2": { - "title": "Move your existing component specs", - "description": "In this step, you'll manually move your existing component specs to their new default location.", - "buttonWait": "Waiting for you to move your component specs...", - "buttonDone": "Continue to next step", - "button": "I'll do this later" - }, - "step3": { - "title": "Rename the Cypress support file", - "description": "In this step, we'll automatically rename your existing support file.", - "button": "Rename the support file for me" - }, - "step4": { - "title": "Migrate to the new Cypress configuration file", - "description": "In this step, we'll automatically migrate your existing Cypress configuration to the new Cypress configuration file.", - "button": "Migrate the configuration for me" - }, - "step5": { - "title": "Reconfigure component testing", - "description": "In this step, we'll explain how you will reconfigure Cypress for component testing.", - "button": "Finish migration and continue" - } - } - }, "majorVersionWelcome": { "title": "What's New in Cypress", "actionContinue": "Continue", diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 9a0e706ad44a..50c448792667 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -1041,9 +1041,6 @@ type CurrentProject implements Node & ProjectLike { """Whether the project has Typescript""" isUsingTypeScript: Boolean - - """Whether the project needs to be migrated before proceeding""" - needsLegacyConfigMigration: Boolean packageManager: PackageManagerEnum! """Cached preferences for this project""" @@ -1129,7 +1126,6 @@ enum ErrorTypeEnum { CANNOT_TRASH_ASSETS CDP_COULD_NOT_CONNECT CDP_COULD_NOT_RECONNECT - CDP_FIREFOX_DEPRECATED CDP_RETRYING_CONNECTION CDP_VERSION_TOO_OLD CHROME_137_LOAD_EXTENSION_NOT_SUPPORTED @@ -1163,18 +1159,15 @@ enum ErrorTypeEnum { CLOUD_STALE_RUN CLOUD_UNKNOWN_CREATE_RUN_WARNING CLOUD_UNKNOWN_INVALID_REQUEST - COMPONENT_FOLDER_REMOVED COMPONENT_TESTING_MISMATCHED_DEPENDENCIES CONFIG_FILES_LANGUAGE_CONFLICT CONFIG_FILE_DEV_SERVER_INVALID_RETURN CONFIG_FILE_DEV_SERVER_IS_NOT_VALID - CONFIG_FILE_INVALID_DEV_START_EVENT CONFIG_FILE_INVALID_ROOT_CONFIG CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT CONFIG_FILE_INVALID_ROOT_CONFIG_E2E CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E - CONFIG_FILE_MIGRATION_NEEDED CONFIG_FILE_NOT_FOUND CONFIG_FILE_REQUIRE_ERROR CONFIG_FILE_SETUP_NODE_EVENTS_ERROR @@ -1187,46 +1180,31 @@ enum ErrorTypeEnum { DUPLICATE_TASK_KEY ERROR_READING_FILE ERROR_WRITING_FILE - EXPERIMENTAL_COMPONENT_TESTING_REMOVED EXPERIMENTAL_JIT_COMPILE_REMOVED - EXPERIMENTAL_NETWORK_STUBBING_REMOVED EXPERIMENTAL_ORIGIN_DEPENDENCIES_E2E_ONLY EXPERIMENTAL_RUN_ALL_SPECS_E2E_ONLY - EXPERIMENTAL_RUN_EVENTS_REMOVED - EXPERIMENTAL_SAMESITE_REMOVED EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED - EXPERIMENTAL_SESSION_SUPPORT_REMOVED - EXPERIMENTAL_SHADOW_DOM_REMOVED EXPERIMENTAL_SINGLE_TAB_RUN_MODE EXPERIMENTAL_SKIP_DOMAIN_INJECTION_REMOVED EXPERIMENTAL_STUDIO_E2E_ONLY - EXPERIMENTAL_STUDIO_REMOVED EXTENSION_NOT_LOADED FIREFOX_CDP_FAILED_TO_CONNECT FIREFOX_COULD_NOT_CONNECT - FIREFOX_GC_INTERVAL_REMOVED FIREFOX_GECKODRIVER_FAILURE FIXTURE_NOT_FOUND FOLDER_NOT_WRITABLE FREE_PLAN_EXCEEDS_MONTHLY_TESTS FREE_PLAN_IN_GRACE_PERIOD_EXCEEDS_MONTHLY_TESTS FREE_PLAN_IN_GRACE_PERIOD_PARALLEL_FEATURE - INCOMPATIBLE_PLUGIN_RETRIES INCORRECT_CI_BUILD_ID_USAGE INDETERMINATE_CI_BUILD_ID INJECT_DOCUMENT_DOMAIN_DEPRECATION INJECT_DOCUMENT_DOMAIN_E2E_ONLY - INTEGRATION_FOLDER_REMOVED INVALID_CONFIG_OPTION INVALID_CYPRESS_INTERNAL_ENV INVALID_REPORTER_NAME INVOKED_BINARY_OUTSIDE_NPM_MODULE JIT_COMPONENT_TESTING - LEGACY_CONFIG_ERROR_DURING_MIGRATION - LEGACY_CONFIG_FILE - MIGRATION_ALREADY_OCURRED - MIGRATION_CYPRESS_NOT_FOUND - MIGRATION_MISMATCHED_CYPRESS_VERSIONS MULTIPLE_SUPPORT_FILES_FOUND NO_DEFAULT_CONFIG_FILE_FOUND NO_PROJECT_FOUND_AT_PROJECT_ROOT @@ -1235,7 +1213,6 @@ enum ErrorTypeEnum { PARALLEL_FEATURE_NOT_AVAILABLE_IN_PLAN PLAN_EXCEEDS_MONTHLY_TESTS PLAN_IN_GRACE_PERIOD_RUN_GROUPING_FEATURE_USED - PLUGINS_FILE_CONFIG_OPTION_REMOVED PLUGINS_RUN_EVENT_ERROR PORT_IN_USE_LONG PORT_IN_USE_SHORT @@ -1248,14 +1225,12 @@ enum ErrorTypeEnum { RENAMED_CONFIG_OPTION RENDERER_CRASHED RUN_GROUPING_FEATURE_NOT_AVAILABLE_IN_PLAN - SETUP_NODE_EVENTS_DO_NOT_SUPPORT_DEV_SERVER SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR SETUP_NODE_EVENTS_IS_NOT_FUNCTION SUPPORT_FILE_NOT_FOUND TESTING_TYPE_NOT_CONFIGURED TESTS_DID_NOT_START_FAILED TESTS_DID_NOT_START_RETRYING - TEST_FILES_RENAMED UNEXPECTED_BEFORE_BROWSER_LAUNCH_PROPERTIES UNEXPECTED_INTERNAL_ERROR UNEXPECTED_MUTATION_ERROR @@ -1483,149 +1458,6 @@ input LocalTestCountsInput { totalTests: Int! } -type ManualMigration implements Node { - """is the manual migration completed (all files are moved)""" - completed: Boolean! - - """files needing manual migration""" - files: [ManualMigrationFile!]! - - """Relay style Node ID field for the ManualMigration field""" - id: ID! -} - -type ManualMigrationFile implements Node { - """Relay style Node ID field for the ManualMigrationFile field""" - id: ID! - - """has the file been moved since opening the migration helper""" - moved: Boolean! - - """name of file to migrate""" - relative: String! -} - -"""Contains all data related to the 9.X to 10.0 migration UI""" -type Migration { - """the component folder path used to store components tests""" - componentFolder: String! - - """contents of the cypress.json file after conversion""" - configAfterCode: String! - - """contents of the cypress.json file before conversion""" - configBeforeCode: String! - - """the name of the config file after the migration""" - configFileNameAfter: String! - - """the name of the config file to be migrated""" - configFileNameBefore: String! - - """Steps filtered with the current context""" - filteredSteps: [MigrationStep!]! - - """whether component testing is set up in the migrated config or not""" - hasComponentTesting: Boolean! - - """whether the component folder is custom or not""" - hasCustomComponentFolder: Boolean! - - """whether the testFiles member is custom or not in component testing""" - hasCustomComponentTestFiles: Boolean! - - """whether the integration folder is custom or not""" - hasCustomIntegrationFolder: Boolean! - - """whether the testFiles member is custom or not in integration""" - hasCustomIntegrationTestFiles: Boolean! - - """the integration folder path used to store e2e tests""" - integrationFolder: String! - - """Whether the project has Typescript""" - isUsingTypeScript: Boolean - - """List of files needing manual conversion""" - manualFiles: ManualMigration - - """whether the pre extension info should be displayed""" - shouldMigratePreExtension: Boolean - - """All spec files after conversion""" - specFiles: [MigrationFile!]! - - """Support files needing automated rename""" - supportFiles: MigrationFile -} - -type MigrationFile { - after: MigrationFileData! - before: MigrationFileData! - testingType: TestingTypeEnum! -} - -type MigrationFileData implements Node { - """Relay style Node ID field for the MigrationFileData field""" - id: ID! - parts: [MigrationFilePart!]! - relative: String! -} - -type MigrationFilePart implements Node { - """is this part a folder or extension that needs migration""" - group: String - - """should highlight in migration UI""" - highlight: Boolean! - - """Relay style Node ID field for the MigrationFilePart field""" - id: ID! - - """part of filename""" - text: String! -} - -type MigrationRegexp { - """regexp to use to rename existing specs in component""" - afterComponent: String! - - """regexp to use to rename existing specs in e2e""" - afterE2E: String! - - """regexp to identify existing specs in component""" - beforeComponent: String! - - """regexp to identify existing specs in e2e""" - beforeE2E: String! -} - -"""Contains all data related to the 9.X to 10.0 migration UI""" -type MigrationStep implements Node { - """Relay style Node ID field for the MigrationStep field""" - id: ID! - - """Index of the step in the list""" - index: Int! - - """Has the current step been completed""" - isCompleted: Boolean! - - """This is the current step""" - isCurrentStep: Boolean! - - """Identifier of the step""" - name: MigrationStepEnum! -} - -enum MigrationStepEnum { - configFile - renameAuto - renameManual - renameSupport - setupComponent -} - type Mutation { """Internal use only, clears the cloud cache""" _clearCloudCache: Boolean @@ -1677,9 +1509,6 @@ type Mutation { dismissWarning(id: ID!): Query e2eExamples: [ScaffoldedFile!]! - """user has finished migration component specs - move to next step""" - finishedRenamingComponentSpecs: Query - """Sets focus to the active browser window""" focusActiveBrowserWindow: Boolean! @@ -1723,34 +1552,6 @@ type Mutation { """Check if a give spec file will match the project spec pattern""" matchesSpecPattern(specFile: String!): Boolean! - """While migrating to 10+ skip manual rename step""" - migrateCloseManualRenameWatcher: Boolean - - """Merges the component testing config in cypress.config.{js,ts}""" - migrateComponentTesting: Query - - """Transforms cypress.json file into cypress.config.js file""" - migrateConfigFile: Query - - """While migrating to 10+ renames files to match the new .cy pattern""" - migrateRenameSpecs( - """specs to move - current name""" - after: [String!] - - """specs to move - current name""" - before: [String!] - skip: Boolean - ): Query - - """When the user decides to skip specs rename""" - migrateRenameSpecsFolder: Query - - """While migrating to 10+ launch renaming of support file""" - migrateRenameSupport: Query - - """While migrating to 10+ skip manual rename step""" - migrateSkipManualRename: Query - """ Allow the relevant run for debugging marked as next to be considered the current relevant run """ @@ -2059,9 +1860,6 @@ type Query { Unique node machine identifier for this instance - may be nil if unable to resolve """ machineId: String - - """Metadata about the migration, null if we aren't showing it""" - migration: Migration node(id: ID!): Node """Defines the suggested polling intervals for various schema resources""" diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts index 9f80d0de8828..724e8ad16749 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts @@ -112,13 +112,6 @@ export const CurrentProject = objectType({ }, }) - t.boolean('needsLegacyConfigMigration', { - description: 'Whether the project needs to be migrated before proceeding', - resolve (source, args, ctx) { - return ctx.migration.needsCypressJsonMigration() - }, - }) - t.boolean('hasValidConfigFile', { description: 'Whether the project has a valid config file', resolve (source, args, ctx) { diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts deleted file mode 100644 index 1ed9082a0b92..000000000000 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { enumType, objectType } from 'nexus' -import { TestingTypeEnum } from '..' -import { MIGRATION_STEPS } from '@packages/types' - -export const MigrationStepEnum = enumType({ - name: 'MigrationStepEnum', - members: MIGRATION_STEPS, -}) - -export const MigrationStep = objectType({ - name: 'MigrationStep', - node: 'name', - description: 'Contains all data related to the 9.X to 10.0 migration UI', - definition (t) { - t.nonNull.field('name', { - type: MigrationStepEnum, - description: 'Identifier of the step', - }) - - t.nonNull.boolean('isCurrentStep', { - description: 'This is the current step', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.step === source.name - }, - }) - - t.nonNull.boolean('isCompleted', { - description: 'Has the current step been completed', - resolve: (source, args, ctx) => { - const indexOfObservedStep = ctx.coreData.migration.filteredSteps.indexOf(source.name) - const indexOfCurrentStep = ctx.coreData.migration.filteredSteps.indexOf(ctx.coreData.migration.step) - - return indexOfObservedStep < indexOfCurrentStep - }, - }) - - t.nonNull.int('index', { - description: 'Index of the step in the list', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.filteredSteps.indexOf(source.name) + 1 - }, - }) - }, -}) - -export const MigrationFilePart = objectType({ - name: 'MigrationFilePart', - node: (obj) => obj.text, - definition (t) { - t.nonNull.string('text', { - description: 'part of filename', - }) - - t.nonNull.boolean('highlight', { - description: 'should highlight in migration UI', - }) - - t.string('group', { - description: 'is this part a folder or extension that needs migration', - }) - }, -}) - -export const ManualMigrationFile = objectType({ - name: 'ManualMigrationFile', - node: 'relative', - definition (t) { - t.nonNull.boolean('moved', { - description: 'has the file been moved since opening the migration helper', - }) - - t.nonNull.string('relative', { - description: 'name of file to migrate', - }) - }, -}) - -export const ManualMigration = objectType({ - name: 'ManualMigration', - node: ({ files }) => files.map((f) => f.relative).join(), - definition (t) { - t.nonNull.list.nonNull.field('files', { - type: ManualMigrationFile, - description: 'files needing manual migration', - }) - - t.nonNull.boolean('completed', { - description: 'is the manual migration completed (all files are moved)', - }) - }, -}) - -export const MigrationFileData = objectType({ - name: 'MigrationFileData', - node: (obj) => obj.parts.map((file) => file.text).join(''), - definition (t) { - t.nonNull.string('relative') - - t.nonNull.list.nonNull.field('parts', { - type: MigrationFilePart, - }) - }, -}) - -export const MigrationFile = objectType({ - name: 'MigrationFile', - definition (t) { - t.nonNull.field('testingType', { - type: TestingTypeEnum, - }) - - t.nonNull.field('before', { - type: MigrationFileData, - }) - - t.nonNull.field('after', { - type: MigrationFileData, - }) - }, -}) - -export const MigrationRegexp = objectType({ - name: 'MigrationRegexp', - definition (t) { - t.nonNull.string('beforeE2E', { - description: 'regexp to identify existing specs in e2e', - }) - - t.nonNull.string('afterE2E', { - description: 'regexp to use to rename existing specs in e2e', - }) - - t.nonNull.string('beforeComponent', { - description: 'regexp to identify existing specs in component', - }) - - t.nonNull.string('afterComponent', { - description: 'regexp to use to rename existing specs in component', - }) - }, -}) - -export const Migration = objectType({ - name: 'Migration', - description: 'Contains all data related to the 9.X to 10.0 migration UI', - definition (t) { - t.nonNull.list.nonNull.field('filteredSteps', { - type: MigrationStep, - description: 'Steps filtered with the current context', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.filteredSteps.map((name) => { - return { - name, - } - }) - }, - }) - - t.nonNull.list.nonNull.field('specFiles', { - description: 'All spec files after conversion', - type: MigrationFile, - resolve: async (source, args, ctx) => { - const result = await ctx.migration.getSpecsForMigrationGuide() - - return result - }, - }) - - t.field('manualFiles', { - description: 'List of files needing manual conversion', - type: ManualMigration, - resolve: async (source, args, ctx) => { - // avoid starting the watcher when not on this step - if (ctx.coreData.migration.step !== 'renameManual') { - return null - } - - const status = await ctx.migration.getComponentTestingMigrationStatus() - - if (!status) { - return null - } - - return { - completed: status.completed, - // we sort it to make sure the endpoint always returns the - // specs in the same order, so things don't jump around. - files: [...status.files.values()] - .sort((x, y) => y.relative.length - x.relative.length), - } - }, - }) - - t.field('supportFiles', { - description: 'Support files needing automated rename', - type: MigrationFile, - resolve: (source, args, ctx) => { - return ctx.migration.supportFilesForMigrationGuide() - }, - }) - - t.nonNull.string('configFileNameBefore', { - description: 'the name of the config file to be migrated', - resolve: (source, args, ctx) => { - return ctx.migration.legacyConfigFile - }, - }) - - t.nonNull.string('configFileNameAfter', { - description: 'the name of the config file after the migration', - resolve: (source, args, ctx) => { - return ctx.migration.configFileNameAfterMigration - }, - }) - - t.nonNull.string('configBeforeCode', { - description: 'contents of the cypress.json file before conversion', - resolve: (source, args, ctx) => { - return JSON.stringify(ctx.coreData.migration.legacyConfigForMigration, null, 2) - }, - }) - - t.nonNull.string('configAfterCode', { - description: 'contents of the cypress.json file after conversion', - resolve: (source, args, ctx) => { - return ctx.migration.createConfigString() - }, - }) - - t.nonNull.string('integrationFolder', { - description: 'the integration folder path used to store e2e tests', - resolve: async (source, args, ctx) => (await ctx.migration.integrationFolder()).toString(), - }) - - t.nonNull.string('componentFolder', { - description: 'the component folder path used to store components tests', - resolve: async (source, args, ctx) => (await ctx.migration.componentFolder()).toString(), - }) - - t.nonNull.boolean('hasCustomIntegrationFolder', { - description: 'whether the integration folder is custom or not', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.flags.hasCustomIntegrationFolder - } - , - }) - - t.nonNull.boolean('hasCustomIntegrationTestFiles', { - description: 'whether the testFiles member is custom or not in integration', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.flags.hasCustomIntegrationTestFiles - }, - }) - - t.nonNull.boolean('hasCustomComponentFolder', { - description: 'whether the component folder is custom or not', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.flags.hasCustomComponentFolder - }, - }) - - t.nonNull.boolean('hasCustomComponentTestFiles', { - description: 'whether the testFiles member is custom or not in component testing', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.flags.hasCustomComponentTestFiles - }, - }) - - t.nonNull.boolean('hasComponentTesting', { - description: 'whether component testing is set up in the migrated config or not', - resolve: (source, args, ctx) => { - return ctx.coreData.migration.flags.hasComponentTesting - }, - }) - - t.boolean('isUsingTypeScript', { - description: 'Whether the project has Typescript', - resolve (source, args, ctx) { - return ctx.lifecycleManager.metaState.isUsingTypeScript - }, - }) - - t.boolean('shouldMigratePreExtension', { - description: 'whether the pre extension info should be displayed', - resolve: (source, args, ctx) => { - return ctx.migration.shouldMigratePreExtension - }, - }) - }, -}) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 3bd84c9a21b2..c0fbc5d9c403 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -489,102 +489,6 @@ export const mutation = mutationType({ }, }) - t.field('migrateRenameSpecs', { - description: 'While migrating to 10+ renames files to match the new .cy pattern', - type: Query, - args: { - skip: booleanArg(), - before: list(nonNull(stringArg({ - description: 'specs to move - current name', - }))), - after: list(nonNull(stringArg({ - description: 'specs to move - current name', - }))), - }, - resolve: async (_, { skip, before, after }, ctx) => { - if (!skip && before && after) { - await ctx.actions.migration.renameSpecFiles(before, after) - } - - await ctx.actions.migration.nextStep() - - return {} - }, - }) - - t.field('migrateRenameSpecsFolder', { - description: 'When the user decides to skip specs rename', - type: Query, - resolve: async (_, args, ctx) => { - await ctx.actions.migration.renameSpecsFolder() - await ctx.actions.migration.nextStep() - - return {} - }, - }) - - t.field('migrateSkipManualRename', { - description: 'While migrating to 10+ skip manual rename step', - type: Query, - resolve: async (_, args, ctx) => { - await ctx.actions.migration.nextStep() - - return {} - }, - }) - - t.field('migrateCloseManualRenameWatcher', { - description: 'While migrating to 10+ skip manual rename step', - type: 'Boolean', - resolve: async (_, args, ctx) => { - await ctx.actions.migration.closeManualRenameWatcher() - - return true - }, - }) - - t.field('finishedRenamingComponentSpecs', { - description: 'user has finished migration component specs - move to next step', - type: Query, - resolve: async (_, args, ctx) => { - await ctx.actions.migration.nextStep() - - return {} - }, - }) - - t.field('migrateRenameSupport', { - description: 'While migrating to 10+ launch renaming of support file', - type: Query, - resolve: async (_, args, ctx) => { - await ctx.actions.migration.renameSupportFile() - await ctx.actions.migration.nextStep() - - return {} - }, - }) - - t.field('migrateConfigFile', { - description: 'Transforms cypress.json file into cypress.config.js file', - type: Query, - resolve: async (_, args, ctx) => { - await ctx.actions.migration.createConfigFile() - await ctx.actions.migration.nextStep() - - return {} - }, - }) - - t.field('migrateComponentTesting', { - description: 'Merges the component testing config in cypress.config.{js,ts}', - type: Query, - resolve: async (_, args, ctx) => { - await ctx.actions.migration.nextStep() - - return {} - }, - }) - t.field('setProjectIdInConfigFile', { description: 'Set the projectId field in the config file of the current project', type: Query, diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts index 7486dbc3a077..3f39fc15cd7c 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Query.ts @@ -5,7 +5,6 @@ import { CurrentProject } from './gql-CurrentProject' import { DevState } from './gql-DevState' import { AuthState } from './gql-AuthState' import { LocalSettings } from './gql-LocalSettings' -import { Migration } from './gql-Migration' import { VersionData } from './gql-VersionData' import { Wizard } from './gql-Wizard' import { ErrorWrapper } from './gql-ErrorWrapper' @@ -40,23 +39,6 @@ export const Query = objectType({ resolve: (root, args, ctx) => ctx.coreData.wizard, }) - t.field('migration', { - type: Migration, - description: 'Metadata about the migration, null if we aren\'t showing it', - resolve: async (root, args, ctx) => { - // First check to see if "legacyConfigForMigration" is defined as that means we have started migration - if (ctx.coreData.migration.legacyConfigForMigration) return ctx.coreData.migration.legacyConfigForMigration - - if (!ctx.migration.needsCypressJsonMigration()) { - return null - } - - await ctx.lifecycleManager.legacyMigration() - - return ctx.coreData.migration.legacyConfigForMigration - }, - }) - t.nonNull.field('dev', { type: DevState, description: 'The state of any info related to local development of the runner', diff --git a/packages/graphql/src/schemaTypes/objectTypes/index.ts b/packages/graphql/src/schemaTypes/objectTypes/index.ts index 127bcb5d8aa4..994260fdca42 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/index.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/index.ts @@ -17,7 +17,6 @@ export * from './gql-GeneratedSpecError' export * from './gql-GitInfo' export * from './gql-GlobalProject' export * from './gql-LocalSettings' -export * from './gql-Migration' export * from './gql-Mutation' export * from './gql-ProjectPreferences' export * from './gql-Query' diff --git a/packages/launcher/lib/known-browsers.ts b/packages/launcher/lib/known-browsers.ts index 0d1fa044fa23..ea6b9fca19a7 100644 --- a/packages/launcher/lib/known-browsers.ts +++ b/packages/launcher/lib/known-browsers.ts @@ -1,5 +1,24 @@ import type { Browser, BrowserValidatorResult, FoundBrowser } from '@packages/types' +const firefoxValidatorFn = (browser: FoundBrowser, platform: NodeJS.Platform): BrowserValidatorResult => { + try { + if (browser.majorVersion) { + const majorVersion = Number(browser.majorVersion) + + if (majorVersion < 135) { + return { + isSupported: false, + warningMessage: `Cypress does not support running ${browser.displayName} version ${browser.majorVersion} due to lack of WebDriver BiDi support. To use ${browser.displayName} with Cypress, install version 135 or newer.`, + } + } + } + } catch (e) { /* empty */ } + + return { + isSupported: true, + } +} + /** list of the browsers we can detect and use by default */ export const knownBrowsers: Browser[] = [ { @@ -62,9 +81,10 @@ export const knownBrowsers: Browser[] = [ family: 'firefox', channel: 'stable', displayName: 'Firefox', - // Mozilla Firefox 70.0.1 + // Mozilla Firefox 135.0.1 versionRegex: /^Mozilla Firefox ([^\sab]+)$/m, binary: 'firefox', + validator: firefoxValidatorFn, }, { name: 'firefox', @@ -75,6 +95,7 @@ export const knownBrowsers: Browser[] = [ versionRegex: /^Mozilla Firefox (\S+b\S*)$/m, // ubuntu PPAs install it as firefox binary: ['firefox-developer-edition', 'firefox'], + validator: firefoxValidatorFn, }, { name: 'firefox', @@ -85,6 +106,7 @@ export const knownBrowsers: Browser[] = [ versionRegex: /^Mozilla Firefox (\S+a\S*)$/m, // ubuntu PPAs install it as firefox-trunk binary: ['firefox-nightly', 'firefox-trunk'], + validator: firefoxValidatorFn, }, { name: 'edge', diff --git a/packages/launcher/test/unit/browsers_spec.ts b/packages/launcher/test/unit/browsers_spec.ts index 79ec5514b3c3..19f8f9ba808d 100644 --- a/packages/launcher/test/unit/browsers_spec.ts +++ b/packages/launcher/test/unit/browsers_spec.ts @@ -71,6 +71,25 @@ describe('browsers', () => { expect(result.isSupported).to.be.true expect(result.warningMessage).to.be.undefined }) + + describe('firefox validation', () => { + const FIREFOX_KNOWN_BROWSER_CHANNELS = knownBrowsers.filter((browser) => { + return browser.family === 'firefox' + }) + + FIREFOX_KNOWN_BROWSER_CHANNELS.forEach((browser) => { + it(`${browser.channel}: fails validation when Firefox major version is below 135`, () => { + // @ts-expect-error + const result = browser.validator({ + majorVersion: '134', + displayName: 'Firefox', + }) + + expect(result.isSupported).to.be.false + expect(result.warningMessage).to.equal('Cypress does not support running Firefox version 134 due to lack of WebDriver BiDi support. To use Firefox with Cypress, install version 135 or newer.') + }) + }) + }) }) }) }) diff --git a/packages/launcher/test/unit/linux_spec.ts b/packages/launcher/test/unit/linux_spec.ts index 1f030b2e2b8a..429894fc9d69 100644 --- a/packages/launcher/test/unit/linux_spec.ts +++ b/packages/launcher/test/unit/linux_spec.ts @@ -85,15 +85,15 @@ describe('linux browser detection', () => { name: 'firefox', family: 'firefox', displayName: 'Firefox', - majorVersion: '99', + majorVersion: '135', path: 'firefox', profilePath: '/home/foo/snap/firefox/current', - version: '99.2.3', + version: '135.0.1', } beforeEach(() => { execa.withArgs('firefox', ['--version']) - .resolves({ stdout: 'Mozilla Firefox 99.2.3' }) + .resolves({ stdout: 'Mozilla Firefox 135.0.1' }) sinon.stub(os, 'homedir').returns('/home/foo') }) diff --git a/packages/launchpad/README.md b/packages/launchpad/README.md index 31677558cbf4..c05f85bb1dbd 100644 --- a/packages/launchpad/README.md +++ b/packages/launchpad/README.md @@ -9,8 +9,7 @@ It replaces the original electron app, `desktop-gui`. - Allow users to log in through Cypress Cloud - Onboarding for new users (configure Component Testing dev server, install dependencies, etc) - Select testing mode (E2E, Component) -- Provide UI to perform automated migration steps (for example migrating `cypress.json` to `cypress.config.js` for projects upgrading from 9.x or below) -- Provide a dismissable Welcome Screen for every major release of Cypress +- Provide a dismissible Welcome Screen for every major release of Cypress It is using the following technologies: @@ -29,7 +28,7 @@ Cypress' entire back-end is powered by the `@packages/server` package. Launchpad ## Major Version Welcome Content -The content is bundled with the launchpad and at the time of writing this, it lives in `src/migration/MajorVersionWelcome.vue`. Shipping it as part of the app means it is always available upon release and it will always work offline. Guidelines for the management of the content itself are documented internally in our `prod-eng-docs`, but the implementation is documented here. +The content is bundled with the launchpad and at the time of writing this, it lives in `src/welcome/MajorVersionWelcome.vue`. Shipping it as part of the app means it is always available upon release and it will always work offline. Guidelines for the management of the content itself are documented internally in our `prod-eng-docs`, but the implementation is documented here. A constant named `MAJOR_VERSION_FOR_CONTENT` defines which major version the content is associated with for the purposes of recording user dismissal in persisted state. This needs to be bumped to match the major version that will be released, since that value is the key that records the dismissal. diff --git a/packages/launchpad/cypress/e2e/config-files-error-handling.cy.ts b/packages/launchpad/cypress/e2e/config-files-error-handling.cy.ts index ee005506d828..cbac655cca88 100644 --- a/packages/launchpad/cypress/e2e/config-files-error-handling.cy.ts +++ b/packages/launchpad/cypress/e2e/config-files-error-handling.cy.ts @@ -1,5 +1,3 @@ -import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' -import pkg from '../../../../package.json' import { getPathForPlatform } from './support/getPathForPlatform' const expectStackToBe = (mode: 'open' | 'closed') => { @@ -34,54 +32,19 @@ describe('Config files error handling', () => { cy.contains('h1', 'Welcome to Cypress', { timeout: 10000 }) }) - it('shows the upgrade screen if there is a legacy config file', () => { - cy.openProject('pristine-with-e2e-testing') - cy.withCtx(async (ctx) => { - await ctx.actions.file.writeFileInProject('cypress.json', '{}') - await ctx.actions.file.removeFileInProject('cypress.config.js') - }) - - cy.openProject('pristine-with-e2e-testing') - - cy.visitLaunchpad() - - cy.get('body').should('contain.text', defaultMessages.migration.wizard.title.replace('{version}', pkg.version.split('.')[0])) - cy.get('body').should('contain.text', defaultMessages.migration.wizard.description) - }) - - it('handles config files with legacy config file in same project', () => { - cy.openProject('pristine-with-e2e-testing') - cy.withCtx(async (ctx) => { - await ctx.actions.file.writeFileInProject('cypress.json', '{}') - }) - - cy.openProject('pristine-with-e2e-testing') - cy.visitLaunchpad() - - cy.contains('p', 'There is both a cypress.config.js and a cypress.json file at the location below:') - cy.contains('body', 'Cypress no longer supports cypress.json') - expectStackToBe('closed') - - cy.withCtx(async (ctx) => { - await ctx.actions.file.removeFileInProject('cypress.json') - }) - - cy.findByRole('button', { name: 'Try again' }).click() - - cy.contains('h1', 'Welcome to Cypress', { timeout: 10000 }) - }) - - it('handles deprecated config fields', () => { + it('handles removed config fields', () => { cy.openProject('pristine') cy.withCtx(async (ctx) => { - await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { e2e: { supportFile: false, experimentalComponentTesting: true } }') + // ensure the config set here has 'isWarning: false' to ensure it errors in UI + // supportFile is required so the config is valid + await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { e2e: { supportFile: false, experimentalSkipDomainInjection: true } }') }) cy.openProject('pristine') cy.visitLaunchpad() cy.get('[data-cy-testingType=e2e]').click() - cy.get('body', { timeout: 10000 }).should('contain.text', 'experimentalComponentTesting') + cy.get('body', { timeout: 10000 }).should('contain.text', 'experimentalSkipDomainInjection') expectStackToBe('closed') cy.withCtx(async (ctx) => { await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { e2e: { supportFile: false } }') @@ -149,7 +112,7 @@ describe('Launchpad: Error System Tests', () => { cy.contains('h1', cy.i18n.launchpadErrors.generic.configErrorTitle, { timeout: 10000 }) cy.findAllByTestId('collapsible').should('be.visible') - cy.contains('h2', 'TSError') + cy.contains('h2', 'TransformError') cy.contains('p', 'Your configFile is invalid:') cy.contains('p', getPathForPlatform('cy-projects/config-with-ts-syntax-error/cypress.config.ts')) cy.contains('p', 'It threw an error when required, check the stack trace below:') @@ -207,48 +170,40 @@ describe('Launchpad: Error System Tests', () => { cy.visitLaunchpad() cy.contains('h1', cy.i18n.launchpadErrors.generic.configErrorTitle, { timeout: 10000 }) cy.findAllByTestId('collapsible').should('be.visible') - cy.contains('h2', 'TSError') + cy.contains('h2', 'TransformError') cy.contains('p', 'Your configFile is invalid:') cy.contains('p', getPathForPlatform('cy-projects/config-with-ts-module-error/cypress.config.ts')) cy.contains('p', 'It threw an error when required, check the stack trace below:') - cy.get('[data-testid="error-code-frame"]').should('contain', 'cypress.config.ts:6:10') + cy.get('[data-testid="error-code-frame"]').should('contain', 'cypress.config.ts:6:9') }) }) describe('setupNodeEvents', () => { - it('throws an error when in setupNodeEvents updating a config value that was removed in 10.X', () => { - cy.scaffoldProject('config-update-non-migrated-value') - cy.openProject('config-update-non-migrated-value') + it('throws an error when in setupNodeEvents updating a config value in the root config that was removed', () => { + cy.scaffoldProject('config-update-in-setup-node-events') + cy.openProject('config-update-in-setup-node-events') cy.visitLaunchpad() cy.findByText('E2E Testing').click() cy.contains('h1', cy.i18n.launchpadErrors.generic.configErrorTitle, { timeout: 10000 }) cy.findAllByTestId('collapsible').should('be.visible') cy.get('h2').contains('Error running e2e.setupNodeEvents()') - cy.get('p').contains('The integrationFolder configuration option is now invalid when set on the config object in Cypress version 10.0.0.') - cy.get('p').contains('It is now renamed to specPattern and configured separately as a end to end testing property: e2e.specPattern') + cy.get('p').contains('The experimentalSkipDomainInjection experiment is over.') + cy.get('p').contains('Read the migration guide for Cypress v14.0.0') }) - it('throws an error when in setupNodeEvents updating a config value on a clone of config that was removed in 10.X', () => { - cy.scaffoldProject('config-update-non-migrated-value-clone') - cy.openProject('config-update-non-migrated-value-clone') + it('throws an error when in setupNodeEvents updating a config value on a clone of config in the root config that was removed', () => { + cy.scaffoldProject('config-update-in-setup-node-events-clone') + cy.openProject('config-update-in-setup-node-events-clone') cy.visitLaunchpad() cy.findByText('E2E Testing').click() cy.contains('h1', cy.i18n.launchpadErrors.generic.configErrorTitle, { timeout: 10000 }) cy.percySnapshot() - cy.get('[data-cy="alert-body"]').should('contain', 'integrationFolder') - }) - - it('throws an error when in setupNodeEvents updating an e2e config value that was removed in 10.X', () => { - cy.scaffoldProject('config-update-non-migrated-value-e2e') - cy.openProject('config-update-non-migrated-value-e2e') - cy.visitLaunchpad() - cy.findByText('E2E Testing').click() - cy.contains('h1', cy.i18n.launchpadErrors.generic.configErrorTitle, { timeout: 10000 }) - cy.percySnapshot() + cy.get('p').contains('The experimentalSkipDomainInjection experiment is over.') + cy.get('p').contains('Read the migration guide for Cypress v14.0.0') }) - it('handles deprecated config fields in setupNodeEvents', () => { + it('handles removed config fields in setupNodeEvents', () => { cy.scaffoldProject('pristine') cy.openProject('pristine') cy.withCtx(async (ctx) => { @@ -257,7 +212,7 @@ describe('setupNodeEvents', () => { e2e: { supportFile: false, setupNodeEvents(on, config){ - config.testFiles = '**/*.spec.js' + config.experimentalSkipDomainInjection = true return config } } @@ -268,7 +223,7 @@ describe('setupNodeEvents', () => { cy.visitLaunchpad() cy.get('[data-cy-testingType=e2e]').click() - cy.get('body', { timeout: 10000 }).should('contain.text', 'testFiles') + cy.get('body', { timeout: 10000 }).should('contain.text', 'experimentalSkipDomainInjection') cy.get('body', { timeout: 10000 }).should('contain.text', 'setupNodeEvents') expectStackToBe('closed') cy.withCtx(async (ctx) => { @@ -300,7 +255,7 @@ describe('setupNodeEvents', () => { cy.findByRole('button', { name: 'Try again' }).click() cy.get('[data-cy-testingType=e2e]').click() cy.contains('h1', cy.i18n.launchpadErrors.generic.configErrorTitle, { timeout: 10000 }) - cy.get('[data-cy="alert-body"]').should('contain', 'The baseUrl configuration option is now invalid when set from the root of the config object') + cy.get('[data-cy="alert-body"]').should('contain', 'The baseUrl configuration option is invalid when set from the root of the config object') cy.withCtx(async (ctx) => { await ctx.actions.file.writeFileInProject('cypress.config.js', `module.exports = { e2e: { baseUrl: 'http://localhost:3000', supportFile: false } }`) diff --git a/packages/launchpad/cypress/e2e/migration.cy.ts b/packages/launchpad/cypress/e2e/migration.cy.ts deleted file mode 100644 index 94d3e96610e9..000000000000 --- a/packages/launchpad/cypress/e2e/migration.cy.ts +++ /dev/null @@ -1,1662 +0,0 @@ -import type { ProjectFixtureDir } from '@tooling/system-tests' -import { getPathForPlatform } from './support/getPathForPlatform' - -// @ts-ignore -const platform = window.Cypress.platform - -const renameAutoStep = `[data-cy="migration-step renameAuto"]` -const renameManualStep = `[data-cy="migration-step renameManual"]` -const renameSupportStep = `[data-cy="migration-step renameSupport"]` -const configFileStep = `[data-cy="migration-step configFile"]` -const setupComponentStep = `[data-cy="migration-step setupComponent"]` - -declare global { - namespace Cypress { - interface Chainable { - waitForWizard(): Cypress.Chainable> - } - } -} - -Cypress.Commands.add('waitForWizard', () => { - return cy.get('[data-cy="migration-wizard"]') -}) - -function scaffoldAndVisitLaunchpad (project: ProjectFixtureDir, argv?: string[]) { - cy.scaffoldProject(project) - cy.openProject(project, argv) - cy.visitLaunchpad() -} - -function startMigrationFor (project: ProjectFixtureDir, argv?: string[]) { - scaffoldAndVisitLaunchpad(project, argv) - cy.waitForWizard() -} - -function skipCTMigration () { - cy.contains(`I'll do this later`).click() -} - -function migrateAndVerifyConfig (migratedConfigFile: string = 'cypress.config.js') { - cy.contains('Migrate the configuration for me').click() - - cy.withRetryableCtx(async (ctx, o) => { - const configStats = await ctx.file.checkIfFileExists(o.migratedConfigFile) - - expect(configStats).to.not.be.null.and.not.be.undefined - - const oldConfigStats = ctx.migration.legacyConfigFileExists() - - expect(oldConfigStats).to.be.false - - await ctx.actions.migration.assertSuccessfulConfigMigration(o.migratedConfigFile) - }, { migratedConfigFile }) -} - -function finishMigrationAndContinue () { - cy.contains('Finish migration and continue').click() -} - -function checkOutcome () { - cy.contains('Welcome to Cypress!', { timeout: 10000 }).should('be.visible') -} - -function runAutoRename () { - cy.get('button').contains('Rename these specs for me').click() -} - -function renameSupport (lang: 'js' | 'ts' | 'coffee' = 'js') { - cy.contains(`Rename the support file for me`).click() - - // give to to finish the file rename - cy.wait(200) - - cy.withCtx(async (ctx, { lang }) => { - expect( - await ctx.file.checkIfFileExists(ctx.path.join('cypress', 'support', `e2e.${lang}`)), 'support file not renamed', - ).not.to.be.null - }, { lang }) -} - -describe('global mode', () => { - it('migrates 2 projects in global mode', () => { - cy.openGlobalMode() - cy.addProject('migration-e2e-export-default') - cy.addProject('migration-e2e-custom-integration-with-projectId') - cy.visitLaunchpad() - - cy.withCtx(async (ctx, o) => { - o.sinon.stub(ctx.actions.migration, 'locallyInstalledCypressVersion').resolves((await ctx.versions.versionData()).current.version) - }) - - cy.contains('migration-e2e-export-default').click() - - // rename integration->e2e - cy.get(renameAutoStep, { timeout: 10000 }).should('exist') - cy.get(renameManualStep).should('not.exist') - - // cypress/support/index.ts -> cypress/support/e2e.ts - cy.get(renameSupportStep).should('exist') - // no component specs - cy.get(setupComponentStep).should('not.exist') - - cy.get(configFileStep).should('exist') - - runAutoRename() - renameSupport('ts') - migrateAndVerifyConfig('cypress.config.ts') - checkOutcome() - - cy.contains('Projects').click() - cy.contains('migration-e2e-custom-integration-with-projectId').click() - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('not.exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) -}) - -describe('Opening unmigrated project', () => { - it('legacy project with --e2e', () => { - cy.scaffoldProject('migration') - cy.openProject('migration', ['--e2e']) - cy.visitLaunchpad() - cy.get('h1').should('contain', 'Migrating') - }) - - it('legacy project with --component', () => { - cy.scaffoldProject('migration-component-testing') - cy.openProject('migration-component-testing', ['--component']) - cy.visitLaunchpad() - cy.get('h1').should('contain', 'Migrating') - }) - - it('major version welcome page appears with correct links and can be dismissed', () => { - cy.scaffoldProject('migration') - cy.openProject('migration') - cy.visitLaunchpad({ showWelcome: true }) - - cy.contains(cy.i18n.majorVersionWelcome.title).should('be.visible') - - cy.validateExternalLink({ - name: cy.i18n.majorVersionWelcome.linkReleaseNotes, - href: 'https://on.cypress.io/changelog', - }) - - cy.validateExternalLink({ - name: '11.0.0', - href: 'https://on.cypress.io/changelog#11-0-0', - }) - - cy.validateExternalLink({ - name: '10.0.0', - href: 'https://on.cypress.io/changelog#10-0-0', - }) - - cy.contains('button', cy.i18n.majorVersionWelcome.actionContinue).click() - cy.contains(cy.i18n.majorVersionWelcome.title).should('not.exist') - cy.contains('h1', `Migrating to Cypress ${Cypress.version.split('.')[0]}`).should('be.visible') - - // Wait for migration prompt and current version to load before taking a snapshot - cy.get('.spinner').should('not.exist') - cy.findByTestId('top-nav-cypress-version-current-link').should('be.visible') - - cy.percySnapshot() - }) -}) - -describe('Full migration flow for each project', { retries: { openMode: 0, runMode: 2 }, defaultCommandTimeout: 10000 }, () => { - it('completes journey for migration-component-testing', () => { - startMigrationFor('migration-component-testing') - // custom testFiles - cannot auto - cy.get(renameAutoStep).should('not.exist') - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - migrateAndVerifyConfig() - finishMigrationAndContinue() - checkOutcome() - }) - - it('completes journey for migration-component-testing-defaults', () => { - startMigrationFor('migration-component-testing-defaults') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/component/button.spec.js') - cy.contains('cypress/component/input-spec.tsx') - - // after auto migration - cy.contains('cypress/component/button.cy.js') - cy.contains('cypress/component/input.cy.tsx') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/component/button.cy.js', - 'cypress/component/input.cy.tsx', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - skipCTMigration() - migrateAndVerifyConfig() - finishMigrationAndContinue() - checkOutcome() - }) - - describe('migration-e2e-component-default-everything', () => { - it('completes journey for migration-e2e-component-default-everything', () => { - startMigrationFor('migration-e2e-component-default-everything') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.ts') - cy.contains('cypress/integration/spec.ts') - cy.contains('cypress/component/button.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.ts') - cy.contains('cypress/e2e/spec.cy.ts') - cy.contains('cypress/component/button.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/e2e/foo.cy.ts', - 'cypress/component/button.cy.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - skipCTMigration() - renameSupport() - migrateAndVerifyConfig() - finishMigrationAndContinue() - - cy.withRetryableCtx(async (ctx) => { - const integrationFolderStats = await ctx.file.checkIfFileExists(ctx.path.join('cypress', 'integration')) - - expect(integrationFolderStats).to.be.null - }) - - checkOutcome() - }) - - it('renames only the folder renaming migration-e2e-defaults-rename-folder-only', () => { - startMigrationFor('migration-e2e-defaults-rename-folder-only') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - - // after auto migration - - cy.get('[data-cy="migrate-before"]').within(() => { - cy.contains('cypress/integration/foo.spec.js') - cy.get('.text-red-500').should('contain', 'spec') - }) - - cy.get('[data-cy="migrate-after"]').within(() => { - cy.contains('cypress/e2e/foo.cy.js') - cy.get('.text-jade-500').should('contain', 'cy') - }) - - cy.findByText('change').click() - - // this project has a default integration folder and default testFiles. - // We rename the integration folder, even if the user skips the spec rename - cy.findByText('Rename folder only.').click() - - cy.findByText('Save changes').click() - - cy.percySnapshot() - - cy.get('[data-cy="migrate-before"]').within(() => { - cy.get('.text-red-500').should('not.contain', 'spec') - }) - - cy.get('[data-cy="migrate-after"]').within(() => { - cy.get('.text-jade-500').should('not.contain', 'cy') - cy.get('.text-jade-500').should('not.contain', 'spec') - }) - - cy.findByText('Rename the folder for me').click() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/e2e/foo.spec.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - }) - - it('completes journey for migration-e2e-component-with-json-files', () => { - startMigrationFor('migration-e2e-component-with-json-files') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.ts') - cy.contains('cypress/integration/spec.ts') - cy.contains('cypress/component/button.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.ts') - cy.contains('cypress/e2e/spec.cy.ts') - cy.contains('cypress/component/button.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/e2e/foo.cy.ts', - 'cypress/component/button.cy.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - skipCTMigration() - renameSupport() - migrateAndVerifyConfig() - finishMigrationAndContinue() - - cy.withCtx(async (ctx) => { - const integrationFolderStats = await ctx.file.checkIfFileExists(ctx.path.join('cypress', 'integration')) - - expect(integrationFolderStats).to.be.null - }) - - checkOutcome() - }) - - it('completes journey for migration-e2e-component-default-with-types', () => { - startMigrationFor('migration-e2e-component-default-with-types') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.ts') - cy.contains('cypress/integration/spec.ts') - cy.contains('cypress/component/button.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.ts') - cy.contains('cypress/e2e/spec.cy.ts') - cy.contains('cypress/component/button.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/e2e/foo.cy.ts', - 'cypress/component/button.cy.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - skipCTMigration() - renameSupport() - migrateAndVerifyConfig() - finishMigrationAndContinue() - - cy.withRetryableCtx(async (ctx) => { - const integrationFolderStats = await ctx.file.checkIfFileExists(ctx.path.join('cypress', 'integration')) - - expect(integrationFolderStats).to.be.null - }) - - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-integration', () => { - startMigrationFor('migration-e2e-custom-integration') - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('src/basic.spec.js') - - // after auto migration - cy.contains('src/basic.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['src/basic.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-supportFile', () => { - startMigrationFor('migration-e2e-custom-supportFile') - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is custom - cannot rename - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/basic.spec.js') - - // after auto migration - cy.contains('cypress/e2e/basic.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/basic.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-supportFile-default-value', () => { - startMigrationFor('migration-e2e-custom-supportFile-default-value') - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/basic.spec.js') - - // after auto migration - cy.contains('cypress/e2e/basic.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/basic.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-integration-default-value', () => { - startMigrationFor('migration-e2e-custom-integration-default-value') - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/basic.spec.js') - - // after auto migration - cy.contains('cypress/e2e/basic.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/basic.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-integration-default-value-and-testFiles', () => { - startMigrationFor('migration-e2e-custom-integration-default-value-and-testFiles') - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/basic.spec.js') - - // after auto migration - cy.contains('cypress/e2e/basic.spec.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/basic.spec.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-integration-with-projectId', () => { - startMigrationFor('migration-e2e-custom-integration-with-projectId') - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('not.exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-custom-test-files', () => { - const project = 'migration-e2e-custom-test-files-array' - - startMigrationFor(project) - // default integration but custom testFiles - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - cy.scaffoldProject(project) - cy.openProject(project) - cy.visitLaunchpad() - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/basic.test.js') - cy.contains('cypress/integration/basic.spec.js') - - // after auto migration - cy.contains('cypress/e2e/basic.test.js') - cy.contains('cypress/e2e/basic.spec.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/basic.test.js', 'cypress/e2e/basic.spec.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-defaults', () => { - startMigrationFor('migration-e2e-defaults') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('ts') - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-defaults-with-projectId', () => { - startMigrationFor('migration-e2e-defaults-with-projectId') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.spec.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.spec.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('ts') - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-defaults-with-nested-projectId', () => { - startMigrationFor('migration-e2e-defaults-with-nested-projectId') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.spec.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.spec.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('ts') - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-module', () => { - startMigrationFor('migration-e2e-module') - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('js') - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-coffeescript', () => { - startMigrationFor('migration-e2e-coffeescript') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.coffee') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.coffee') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.coffee'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('coffee') - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-cjsx', () => { - startMigrationFor('migration-e2e-cjsx') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.cjsx') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.cjsx') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.cjsx'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-plugins-modify-config', () => { - startMigrationFor('migration-e2e-plugins-modify-config') - // No rename, integrationFolder and testFiles are custom (via plugins) - cy.get(renameAutoStep).should('not.exist') - // no CT - cy.get(renameManualStep).should('not.exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-no-plugins-support-file', () => { - startMigrationFor('migration-e2e-no-plugins-support-file') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // no supportFile - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-false-plugins-support-file', () => { - startMigrationFor('migration-e2e-false-plugins-support-file') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // no supportFile - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-plugins-implicit-index-js', () => { - startMigrationFor('migration-e2e-plugins-implicit-index-js') - // no specs, nothing to rename? - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - runAutoRename() - renameSupport() - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-e2e-fully-custom', () => { - startMigrationFor('migration-e2e-fully-custom') - // integration folder and testFiles are custom, cannot rename anything - cy.get(renameAutoStep).should('not.exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is custom - cannot rename - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - migrateAndVerifyConfig() - checkOutcome() - }) - - it('completes journey for migration-component-testing-customized', () => { - startMigrationFor('migration-component-testing-customized') - // cannot rename anything automatically here, testFiles are customized - cy.get(renameAutoStep).should('not.exist') - - cy.get(renameManualStep).should('not.exist') - - // no supportFile rename for CT - cy.get(renameSupportStep).should('not.exist') - - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - migrateAndVerifyConfig() - finishMigrationAndContinue() - checkOutcome() - }) - - it('completes journey for migration-e2e-export-default', () => { - startMigrationFor('migration-e2e-export-default') - // rename integration->e2e - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('not.exist') - - // cypress/support/index.ts -> cypress/support/e2e.ts - cy.get(renameSupportStep).should('exist') - // no component specs - cy.get(setupComponentStep).should('not.exist') - - cy.get(configFileStep).should('exist') - - runAutoRename() - renameSupport('ts') - migrateAndVerifyConfig('cypress.config.ts') - checkOutcome() - }) - - it('completes journey for migration-typescript-project', () => { - startMigrationFor('migration-typescript-project') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx((ctx) => { - ['cypress/e2e/foo.cy.js'].forEach(async (spec) => { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - }) - }) - - renameSupport() - migrateAndVerifyConfig('cypress.config.ts') - checkOutcome() - }) - - it('handles re-migrating a partially migrated codebase', { retries: 0 }, () => { - startMigrationFor('migration-specs-already-migrated') - cy.get(renameAutoStep).should('not.exist') - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/tests/foo.cy.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport('ts') - migrateAndVerifyConfig() - }) - - it('completes journey for migration-e2e-duplicated-spec-names', () => { - startMigrationFor('migration-e2e-duplicated-spec-names') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(configFileStep).should('exist') - - cy.get('[data-cy="migrate-before"]').within(() => { - cy.get('code').eq(0).should('contain', 'cypress/integration/app-spec2.js') - cy.get('code').eq(1).should('contain', 'cypress/integration/app_spec2.js') - cy.get('code').eq(2).should('contain', 'cypress/integration/app.spec.js') - cy.get('code').eq(3).should('contain', 'cypress/integration/app2_spec.js') - cy.get('code').eq(4).should('contain', 'cypress/integration/app_spec.js') - cy.get('code').eq(5).should('contain', 'cypress/integration/app-spec.js') - }) - - cy.get('[data-cy="migrate-after"]').within(() => { - cy.get('code').eq(0).should('contain', 'cypress/e2e/app-spec2.cy.js') - cy.get('code').eq(1).should('contain', 'cypress/e2e/app_spec2.cy.js') - cy.get('code').eq(2).should('contain', 'cypress/e2e/app.cy.js') - cy.get('code').eq(3).should('contain', 'cypress/e2e/app2.cy.js') - cy.get('code').eq(4).should('contain', 'cypress/e2e/app_spec.cy.js') - cy.get('code').eq(5).should('contain', 'cypress/e2e/app-spec.cy.js') - }) - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/e2e/app-spec2.cy.js', - 'cypress/e2e/app_spec2.cy.js', - 'cypress/e2e/app.cy.js', - 'cypress/e2e/app2.cy.js', - 'cypress/e2e/app_spec.cy.js', - 'cypress/e2e/app-spec.cy.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - migrateAndVerifyConfig() - - cy.withCtx(async (ctx) => { - const integrationFolderStats = await ctx.file.checkIfFileExists(ctx.path.join('cypress', 'integration')) - - expect(integrationFolderStats).to.be.null - }) - - checkOutcome() - }) - - context('migration-e2e-component-default-test-files', () => { - it('completes journey', () => { - startMigrationFor('migration-e2e-component-default-test-files') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/custom-integration/foo.spec.ts') - cy.contains('cypress/custom-component/button.spec.js') - - // after auto migration - cy.contains('cypress/custom-integration/foo.cy.ts') - cy.contains('cypress/custom-component/button.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/custom-integration/foo.cy.ts', - 'cypress/custom-component/button.cy.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - finishMigrationAndContinue() - }) - - it('skips the file renaming', () => { - startMigrationFor('migration-e2e-component-default-test-files') - // default testFiles - auto - cy.get(renameAutoStep).should('exist') - // non default component folder - should skip. - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/custom-integration/foo.spec.ts') - cy.contains('cypress/custom-component/button.spec.js') - - // after auto migration - cy.contains('cypress/custom-integration/foo.cy.ts') - cy.contains('cypress/custom-component/button.cy.js') - - cy.findByText('change').click() - - cy.contains('I may need to change my specPattern later').should('not.exist') - cy.findByText('Don\'t rename anything — keep what I have.').click() - cy.contains('I may need to change my specPattern later') - - cy.findByText('Save changes').click() - - cy.findByText('Skip renaming specs').click() - - cy.withRetryableCtx(async (ctx) => { - const specs = [ - 'cypress/custom-integration/foo.spec.ts', - 'cypress/custom-component/button.spec.js', - ] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats).to.not.be.null - } - }) - - renameSupport() - migrateAndVerifyConfig() - }) - }) - - it('completes journey for migration-e2e-legacy-plugins-throws-error and recovers', () => { - scaffoldAndVisitLaunchpad('migration-e2e-legacy-plugins-throws-error') - // no steps are shown - we show the error that surfaced when executing pluginsFile. - cy.get(renameAutoStep).should('not.exist') - cy.get(renameManualStep).should('not.exist') - cy.get(renameSupportStep).should('not.exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('not.exist') - - cy.contains(cy.i18n.launchpadErrors.generic.configErrorTitle) - // correct location of error - const pluginsPath = platform === 'win32' ? 'cypress\\plugins\\index.js:2:9' : 'cypress/plugins/index.js:2:9' - - cy.get('[data-testid="error-code-frame"]').contains(pluginsPath) - // correct error from pluginsFile - cy.contains(`throw Error('Uh oh, there was an error!')`) - - cy.withCtx(async (ctx, o) => { - await ctx.actions.file.writeFileInProject(o.path, 'module.exports = (on, config) => {}') - }, { path: getPathForPlatform('cypress/plugins/index.js') }) - - cy.findByRole('button', { name: 'Try again' }).click() - - cy.waitForWizard() - }) - - it('completes journey for migration-e2e-with-extra-files', () => { - startMigrationFor('migration-e2e-with-extra-files') - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const files = ['cypress/e2e/foo.cy.js', 'cypress/e2e/example.json'] - - for (const file of files) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(file)) - - expect(stats, `file ${file}`).to.not.be.null - } - }) - - renameSupport('ts') - migrateAndVerifyConfig() - checkOutcome() - }) -}) - -// TODO: UNIFY-1350 toLaunchpad emitter not working in Cypress in Cypress, -// re-evaluate after conversion to subscriptions -describe.skip('component testing migration - defaults', () => { - it('live update migration UI as user moves files', () => { - cy.scaffoldProject('migration-component-testing-customized') - cy.openProject('migration-component-testing-customized') - cy.visitLaunchpad() - cy.waitForWizard() - - // need to move your specs before this button shows - cy.get('button').contains('I have moved my component specs').should('not.exist') - - // two files to move, src/button.spec.js and src/input-spec.tsx. - cy.withCtx((ctx) => { - return ctx.actions.file.moveFileInProject('src/button.spec.js', 'src/button.cy.js') - }).then(() => { - cy.get('[data-cy="moved"]').contains('src/button.spec.js') - }) - - cy.withCtx((ctx) => { - return ctx.actions.file.moveFileInProject('src/input-spec.tsx', 'src/input.cy.tsx') - }).then(() => { - cy.get('[data-cy="moved"]').contains('src/input-spec.tsx') - }) - - cy.get('button').contains('I have moved my component specs') - }) - - it('live update migration UI as user moves files', () => { - cy.scaffoldProject('migration-component-testing-customized') - cy.openProject('migration-component-testing-customized') - cy.visitLaunchpad() - cy.waitForWizard() - - // need to move your specs before this button shows - cy.get('button').contains('I have moved my component specs').should('not.exist') - - // two files to move, src/button.spec.js and src/input-spec.tsx. - cy.withCtx((ctx) => { - return ctx.actions.file.moveFileInProject('src/button.spec.js', 'src/button.cy.js') - }).then(() => { - cy.get('[data-cy="moved"]').contains('src/button.spec.js') - }) - - cy.withCtx((ctx) => { - return ctx.actions.file.moveFileInProject('src/input-spec.tsx', 'src/input.cy.tsx') - }).then(() => { - cy.get('[data-cy="moved"]').contains('src/input-spec.tsx') - }) - - cy.get('button').contains('I have moved my component specs') - }) -}) - -describe('Migration', { viewportWidth: 1200, retries: { openMode: 0, runMode: 2 } }, () => { - it('should create the cypress.config.js file and delete old config', () => { - startMigrationFor('migration') - - // all steps - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('not.exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - cy.get('button').contains('Rename these specs for me').scrollIntoView().click() - cy.findByText('Rename the support file for me').click() - cy.findByText('Migrate the configuration for me').click() - - cy.withRetryableCtx(async (ctx) => { - const configStats = await ctx.file.checkIfFileExists('cypress.config.js') - - expect(configStats).to.not.be.null.and.not.be.undefined - - const oldConfigStats = await ctx.file.checkIfFileExists('cypress.json') - - expect(oldConfigStats).to.be.null - - await ctx.actions.migration.assertSuccessfulConfigMigration() - }) - - finishMigrationAndContinue() - checkOutcome() - }) - - it('should show spec pattern rename change modal', () => { - startMigrationFor('migration') - - cy.get(renameAutoStep).should('exist') - cy.get(renameManualStep).should('not.exist') - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('exist') - cy.get(configFileStep).should('exist') - - cy.findByText('change').click() - cy.get('h2').should('contain', 'Change the existing spec file extension') - cy.get('button').get('[aria-label="Close"]').click() - cy.get('h2').should('not.contain', 'Change the existing spec file extension') - - cy.findByText('change').click() - cy.get('h2').should('contain', 'Change the existing spec file extension') - cy.get(renameAutoStep).click({ force: true }) - cy.get('h2').should('not.contain', 'Change the existing spec file extension') - - cy.findByText('change').click() - cy.get('h2').should('contain', 'Change the existing spec file extension') - cy.get('button').contains('Save changes').click() - cy.get('h2').should('not.contain', 'Change the existing spec file extension') - - cy.findByText('change').click() - cy.get('h2').should('contain', 'Change the existing spec file extension') - cy.get('button').contains('Cancel').click() - cy.get('h2').should('not.contain', 'Change the existing spec file extension') - }) - - it('shows error if plugins file throws an error', () => { - scaffoldAndVisitLaunchpad('migration-e2e-plugins-throw-error') - - cy.contains(`${getPathForPlatform('cypress/plugins/index.js')} file threw an error.`) - cy.contains('Please ensure your pluginsFile is valid and relaunch the migration tool to migrate to Cypress version 10.0.0.') - cy.contains('throw new Error(\'New error from plugin\')') - }) -}) - -describe('Migrate custom config files', () => { - it('completes journey for migration-custom-config-file-root-level spaces', () => { - startMigrationFor('migration-custom-config-file-root-level spaces', ['--config-file', 'customConfig.json']) - - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('ts') - - cy.contains('customConfig.json') - cy.contains('customConfig.config.js') - - migrateAndVerifyConfig('customConfig.config.js') - checkOutcome() - }) - - it('completes journey for migration-custom-config-file-respect-pathname', () => { - startMigrationFor('migration-custom-config-file-respect-pathname', ['--config-file', 'cypress.foo.json']) - - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('ts') - - cy.contains('cypress.foo.json') - cy.contains('cypress.foo.config.js') - - migrateAndVerifyConfig('cypress.foo.config.js') - checkOutcome() - }) - - it('completes journey for migration-custom-config-file-respect-dirname', () => { - startMigrationFor('migration-custom-config-file-respect-dirname', ['--config-file', 'config/cypress.foo.json']) - - // defaults, rename all the things - // can rename integration->e2e - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // default testFiles but custom integration - can rename automatically - cy.get(renameAutoStep).should('exist') - // no CT - cy.get(renameManualStep).should('not.exist') - // supportFile is false - cannot migrate - cy.get(renameSupportStep).should('exist') - cy.get(setupComponentStep).should('not.exist') - cy.get(configFileStep).should('exist') - - // Migration workflow - // before auto migration - cy.contains('cypress/integration/foo.spec.js') - - // after auto migration - cy.contains('cypress/e2e/foo.cy.js') - - runAutoRename() - - cy.withRetryableCtx(async (ctx) => { - const specs = ['cypress/e2e/foo.cy.js'] - - for (const spec of specs) { - const stats = await ctx.file.checkIfFileExists(ctx.path.join(spec)) - - expect(stats, `spec file not renamed ${spec}`).to.not.be.null - } - }) - - renameSupport('ts') - - cy.contains('config/cypress.foo.json') - cy.contains('config/cypress.foo.config.js') - - migrateAndVerifyConfig('config/cypress.foo.config.js') - checkOutcome() - }) - - it('shows error for migration-custom-config-file-migration-already-ocurred', () => { - scaffoldAndVisitLaunchpad('migration-custom-config-file-migration-already-ocurred', ['--config-file', 'customConfig.json']) - - cy.contains('You are attempting to use Cypress with an older config file: customConfig.json') - cy.contains('When you upgraded to Cypress v10.0 the config file was updated and moved to a new location: customConfig.config.js') - }) - - it('shows error for migration-custom-config-file-with-existing-v10-config-file', () => { - scaffoldAndVisitLaunchpad('migration-custom-config-file-with-existing-v10-config-file', ['--config-file', 'customConfig.json']) - - cy.contains('There is both a customConfig.config.js and a customConfig.json file at the location below:') - cy.contains('Cypress no longer supports customConfig.json, please remove it from your project.') - }) - - it('shows error if plugins file do not exist', () => { - scaffoldAndVisitLaunchpad('migration', ['--config-file', 'erroredConfigFiles/incorrectPluginsFile.json']) - - const err = `Looked for pluginsFile at foo/bar, but it was not found.` - - cy.contains(err) - }) -}) diff --git a/packages/launchpad/src/Main.vue b/packages/launchpad/src/Main.vue index 4e9e0b0912a6..f60256f5aa98 100644 --- a/packages/launchpad/src/Main.vue +++ b/packages/launchpad/src/Main.vue @@ -23,9 +23,6 @@ v-else-if="query.data.value.isGlobalMode && !query.data.value?.currentProject" :gql="query.data.value" /> -