diff --git a/.circleci/cache-version.txt b/.circleci/cache-version.txt index 42bdc8556864..e0a263cff4be 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. -11-07-23.2 +12-13-23 diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 81c8eb1b9ef1..ed8fb6f1bb70 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -518,13 +518,13 @@ commands: which google-chrome-<> || (printf "\n\033[0;31mChrome was not successfully downloaded - bailing\033[0m\n\n" && exit 1) echo "Location of Google Chrome Installation: `which google-chrome-<>`" echo "Google Chrome Version: `google-chrome-<> --version`" - + # This code builds better-sqlite3 on CentOS 7. This is necessary because CentOS 7 has the oldest glibc version # that we support. The script checks for the existence of the Centos7-builder image tar file, and skips if it already # exists. If you want to rebuild the image, set the REBUILD_CENTOS_BUILDER_IMAGE environment variable to any value. # Since this is running Docker remote, we need to copy the project into the container, and copy the built plugin out # of the container because the host running docker does not have access to the project directory so volume mounts are - # not possible. The built plugin is copied to the project directory so it can be injected into the final binary. + # not possible. The built plugin is copied to the project directory so it can be injected into the final binary. build-better-sqlite3: description: Build better-sqlite3 for CentOS 7 steps: @@ -2048,6 +2048,38 @@ jobs: command: yarn lerna run build --scope @cypress/angular - store-npm-logs + npm-puppeteer-unit-tests: + <<: *defaults + steps: + - restore_cached_workspace + - run: + name: Build + command: yarn lerna run build --scope @cypress/puppeteer + - run: + name: Run tests + command: yarn test + working_directory: npm/puppeteer + - store_test_results: + path: npm/puppeteer/test_results + - store_artifacts: + path: npm/puppeteer/test_results + - store-npm-logs + + npm-puppeteer-cypress-tests: + <<: *defaults + resource_class: small + steps: + - restore_cached_workspace + - restore_cached_system_tests_deps + - run: + command: yarn cypress:run + working_directory: npm/puppeteer + - store_test_results: + path: /tmp/cypress + - store_artifacts: + path: /tmp/artifacts + - store-npm-logs + npm-react: <<: *defaults steps: @@ -2105,13 +2137,6 @@ jobs: path: npm/grep/test_results - store-npm-logs - npm-create-cypress-tests: - <<: *defaults - resource_class: small - steps: - - restore_cached_workspace - - run: yarn lerna run build --scope create-cypress-tests - npm-eslint-plugin-dev: <<: *defaults steps: @@ -2754,6 +2779,12 @@ linux-x64-workflow: &linux-x64-workflow - npm-vue: requires: - build + - npm-puppeteer-unit-tests: + requires: + - build + - npm-puppeteer-cypress-tests: + requires: + - build - npm-react: requires: - build @@ -2763,9 +2794,6 @@ linux-x64-workflow: &linux-x64-workflow - npm-mount-utils: requires: - build - - npm-create-cypress-tests: - requires: - - build - npm-eslint-plugin-dev: requires: - build @@ -2785,7 +2813,6 @@ linux-x64-workflow: &linux-x64-workflow - check-ts - npm-angular - npm-eslint-plugin-dev - - npm-create-cypress-tests - npm-react - npm-mount-utils - npm-vue @@ -2828,6 +2855,8 @@ linux-x64-workflow: &linux-x64-workflow - run-launchpad-component-tests-chrome - run-launchpad-integration-tests-chrome - run-reporter-component-tests-chrome + - npm-puppeteer-unit-tests + - npm-puppeteer-cypress-tests - run-webpack-dev-server-integration-tests - run-vite-dev-server-integration-tests - v8-integration-tests @@ -2839,7 +2868,6 @@ linux-x64-workflow: &linux-x64-workflow - check-ts - npm-angular - npm-eslint-plugin-dev - - npm-create-cypress-tests - npm-react - npm-mount-utils - npm-vue @@ -2875,6 +2903,8 @@ linux-x64-workflow: &linux-x64-workflow - run-launchpad-component-tests-chrome - run-launchpad-integration-tests-chrome - run-reporter-component-tests-chrome + - npm-puppeteer-unit-tests + - npm-puppeteer-cypress-tests - run-webpack-dev-server-integration-tests - run-vite-dev-server-integration-tests - v8-integration-tests @@ -3140,6 +3170,12 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow - npm-vue: requires: - build + - npm-puppeteer-unit-tests: + requires: + - build + - npm-puppeteer-cypress-tests: + requires: + - build - npm-react: requires: - build @@ -3149,9 +3185,6 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow - npm-mount-utils: requires: - build - - npm-create-cypress-tests: - requires: - - build - npm-eslint-plugin-dev: requires: - build @@ -3171,7 +3204,8 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow - check-ts - npm-angular - npm-eslint-plugin-dev - - npm-create-cypress-tests + - npm-puppeteer-unit-tests + - npm-puppeteer-cypress-tests - npm-react - npm-mount-utils - npm-vue @@ -3225,7 +3259,6 @@ linux-x64-contributor-workflow: &linux-x64-contributor-workflow - check-ts - npm-angular - npm-eslint-plugin-dev - - npm-create-cypress-tests - npm-react - npm-mount-utils - npm-vue diff --git a/.eslintrc.js b/.eslintrc.js index 0f6bc16d6bd2..999d42521ef8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,8 +53,6 @@ module.exports = { 'tooling/**', 'packages/{app,driver,frontend-shared,launchpad}/cypress/**', '*.test.ts', - // ignore in packages that don't run in the Cypress process - 'npm/create-cypress-tests/**', ], rules: { 'no-restricted-properties': 'off', diff --git a/.gitignore b/.gitignore index b6583eab0c39..cb6f213a1c87 100644 --- a/.gitignore +++ b/.gitignore @@ -92,7 +92,7 @@ system-tests/lib/fixtureDirs.ts /packages/frontend-shared/src/generated /packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts -# from npm/create-cypress-tests +# from old npm/create-cypress-tests /npm/create-cypress-tests/initial-template /npm/create-cypress-tests/src/test-output diff --git a/CHANGELOG.md b/CHANGELOG.md index becc1f915c87..be435cb6b72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ - [Cypress App](https://on.cypress.io/changelog) - [`@cypress/angular`](https://github.com/cypress-io/cypress/blob/develop/npm/angular/CHANGELOG.md) -- [`@cypress/create-cypress-tests`](https://github.com/cypress-io/cypress/blob/develop/npm/create-cypress-tests/CHANGELOG.md) - [`@cypress/eslint-plugin-dev`](https://github.com/cypress-io/cypress/blob/develop/npm/eslint-plugin-dev/CHANGELOG.md) - [`@cypress/mount-utils`](https://github.com/cypress-io/cypress/blob/develop/npm/mount-utils/CHANGELOG.md) - [`@cypress/react`](https://github.com/cypress-io/cypress/blob/develop/npm/react/CHANGELOG.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db769e1eedd8..9acb46686376 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -183,7 +183,6 @@ Here is a list of the npm packages in this repository: | Folder Name | Package Name | Purpose | | :----------------------------------------------------- | :--------------------------------- | :--------------------------------------------------------------------------- | | [angular](./npm/angular) | `@cypress/angular` | Cypress component testing for Angular. | - | [create-cypress-tests](./npm/create-cypress-tests) | `@cypress/create-cypress-tests` | Tooling to scaffold Cypress configuration and demo test files. | | [eslint-plugin-dev](./npm/eslint-plugin-dev) | `@cypress/eslint-plugin-dev` | Eslint plugin for internal development. | | [grep](./npm/grep) | `@cypress/grep` | Filter tests using substring | | [mount-utils](./npm/mount-utils) | `@cypress/mount-utils` | Common functionality for Vue/React/Angular adapters. | diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 25a51cadf6b8..1a6cac0810c1 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,20 +1,53 @@ +## 13.6.2 + +_Released 12/19/2023 (PENDING)_ + +**Bugfixes:** + +- Fixed a regression in [`12.4.0`](https://docs.cypress.io/guides/references/changelog/12.4.0) where erroneous `
` tags were displaying in error messages in the Command Log making them less readable. Fixes [#28452](https://github.com/cypress-io/cypress/issues/28452). + +## 13.6.1 + +_Released 12/5/2023_ + +**Bugfixes:** + +- Fixed an issue where pages or downloads opened in a new tab were missing basic auth headers. Fixes [#28350](https://github.com/cypress-io/cypress/issues/28350). +- Fixed an issue where request logging would default the `message` to the `args` of the currently running command even though those `args` would not apply to the request log and are not displayed. If the `args` are sufficiently large (e.g. when running the `cy.task` from the [code-coverage](https://github.com/cypress-io/code-coverage/) plugin) there could be performance/memory implications. Addressed in [#28411](https://github.com/cypress-io/cypress/pull/28411). +- Fixed an issue where commands would fail with the error `must only be invoked from the spec file or support file` if the project's `baseUrl` included basic auth credentials. Fixes [#27457](https://github.com/cypress-io/cypress/issues/27457) and [#28336](https://github.com/cypress-io/cypress/issues/28336). +- Fixed an issue where some URLs would timeout in pre-request correlation. Addressed in [#28427](https://github.com/cypress-io/cypress/pull/28427). +- Cypress will now correctly log errors and debug logs on Linux machines. Fixes [#5051](https://github.com/cypress-io/cypress/issues/5051) and [#24713](https://github.com/cypress-io/cypress/issues/24713). + +**Misc:** + +- Artifact upload duration is now reported to Cypress Cloud. Fixes [#28238](https://github.com/cypress-io/cypress/issues/28238). Addressed in [#28418](https://github.com/cypress-io/cypress/pull/28418). + ## 13.6.0 -_Released 11/21/2023 (PENDING)_ +_Released 11/21/2023_ **Features:** -- When artifacts are uploaded to the Cypress Cloud, the duration of each upload will be displayed in the console. Addresses [#28237](https://github.com/cypress-io/cypress/issues/28237). +- Added an activity indicator to CLI output when artifacts (screenshots, videos, or Test Replay) are being uploaded to the cloud. Addresses [#28239](https://github.com/cypress-io/cypress/issues/28239). Addressed in [#28277](https://github.com/cypress-io/cypress/pull/28277). +- When artifacts are uploaded to the Cypress Cloud, the duration of each upload will be displayed in the terminal. Addresses [#28237](https://github.com/cypress-io/cypress/issues/28237). **Bugfixes:** +- We now allow absolute paths when setting `component.indexHtmlFile` in the Cypress config. Fixes [#27750](https://github.com/cypress-io/cypress/issues/27750). +- Fixed an issue where dynamic intercept aliases now show with alias name instead of "no alias" in driver. Addresses [#24653](https://github.com/cypress-io/cypress/issues/24653) +- Fixed an issue where [aliasing individual requests](https://docs.cypress.io/api/commands/intercept#Aliasing-individual-requests) with `cy.intercept()` led to an error when retrieving all of the aliases with `cy.get(@alias.all)` . Addresses [#25448](https://github.com/cypress-io/cypress/issues/25448) - The URL of the application under test and command error "Learn more" links now open externally instead of in the Cypress-launched browser. Fixes [#24572](https://github.com/cypress-io/cypress/issues/24572). +- Fixed issue where some URLs would timeout in pre-request correlation. Addressed in [#28354](https://github.com/cypress-io/cypress/pull/28354). **Misc:** - Browser tabs and windows other than the Cypress tab are now closed between tests in Chromium-based browsers. Addressed in [#28204](https://github.com/cypress-io/cypress/pull/28204). -- Cypress now ensures the main browser tab is active before running eaech command in Chromium-based browsers. Addressed in [#28334](https://github.com/cypress-io/cypress/pull/28334). +- Cypress now ensures the main browser tab is active before running each command in Chromium-based browsers. Addressed in [#28334](https://github.com/cypress-io/cypress/pull/28334). + +**Dependency Updates:** + +- Upgraded [`chrome-remote-interface`](https://www.npmjs.com/package/chrome-remote-interface) from `0.31.3` to `0.33.0` to increase the max payload from 100MB to 256MB. Addressed in [#27998](https://github.com/cypress-io/cypress/pull/27998). ## 13.5.1 diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index d158aa8eb7a9..dabf8f6a3233 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -4,7 +4,6 @@ const cp = require('child_process') const path = require('path') const Promise = require('bluebird') const debug = require('debug')('cypress:cli') -const debugElectron = require('debug')('cypress:electron') const util = require('../util') const state = require('../tasks/state') @@ -122,10 +121,9 @@ module.exports = { return new Promise((resolve, reject) => { _.defaults(overrides, { onStderrData: false, - electronLogging: false, }) - const { onStderrData, electronLogging } = overrides + const { onStderrData } = overrides const envOverrides = util.getEnvOverrides(options) const electronArgs = [] const node11WindowsFix = isPlatform('win32') @@ -160,10 +158,6 @@ module.exports = { stdioOptions = _.extend({}, stdioOptions, { windowsHide: false }) } - if (electronLogging) { - stdioOptions.env.ELECTRON_ENABLE_LOGGING = true - } - if (util.isPossibleLinuxWithIncorrectDisplay()) { // make sure we use the latest DISPLAY variable if any debug('passing DISPLAY', process.env.DISPLAY) @@ -241,7 +235,7 @@ module.exports = { // if we have a callback and this explicitly returns // false then bail - if (onStderrData && onStderrData(str) === false) { + if (onStderrData && onStderrData(str)) { return } @@ -294,13 +288,6 @@ module.exports = { if (util.isBrokenGtkDisplay(str)) { brokenGtkDisplay = true } - - // we should attempt to always slurp up - // the stderr logs unless we've explicitly - // enabled the electron debug logging - if (!debugElectron.enabled) { - return false - } }, }) } diff --git a/cli/lib/tasks/verify.js b/cli/lib/tasks/verify.js index 826c35e1654f..2d946b301e37 100644 --- a/cli/lib/tasks/verify.js +++ b/cli/lib/tasks/verify.js @@ -101,12 +101,8 @@ const runSmokeTest = (binaryDir, options) => { debug('smoke test command:', smokeTestCommand) debug('smoke test timeout %d ms', options.smokeTestTimeout) - const env = _.extend({}, process.env, { - ELECTRON_ENABLE_LOGGING: true, - }) - const stdioOptions = _.extend({}, { - env, + env: process.env, timeout: options.smokeTestTimeout, }) diff --git a/cli/test/lib/tasks/verify_spec.js b/cli/test/lib/tasks/verify_spec.js index b81ff36a55b5..ab4fd5146be0 100644 --- a/cli/test/lib/tasks/verify_spec.js +++ b/cli/test/lib/tasks/verify_spec.js @@ -278,35 +278,6 @@ context('lib/tasks/verify', () => { }) }) - it('sets ELECTRON_ENABLE_LOGGING without mutating process.env', () => { - createfs({ - alreadyVerified: false, - executable: mockfs.file({ mode: 0o777 }), - packageVersion, - }) - - expect(process.env.ELECTRON_ENABLE_LOGGING).to.be.undefined - - util.exec.resolves() - sinon.stub(util, 'stdoutLineMatches').returns(true) - - return verify - .start() - .then(() => { - expect(process.env.ELECTRON_ENABLE_LOGGING).to.be.undefined - - const stdioOptions = util.exec.firstCall.args[2] - - expect(stdioOptions).to.include({ - timeout: verify.VERIFY_TEST_RUNNER_TIMEOUT_MS, - }) - - expect(stdioOptions.env).to.include({ - ELECTRON_ENABLE_LOGGING: true, - }) - }) - }) - describe('with force: true', () => { beforeEach(() => { createfs({ diff --git a/npm/create-cypress-tests/.eslintignore b/npm/create-cypress-tests/.eslintignore deleted file mode 100644 index 341078756065..000000000000 --- a/npm/create-cypress-tests/.eslintignore +++ /dev/null @@ -1,9 +0,0 @@ -**/dist -**/*.d.ts -**/package-lock.json -**/tsconfig.json -**/cypress/fixtures -**/test/fixtures -**/__snapshots__ -/initial-template -/**/*.template.* \ No newline at end of file diff --git a/npm/create-cypress-tests/.eslintrc b/npm/create-cypress-tests/.eslintrc deleted file mode 100644 index d1ad4a04ebbf..000000000000 --- a/npm/create-cypress-tests/.eslintrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "plugins": [ - "cypress", - "@cypress/dev" - ], - "extends": [ - "plugin:@cypress/dev/general", - "plugin:@cypress/dev/tests" - ], - "env": { - "cypress/globals": true - }, - "rules": { - "no-console": "off" - } -} diff --git a/npm/create-cypress-tests/.npmignore b/npm/create-cypress-tests/.npmignore deleted file mode 100644 index c9cfa44194a1..000000000000 --- a/npm/create-cypress-tests/.npmignore +++ /dev/null @@ -1,4 +0,0 @@ -./src/ -./initial-template/ -scripts/ -__snapshots__/ \ No newline at end of file diff --git a/npm/create-cypress-tests/CHANGELOG.md b/npm/create-cypress-tests/CHANGELOG.md deleted file mode 100644 index 720d4a52aab4..000000000000 --- a/npm/create-cypress-tests/CHANGELOG.md +++ /dev/null @@ -1,118 +0,0 @@ -# [create-cypress-tests-v2.0.4](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v2.0.3...create-cypress-tests-v2.0.4) (2023-10-16) - -# [create-cypress-tests-v2.0.3](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v2.0.2...create-cypress-tests-v2.0.3) (2023-09-07) - -# [create-cypress-tests-v2.0.2](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v2.0.1...create-cypress-tests-v2.0.2) (2023-04-07) - -# [create-cypress-tests-v2.0.1](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v2.0.0...create-cypress-tests-v2.0.1) (2023-01-03) - - -### Bug Fixes - -* change wording for spec creation ([#25271](https://github.com/cypress-io/cypress/issues/25271)) ([c12a7e3](https://github.com/cypress-io/cypress/commit/c12a7e37c73d972eb0514e4b602940df210d86c7)) - -# [create-cypress-tests-v2.0.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.3.0...create-cypress-tests-v2.0.0) (2022-06-13) - - -### Bug Fixes - -* scope config to current testing type ([#20677](https://github.com/cypress-io/cypress/issues/20677)) ([61f7cfc](https://github.com/cypress-io/cypress/commit/61f7cfc59284a2938e0a1c15d74ee75215ba5f8b)) -* support using create-cypress-tests as part of build process ([#18714](https://github.com/cypress-io/cypress/issues/18714)) ([0501452](https://github.com/cypress-io/cypress/commit/0501452fb9e2df954ee871171052ab9f01367b25)) -* **unified-desktop-gui branch:** initial installation on windows ([#18247](https://github.com/cypress-io/cypress/issues/18247)) ([8614e97](https://github.com/cypress-io/cypress/commit/8614e978029bcbf7155b7ae98ac54feb11f2e7f3)) - - -### chore - -* prep npm packages for use with Cypress v10 ([b924d08](https://github.com/cypress-io/cypress/commit/b924d086ee2e2ccc93303731e001b2c9e9d0af17)) - - -### Features - -* Add vue2 package from npm/vue/v2 branch ([#21026](https://github.com/cypress-io/cypress/issues/21026)) ([3aa69e2](https://github.com/cypress-io/cypress/commit/3aa69e2538aae5702bfc48789c54f37263ce08fc)) -* Deprecate run-ct / open-ct, and update all examples to use --ct instead ([#18422](https://github.com/cypress-io/cypress/issues/18422)) ([196e8f6](https://github.com/cypress-io/cypress/commit/196e8f62cc6d27974f235945cb5700624b3dae41)) -* remove testFiles reference ([#20565](https://github.com/cypress-io/cypress/issues/20565)) ([5670344](https://github.com/cypress-io/cypress/commit/567034459089d9d53dfab5556cb9369fb335c3db)) -* update on-links ([#19235](https://github.com/cypress-io/cypress/issues/19235)) ([cc2d734](https://github.com/cypress-io/cypress/commit/cc2d7348185e2a090c60d92d9319ab460d8c7827)) -* Use .config files ([#18578](https://github.com/cypress-io/cypress/issues/18578)) ([081dd19](https://github.com/cypress-io/cypress/commit/081dd19cc6da3da229a7af9c84f62730c85a5cd6)) -* use supportFile by testingType ([#19364](https://github.com/cypress-io/cypress/issues/19364)) ([0366d4f](https://github.com/cypress-io/cypress/commit/0366d4fa8971e5e5189c6fd6450cc3c8d72dcfe1)) - - -### BREAKING CHANGES - -* new version of packages for Cypress v10 - -# [create-cypress-tests-v1.3.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.2.0...create-cypress-tests-v1.3.0) (2021-12-16) - - -### Bug Fixes - -* Restore broken gif ([#18987](https://github.com/cypress-io/cypress/issues/18987)) ([f251681](https://github.com/cypress-io/cypress/commit/f251681b814b102ca374abdef148b777c4e72c67)) - - -### Features - -* use hoisted yarn install in binary build ([#17285](https://github.com/cypress-io/cypress/issues/17285)) ([e4f5b10](https://github.com/cypress-io/cypress/commit/e4f5b106d49d6ac0857c5fdac886f83b99558c88)) - -# [create-cypress-tests-v1.2.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.3...create-cypress-tests-v1.2.0) (2021-11-10) - - -### Features - -* **deps:** update dependency electron to v15 🌟 ([#18317](https://github.com/cypress-io/cypress/issues/18317)) ([3095d73](https://github.com/cypress-io/cypress/commit/3095d733e92527ffd67344c6899211e058ceefa3)) - -# [create-cypress-tests-v1.1.3](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.2...create-cypress-tests-v1.1.3) (2021-10-29) - - -### Bug Fixes - -* revive type checker ([#18172](https://github.com/cypress-io/cypress/issues/18172)) ([af472b6](https://github.com/cypress-io/cypress/commit/af472b6419ecb2aec1abdb09df99b2fa5f56e033)) - -# [create-cypress-tests-v1.1.2](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.1...create-cypress-tests-v1.1.2) (2021-06-17) - - -### Bug Fixes - -* case issue create cypress tests with `react/plugins/load-webpack` ([#16961](https://github.com/cypress-io/cypress/issues/16961)) ([c37ecea](https://github.com/cypress-io/cypress/commit/c37ecea3ca462015637515b331d1c9828ac1ed29)), closes [#16960](https://github.com/cypress-io/cypress/issues/16960) - -# [create-cypress-tests-v1.1.1](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.1.0...create-cypress-tests-v1.1.1) (2021-05-10) - - -### Bug Fixes - -* add return config for vitejs templates ([69d9de5](https://github.com/cypress-io/cypress/commit/69d9de581a03dce8e3535917a4cdcea8fa4eb6e9)) -* add return config for vueCli and vueWebpack ([9c12ee6](https://github.com/cypress-io/cypress/commit/9c12ee6d8467c65414ab2d413a9c45b2bbec64e9)) -* remove all of rollup, not supported anymore ([f8a71e7](https://github.com/cypress-io/cypress/commit/f8a71e75ae8208dc628d342cb1054c12f98338e9)) -* typo in the final message (run vs run-ct) ([294db04](https://github.com/cypress-io/cypress/commit/294db04f042dba86b69bb15d847c80a2c4202e80)) -* vueCli and webpack key vue@2 fix when guessing ([89f1bb9](https://github.com/cypress-io/cypress/commit/89f1bb9bc6bd987fbf6679a9d955c3587e69aa61)) - -# [create-cypress-tests-v1.1.0](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.0.1...create-cypress-tests-v1.1.0) (2021-04-05) - - -### Bug Fixes - -* **component-testing:** Fix webpack-dev-server deps validation crash ([#15708](https://github.com/cypress-io/cypress/issues/15708)) ([254eb47](https://github.com/cypress-io/cypress/commit/254eb47d91c75a9f56162e7493ab83e5be169935)) - - -### Features - -* support ct/e2e specific overrides in cypress.json ([#15526](https://github.com/cypress-io/cypress/issues/15526)) ([43c8ae2](https://github.com/cypress-io/cypress/commit/43c8ae2a7c20ba70a0bb0b45b8f6a086e2782f29)) - -# [create-cypress-tests-v1.0.1](https://github.com/cypress-io/cypress/compare/create-cypress-tests-v1.0.0...create-cypress-tests-v1.0.1) (2021-03-16) - - -### Bug Fixes - -* add missing script for building wizard ([#15502](https://github.com/cypress-io/cypress/issues/15502)) ([393a8ca](https://github.com/cypress-io/cypress/commit/393a8ca9cac905e0f6d8623bff889b041dd076b6)) - -# create-cypress-tests-v1.0.0 (2021-03-15) - - -### Bug Fixes - -* **runner-ct:** open link in external browser ([#15420](https://github.com/cypress-io/cypress/issues/15420)) ([d291157](https://github.com/cypress-io/cypress/commit/d291157f07ffebe961527fdd85c7ec51056801e7)) - - -### Features - -* **@cypress/react:** Make correct plugins for different adapters/bundlers ([#15337](https://github.com/cypress-io/cypress/issues/15337)) ([fc30118](https://github.com/cypress-io/cypress/commit/fc301182523f0a645bfb17ea3b541644b9732dd0)), closes [#9116](https://github.com/cypress-io/cypress/issues/9116) -* create-cypress-tests installation wizard ([#9563](https://github.com/cypress-io/cypress/issues/9563)) ([c405ee8](https://github.com/cypress-io/cypress/commit/c405ee89ef5321df6151fdeec1e917ac952c0d38)), closes [#9116](https://github.com/cypress-io/cypress/issues/9116) -* create-cypress-tests wizard ([#8857](https://github.com/cypress-io/cypress/issues/8857)) ([21ee591](https://github.com/cypress-io/cypress/commit/21ee591d1e9c4083a0c67f2062ced92708c0cedd)) diff --git a/npm/create-cypress-tests/README.md b/npm/create-cypress-tests/README.md deleted file mode 100644 index 528987627d36..000000000000 --- a/npm/create-cypress-tests/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Create Cypress Tests - -Installs and injects all the required configuration to run cypress tests. - -## Quick overview - -``` -cd my-app -npx create-cypress-tests -npx cypress open -``` - -![demo](./demo.gif) - -## Package manager - -This wizard will automatically determine which package do you use. If `yarn` available as global dependency it will use yarn to install dependencies and create lock file. - -If you need to use `npm` over `yarn` you can do the following - -``` -npx create-cypress-tests --use-npm -``` - -By the way you can use yarn to run the installation wizard 😉 - -``` -yarn create cypress-tests -``` - -## Typescript - -This package will also automatically determine if typescript if available in this project and inject the required typescript configuration for cypress. If you are starting a new project and want to create typescript configuration, please do the following: - -``` -npm init -npm install typescript -npx create-cypress-tests -``` - -## Configuration - -Here is a list of available configuration options: - -`--use-npm` – use npm if yarn available -`--ignore-typescript` – will not create typescript configuration if available -`--ignore-examples` – will create a 1 template spec file (`cypress/integration/spec.js`) to start with -`--component-tests` – will not ask should setup component testing or not - -## License - -The project is licensed under the terms of [MIT license](../../LICENSE) - -## Changelog - -[Changelog](./CHANGELOG.md) diff --git a/npm/create-cypress-tests/__snapshots__/babel.test.ts.js b/npm/create-cypress-tests/__snapshots__/babel.test.ts.js deleted file mode 100644 index 182ba10a2fb4..000000000000 --- a/npm/create-cypress-tests/__snapshots__/babel.test.ts.js +++ /dev/null @@ -1,13 +0,0 @@ -exports['babel installation template correctly generates plugins config 1'] = ` -const injectDevServer = require('@cypress/react/plugins/babel'); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js b/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js deleted file mode 100644 index f1ff8aa8b366..000000000000 --- a/npm/create-cypress-tests/__snapshots__/init-component-testing.test.ts.js +++ /dev/null @@ -1,43 +0,0 @@ -exports['injects guessed next.js template cypress.config.ts'] = ` -export default { - specPattern: "src/**/*.spec.{js,ts,jsx,tsx}" -}; - -` - -exports['injects guessed next.js template plugins/index.js'] = ` -const injectDevServer = require("@cypress/react/plugins/next"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; - -` - -exports['Injected overridden webpack template cypress.config.ts'] = ` -export default { - specPattern: "cypress/component/**/*.spec.{js,ts,jsx,tsx}" -}; - -` - -exports['Injected overridden webpack template plugins/index.js'] = ` -const injectDevServer = require("@cypress/react/plugins/react-scripts"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; - -` - -exports['Injected overridden webpack template support/component.js'] = ` -import "./commands.js"; -` diff --git a/npm/create-cypress-tests/__snapshots__/next.test.ts.js b/npm/create-cypress-tests/__snapshots__/next.test.ts.js deleted file mode 100644 index 5d8602de1bbd..000000000000 --- a/npm/create-cypress-tests/__snapshots__/next.test.ts.js +++ /dev/null @@ -1,13 +0,0 @@ -exports['next.js install template correctly generates plugins config 1'] = ` -const injectDevServer = require('@cypress/react/plugins/next'); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js b/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js deleted file mode 100644 index b960b4be6f6a..000000000000 --- a/npm/create-cypress-tests/__snapshots__/react-scripts.test.ts.js +++ /dev/null @@ -1,13 +0,0 @@ -exports['create-react-app install template correctly generates plugins config 1'] = ` -const injectDevServer = require('@cypress/react/plugins/react-scripts'); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js deleted file mode 100644 index 5e36dd80347c..000000000000 --- a/npm/create-cypress-tests/__snapshots__/reactWebpackFile.test.ts.js +++ /dev/null @@ -1,32 +0,0 @@ -exports['webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` -const injectDevServer = require("@cypress/react/plugins/load-webpack"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config, { - // TODO replace with valid webpack config path - webpackFilename: './webpack.config.js' - }); - } - - return config; // IMPORTANT to return a config -}; -` - -exports['webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` -const injectDevServer = require("@cypress/react/plugins/load-webpack"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - injectDevServer(on, config, { - webpackFilename: 'config/webpack.config.js' - }); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/vite.test.ts.js b/npm/create-cypress-tests/__snapshots__/vite.test.ts.js deleted file mode 100644 index 5fc7bc31a4fb..000000000000 --- a/npm/create-cypress-tests/__snapshots__/vite.test.ts.js +++ /dev/null @@ -1,17 +0,0 @@ -exports['vue: vite template correctly generates plugins config 1'] = ` -const { - startDevServer -} = require("@cypress/vite-dev-server"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on("dev-server:start", async options => startDevServer({ - options - })); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js deleted file mode 100644 index fa70c0ef2db7..000000000000 --- a/npm/create-cypress-tests/__snapshots__/vueCli.test.ts.js +++ /dev/null @@ -1,20 +0,0 @@ -exports['vue webpack-file install template correctly generates plugins for vue-cli-service 1'] = ` -const { - startDevServer -} = require("@cypress/webpack-dev-server"); - -const webpackConfig = require("@vue/cli-service/webpack.config.js"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js b/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js deleted file mode 100644 index b686b719276e..000000000000 --- a/npm/create-cypress-tests/__snapshots__/vueWebpackFile.test.ts.js +++ /dev/null @@ -1,42 +0,0 @@ -exports['vue webpack-file install template correctly generates plugins config when webpack config path is missing 1'] = ` -const { - startDevServer -} = require("@cypress/webpack-dev-server"); - -const webpackConfig = require("./webpack.config.js"); // TODO replace with valid webpack config path - - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } - - return config; // IMPORTANT to return a config -}; -` - -exports['vue webpack-file install template correctly generates plugins config when webpack config path is provided 1'] = ` -const { - startDevServer -} = require("@cypress/webpack-dev-server"); - -const webpackConfig = require("build/webpack.config.js"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } - - return config; // IMPORTANT to return a config -}; -` diff --git a/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js b/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js deleted file mode 100644 index f384e4b912a1..000000000000 --- a/npm/create-cypress-tests/__snapshots__/webpackOptions.test.ts.js +++ /dev/null @@ -1,40 +0,0 @@ -exports['webpack-options template correctly generates plugins config 1'] = ` -const path = require("path"); - -const { - startDevServer -} = require("@cypress/webpack-dev-Server"); - -const something = require("something"); - -module.exports = (on, config) => { - if (config.testingType === "component") { - /** @type import("webpack").Configuration */ - const webpackConfig = { - resolve: { - extensions: ['.js', '.ts', '.jsx', '.tsx'] - }, - mode: 'development', - devtool: false, - output: { - publicPath: '/', - chunkFilename: '[name].bundle.js' - }, - // TODO: update with valid configuration for your components - module: { - rules: [{ - test: /\\.(js|jsx|mjs|ts|tsx)$/, - loader: 'babel-loader', - options: { - cacheDirectory: path.resolve(__dirname, '.babel-cache') - } - }] - } - }; - on('dev-server:start', options => startDevServer({ - options, - webpackConfig - })); - } -}; -` diff --git a/npm/create-cypress-tests/cypress.config.js b/npm/create-cypress-tests/cypress.config.js deleted file mode 100644 index 4ba52ba2c8df..000000000000 --- a/npm/create-cypress-tests/cypress.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {} diff --git a/npm/create-cypress-tests/demo.gif b/npm/create-cypress-tests/demo.gif deleted file mode 100644 index 344fc47d3faa..000000000000 Binary files a/npm/create-cypress-tests/demo.gif and /dev/null differ diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json deleted file mode 100644 index 695a20a2695a..000000000000 --- a/npm/create-cypress-tests/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "create-cypress-tests", - "version": "0.0.0-development", - "description": "Cypress smart installation wizard", - "main": "dist/src/main.js", - "scripts": { - "build": "yarn prepare-example && tsc -p ./tsconfig.json && node scripts/example copy-to ./dist/initial-template && yarn prepare-copy-templates", - "prepare-example": "node scripts/example copy-to ./initial-template", - "prepare-copy-templates": "node scripts/copy-templates copy-to ./dist/src", - "test": "cross-env TS_NODE_PROJECT=./tsconfig.test.json mocha --config .mocharc.json './src/**/*.test.ts'", - "test:watch": "yarn test -w", - "lint": "eslint --ext .js,.ts,.json, ." - }, - "dependencies": { - "@babel/core": "^7.5.4", - "@babel/plugin-transform-typescript": "^7.2.0", - "@babel/template": "^7.5.4", - "@babel/types": "^7.5.0", - "bluebird": "3.7.2", - "chalk": "4.1.0", - "cli-highlight": "2.1.10", - "commander": "6.2.1", - "find-up": "5.0.0", - "fs-extra": "^9.1.0", - "glob": "^7.1.6", - "inquirer": "8.2.4", - "ora": "^5.1.0", - "recast": "0.20.4", - "semver": "7.3.7" - }, - "devDependencies": { - "@types/babel__core": "^7.1.2", - "@types/inquirer": "8.2.4", - "@types/mock-fs": "4.10.0", - "@types/node": "18.17.5", - "@types/ora": "^3.2.0", - "@types/semver": "7.5.0", - "copy": "0.3.2", - "mocha": "7.1.1", - "mock-fs": "5.2.0", - "snap-shot-it": "7.9.3", - "typescript": "^4.7.4" - }, - "files": [ - "dist", - "bin" - ], - "bin": { - "create-cypress-tests": "dist/src/index.js" - }, - "license": "MIT", - "repository": "https://github.com/cypress-io/cypress.git", - "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/create-cypress-tests/#readme", - "nx": { - "implicitDependencies": [ - "@packages/example" - ] - } -} diff --git a/npm/create-cypress-tests/scripts/copy-templates.js b/npm/create-cypress-tests/scripts/copy-templates.js deleted file mode 100644 index 583538450087..000000000000 --- a/npm/create-cypress-tests/scripts/copy-templates.js +++ /dev/null @@ -1,40 +0,0 @@ -const globby = require('globby') -const fs = require('fs-extra') -const chalk = require('chalk') -const path = require('path') -const program = require('commander') - -program -.command('copy-to [destination]') -.description('copy ./src/**/*.template.js into destination') -.action(async (destination) => { - const srcPath = path.resolve(__dirname, '..', 'src') - const destinationPath = path.resolve(process.cwd(), destination) - - const templates = await globby('**/*.template.js', { - cwd: srcPath, - onlyFiles: true, - unique: true, - }) - - const srcOutput = './src/' - let destinationOutput = destination.replace('/\\/g', '/') - - if (!destinationOutput.endsWith('/')) { - destinationOutput += '/' - } - - const relOutput = (template, forSource) => { - return `${forSource ? srcOutput : destinationOutput}${template}` - } - - const result = await Promise.all(templates.map(async (template) => { - await fs.copy(path.join(srcPath, template), path.join(destinationPath, template)) - - return () => console.log(`✅ ${relOutput(template, true)} successfully copied to ${chalk.cyan(relOutput(template, false))}`) - })) - - result.forEach((r) => r()) -}) - -program.parse(process.argv) diff --git a/npm/create-cypress-tests/scripts/example.js b/npm/create-cypress-tests/scripts/example.js deleted file mode 100644 index 0925901447ab..000000000000 --- a/npm/create-cypress-tests/scripts/example.js +++ /dev/null @@ -1,27 +0,0 @@ -const fs = require('fs-extra') -const chalk = require('chalk') -const path = require('path') -const program = require('commander') - -program -.command('copy-to [destination]') -.description('copy cypress/packages/example into destination') -.action(async (destination) => { - const exampleFolder = path.resolve(__dirname, '..', '..', '..', 'packages', 'example', 'cypress') - const destinationPath = path.resolve(process.cwd(), destination) - - await fs.remove(destinationPath) - await fs.copy(exampleFolder, destinationPath, { recursive: true }) - - console.log(`✅ E2E Examples were successfully created at ${chalk.cyan(destination)}`) - - await fs.copy(path.join(__dirname, 'examples', 'cypress'), path.join(destination)) - - console.log(`✅ Cypress Setup was successfully created at ${chalk.cyan(destination)}`) - - await fs.copy(path.join(__dirname, 'examples', 'tsconfig.json'), path.join(destination, 'tsconfig.json')) - - console.log(`✅ tsconfig.json was created for ${chalk.cyan(destination)}`) -}) - -program.parse(process.argv) diff --git a/npm/create-cypress-tests/scripts/examples/cypress/support/commands.js b/npm/create-cypress-tests/scripts/examples/cypress/support/commands.js deleted file mode 100644 index 119ab03f7cda..000000000000 --- a/npm/create-cypress-tests/scripts/examples/cypress/support/commands.js +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/npm/create-cypress-tests/scripts/examples/cypress/support/component.js b/npm/create-cypress-tests/scripts/examples/cypress/support/component.js deleted file mode 100644 index 5e450a7b1a4a..000000000000 --- a/npm/create-cypress-tests/scripts/examples/cypress/support/component.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/component.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js b/npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js deleted file mode 100644 index d1dd1353e812..000000000000 --- a/npm/create-cypress-tests/scripts/examples/cypress/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/e2e.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts deleted file mode 100644 index 5bc9e1ed33dc..000000000000 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/// - -import * as babel from '@babel/core' -import { expect } from 'chai' -import { createTransformPluginsFileBabelPlugin } from './babelTransform' - -describe('babel transform utils', () => { - context('Plugins config babel plugin', () => { - it('injects code into the plugins file based on ast', () => { - const plugin = createTransformPluginsFileBabelPlugin({ - RequireAst: babel.template.ast('require("something")'), - IfComponentTestingPluginsAst: babel.template.ast('yey()'), - }) - - const output = babel.transformSync([ - 'module.exports = (on, config) => {', - 'on("do")', - '}', - ].join('\n'), { - plugins: [plugin], - })?.code - - expect(output).to.equal([ - 'require("something");', - '', - 'module.exports = (on, config) => {', - ' on("do");', - '', - ' if (config.testingType === "component") {', - ' yey();', - ' }', - '};', - ].join(`\n`)) - }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts b/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts deleted file mode 100644 index 0c06f49ca80c..000000000000 --- a/npm/create-cypress-tests/src/component-testing/babel/babelTransform.ts +++ /dev/null @@ -1,138 +0,0 @@ -import path from 'path' -import * as fs from 'fs-extra' -import * as babel from '@babel/core' -import * as babelTypes from '@babel/types' -import { prettifyCode } from '../../utils' - -type AST = ReturnType - -export type PluginsConfigAst = { - RequireAst: AST - IfComponentTestingPluginsAst: AST - requiresReturnConfig?: true -} - -const sharedBabelOptions = { - // disable user config - configFile: false, - babelrc: false, - presets: [], - root: process.env.BABEL_TEST_ROOT, // for testing -} - -async function transformFileViaPlugin (filePath: string, babelPlugin: babel.PluginObj) { - try { - const initialCode = await fs.readFile(filePath, { encoding: 'utf-8' }) - - const updatedResult = await babel.transformAsync(initialCode, { - filename: path.basename(filePath), - filenameRelative: path.relative(process.cwd(), filePath), - plugins: [babelPlugin], - ...sharedBabelOptions, - }) - - if (!updatedResult) { - return false - } - - let finalCode = updatedResult.code - - if (finalCode === initialCode) { - return false - } - - finalCode = await prettifyCode(finalCode) - - await fs.writeFile(filePath, finalCode) - - return true - } catch (e) { - return false - } -} - -const returnConfigAst = babel.template.ast('return config; // IMPORTANT to return a config', { preserveComments: true }) - -export function createTransformPluginsFileBabelPlugin (ast: PluginsConfigAst): babel.PluginObj { - return { - visitor: { - Program: (path) => { - path.unshiftContainer('body', ast.RequireAst) - }, - Function: (path) => { - if (!babelTypes.isAssignmentExpression(path.parent)) { - return - } - - const assignment = path.parent.left - - const isModuleExports = - babelTypes.isMemberExpression(assignment) - && babelTypes.isIdentifier(assignment.object) - && assignment.object.name === 'module' - && babelTypes.isIdentifier(assignment.property) - && assignment.property.name === 'exports' - - if (isModuleExports && babelTypes.isFunction(path.parent.right)) { - const paramsLength = path.parent.right.params.length - - if (paramsLength === 0) { - path.parent.right.params.push(babelTypes.identifier('on')) - path.parent.right.params.push(babelTypes.identifier('config')) - } - - if (paramsLength === 1) { - path.parent.right.params.push(babelTypes.identifier('config')) - } - - const statementToInject = Array.isArray(ast.IfComponentTestingPluginsAst) - ? ast.IfComponentTestingPluginsAst - : [ast.IfComponentTestingPluginsAst] - - const ifComponentMode = babelTypes.ifStatement( - babelTypes.binaryExpression( - '===', - babelTypes.identifier('config.testingType'), - babelTypes.stringLiteral('component'), - ), - babelTypes.blockStatement(statementToInject as babelTypes.Statement[] | babelTypes.Statement[]), - ) - - path.get('body').pushContainer('body' as never, ifComponentMode as never) - - if (ast.requiresReturnConfig) { - path.get('body').pushContainer('body' as never, returnConfigAst as never) - } - } - }, - }, - } -} - -export async function injectPluginsCode (pluginsFilePath: string, ast: PluginsConfigAst) { - return transformFileViaPlugin(pluginsFilePath, createTransformPluginsFileBabelPlugin(ast)) -} - -export async function getPluginsSourceExample (ast: PluginsConfigAst) { - const exampleCode = [ - 'module.exports = (on, config) => {', - '', - '}', - ].join('\n') - - try { - const babelResult = await babel.transformAsync(exampleCode, { - filename: 'nothing.js', - plugins: [createTransformPluginsFileBabelPlugin(ast)], - ...sharedBabelOptions, - }) - - if (!babelResult?.code) { - throw new Error() - } - - return babelResult.code - } catch (e) { - throw new Error('Can not generate code example for plugins file because of unhandled error. Please update the plugins file manually.') - } -} diff --git a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.test.ts b/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.test.ts deleted file mode 100644 index 8a2dee76b5ff..000000000000 --- a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.test.ts +++ /dev/null @@ -1,405 +0,0 @@ -/// - -import { expect } from 'chai' -import { insertValueInJSString } from './configFileUpdater' - -// Test util - if needed outside the tests we can move it to utils -const stripIndent = (strings: any, ...args: any) => { - const parts = [] - - for (let i = 0; i < strings.length; i++) { - parts.push(strings[i]) - - if (i < strings.length - 1) { - parts.push(`<<${i}>>`) - } - } - - const lines = parts.join('').split('\n') - const firstLine = lines[0].length === 0 ? lines[1] : lines[0] - let indentSize = 0 - - for (let i = 0; i < firstLine.length; i++) { - if (firstLine[i] === ' ') { - indentSize++ - continue - } - - break - } - - const strippedLines = lines.map((line) => line.substring(indentSize)) - - let result = strippedLines.join('\n').trimLeft() - - args.forEach((arg: any, i: any) => { - result = result.replace(`<<${i}>>`, `${arg}`) - }) - - return result -} - -describe('lib/util/config-file-updater', () => { - context('with js files', () => { - describe('#insertValueInJSString', () => { - describe('es6 vs es5', () => { - it('finds the object literal and adds the values to it es6', async () => { - const src = stripIndent`\ - export default { - foo: 42, - } - ` - - const expectedOutput = stripIndent`\ - export default { - projectId: "id1234", - viewportWidth: 400, - foo: 42, - } - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('finds the object literal and adds the values to it es5', async () => { - const src = stripIndent`\ - module.exports = { - foo: 42, - } - ` - - const expectedOutput = stripIndent`\ - module.exports = { - projectId: "id1234", - viewportWidth: 400, - foo: 42, - } - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('works with and without the quotes around keys', async () => { - const src = stripIndent`\ - export default { - "foo": 42, - } - ` - - const expectedOutput = stripIndent`\ - export default { - projectId: "id1234", - viewportWidth: 400, - "foo": 42, - } - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('defineConfig', () => { - it('skips defineConfig and add to the object inside', async () => { - const src = stripIndent`\ - import { defineConfig } from "cypress" - export default defineConfig({ - foo: 42, - }) - ` - - const expectedOutput = stripIndent`\ - import { defineConfig } from "cypress" - export default defineConfig({ - projectId: "id1234", - viewportWidth: 400, - foo: 42, - }) - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('skips defineConfig even if it renamed in an import (es6)', async () => { - const src = stripIndent`\ - import { defineConfig as cy_defineConfig } from "cypress" - export default cy_defineConfig({ - foo: 42, - }) - ` - - const expectedOutput = stripIndent`\ - import { defineConfig as cy_defineConfig } from "cypress" - export default cy_defineConfig({ - projectId: "id1234", - viewportWidth: 400, - foo: 42, - }) - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - - it('skips defineConfig even if it renamed in a require (es5)', async () => { - const src = stripIndent`\ - const { defineConfig: cy_defineConfig } = require("cypress") - module.exports = cy_defineConfig({ - foo: 42, - }) - ` - - const expectedOutput = stripIndent`\ - const { defineConfig: cy_defineConfig } = require("cypress") - module.exports = cy_defineConfig({ - projectId: "id1234", - viewportWidth: 400, - foo: 42, - }) - ` - - const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }) - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('updates', () => { - it('updates a value if the same value is found in resolved config', async () => { - const src = stripIndent`\ - export default { - foo: 42, - } - ` - const expectedOutput = stripIndent`\ - export default { - foo: 1000, - } - ` - - const output = await insertValueInJSString(src, { foo: 1000 }) - - expect(output).to.equal(expectedOutput) - }) - - it('accepts inline comments', async () => { - const src = stripIndent`\ - export default { - foo: 12, // will do this later - viewportWidth: 800, - } - ` - const expectedOutput = stripIndent`\ - export default { - foo: 1000, // will do this later - viewportWidth: 800, - } - ` - - const output = await insertValueInJSString(src, { foo: 1000 }) - - expect(output).to.equal(expectedOutput) - }) - - it('updates a value even when this value is explicitely undefined', async () => { - const src = stripIndent`\ - export default { - foo: undefined, // will do this later - viewportWidth: 800, - } - ` - const expectedOutput = stripIndent`\ - export default { - foo: 1000, // will do this later - viewportWidth: 800, - } - ` - - const output = await insertValueInJSString(src, { foo: 1000 }) - - expect(output).to.equal(expectedOutput) - }) - - it('updates values and inserts config', async () => { - const src = stripIndent`\ - export default { - foo: 42, - bar: 84, - component: { - devServer() { - return null - } - } - } - ` - - const expectedOutput = stripIndent`\ - export default { - projectId: "id1234", - foo: 1000, - bar: 3000, - component: { - devServer() { - return null - } - } - } - ` - - const output = await insertValueInJSString(src, { foo: 1000, bar: 3000, projectId: 'id1234' }) - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('subkeys', () => { - it('inserts nested values', async () => { - const src = stripIndent`\ - module.exports = { - foo: 42 - } - ` - - const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } }) - - const expectedOutput = stripIndent`\ - module.exports = { - component: { - specPattern: "src/**/*.spec.cy.js", - }, - foo: 42 - } - ` - - expect(output).to.equal(expectedOutput) - }) - - it('inserts nested values into existing keys', async () => { - const src = stripIndent`\ - module.exports = { - component: { - viewportWidth: 800 - }, - foo: 42 - } - ` - - const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } }) - - const expectedOutput = stripIndent`\ - module.exports = { - component: { - specPattern: "src/**/*.spec.cy.js", - viewportWidth: 800 - }, - foo: 42 - } - ` - - expect(output).to.equal(expectedOutput) - }) - - it('updates nested values', async () => { - const src = stripIndent`\ - module.exports = { - foo: 42, - component: { - specPattern: 'components/**/*.spec.cy.js', - foo: 82 - } - }` - - const output = await insertValueInJSString(src, { component: { specPattern: 'src/**/*.spec.cy.js' } }) - - const expectedOutput = stripIndent`\ - module.exports = { - foo: 42, - component: { - specPattern: "src/**/*.spec.cy.js", - foo: 82 - } - }` - - expect(output).to.equal(expectedOutput) - }) - }) - - describe('failures', () => { - it('fails if not an object literal', () => { - const src = [ - 'const foo = {}', - 'export default foo', - ].join('\n') - - return insertValueInJSString(src, { bar: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - - it('fails if one of the values to update is not a literal', () => { - const src = [ - 'const bar = 12', - 'export default {', - ' foo: bar', - '}', - ].join('\n') - - return insertValueInJSString(src, { foo: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - - it('fails with inlined values', () => { - const src = stripIndent`\ - const foo = 12 - export default { - foo - } - ` - - return insertValueInJSString(src, { foo: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - - it('fails if there is a spread', () => { - const src = stripIndent`\ - const foo = { bar: 12 } - export default { - bar: 8, - ...foo - } - ` - - return insertValueInJSString(src, { bar: 10 }) - .then(() => { - throw Error('this should not succeed') - }) - .catch((err) => { - expect(err.message).to.equal('Cypress was unable to add/update values in your configuration file.') - }) - }) - }) - }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.ts b/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.ts deleted file mode 100644 index b2e587f9ee3c..000000000000 --- a/npm/create-cypress-tests/src/component-testing/config-file-updater/configFileUpdater.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { parse } from '@babel/parser' -import type { File } from '@babel/types' -import type { NodePath } from 'ast-types/lib/node-path' -import { visit } from 'recast' -import type { namedTypes } from 'ast-types' -import * as fs from 'fs-extra' -import { prettifyCode } from '../../utils' - -export async function insertValuesInConfigFile (filePath: string, obj: Record = {}) { - await insertValuesInJavaScript(filePath, obj) - - return true -} - -export async function insertValuesInJavaScript (filePath: string, obj: Record) { - const fileContents = await fs.readFile(filePath, { encoding: 'utf8' }) - - let finalCode = await insertValueInJSString(fileContents, obj) - - const prettifiedCode = await prettifyCode(finalCode) - - if (prettifiedCode) { - finalCode = prettifiedCode - } - - await fs.writeFile(filePath, finalCode) -} - -export async function insertValueInJSString (fileContents: string, obj: Record): Promise { - const ast = parse(fileContents, { plugins: ['typescript'], sourceType: 'module' }) - - let objectLiteralNode: namedTypes.ObjectExpression | undefined - - function handleExport (nodePath: NodePath | NodePath): void { - if (nodePath.node.type === 'CallExpression' - && nodePath.node.callee.type === 'Identifier') { - const functionName = nodePath.node.callee.name - - if (isDefineConfigFunction(ast, functionName)) { - return handleExport(nodePath.get('arguments', 0)) - } - } - - if (nodePath.node.type === 'ObjectExpression' && !nodePath.node.properties.find((prop) => prop.type !== 'ObjectProperty')) { - objectLiteralNode = nodePath.node - - return - } - - throw new Error('Cypress was unable to add/update values in your configuration file.') - } - - visit(ast, { - visitAssignmentExpression (nodePath) { - if (nodePath.node.left.type === 'MemberExpression') { - if (nodePath.node.left.object.type === 'Identifier' && nodePath.node.left.object.name === 'module' - && nodePath.node.left.property.type === 'Identifier' && nodePath.node.left.property.name === 'exports') { - handleExport(nodePath.get('right')) - } - } - - return false - }, - visitExportDefaultDeclaration (nodePath) { - handleExport(nodePath.get('declaration')) - - return false - }, - }) - - const splicers: Splicer[] = [] - - if (!objectLiteralNode) { - // if the export is no object literal - throw new Error('Cypress was unable to add/update values in your configuration file.') - } - - setRootKeysSplicers(splicers, obj, objectLiteralNode!, ' ') - setSubKeysSplicers(splicers, obj, objectLiteralNode!, ' ', ' ') - - // sort splicers to keep the order of the original file - const sortedSplicers = splicers.sort((a, b) => a.start === b.start ? 0 : a.start > b.start ? 1 : -1) - - if (!sortedSplicers.length) return fileContents - - let nextStartingIndex = 0 - let resultCode = '' - - sortedSplicers.forEach((splicer) => { - resultCode += fileContents.slice(nextStartingIndex, splicer.start) + splicer.replaceString - nextStartingIndex = splicer.end - }) - - return resultCode + fileContents.slice(nextStartingIndex) -} - -export function isDefineConfigFunction (ast: File, functionName: string): boolean { - let value = false - - visit(ast, { - visitVariableDeclarator (nodePath) { - // if this is a require of cypress - if (nodePath.node.init?.type === 'CallExpression' - && nodePath.node.init.callee.type === 'Identifier' - && nodePath.node.init.callee.name === 'require' - && nodePath.node.init.arguments[0].type === 'StringLiteral' - && nodePath.node.init.arguments[0].value === 'cypress') { - if (nodePath.node.id?.type === 'ObjectPattern') { - const defineConfigFunctionNode = nodePath.node.id.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === 'defineConfig' - }) - - if (defineConfigFunctionNode) { - value = (defineConfigFunctionNode as any).value?.name === functionName - } - } - } - - return false - }, - visitImportDeclaration (nodePath) { - if (nodePath.node.source.type === 'StringLiteral' - && nodePath.node.source.value === 'cypress') { - const defineConfigFunctionNode = nodePath.node.specifiers?.find((specifier) => { - return specifier.type === 'ImportSpecifier' - && specifier.imported.type === 'Identifier' - && specifier.imported.name === 'defineConfig' - }) - - if (defineConfigFunctionNode) { - value = (defineConfigFunctionNode as any).local?.name === functionName - } - } - - return false - }, - }) - - return value -} - -function setRootKeysSplicers ( - splicers: Splicer[], - obj: Record, - objectLiteralNode: namedTypes.ObjectExpression, - lineStartSpacer: string, -) { - const objectLiteralStartIndex = (objectLiteralNode as any).start + 1 - // add values - const objKeys = Object.keys(obj).filter((key) => ['boolean', 'number', 'string'].includes(typeof obj[key])) - - // update values - const keysToUpdate = objKeys.filter((key) => { - return objectLiteralNode.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === key - }) - }) - - keysToUpdate.forEach( - (key) => { - const propertyToUpdate = propertyFromKey(objectLiteralNode, key) - - if (propertyToUpdate) { - setSplicerToUpdateProperty(splicers, propertyToUpdate, obj[key], key, obj) - } - }, - ) - - const keysToInsert = objKeys.filter((key) => !keysToUpdate.includes(key)) - - if (keysToInsert.length) { - const valuesInserted = `\n${lineStartSpacer}${ keysToInsert.map((key) => `${key}: ${JSON.stringify(obj[key])},`).join(`\n${lineStartSpacer}`)}` - - splicers.push({ - start: objectLiteralStartIndex, - end: objectLiteralStartIndex, - replaceString: valuesInserted, - }) - } -} - -function setSubKeysSplicers ( - splicers: Splicer[], - obj: Record, - objectLiteralNode: namedTypes.ObjectExpression, - lineStartSpacer: string, - parentLineStartSpacer: string, -) { - const objectLiteralStartIndex = (objectLiteralNode as any).start + 1 - - const keysToUpdateWithObjects: string[] = [] - - const objSubkeys = Object.keys(obj).filter((key) => typeof obj[key] === 'object').reduce((acc: Array<{parent: string, subkey: string}>, key) => { - keysToUpdateWithObjects.push(key) - Object.entries(obj[key]).forEach(([subkey, value]) => { - if (['boolean', 'number', 'string'].includes(typeof value)) { - acc.push({ parent: key, subkey }) - } - }) - - return acc - }, []) - - // add values where the parent key needs to be created - const subkeysToInsertWithoutKey = objSubkeys.filter(({ parent }) => { - return !objectLiteralNode.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === parent - }) - }) - const keysToInsertForSubKeys: Record = {} - - subkeysToInsertWithoutKey.forEach((keyTuple) => { - const subkeyList = keysToInsertForSubKeys[keyTuple.parent] || [] - - subkeyList.push(keyTuple.subkey) - keysToInsertForSubKeys[keyTuple.parent] = subkeyList - }) - - let subvaluesInserted = '' - - for (const key in keysToInsertForSubKeys) { - subvaluesInserted += `\n${parentLineStartSpacer}${key}: {` - keysToInsertForSubKeys[key].forEach((subkey) => { - subvaluesInserted += `\n${parentLineStartSpacer}${lineStartSpacer}${subkey}: ${JSON.stringify(obj[key][subkey])},` - }) - - subvaluesInserted += `\n${parentLineStartSpacer}},` - } - - if (subkeysToInsertWithoutKey.length) { - splicers.push({ - start: objectLiteralStartIndex, - end: objectLiteralStartIndex, - replaceString: subvaluesInserted, - }) - } - - // add/update values where parent key already exists - keysToUpdateWithObjects.filter((parent) => { - return objectLiteralNode.properties.find((prop) => { - return prop.type === 'ObjectProperty' - && prop.key.type === 'Identifier' - && prop.key.name === parent - }) - }).forEach((key) => { - const propertyToUpdate = propertyFromKey(objectLiteralNode, key) - - if (propertyToUpdate?.value.type === 'ObjectExpression') { - setRootKeysSplicers(splicers, obj[key], propertyToUpdate.value, parentLineStartSpacer + lineStartSpacer) - } - }) -} - -function setSplicerToUpdateProperty (splicers: Splicer[], - propertyToUpdate: namedTypes.ObjectProperty, - updatedValue: any, - key: string, - obj: Record) { - if (propertyToUpdate && (isPrimitive(propertyToUpdate.value) || isUndefinedOrNull(propertyToUpdate.value))) { - splicers.push({ - start: (propertyToUpdate.value as any).start, - end: (propertyToUpdate.value as any).end, - replaceString: JSON.stringify(updatedValue), - }) - } else { - throw new Error('Cypress was unable to add/update values in your configuration file.') - } -} - -function propertyFromKey (objectLiteralNode: namedTypes.ObjectExpression | undefined, key: string): namedTypes.ObjectProperty | undefined { - return objectLiteralNode?.properties.find((prop) => { - return prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === key - }) as namedTypes.ObjectProperty -} - -function isPrimitive (value: NodePath['node']): value is namedTypes.NumericLiteral | namedTypes.StringLiteral | namedTypes.BooleanLiteral { - return value.type === 'NumericLiteral' || value.type === 'StringLiteral' || value.type === 'BooleanLiteral' -} - -function isUndefinedOrNull (value: NodePath['node']): value is namedTypes.Identifier { - return value.type === 'Identifier' && ['undefined', 'null'].includes(value.name) -} - -interface Splicer{ - start: number - end: number - replaceString: string -} diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts deleted file mode 100644 index 2db6b20baf13..000000000000 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import path from 'path' -import fs from 'fs-extra' -import snapshot from 'snap-shot-it' -import { expect, use } from 'chai' -import sinon, { SinonStub, SinonSpy } from 'sinon' -import chalk from 'chalk' -import mockFs from 'mock-fs' -import { initComponentTesting } from './init-component-testing' -import inquirer from 'inquirer' -import sinonChai from 'sinon-chai' -import childProcess from 'child_process' -import { someOfSpyCallsIncludes } from '../test-utils' - -use(sinonChai) - -describe('init component tests script', () => { - let promptSpy: SinonStub | null = null - let logSpy: SinonSpy | null = null - let processExitStub: SinonStub | null = null - let execStub: SinonStub | null = null - - const e2eTestOutputPath = path.resolve(__dirname, '..', 'test-output') - const cypressConfigPath = path.join(e2eTestOutputPath, 'cypress.config.ts') - - beforeEach(async () => { - logSpy = sinon.spy(global.console, 'log') - // @ts-ignores - execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) - processExitStub = sinon.stub(process, 'exit').callsFake(() => { - throw new Error(`${chalk.red('process.exit')} should not be called`) - }) - - await fs.remove(e2eTestOutputPath) - await fs.mkdir(e2eTestOutputPath) - - process.env.BABEL_TEST_ROOT = e2eTestOutputPath - }) - - afterEach(() => { - mockFs.restore() - logSpy?.restore() - promptSpy?.restore() - processExitStub?.restore() - execStub?.restore() - }) - - function createTempFiles (tempFiles: Record) { - Object.entries(tempFiles).forEach(([fileName, content]) => { - fs.outputFileSync( - path.join(e2eTestOutputPath, fileName), - content, - ) - }) - } - - function snapshotGeneratedFiles (name: string) { - snapshot( - `${name} cypress.config.ts`, - fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress.config.ts'), - { encoding: 'utf-8' }, - ), - ) - - snapshot( - `${name} plugins/index.js`, - fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), - { encoding: 'utf-8' }, - ), - ) - - const supportFile = fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress', 'support', 'component.js'), - { encoding: 'utf-8' }, - ) - - // Comparing empty snapshot errors. - if (supportFile.length === 0) { - return - } - - snapshot( - `${name} support/component.js`, - fs.readFileSync( - path.join(e2eTestOutputPath, 'cypress', 'support', 'component.js'), - { encoding: 'utf-8' }, - ), - ) - } - - it('determines more presumable configuration to suggest', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/cypress/support/component.js': '', - '/cypress/plugins/index.js': 'module.exports = (on, config) => {}', - // For next.js user will have babel config, but we want to suggest to use the closest config for the application code - '/babel.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { react: '^17.x', next: '^9.2.0' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'next.js', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - const [{ choices }] = (inquirer.prompt as any).args[0][0] - - expect(choices[0]).to.equal('next.js') - snapshotGeneratedFiles('injects guessed next.js template') - }) - - it('automatically suggests to the user which config to use', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/cypress/support/component.js': 'import "./commands.js";', - '/cypress/plugins/index.js': 'module.exports = () => {}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.10.0', - }, - }), - '/webpack.config.js': 'module.exports = { }', - }) - - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - const [{ choices, message }] = (inquirer.prompt as any).args[0][0] - - expect(choices[0]).to.equal('webpack') - expect(message).to.contain( - `Press ${chalk.inverse(' Enter ')} to continue with ${chalk.green( - 'webpack', - )} configuration`, - ) - - snapshotGeneratedFiles('Injected overridden webpack template') - }) - - it('Asks for preferred bundling tool if can not determine the right one', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/webpack.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - framework: 'vue@2', - }) as any) - .onCall(1) - .returns(Promise.resolve({ - chosenTemplateName: 'webpack', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - expect( - someOfSpyCallsIncludes(global.console.log, 'We were unable to automatically determine your framework 😿'), - ).to.be.true - }) - - it('Asks for framework if more than 1 option was auto detected', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/webpack.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { react: '*', vue: '^2.4.5' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - framework: 'vue@3', - }) as any) - .onCall(1) - .returns(Promise.resolve({ - chosenTemplateName: 'webpack', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - expect( - someOfSpyCallsIncludes(global.console.log, `It looks like all these frameworks: ${chalk.yellow('react, vue@2')} are available from this directory.`), - ).to.be.true - }) - - it('installs the right adapter', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/webpack.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { react: '16.4.5' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - chosenTemplateName: 'vite', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - expect(execStub).to.be.calledWith('yarn add @cypress/react --dev') - }) - - it('installs the right adapter for vue 3', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/vite.config.js': 'module.exports = { }', - '/package.json': JSON.stringify({ dependencies: { vue: '^3.0.0' } }), - }) - - promptSpy = sinon.stub(inquirer, 'prompt') - .onCall(0) - .returns(Promise.resolve({ - chosenTemplateName: 'vite', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - expect(execStub).to.be.calledWith('yarn add @cypress/vue --dev') - }) - - it('suggest the right instruction based on user template choice', async () => { - createTempFiles({ - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.0.0', - }, - }), - '/cypress.config.ts': 'export default {}', - }) - - promptSpy = sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'src', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - expect( - someOfSpyCallsIncludes( - global.console.log, - 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts', - ), - ).to.be.true - }) - - it('suggests right docs example and cypress.config.ts config based on the `componentFolder` answer', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.0.0', - }, - }), - }) - - sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - const injectedCode = require(path.join(e2eTestOutputPath, 'cypress.config.ts')) - - expect(JSON.stringify(injectedCode.default, null, 2)).to.equal(JSON.stringify( - { - specPattern: 'cypress/component/**/*.spec.{js,ts,jsx,tsx}', - }, - null, - 2, - )) - }) - - it('Shows help message if cypress files are not created', async () => { - createTempFiles({ - '/cypress.config.ts': 'export default {}', - '/package.json': JSON.stringify({ - dependencies: { - react: '^16.0.0', - }, - }), - }) - - sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - - expect( - someOfSpyCallsIncludes( - global.console.log, - 'was not updated automatically. Please add the following config manually:', - ), - ).to.be.true - }) - - it(`Doesn't affect injected code if user has custom babel.config.js`, async () => { - createTempFiles({ - '/cypress/plugins/index.js': 'module.exports = (on, config) => {}', - '/cypress.config.ts': 'export default {}', - 'babel.config.js': `module.exports = ${JSON.stringify({ - presets: [ - '@babel/preset-env', - ], - })}`, - '/package.json': JSON.stringify({ - dependencies: { - babel: '*', - react: '^16.0.0', - }, - }), - }) - - sinon.stub(inquirer, 'prompt').returns(Promise.resolve({ - chosenTemplateName: 'create-react-app', - componentFolder: 'cypress/component', - }) as any) - - await initComponentTesting({ config: {}, cypressConfigPath, useYarn: true }) - const babelPluginsOutput = await fs.readFile( - path.join(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'), - 'utf-8', - ) - - expect(babelPluginsOutput).not.to.contain('use strict') - expect(babelPluginsOutput).to.contain('module.exports = (on, config) => {') - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts b/npm/create-cypress-tests/src/component-testing/init-component-testing.ts deleted file mode 100644 index a79908088fef..000000000000 --- a/npm/create-cypress-tests/src/component-testing/init-component-testing.ts +++ /dev/null @@ -1,194 +0,0 @@ -import fs from 'fs-extra' -import path from 'path' -import chalk from 'chalk' -import inquirer from 'inquirer' -import highlight from 'cli-highlight' -import { Template } from './templates/Template' -import { guessTemplate } from './templates/guessTemplate' -import { installFrameworkAdapter } from './installFrameworkAdapter' -import { injectPluginsCode, getPluginsSourceExample } from './babel/babelTransform' -import { installDependency } from '../utils' -import { insertValuesInConfigFile } from './config-file-updater/configFileUpdater' - -async function injectOrShowConfigCode (injectFn: () => Promise, { - code, - filePath, - fallbackFileMessage, - language, -}: { - code: string - filePath: string - language: string - fallbackFileMessage: string -}) { - const fileExists = fs.existsSync(filePath) - const readableFilePath = fileExists ? path.relative(process.cwd(), filePath) : fallbackFileMessage - - const printCode = () => { - console.log() - console.log(highlight(code, { language })) - console.log() - } - - const printSuccess = () => { - console.log(`✅ ${chalk.bold.green(readableFilePath)} was updated with the following config:`) - printCode() - } - - const printFailure = () => { - console.log(`❌ ${chalk.bold.red(readableFilePath)} was not updated automatically. Please add the following config manually: `) - printCode() - } - - if (!fileExists) { - printFailure() - - return - } - - // something get completely wrong when using babel or something. Print error message. - const injected = await injectFn().catch(() => false) - - injected ? printSuccess() : printFailure() -} - -async function injectAndShowCypressConfig ( - cypressJsonPath: string, - componentFolder: string, -) { - const configToInject = { - specPattern: `${componentFolder}/**/*.spec.{js,ts,jsx,tsx}`, - } - - await injectOrShowConfigCode(() => insertValuesInConfigFile(cypressJsonPath, configToInject), { - code: JSON.stringify(configToInject, null, 2), - language: 'js', - filePath: cypressJsonPath, - fallbackFileMessage: 'cypress.json config file', - }) -} - -async function injectAndShowPluginConfig (template: Template, { - templatePayload, - pluginsFilePath, - cypressProjectRoot, -}: { - templatePayload: T | null - pluginsFilePath: string - cypressProjectRoot: string -}) { - const ast = template.getPluginsCodeAst(templatePayload, { cypressProjectRoot }) - - await injectOrShowConfigCode(() => injectPluginsCode(pluginsFilePath, ast), { - code: await getPluginsSourceExample(ast), - language: 'js', - filePath: pluginsFilePath, - fallbackFileMessage: 'plugins file (https://on.cypress.io/plugins-file)', - }) -} - -type InitComponentTestingOptions = { - config: Record - cypressConfigPath: string - useYarn: boolean -} - -export async function initComponentTesting ({ config, useYarn, cypressConfigPath }: InitComponentTestingOptions) { - const cypressProjectRoot = path.resolve(cypressConfigPath, '..') - - const framework = await installFrameworkAdapter(cypressProjectRoot, { useYarn }) - const { - possibleTemplates, - defaultTemplate, - defaultTemplateName, - templatePayload, - } = await guessTemplate(framework, cypressProjectRoot) - - const pluginsFilePath = path.resolve( - cypressProjectRoot, - config.pluginsFile ?? './cypress/plugins/index.js', - ) - - const templateChoices = Object.keys(possibleTemplates).sort((key) => { - return key === defaultTemplateName ? -1 : 0 - }) - - const { - chosenTemplateName, - componentFolder, - }: Record = await inquirer.prompt([ - { - type: 'list', - name: 'chosenTemplateName', - choices: templateChoices, - default: defaultTemplate ? 0 : undefined, - message: defaultTemplate?.message - ? `${defaultTemplate?.message}\n\n Press ${chalk.inverse( - ' Enter ', - )} to continue with ${chalk.green( - defaultTemplateName, - )} configuration or select another template from the list:` - : 'We were not able to automatically determine which framework or bundling tool you are using. Please choose one from the list:', - }, - { - type: 'input', - name: 'componentFolder', - filter: (input) => input.trim(), - validate: (input) => { - return input === '' || !/^[a-zA-Z].*/.test(input) - ? `Directory "${input}" is invalid` - : true - }, - message: 'Which folder would you like to use for your component tests?', - default: (answers: { chosenTemplateName: keyof typeof possibleTemplates }) => { - return possibleTemplates[answers.chosenTemplateName].recommendedComponentFolder - }, - }, - ]) - - const chosenTemplate = possibleTemplates[chosenTemplateName] as Template - - console.log() - console.log(`Installing required dependencies`) - console.log() - - for (const dependency of chosenTemplate.dependencies) { - await installDependency(dependency, { useYarn }) - } - - console.log() - console.log(`Let's setup everything for component testing with ${chalk.cyan(chosenTemplateName)}:`) - console.log() - - await injectAndShowCypressConfig(cypressConfigPath, componentFolder) - await injectAndShowPluginConfig(chosenTemplate, { - templatePayload, - pluginsFilePath, - cypressProjectRoot, - }) - - if (chosenTemplate.printHelper) { - chosenTemplate.printHelper() - } - - console.log( - `Find examples of component tests for ${chalk.green( - chosenTemplateName, - )} in ${chalk.underline(chosenTemplate.getExampleUrl({ componentFolder }))}.`, - ) - - if (framework === 'react') { - console.log() - - console.log( - `Docs for different recipes of bundling tools: ${chalk.bold.underline( - 'https://github.com/cypress-io/cypress/tree/develop/npm/react/docs/recipes.md', - )}`, - ) - } - - // render delimiter - console.log() - console.log(new Array(process.stdout.columns).fill('═').join('')) - console.log() -} diff --git a/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts b/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts deleted file mode 100644 index 18e669a02eab..000000000000 --- a/npm/create-cypress-tests/src/component-testing/installFrameworkAdapter.ts +++ /dev/null @@ -1,63 +0,0 @@ -import chalk from 'chalk' -import inquirer from 'inquirer' -import { scanFSForAvailableDependency } from '../findPackageJson' -import { installDependency } from '../utils' - -async function guessOrAskForFramework (cwd: string): Promise<'react' | 'vue@2' | 'vue@3'> { - // please sort this alphabetically - const frameworks = { - react: () => scanFSForAvailableDependency(cwd, { react: '*', 'react-dom': '*' }), - 'vue@2': () => scanFSForAvailableDependency(cwd, { vue: '2.x' }), - 'vue@3': () => scanFSForAvailableDependency(cwd, { vue: '3.x' }), - } - - const guesses = Object.keys(frameworks).filter((framework) => { - return frameworks[framework as keyof typeof frameworks]() - }) as Array<'react' | 'vue@2' | 'vue@3'> - - // found 1 precise guess. Continue - if (guesses.length === 1) { - const framework = guesses[0] - - console.log(`\nThis project is using ${chalk.bold.cyan(framework)}. Let's install the right adapter:`) - - return framework - } - - if (guesses.length === 0) { - console.log(`We were unable to automatically determine your framework 😿. ${chalk.grey('Make sure to run this command from the directory where your components located in order to make smart detection works. Or continue with manual setup:')}`) - } - - if (guesses.length > 0) { - console.log(`It looks like all these frameworks: ${chalk.yellow(guesses.join(', '))} are available from this directory. ${chalk.grey('Make sure to run this command from the directory where your components located in order to make smart detection works. Or continue with manual setup:')}`) - } - - const { framework } = await inquirer.prompt([ - { - type: 'list', - name: 'framework', - choices: Object.keys(frameworks), - message: `Which framework do you use?`, - }, - ]) - - return framework -} - -type InstallAdapterOptions = { - useYarn: boolean -} - -const frameworkDependencies = { - react: '@cypress/react', - 'vue@2': '@cypress/vue2', - 'vue@3': '@cypress/vue', -} - -export async function installFrameworkAdapter (cwd: string, options: InstallAdapterOptions) { - const framework = await guessOrAskForFramework(cwd) - - await installDependency(frameworkDependencies[framework], options) - - return framework -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/Template.ts b/npm/create-cypress-tests/src/component-testing/templates/Template.ts deleted file mode 100644 index 72ea8de09d2b..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/Template.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PluginsConfigAst } from '../babel/babelTransform' - -export interface Template { - message: string - getExampleUrl: ({ componentFolder }: { componentFolder: string }) => string - recommendedComponentFolder: string - test(rootPath: string): { success: boolean, payload?: T } - getPluginsCodeAst: ( - payload: T | null, - options: { cypressProjectRoot: string }, - ) => PluginsConfigAst - dependencies: string[] - printHelper?: () => void -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/_shared/index.ts b/npm/create-cypress-tests/src/component-testing/templates/_shared/index.ts deleted file mode 100644 index 58f0af7bbb8f..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/_shared/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Template } from '../Template' -import { ViteTemplate } from './vite' - -export const frameworkAgnosticTemplates: Record> = { - vite: ViteTemplate, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.test.ts b/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.test.ts deleted file mode 100644 index 78b557d86091..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ViteTemplate } from './vite' -import { snapshotPluginsAstCode } from '../../../test-utils' - -describe('vue: vite template', () => { - it('correctly generates plugins config', () => snapshotPluginsAstCode(ViteTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.ts b/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.ts deleted file mode 100644 index b9b6f8f93c57..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/_shared/vite.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as babel from '@babel/core' -import { scanFSForAvailableDependency } from '../../../findPackageJson' -import { Template } from '../Template' - -export const ViteTemplate: Template = { - message: - 'It looks like you are using vitejs to run and build an application.', - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/vite', - recommendedComponentFolder: 'src', - dependencies: ['@cypress/vite-dev-server'], - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast( - 'const { startDevServer } = require("@cypress/vite-dev-server");', - ), - IfComponentTestingPluginsAst: babel.template.ast([ - 'on("dev-server:start", async (options) => startDevServer({ options }))', - ].join('\n'), { preserveComments: true }), - } - }, - test: (root) => { - return { - success: scanFSForAvailableDependency(root, { vite: '*' }), - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts b/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts deleted file mode 100644 index 10bcdfaa93a8..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/guessTemplate.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Template } from './Template' -import { reactTemplates } from './react' -import { vueTemplates } from './vue' -import { frameworkAgnosticTemplates } from './_shared' - -const frameworkSpecificTemplates = { - react: reactTemplates, - 'vue@2': vueTemplates, - 'vue@3': vueTemplates, -} - -export async function guessTemplate (framework: keyof typeof frameworkSpecificTemplates, cwd: string) { - const templates = { ...frameworkAgnosticTemplates, ...frameworkSpecificTemplates[framework] } - - for (const [name, template] of Object.entries(templates)) { - const typedTemplate = template as Template - const { success, payload } = typedTemplate.test(cwd) - - if (success) { - return { - defaultTemplate: typedTemplate, - defaultTemplateName: name, - templatePayload: payload ?? null, - possibleTemplates: templates, - } - } - } - - return { - templatePayload: null, - defaultTemplate: null, - defaultTemplateName: null, - possibleTemplates: templates, - } -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts deleted file mode 100644 index 1bff7af467af..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { BabelTemplate } from './babel' -import { snapshotPluginsAstCode } from '../../../test-utils' - -describe('babel installation template', () => { - beforeEach(mockFs.restore) - - it('resolves babel.config.json', () => { - mockFs({ - '/babel.config.json': JSON.stringify({ - presets: [], - plugins: [], - }), - }) - - const { success } = BabelTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('resolves babel.config.js', () => { - mockFs({ - '/project/babel.config.js': - 'module.exports = { presets: [], plugins: [] };', - '/project/index/package.json': 'dev/null', - }) - - const { success } = BabelTemplate.test('/project/index') - - expect(success).to.equal(true) - }) - - it('resolves babel config from the deep folder', () => { - mockFs({ - '/some/.babelrc': JSON.stringify({ - presets: [], - plugins: [], - }), - '/some/deep/folder/text.txt': '1', - }) - - const { success } = BabelTemplate.test('/some/deep/folder') - - expect(success).to.equal(true) - }) - - it('fails if no babel config found', () => { - mockFs({ - '/some.txt': '1', - }) - - const { success } = BabelTemplate.test('/') - - expect(success).to.equal(false) - }) - - it('resolves babel.config from package.json', () => { - mockFs({ - '/package.json': JSON.stringify({ - babel: { - presets: [], - }, - }), - }) - - const { success } = BabelTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('correctly generates plugins config', () => snapshotPluginsAstCode(BabelTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts b/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts deleted file mode 100644 index 0202d05b4400..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/babel.ts +++ /dev/null @@ -1,44 +0,0 @@ -import chalk from 'chalk' -import findUp from 'find-up' -import * as babel from '@babel/core' -import { Template } from '../Template' -import { createFindPackageJsonIterator } from '../../../findPackageJson' - -export const BabelTemplate: Template = { - message: `It looks like you have babel config defined. We can use it to transpile your components for testing.\n ${chalk.red( - '>>', - )} This is not a replacement for bundling tool. We will use ${chalk.red( - 'webpack', - )} to bundle the components for testing.`, - recommendedComponentFolder: 'cypress/component', - dependencies: ['webpack', '@cypress/webpack-dev-server'], - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/babel', - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/babel\')'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config)', - ].join('\n'), { preserveComments: true }), - } - }, - test: (cwd) => { - const babelConfig = findUp.sync( - ['babel.config.js', 'babel.config.json', '.babelrc', '.babelrc.json'], - { type: 'file', cwd }, - ) - - if (babelConfig) { - return { success: true } - } - - // babel config can also be declared in package.json with `babel` key https://babeljs.io/docs/en/configuration#packagejson - const packageJsonIterator = createFindPackageJsonIterator(cwd) - - return packageJsonIterator.map(({ babel }) => { - return { - success: Boolean(babel), - } - }) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/index.ts b/npm/create-cypress-tests/src/component-testing/templates/react/index.ts deleted file mode 100644 index bbbfca5178fb..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Template } from '../Template' -import { NextTemplate } from './next' -import { WebpackTemplate } from './reactWebpackFile' -import { ReactScriptsTemplate } from './react-scripts' -import { BabelTemplate } from './babel' -import { WebpackOptions } from './webpack-options' - -export const reactTemplates: Record> = { - 'create-react-app': ReactScriptsTemplate, - 'next.js': NextTemplate, - webpack: WebpackTemplate, - babel: BabelTemplate, - 'default (webpack options)': WebpackOptions, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts deleted file mode 100644 index 4701791877e3..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import sinon, { SinonSpy } from 'sinon' -import { expect, use } from 'chai' -import sinonChai from 'sinon-chai' -import mockFs from 'mock-fs' -import { NextTemplate } from './next' -import { snapshotPluginsAstCode } from '../../../test-utils' - -use(sinonChai) - -describe('next.js install template', () => { - let warnSpy: SinonSpy | null = null - - beforeEach(() => { - warnSpy = sinon.spy(global.console, 'warn') - }) - - afterEach(() => { - mockFs.restore() - warnSpy?.restore() - }) - - it('finds the closest package.json and checks that next is declared as dependency', () => { - mockFs({ - '/package.json': JSON.stringify({ - dependencies: { - next: '^9.2.3', - }, - scripts: { - build: 'next', - }, - }), - }) - - const { success } = NextTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('works if next is declared in the devDependencies as well', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - next: '^9.2.3', - }, - scripts: { - build: 'next', - }, - }), - }) - - const { success } = NextTemplate.test(process.cwd()) - - expect(success).to.equal(true) - }) - - it('warns and fails if version is not supported', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - next: '^8.2.3', - }, - scripts: { - build: 'next', - }, - }), - }) - - const { success } = NextTemplate.test('i/am/in/some/deep/folder') - - console.log(global.console.warn) - expect(success).to.equal(false) - - expect(global.console.warn).to.be.called - }) - - it('correctly generates plugins config', () => snapshotPluginsAstCode(NextTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts b/npm/create-cypress-tests/src/component-testing/templates/react/next.ts deleted file mode 100644 index 2f7ef89adfa1..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/next.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as babel from '@babel/core' -import { createFindPackageJsonIterator } from '../../../findPackageJson' -import { Template } from '../Template' -import { validateSemverVersion } from '../../../utils' -import { MIN_SUPPORTED_VERSION } from '../../versions' - -export const NextTemplate: Template = { - message: 'It looks like you are using next.js.', - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/nextjs' - }, - recommendedComponentFolder: 'cypress/component', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/next\')'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config)', - ].join('\n'), { preserveComments: true }), - } - }, - test: (cwd) => { - const packageJsonIterator = createFindPackageJsonIterator(cwd) - - return packageJsonIterator.map(({ dependencies, devDependencies }, path) => { - if (!dependencies && !devDependencies) { - return { success: false } - } - - const allDeps = { - ...(devDependencies || {}), - ...(dependencies || {}), - } as Record - - const nextVersion = allDeps['next'] - - if (!nextVersion) { - return { success: false } - } - - if ( - !validateSemverVersion( - nextVersion, - MIN_SUPPORTED_VERSION['next'], - 'next.js', - ) - ) { - return { success: false } - } - - return { success: true } - }) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts deleted file mode 100644 index 7bf01624006a..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import sinon, { SinonSpy } from 'sinon' -import { expect, use } from 'chai' -import sinonChai from 'sinon-chai' -import mockFs from 'mock-fs' -import { ReactScriptsTemplate } from './react-scripts' -import { snapshotPluginsAstCode } from '../../../test-utils' - -use(sinonChai) - -describe('create-react-app install template', () => { - let warnSpy: SinonSpy | null = null - - beforeEach(() => { - warnSpy = sinon.spy(global.console, 'warn') - }) - - afterEach(() => { - mockFs.restore() - warnSpy?.restore() - }) - - it('finds the closest package.json and checks that react-scripts is declared as dependency', () => { - mockFs({ - '/package.json': JSON.stringify({ - dependencies: { - 'react-scripts': '^3.2.3', - }, - }), - }) - - const { success } = ReactScriptsTemplate.test(process.cwd()) - - expect(success).to.equal(true) - }) - - it('works if react-scripts is declared in the devDependencies as well', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - 'react-scripts': '^3.2.3', - }, - }), - }) - - const { success } = ReactScriptsTemplate.test(process.cwd()) - - expect(success).to.equal(true) - }) - - it('warns and fails if version is not supported', () => { - mockFs({ - './package.json': JSON.stringify({ - devDependencies: { - 'react-scripts': '^2.2.3', - }, - }), - }) - - const { success } = ReactScriptsTemplate.test(process.cwd()) - - expect(success).to.equal(false) - expect(global.console.warn).to.be.called - }) - - it('correctly generates plugins config', () => snapshotPluginsAstCode(ReactScriptsTemplate)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts b/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts deleted file mode 100644 index 860106b74b24..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/react-scripts.ts +++ /dev/null @@ -1,62 +0,0 @@ -import chalk from 'chalk' -import { createFindPackageJsonIterator } from '../../../findPackageJson' -import { Template } from '../Template' -import { validateSemverVersion } from '../../../utils' -import { MIN_SUPPORTED_VERSION } from '../../versions' -import * as babel from '@babel/core' - -export const ReactScriptsTemplate: Template = { - recommendedComponentFolder: 'src', - message: 'It looks like you are using create-react-app.', - dependencies: ['@cypress/webpack-dev-server'], - getExampleUrl: ({ componentFolder }) => { - return componentFolder === 'src' - ? 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts' - : 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/react-scripts-folder' - }, - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require(\'@cypress/react/plugins/react-scripts\')'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config)', - ].join('\n'), { preserveComments: true }), - } - }, - test: () => { - // TODO also determine ejected create react app - const packageJsonIterator = createFindPackageJsonIterator(process.cwd()) - - return packageJsonIterator.map(({ dependencies, devDependencies }) => { - if (dependencies || devDependencies) { - const allDeps = { ...devDependencies, ...dependencies } || {} - - if (!allDeps['react-scripts']) { - return { success: false } - } - - if ( - !validateSemverVersion( - allDeps['react-scripts'], - MIN_SUPPORTED_VERSION['react-scripts'], - ) - ) { - console.warn( - `It looks like you are using ${chalk.green( - 'create-react-app', - )}, but we support only projects with version ${chalk.bold( - MIN_SUPPORTED_VERSION['react-scripts'], - )} of react-scripts.`, - ) - - // yey found the template - return { success: false } - } - - return { success: true } - } - - return { success: false } - }) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts deleted file mode 100644 index 4212fcc04f51..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { snapshotPluginsAstCode } from '../../../test-utils' -import { WebpackTemplate } from './reactWebpackFile' - -describe('webpack-file install template', () => { - afterEach(mockFs.restore) - - it('resolves webpack.config.js', () => { - mockFs({ - '/webpack.config.js': 'module.exports = { }', - }) - - const { success, payload } = WebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/webpack.config.js') - }) - - it('finds the closest package.json and tries to fetch webpack config path from scrips', () => { - mockFs({ - '/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config configs/webpack.js', - }, - }), - }) - - const { success, payload } = WebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/configs/webpack.js') - }) - - it('looks for package.json in the upper folder', () => { - mockFs({ - '/i/am/in/some/deep/folder/withFile': 'test', - '/somewhere/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config somewhere/configs/webpack.js', - }, - }), - }) - - const { success, payload } = WebpackTemplate.test( - 'i/am/in/some/deep/folder', - ) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/somewhere/configs/webpack.js') - }) - - it('returns success:false if cannot find webpack config', () => { - mockFs({ - '/a.js': '1', - '/b.js': '2', - }) - - const { success, payload } = WebpackTemplate.test('/') - - expect(success).to.equal(false) - expect(payload).to.equal(undefined) - }) - - it('correctly generates plugins config when webpack config path is missing', () => { - snapshotPluginsAstCode(WebpackTemplate) - }) - - it('correctly generates plugins config when webpack config path is provided', () => { - snapshotPluginsAstCode(WebpackTemplate, { webpackConfigPath: '/config/webpack.config.js' }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts deleted file mode 100644 index 6bc89f7fe0fe..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/reactWebpackFile.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as babel from '@babel/core' -import path from 'path' -import { Template } from '../Template' -import { findWebpackConfig } from '../templateUtils' - -export const WebpackTemplate: Template<{ webpackConfigPath: string }> = { - message: - 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-file' - }, - recommendedComponentFolder: 'cypress/component', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: (payload, { cypressProjectRoot }) => { - const includeWarnComment = !payload - const webpackConfigPath = payload - ? path.relative(cypressProjectRoot, payload.webpackConfigPath) - : './webpack.config.js' - - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast('const injectDevServer = require("@cypress/react/plugins/load-webpack")'), - IfComponentTestingPluginsAst: babel.template.ast([ - 'injectDevServer(on, config, {', - includeWarnComment - ? ' // TODO replace with valid webpack config path' - : '', - ` webpackFilename: '${webpackConfigPath}'`, - '})', - ].join('\n'), { preserveComments: true }), - } - }, - test: (root) => { - const webpackConfigPath = findWebpackConfig(root) - - return webpackConfigPath ? { - success: true, - payload: { webpackConfigPath }, - } : { - success: false, - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js deleted file mode 100644 index cffd3bce12ff..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options-module-exports.template.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @type import("webpack").Configuration */ -const webpackConfig = { - resolve: { - extensions: ['.js', '.ts', '.jsx', '.tsx'], - }, - mode: 'development', - devtool: false, - output: { - publicPath: '/', - chunkFilename: '[name].bundle.js', - }, - // TODO: update with valid configuration for your components - module: { - rules: [ - { - test: /\.(js|jsx|mjs|ts|tsx)$/, - loader: 'babel-loader', - options: { cacheDirectory: path.resolve(__dirname, '.babel-cache') }, - }, - ] - }, -} - -on('dev-server:start', (options) => startDevServer({ options, webpackConfig })) diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts deleted file mode 100644 index 8765be52910c..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpack-options.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fs from 'fs' -import path from 'path' -import * as babel from '@babel/core' -import chalk from 'chalk' -import { Template } from '../Template' - -export const WebpackOptions: Template = { - // this should never show ideally - message: `Unable to detect where webpack options are.`, - getExampleUrl: () => { - return 'https://github.com/cypress-io/cypress/tree/develop/npm/react/examples/webpack-options' - }, - test: () => ({ success: false }), - recommendedComponentFolder: 'src', - dependencies: ['webpack', '@cypress/webpack-dev-server'], - getPluginsCodeAst: () => { - return { - RequireAst: babel.template.ast([ - 'const path = require("path")', - 'const { startDevServer } = require("@cypress/webpack-dev-Server")', - ].join('\n')), - IfComponentTestingPluginsAst: babel.template.ast( - fs.readFileSync(path.resolve(__dirname, 'webpack-options-module-exports.template.js'), { encoding: 'utf-8' }), - { preserveComments: true }, - ), - } - }, - printHelper: () => { - console.log( - `${chalk.inverse('Important:')} this configuration is using ${chalk.blue( - 'new webpack configuration', - )} to bundle components. If you are using some framework (e.g. next) or bundling tool (e.g. rollup/vite) consider using them to bundle component specs for cypress. \n`, - ) - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts b/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts deleted file mode 100644 index c7b7e2e6f9a3..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/react/webpackOptions.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { WebpackOptions } from './webpack-options' -import { snapshotPluginsAstCode } from '../../../test-utils' - -describe('webpack-options template', () => { - it('correctly generates plugins config', () => snapshotPluginsAstCode(WebpackOptions)) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts b/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts deleted file mode 100644 index 8b1f8e978f3e..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/templateUtils.ts +++ /dev/null @@ -1,53 +0,0 @@ -import findUp from 'find-up' -import path from 'path' -import { createFindPackageJsonIterator } from '../../findPackageJson' - -export function extractWebpackConfigPathFromScript (script: string) { - if (script.includes('webpack ') || script.includes('webpack-dev-server ')) { - const webpackCliArgs = script.split(' ').map((part) => part.trim()) - const configArgIndex = webpackCliArgs.findIndex((arg) => arg === '--config') - - return configArgIndex === -1 ? null : webpackCliArgs[configArgIndex + 1] - } - - return null -} - -export function findWebpackConfig (root: string) { - const webpackConfigPath = findUp.sync('webpack.config.js', { cwd: root }) - - if (webpackConfigPath) { - return webpackConfigPath - } - - const packageJsonIterator = createFindPackageJsonIterator(root) - - const { success, payload } = packageJsonIterator.map(({ scripts }, packageJsonPath) => { - if (!scripts) { - return { success: false } - } - - for (const script of Object.values(scripts)) { - const webpackConfigRelativePath = extractWebpackConfigPathFromScript( - script, - ) - - if (webpackConfigRelativePath) { - const directoryRoot = path.resolve(packageJsonPath, '..') - const webpackConfigPath = path.resolve( - directoryRoot, - webpackConfigRelativePath, - ) - - return { - success: true, - payload: { webpackConfigPath }, - } - } - } - - return { success: false } - }) - - return success ? payload?.webpackConfigPath : null -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts deleted file mode 100644 index 512f4ff6ee8d..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Template } from '../Template' -import { VueCliTemplate } from './vueCli' -import { VueWebpackTemplate } from './vueWebpackFile' - -export const vueTemplates: Record> = { - webpack: VueWebpackTemplate, - 'vue-cli': VueCliTemplate, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts deleted file mode 100644 index 29bb76b76d2f..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { snapshotPluginsAstCode } from '../../../test-utils' -import { VueCliTemplate } from './vueCli' - -describe('vue webpack-file install template', () => { - beforeEach(mockFs.restore) - - it('resolves webpack.config.js', () => { - mockFs({ - '/package.json': JSON.stringify({ - 'devDependencies': { - '@vue/cli-plugin-babel': '~4.5.0', - '@vue/cli-plugin-eslint': '~4.5.0', - '@vue/cli-plugin-router': '~4.5.0', - '@vue/cli-service': '~4.5.0', - }, - }), - }) - - const { success } = VueCliTemplate.test('/') - - expect(success).to.equal(true) - }) - - it('returns success:false if vue-cli-service is not installed', () => { - mockFs({ - '/package.json': JSON.stringify({ - 'devDependencies': { - 'webpack': '*', - 'vue': '2.x', - }, - }), - }) - - const { success } = VueCliTemplate.test('/') - - expect(success).to.equal(false) - }) - - it('correctly generates plugins for vue-cli-service', () => { - snapshotPluginsAstCode(VueCliTemplate) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts deleted file mode 100644 index f920c85fa89a..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueCli.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as babel from '@babel/core' -import { scanFSForAvailableDependency } from '../../../findPackageJson' -import { Template } from '../Template' - -export const VueCliTemplate: Template = { - message: - 'It looks like you are using vue-cli-service to run and build an application.', - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', - recommendedComponentFolder: 'src', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: () => { - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast([ - 'const { startDevServer } = require("@cypress/webpack-dev-server")', - `const webpackConfig = require("@vue/cli-service/webpack.config.js")`, - ].join('\n')), - IfComponentTestingPluginsAst: babel.template.ast([ - `on('dev-server:start', (options) => startDevServer({ options, webpackConfig }))`, - ].join('\n'), { preserveComments: true }), - } - }, - test: (root) => { - const hasVueCliService = scanFSForAvailableDependency(root, { '@vue/cli-service': '>=4' }) - - return { - success: hasVueCliService, - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts deleted file mode 100644 index e0b058958ef2..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { expect } from 'chai' -import mockFs from 'mock-fs' -import { snapshotPluginsAstCode } from '../../../test-utils' -import { VueWebpackTemplate } from './vueWebpackFile' - -describe('vue webpack-file install template', () => { - beforeEach(mockFs.restore) - - it('resolves webpack.config.js', () => { - mockFs({ - '/webpack.config.js': 'module.exports = { }', - }) - - const { success, payload } = VueWebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/webpack.config.js') - }) - - it('finds the closest package.json and tries to fetch webpack config path from scrips', () => { - mockFs({ - '/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config configs/webpack.js', - }, - }), - }) - - const { success, payload } = VueWebpackTemplate.test(process.cwd()) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/configs/webpack.js') - }) - - it('looks for package.json in the upper folder', () => { - mockFs({ - '/some/deep/folder/withFile': 'test', - '/somewhere/configs/webpack.js': 'module.exports = { }', - '/package.json': JSON.stringify({ - scripts: { - build: 'webpack --config somewhere/configs/webpack.js', - }, - }), - }) - - const { success, payload } = VueWebpackTemplate.test( - '/some/deep/folder', - ) - - expect(success).to.equal(true) - expect(payload?.webpackConfigPath).to.equal('/somewhere/configs/webpack.js') - }) - - it('returns success:false if cannot find webpack config', () => { - mockFs({ - '/a.js': '1', - '/b.js': '2', - }) - - const { success, payload } = VueWebpackTemplate.test('/') - - expect(success).to.equal(false) - expect(payload).to.equal(undefined) - }) - - it('correctly generates plugins config when webpack config path is missing', () => { - snapshotPluginsAstCode(VueWebpackTemplate) - }) - - it('correctly generates plugins config when webpack config path is provided', () => { - snapshotPluginsAstCode(VueWebpackTemplate, { webpackConfigPath: '/build/webpack.config.js' }) - }) -}) diff --git a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts b/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts deleted file mode 100644 index 106145b4067c..000000000000 --- a/npm/create-cypress-tests/src/component-testing/templates/vue/vueWebpackFile.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as babel from '@babel/core' -import path from 'path' -import { Template } from '../Template' -import { findWebpackConfig } from '../templateUtils' - -export const VueWebpackTemplate: Template<{ webpackConfigPath: string }> = { - message: - 'It looks like you have custom `webpack.config.js`. We can use it to bundle the components for testing.', - getExampleUrl: () => 'https://github.com/cypress-io/cypress/tree/develop/npm/vue/examples/cli', - recommendedComponentFolder: 'cypress/component', - dependencies: ['@cypress/webpack-dev-server'], - getPluginsCodeAst: (payload, { cypressProjectRoot }) => { - const includeWarnComment = !payload - const webpackConfigPath = payload - ? path.relative(cypressProjectRoot, payload.webpackConfigPath) - : './webpack.config.js' - - return { - requiresReturnConfig: true, - RequireAst: babel.template.ast([ - 'const { startDevServer } = require("@cypress/webpack-dev-server")', - - `const webpackConfig = require("${webpackConfigPath}")`, - includeWarnComment - ? '// TODO replace with valid webpack config path' - : '', - ].join('\n'), { preserveComments: true }), - IfComponentTestingPluginsAst: babel.template.ast([ - `on('dev-server:start', (options) => startDevServer({ options, webpackConfig }))`, - ].join('\n')), - } - }, - test: (root) => { - const webpackConfigPath = findWebpackConfig(root) - - return webpackConfigPath ? { - success: true, - payload: { webpackConfigPath }, - } : { - success: false, - } - }, -} diff --git a/npm/create-cypress-tests/src/component-testing/versions.ts b/npm/create-cypress-tests/src/component-testing/versions.ts deleted file mode 100644 index 734122ff5fe2..000000000000 --- a/npm/create-cypress-tests/src/component-testing/versions.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const MIN_SUPPORTED_VERSION = { - 'react-scripts': '^=3.x || ^=4.x', - next: '^=9.x || ^=10.x', -} diff --git a/npm/create-cypress-tests/src/findPackageJson.ts b/npm/create-cypress-tests/src/findPackageJson.ts deleted file mode 100644 index 81858d319c95..000000000000 --- a/npm/create-cypress-tests/src/findPackageJson.ts +++ /dev/null @@ -1,116 +0,0 @@ -import path from 'path' -import fs from 'fs' -import findUp from 'find-up' -import { validateSemverVersion } from './utils' - -type PackageJsonLike = { - name?: string - scripts?: Record - dependencies?: Record - devDependencies?: Record - [key: string]: unknown -} - -type FindPackageJsonResult = - | { - packageData: PackageJsonLike - filename: string - done: false - } - | { - packageData: undefined - filename: undefined - done: true - } - -/** - * Return the parsed package.json that we find in a parent folder. - * - * @returns {Object} Value, filename and indication if the iteration is done. - */ -export function createFindPackageJsonIterator (rootPath = process.cwd()) { - function scanForPackageJson (cwd: string): FindPackageJsonResult { - const packageJsonPath = findUp.sync('package.json', { cwd }) - - if (!packageJsonPath) { - return { - packageData: undefined, - filename: undefined, - done: true, - } - } - - const packageData = JSON.parse( - fs.readFileSync(packageJsonPath, { - encoding: 'utf-8', - }), - ) - - return { - packageData, - filename: packageJsonPath, - done: false, - } - } - - return { - map: ( - cb: ( - data: PackageJsonLike, - packageJsonPath: string, - ) => { success: boolean, payload?: TPayload }, - ) => { - let stepPathToScan = rootPath - - // eslint-disable-next-line - while (true) { - const result = scanForPackageJson(stepPathToScan) - - if (result.done) { - // didn't find the package.json - return { success: false } - } - - if (result.packageData) { - const cbResult = cb(result.packageData, result.filename) - - if (cbResult.success) { - return { success: true, payload: cbResult.payload } - } - } - - const nextStepPathToScan = path.resolve(stepPathToScan, '..') - - if (nextStepPathToScan === stepPathToScan) { - // we are at the root. Give up - return { success: false } - } - - stepPathToScan = nextStepPathToScan - } - }, - } -} - -export function scanFSForAvailableDependency (cwd: string, lookingForDeps: Record) { - const { success } = createFindPackageJsonIterator(cwd) - .map(({ dependencies, devDependencies }, path) => { - if (!dependencies && !devDependencies) { - return { success: false } - } - - return { - success: Object.entries({ ...dependencies, ...devDependencies }) - .some(([dependency, version]) => { - return ( - Boolean(lookingForDeps[dependency]) - && validateSemverVersion(version, lookingForDeps[dependency] as string, dependency) - ) - }), - } - }) - - return success -} - -export type PackageJsonIterator = ReturnType diff --git a/npm/create-cypress-tests/src/index.ts b/npm/create-cypress-tests/src/index.ts deleted file mode 100644 index 9e6e68e3f4ec..000000000000 --- a/npm/create-cypress-tests/src/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node -import { program } from 'commander' -import { main } from './main' -import { version } from '../package.json' - -program -.option('--ignore-examples', 'Ignore generating example tests and fixtures by creating one ready-to-fill spec file') -.option('--use-npm', 'Use npm even if yarn is available') -.option('--ignore-ts', 'Ignore typescript if available') -.option('--component-tests', 'Run component testing installation without asking') - -program.version(version, '-v --version') -program.parse(process.argv) - -main({ - useNpm: program.useNpm, - ignoreTs: program.ignoreTs, - ignoreExamples: Boolean(program.ignoreExamples), - setupComponentTesting: program.componentTests, -}).catch(console.error) diff --git a/npm/create-cypress-tests/src/initialTemplate.ts b/npm/create-cypress-tests/src/initialTemplate.ts deleted file mode 100644 index 332df33e3899..000000000000 --- a/npm/create-cypress-tests/src/initialTemplate.ts +++ /dev/null @@ -1,18 +0,0 @@ -import path from 'path' -import fs from 'fs-extra' - -const INITIAL_TEMPLATE_PATH = path.resolve(__dirname, '..', 'initial-template') - -export async function getInitialSupportFilesPaths () { - return ( - await fs.readdir(path.join(INITIAL_TEMPLATE_PATH, 'support')) - ).map((filename) => path.join(INITIAL_TEMPLATE_PATH, 'support', filename)) -} - -export function getInitialPluginsFilePath () { - return path.join(INITIAL_TEMPLATE_PATH, 'plugins', 'index.js') -} - -export function getInitialTsConfigPath () { - return path.join(INITIAL_TEMPLATE_PATH, 'tsconfig.json') -} diff --git a/npm/create-cypress-tests/src/installCypress.ts b/npm/create-cypress-tests/src/installCypress.ts deleted file mode 100644 index 9abd21345b52..000000000000 --- a/npm/create-cypress-tests/src/installCypress.ts +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'fs-extra' -import findUp from 'find-up' -import path from 'path' -import { installDependency } from './utils' -import chalk from 'chalk' -import ora from 'ora' -import * as initialTemplate from './initialTemplate' - -type InstallCypressOpts = { - useYarn: boolean - useTypescript: boolean - ignoreExamples: boolean -} - -async function copyFiles ({ ignoreExamples, useTypescript }: InstallCypressOpts) { - let fileSpinner = ora('Creating config files').start() - - await fs.outputFile(path.resolve(process.cwd(), useTypescript ? 'cypress.config.ts' : 'cypress.config.js'), useTypescript ? `export default {}` : `module.exports = {}\n`) - await fs.copy( - initialTemplate.getInitialPluginsFilePath(), - path.resolve('cypress', 'plugins/index.js'), - ) - - const supportFiles: string[] = await initialTemplate.getInitialSupportFilesPaths() - - await Promise.all( - supportFiles.map((supportFilePath) => { - const newSupportFilePath = path.resolve('cypress', 'support', path.basename(supportFilePath)) - - return fs.copy(supportFilePath, newSupportFilePath) - }), - ) - - if (useTypescript) { - await fs.copy(initialTemplate.getInitialTsConfigPath(), path.resolve('cypress', 'tsconfig.json')) - } - - // TODO think about better approach - if (ignoreExamples) { - const dummySpec = [ - 'describe("Spec", () => {', - '', - '})', - '', - ].join('\n') - - const specFileName = useTypescript ? 'spec.cy.ts' : 'spec.cy.js' - const specFileToCreate = path.resolve('cypress', 'e2e', specFileName) - - await fs.outputFile(specFileToCreate, dummySpec) - console.log(`In order to ignore examples a spec file ${chalk.green(path.relative(process.cwd(), specFileToCreate))}.`) - } - - fileSpinner.succeed() -} - -export async function findInstalledOrInstallCypress (options: InstallCypressOpts) { - const configFile = options.useTypescript ? 'cypress.config.ts' : 'cypress.config.js' - let cypressConfigPath = await findUp(configFile) - - if (!cypressConfigPath) { - await installDependency('cypress', options) - await copyFiles(options) - - cypressConfigPath = await findUp(configFile) - } - - if (!cypressConfigPath) { - throw new Error('Unexpected error during cypress installation.') - } - - const config = await import(cypressConfigPath) - - return { - cypressConfigPath, - config: config.default, - } -} diff --git a/npm/create-cypress-tests/src/main.test.ts b/npm/create-cypress-tests/src/main.test.ts deleted file mode 100644 index 3b3a81eefa1b..000000000000 --- a/npm/create-cypress-tests/src/main.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { expect, use } from 'chai' -import path from 'path' -import sinon, { SinonStub, SinonSpy, SinonSpyCallApi } from 'sinon' -import mockFs from 'mock-fs' -import fsExtra from 'fs-extra' -import { main } from './main' -import sinonChai from 'sinon-chai' -import childProcess from 'child_process' - -use(sinonChai) - -function mockFsWithInitialTemplate (...args: Parameters) { - const [fsConfig, options] = args - - mockFs({ - ...fsConfig, - // @ts-expect-error Load required template files - [path.resolve(__dirname, '..', 'initial-template')]: mockFs.load(path.resolve(__dirname, '..', 'initial-template')), - }, options) -} - -function someOfSpyCallsIncludes (spy: any, logPart: string) { - return spy.getCalls().some( - (spy: SinonSpyCallApi) => { - return spy.args.some((callArg) => typeof callArg === 'string' && callArg.includes(logPart)) - }, - ) -} - -describe('create-cypress-tests', () => { - let promptSpy: SinonStub | null = null - let logSpy: SinonSpy | null = null - let errorSpy: SinonSpy | null = null - let execStub: SinonStub | null = null - let fsCopyStub: SinonStub | null = null - let processExitStub: SinonStub | null = null - - beforeEach(() => { - logSpy = sinon.spy(global.console, 'log') - errorSpy = sinon.spy(global.console, 'error') - // @ts-ignore - execStub = sinon.stub(childProcess, 'exec').callsFake((command, callback) => callback()) - // @ts-ignore - fsCopyStub = sinon.stub(fsExtra, 'copy').returns(Promise.resolve()) - processExitStub = sinon.stub(process, 'exit').callsFake(() => { - throw new Error('process.exit should not be called') - }) - }) - - afterEach(() => { - mockFs.restore() - logSpy?.restore() - promptSpy?.restore() - execStub?.restore() - fsCopyStub?.restore() - processExitStub?.restore() - execStub?.restore() - errorSpy?.restore() - }) - - it('Install cypress if no config found', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect(execStub).calledWith('yarn add cypress --dev') - }) - - it('Uses npm if yarn is not available', async () => { - execStub - ?.onFirstCall().callsFake((command, callback) => callback('yarn is not available')) - ?.onSecondCall().callsFake((command, callback) => callback()) - ?.onThirdCall().callsFake((command, callback) => callback()) - - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - expect(execStub).calledWith('npm install -D cypress') - }) - - it('Uses npm if --use-npm was provided', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect(execStub).calledWith('npm install -D cypress') - }) - - it('Prints correct commands helper for npm', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - expect(someOfSpyCallsIncludes(logSpy, 'npx cypress open')).to.be.true - }) - - it('Prints correct commands helper for yarn', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - expect(someOfSpyCallsIncludes(logSpy, 'yarn cypress open')).to.be.true - }) - - it('Fails if git repository have untracked or uncommitted files', async () => { - mockFsWithInitialTemplate({ - '/package.json': JSON.stringify({ }), - }) - - execStub?.callsFake((_, callback) => callback(null, { stdout: 'test' })) - processExitStub?.callsFake(() => {}) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect( - someOfSpyCallsIncludes(errorSpy, 'This repository has untracked files or uncommitted changes.'), - ).to.equal(true) - - expect(processExitStub).to.be.called - }) - - context('e2e fs tests', () => { - const e2eTestOutputPath = path.resolve(__dirname, 'test-output') - - beforeEach(async () => { - fsCopyStub?.restore() - mockFs.restore() - sinon.stub(process, 'cwd').returns(e2eTestOutputPath) - - await fsExtra.remove(e2eTestOutputPath) - await fsExtra.mkdir(e2eTestOutputPath) - }) - - it('Copies plugins and support files', async () => { - await fsExtra.outputFile( - path.join(e2eTestOutputPath, 'package.json'), - JSON.stringify({ name: 'test' }, null, 2), - ) - - await main({ useNpm: true, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'plugins', 'index.js'))).to.equal(true) - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'e2e.js'))).to.equal(true) - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'support', 'commands.js'))).to.equal(true) - expect(await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress.config.ts'))).to.equal(true) - }) - - it('Copies tsconfig if typescript is installed', async () => { - await fsExtra.outputFile( - path.join(e2eTestOutputPath, 'package.json'), - JSON.stringify({ - name: 'test-typescript', - dependencies: { typescript: '^4.0.0' }, - }, null, 2), - ) - - await main({ useNpm: false, ignoreTs: false, ignoreExamples: false, setupComponentTesting: false }) - await fsExtra.pathExists(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) - console.log(path.resolve(e2eTestOutputPath, 'cypress', 'tsconfig.json')) - }) - }) -}) diff --git a/npm/create-cypress-tests/src/main.ts b/npm/create-cypress-tests/src/main.ts deleted file mode 100644 index fc28840559e6..000000000000 --- a/npm/create-cypress-tests/src/main.ts +++ /dev/null @@ -1,106 +0,0 @@ -import fs from 'fs' -import findUp from 'find-up' -import chalk from 'chalk' -import util from 'util' -import inquirer from 'inquirer' -import { initComponentTesting } from './component-testing/init-component-testing' -import { exec } from 'child_process' -import { scanFSForAvailableDependency } from './findPackageJson' -import { findInstalledOrInstallCypress } from './installCypress' - -type MainArgv = { - useNpm: boolean - ignoreTs: boolean - ignoreExamples: boolean - setupComponentTesting: boolean -} - -async function getGitStatus () { - const execAsync = util.promisify(exec) - - try { - let { stdout } = await execAsync(`git status --porcelain`) - - console.log(stdout) - - return stdout.trim() - } catch (e) { - return '' - } -} - -async function shouldUseYarn () { - const execAsync = util.promisify(exec) - - return execAsync('yarn --version') - .then(() => true) - .catch(() => false) -} - -function shouldUseTypescript () { - return scanFSForAvailableDependency(process.cwd(), { typescript: '*' }) -} - -async function askForComponentTesting () { - const { shouldSetupComponentTesting } = await inquirer.prompt({ - type: 'confirm', - name: 'shouldSetupComponentTesting', - message: `Do you want to setup ${chalk.cyan('component testing')}? ${chalk.grey('You can do this later by rerunning this command')}.`, - }) - - return shouldSetupComponentTesting -} - -function printCypressCommandsHelper (options: { shouldSetupComponentTesting: boolean, useYarn: boolean }) { - const printCommand = (command: string, description: string) => { - const displayedRunner = options.useYarn ? 'yarn' : 'npx' - - console.log() - console.log(chalk.cyan(` ${displayedRunner} ${command}`)) - console.log(` ${description}`) - } - - printCommand('cypress open', 'Opens cypress local development app.') - printCommand('cypress run', 'Runs tests in headless mode.') - - if (options.shouldSetupComponentTesting) { - printCommand('cypress open --component', 'Opens Cypress component testing interactive mode.') - printCommand('cypress run-ct', 'Runs all Cypress component testing suites.') - } -} - -export async function main ({ useNpm, ignoreTs, setupComponentTesting, ignoreExamples }: MainArgv) { - const rootPackageJsonPath = await findUp('package.json') - const useYarn = useNpm === true ? false : await shouldUseYarn() - const useTypescript = ignoreTs ? false : shouldUseTypescript() - - if (!rootPackageJsonPath) { - console.log(`${chalk.bold.red(`It looks like you are running cypress installation wizard outside of npm module.`)}\nIf you would like to setup a new project for cypress tests please run the ${chalk.inverse(useNpm ? ' npm init ' : ' yarn init ')} first.`) - process.exit(1) - } - - const { name = 'unknown', version = '0.0.0' } = JSON.parse(fs.readFileSync(rootPackageJsonPath).toString()) - - console.log(`Running ${chalk.green('cypress 🌲')} installation wizard for ${chalk.cyan(`${name}@${version}`)}`) - - const gitStatus = await getGitStatus() - - if (gitStatus) { - console.error(`\n${chalk.bold.red('This repository has untracked files or uncommitted changes.')}\nThis command will ${chalk.cyan('make changes in the codebase')}, so please remove untracked files, stash or commit any changes, and try again.`) - process.exit(1) - } - - const { config, cypressConfigPath } = await findInstalledOrInstallCypress({ useYarn, useTypescript, ignoreExamples }) - const shouldSetupComponentTesting = setupComponentTesting ?? await askForComponentTesting() - - if (shouldSetupComponentTesting) { - await initComponentTesting({ config, cypressConfigPath, useYarn }) - } - - console.log(`\n👍 Success! Cypress is installed and ready to run tests.`) - printCypressCommandsHelper({ useYarn, shouldSetupComponentTesting }) - - console.log(`\nHappy testing with ${chalk.green('cypress.io')} 🌲\n`) -} - -export { scanFSForAvailableDependency } diff --git a/npm/create-cypress-tests/src/test-utils.ts b/npm/create-cypress-tests/src/test-utils.ts deleted file mode 100644 index 6021690678b9..000000000000 --- a/npm/create-cypress-tests/src/test-utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as babel from '@babel/core' -import snapshot from 'snap-shot-it' -import mockFs from 'mock-fs' -import { SinonSpyCallApi } from 'sinon' -import { createTransformPluginsFileBabelPlugin } from './component-testing/babel/babelTransform' -import { Template } from './component-testing/templates/Template' - -export function someOfSpyCallsIncludes (spy: any, logPart: string) { - return spy.getCalls().some( - (spy: SinonSpyCallApi) => { - return spy.args.some((callArg) => typeof callArg === 'string' && callArg.includes(logPart)) - }, - ) -} - -export function snapshotPluginsAstCode (template: Template, payload?: T) { - mockFs.restore() - const code = [ - 'const something = require("something")', - 'module.exports = (on) => {', - '};', - ].join('\n') - - const babelPlugin = createTransformPluginsFileBabelPlugin(template.getPluginsCodeAst(payload ?? null, { cypressProjectRoot: '/' })) - const output = babel.transformSync(code, { - plugins: [babelPlugin], - }) - - if (!output || !output.code) { - throw new Error('Babel transform output is empty.') - } - - snapshot(output.code) -} diff --git a/npm/create-cypress-tests/src/utils.ts b/npm/create-cypress-tests/src/utils.ts deleted file mode 100644 index e79055e60283..000000000000 --- a/npm/create-cypress-tests/src/utils.ts +++ /dev/null @@ -1,76 +0,0 @@ -import semver from 'semver' -import chalk from 'chalk' -import ora from 'ora' -import util from 'util' -import { exec } from 'child_process' - -/** - * Compare available version range with the provided version from package.json - * @param packageName Package name used to display a helper message to user. - */ -export function validateSemverVersion ( - version: string, - allowedVersionRange: string, - packageName?: string, -) { - let isValid: boolean - - try { - const minAvailableVersion = semver.minVersion(version)?.raw - - isValid = Boolean( - minAvailableVersion && - semver.satisfies(minAvailableVersion, allowedVersionRange), - ) - } catch (e) { - // handle not semver versions like "latest", "git:" or "file:" - isValid = false - } - - if (!isValid && packageName) { - const packageNameSymbol = chalk.green(packageName) - - console.warn( - `It seems like you are using ${packageNameSymbol} with version ${chalk.bold( - version, - )}, however we support only ${packageNameSymbol} projects with version ${chalk.bold( - allowedVersionRange, - )}. \n`, - ) - } - - return isValid -} - -export async function installDependency (name: string, options: { useYarn: boolean}) { - const commandToRun = options.useYarn ? `yarn add ${name} --dev` : `npm install -D ${name}` - let cliSpinner = ora(`Installing ${name} ${chalk.gray(`(${commandToRun})`)}`).start() - - try { - // do this inside function for test stubbing - const execAsync = util.promisify(exec) - - await execAsync(commandToRun) - } catch (e) { - cliSpinner.fail(`Can not install ${name} using ${chalk.inverse(commandToRun)})}`) - console.log(e) - - process.exit(1) - } - - cliSpinner.succeed() -} - -export async function prettifyCode (finalCode?: string | null) { - try { - const maybePrettier = require('prettier') - - if (maybePrettier && maybePrettier.format) { - finalCode = maybePrettier.format(finalCode, { parser: 'babel' }) - } - } catch (e) { - return null - } finally { - return finalCode - } -} diff --git a/npm/create-cypress-tests/tsconfig.json b/npm/create-cypress-tests/tsconfig.json deleted file mode 100644 index 5cf13aaaaa09..000000000000 --- a/npm/create-cypress-tests/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "outDir": "./dist", - "rootDir": ".", - "esModuleInterop": true, - "allowJs": true, - "allowSyntheticDefaultImports": true, - "declaration": true, - "moduleResolution": "node", - "strict": true, - "strictNullChecks": true, - "resolveJsonModule": true, - "module": "CommonJS", - "target": "ES2018", - "types": [ - "node", - ], - "lib": [ - "ES2018" - ], - "noImplicitAny": true - }, - "exclude": [ - "./src/**/*.test.ts", - "node_modules" - ], - "include": [ - "./src/**/*.ts", - ] -} diff --git a/npm/create-cypress-tests/tsconfig.test.json b/npm/create-cypress-tests/tsconfig.test.json deleted file mode 100644 index df01205d9a40..000000000000 --- a/npm/create-cypress-tests/tsconfig.test.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "noEmit": true, - "types": [ - "mocha" - ] - }, - "include": [ - "./src/**/*.test.ts" - ] -} \ No newline at end of file diff --git a/npm/puppeteer/.eslintignore b/npm/puppeteer/.eslintignore new file mode 100644 index 000000000000..9bd1cd76241e --- /dev/null +++ b/npm/puppeteer/.eslintignore @@ -0,0 +1,4 @@ +**/dist +**/*.d.ts +**/package-lock.json +**/tsconfig.json diff --git a/npm/create-cypress-tests/.mocharc.json b/npm/puppeteer/.mocharc.json similarity index 100% rename from npm/create-cypress-tests/.mocharc.json rename to npm/puppeteer/.mocharc.json diff --git a/npm/puppeteer/CHANGELOG.md b/npm/puppeteer/CHANGELOG.md new file mode 100644 index 000000000000..540675e4467a --- /dev/null +++ b/npm/puppeteer/CHANGELOG.md @@ -0,0 +1,13 @@ +# [@cypress/puppeteer-v0.1.1](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.1.0...@cypress/puppeteer-v0.1.1) (2023-11-29) + + +### Bug Fixes + +* Resolve types and dist issues with @cypress/puppeteer ([#28424](https://github.com/cypress-io/cypress/issues/28424)) ([72225db](https://github.com/cypress-io/cypress/commit/72225db03327744844dcfbcc72b40e85de6a2761)) + +# [@cypress/puppeteer-v0.1.0](https://github.com/cypress-io/cypress/compare/@cypress/puppeteer-v0.0.1...@cypress/puppeteer-v0.1.0) (2023-11-28) + + +### Features + +* Initial release ([#28370](https://github.com/cypress-io/cypress/issues/28370)) ([b34d145](https://github.com/cypress-io/cypress/commit/b34d14571689a9b36efc707a3a48f27edcb98113)) diff --git a/npm/puppeteer/README.md b/npm/puppeteer/README.md new file mode 100644 index 000000000000..3e1f1c9448d7 --- /dev/null +++ b/npm/puppeteer/README.md @@ -0,0 +1,374 @@ +# @cypress/puppeteer [beta] + +Utilize [Puppeteer's browser API](https://pptr.dev/api) within Cypress with a single command. + +> This plugin is in public beta, so we'd love to get your feedback to improve it. Please leave any feedback you have in [this discussion](https://github.com/cypress-io/cypress/discussions/28410). + +# Table of Contents + +- [Installation](#installation) +- [Compatibility](#compatibility) +- [Usage](#usage) +- [API](#api) +- [Examples](#examples) +- [Contributing](#contributing) +- [Changelog](./CHANGELOG.md) + +# Installation + +## npm + +```sh +npm install --save-dev @cypress/puppeteer +``` + +## yarn + +```sh +yarn add --dev @cypress/puppeteer +``` + +## With TypeScript + +Add the following in `tsconfig.json`: + +```json +{ + "compilerOptions": { + "types": ["cypress", "@cypress/puppeteer/support"] + } +} +``` + +## Compatibility + +`@cypress/puppeteer` requires Cypress version 13.6.0 or greater. + +Only Chromium-based browsers (e.g. Chrome, Chromium, Electron) are supported. + +## Usage + +`@cypress/puppeteer` is set up in your Cypress config and support file, then executed in your spec. See [API](#api) and [Examples](#examples) below for more details. + +While the `cy.puppeteer()` command is executed in the browser, the majority of the Puppeteer execution is run in the Node process via your Cypress config. You pass a string message name to `cy.puppeteer()` that indicates which message handler to execute in the Cypress config. This is similar to how [cy.task()](on.cypress.io/task) operates. + +In your Cypress config (e.g. `cypress.config.ts`): + +```typescript +import { setup } from '@cypress/puppeteer' + +export default defineConfig({ + e2e: { + setupNodeEvents (on) { + setup({ + on, + onMessage: { + async myMessageHander (browser) { + // Utilize the Puppeteer browser instance and the Puppeteer API to interact with and automate the browser + }, + }, + }) + }, + }, +} +``` + +In your support file (e.g. `cypress/support/e2e.ts`): + +```typescript +import '@cypress/puppeteer/support' +``` + +In your spec (e.g. `spec.cy.ts`): + +```typescript + it('switches to and tests a new tab', () => { + cy.visit('/') + cy.get('button').click() // opens a new tab + + cy + .puppeteer('myMessageHander') + .should('equal', 'You said: Hello from Page 1') + }) +``` + +## API + +### Cypress Config - setup + +This sets up `@cypress/puppeteer` message handlers that run Puppeteer browser automation. + +```typescript +setup(options) +``` + +#### Options + +- `on` _required_: The `on` event registration function provided by `setupNodeEvents` +- `onMessage` _required_: An object with string keys and function values (see more details [below](#onmessage)) +- `puppeteer` _optional_: The `puppeteer` library imported from `puppeteer-core`, overriding the default version of `puppeteer-core` used by this plugin + +##### onMessage + +The keys provided in this are used to invoke their corresponding functions by calling `cy.puppeteer(key)` in your Cypress test. + +The functions should contain Puppeteer code for automating the browser. The code is executed within Node.js and not within the browser, so Cypress commands and DOM APIs cannot be utilized. + +The functions receive the following arguments: + +###### browser + +A [puppeteer browser instance](https://pptr.dev/api/puppeteer.browser) connected to the Cypress-launched browser. + +###### ...args + +The rest of the arguments are any de-serialized arguments passed to the `cy.puppeteer()` command from your Cypress test. + +### Cypress Config - retry + +This is a utility function provided to aid in retrying actions that may initially fail. + +```typescript +retry(functionToRetry[, options]) +``` + +#### functionToRetry + +_required_ + +A function that will run and retry if an error is thrown. If an error is not thrown, `retry` will return the value returned by this function. + +The function will continue to run at the default or configured interval until the default or configured timeout, at which point `retry` will throw an error and cease retrying this function. + +#### Options + +_optional_ + +- `timeout` _optional_: The total time in milliseconds during which to attempt retrying the function. Default: `4000ms` +- `delayBetweenTries` _optional_: The time to wait between retries. Default: `200ms` + +### Cypress Spec - cy.puppeteer() + +```typescript +cy.puppeteer(messageName[, ...args]) +``` + +#### messageName + +_required_ + +A string matching one of the keys passed to the `onMessage` option of `setup` in your Cypress config. + +#### ...args + +_optional_ + +Values that will be passed to the message handler. These values must be JSON-serializable. + +Example: + +```typescript +// spec +cy.puppeteer('testNewTab', 'value 1', 42, [true, false]) + +// Cypress config +setup({ + on, + onMessage: { + testNewTab (browser, stringArg, numberArg, arrayOfBooleans) { + // stringArg === 'value 1' + // numberArg === 42 + // arrayOfBooleans[0] === true / arrayOfBooleans[1] === false + } + } +}) +``` + +## Examples + +These examples can be found and run in the [Cypress tests of this package](./cypress) with this project's [cypress.config.ts](./cypress.config.ts). + +While these examples use tabs, they could just as easily apply to windows. Tabs and windows are essentially the same things as far as Puppeteer is concerned and encapsulated by instances of the [Page class](https://pptr.dev/api/puppeteer.page/). + +### Switching to a new tab + +This example demonstrates the following: + +- Switching to a tab opened by an action in the Cypress test +- Getting the page instance via Puppeteer utilizing the `retry` function +- Getting page references and content via puppeteer +- Passing that content back to be asserted on in Cypress + +_spec.cy.ts_ + +```typescript +it('switches to a new tab', () => { + cy.visit('/cypress/fixtures/page-1.html') + cy.get('input').type('Hello from Page 1') + cy.get('button').click() // Triggers a new tab to open + + cy + .puppeteer('switchToTabAndGetContent') + .should('equal', 'You said: Hello from Page 1') +}) +``` + +_cypress.config.ts_ + +```typescript +import { defineConfig } from 'cypress' +import type { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' + +import { setup, retry } from '@cypress/puppeteer' + +export default defineConfig({ + e2e: { + setupNodeEvents (on) { + setup({ + on, + onMessage: { + async switchToTabAndGetContent (browser: PuppeteerBrowser) { + // In this message handler, we utilize the Puppeteer API to interact with the browser and the new tab that our Cypress tests has opened + + // Utilize the retry since the page may not have opened and loaded by the time this runs + const page = await retry>(async () => { + // The browser will (eventually) have 2 tabs open: the Cypress tab and the newly opened tab + // In Puppeteer, tabs and windows are called pages + const pages = await browser.pages() + // Try to find the page we want to interact with + const page = pages.find((page) => page.url().includes('page-2.html')) + + // If we can't find the page, it probably hasn't loaded yet, so throw an error to signal that this function should retry + if (!page) throw new Error('Could not find page') + + // Otherwise, return the page instance and it will be returned by the `retry` function itself + return page + }) + + // Cypress will maintain focus on the Cypress tab within the browser. It's generally a good idea to bring the page to the front to interact with it. + await page.bringToFront() + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + }, + }) + }, + }, +}) +``` + +### Creating a new tab + +This example demonstrates the following: + +- Passing a non-default version of puppeteer to `@cypress/puppeteer` +- Passing arguments from `cy.puppeteer()` to the message handler +- Creating a new tab and visiting a page via Puppeteer +- Getting page references and content via puppeteer +- Passing that content back to be asserted on in Cypress + +_spec.cy.ts_ + +```typescript +it('creates a new tab', () => { + cy.visit('/cypress/fixtures/page-3.html') + // We get a dynamic value from the page and pass it through to the puppeteer + // message handler + cy.get('#message').invoke('text').then((message) => { + cy.puppeteer('createTabAndGetContent', message) + .should('equal', 'I approve this message: Cypress and Puppeteer make a great combo') + }) +}) +``` + +_cypress.config.ts_ + +```typescript +import { defineConfig } from 'cypress' +import puppeteer, { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' + +import { setup, retry } from '@cypress/puppeteer' + +export default defineConfig({ + e2e: { + setupNodeEvents (on) { + setup({ + on, + // Pass in your own version of puppeteer to be used instead of the default one + puppeteer, + onMessage: { + async createTabAndGetContent (browser: PuppeteerBrowser, text: string) { + // In this message handler, we utilize the Puppeteer API to interact with the browser, creating a new tab and getting its content + + // This will create a new tab within the Cypress-launched browser + const page = await browser.newPage() + + // Text comes from the test invocation of `cy.puppeteer()` + await page.goto(`http://localhost:8000/cypress/fixtures/page-4.html?text=${text}`) + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + }, + }) + }, + }, +}) +``` + +## Contributing + +Build the TypeScript files: + +```shell +yarn build +``` + +Watch the TypeScript files and rebuild on file change: + +```shell +yarn watch +``` + +Open Cypress tests: + +```shell +yarn cypress:open +``` + +Run Cypress tests once: + +```shell +yarn cypress:run +``` + +Run all unit tests once: + +```shell +yarn test +``` + +Run unit tests in watch mode: + +```shell +yarn test-watch +``` + +## [Changelog](./CHANGELOG.md) diff --git a/npm/puppeteer/cypress.config.ts b/npm/puppeteer/cypress.config.ts new file mode 100644 index 000000000000..e0eda55b48f3 --- /dev/null +++ b/npm/puppeteer/cypress.config.ts @@ -0,0 +1,81 @@ +import { defineConfig } from 'cypress' +import type { Browser as PuppeteerBrowser, Page } from 'puppeteer-core' +import path from 'path' +import express from 'express' + +import { setup, retry } from './src/plugin' + +const serverPort = 8000 + +export default defineConfig({ + e2e: { + baseUrl: `http://localhost:${serverPort}`, + setupNodeEvents (on) { + setup({ + on, + onMessage: { + async switchToTabAndGetContent (browser: PuppeteerBrowser) { + // Utilize the retry since the page may not have opened and loaded by the time this runs + const page = await retry>(async () => { + const pages = await browser.pages() + const page = pages.find((page) => page.url().includes('page-2.html')) + + // If we haven't found the page, throw an error to signal that it should retry + if (!page) throw new Error('Could not find page matching `page-2.html`') + + // Otherwise, return the page instance and it will be returned by the `retry` function itself + return page + }) + + // Cypress will maintain focus on the Cypress tab within the browser. It's generally a good idea to bring the page to the front to interact with it. + await page.bringToFront() + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + + async createTabAndGetContent (browser: PuppeteerBrowser, text: string) { + const page = await browser.newPage() + + // Text comes from the test invocation of `cy.puppeteer()` + await page.goto(`http://localhost:${serverPort}/cypress/fixtures/page-4.html?text=${text}`) + + const paragraph = (await page.waitForSelector('p'))! + const paragraphText = await page.evaluate((el) => el.textContent, paragraph) + + // Clean up any references before finishing up + paragraph.dispose() + + await page.close() + + // Return the paragraph text and it will be the value yielded by the `cy.puppeteer()` invocation in the spec + return paragraphText + }, + }, + }) + + return new Promise((resolve) => { + const app = express() + + app.set('port', serverPort) + app.set('view engine', 'html') + app.use(express.static(path.join(__dirname))) + + app.listen(serverPort, () => { + // eslint-disable-next-line no-console + console.log(`Express server listening on http://localhost:${serverPort}`) + + resolve() + }) + }) + }, + }, +}) diff --git a/npm/puppeteer/cypress/e2e/multi-tab.cy.ts b/npm/puppeteer/cypress/e2e/multi-tab.cy.ts new file mode 100644 index 000000000000..aceb1de32c20 --- /dev/null +++ b/npm/puppeteer/cypress/e2e/multi-tab.cy.ts @@ -0,0 +1,22 @@ +describe('multi-tab testing', () => { + it('switches to a new tab', () => { + cy.visit('/cypress/fixtures/page-1.html') + cy.get('input').type('Hello from Page 1') + cy.get('button').click() // Triggers a new tab to open + + cy + .puppeteer('switchToTabAndGetContent') + .should('equal', 'You said: Hello from Page 1') + }) + + it('creates a new tab', () => { + cy.visit('/cypress/fixtures/page-3.html') + // We get a dynamic value from the page and pass it through to the puppeteer + // message handler + cy.get('#message').invoke('text').then((message) => { + cy + .puppeteer('createTabAndGetContent', message) + .should('equal', 'I approve this message: Cypress and Puppeteer make a great combo') + }) + }) +}) diff --git a/npm/puppeteer/cypress/fixtures/page-1.html b/npm/puppeteer/cypress/fixtures/page-1.html new file mode 100644 index 000000000000..0bdcc597cfdd --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/page-1.html @@ -0,0 +1,24 @@ + + + + Page 1 + + + +
+

Page 1

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

Page 2

+

You said:

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

Page 3

+ +

The message: Cypress and Puppeteer make a great combo

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

Page 4

+

I approve this message:

+
+ + + + diff --git a/npm/puppeteer/cypress/fixtures/styles.css b/npm/puppeteer/cypress/fixtures/styles.css new file mode 100644 index 000000000000..4dbd6b880842 --- /dev/null +++ b/npm/puppeteer/cypress/fixtures/styles.css @@ -0,0 +1,55 @@ +* { + box-sizing: border-box; +} + +html, +body { + background: #e9e9e9; + color: #333; + font-family: sans-serif; + font-size: 18px; + height: 100%; +} + +main { + align-items: center; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + margin: 0 auto; + width: 800px; +} + +p { + font-size: 1.4em; + line-height: 1.5 +} + +a, +button { + background: #0a595e; + border: none; + border-radius: 2em; + color: white; + display: block; + font-size: 1em; + text-align: center; + margin: 1em 0; + padding: 1em 2em; + text-decoration: none; + width: 100%; +} + +code { + font-family: monospace; + color: #b2cdcd; +} + +input { + border: solid 1px #c8c8c8; + border-radius: 0.5em; + font-size: 1em; + padding: 1em 1.5em; + width: 100%; +} diff --git a/npm/puppeteer/cypress/support/e2e.ts b/npm/puppeteer/cypress/support/e2e.ts new file mode 100644 index 000000000000..8a7842cd60ca --- /dev/null +++ b/npm/puppeteer/cypress/support/e2e.ts @@ -0,0 +1 @@ +import '../../src/support' diff --git a/npm/create-cypress-tests/scripts/examples/tsconfig.json b/npm/puppeteer/cypress/tsconfig.json similarity index 51% rename from npm/create-cypress-tests/scripts/examples/tsconfig.json rename to npm/puppeteer/cypress/tsconfig.json index 08c029d44419..ea3daa2c3185 100644 --- a/npm/create-cypress-tests/scripts/examples/tsconfig.json +++ b/npm/puppeteer/cypress/tsconfig.json @@ -1,15 +1,11 @@ { "compilerOptions": { - "target": "es5", - "lib": [ - "es5", - "dom" - ], "types": [ "cypress" ] }, "include": [ - "**/*.ts*" + "../index.d.ts", + "e2e/**/*.ts" ] -} \ No newline at end of file +} diff --git a/npm/puppeteer/package.json b/npm/puppeteer/package.json new file mode 100644 index 000000000000..50358f58738a --- /dev/null +++ b/npm/puppeteer/package.json @@ -0,0 +1,54 @@ +{ + "name": "@cypress/puppeteer", + "version": "0.0.0-development", + "description": "Plugin for enhancing Cypress tests with Puppeteer", + "private": false, + "main": "dist/plugin/index.js", + "scripts": { + "build-prod": "yarn build", + "build": "rimraf dist && tsc || echo 'built, with errors'", + "check-ts": "tsc --noEmit", + "cypress:open": "node ../../scripts/cypress.js open", + "cypress:run": "node ../../scripts/cypress.js run --browser chrome", + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .", + "semantic-release": "semantic-release", + "test": "mocha test/unit/*.spec.ts", + "test-watch": "yarn test & chokidar '**/*.ts' 'test/unit/*.ts' -c 'yarn test'", + "watch": "rimraf dist && tsc --watch" + }, + "dependencies": { + "lodash": "^4.17.21", + "puppeteer-core": "^21.2.1" + }, + "devDependencies": { + "chai-as-promised": "^7.1.1", + "chokidar": "^3.5.3", + "express": "4.17.3", + "mocha": "^9.2.2", + "rimraf": "^5.0.1", + "semantic-release": "19.0.3", + "sinon": "^13.0.1", + "sinon-chai": "^3.7.0", + "ts-node": "^10.9.1", + "typescript": "4.7.4" + }, + "peerDependencies": { + "cypress": ">=13.6.0" + }, + "files": [ + "dist", + "support" + ], + "types": "dist/plugin/index.d.ts", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/cypress-io/cypress.git" + }, + "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/puppeteer/#readme", + "author": "Cypress App Team", + "bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fpuppeteer&template=1-bug-report.md&title=", + "publishConfig": { + "access": "public" + } +} diff --git a/npm/puppeteer/src/plugin/index.ts b/npm/puppeteer/src/plugin/index.ts new file mode 100644 index 000000000000..d880e3c57a3b --- /dev/null +++ b/npm/puppeteer/src/plugin/index.ts @@ -0,0 +1,3 @@ +export { setup } from './setup' + +export { retry } from './retry' diff --git a/npm/puppeteer/src/plugin/retry.ts b/npm/puppeteer/src/plugin/retry.ts new file mode 100644 index 000000000000..3ee7bd3cdc2c --- /dev/null +++ b/npm/puppeteer/src/plugin/retry.ts @@ -0,0 +1,28 @@ +import { pluginError } from './util' + +function delay (time: number) { + return new Promise((resolve) => { + setTimeout(() => resolve(), time) + }) +} + +export async function retry (functionToRetry: () => T, options?: { timeout?: number, delayBetweenTries?: number }): Promise { + const timeout = options?.timeout !== undefined ? options?.timeout : 4000 + const delayBetweenTries = options?.delayBetweenTries !== undefined ? options?.delayBetweenTries : 200 + + const makeAttempt = async (timeElapsed = 0): Promise => { + try { + return await functionToRetry() + } catch (err: any) { + await delay(delayBetweenTries) + + if (timeElapsed >= timeout) { + throw pluginError(`Failed retrying after ${timeout}ms: ${err.message}`) + } + + return makeAttempt(timeElapsed + delayBetweenTries) + } + } + + return makeAttempt() +} diff --git a/npm/puppeteer/src/plugin/setup.ts b/npm/puppeteer/src/plugin/setup.ts new file mode 100644 index 000000000000..999e52c45546 --- /dev/null +++ b/npm/puppeteer/src/plugin/setup.ts @@ -0,0 +1,124 @@ +import isPlainObject from 'lodash/isPlainObject' +import defaultPuppeteer, { Browser, PuppeteerNode } from 'puppeteer-core' +import { pluginError } from './util' + +type MessageHandler = (browser: Browser, ...args: any[]) => any | Promise + +interface SetupOptions { + onMessage: Record + on: Cypress.PluginEvents + puppeteer?: PuppeteerNode +} + +function messageHandlerError (err: any) { + const errObject = {} as any + + if (typeof err === 'string') { + errObject.message = err + } else if (typeof err === 'object') { + Object.assign(errObject, { + name: err.name, + message: err.message, + stack: err.stack, + }) + } else { + errObject.message = err + } + + return { + __error__: errObject, + } +} + +export function setup (options: SetupOptions) { + if (!options) { + throw pluginError('Must provide options argument to `setup`.') + } + + if (!isPlainObject(options)) { + throw pluginError('The options argument provided to `setup` must be an object.') + } + + if (!options.on) { + throw pluginError('Must provide `on` function to `setup`.') + } + + if (typeof options.on !== 'function') { + throw pluginError('The `on` option provided to `setup` must be a function.') + } + + if (!options.onMessage) { + throw pluginError('Must provide `onMessage` object to `setup`.') + } + + if (!isPlainObject(options.onMessage)) { + throw pluginError('The `onMessage` option provided to `setup` must be an object.') + } + + const puppeteer = options.puppeteer || defaultPuppeteer + + let cypressBrowser: Cypress.Browser + let debuggerUrl: string + + try { + options.on('after:browser:launch', async (browser, options) => { + cypressBrowser = browser + debuggerUrl = options.webSocketDebuggerUrl + }) + } catch (err: any) { + throw pluginError(`Could not set up \`after:browser:launch\` task. Ensure you are running Cypress >= 13.6.0. The following error was encountered:\n\n${err.stack}`) + } + + options.on('task', { + async __cypressPuppeteer__ ({ name, args }: { name: string, args: any[] }) { + if (!cypressBrowser) { + return messageHandlerError(pluginError(`Lost the reference to the browser. This usually occurs because the Cypress config was reloaded without the browser re-launching. Close and re-open the browser.`)) + } + + if (cypressBrowser.family !== 'chromium') { + return messageHandlerError(pluginError(`Only browsers in the "Chromium" family are supported. You are currently running a browser with the family: ${cypressBrowser.family}`)) + } + + const messageHandler = options.onMessage[name] + + if (!messageHandler) { + return messageHandlerError(pluginError(`Could not find message handler with the name \`${name}\`. Registered message handler names are: ${Object.keys(options.onMessage).join(', ')}.`)) + } + + const handlerType = typeof messageHandler + + if (handlerType !== 'function') { + return messageHandlerError(pluginError(`Message handlers must be functions, but the message handler for the name \`${name}\` was type \`${handlerType}\`.`)) + } + + let browser: Browser + + try { + browser = await puppeteer.connect({ + browserWSEndpoint: debuggerUrl, + defaultViewport: null, + }) + } catch (err: any) { + return messageHandlerError(err) + } + + let result: any + let error: any + + try { + result = await messageHandler(browser, ...args) + } catch (err: any) { + error = err + } finally { + await browser.disconnect() + } + + if (error) { + return messageHandlerError(error) + } + + // cy.task() errors if `undefined` is returned, so return null in that case + return result === undefined ? null : result + }, + }) +} diff --git a/npm/puppeteer/src/plugin/util.ts b/npm/puppeteer/src/plugin/util.ts new file mode 100644 index 000000000000..3a8fc7c9a831 --- /dev/null +++ b/npm/puppeteer/src/plugin/util.ts @@ -0,0 +1,5 @@ +export const pluginName = '@cypress/puppeteer' + +export function pluginError (message: string) { + return new Error(message) +} diff --git a/npm/puppeteer/src/support/index.ts b/npm/puppeteer/src/support/index.ts new file mode 100644 index 000000000000..5e8a14a63a71 --- /dev/null +++ b/npm/puppeteer/src/support/index.ts @@ -0,0 +1,14 @@ +Cypress.Commands.add('puppeteer', (name, ...args) => { + Cypress.log({ + name: 'puppeteer', + message: name, + }) + + cy.task('__cypressPuppeteer__', { name, args }, { log: false }).then((result: any) => { + if (result && result.__error__) { + throw new Error(`cy.puppeteer() failed with the following error:\n> ${result.__error__.message || result.__error__}`) + } + + return result + }) +}) diff --git a/npm/puppeteer/support/index.d.ts b/npm/puppeteer/support/index.d.ts new file mode 100644 index 000000000000..0c58bc4b1619 --- /dev/null +++ b/npm/puppeteer/support/index.d.ts @@ -0,0 +1,7 @@ +/// + +declare namespace Cypress { + interface Chainable { + puppeteer(messageName: string, ...args: any[]): Chainable + } +} diff --git a/npm/puppeteer/support/index.js b/npm/puppeteer/support/index.js new file mode 100644 index 000000000000..cdde21eaaaff --- /dev/null +++ b/npm/puppeteer/support/index.js @@ -0,0 +1 @@ +require('../dist/support') diff --git a/npm/puppeteer/test/unit/retry.spec.ts b/npm/puppeteer/test/unit/retry.spec.ts new file mode 100644 index 000000000000..50bf30832e40 --- /dev/null +++ b/npm/puppeteer/test/unit/retry.spec.ts @@ -0,0 +1,53 @@ +import { expect, use } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' + +import { retry } from '../../src/plugin' + +use(chaiAsPromised) +use(sinonChai) + +describe('#retry', () => { + it('returns result of passing 1st attempt', async () => { + const fn = sinon.stub().returns('passes 1st attempt') + const result = await retry(fn) + + expect(fn).to.be.calledOnce + expect(result).to.equal('passes 1st attempt') + }) + + it('retries after delay and returns result of subsequent passing attempt', async () => { + const fn = sinon.stub() + + fn.onFirstCall().throws('fail') + fn.onSecondCall().returns('passes 2nd attempt') + + const result = await retry(fn, { delayBetweenTries: 1 }) + + expect(fn).to.be.calledTwice + expect(result).to.equal('passes 2nd attempt') + }) + + it('retries up to timeout and returns result of subsequent passing attempt', async () => { + const fn = sinon.stub() + + fn.throws('fail') + fn.onCall(5).returns('passes 5th attempt') + + const result = await retry(fn, { delayBetweenTries: 1 }) + + expect(fn.callCount).to.equal(6) + expect(result).to.equal('passes 5th attempt') + }) + + it('fails if function does not pass before timeout', async () => { + const fn = sinon.stub().callsFake(() => { + throw new Error('fail') + }) + + await expect( + retry(fn, { timeout: 5, delayBetweenTries: 1 }), + ).to.be.rejectedWith('Failed retrying after 5ms: fail') + }) +}) diff --git a/npm/puppeteer/test/unit/setup.spec.ts b/npm/puppeteer/test/unit/setup.spec.ts new file mode 100644 index 000000000000..a2b1ee1d16d2 --- /dev/null +++ b/npm/puppeteer/test/unit/setup.spec.ts @@ -0,0 +1,280 @@ +import { expect, use } from 'chai' +import chaiAsPromised from 'chai-as-promised' +import type { PuppeteerNode } from 'puppeteer-core' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' + +import { setup } from '../../src/plugin' + +use(chaiAsPromised) +use(sinonChai) + +function getTask (on: sinon.SinonStub) { + return on.withArgs('task').lastCall.args[1].__cypressPuppeteer__ +} + +describe('#setup', () => { + it('registers `after:browser:launch` and `task` handlers', () => { + const on = sinon.stub() + + setup({ on, onMessage: {} }) + + expect(on).to.be.calledWith('after:browser:launch') + expect(on).to.be.calledWith('task') + }) + + it('errors if registering `after:browser:launch` fails', () => { + const error = new Error('Event not registered') + + error.stack = '' + const on = sinon.stub().throws(error) + + expect(() => setup({ on, onMessage: {} })).to.throw('Could not set up `after:browser:launch` task. Ensure you are running Cypress >= 13.6.0. The following error was encountered:\n\n') + }) + + describe('running message handler', () => { + it('connects puppeteer to browser', async () => { + const on = sinon.stub() + const puppeteer = { + connect: sinon.stub().resolves({ + disconnect () {}, + }), + } + + setup({ + on, + puppeteer: puppeteer as unknown as PuppeteerNode, + onMessage: { test: sinon.stub() }, + }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + + await task({ name: 'test', args: [] }) + + expect(puppeteer.connect).to.be.calledWith({ + browserWSEndpoint: 'ws://debugger', + defaultViewport: null, + }) + }) + + it('calls the specified message handler with the browser and args', async () => { + const on = sinon.stub() + const browser = { disconnect () {} } + const puppeteer = { + connect: sinon.stub().resolves(browser), + } + const handler = sinon.stub() + + setup({ + on, + puppeteer: puppeteer as unknown as PuppeteerNode, + onMessage: { test: handler }, + }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + + await task({ name: 'test', args: ['arg1', 'arg2'] }) + + expect(handler).to.be.calledWith(browser, 'arg1', 'arg2') + }) + + it('disconnects the browser once the message handler is finished', async () => { + const on = sinon.stub() + const browser = { disconnect: sinon.stub() } + const puppeteer = { + connect: sinon.stub().resolves(browser), + } + const handler = sinon.stub() + + setup({ + on, + puppeteer: puppeteer as unknown as PuppeteerNode, + onMessage: { test: handler }, + }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + + await task({ name: 'test', args: ['arg1', 'arg2'] }) + + expect(browser.disconnect).to.be.called + }) + + it('returns the result of the handler', async () => { + const on = sinon.stub() + const browser = { disconnect: sinon.stub() } + const puppeteer = { + connect: sinon.stub().resolves(browser), + } + const handler = sinon.stub().resolves('result') + + setup({ + on, + puppeteer: puppeteer as unknown as PuppeteerNode, + onMessage: { test: handler }, + }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + const returnValue = await task({ name: 'test', args: ['arg1', 'arg2'] }) + + expect(returnValue).to.equal('result') + }) + + it('returns null if message handler returns undefined', async () => { + const on = sinon.stub() + const browser = { disconnect: sinon.stub() } + const puppeteer = { + connect: sinon.stub().resolves(browser), + } + const handler = sinon.stub().resolves(undefined) + + setup({ + on, + puppeteer: puppeteer as unknown as PuppeteerNode, + onMessage: { test: handler }, + }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + const returnValue = await task({ name: 'test', args: ['arg1', 'arg2'] }) + + expect(returnValue).to.be.null + }) + + it('returns error object if debugger URL reference is lost', async () => { + const on = sinon.stub() + + setup({ on, onMessage: { + exists1: () => {}, + exists2: () => {}, + } }) + + const task = getTask(on) + const returnValue = await task({ name: 'nonexistent', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Lost the reference to the browser. This usually occurs because the Cypress config was reloaded without the browser re-launching. Close and re-open the browser.', + ) + }) + + it('returns error object if browser is not supported', async () => { + const on = sinon.stub() + + setup({ on, onMessage: { + exists1: () => {}, + exists2: () => {}, + } }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'Firefox' }, {}) + + const task = getTask(on) + const returnValue = await task({ name: 'nonexistent', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Only browsers in the "Chromium" family are supported. You are currently running a browser with the family: Firefox', + ) + }) + + it('disconnects browser and returns error object if message handler errors', async () => { + const on = sinon.stub() + const browser = { disconnect: sinon.stub() } + const puppeteer = { + connect: sinon.stub().resolves(browser), + } + const handler = sinon.stub().rejects(new Error('handler error')) + + setup({ + on, + puppeteer: puppeteer as unknown as PuppeteerNode, + onMessage: { test: handler }, + }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + const returnValue = await task({ name: 'test', args: ['arg1', 'arg2'] }) + + expect(browser.disconnect).to.be.called + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal('handler error') + }) + + it('returns error object if message handler with given name cannot be found', async () => { + const on = sinon.stub() + + setup({ on, onMessage: { + exists1: () => {}, + exists2: () => {}, + } }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + const returnValue = await task({ name: 'nonexistent', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Could not find message handler with the name `nonexistent`. Registered message handler names are: exists1, exists2.', + ) + }) + + it('returns error object if message handler with given name cannot be found', async () => { + const on = sinon.stub() + + // @ts-expect-error + setup({ on, onMessage: { notAFunction: true } }) + + on.withArgs('after:browser:launch').lastCall.args[1]({ family: 'chromium' }, { webSocketDebuggerUrl: 'ws://debugger' }) + + const task = getTask(on) + const returnValue = await task({ name: 'notAFunction', args: [] }) + + expect(returnValue.__error__).to.be.an('object') + expect(returnValue.__error__.message).to.equal( + 'Message handlers must be functions, but the message handler for the name `notAFunction` was type `boolean`.', + ) + }) + }) + + describe('validation', () => { + it('errors if options argument is not provided', () => { + // @ts-expect-error + expect(() => setup()).to.throw('Must provide options argument to `setup`.') + }) + + it('errors if options argument is not an object', () => { + // @ts-expect-error + expect(() => setup(true)).to.throw('The options argument provided to `setup` must be an object.') + }) + + it('errors if `on` option is not provided', () => { + // @ts-expect-error + expect(() => setup({})).to.throw('Must provide `on` function to `setup`.') + }) + + it('errors if `on` option is not a function', () => { + // @ts-expect-error + expect(() => setup({ on: 'string' })).to.throw('The `on` option provided to `setup` must be a function.') + }) + + it('errors if `onMessage` option is not provided', () => { + // @ts-expect-error + expect(() => setup({ on: sinon.stub() })).to.throw('Must provide `onMessage` object to `setup`.') + }) + + it('errors if `onMessage` option is not an object', () => { + // @ts-expect-error + expect(() => setup({ on: sinon.stub(), onMessage: () => {} })).to.throw('The `onMessage` option provided to `setup` must be an object.') + }) + }) +}) diff --git a/npm/puppeteer/tsconfig.json b/npm/puppeteer/tsconfig.json new file mode 100644 index 000000000000..fc6b5493beba --- /dev/null +++ b/npm/puppeteer/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noImplicitAny": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "./dist", + "skipLibCheck": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "target": "ES2020", + "types": [ + "cypress", + "./support" + ] + }, + "include": [ + "src/" + ] +} diff --git a/npm/webpack-dev-server/CHANGELOG.md b/npm/webpack-dev-server/CHANGELOG.md index 0d88ad046b6d..cdb0dca206bb 100644 --- a/npm/webpack-dev-server/CHANGELOG.md +++ b/npm/webpack-dev-server/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/webpack-dev-server-v3.7.1](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.7.0...@cypress/webpack-dev-server-v3.7.1) (2023-11-22) + + +### Bug Fixes + +* Allow absolute paths in indexHtmlFile ([#28324](https://github.com/cypress-io/cypress/issues/28324)) ([dcd6a18](https://github.com/cypress-io/cypress/commit/dcd6a18d9b43707a240101696e7b718f97380149)) + # [@cypress/webpack-dev-server-v3.7.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.6.1...@cypress/webpack-dev-server-v3.7.0) (2023-11-07) diff --git a/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts b/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts index 28726ece76c3..bbed2620e112 100644 --- a/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts +++ b/npm/webpack-dev-server/src/makeDefaultWebpackConfig.ts @@ -81,7 +81,7 @@ export function makeCypressWebpackConfig ( }, plugins: [ new (HtmlWebpackPlugin as typeof import('html-webpack-plugin-5'))({ - template: indexHtmlFile ? path.join(projectRoot, indexHtmlFile) : undefined, + template: indexHtmlFile ? path.isAbsolute(indexHtmlFile) ? indexHtmlFile : path.join(projectRoot, indexHtmlFile) : undefined, // Angular generates all of it's scripts with