diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index fa5c04eb9f65..53e70de7e7d3 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -29,6 +29,8 @@ mainBuildFilters: &mainBuildFilters - develop - /^release\/\d+\.\d+\.\d+$/ - 'feature/ct-public-api' + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - 'update-v8-snapshot-cache-on-develop' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -38,6 +40,8 @@ macWorkflowFilters: &darwin-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ 'feature/ct-public-api', << pipeline.git.branch >> ] + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -47,6 +51,8 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ 'feature/ct-public-api', << pipeline.git.branch >> ] + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -64,7 +70,8 @@ windowsWorkflowFilters: &windows-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'mschile/chrome_memory_fix', << pipeline.git.branch >> ] + # use the following branch as well to ensure that v8 snapshot cache updates are fully tested + - equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -201,7 +208,8 @@ commands: name: Generate v8 snapshot command: | source ./scripts/ensure-node.sh - yarn build-v8-snapshot-prod + # Minification takes some time. We only really need to do that for the binary (and we regenerate snapshots separately there) + V8_SNAPSHOT_DISABLE_MINIFY=1 yarn build-v8-snapshot-prod - prepare-modules-cache # So we don't throw these in the workspace cache - persist_to_workspace: root: ~/ @@ -1443,6 +1451,8 @@ jobs: - update_known_hosts - run: yarn test-npm-package-release-script - run: node ./scripts/semantic-commits/validate-binary-changelog.js + - store_artifacts: + path: /tmp/releaseData lint-types: <<: *defaults diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 99a767e520c4..ff1b1ac51f32 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,6 +32,5 @@ DO NOT DELETE the PR checklist. --> - [ ] Have tests been added/updated? -- [ ] Has the original issue (or this PR, if no issue exists) been tagged with a release in ZenHub? (user-facing changes only) - [ ] Has a PR for user-facing changes been opened in [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation)? - [ ] Have API changes been updated in the [`type definitions`](https://github.com/cypress-io/cypress/blob/develop/cli/types/cypress.d.ts)? diff --git a/.github/workflows/update-browser-versions.yml b/.github/workflows/update-browser-versions.yml index 974281fc06e2..60ecdbe71099 100644 --- a/.github/workflows/update-browser-versions.yml +++ b/.github/workflows/update-browser-versions.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} - name: Set committer info ## attribute the commit to cypress-bot: https://github.community/t/logging-into-git-as-a-github-app/115916 run: | diff --git a/.github/workflows/update_v8_snapshot_cache.yml b/.github/workflows/update_v8_snapshot_cache.yml index 9b05036dec79..345b718fb22c 100644 --- a/.github/workflows/update_v8_snapshot_cache.yml +++ b/.github/workflows/update_v8_snapshot_cache.yml @@ -1,12 +1,12 @@ name: Update V8 Snapshot Cache on: schedule: + # Run everyday except Wednesday at 00:00 UTC + - cron: '0 0 * * 0,1,2,4,5,6' # Run every Wednesday at 00:00 UTC - cron: '0 0 * * 3' push: branches: - - ryanm/feature/v8-snapshots-auto-pr - - develop - 'release/**' workflow_dispatch: inputs: @@ -35,7 +35,8 @@ jobs: env: CYPRESS_BOT_APP_ID: ${{ secrets.RAM_APP }} BASE_BRANCH: ${{ inputs.branch || github.ref_name }} - GENERATE_FROM_SCRATCH: ${{ inputs.generate_from_scratch == true || github.event_name == 'schedule' }} + # Flex the generate from scratch option based on manual input or if we are on the weekly schedule + GENERATE_FROM_SCRATCH: ${{ inputs.generate_from_scratch == true || (github.event_name == 'schedule' && github.event.schedule == '0 0 * * 3') }} steps: - name: Determine snapshot files - Windows if: ${{ matrix.platform == 'windows-latest' }} @@ -51,7 +52,7 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.BOT_GITHUB_ACTION_TOKEN }} ref: ${{ env.BASE_BRANCH }} - name: Set committer info ## attribute the commit to cypress-bot: https://github.community/t/logging-into-git-as-a-github-app/115916 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2be57763c274..9bdad1a4a574 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -476,8 +476,6 @@ We do not continuously deploy the Cypress binary, so `develop` contains all of t - `test` - Adding missing or correcting existing tests - For user-facing changes that will be released with the next Cypress version, be sure to add a changelog entry to the appropriate section in [`cli/CHANGELOG.md`](./cli/CHANGELOG.md). See [Writing the Cypress Changelog Guide](./guides/writing-the-cypress-changelog.md) for more details. - Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PRs will not be reviewed if this template is not filled in. -- If the PR is a user facing change and you're a Cypress team member that has logged into [ZenHub](https://www.zenhub.com/) and downloaded the [ZenHub for GitHub extension](https://www.zenhub.com/extension), set the release the PR is intended to ship in from the sidebar of the PR. Follow semantic versioning to select the intended release. This is used to generate the changelog for the release. If you don't tag a PR for release, it won't be mentioned in the changelog. - ![Select release for PR](https://user-images.githubusercontent.com/1271364/135139641-657015d6-2dca-42d4-a4fb-16478f61d63f.png) - Please check the "Allow edits from maintainers" checkbox when submitting your PR. This will make it easier for the maintainers to make minor adjustments, to help with tests or any other changes we may need. ![Allow edits from maintainers checkbox](https://user-images.githubusercontent.com/1271181/31393427-b3105d44-ada9-11e7-80f2-0dac51e3919e.png) - All Pull Requests require a minimum of **two** approvals. @@ -561,10 +559,6 @@ Below are guidelines to help during code review. If any of the following require - [ ] There is no irrelevant code to the issue being addressed. If there is, ask the contributor to break the work out into a separate PR. - [ ] Tests are testing the code's intended functionality in the best way possible. -#### Internal - -- [ ] The original issue has been tagged with a release in ZenHub. - ### Code Review of Dependency Updates Below are some guidelines Cypress uses when reviewing dependency updates. @@ -579,7 +573,6 @@ Below are some guidelines Cypress uses when reviewing dependency updates. - [ ] Code using the dependency has been updated to accommodate any breaking changes - [ ] The dependency still supports the version of Node that the package requires. -- [ ] The PR been tagged with a release in ZenHub. - [ ] Appropriate labels have been added to the PR (for example: label `type: breaking change` if it is a breaking change) ## Releases diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 899a0ead4532..4c2141b646f7 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,18 +1,22 @@ - + ## 12.6.0 _Released 02/14/2023 (PENDING)_ +**Features:** + +- It is now possible to overwrite query commands using [`Cypress.Commands.overwriteQuery`](https://on.cypress.io/api/custom-queries). Addressed in [#25674](https://github.com/cypress-io/cypress/pull/25674). +- Added the "Open in IDE" feature for failed tests reported from the Debug page. Addressed in [#25691](https://github.com/cypress-io/cypress/pull/25691). +- Added a new CLI flag, called [`--auto-cancel-after-failures`](https://docs.cypress.io/guides/guides/command-line#Options), that overrides the project-level CI ["Auto Cancellation"](https://docs.cypress.io/guides/cloud/smart-orchestration#Auto-Cancellation) value when recording to the Cloud. This gives Cloud users on Business and Enterprise plans the flexibility to alter the auto-cancellation value per run. Addressed in [#25237](https://github.com/cypress-io/cypress/pull/25237). +- Added `Cypress.require()` for including dependencies within the `cy.origin()` callback. Removed support for `require()` and `import()` within the callback. Addresses [#24976](https://github.com/cypress-io/cypress/issues/24976). + **Bugfixes:** - Fixed an issue with the Cloud project selection modal not showing the correct prompts. Fixes [#25520](https://github.com/cypress-io/cypress/issues/25520). - Fixed an issue in middleware where error-handling code could itself generate an error and fail to report the original issue. Fixes [#22825](https://github.com/cypress-io/cypress/issues/22825). - Fixed an issue that could cause the Debug page to display a different number of specs for in-progress runs than shown in Cypress Cloud. Fixes [#25647](https://github.com/cypress-io/cypress/issues/25647). - -**Features:** - -- Added the "Open in IDE" feature for failed tests reported from the Debug page. Addressed in [#25691](https://github.com/cypress-io/cypress/pull/25691). -- Added a new CLI flag, called [`--auto-cancel-after-failures`](https://docs.cypress.io/guides/guides/command-line#Options), that overrides the project-level CI ["Auto Cancellation"](https://docs.cypress.io/guides/cloud/smart-orchestration#Auto-Cancellation) value when recording to the Cloud. This gives Cloud users on Business and Enterprise plans the flexibility to alter the auto-cancellation value per run. Addressed in [#25237](https://github.com/cypress-io/cypress/pull/25237). +- Fixed an issue introduced in Cypress 12.3.0 where custom browsers that relied on process environment variables were not found on macOS arm64 architectures. Fixed in [#25753](https://github.com/cypress-io/cypress/pull/25753). +- Fixed an issue where Cypress would fail to load any specs if the project `specPattern` included a resource that could not be accessed due to filesystem permissions. Fixes [#24109](https://github.com/cypress-io/cypress/issues/24109). **Misc:** @@ -29,7 +33,7 @@ _Released 02/14/2023 (PENDING)_ ## 12.5.1 -_Released 02/10/2023_ +_Released 02/02/2023_ **Bugfixes:** diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 52dd86aa9892..5906955e0a14 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -53,6 +53,9 @@ declare namespace Cypress { interface QueryFn { (this: Command, ...args: Parameters): (subject: any) => any } + interface QueryFnWithOriginalFn { + (this: Command, originalFn: QueryFn, ...args: Parameters): (subject: any) => any + } interface ObjectLike { [key: string]: any } @@ -648,6 +651,12 @@ declare namespace Cypress { * @see https://on.cypress.io/api/custom-queries */ addQuery(name: T, fn: QueryFn): void + + /** + * Overwrite an existing Cypress query with a new implementation + * @see https://on.cypress.io/api/custom-queries + */ + overwriteQuery(name: T, fn: QueryFnWithOriginalFn): void } /** @@ -786,6 +795,12 @@ declare namespace Cypress { */ off: Actions + /** + * Used to include dependencies within the cy.origin() callback + * @see https://on.cypress.io/origin + */ + require: (id: string) => T + /** * Trigger action * @private @@ -793,7 +808,7 @@ declare namespace Cypress { action: (action: string, ...args: any[]) => any[] | void /** - * Load files + * Load files * @private */ onSpecWindow: (window: Window, specList: string[] | Array<() => Promise>) => void @@ -3126,7 +3141,7 @@ declare namespace Cypress { */ experimentalRunAllSpecs?: boolean /** - * Enables support for require/import within cy.origin. + * Enables support for `Cypress.require()` for including dependencies within the `cy.origin()` callback. * @default false */ experimentalOriginDependencies?: boolean diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index ec6bb1b9facc..26010d29df3a 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -1164,3 +1164,20 @@ namespace CypressTraversalTests { cy.wrap({}).parentsUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> cy.wrap({}).parentsUntil('#myItem', 'a', { log: 'true' }) // $ExpectError } + +namespace CypressRequireTests { + Cypress.require('lodash') + + const anydep = Cypress.require('anydep') + anydep // $ExpectType any + + const sinon = Cypress.require('sinon') as typeof import('sinon') + sinon // $ExpectType SinonStatic + + const lodash = Cypress.require<_.LoDashStatic>('lodash') + lodash // $ExpectType LoDashStatic + + Cypress.require() // $ExpectError + Cypress.require({}) // $ExpectError + Cypress.require(123) // $ExpectError +} diff --git a/guides/code-signing.md b/guides/code-signing.md index db166d937cf0..75cb40b39a92 100644 --- a/guides/code-signing.md +++ b/guides/code-signing.md @@ -4,20 +4,37 @@ Code signing is done for the Windows and Mac distributions of Cypress when they `electron-builder` handles code signing during the `create-build-artifacts` jobs. This guide assumes that the reader is already familiar with [`electron-builder`'s Code Signing documentation](https://www.electron.build/code-signing). -## Installing a new Mac code signing key +## Rotating the Mac code signing key -Follow the directions supplied by `electron-builder`: https://www.electron.build/code-signing#travis-appveyor-and-other-ci-servers +1. On a Mac, log in to Xcode using Cypress's Apple developer program identity. +2. Follow Apple's [Create, export, and delete signing certificates](https://help.apple.com/xcode/mac/current/#/dev154b28f09) instructions: + 1. Follow "View signing certificates". + 2. Follow "Create a signing certificate", and choose the type of "Developer ID Application" when prompted. + 3. Follow "Export a signing certificate". Set a strong passphrase when prompted, which will later become `CSC_KEY_PASSWORD`. +3. Upload the exported, encrypted `.p12` file to the [Code Signing folder][code-signing-folder] in Google Drive and obtain a public [direct download link][direct-download]. +4. Within the `test-runner:sign-mac-binary` CircleCI context, set `CSC_LINK` to that direct download URL and set `CSC_KEY_PASSWORD` to the passphrase used to encrypt the `p12` file. -Set the environment variables `CSC_LINK` and `CSC_KEY_PASSWORD` in the `test-runner:sign-mac-binary` CircleCI context. +## Rotating the Windows code signing key -## Installing a new Windows code signing key - -1. Obtain the private key and full certificate chain in ASCII-armored PEM format and store each in a file (`-----BEGIN PRIVATE KEY-----`, `-----BEGIN CERTIFICATE-----`) -2. Using `openssl`, convert the plaintext PEM public and private key to binary PKCS#12/PFX format and encrypt it with a real strong password. +1. Generate a certificate signing request (CSR) file using `openssl`. For example: + ```shell + # generate a new private key + openssl genrsa -out win-code-signing.key 4096 + # create a CSR using the private key + openssl req -new -key win-code-signing.key -out win-code-signing.csr + ``` +2. Obtain a certificate by submitting the CSR to SSL.com using the Cypress SSL.com account. + * If renewing, follow the [renewal instructions](https://www.ssl.com/how-to/renewing-ev-ov-and-iv-certificates/). + * If rotating, contact SSL.com's support to request certificate re-issuance. +3. Obtain the full certificate chain from SSL.com's dashboard in ASCII-armored PEM format and save it as `win-code-signing.crt`. (`-----BEGIN PRIVATE KEY-----`, `-----BEGIN CERTIFICATE-----`) +4. Using `openssl`, convert the plaintext PEM public and private key to binary PKCS#12/PFX format and encrypt it with a strong passphrase, which will later become `CSC_KEY_PASSWORD`. ```shell - ➜ openssl pkcs12 -export -inkey key.pem -in cert.pem -out encrypted.pfx + ➜ openssl pkcs12 -export -inkey win-code-signing.key -in win-code-signing.crt -out encrypted-win-code-signing.pfx Enter Export Password: Verifying - Enter Export Password: ``` -3. Upload the `encrypted.pfx` file to the Cypress App Google Drive and obtain a [direct download link](http://www.syncwithtech.org/p/direct-download-link-generator.html). -4. Within the `test-runner:sign-windows-binary` CircleCI context, set `CSC_LINK` to that URL and `CSC_KEY_PASSWORD` to the password. \ No newline at end of file +5. Upload the `encrypted-win-code-signing.pfx` file to the [Code Signing folder][code-signing-folder] in Google Drive and obtain a public [direct download link][direct-download]. +6. Within the `test-runner:sign-windows-binary` CircleCI context, set `CSC_LINK` to that direct download URL and set `CSC_KEY_PASSWORD` to the passphrase used to encrypt the `pfx` file. + +[direct-download]: https://www.syncwithtech.org/p/direct-download-link-generator.html +[code-signing-folder]: https://drive.google.com/drive/u/1/folders/1CsuoXRDmXvd3ImvFI-sChniAMJBASUW diff --git a/guides/release-process.md b/guides/release-process.md index 4b399977a717..20d05e3c7406 100644 --- a/guides/release-process.md +++ b/guides/release-process.md @@ -13,7 +13,6 @@ The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) di - Ensure you have the following permissions set up: - An AWS account with permission to access and write to the AWS S3, i.e. the Cypress CDN. - Permissions for your npm account to publish the `cypress` package. - - Permissions to update releases in ZenHub. - [Set up](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress) an AWS SSO profile with the [Team-CypressApp-Prod](https://cypress-io.atlassian.net/wiki/spaces/INFRA/pages/1534853121/AWS+SSO+Cypress#Team-CypressApp-Prod) role. The release scripts assumes the name of your profile is `prod`. Make sure to open the "App Developer" expando for some necessary config values. Your AWS config file should end up looking like the following: @@ -27,19 +26,17 @@ The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) di ``` - Set up the following environment variables: - - For the `release-automations` steps, you will need setup the following envs: + - For the `release-automations` step, you will need setup the following envs: - GitHub token - Found in 1Password. - - [ZenHub API token](https://app.zenhub.com/dashboard/tokens) to interact with Zenhub. Found in 1Password. - The `cypress-bot` GitHub app credentials. Found in 1Password. ```text GITHUB_TOKEN="..." - ZENHUB_API_TOKEN="..." GITHUB_APP_CYPRESS_INSTALLATION_ID= GITHUB_APP_ID= GITHUB_PRIVATE_KEY= ``` - - For purging the Cloudflare cache (part of the `move-binaries` step), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. + - For purging the Cloudflare cache (needed for the `prepare-release-artifacts` script in step 6), you'll need `CF_ZONEID` and `CF_TOKEN` set. These can be found in 1Password. ```text CF_ZONEID="..." CF_TOKEN="..." @@ -78,13 +75,14 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy - [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app) uses yarn and represents a typical consumer implementation. - Optionally, do more thorough tests, for example test the new version of Cypress against the Cypress Cloud repo. -2. Confirm that every issue labeled [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) has a ZenHub release set. **Tip:** there is a command in [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to list and check such issues. Without a ZenHub release issues will not be included in the right changelog. Also ensure that every closed issue in any obsolete releases are moved to the appropriate release in ZehHub. For example, if the open releases are 9.5.5 and 9.6.0, the current release is 9.6.0, then all closed issues marked as 9.5.5 should be moved to 9.6.0. Ensure that there are no commits on `develop` since the last release that are user facing and aren't marked with the current release. +2. Ensure all changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on) have been merged to `develop` and deployed. 3. Create a Release PR Bump, submit, get approvals on, and merge a new PR. This PR Should: - - Bump the Cypress `version` in [`package.json`](package.json) - - Bump the [`packages/example`](../packages/example) dependency if there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version - - Follow the writing the [Cypress Changelog release steps](./writing-the-cypress-changelog.md#release) to update the [`cli/CHANGELOG.md`](../cli/CHANGELOG.md). -4. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/develop-/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-/cypress.tgz`, publishing can proceed. + - Bump the Cypress `version` in [`package.json`](package.json) + - Bump the [`packages/example`](../packages/example) dependency if there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version + - Follow the writing the [Cypress Changelog release steps](./writing-the-cypress-changelog.md#release) to update the [`cli/CHANGELOG.md`](../cli/CHANGELOG.md). + +4. Once the `develop` branch is passing in CI and you have confirmed the `cypress-bot` has commented on the commit with the pre-release versions for `darwin-x64`, `darwin-arm64`, `linux-x64`,`linux-arm64`, and `win32-x64`, publishing can proceed. 5. Log into AWS SSO with `aws sso login --profile `. If you have setup your credentials under a different profile than `prod`, be sure to set the `AWS_PROFILE` environment variable to that profile name for the remaining steps. For example, if you are using `production` instead of `prod`, do `export AWS_PROFILE=production`. @@ -142,36 +140,29 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy yarn binary-release --version X.Y.Z ``` -15. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on). - -16. Merge the documentation PR from step 11 and the new docker image PR created in step 12 to release the image. - -17. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md). +15. Merge the documentation PR from step 11 and the new docker image PR created in step 12 to release the image. -18. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release): - - Close the current release in ZenHub. - - Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date. - - Move all issues that are still open from the current release to the appropriate future release. +16. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](../packages/example/README.md). -19. Once the release is complete, create a Github tag off of the release commit which bumped the version: +17. Once the release is complete, create a Github tag off of the release commit which bumped the version: ```shell git checkout develop git pull origin develop git log --pretty=oneline - # copy sha of the previous commit + # copy sha of the version bump commit git tag -a vX.Y.Z -m vX.Y.Z git push origin vX.Y.Z ``` -20. Create a new [GitHub release](https://github.com/cypress-io/cypress/releases). Choose the tag you created previously and add contents to match previous releases. +18. Create a new [GitHub release](https://github.com/cypress-io/cypress/releases). Choose the tag you created previously and add contents to match previous releases. -21. Inside of [cypress-io/release-automations][release-automations], run the following to add a comment to each GH issue that has been resolved with the new published version: +19. Add a comment to each GH issue that has been resolved with the new published version. Download the `releaseData.json` artifact from the `verify-release-readiness` CircleCI job and run the following command inside of [cypress-io/release-automations][release-automations]: ```shell - cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z + cd packages/issues-in-release && npm run do:comment -- --release-data ``` -22. Confirm there are no issues with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left +22. Confirm there are no issues from the release with the label [stage: pending release](https://github.com/cypress-io/cypress/issues?q=label%3A%22stage%3A+pending+release%22+is%3Aclosed) left. 23. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z` for testing the features or fixes from the newly published version `x.y.z`, update that branch to refer to the newly published NPM version in `package.json`. Then, get the changes approved and merged into that project's main branch. For projects without a `x.y.z` branch, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects: - [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99) diff --git a/npm/webpack-batteries-included-preprocessor/index.js b/npm/webpack-batteries-included-preprocessor/index.js index 7b28ecb09348..897a5dbecbd6 100644 --- a/npm/webpack-batteries-included-preprocessor/index.js +++ b/npm/webpack-batteries-included-preprocessor/index.js @@ -180,6 +180,16 @@ preprocessor.defaultOptions = { watchOptions: {}, } +preprocessor.getFullWebpackOptions = (filePath, typescript) => { + const options = { typescript } + + options.webpackOptions = getDefaultWebpackOptions() + + addTypeScriptConfig({ filePath }, options) + + return options.webpackOptions +} + // for testing purposes, but do not add this to the typescript interface preprocessor.__reset = webpackPreprocessor.__reset diff --git a/npm/webpack-preprocessor/index.ts b/npm/webpack-preprocessor/index.ts index c61f9bf12291..b6662dd9e2f2 100644 --- a/npm/webpack-preprocessor/index.ts +++ b/npm/webpack-preprocessor/index.ts @@ -5,21 +5,11 @@ import * as events from 'events' import * as path from 'path' import webpack from 'webpack' import utils from './lib/utils' -import { crossOriginCallbackStore } from './lib/cross-origin-callback-store' import { overrideSourceMaps } from './lib/typescript-overrides' -import { compileCrossOriginCallbackFiles } from './lib/cross-origin-callback-compile' const debug = Debug('cypress:webpack') const debugStats = Debug('cypress:webpack:stats') -declare global { - // this indicates which commands should be acted upon by the - // cross-origin-callback-loader. its absence means the loader - // should not be utilized at all - // eslint-disable-next-line no-var - var __cypressCallbackReplacementCommands: string[] | undefined -} - type FilePath = string interface BundleObject { promise: Bluebird @@ -163,8 +153,6 @@ interface WebpackPreprocessor extends WebpackPreprocessorFn { const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): FilePreprocessor => { debug('user options: %o', options) - let crossOriginCallbackLoaderAdded = false - // we return function that accepts the arguments provided by // the event 'file:preprocessor' // @@ -241,25 +229,6 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F }) .value() as any - const callbackReplacementCommands = global.__cypressCallbackReplacementCommands - - if (!crossOriginCallbackLoaderAdded && !!callbackReplacementCommands) { - // webpack runs loaders last-to-first and we want ours to run last - // so that it's working with plain javascript - webpackOptions.module.rules.unshift({ - test: /\.(js|ts|jsx|tsx)$/, - exclude: /node_modules/, - use: [{ - loader: require.resolve('@cypress/webpack-preprocessor/dist/lib/cross-origin-callback-loader.js'), - options: { - commands: callbackReplacementCommands, - }, - }], - }) - - crossOriginCallbackLoaderAdded = true - } - debug('webpackOptions: %o', webpackOptions) debug('watchOptions: %o', watchOptions) if (options.typescript) debug('typescript: %s', options.typescript) @@ -327,62 +296,12 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F } debug('finished bundling', outputPath) + if (debugStats.enabled) { /* eslint-disable-next-line no-console */ console.error(stats.toString({ colors: true })) } - const resolveAllBundles = () => { - bundles[filePath].deferreds.forEach((deferred) => { - // resolve with the outputPath so Cypress knows where to serve - // the file from - deferred.resolve(outputPath) - }) - - bundles[filePath].deferreds.length = 0 - } - - // the cross-origin-callback-loader extracts any cross-origin callback - // functions that require dependencies and stores their sources - // in the CrossOriginCallbackStore. it saves the callbacks per source - // files, since that's the context it has. here we need to unfurl - // what dependencies the input source file has so we can know which - // files stored in the CrossOriginCallbackStore to compile - const handleCrossOriginCallbackFiles = () => { - // get the source file and any of its dependencies - const sourceFiles = jsonStats.modules - .filter((module) => { - // entries have duplicate modules whose ids are numbers - return _.isString(module.id) - }) - .map((module) => { - // module id is the path relative to the cwd, - // e.g. ./cypress/support/e2e.js, but we need it absolute - return path.join(process.cwd(), module.id as string) - }) - - if (!crossOriginCallbackStore.hasFilesFor(sourceFiles)) { - debug('no cross-origin callback files') - - return resolveAllBundles() - } - - compileCrossOriginCallbackFiles(crossOriginCallbackStore.getFilesFor(sourceFiles), { - originalFilePath: filePath, - webpackOptions, - }) - .then(() => { - debug('resolve all after handling cross-origin callback files') - resolveAllBundles() - }) - .catch((err) => { - rejectWithErr(err) - }) - .finally(() => { - crossOriginCallbackStore.reset(filePath) - }) - } - // seems to be a race condition where changing file before next tick // does not cause build to rerun Bluebird.delay(0).then(() => { @@ -390,11 +309,13 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F return } - if (!callbackReplacementCommands) { - return resolveAllBundles() - } + bundles[filePath].deferreds.forEach((deferred) => { + // resolve with the outputPath so Cypress knows where to serve + // the file from + deferred.resolve(outputPath) + }) - handleCrossOriginCallbackFiles() + bundles[filePath].deferreds.length = 0 }) } @@ -454,17 +375,6 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F bundler.close(cb) } } - - // clean up temp dir where cross-origin callback files are output - const tmpdir = utils.tmpdir(utils.hash(filePath)) - - debug('remove temp directory:', tmpdir) - - utils.rmdir(tmpdir).catch((err) => { - // not the end of the world if removing the tmpdir fails, but we - // don't want it to crash the whole process by going uncaught - debug('failed removing temp directory: %s', err.stack) - }) }) // return the promise, which will resolve with the outputPath or reject diff --git a/npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts b/npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts deleted file mode 100644 index d66606c5ca5b..000000000000 --- a/npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts +++ /dev/null @@ -1,104 +0,0 @@ -import _ from 'lodash' -import Debug from 'debug' -import * as path from 'path' -import webpack from 'webpack' -import { CrossOriginCallbackStoreFile } from './cross-origin-callback-store' - -const VirtualModulesPlugin = require('webpack-virtual-modules') - -const debug = Debug('cypress:webpack') - -interface Entry { - [key: string]: string -} - -interface VirtualConfig { - [key: string]: string -} - -interface EntryConfig { - entry: Entry - virtualConfig: VirtualConfig -} - -// takes the files stored by the cross-origin-callback-loader and turns -// them into config we can pass to webpack to compile all the files. the -// virtual config allows us to just use the source we have in memory without -// needing to write it to file -const getConfig = ({ files, originalFilePath }): EntryConfig => { - const dir = path.dirname(originalFilePath) - - return files.reduce((memo, file) => { - const { inputFileName, source } = file - const inputPath = path.join(dir, inputFileName) - - memo.entry[inputFileName] = inputPath - memo.virtualConfig[inputPath] = source - - return memo - }, { entry: {}, virtualConfig: {} }) -} - -interface ConfigProperties { - webpackOptions: webpack.Configuration - entry: Entry - virtualConfig: VirtualConfig - outputDir: string -} - -const getWebpackOptions = ({ webpackOptions, entry, virtualConfig, outputDir }: ConfigProperties): webpack.Configuration => { - const modifiedWebpackOptions = _.extend({}, webpackOptions, { - entry, - output: { - path: outputDir, - }, - }) - const plugins = modifiedWebpackOptions.plugins || [] - - modifiedWebpackOptions.plugins = plugins.concat( - new VirtualModulesPlugin(virtualConfig), - ) - - return modifiedWebpackOptions -} - -interface CompileOptions { - originalFilePath: string - webpackOptions: webpack.Configuration -} - -// the cross-origin-callback-loader extracts any cy.origin() callback functions -// that includes dependencies and stores their sources in the -// CrossOriginCallbackStore. this sends those sources through webpack again -// to process any dependencies and create bundles for each callback function -export const compileCrossOriginCallbackFiles = (files: CrossOriginCallbackStoreFile[], options: CompileOptions): Promise => { - debug('compile cross-origin callback files: %o', files) - - const { originalFilePath, webpackOptions } = options - const outputDir = path.dirname(files[0].outputFilePath) - const { entry, virtualConfig } = getConfig({ files, originalFilePath }) - const modifiedWebpackOptions = getWebpackOptions({ - webpackOptions, - entry, - virtualConfig, - outputDir, - }) - - return new Promise((resolve, reject) => { - const compiler = webpack(modifiedWebpackOptions) - - const handle = (err: Error) => { - if (err) { - debug('errored compiling cross-origin callback files with: %s', err.stack) - - return reject(err) - } - - debug('successfully compiled cross-origin callback files') - - resolve() - } - - compiler.run(handle) - }) -} diff --git a/npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts b/npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts deleted file mode 100644 index 91d196b2a6ee..000000000000 --- a/npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts +++ /dev/null @@ -1,180 +0,0 @@ -import _ from 'lodash' -import { parse } from '@babel/parser' -import { default as traverse } from '@babel/traverse' -import { default as generate } from '@babel/generator' -import { NodePath, types as t } from '@babel/core' -import * as loaderUtils from 'loader-utils' -import * as pathUtil from 'path' -import Debug from 'debug' - -import mergeSourceMaps from './merge-source-map' -import { crossOriginCallbackStore } from './cross-origin-callback-store' -import utils from './utils' - -const debug = Debug('cypress:webpack') - -// this loader makes supporting dependencies within cross-origin callbacks -// possible. if there are no dependencies (e.g. no requires/imports), it's a -// noop. otherwise: it does this by doing the following: -// - extracts the callbacks -// - the callbacks are kept in memory and then run back through webpack -// once the initial file compilation is complete -// - replaces the callbacks with objects -// - this object references the file the callback will be output to by -// its own compilation. this allows the runtime to get the file and -// run it in its origin's context. -export default function (source: string, map, meta, store = crossOriginCallbackStore) { - const { resourcePath } = this - const options = typeof this.getOptions === 'function' - ? this.getOptions() // webpack 5 - : loaderUtils.getOptions(this) // webpack 4 - const commands = (options.commands || []) as string[] - - let ast: t.File - - try { - // purposefully lenient in allowing syntax since the user can't configure - // this, but probably has their own webpack or target configured to - // handle it - ast = parse(source, { - allowImportExportEverywhere: true, - allowAwaitOutsideFunction: true, - allowSuperOutsideMethod: true, - allowUndeclaredExports: true, - sourceType: 'unambiguous', - sourceFilename: resourcePath, - }) - } catch (err) { - // it's unlikely there will be a parsing error, since that should have - // already been caught by a previous loader, but if there is and it isn't - // possible to get the AST, there's nothing we can do, so just callback - // with the original source - debug('parsing error for file (%s): %s', resourcePath, err.stack) - - this.callback(null, source, map) - - return - } - - let hasDependencies = false - - traverse(ast, { - CallExpression (path) { - const callee = path.get('callee') as NodePath - - if (!callee.isMemberExpression()) return - - // bail if we're not inside a supported command - if (!commands.includes((callee.node.property as t.Identifier).name)) { - return - } - - const lastArg = _.last(path.get('arguments')) - - // the user could try an invalid signature where the last argument is - // not a function. in this case, we'll return the unmodified code and - // it will be a runtime validation error - if ( - !lastArg || ( - !lastArg.isArrowFunctionExpression() - && !lastArg.isFunctionExpression() - ) - ) { - return - } - - // determine if there are any requires/imports within the callback - lastArg.traverse({ - CallExpression (path) { - if ( - // e.g. const dep = require('../path/to/dep') - // @ts-ignore - path.node.callee.name === 'require' - // e.g. const dep = await import('../path/to/dep') - || path.node.callee.type as string === 'Import' - ) { - hasDependencies = true - } - }, - }, this) - - if (!hasDependencies) return - - // generate the extracted callback function from an AST into a string - // and assign it to a variable. we wrap this generated code when we - // eval the code, so the variable is set up and then invoked. it ends up - // like this: - // - // let __cypressCrossOriginCallback 】added at runtime - // (function () { ┓ added by webpack - // // ... webpack stuff stuff ... ┛ - // __cypressCrossOriginCallback = (args) => { ┓ extracted callback - // const dep = require('../path/to/dep') ┃ - // // ... test stuff ... ┃ - // } ┛ - // // ... webpack stuff stuff ... ┓ added by webpack - // }()) ┛ - // __cypressCrossOriginCallback(args) 】added at runtime - // - const callbackName = '__cypressCrossOriginCallback' - const generatedCode = generate(lastArg.node, {}).code - const modifiedGeneratedCode = `${callbackName} = ${generatedCode}` - // the tmpdir path uses a hashed version of the source file path - // so that it can be cleaned up without removing other in-use tmpdirs - // (notably the support file persists between specs, so its cross-origin - // callback output files need to persist as well) - const sourcePathHash = utils.hash(resourcePath) - const outputDir = utils.tmpdir(sourcePathHash) - // use a hash of the contents in file name to ensure it's unique. if - // the contents happen to be the same, it's okay if they share a file - const codeHash = utils.hash(modifiedGeneratedCode) - const inputFileName = `cross-origin-cb-${codeHash}` - const outputFilePath = `${pathUtil.join(outputDir, inputFileName)}.js` - - store.addFile(resourcePath, { - inputFileName, - outputFilePath, - source: modifiedGeneratedCode, - }) - - // replaces callback function with object referencing the extracted - // function's callback name and output file path in the form - // { callbackName: , outputFilePath: } - // this is used at runtime when the command is run to execute the bundle - // generated for the extracted callback function - lastArg.replaceWith( - t.objectExpression([ - t.objectProperty( - t.stringLiteral('callbackName'), - t.stringLiteral(callbackName), - ), - t.objectProperty( - t.stringLiteral('outputFilePath'), - t.stringLiteral(outputFilePath), - ), - ]), - ) - }, - }) - - // if no requires/imports were found, callback with the original source/map - if (!hasDependencies) { - debug('callback with original source') - this.callback(null, source, map) - - return - } - - // if we found requires/imports, re-generate the code from the AST - const result = generate(ast, { sourceMaps: true }, { - [resourcePath]: source, - }) - // result.map needs to be merged with the original map for it to include - // the changes made in this loader. we can't return result.map because it - // is based off the intermediary code provided to the loader and not the - // original source code (which could be TypeScript or JSX or something) - const newMap = mergeSourceMaps(map, result.map) - - debug('callback with modified source') - this.callback(null, result.code, newMap) -} diff --git a/npm/webpack-preprocessor/lib/cross-origin-callback-store.ts b/npm/webpack-preprocessor/lib/cross-origin-callback-store.ts deleted file mode 100644 index 1167f26ab30a..000000000000 --- a/npm/webpack-preprocessor/lib/cross-origin-callback-store.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface CrossOriginCallbackStoreFile { - inputFileName: string - outputFilePath: string - source: string -} - -export class CrossOriginCallbackStore { - private files: { [key: string]: CrossOriginCallbackStoreFile[] } = {} - - addFile (sourceFilePath: string, file: CrossOriginCallbackStoreFile) { - this.files[sourceFilePath] = (this.files[sourceFilePath] || []).concat(file) - } - - hasFilesFor (sourceFiles: string[]) { - return !!this.getFilesFor(sourceFiles)?.length - } - - getFilesFor (sourceFiles: string[]) { - return Object.keys(this.files).reduce((files, sourceFilePath) => { - return sourceFiles.includes(sourceFilePath) ? files.concat(this.files[sourceFilePath]) : files - }, [] as CrossOriginCallbackStoreFile[]) - } - - reset (sourceFilePath: string) { - this.files[sourceFilePath] = [] - } -} - -export const crossOriginCallbackStore = new CrossOriginCallbackStore() diff --git a/npm/webpack-preprocessor/lib/merge-source-map.ts b/npm/webpack-preprocessor/lib/merge-source-map.ts deleted file mode 100644 index 4e2a6b175c3f..000000000000 --- a/npm/webpack-preprocessor/lib/merge-source-map.ts +++ /dev/null @@ -1,95 +0,0 @@ -// https://github.com/keik/merge-source-map -// -// The MIT License (MIT) - -// Copyright (c) keik - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import sourceMap from 'source-map' - -const SourceMapConsumer = sourceMap.SourceMapConsumer -const SourceMapGenerator = sourceMap.SourceMapGenerator - -/** - * Merge old source map and new source map and return merged. - * If old or new source map value is falsy, return another one as it is. - * - * @param {object|string} [oldMap] old source map object - * @param {object|string} [newmap] new source map object - * @return {object|undefined} merged source map object, or undefined when both old and new source map are undefined - */ -export default function merge (oldMap, newMap) { - if (!oldMap) return newMap - - if (!newMap) return oldMap - - const oldMapConsumer = new SourceMapConsumer(oldMap) - const newMapConsumer = new SourceMapConsumer(newMap) - const mergedMapGenerator = new SourceMapGenerator() - - // iterate on new map and overwrite original position of new map with one of old map - newMapConsumer.eachMapping(function (m) { - // pass when `originalLine` is null. - // It occurs in case that the node does not have origin in original code. - if (m.originalLine == null) return - - const origPosInOldMap = oldMapConsumer.originalPositionFor({ - line: m.originalLine, - column: m.originalColumn, - }) - - if (origPosInOldMap.source == null) return - - mergedMapGenerator.addMapping({ - original: { - line: origPosInOldMap.line, - column: origPosInOldMap.column, - }, - generated: { - line: m.generatedLine, - column: m.generatedColumn, - }, - source: origPosInOldMap.source, - name: origPosInOldMap.name, - }) - }) - - const consumers = [newMapConsumer, oldMapConsumer] - - consumers.forEach(function (consumer) { - // @ts-ignore - consumer.sources.forEach(function (sourceFile) { - // @ts-ignore - mergedMapGenerator._sources.add(sourceFile) - const sourceContent = consumer.sourceContentFor(sourceFile) - - if (sourceContent != null) { - mergedMapGenerator.setSourceContent(sourceFile, sourceContent) - } - }) - }) - - // @ts-ignore - mergedMapGenerator._sourceRoot = oldMap.sourceRoot - // @ts-ignore - mergedMapGenerator._file = oldMap.file - - return JSON.parse(mergedMapGenerator.toString()) -} diff --git a/npm/webpack-preprocessor/lib/utils.ts b/npm/webpack-preprocessor/lib/utils.ts index e51a2e723d06..9257c0194b01 100644 --- a/npm/webpack-preprocessor/lib/utils.ts +++ b/npm/webpack-preprocessor/lib/utils.ts @@ -1,9 +1,4 @@ -import _ from 'lodash' -import * as os from 'os' -import path from 'path' -import md5 from 'md5' import Bluebird from 'bluebird' -import fs from 'fs-extra' function createDeferred () { let resolve: (thenableOrResult?: T | PromiseLike | undefined) => void @@ -21,23 +16,6 @@ function createDeferred () { } } -function hash (contents: string) { - return md5(contents) -} - -function rmdir (dirPath: string) { - return fs.emptyDir(dirPath) -} - -function tmpdir (dirname?: string) { - const pathParts = _.compact([os.tmpdir(), 'cypress', 'webpack-preprocessor', dirname]) - - return path.join(...pathParts) -} - export default { createDeferred, - hash, - rmdir, - tmpdir, } diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index f08c70d242da..1f4888a7db43 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -21,20 +21,12 @@ "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, ." }, "dependencies": { - "@babel/core": "^7.0.1", - "@babel/generator": "^7.17.9", - "@babel/parser": "^7.13.0", - "@babel/traverse": "^7.17.9", "bluebird": "3.7.1", "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "loader-utils": "^2.0.0", - "lodash": "^4.17.20", - "md5": "2.3.0", - "source-map": "^0.6.1", - "webpack-virtual-modules": "^0.4.4" + "lodash": "^4.17.20" }, "devDependencies": { + "@babel/core": "^7.0.1", "@babel/preset-env": "^7.0.0", "@types/mocha": "9.0.0", "@types/webpack": "^4.41.12", @@ -48,6 +40,7 @@ "deps-ok": "1.2.1", "fast-glob": "3.1.1", "find-webpack": "1.5.0", + "fs-extra": "^10.1.0", "mocha": "^7.1.0", "mockery": "2.1.0", "proxyquire": "2.1.3", diff --git a/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts b/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts deleted file mode 100644 index 06a334b48223..000000000000 --- a/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -'use strict' - -import chai, { expect } from 'chai' -import { stripIndent } from 'common-tags' -import * as sinon from 'sinon' -import sinonChai from 'sinon-chai' -import utils from '../../lib/utils' -import { CrossOriginCallbackStore } from '../../lib/cross-origin-callback-store' - -chai.use(sinonChai) - -import loader from '../../lib/cross-origin-callback-loader' - -const expectAddFileSource = (store) => { - return expect(store.addFile.lastCall.args[1].source) -} - -describe('./lib/cross-origin-callback-loader', () => { - const callLoader = (source, commands = ['origin']) => { - const store = new CrossOriginCallbackStore() - const callback = sinon.spy() - const context = { - callback, - resourcePath: '/path/to/file', - query: { commands }, - } - const originalMap = { - sources: [], - sourcesContent: [], - version: 3, - mappings: [], - } - - store.addFile = sinon.stub() - loader.call(context, source, originalMap, null, store) - - return { - store, - originalMap, - resultingSource: callback.lastCall.args[1], - resultingMap: callback.lastCall.args[2], - } - } - - beforeEach(() => { - sinon.restore() - }) - - describe('noop scenarios', () => { - it('is a noop when parsing source fails', () => { - const { originalMap, resultingSource, resultingMap, store } = callLoader(undefined) - - expect(resultingSource).to.be.undefined - expect(resultingMap).to.be.equal(originalMap) - expect(store.addFile).not.to.be.called - }) - - it('is a noop when source does not contain cy.origin()', () => { - const source = `it('test', () => { - cy.get('h1') - })` - const { originalMap, resultingSource, resultingMap, store } = callLoader(source) - - expect(resultingSource).to.be.equal(source) - expect(resultingMap).to.be.equal(originalMap) - expect(store.addFile).not.to.be.called - }) - - it('is a noop when cy.origin() callback does not contain require() or import()', () => { - const source = `it('test', () => { - cy.origin('http://www.foobar.com:3500', () => {}) - })` - const { originalMap, resultingSource, resultingMap, store } = callLoader(source) - - expect(resultingSource).to.be.equal(source) - expect(resultingMap).to.be.equal(originalMap) - expect(store.addFile).not.to.be.called - }) - - it('is a noop when last argument to cy.origin() is not a callback', () => { - const source = `it('test', () => { - cy.origin('http://www.foobar.com:3500', {}) - })` - const { originalMap, resultingSource, resultingMap, store } = callLoader(source) - - expect(resultingSource).to.be.equal(source) - expect(resultingMap).to.be.equal(originalMap) - expect(store.addFile).not.to.be.called - }) - }) - - describe('replacement scenarios', () => { - beforeEach(() => { - sinon.stub(utils, 'hash').returns('abc123') - sinon.stub(utils, 'tmpdir').returns('/path/to/tmp') - }) - - it('replaces cy.origin() callback with an object when using require()', () => { - const source = stripIndent` - it('test', () => { - cy.origin('http://www.foobar.com:3500', () => { - require('../support/utils') - }) - })` - const { originalMap, resultingSource, resultingMap } = callLoader(source) - - expect(resultingSource).to.equal(stripIndent` - it('test', () => { - cy.origin('http://www.foobar.com:3500', { - "callbackName": "__cypressCrossOriginCallback", - "outputFilePath": "/path/to/tmp/cross-origin-cb-abc123.js" - }); - });`) - - expect(resultingMap).to.exist - expect(resultingMap).not.to.equal(originalMap) - expect(resultingMap.sourcesContent[0]).to.equal(source) - }) - - it('replaces cy.origin() callback with an object when using import()', () => { - const source = stripIndent` - it('test', () => { - cy.origin('http://www.foobar.com:3500', async () => { - await import('../support/utils') - }) - })` - const { originalMap, resultingSource, resultingMap } = callLoader(source) - - expect(resultingSource).to.equal(stripIndent` - it('test', () => { - cy.origin('http://www.foobar.com:3500', { - "callbackName": "__cypressCrossOriginCallback", - "outputFilePath": "/path/to/tmp/cross-origin-cb-abc123.js" - }); - });`) - - expect(resultingMap).to.exist - expect(resultingMap).not.to.equal(originalMap) - expect(resultingMap.sourcesContent[0]).to.equal(source) - }) - - it('replaces cy.other() when specified in commands', () => { - const source = stripIndent` - it('test', () => { - cy.other('http://www.foobar.com:3500', () => { - require('../support/utils') - }) - })` - const { originalMap, resultingSource, resultingMap } = callLoader(source, ['other']) - - expect(resultingSource).to.equal(stripIndent` - it('test', () => { - cy.other('http://www.foobar.com:3500', { - "callbackName": "__cypressCrossOriginCallback", - "outputFilePath": "/path/to/tmp/cross-origin-cb-abc123.js" - }); - });`) - - expect(resultingMap).to.exist - expect(resultingMap).not.to.equal(originalMap) - expect(resultingMap.sourcesContent[0]).to.equal(source) - }) - - it('adds the file to the store, replacing require() with require()', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', () => { - require('../support/utils') - }) - })`, - ) - - expect(store.addFile).to.be.calledWithMatch('/path/to/file', { - inputFileName: 'cross-origin-cb-abc123', - outputFilePath: '/path/to/tmp/cross-origin-cb-abc123.js', - }) - }) - - // arrow expression is implicitly tested in other tests - it('works when callback is a function expression', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', function () { - require('../support/utils') - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = function () { - require('../support/utils'); - }`) - }) - - it('works when dep is not assigned to a variable', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', () => { - require('../support/utils') - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = () => { - require('../support/utils'); - }`) - }) - - it('works when dep is assigned to a variable', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', () => { - const utils = require('../support/utils') - utils.foo() - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = () => { - const utils = require('../support/utils'); - utils.foo(); - }`) - }) - - it('works with multiple require()s', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', () => { - require('../support/commands') - const utils = require('../support/utils') - const _ = require('lodash') - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = () => { - require('../support/commands'); - const utils = require('../support/utils'); - const _ = require('lodash'); - }`) - }) - - it('works when .origin() is chained off another command', () => { - const { store } = callLoader( - `it('test', () => { - cy - .wrap({}) - .origin('http://www.foobar.com:3500', () => { - require('../support/commands') - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = () => { - require('../support/commands'); - }`) - }) - - it('works when result of require() is invoked', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', () => { - const someVar = 'someValue' - const result = require('./fn')(someVar) - expect(result).to.equal('mutated someVar') - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = () => { - const someVar = 'someValue'; - const result = require('./fn')(someVar); - expect(result).to.equal('mutated someVar'); - }`) - }) - - it('works when dependencies passed into called', () => { - const { store } = callLoader( - `it('test', () => { - cy.origin('http://www.foobar.com:3500', { args: { foo: 'foo'}}, ({ foo }) => { - const result = require('./fn')(foo) - expect(result).to.equal('mutated someVar') - }) - })`, - ) - - expectAddFileSource(store).to.equal(stripIndent` - __cypressCrossOriginCallback = ({ - foo - }) => { - const result = require('./fn')(foo); - expect(result).to.equal('mutated someVar'); - }`) - }) - }) -}) diff --git a/npm/webpack-preprocessor/test/unit/index.spec.js b/npm/webpack-preprocessor/test/unit/index.spec.js index b22f31002618..1b3e946f40d1 100644 --- a/npm/webpack-preprocessor/test/unit/index.spec.js +++ b/npm/webpack-preprocessor/test/unit/index.spec.js @@ -23,10 +23,7 @@ mockery.enable({ mockery.registerMock('webpack', webpack) const preprocessor = require('../../index') -const utils = require('../../lib/utils').default const typescriptOverrides = require('../../lib/typescript-overrides') -const crossOriginCallbackStore = require('../../lib/cross-origin-callback-store').crossOriginCallbackStore -const crossOriginCallbackCompile = require('../../lib/cross-origin-callback-compile') describe('webpack preprocessor', function () { beforeEach(function () { @@ -68,9 +65,6 @@ describe('webpack preprocessor', function () { onClose: sinon.stub(), } - sinon.stub(utils, 'rmdir').resolves() - sinon.stub(utils, 'tmpdir').returns('/path/to/tmp/dir') - this.run = (options, file = this.file) => { return preprocessor(options)(file) } @@ -167,79 +161,6 @@ describe('webpack preprocessor', function () { }) }) - describe('cross-origin callback compilation', function () { - beforeEach(function () { - global.__cypressCallbackReplacementCommands = ['origin'] - - this.files = [] - - sinon.stub(crossOriginCallbackStore, 'hasFilesFor').returns(true) - sinon.stub(crossOriginCallbackStore, 'getFilesFor').returns(this.files) - sinon.stub(crossOriginCallbackCompile, 'compileCrossOriginCallbackFiles').resolves() - sinon.stub(crossOriginCallbackStore, 'reset') - - this.statsApi = { - hasErrors: () => false, - toJson () { - return { warnings: [], errors: [], modules: [] } - }, - } - - this.compilerApi.run.yields(null, this.statsApi) - }) - - afterEach(function () { - global.__cypressCallbackReplacementCommands = undefined - }) - - it('adds cross-origin callback loader when flag is on', function () { - const options = { webpackOptions: { devtool: false, module: { rules: [] } } } - - return this.run(options).then(() => { - expect(options.webpackOptions.module.rules[0].use[0].loader).to.include('cross-origin-callback-loader') - }) - }) - - it('runs additional compilation for cross-origin callback files', function () { - return this.run().then(() => { - expect(crossOriginCallbackCompile.compileCrossOriginCallbackFiles).to.be.calledWith(this.files) - expect(crossOriginCallbackStore.reset).to.be.called - }) - }) - - it('rejects the main bundle promise if callback file compilation errors', function () { - const err = new Error('compilation failed') - - crossOriginCallbackCompile.compileCrossOriginCallbackFiles.rejects(err) - - return this.run() - .then(() => { - throw new Error('should not resolve') - }) - .catch((_err) => { - expect(_err).to.equal(err) - expect(crossOriginCallbackStore.reset).to.be.called - }) - }) - - it('does not compile files when no commands are specified', function () { - global.__cypressCallbackReplacementCommands = undefined - - return this.run().then(() => { - expect(crossOriginCallbackStore.hasFilesFor).not.to.be.called - expect(crossOriginCallbackCompile.compileCrossOriginCallbackFiles).not.to.be.called - }) - }) - - it('does not compile files there are no files', function () { - crossOriginCallbackStore.hasFilesFor.returns(false) - - return this.run().then(() => { - expect(crossOriginCallbackCompile.compileCrossOriginCallbackFiles).not.to.be.called - }) - }) - }) - describe('devtool', function () { beforeEach((() => { sinon.stub(typescriptOverrides, 'overrideSourceMaps') @@ -386,15 +307,6 @@ describe('webpack preprocessor', function () { }) }) - it('deletes temp dir when `close` is emitted', function () { - this.compilerApi.watch.yields(null, this.statsApi) - - return this.run().then(() => { - this.file.on.withArgs('close').yield() - expect(utils.rmdir).to.be.calledWith(utils.tmpdir()) - }) - }) - it('uses default webpack options when no user options', function () { return this.run().then(() => { expect(webpack.lastCall.args[0].module.rules[0].use).to.have.length(1) diff --git a/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts b/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts index 5c3ee15c4c99..2edad8a786c0 100644 --- a/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts +++ b/packages/app/cypress/e2e/runner/ct-framework-errors.cy.ts @@ -74,7 +74,7 @@ reactVersions.forEach((reactVersion) => { }) verify('error on mount', { - line: 6, + line: 5, column: 33, uncaught: true, uncaughtMessage: 'mount error', @@ -86,8 +86,8 @@ reactVersions.forEach((reactVersion) => { }) verify('sync error', { - line: 11, - column: 34, + line: 12, + column: 19, uncaught: true, uncaughtMessage: 'sync error', message: [ @@ -101,8 +101,8 @@ reactVersions.forEach((reactVersion) => { }) verify('async error', { - line: 18, - column: 38, + line: 21, + column: 21, uncaught: true, uncaughtMessage: 'async error', message: [ @@ -116,7 +116,7 @@ reactVersions.forEach((reactVersion) => { }) verify('command failure', { - line: 43, + line: 47, column: 8, command: 'get', message: [ @@ -148,7 +148,7 @@ describe('Next.js', { }) verify('error on mount', { - line: 7, + line: 6, column: 33, uncaught: true, uncaughtMessage: 'mount error', @@ -160,8 +160,8 @@ describe('Next.js', { }) verify('sync error', { - line: 12, - column: 34, + line: 13, + column: 19, uncaught: true, uncaughtMessage: 'sync error', message: [ @@ -175,8 +175,8 @@ describe('Next.js', { }) verify('async error', { - line: 19, - column: 38, + line: 22, + column: 21, uncaught: true, uncaughtMessage: 'async error', message: [ @@ -189,7 +189,7 @@ describe('Next.js', { }) verify('command failure', { - line: 44, + line: 48, column: 8, command: 'get', message: [ @@ -338,8 +338,8 @@ describe('Nuxt', { 'Timed out retrying', 'element-that-does-not-exist', ], - codeFrameRegex: /Errors\.cy\.js:26/, - stackRegex: /Errors\.cy\.js:26/, + codeFrameRegex: /Errors\.cy\.js:25/, + stackRegex: /Errors\.cy\.js:25/, }) }) }) @@ -465,7 +465,7 @@ angularVersions.forEach((angularVersion) => { }) verify('command failure', { - line: 21, + line: 20, column: 8, command: 'get', message: [ diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index cf9f75c79494..2a48153160ba 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -231,7 +231,8 @@ const driverConfigOptions: Array = [ defaultValue: false, validation: validate.isBoolean, isExperimental: true, - requireRestartOnChange: 'server', + overrideLevel: 'any', + requireRestartOnChange: 'browser', }, { name: 'experimentalSourceRewriting', defaultValue: false, diff --git a/packages/data-context/src/sources/FileDataSource.ts b/packages/data-context/src/sources/FileDataSource.ts index 4ef6b9671b05..52b07410e46b 100644 --- a/packages/data-context/src/sources/FileDataSource.ts +++ b/packages/data-context/src/sources/FileDataSource.ts @@ -34,7 +34,7 @@ export class FileDataSource { return this.ctx.fs.readFile(path.join(this.ctx.currentProject, relative), 'utf-8') } - async getFilesByGlob (cwd: string, glob: string | string[], globOptions?: GlobbyOptions) { + async getFilesByGlob (cwd: string, glob: string | string[], globOptions: GlobbyOptions = {}): Promise { const globs = ([] as string[]).concat(glob).map((globPattern) => { const workingDirectoryPrefix = path.join(cwd, path.sep) @@ -49,7 +49,7 @@ export class FileDataSource { return globPattern }) - const ignoreGlob = (globOptions?.ignore ?? []).concat('**/node_modules/**') + const ignoreGlob = (globOptions.ignore ?? []).concat('**/node_modules/**') if (os.platform() === 'win32') { // globby can't work with backwards slashes @@ -72,7 +72,15 @@ export class FileDataSource { return files } catch (e) { - debug('error in getFilesByGlob %o', e) + if (!globOptions.suppressErrors) { + // Log error and retry with filesystem errors suppressed - this allows us to find partial + // results even if the glob search hits permission issues (#24109) + debug('Error in getFilesByGlob %o, retrying with filesystem errors suppressed', e) + + return await this.getFilesByGlob(cwd, glob, { ...globOptions, suppressErrors: true }) + } + + debug('Non-suppressible error in getFilesByGlob %o', e) return [] } diff --git a/packages/data-context/test/unit/sources/FileDataSource.spec.ts b/packages/data-context/test/unit/sources/FileDataSource.spec.ts index 2484c427cb1d..98da6e245d3f 100644 --- a/packages/data-context/test/unit/sources/FileDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/FileDataSource.spec.ts @@ -272,6 +272,35 @@ describe('FileDataSource', () => { }, ) }) + + it('should retry search with `suppressErrors` if non-suppressed attempt fails', async () => { + matchGlobsStub.onFirstCall().rejects(new Error('mocked filesystem error')) + matchGlobsStub.onSecondCall().resolves(mockMatches) + + const files = await fileDataSource.getFilesByGlob( + '/', + '/cypress/e2e/**.cy.js', + { absolute: false, objectMode: true }, + ) + + expect(files).to.eq(mockMatches) + expect(matchGlobsStub).to.have.callCount(2) + expect(matchGlobsStub.getCall(0).args[1].suppressErrors).to.be.undefined + expect(matchGlobsStub.getCall(1).args[1].suppressErrors).to.equal(true) + }) + + it('should return empty array if retry with suppression fails', async () => { + matchGlobsStub.rejects(new Error('mocked filesystem error')) + + const files = await fileDataSource.getFilesByGlob( + '/', + '/cypress/e2e/**.cy.js', + { absolute: false, objectMode: true }, + ) + + expect(files).to.eql([]) + expect(matchGlobsStub).to.have.callCount(2) + }) }) }) }) diff --git a/packages/driver/cross-origin-testing.md b/packages/driver/cross-origin-testing.md index 48858eb7c3b8..bd6923f072e3 100644 --- a/packages/driver/cross-origin-testing.md +++ b/packages/driver/cross-origin-testing.md @@ -194,7 +194,7 @@ We patch `XMLHttpRequest` and `fetch` client-side to capture their `withCredenti ## Dependencies -Users can utilize `require()` or (dynamic) `import()` to include dependencies. We handle the dependency resolution and bundling with the webpack preprocessor. We add a webpack loader that runs last. If we find a `require()` or `import()` call inside a `cy.origin()` callback, we extract that callback from the output code. We then run that extracted callback through webpack again, so that it gets its own output bundle with all dependencies included. The original callback is replaced with an object that references the output bundle. At runtime, when executing `cy.origin()`, it loads and executes the callback bundle. +Users can utilize `Cypress.require()` to include dependencies. It's functionally the same as the CommonJS `require()`. At runtime, before evaluating the **cy.origin()** callback, we send it to the server, replace references to `Cypress.require()` with `require()`, and run it through the default preprocessor (currently webpack) to bundle any dependencies with it. We send that bundle back to the cross-origin driver and evaluate it. ## Unsupported APIs diff --git a/packages/driver/cypress/e2e/cypress/cy.cy.js b/packages/driver/cypress/e2e/cypress/cy.cy.js index fd7f5291d006..52aa44df2b6d 100644 --- a/packages/driver/cypress/e2e/cypress/cy.cy.js +++ b/packages/driver/cypress/e2e/cypress/cy.cy.js @@ -565,5 +565,54 @@ describe('driver/src/cypress/cy', () => { cy.get('body').find('#specific-contains').children().should('have.class', 'active') }) + + context('overwriting queries', () => { + it('does not allow commands to overwrite queries', () => { + const fn = () => Cypress.Commands.overwrite('get', () => {}) + + expect(fn).to.throw().with.property('message') + .and.include('Cannot overwite the `get` query. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`.') + + expect(fn).to.throw().with.property('docsUrl') + .and.include('https://on.cypress.io/api') + }) + + it('does not allow queries to overwrite commands', () => { + const fn = () => Cypress.Commands.overwriteQuery('click', () => {}) + + expect(fn).to.throw().with.property('message') + .and.include('Cannot overwite the `click` command. Commands can only be overwritten with `Cypress.Commands.overwrite()`.') + + expect(fn).to.throw().with.property('docsUrl') + .and.include('https://on.cypress.io/api') + }) + + it('can call the originalFn', () => { + // Ensure nothing gets confused when we overwrite the same query multiple times. + // Both overwrites should succeed, layered on top of each other. + + let overwriteCalled = 0 + + Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) { + overwriteCalled++ + + return originalFn.call(this, ...args) + }) + + let secondOverwriteCalled = 0 + + Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) { + secondOverwriteCalled++ + + return originalFn.call(this, ...args) + }) + + cy.get('button').should('have.length', 24) + cy.then(() => { + expect(overwriteCalled).to.eq(1) + expect(secondOverwriteCalled).to.eq(1) + }) + }) + }) }) }) diff --git a/packages/driver/cypress/e2e/e2e/origin/cookie_behavior.cy.ts b/packages/driver/cypress/e2e/e2e/origin/cookie_behavior.cy.ts index 9631ed5ad713..6e0965aada61 100644 --- a/packages/driver/cypress/e2e/e2e/origin/cookie_behavior.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/cookie_behavior.cy.ts @@ -1,5 +1,16 @@ import { makeRequestForCookieBehaviorTests as makeRequest } from '../../../support/utils' +declare global { + interface Window { + makeRequest: ( + win: Cypress.AUTWindow, + url: string, + client?: 'fetch' | 'xmlHttpRequest', + credentials?: 'same-origin' | 'include' | 'omit' | boolean, + ) => Promise + } +} + describe('Cookie Behavior', { browser: '!webkit' }, () => { const serverConfig = { http: { @@ -29,12 +40,8 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { before(() => { originUrl = `${scheme}://www.foobar.com:${sameOriginPort}` - // add httpClient here globally until Cypress.require PR is merged cy.origin(`${scheme}://www.foobar.com:${sameOriginPort}`, () => { - const { makeRequestForCookieBehaviorTests: makeRequest } = require('../../../support/utils') - - // @ts-ignore - window.makeRequest = makeRequest + window.makeRequest = Cypress.require('../../../support/utils').makeRequestForCookieBehaviorTests }) }) @@ -59,11 +66,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { args: originUrl, }, (originUrl) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${originUrl}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${originUrl}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${originUrl}/test-request`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${originUrl}/test-request`, 'xmlHttpRequest')) }) cy.wait('@cookieCheck') @@ -87,11 +94,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cookie jar should now mimic http://www.foobar.com:3500 / https://foobar.com:3502 as top cy.origin(originUrl, () => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/test-request', 'fetch')) + return cy.wrap(window.makeRequest(win, '/test-request', 'fetch')) }) cy.wait('@cookieCheck') @@ -114,11 +121,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cookie jar should now mimic http://www.foobar.com:3500 / https://foobar.com:3502 as top cy.origin(originUrl, () => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/test-request', 'fetch')) + return cy.wrap(window.makeRequest(win, '/test-request', 'fetch')) }) cy.wait('@cookieCheck') @@ -141,12 +148,12 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { cy.origin(originUrl, () => { cy.window().then((win) => { // set the cookie in the browser - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch')) }) cy.window().then((win) => { // but omit the cookies in the request - return cy.wrap(makeRequest(win, '/test-request', 'fetch', 'omit')) + return cy.wrap(window.makeRequest(win, '/test-request', 'fetch', 'omit')) }) cy.wait('@cookieCheck') @@ -169,12 +176,12 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { cy.origin(originUrl, () => { cy.window().then((win) => { // do NOT set the cookie in the browser - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch', 'omit')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch', 'omit')) }) cy.window().then((win) => { // but send the cookies in the request - return cy.wrap(makeRequest(win, '/test-request', 'fetch')) + return cy.wrap(window.makeRequest(win, '/test-request', 'fetch')) }) cy.wait('@cookieCheck') @@ -206,12 +213,12 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { // do NOT set the cookie in the browser - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest')) }) cy.window().then((win) => { // but send the cookies in the request - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest')) }) cy.wait('@cookieCheck') @@ -239,7 +246,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { // do NOT set the cookie in the browser - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true)) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true)) }) // though request is cross origin, site should have access directly to cookie because it is same site @@ -258,7 +265,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { cy.window().then((win) => { // but send the cookies in the request - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest')) }) cy.wait('@cookieCheck') @@ -286,7 +293,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { // do NOT set the cookie in the browser - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true)) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true)) }) // though request is cross origin, site should have access directly to cookie because it is same site @@ -305,7 +312,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { cy.window().then((win) => { // but send the cookies in the request - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'xmlHttpRequest', true)) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'xmlHttpRequest', true)) }) cy.wait('@cookieCheck') @@ -334,11 +341,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch')) }) cy.wait('@cookieCheck') @@ -365,7 +372,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include')) }) // assert cookie value is actually set in the browser @@ -382,7 +389,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cy.getCookie('foo1').its('value').should('equal', 'bar1') cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'include')) }) }) }) @@ -407,7 +414,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include')) }) // assert cookie value is actually set in the browser @@ -424,7 +431,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cy.getCookie('foo1').its('value').should('equal', 'bar1') cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch')) }) cy.wait('@cookieCheck') @@ -451,11 +458,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'omit')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'omit')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'omit')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'omit')) }) cy.wait('@cookieCheck') @@ -486,11 +493,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'xmlHttpRequest')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest')) }) cy.wait('@cookieCheck') @@ -519,7 +526,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true)) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true)) }) // assert cookie value is actually set in the browser @@ -535,7 +542,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { } cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest')) }) cy.wait('@cookieCheck') @@ -562,7 +569,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true)) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true)) }) // FIXME: cy.getCookie does not believe this cookie exists. Should be fixed in https://github.com/cypress-io/cypress/pull/23643. @@ -573,7 +580,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cy.getCookie('bar1').its('value').should('equal', 'baz1') cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'xmlHttpRequest', true)) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'xmlHttpRequest', true)) }) cy.wait('@cookieCheck') @@ -605,13 +612,13 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort, credentialOption }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'fetch', credentialOption as 'same-origin' | 'omit')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'fetch', credentialOption as 'same-origin' | 'omit')) }) cy.getCookie('bar1').should('equal', null) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit')) }) cy.wait('@cookieCheck') @@ -639,7 +646,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort, credentialOption }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include')) }) // assert cookie value is actually set in the browser @@ -655,7 +662,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { } cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit')) }) cy.wait('@cookieCheck') @@ -685,7 +692,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include')) }) // assert cookie value is actually set in the browser @@ -698,7 +705,7 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cy.getCookie('bar1').its('value').should('equal', 'baz1') cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include')) }) cy.wait('@cookieCheck') @@ -730,20 +737,20 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, crossOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch', 'include')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=bar=baz; Domain=.foobar.com`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=bar=baz; Domain=.foobar.com`, 'fetch', 'include')) }) // Cookie should not be sent with app.foobar.com:3500/test as it does NOT fit the domain cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=baz=quux; Domain=app.foobar.com`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=baz=quux; Domain=app.foobar.com`, 'fetch', 'include')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'fetch', 'include')) }) cy.wait('@cookieCheck') @@ -770,15 +777,15 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch')) + return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include')) }) cy.wait('@cookieCheck') @@ -803,15 +810,15 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { // cookie jar should now mimic http://www.foobar.com:3500 / https://foobar.com:3502 as top cy.origin(originUrl, () => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com; Path=/', 'fetch')) + return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com; Path=/', 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com; Path=/test-request`, 'fetch')) + return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com; Path=/test-request`, 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `/test-request`, 'fetch')) + return cy.wrap(window.makeRequest(win, `/test-request`, 'fetch')) }) cy.wait('@cookieCheck') @@ -840,17 +847,17 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => { }, }, ({ scheme, sameOriginPort }) => { cy.window().then((win) => { - return cy.wrap(makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=www.foobar.com`, 'fetch')) + return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=www.foobar.com`, 'fetch')) }) cy.wait(200) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch')) + return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch')) }) cy.window().then((win) => { - return cy.wrap(makeRequest(win, `${scheme}://www.foobar.com:${sameOriginPort}/test-request`, 'fetch', 'include')) + return cy.wrap(window.makeRequest(win, `${scheme}://www.foobar.com:${sameOriginPort}/test-request`, 'fetch', 'include')) }) cy.wait('@cookieCheck') diff --git a/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx index b88604253101..872827dcfadc 100644 --- a/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx +++ b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx @@ -6,7 +6,7 @@ describe('cy.origin dependencies - jsx', { browser: '!webkit' }, () => { it('works with a jsx file', () => { cy.origin('http://www.foobar.com:3500', () => { - const lodash = require('lodash') + const lodash = Cypress.require('lodash') expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') }) diff --git a/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts index 4834b7608e8d..5c9eeabe0bd1 100644 --- a/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts @@ -1,29 +1,29 @@ +import type { LoDashStatic } from 'lodash' + describe('cy.origin dependencies', { browser: '!webkit' }, () => { beforeEach(() => { cy.visit('/fixtures/primary-origin.html') cy.get('a[data-cy="cross-origin-secondary-link"]').click() }) - it('works with require()', () => { + it('works', () => { cy.origin('http://www.foobar.com:3500', () => { - const lodash = require('lodash') - - expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') - }) - }) - - it('works with dynamic import()', () => { - cy.origin('http://www.foobar.com:3500', async () => { - const lodash = await import('lodash') - - expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') + // default type: any + const lodash1 = Cypress.require('lodash') + // 2 ways of getting the proper type + const lodash2 = Cypress.require('lodash') as typeof import('lodash') + const lodash3 = Cypress.require('lodash') + + expect(lodash1.get({ foo: 'foo' }, 'foo')).to.equal('foo') + expect(lodash2.get({ foo: 'foo' }, 'foo')).to.equal('foo') + expect(lodash3.get({ foo: 'foo' }, 'foo')).to.equal('foo') }) }) it('works with an arrow function', () => { cy.origin('http://www.foobar.com:3500', () => { - const lodash = require('lodash') - const dayjs = require('dayjs') + const lodash = Cypress.require('lodash') + const dayjs = Cypress.require('dayjs') expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') expect(dayjs('2022-07-29 12:00:00').format('MMMM D, YYYY')).to.equal('July 29, 2022') @@ -34,7 +34,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { it('works with a function expression', () => { cy.origin('http://www.foobar.com:3500', function () { - const lodash = require('lodash') + const lodash = Cypress.require('lodash') expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') }) @@ -42,7 +42,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { it('works with options object + args', () => { cy.origin('http://www.foobar.com:3500', { args: ['arg1'] }, ([arg1]) => { - const lodash = require('lodash') + const lodash = Cypress.require('lodash') expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') expect(arg1).to.equal('arg1') @@ -51,7 +51,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { it('works with a yielded value', () => { cy.origin('http://www.foobar.com:3500', () => { - const lodash = require('lodash') + const lodash = Cypress.require('lodash') expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') @@ -62,18 +62,16 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { it('works with a returned value', () => { cy.origin('http://www.foobar.com:3500', () => { - const lodash = require('lodash') + const { add } = Cypress.require('./dependencies.support-esm') - expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') - - return 'returned value' + return add(1, 2) }) - .should('equal', 'returned value') + .should('equal', 3) }) it('works with multiple cy.origin calls', () => { cy.origin('http://www.foobar.com:3500', () => { - const lodash = require('lodash') + const lodash = Cypress.require('lodash') expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo') @@ -81,7 +79,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { }) cy.origin('http://www.idp.com:3500', () => { - const dayjs = require('dayjs') + const dayjs = Cypress.require('dayjs') expect(dayjs('2022-07-29 12:00:00').format('MMMM D, YYYY')).to.equal('July 29, 2022') }) @@ -89,7 +87,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { it('works with a relative esm dependency', () => { cy.origin('http://www.foobar.com:3500', () => { - const { add } = require('./dependencies.support-esm') + const { add } = Cypress.require('./dependencies.support-esm') expect(add(1, 2)).to.equal(3) }) @@ -97,7 +95,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { it('works with a relative commonjs dependency', () => { cy.origin('http://www.foobar.com:3500', () => { - const { add } = require('./dependencies.support-commonjs') + const { add } = Cypress.require('./dependencies.support-commonjs') expect(add(1, 2)).to.equal(3) }) @@ -107,7 +105,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { const args = ['some string'] cy.origin('http://www.foobar.com:3500', { args }, ([arg1]) => { - const result = require('./dependencies.support-commonjs')(arg1) + const result = Cypress.require('./dependencies.support-commonjs')(arg1) expect(result).to.equal('some_string') }) @@ -126,13 +124,26 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => { }) describe('errors', () => { - it('when dependency does not exist', () => { + it('when dependency does not exist', (done) => { cy.on('fail', (err) => { expect(err.message).to.include('Cannot find module') + done() + }) + + cy.origin('http://www.foobar.com:3500', () => { + Cypress.require('./does-not-exist') + }) + }) + + // @ts-ignore + it('when experimental flag is disabled', { experimentalOriginDependencies: false }, (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('Using `Cypress.require()` requires enabling the `experimentalOriginDependencies` flag.') + done() }) cy.origin('http://www.foobar.com:3500', () => { - require('./does-not-exist') + Cypress.require('lodash') }) }) }) diff --git a/packages/driver/cypress/support/defaults.js b/packages/driver/cypress/support/defaults.js index 2b0396c80b08..332f3834cc22 100644 --- a/packages/driver/cypress/support/defaults.js +++ b/packages/driver/cypress/support/defaults.js @@ -36,6 +36,6 @@ beforeEach(() => { // support file work properly Cypress.Commands.add('originLoadUtils', (origin) => { cy.origin(origin, () => { - require('./utils') + Cypress.require('./utils') }) }) diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index ee206d43416b..5b07d66ea363 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -8,6 +8,7 @@ import $Cypress from '../cypress' import { $Cy } from '../cypress/cy' import { $Location } from '../cypress/location' import $Commands from '../cypress/commands' +import $errUtils from '../cypress/error_utils' import { create as createLog } from '../cypress/log' import { bindToListeners } from '../cy/listeners' import { handleOriginFn } from './origin_fn' @@ -135,6 +136,20 @@ const setup = (cypressConfig: Cypress.Config, env: Cypress.ObjectLike) => { // @ts-ignore Cypress.isCy = cy.isCy + // this is "valid" inside the cy.origin() callback (as long as the experimental + // flag is enabled), but it should be replaced by the preprocessor at runtime + // with an actual require() before it's run in the browser. if it's not, + // something unexpected has gone wrong + // @ts-expect-error + Cypress.require = () => { + // @ts-ignore + if (!Cypress.config('experimentalOriginDependencies')) { + $errUtils.throwErrByPath('require.invalid_without_flag') + } + + $errUtils.throwErrByPath('require.invalid_inside_origin') + } + handleOriginFn(Cypress, cy) handleLogs(Cypress) handleSocketEvents(Cypress) diff --git a/packages/driver/src/cross-origin/origin_fn.ts b/packages/driver/src/cross-origin/origin_fn.ts index 8893d9325540..b4966858bfe8 100644 --- a/packages/driver/src/cross-origin/origin_fn.ts +++ b/packages/driver/src/cross-origin/origin_fn.ts @@ -5,18 +5,13 @@ import { $Location } from '../cypress/location' import { syncConfigToCurrentOrigin, syncEnvToCurrentOrigin } from '../util/config' import type { Runnable, Test } from 'mocha' import { LogUtils } from '../cypress/log' -import $networkUtils from '../cypress/network_utils' - -interface CrossOriginCallbackObject { - callbackName: string - outputFilePath: string -} interface RunOriginFnOptions { config: Cypress.Config args: any env: Cypress.ObjectLike - fn: string | CrossOriginCallbackObject + file?: string + fn: string skipConfigValidation: boolean state: {} logCounter: number @@ -66,6 +61,49 @@ const rehydrateRunnable = (data: serializedRunnable): Runnable|Test => { return runnable } +// Callback function handling / preprocessing for dependencies +// --- +// 1. If experimentalOriginDependencies is disabled or the string "Cypress.require" +// does not exist in the callback, just eval the callback as-is +// 2. Otherwise, we send it to the server +// 3. The server webpacks the callback to bundle in all the deps, then returns +// that bundle +// 4. Eval the callback like normal +const getCallbackFn = async (fn: string, file?: string) => { + if ( + // @ts-expect-error + !Cypress.config('experimentalOriginDependencies') + || !fn.includes('Cypress.require') + ) { + return fn + } + + // Since webpack will wrap everything up in a closure, we create a variable + // in the outer scope (see the return value below), assign the function to it + // in the inner scope, then call the function with the args + const callbackName = '__cypressCallback' + const response = await fetch('/__cypress/process-origin-callback', { + body: JSON.stringify({ file, fn: `${callbackName} = ${fn};` }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + const result = await response.json() as GetFileResult + + if (result.error) { + $errUtils.throwErrByPath('origin.failed_to_get_callback', { + args: { error: result.error }, + }) + } + + return `(args) => { + let ${callbackName}; + ${result.contents}; + return ${callbackName}(args); + }` +} + export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { const reset = (state) => { cy.reset({}) @@ -98,7 +136,7 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { } Cypress.specBridgeCommunicator.on('run:origin:fn', async (options: RunOriginFnOptions) => { - const { config, args, env, fn, state, skipConfigValidation, logCounter } = options + const { config, args, env, file, fn, state, skipConfigValidation, logCounter } = options let queueFinished = false @@ -139,27 +177,8 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => { }) try { - let value - - if (_.isString(fn)) { - value = window.eval(`(${fn})`)(args) - } else { - const { callbackName, outputFilePath } = fn - const rawResult = await $networkUtils.fetch(`/__cypress/get-file/${encodeURIComponent(outputFilePath)}`) as string - const result = JSON.parse(rawResult) as GetFileResult - - if (result.error) { - $errUtils.throwErrByPath('origin.failed_to_get_callback', { - args: { error: result.error }, - }) - } - - value = window.eval(`(args) => { - let ${callbackName}; - ${result.contents} - return ${callbackName}(args); - }`)(args) - } + const callback = await getCallbackFn(fn, file) + const value = window.eval(`(${callback})`)(args) // If we detect a non promise value with commands in queue, throw an error if (value && cy.queue.length > 0 && !value.then) { diff --git a/packages/driver/src/cy/commands/origin/index.ts b/packages/driver/src/cy/commands/origin/index.ts index 45e405800586..e71132398b9f 100644 --- a/packages/driver/src/cy/commands/origin/index.ts +++ b/packages/driver/src/cy/commands/origin/index.ts @@ -183,6 +183,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State communicator.toSpecBridge(origin, 'attach:to:window') const fn = _.isFunction(callbackFn) ? callbackFn.toString() : callbackFn + const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, config('projectRoot'))?.absoluteFile // once the secondary origin page loads, send along the // user-specified callback to run in that origin @@ -190,6 +191,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State communicator.toSpecBridge(origin, 'run:origin:fn', { args: options?.args || undefined, fn, + file, // let the spec bridge version of Cypress know if config read-only values can be overwritten since window.top cannot be accessed in cross-origin iframes // this should only be used for internal testing. Cast to boolean to guarantee serialization // @ts-ignore diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index 2d0ded980238..d8141ee19c4b 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -772,6 +772,11 @@ class $Cypress { return throwPrivateCommandInterface('addUtilityCommand') } + // Cypress.require() is only valid inside the cy.origin() callback + require () { + $errUtils.throwErrByPath('require.invalid_outside_origin') + } + get currentTest () { const r = this.cy.state('runnable') diff --git a/packages/driver/src/cypress/commands.ts b/packages/driver/src/cypress/commands.ts index c4e51904bafa..36bf924f3085 100644 --- a/packages/driver/src/cypress/commands.ts +++ b/packages/driver/src/cypress/commands.ts @@ -27,11 +27,9 @@ const getTypeByPrevSubject = (prevSubject) => { return 'parent' } -const internalError = (path, name) => { +const internalError = (path, args) => { $errUtils.throwErrByPath(path, { - args: { - name, - }, + args, stack: (new cy.state('specWindow').Error('add command stack')).stack, errProps: { appendToStack: { @@ -88,11 +86,11 @@ export default { add (name, options, fn) { if (builtInCommandNames[name]) { - internalError('miscellaneous.invalid_new_command', name) + internalError('miscellaneous.invalid_new_command', { name }) } if (reservedCommandNames.has(name)) { - internalError('miscellaneous.reserved_command', name) + internalError('miscellaneous.reserved_command', { name }) } // .hover & .mount are special case commands. allow as builtins so users @@ -126,11 +124,11 @@ export default { const original = commands[name] if (queries[name]) { - internalError('miscellaneous.invalid_overwrite_query_with_command', name) + internalError('miscellaneous.invalid_overwrite_query_with_command', { name }) } if (!original) { - internalError('miscellaneous.invalid_overwrite', name) + internalError('miscellaneous.invalid_overwrite', { name, type: 'command' }) } function originalFn (...args) { @@ -157,13 +155,13 @@ export default { return cy.addCommand(overridden) }, - addQuery (name: string, fn: () => QueryFunction) { + addQuery (name: string, fn: (...args: any[]) => QueryFunction) { if (reservedCommandNames.has(name)) { - internalError('miscellaneous.reserved_command_query', name) + internalError('miscellaneous.reserved_command_query', { name }) } if (cy[name]) { - internalError('miscellaneous.invalid_new_query', name) + internalError('miscellaneous.invalid_new_query', { name }) } if (addingBuiltIns) { @@ -173,6 +171,26 @@ export default { queries[name] = fn cy.addQuery({ name, fn }) }, + + overwriteQuery (name: string, fn: (...args: any[]) => QueryFunction) { + if (commands[name]) { + internalError('miscellaneous.invalid_overwrite_command_with_query', { name }) + } + + const original = queries[name] + + if (!original) { + internalError('miscellaneous.invalid_overwrite', { name, type: 'command' }) + } + + queries[name] = function overridden (...args) { + args.unshift(original) + + return fn.apply(this, args) + } + + cy.addQuery({ name, fn: queries[name] }) + }, } addingBuiltIns = true diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index f89958dd2d8a..008304d7017b 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -862,12 +862,16 @@ export default { docsUrl: 'https://on.cypress.io/api/custom-queries', }, invalid_overwrite: { - message: 'Cannot overwite command for: `{{name}}`. An existing command does not exist by that name.', - docsUrl: 'https://on.cypress.io/api', + message: 'Cannot overwite command for: `{{name}}`. An existing {{type}} does not exist by that name.', + docsUrl: 'https://on.cypress.io/api/custom-commands', + }, + invalid_overwrite_command_with_query: { + message: 'Cannot overwite the `{{name}}` command. Commands can only be overwritten with `Cypress.Commands.overwrite()`.', + docsUrl: 'https://on.cypress.io/api/custom-commands', }, invalid_overwrite_query_with_command: { - message: 'Cannot overwite the `{{name}}` query. Queries cannot be overwritten.', - docsUrl: 'https://on.cypress.io/api', + message: 'Cannot overwite the `{{name}}` query. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`.', + docsUrl: 'https://on.cypress.io/api/custom-queries', }, invoking_child_without_parent (obj) { return stripIndent`\ @@ -1232,9 +1236,7 @@ export default { Variables must either be defined within the ${cmd('origin')} command or passed in using the args option. - Using \`require()\` or \`import()\` to include dependencies requires enabling the \`experimentalOriginDependencies\` flag and using the latest version of \`@cypress/webpack-preprocessor\`. - - Note: Using \`require()\` or \`import()\` within ${cmd('origin')} from a \`node_modules\` plugin is not currently supported.`, + Using \`require()\` or \`import()\` within the ${cmd('origin')} callback is not supported. Use ${cmd('Cypress.require')} to include dependencies instead, but note that it currently requires enabling the \`experimentalOriginDependencies\` flag.`, }, callback_mixes_sync_and_async: { message: stripIndent`\ @@ -1490,6 +1492,21 @@ export default { }, }, + require: { + invalid_inside_origin: { + message: `${cmd('Cypress.require')} is supposed to be replaced with a \`require()\` statement before the test code using it is run. If this error is being thrown, something unexpected has occurred. Please submit an issue.`, + docsUrl: 'https://on.cypress.io/origin', + }, + invalid_outside_origin: { + message: `${cmd('Cypress.require')} can only be used inside the ${cmd('origin')} callback and requires enabling the \`experimentalOriginDependencies\` flag.`, + docsUrl: 'https://on.cypress.io/origin', + }, + invalid_without_flag: { + message: `Using ${cmd('Cypress.require')} requires enabling the \`experimentalOriginDependencies\` flag.`, + docsUrl: 'https://on.cypress.io/origin', + }, + }, + route: { removed (obj) { return { diff --git a/packages/driver/src/cypress/stack_utils.ts b/packages/driver/src/cypress/stack_utils.ts index 1b838f9b6404..641179f995af 100644 --- a/packages/driver/src/cypress/stack_utils.ts +++ b/packages/driver/src/cypress/stack_utils.ts @@ -100,7 +100,7 @@ const stackWithUserInvocationStackSpliced = (err, userInvocationStack): StackAnd } } -type InvocationDetails = LineDetail | {} +type InvocationDetails = MessageLineDetail | {} const getInvocationDetails = (specWindow, config) => { if (specWindow.Error) { @@ -305,12 +305,12 @@ const stripCustomProtocol = (filePath) => { return filePath.replace(customProtocolRegex, '') } -type LineDetail = -{ +interface MessageLineDetail { message: any whitespace: any -} | -{ +} + +interface StackLineDetail { function: any fileUrl: any originalFile: any @@ -321,7 +321,7 @@ type LineDetail = whitespace: any } -const getSourceDetailsForLine = (projectRoot, line): LineDetail => { +const getSourceDetailsForLine = (projectRoot, line): MessageLineDetail | StackLineDetail => { const whitespace = getWhitespace(line) const generatedDetails = parseLine(line) @@ -382,7 +382,7 @@ const getSourceDetailsForFirstLine = (stack, projectRoot) => { if (!line) return - return getSourceDetailsForLine(projectRoot, line) + return getSourceDetailsForLine(projectRoot, line) as StackLineDetail } const reconstructStack = (parsedStack) => { diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 947962448157..0e05b81e5cd2 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -525,7 +525,7 @@ }, "experimentalOriginDependencies": { "name": "Origin Dependencies", - "description": "Enables support for `require`/`import` within `cy.origin`." + "description": "Enables support for `Cypress.require()` for including dependencies within the `cy.origin()` callback." }, "experimentalMemoryManagement": { "name": "Memory Management", diff --git a/packages/launcher/lib/utils.ts b/packages/launcher/lib/utils.ts index de07e4213fd0..c2e68a948361 100644 --- a/packages/launcher/lib/utils.ts +++ b/packages/launcher/lib/utils.ts @@ -42,7 +42,7 @@ export const utils = { let stdout = '' let stderr = '' - const proc = utils.spawnWithArch(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }) + const proc = utils.spawnWithArch(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env }) const finish = () => { proc.kill() diff --git a/packages/launcher/test/unit/darwin_spec.ts b/packages/launcher/test/unit/darwin_spec.ts index ee49925c47cc..d62101d27b09 100644 --- a/packages/launcher/test/unit/darwin_spec.ts +++ b/packages/launcher/test/unit/darwin_spec.ts @@ -90,6 +90,7 @@ describe('darwin browser detection', () => { context('forces correct architecture', () => { function stubForArch (arch: 'arm64' | 'x64') { + sinon.stub(process, 'env').value({ env2: 'false', env3: 'true' }) sinon.stub(os, 'arch').returns(arch) sinon.stub(os, 'platform').returns('darwin') getOutput.restore() @@ -111,6 +112,8 @@ describe('darwin browser detection', () => { expect(args[1]).to.deep.eq([knownBrowsers[0].binary, '--version']) expect(args[2].env).to.deep.include({ ARCHPREFERENCE: 'arm64,x86_64', + env2: 'false', + env3: 'true', }) }) @@ -125,7 +128,10 @@ describe('darwin browser detection', () => { expect(args[0]).to.eq(knownBrowsers[0].binary) expect(args[1]).to.deep.eq(['--version']) - expect(args[2].env).to.not.exist + expect(args[2].env).to.deep.include({ + env2: 'false', + env3: 'true', + }) }) }) @@ -133,7 +139,7 @@ describe('darwin browser detection', () => { it('uses arch and ARCHPREFERENCE on arm64', async () => { const cpSpawn = stubForArch('arm64') - await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true' }) + await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) const { args } = cpSpawn.getCall(0) @@ -142,13 +148,15 @@ describe('darwin browser detection', () => { expect(args[2].env).to.deep.include({ ARCHPREFERENCE: 'arm64,x86_64', env1: 'true', + env2: 'false', + env3: 'true', }) }) it('does not use `arch` on x64', async () => { const cpSpawn = stubForArch('x64') - await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true' }) + await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) const { args } = cpSpawn.getCall(0) @@ -156,6 +164,8 @@ describe('darwin browser detection', () => { expect(args[1]).to.deep.eq(['url', 'arg1']) expect(args[2].env).to.deep.include({ env1: 'true', + env2: 'false', + env3: 'true', }) expect(args[2].env).to.not.have.property('ARCHPREFERENCE') diff --git a/packages/server/lib/browsers/memory/index.ts b/packages/server/lib/browsers/memory/index.ts index 9bcddb6468a7..ded700422ddc 100644 --- a/packages/server/lib/browsers/memory/index.ts +++ b/packages/server/lib/browsers/memory/index.ts @@ -18,8 +18,10 @@ const debugVerbose = debugModule('cypress-verbose:server:browsers:memory') const MEMORY_THRESHOLD_PERCENTAGE = Number(process.env.CYPRESS_INTERNAL_MEMORY_THRESHOLD_PERCENTAGE) || 50 const MEMORY_PROFILER_INTERVAL = Number(process.env.CYPRESS_INTERNAL_MEMORY_PROFILER_INTERVAL) || 1000 const MEMORY_FOLDER = process.env.CYPRESS_INTERNAL_MEMORY_FOLDER_PATH || path.join('cypress', 'logs', 'memory') -const SAVE_MEMORY_STATS = ['1', 'true'].includes(process.env.CYPRESS_INTERNAL_MEMORY_SAVE_STATS?.toLowerCase() as string) -const SKIP_GC = ['1', 'true'].includes(process.env.CYPRESS_INTERNAL_MEMORY_SKIP_GC?.toLowerCase() as string) +const CYPRESS_INTERNAL_MEMORY_SAVE_STATS = process.env.CYPRESS_INTERNAL_MEMORY_SAVE_STATS || 'false' +const SAVE_MEMORY_STATS = ['1', 'true'].includes(CYPRESS_INTERNAL_MEMORY_SAVE_STATS.toLowerCase()) +const CYPRESS_INTERNAL_MEMORY_SKIP_GC = process.env.CYPRESS_INTERNAL_MEMORY_SKIP_GC || 'false' +const SKIP_GC = ['1', 'true'].includes(CYPRESS_INTERNAL_MEMORY_SKIP_GC.toLowerCase()) const KIBIBYTE = 1024 const FOUR_GIBIBYTES = 4 * (KIBIBYTE ** 3) diff --git a/packages/server/lib/cross-origin/process-callback.ts b/packages/server/lib/cross-origin/process-callback.ts new file mode 100644 index 000000000000..6db08cabeb01 --- /dev/null +++ b/packages/server/lib/cross-origin/process-callback.ts @@ -0,0 +1,67 @@ +import { getFullWebpackOptions } from '@cypress/webpack-batteries-included-preprocessor' +import md5 from 'md5' +import { fs } from 'memfs' +import * as path from 'path' +import webpack from 'webpack' + +const VirtualModulesPlugin = require('webpack-virtual-modules') + +interface Options { + file: string + fn: string +} + +// @ts-expect-error - webpack expects `fs.join` to exist for some reason +fs.join = path.join + +export const processCallback = ({ file, fn }: Options) => { + const source = fn.replace(/Cypress\.require/g, 'require') + const webpackOptions = getFullWebpackOptions(file, require.resolve('typescript')) + + const inputFileName = md5(source) + const inputDir = path.dirname(file) + const inputPath = path.join(inputDir, inputFileName) + const outputDir = '/' + const outputFileName = 'output' + const outputPath = `${outputDir}${outputFileName}.js` + + const modifiedWebpackOptions = { + ...webpackOptions, + entry: { + [outputFileName]: inputPath, + }, + output: { + path: outputDir, + }, + plugins: [ + new VirtualModulesPlugin({ + [inputPath]: source, + }), + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], + } + + const compiler = webpack(modifiedWebpackOptions) + + // @ts-expect-error + compiler.outputFileSystem = fs + + return new Promise((resolve, reject) => { + const handle = (err: Error) => { + if (err) { + return reject(err) + } + + // Using an in-memory file system, so the usual restrictions on sync + // methods don't apply, since this won't throw an EMFILE error + // eslint-disable-next-line no-restricted-syntax + const result = fs.readFileSync(outputPath).toString() + + resolve(result) + } + + compiler.run(handle) + }) +} diff --git a/packages/server/lib/experiments.ts b/packages/server/lib/experiments.ts index 3cc0bf87a0f8..078877a0f3ea 100644 --- a/packages/server/lib/experiments.ts +++ b/packages/server/lib/experiments.ts @@ -60,7 +60,7 @@ const _summaries: StringValues = { experimentalStudio: 'Generate and save commands directly to your test suite by interacting with your app as an end user would.', experimentalWebKitSupport: 'Adds support for testing in the WebKit browser engine used by Safari. See https://on.cypress.io/webkit-experiment for more information.', experimentalRunAllSpecs: 'Enables the "Run All Specs" UI feature, allowing the execution of multiple specs sequentially', - experimentalOriginDependencies: 'Enables support for `require`/`import` within `cy.origin`', + experimentalOriginDependencies: 'Enables support for `Cypress.require()` for including dependencies within the `cy.origin()` callback.', experimentalMemoryManagement: 'Enables support for improved memory management within Chromium-based browsers.', } diff --git a/packages/server/lib/plugins/child/run_plugins.js b/packages/server/lib/plugins/child/run_plugins.js index dc5e0564157b..4e7fee4afef1 100644 --- a/packages/server/lib/plugins/child/run_plugins.js +++ b/packages/server/lib/plugins/child/run_plugins.js @@ -126,14 +126,6 @@ class RunPlugins { .then((modifiedCfg) => { debug('plugins file successfully loaded') - // if the experimentalOriginDependencies flag is true, specify which - // commands should (potentially) have their callbacks replaced. this is - // currently used by the webpack preprocessor. see its implementation for - // more details - if (this._getExperimentalOriginDependenciesValue(initialConfig, modifiedCfg)) { - global.__cypressCallbackReplacementCommands = ['origin'] - } - this.ipc.send('setupTestingType:reply', { setupConfig: modifiedCfg, registrations: this.registrations, @@ -151,19 +143,6 @@ class RunPlugins { }) } - _getExperimentalOriginDependenciesValue (initialConfig, modifiedConfig) { - // prefer the modified value if it's specified - if ( - typeof modifiedConfig === 'object' - && typeof modifiedConfig.experimentalOriginDependencies === 'boolean' - ) { - return modifiedConfig.experimentalOriginDependencies - } - - // otherwise, use the initial value - return initialConfig.experimentalOriginDependencies - } - execute (event, ids, args = []) { debug(`execute plugin event: ${event} (%o)`, ids) diff --git a/packages/server/lib/routes-e2e.ts b/packages/server/lib/routes-e2e.ts index cadce9c5eec4..b75efd74208a 100644 --- a/packages/server/lib/routes-e2e.ts +++ b/packages/server/lib/routes-e2e.ts @@ -1,7 +1,8 @@ -import fs from 'fs-extra' -import path from 'path' +import bodyParser from 'body-parser' import Debug from 'debug' import { Router } from 'express' +import fs from 'fs-extra' +import path from 'path' import AppData from './util/app_data' import CacheBuster from './util/cache_buster' @@ -10,6 +11,7 @@ import reporter from './controllers/reporter' import client from './controllers/client' import files from './controllers/files' import type { InitializeRoutes } from './routes' +import { processCallback } from './cross-origin/process-callback' const debug = Debug('cypress:server:routes-e2e') @@ -17,6 +19,7 @@ export const createRoutesE2E = ({ config, networkProxy, onError, + getSpec, }: InitializeRoutes) => { const routesE2E = Router() @@ -49,6 +52,26 @@ export const createRoutesE2E = ({ } }) + routesE2E.post(`/${config.namespace}/process-origin-callback`, bodyParser.json(), async (req, res) => { + try { + const { file, fn } = req.body + + debug('process origin callback: %s', fn) + + const contents = await processCallback({ file, fn }) + + res.json({ contents }) + } catch (err) { + const errorMessage = `Processing the origin callback errored:\n\n${err.stack}` + + debug(errorMessage) + + res.json({ + error: errorMessage, + }) + } + }) + routesE2E.get(`/${config.namespace}/socket.io.js`, (req, res) => { client.handle(req, res) }) diff --git a/packages/server/package.json b/packages/server/package.json index f9b4642b03d8..f41b03e53c6e 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -86,6 +86,7 @@ "log-symbols": "2.2.0", "marionette-client": "https://github.com/cypress-io/marionette-client.git#5fc10cdf6c02627e9a2add98ca52de4d0c2fe74d", "md5": "2.3.0", + "memfs": "3.4.12", "mime": "2.4.4", "mime-db": "1.45.0", "minimatch": "3.0.5", @@ -123,6 +124,7 @@ "underscore.string": "3.3.6", "url-parse": "1.5.9", "uuid": "8.3.2", + "webpack-virtual-modules": "0.5.0", "widest-line": "3.1.0" }, "devDependencies": { diff --git a/scripts/binary/binary-cleanup.js b/scripts/binary/binary-cleanup.js index f652b629b4fd..507d35b6911d 100644 --- a/scripts/binary/binary-cleanup.js +++ b/scripts/binary/binary-cleanup.js @@ -40,7 +40,10 @@ const getDependencyPathsToKeep = async (buildAppDir) => { const startingEntryPoints = [ 'packages/server/lib/plugins/child/require_async_child.js', 'packages/server/lib/plugins/child/register_ts_node.js', + 'packages/server/node_modules/@cypress/webpack-batteries-included-preprocessor/index.js', + 'packages/server/node_modules/ts-loader/index.js', 'packages/rewriter/lib/threads/worker.js', + 'npm/webpack-batteries-included-preprocessor/index.js', 'node_modules/webpack/lib/webpack.js', 'node_modules/webpack-dev-server/lib/Server.js', 'node_modules/html-webpack-plugin-4/index.js', @@ -83,6 +86,7 @@ const getDependencyPathsToKeep = async (buildAppDir) => { 'pnpapi', '@swc/core', 'emitter', + 'ts-loader', ], }) @@ -128,10 +132,12 @@ const createServerEntryPointBundle = async (buildAppDir) => { ], }) + // eslint-disable-next-line no-console console.log(`copying server entry point bundle from ${path.join(workingDir, 'index.js')} to ${path.join(buildAppDir, 'packages', 'server', 'index.js')}`) await fs.copy(path.join(workingDir, 'index.js'), path.join(buildAppDir, 'packages', 'server', 'index.js')) + // eslint-disable-next-line no-console console.log(`compiling server entry point bundle to ${path.join(buildAppDir, 'packages', 'server', 'index.jsc')}`) // Use bytenode to compile the entry point bundle. This will save time on the v8 compile step and ensure the integrity of the entry point @@ -165,6 +171,7 @@ const buildEntryPointAndCleanup = async (buildAppDir) => { ...serverEntryPointBundleDependencies, ] + // eslint-disable-next-line no-console console.log(`potentially removing ${potentiallyRemovedDependencies.length} dependencies`) // 4. Remove all dependencies that are in the snapshot but not in the list of kept dependencies from the binary diff --git a/scripts/semantic-commits/get-binary-release-data.js b/scripts/semantic-commits/get-binary-release-data.js index 82651189e5c9..6e5d1f9dcf63 100644 --- a/scripts/semantic-commits/get-binary-release-data.js +++ b/scripts/semantic-commits/get-binary-release-data.js @@ -92,6 +92,20 @@ const getReleaseData = async (latestReleaseInfo) => { }) })) + console.log('Next release version is', nextVersion) + + console.log(`${prsInRelease.length} pull requests have merged since ${latestReleaseInfo.version} was released.`) + + prsInRelease.forEach((link) => { + console.log(' -', link) + }) + + console.log(`${issuesInRelease.length} issues addressed since ${latestReleaseInfo.version} was released.`) + + issuesInRelease.forEach((link) => { + console.log(' -', link) + }) + return { nextVersion, changedFiles, @@ -112,23 +126,5 @@ if (require.main !== module) { (async () => { const latestReleaseInfo = await getCurrentReleaseData() - const { - changelogData, - issuesInRelease, - prsInRelease, - } = await getReleaseData(latestReleaseInfo) - - console.log('Next release version is', changelogData.nextVersion) - - console.log(`${prsInRelease.length} user-facing pull requests have merged since ${latestReleaseInfo.version} was released.`) - - .prsInRelease.forEach((link) => { - console.log(' -', link) - }) - - console.log(`${issuesInRelease.length} user-facing issues addressed since ${latestReleaseInfo.version} was released.`) - - issuesInRelease.forEach((link) => { - console.log(' -', link) - }) + await getReleaseData(latestReleaseInfo) })() diff --git a/scripts/semantic-commits/get-current-release-data.js b/scripts/semantic-commits/get-current-release-data.js index d031c5160d4d..ad35bf15b5cc 100644 --- a/scripts/semantic-commits/get-current-release-data.js +++ b/scripts/semantic-commits/get-current-release-data.js @@ -7,17 +7,20 @@ const childProcess = require('child_process') const getCurrentReleaseData = (verbose = true) => { verbose && console.log('Get Current Release Information\n') - const stdout = childProcess.execSync('npm info cypress --json') - const npmInfo = JSON.parse(stdout) + const stdout = childProcess.execSync('yarn info cypress --json') + const { data: npmInfo } = JSON.parse(stdout) const latestReleaseInfo = { version: npmInfo['dist-tags'].latest, + distTags: npmInfo['dist-tags'], commitDate: npmInfo.buildInfo.commitDate, buildSha: npmInfo.buildInfo.commitSha, } verbose && console.log({ latestReleaseInfo }) + latestReleaseInfo.versions = npmInfo.versions + return latestReleaseInfo } diff --git a/scripts/semantic-commits/get-linked-issues.js b/scripts/semantic-commits/get-linked-issues.js index eff5191debb8..ebc95e6676b1 100644 --- a/scripts/semantic-commits/get-linked-issues.js +++ b/scripts/semantic-commits/get-linked-issues.js @@ -7,7 +7,7 @@ const getLinkedIssues = (body = '') => { // remove markdown comments body.replace(/()|()|(