diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 15701f41fac8..686e662911a3 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -28,7 +28,8 @@ mainBuildFilters: &mainBuildFilters only: - develop - /^release\/\d+\.\d+\.\d+$/ - - 'emily/next-version' + # 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 @@ -37,7 +38,8 @@ macWorkflowFilters: &darwin-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 >> @@ -46,7 +48,8 @@ linuxArm64WorkflowFilters: &linux-arm64-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 >> @@ -64,7 +67,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 >> @@ -130,7 +134,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "emily/next-version" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -201,7 +205,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 +1448,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/triage_add_to_project.yml b/.github/workflows/triage_add_to_project.yml index 4dcf519a2a17..0b1b5f16f0f0 100644 --- a/.github/workflows/triage_add_to_project.yml +++ b/.github/workflows/triage_add_to_project.yml @@ -1,6 +1,11 @@ name: 'Triage: add issue/PR to project' on: + # makes this workflow reusable + workflow_call: + secrets: + ADD_TO_TRIAGE_BOARD_TOKEN: + required: true issues: types: - opened @@ -12,9 +17,25 @@ jobs: add-to-triage-project: name: Add to triage project runs-on: ubuntu-latest + env: + PROJECT_NUMBER: 9 + GITHUB_TOKEN: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} steps: - - uses: actions/add-to-project@v0.3.0 + - name: is-collaborator + run: | + gh api graphql -f query=' + query($org: String!, $repo: String!, $user: String!) { + repository(owner: $org, name: $repo) { + collaborators(query: $user, first: 1) { + totalCount + } + } + } ' -f org=${{ github.repository_owner }} -f repo=${{ github.event.repository.name }} -f user=${{ github.actor }} > collaborators.json + + echo 'IS_COLLABORATOR='$(jq -r '.data.repository.collaborators.totalCount' collaborators.json) >> $GITHUB_ENV + - uses: actions/add-to-project@v0.4.0 + # only add issues/prs from outside contributors to the project + if: ${{ env.IS_COLLABORATOR == 0 }} with: - project-url: https://github.com/orgs/cypress-io/projects/9 - github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} - + project-url: https://github.com/orgs/${{github.repository_owner}}/projects/${{env.PROJECT_NUMBER}} + github-token: ${{ secrets.ADD_TO_TRIAGE_BOARD_TOKEN }} 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 11150a178c1c..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: @@ -33,9 +33,10 @@ jobs: platform: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} env: - CYPRESS_BOT_APP_ID: ${{ secrets.CYPRESS_BOT_APP_ID }} + 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 c5036166c8b0..2105ad9bd9d6 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,33 +1,61 @@ ## 13.0.0 -_Released 01/31/2023 (PENDING)_ +_Released 03/1/2023 (PENDING)_ **Breaking Changes:** - The [`cy.readFile()`](/api/commands/readfile) command is now retry-able as a [query command](https://on.cypress.io/retry-ability). This should not affect any tests using it; the functionality is unchanged. However, it can no longer be overwritten using [`Cypress.Commands.overwrite()`](/api/cypress-api/custom-commands#Overwrite-Existing-Commands). Addressed in [#25595](https://github.com/cypress-io/cypress/pull/25595). +**Features:** + +- It is now possible to set `hostOnly` cookies with [`cy.setCookie()`](https://docs.cypress.io/api/commands/setcookie) for a given domain. Addresses [#16856](https://github.com/cypress-io/cypress/issues/16856) and [#17527](https://github.com/cypress-io/cypress/issues/17527). +- Added a Public API for third party component libraries to define a Framework Definition, embedding their library into the Cypress onboarding workflow. Learn more [here](https://docs.cypress.io/guides/component-testing/third-party-definitions). Implemented in [#25780](https://github.com/cypress-io/cypress/pull/25780) and closes [#25638](https://github.com/cypress-io/cypress/issues/25638). + +**Bugfixes:** + +- Fixed an issue where cookies were being duplicated with the same hostname, but a prepended dot. Fixed an issue where cookies may not be expiring correctly. Fixes [#25174](https://github.com/cypress-io/cypress/issues/25174), [#25205](https://github.com/cypress-io/cypress/issues/25205) and [#25495](https://github.com/cypress-io/cypress/issues/25495). +- Fixed an issue where cookies weren't being synced when the application was stable. Fixed in [#25855](https://github.com/cypress-io/cypress/pull/25855). Fixes [#25835](https://github.com/cypress-io/cypress/issues/25835). +- Added missing TypeScript type definitions for the [`cy.reload()`](https://docs.cypress.io/api/commands/reload) command. Addressed in [#25779](https://github.com/cypress-io/cypress/pull/25779). +- Ensure Angular components are mounted inside the correct element. Fixes [#24385](https://github.com/cypress-io/cypress/issues/24385) +- Fix a bug where files outside the project root in a monorepo are not correctly served when using Vite. Addressed in [#25801](https://github.com/cypress-io/cypress/pull/25801) +- Fixed an issue where using [`cy.intercept`](https://docs.cypress.io/api/commands/intercept)'s `req.continue()` with a non-function parameter would not provide an appropriate error message. Fixed in [#25884](https://github.com/cypress-io/cypress/pull/25884). + +**Misc:** + + - Made updates to the way that the Debug Page header displays information. Addresses [#25796](https://github.com/cypress-io/cypress/issues/25796) and [#25798](https://github.com/cypress-io/cypress/issues/25798). + ## 12.6.0 -_Released 02/14/2023 (PENDING)_ +_Released 02/15/2023_ **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 ["Auto Cancellation"](https://docs.cypress.io/guides/cloud/smart-orchestration#Auto-Cancellation) value when recording to the Cloud. This gives Cloud users on Business and Enterprise plans the flexibility to alter the auto-cancellation value per run. Addressed in [#25237](https://github.com/cypress-io/cypress/pull/25237). +- It is now possible to overwrite query commands using [`Cypress.Commands.overwriteQuery`](https://on.cypress.io/api/custom-queries). Addressed in [#25078](https://github.com/cypress-io/cypress/issues/25078). +- Added [`Cypress.require()`](https://docs.cypress.io/api/cypress-api/require) for including dependencies within the [`cy.origin()`](https://docs.cypress.io/api/commands/origin) callback. This change removed support for using `require()` and `import()` directly within the callback because we found that it impacted performance not only for spec files using them within the [`cy.origin()`](https://docs.cypress.io/api/commands/origin) callback, but even for spec files that did not use them. Addresses [#24976](https://github.com/cypress-io/cypress/issues/24976). +- Added the ability to open the failing test in the IDE from the Debug page before needing to re-run the test. Addressed in [#24850](https://github.com/cypress-io/cypress/issues/24850). + +**Bugfixes:** + +- When a Cloud user is apart of multiple Cloud organizations, the [Connect to Cloud setup](https://docs.cypress.io/guides/cloud/projects#Set-up-a-project-to-record) now shows the correct organizational prompts when connecting a new project. Fixes [#25520](https://github.com/cypress-io/cypress/issues/25520). +- Fixed an issue where Cypress would fail to load any specs if the project `specPattern` included a resource that could not be accessed due to filesystem permissions. Fixes [#24109](https://github.com/cypress-io/cypress/issues/24109). +- Fixed an issue where the Debug page would display a different number of specs for in-progress runs than the in-progress specs reported in Cypress Cloud. Fixes [#25647](https://github.com/cypress-io/cypress/issues/25647). +- Fixed an issue in middleware where error-handling code could itself generate an error and fail to report the original issue. Fixes [#22825](https://github.com/cypress-io/cypress/issues/22825). +- Fixed an regression introduced in Cypress [12.3.0](#12-3-0) where custom browsers that relied on process environment variables were not found on macOS arm64 architectures. Fixed in [#25753](https://github.com/cypress-io/cypress/pull/25753). **Misc:** -- Improved the layout of the Debug Page on smaller viewports when there is a pending run. Addresses [#25664](https://github.com/cypress-io/cypress/issues/25664). -- Improved the layout of the Debug Page when displaying informational messages. Addresses [#25669](https://github.com/cypress-io/cypress/issues/25669). -- Icons in Debug page will no longer shrink at small viewports. Addresses [#25665](https://github.com/cypress-io/cypress/issues/25665). +- Improved the UI of the Debug page. Addresses [#25664](https://github.com/cypress-io/cypress/issues/25664), [#25669](https://github.com/cypress-io/cypress/issues/25669), [#25665](https://github.com/cypress-io/cypress/issues/25665), [#25666](https://github.com/cypress-io/cypress/issues/25666), and [#25667](https://github.com/cypress-io/cypress/issues/25667). +- Updated the Debug page sidebar badge to to show 0 to 99+ failing tests, increased from showing 0 to 9+ failing tests, to provide better test failure insights. Addresses [#25662](https://github.com/cypress-io/cypress/issues/25662). **Dependency Updates:** -- Upgrade [`debug`][(https://www.npmjs.com/package/debug) to `4.3.4`. Addressed in [#25699](https://github.com/cypress-io/cypress/pull/25699). +- Upgrade [`debug`](https://www.npmjs.com/package/debug) to `4.3.4`. Addressed in [#25699](https://github.com/cypress-io/cypress/pull/25699). ## 12.5.1 -_Released 02/10/2023_ +_Released 02/02/2023_ **Bugfixes:** diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index 69137521fbac..562cbeb7fdf7 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -67,29 +67,30 @@ exports['shows help for run --foo 1'] = ` Runs Cypress tests from the CLI without the GUI Options: - -b, --browser runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path. - --ci-build-id the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers - --component runs component tests - -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}. - -C, --config-file path to script file where configuration values are set. defaults to "cypress.config.{js,ts,mjs,cjs}". - --e2e runs end to end tests - -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs} or cypress.env.json - --group a named group for recorded runs in Cypress Cloud - -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. - --headed displays the browser instead of running headlessly - --headless hide the browser instead of running headed (default for cypress run) - --no-exit keep the browser open after tests finish - --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes - -p, --port runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}. - -P, --project path to the project - -q, --quiet run quietly, using only the configured reporter - --record [bool] records the run. sends test results, screenshots and videos to Cypress Cloud. - -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" - -o, --reporter-options options for the mocha reporter. defaults to "null" - -s, --spec runs specific spec file(s). defaults to "all" - -t, --tag named tag(s) for recorded runs in Cypress Cloud - --dev runs cypress in development and bypasses binary check - -h, --help display help for command + --auto-cancel-after-failures overrides the project-level Cloud configuration to set the failed test threshold for auto cancellation or to disable auto cancellation when recording to the Cloud + -b, --browser runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path. + --ci-build-id the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers + --component runs component tests + -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}. + -C, --config-file path to script file where configuration values are set. defaults to "cypress.config.{js,ts,mjs,cjs}". + --e2e runs end to end tests + -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs} or cypress.env.json + --group a named group for recorded runs in Cypress Cloud + -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. + --headed displays the browser instead of running headlessly + --headless hide the browser instead of running headed (default for cypress run) + --no-exit keep the browser open after tests finish + --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes + -p, --port runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}. + -P, --project path to the project + -q, --quiet run quietly, using only the configured reporter + --record [bool] records the run. sends test results, screenshots and videos to Cypress Cloud. + -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" + -o, --reporter-options options for the mocha reporter. defaults to "null" + -s, --spec runs specific spec file(s). defaults to "all" + -t, --tag named tag(s) for recorded runs in Cypress Cloud + --dev runs cypress in development and bypasses binary check + -h, --help display help for command ------- stderr: ------- diff --git a/cli/index.mjs b/cli/index.mjs index 7b616f65a344..dcf09178f398 100644 --- a/cli/index.mjs +++ b/cli/index.mjs @@ -8,6 +8,8 @@ export default cypress export const defineConfig = cypress.defineConfig +export const defineComponentFramework = cypress.defineComponentFramework + export const run = cypress.run export const open = cypress.open diff --git a/cli/lib/cli.js b/cli/lib/cli.js index ce34a1b696f3..04599c93fc0f 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -93,6 +93,7 @@ const parseVariableOpts = (fnArgs, args) => { } const descriptions = { + autoCancelAfterFailures: 'overrides the project-level Cloud configuration to set the failed test threshold for auto cancellation or to disable auto cancellation when recording to the Cloud', browser: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.', cacheClear: 'delete all cached binaries', cachePrune: 'deletes all cached binaries except for the version currently in use', @@ -242,6 +243,7 @@ const addCypressRunCommand = (program) => { .command('run') .usage('[options]') .description('Runs Cypress tests from the CLI without the GUI') + .option('--auto-cancel-after-failures ', text('autoCancelAfterFailures')) .option('-b, --browser ', text('browser')) .option('--ci-build-id ', text('ciBuildId')) .option('--component', text('component')) diff --git a/cli/lib/cypress.js b/cli/lib/cypress.js index 02348266aa54..ff140859ff22 100644 --- a/cli/lib/cypress.js +++ b/cli/lib/cypress.js @@ -87,6 +87,24 @@ const cypressModuleApi = { defineConfig (config) { return config }, + + /** + * Provides automatic code completion for Component Frameworks Definitions. + * While it's not strictly necessary for Cypress to parse your configuration, we + * recommend wrapping your Component Framework Definition object with `defineComponentFramework()` + * @example + * module.exports = defineComponentFramework({ + * type: 'cypress-ct-solid-js' + * // ... + * }) + * + * @see ../types/cypress-npm-api.d.ts + * @param {Cypress.ThirdPartyComponentFrameworkDefinition} config + * @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter + */ + defineComponentFramework (config) { + return config + }, } module.exports = cypressModuleApi diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index e070205901b1..5eedabc27b53 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -45,6 +45,10 @@ const processRunOptions = (options = {}) => { const args = ['--run-project', options.project] + if (options.autoCancelAfterFailures || options.autoCancelAfterFailures === 0 || options.autoCancelAfterFailures === false) { + args.push('--auto-cancel-after-failures', options.autoCancelAfterFailures) + } + if (options.browser) { args.push('--browser', options.browser) } diff --git a/cli/lib/util.js b/cli/lib/util.js index 1a5e54899018..30a4f763ef68 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -192,6 +192,7 @@ const dequote = (str) => { const parseOpts = (opts) => { opts = _.pick(opts, + 'autoCancelAfterFailures', 'browser', 'cachePath', 'cacheList', diff --git a/cli/test/lib/cli_spec.js b/cli/test/lib/cli_spec.js index e5d8506fe0a2..bf6ebf97cb02 100644 --- a/cli/test/lib/cli_spec.js +++ b/cli/test/lib/cli_spec.js @@ -493,6 +493,16 @@ describe('cli', () => { this.exec('run --ci-build-id "123" --group "staging"') expect(run.start).to.be.calledWith({ ciBuildId: '123', group: 'staging' }) }) + + it('call run with --auto-cancel-after-failures', () => { + this.exec('run --auto-cancel-after-failures 4') + expect(run.start).to.be.calledWith({ autoCancelAfterFailures: '4' }) + }) + + it('call run with --auto-cancel-after-failures with false', () => { + this.exec('run --auto-cancel-after-failures false') + expect(run.start).to.be.calledWith({ autoCancelAfterFailures: 'false' }) + }) }) context('cypress open', () => { diff --git a/cli/test/lib/cypress_spec.js b/cli/test/lib/cypress_spec.js index 44579d7ac926..0ed0dd9ef0ea 100644 --- a/cli/test/lib/cypress_spec.js +++ b/cli/test/lib/cypress_spec.js @@ -100,10 +100,27 @@ describe('cypress', function () { } it('calls run#start, passing in options', () => { - return cypress.run({ spec: 'foo' }) + return cypress.run({ spec: 'foo', autoCancelAfterFailures: 4 }) .then(getStartArgs) .then((args) => { expect(args.spec).to.equal('foo') + expect(args.autoCancelAfterFailures).to.equal(4) + }) + }) + + it('calls run#start, passing in autoCancelAfterFailures false', () => { + return cypress.run({ autoCancelAfterFailures: false }) + .then(getStartArgs) + .then((args) => { + expect(args.autoCancelAfterFailures).to.equal(false) + }) + }) + + it('calls run#start, passing in autoCancelAfterFailures 0', () => { + return cypress.run({ autoCancelAfterFailures: 0 }) + .then(getStartArgs) + .then((args) => { + expect(args.autoCancelAfterFailures).to.equal(0) }) }) diff --git a/cli/test/lib/exec/run_spec.js b/cli/test/lib/exec/run_spec.js index a79d1a6e0067..312d77791f3f 100644 --- a/cli/test/lib/exec/run_spec.js +++ b/cli/test/lib/exec/run_spec.js @@ -216,5 +216,23 @@ describe('exec run', function () { ]) }) }) + + it('spawns with --auto-cancel-after-failures value', function () { + return run.start({ autoCancelAfterFailures: 4 }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--auto-cancel-after-failures', 4, + ]) + }) + }) + + it('spawns with --auto-cancel-after-failures value false', function () { + return run.start({ autoCancelAfterFailures: false }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--auto-cancel-after-failures', false, + ]) + }) + }) }) }) diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index a7051b7f1462..41a38abfdacd 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -95,6 +95,10 @@ declare namespace CypressCommandLine { * Specify the specs to run */ spec: string + /** + * Specify the number of failures to cancel a run being recorded to the Cloud or false to disable auto-cancellation. + */ + autoCancelAfterFailures: number | false } /** @@ -393,6 +397,21 @@ declare module 'cypress' { * @returns {Cypress.ConfigOptions} the configuration passed in parameter */ defineConfig(config: Cypress.ConfigOptions): Cypress.ConfigOptions + + /** + * Provides automatic code completion for Component Frameworks Definitions. + * While it's not strictly necessary for Cypress to parse your configuration, we + * recommend wrapping your Component Framework Definition object with `defineComponentFramework()` + * @example + * module.exports = defineComponentFramework({ + * type: 'cypress-ct-solid-js' + * }) + * + * @see ../types/cypress-npm-api.d.ts + * @param {Cypress.ThirdPartyComponentFrameworkDefinition} config + * @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter + */ + defineComponentFramework(config: Cypress.ThirdPartyComponentFrameworkDefinition): Cypress.ThirdPartyComponentFrameworkDefinition } // export Cypress NPM module interface diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 3dfaab987170..d381933ace47 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 @@ -1801,9 +1816,21 @@ declare namespace Cypress { * * @see https://on.cypress.io/reload * @example + * cy.visit('http://localhost:3000/admin') * cy.reload() */ - reload(options?: Partial): Chainable + reload(): Chainable + /** + * Reload the page. + * + * @see https://on.cypress.io/reload + * @param {Partial} options Pass in an options object to modify the default behavior of cy.reload() + * @example + * // Reload the page, do not log it in the command log and timeout after 15s + * cy.visit('http://localhost:3000/admin') + * cy.reload({log: false, timeout: 15000}) + */ + reload(options: Partial): Chainable /** * Reload the page without cache * @@ -1815,6 +1842,18 @@ declare namespace Cypress { * cy.reload(true) */ reload(forceReload: boolean): Chainable + /** + * Reload the page without cache and with log and timeout options + * + * @see https://on.cypress.io/reload + * @param {Boolean} forceReload Whether to reload the current page without using the cache. true forces the reload without cache. + * @param {Partial} options Pass in an options object to modify the default behavior of cy.reload() + * @example + * // Reload the page without using the cache, do not log it in the command log and timeout after 15s + * cy.visit('http://localhost:3000/admin') + * cy.reload(true, {log: false, timeout: 15000}) + */ + reload(forceReload: boolean, options: Partial): Chainable /** * Make an HTTP GET request. @@ -3126,7 +3165,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 @@ -3244,6 +3283,179 @@ declare namespace Cypress { type PickConfigOpt = T extends keyof DefineDevServerConfig ? DefineDevServerConfig[T] : any + interface DependencyToInstall { + dependency: CypressComponentDependency + satisfied: boolean + loc: string | null + detectedVersion: string | null + } + + interface CypressComponentDependency { + /** + * Unique idenitifer. + * @example 'reactscripts' + */ + type: string + + /** + * Name to display in the user interface. + * @example "React Scripts" + */ + name: string + + /** + * Package name on npm. + * @example react-scripts + */ + package: string + + /** + * Code to run when installing. Version is optional. + * + * Should be @. + * + * @example `react` + * @example `react@18` + * @example `react-scripts` + */ + installer: string + + /** + * Description shown in UI. It is recommended to use the same one the package uses on npm. + * @example 'Create React apps with no build configuration' + */ + description: string + + /** + * Minimum version supported. Should conform to Semantic Versioning as used in `package.json`. + * @see https://docs.npmjs.com/cli/v9/configuring-npm/package-json#dependencies + * @example '^=4.0.0 || ^=5.0.0' + * @example '^2.0.0' + */ + minVersion: string + } + + interface ResolvedComponentFrameworkDefinition { + /** + * A semantic, unique identifier. + * Must begin with `cypress-ct-` or `@org/cypress-ct-` for third party implementations. + * @example 'reactscripts' + * @example 'nextjs' + * @example 'cypress-ct-solid-js' + */ + type: string + + /** + * Used as the flag for `getPreset` for meta framworks, such as finding the webpack config for CRA, Angular, etc. + * It is also the name of the string added to `cypress.config` + * + * @example + * export default { + * component: { + * devServer: { + * framework: 'create-react-app' // can be 'next', 'create-react-app', etc etc. + * } + * } + * } + */ + configFramework: string + + /** + * Library (React, Vue) or template (aka "meta framework") (CRA, Next.js, Angular) + */ + category: 'library' | 'template' + + /** + * Name displayed in Launchpad when doing initial setup. + * @example 'Solid.js' + * @example 'Create React App' + */ + name: string + + /** + * Supported bundlers. + */ + supportedBundlers: Array<'webpack' | 'vite'> + + /** + * Used to attempt to automatically select the correct framework/bundler from the dropdown. + * + * @example + * const SOLID_DETECTOR: Dependency = { + * type: 'solid', + * name: 'Solid.js', + * package: 'solid-js', + * installer: 'solid-js', + * description: 'Solid is a declarative JavaScript library for creating user interfaces', + * minVersion: '^1.0.0', + * } + */ + detectors: CypressComponentDependency[] + + /** + * Array of required dependencies. This could be the bundler and JavaScript library. + */ + dependencies: (bundler: 'webpack' | 'vite', projectPath: string) => Promise + + /** + * This is used interally by Cypress for the "Create From Component" feature. + */ + codeGenFramework?: 'react' | 'vue' | 'svelte' | 'angular' + + /** + * This is used interally by Cypress for the "Create From Component" feature. + * @example '*.{js,jsx,tsx}' + */ + glob?: string + + /** + * This is the path to get mount, eg `import { mount } from , + * @example: `cypress-ct-solidjs/src/mount` + */ + mountModule: (projectPath: string) => Promise + + /** + * Support status. Internally alpha | beta | full. + * Community integrations are "community". + */ + supportStatus: 'alpha' | 'beta' | 'full' | 'community' + + /** + * Function returning string for used for the component-index.html file. + * Cypress provides a default if one isn't specified for third party integrations. + */ + componentIndexHtml?: () => string + + /** + * Used for the Create From Comopnent feature. + * This is currently not supported for third party frameworks. + */ + specPattern?: '**/*.cy.ts' + } + + type ComponentFrameworkDefinition = Omit & { + dependencies: (bundler: 'webpack' | 'vite') => CypressComponentDependency[] + } + + /** + * Certain properties are not supported for third party frameworks right now, + * such as ones related to the "Create From" feature. This is a subset of + * properties that are exposed for public usage. + */ + + type ThirdPartyComponentFrameworkDefinition = Pick & { + /** + * @example `cypress-ct-${string} for third parties. Any string is valid internally. + */ + type: string + + /** + * Raw SVG icon that will be displayed in the Project Setup Wizard. Used for third parties that + * want to render a custom icon. + */ + icon?: string + } + interface AngularDevServerProjectConfig { root: string sourceRoot: string @@ -3530,12 +3742,49 @@ declare namespace Cypress { action: 'select' | 'drag-drop' } + /** + * Options that control how the `cy.setCookie` command + * sets the cookie in the browser. + * @see https://on.cypress.io/setcookie#Arguments + */ interface SetCookieOptions extends Loggable, Timeoutable { + /** + * The path of the cookie. + * @default "/" + */ path: string + /** + * Represents the domain the cookie belongs to (e.g. "docs.cypress.io", "github.com"). + * @default location.hostname + */ domain: string + /** + * Whether a cookie's scope is limited to secure channels, such as HTTPS. + * @default false + */ secure: boolean + /** + * Whether or not the cookie is HttpOnly, meaning the cookie is inaccessible to client-side scripts. + * The Cypress cookie API has access to HttpOnly cookies. + * @default false + */ httpOnly: boolean + /** + * Whether or not the cookie is a host-only cookie, meaning the request's host must exactly match the domain of the cookie. + * @default false + */ + hostOnly: boolean + /** + * The cookie's expiry time, specified in seconds since Unix Epoch. + * The default is expiry is 20 years in the future from current time. + */ expiry: number + /** + * The cookie's SameSite value. If set, should be one of `lax`, `strict`, or `no_restriction`. + * `no_restriction` is the equivalent of `SameSite=None`. Pass `undefined` to use the browser's default. + * Note: `no_restriction` can only be used if the secure flag is set to `true`. + * @default undefined + */ sameSite: SameSiteStatus } @@ -5776,6 +6025,7 @@ declare namespace Cypress { specPattern?: string[] system: SystemDetails tag?: string + autoCancelAfterFailures?: number | false } interface DevServerConfig { @@ -6064,6 +6314,7 @@ declare namespace Cypress { value: string path: string domain: string + hostOnly?: boolean httpOnly: boolean secure: boolean expiry?: number diff --git a/cli/types/tests/cypress-npm-api-test.ts b/cli/types/tests/cypress-npm-api-test.ts index 3a0feebd7df6..ba04ed7a5802 100644 --- a/cli/types/tests/cypress-npm-api-test.ts +++ b/cli/types/tests/cypress-npm-api-test.ts @@ -1,6 +1,6 @@ // type tests for Cypress NPM module // https://on.cypress.io/module-api -import cypress, { defineConfig } from 'cypress' +import cypress, { defineComponentFramework, defineConfig } from 'cypress' cypress.run // $ExpectType (options?: Partial | undefined) => Promise cypress.open // $ExpectType (options?: Partial | undefined) => Promise @@ -55,6 +55,32 @@ const config = defineConfig({ modifyObstructiveCode: true }) +const solid = { + type: 'solid-js', + name: 'Solid.js', + package: 'solid-js', + installer: 'solid-js', + description: 'Solid is a declarative JavaScript library for creating user interfaces', + minVersion: '^1.0.0' +} + +const thirdPartyFrameworkDefinition = defineComponentFramework({ + type: 'cypress-ct-third-party', + name: 'Third Party', + dependencies: (bundler) => [solid], + detectors: [solid], + supportedBundlers: ['vite', 'webpack'], + icon: '...' +}) + +const thirdPartyFrameworkDefinitionInvalidStrings = defineComponentFramework({ + type: 'cypress-ct-third-party', + name: 'Third Party', + dependencies: (bundler) => [], + detectors: [{}], // $ExpectError + supportedBundlers: ['metro', 'webpack'] // $ExpectError +}) + // component options const componentConfigNextWebpack: Cypress.ConfigOptions = { component: { diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index ec6bb1b9facc..4b036baa10e5 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -1063,6 +1063,14 @@ namespace CypressSetCookieTests { expiry: 12345, sameSite: 'lax', }) + cy.setCookie('name', 'value', { + domain: 'www.foobar.com', + path: '/', + secure: false, + httpOnly: false, + hostOnly: true, + sameSite: 'lax', + }) cy.setCookie('name', 'value', { log: true, timeout: 10, domain: 'localhost' }) cy.setCookie('name') // $ExpectError @@ -1164,3 +1172,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/angular/CHANGELOG.md b/npm/angular/CHANGELOG.md index f81c22d0854c..5e04f5bb572a 100644 --- a/npm/angular/CHANGELOG.md +++ b/npm/angular/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/angular-v2.0.2](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.1...@cypress/angular-v2.0.2) (2023-02-17) + + +### Bug Fixes + +* mount component in [data-cy-root] ([#25807](https://github.com/cypress-io/cypress/issues/25807)) ([104eef5](https://github.com/cypress-io/cypress/commit/104eef5dfb4b619a748e7ddd59534eadb1044ae7)) + # [@cypress/angular-v2.0.1](https://github.com/cypress-io/cypress/compare/@cypress/angular-v2.0.0...@cypress/angular-v2.0.1) (2022-11-08) diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index a0be116911b2..be8c85294eea 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -14,6 +14,7 @@ import { getTestBed, TestModuleMetadata, TestBed, + TestComponentRenderer, } from '@angular/core/testing' import { BrowserDynamicTestingModule, @@ -21,6 +22,7 @@ import { } from '@angular/platform-browser-dynamic/testing' import { setupHooks, + getContainerEl, } from '@cypress/mount-utils' /** @@ -169,6 +171,22 @@ function bootstrapModule ( return testModuleMetaData } +@Injectable() +export class CypressTestComponentRenderer extends TestComponentRenderer { + override insertRootElement (rootElId: string) { + this.removeAllRootElements() + + const rootElement = getContainerEl() + + rootElement.setAttribute('id', rootElId) + document.body.appendChild(rootElement) + } + + override removeAllRootElements () { + getContainerEl().innerHTML = '' + } +} + /** * Initializes the TestBed * @@ -186,6 +204,8 @@ function initTestBed ( ...bootstrapModule(componentFixture, config), }) + getTestBed().overrideProvider(TestComponentRenderer, { useValue: new CypressTestComponentRenderer() }) + return componentFixture } diff --git a/npm/vite-dev-server/CHANGELOG.md b/npm/vite-dev-server/CHANGELOG.md index 2676351fe72c..baebaca46183 100644 --- a/npm/vite-dev-server/CHANGELOG.md +++ b/npm/vite-dev-server/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/vite-dev-server-v5.0.3](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v5.0.2...@cypress/vite-dev-server-v5.0.3) (2023-02-17) + + +### Bug Fixes + +* allow running tests outside Vite project root folder ([#25801](https://github.com/cypress-io/cypress/issues/25801)) ([d54fa65](https://github.com/cypress-io/cypress/commit/d54fa65f587da2b86c8d3140f44c653888fb62ee)) + # [@cypress/vite-dev-server-v5.0.2](https://github.com/cypress-io/cypress/compare/@cypress/vite-dev-server-v5.0.1...@cypress/vite-dev-server-v5.0.2) (2022-12-09) diff --git a/npm/vite-dev-server/client/initCypressTests.js b/npm/vite-dev-server/client/initCypressTests.js index 4406c9ab636d..57b983ca8fd2 100644 --- a/npm/vite-dev-server/client/initCypressTests.js +++ b/npm/vite-dev-server/client/initCypressTests.js @@ -32,13 +32,17 @@ if (supportFile) { }) } +// Using relative path wouldn't allow to load tests outside Vite project root folder +// So we use the "@fs" bit to load the test file using its absolute path +const testFileAbsolutePathRoute = `${devServerPublicPathRoute}/@fs${CypressInstance.spec.absolute}` + /* Spec file import logic */ // We need a slash before /src/my-spec.js, this does not happen by default. importsToLoad.push({ - load: () => import(`${devServerPublicPathRoute}/${CypressInstance.spec.relative}`), + load: () => import(testFileAbsolutePathRoute), absolute: CypressInstance.spec.absolute, relative: CypressInstance.spec.relative, - relativeUrl: `${devServerPublicPathRoute}/${CypressInstance.spec.relative}`, + relativeUrl: testFileAbsolutePathRoute, }) if (!CypressInstance) { diff --git a/npm/webpack-batteries-included-preprocessor/CHANGELOG.md b/npm/webpack-batteries-included-preprocessor/CHANGELOG.md index b5df0a53bb14..b9e2d153cf63 100644 --- a/npm/webpack-batteries-included-preprocessor/CHANGELOG.md +++ b/npm/webpack-batteries-included-preprocessor/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/webpack-batteries-included-preprocessor-v2.4.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-batteries-included-preprocessor-v2.3.0...@cypress/webpack-batteries-included-preprocessor-v2.4.0) (2023-02-15) + + +### Features + +* Bundle cy.origin() dependencies at runtime ([#25626](https://github.com/cypress-io/cypress/issues/25626)) ([41512c4](https://github.com/cypress-io/cypress/commit/41512c416a80e5158752fef9ffbe722402a5ada4)) + # [@cypress/webpack-batteries-included-preprocessor-v2.3.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-batteries-included-preprocessor-v2.2.4...@cypress/webpack-batteries-included-preprocessor-v2.3.0) (2022-12-02) 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-dev-server/CHANGELOG.md b/npm/webpack-dev-server/CHANGELOG.md index 9b09eaa932c6..bf0bc985bdde 100644 --- a/npm/webpack-dev-server/CHANGELOG.md +++ b/npm/webpack-dev-server/CHANGELOG.md @@ -1,3 +1,17 @@ +# [@cypress/webpack-dev-server-v3.3.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.2.4...@cypress/webpack-dev-server-v3.3.0) (2023-02-21) + + +### Features + +* Public API for CT Framework Definitions ([#25780](https://github.com/cypress-io/cypress/issues/25780)) ([1d3aab9](https://github.com/cypress-io/cypress/commit/1d3aab9d70acbce6d3571ab5b9df771f1c455964)), closes [#25713](https://github.com/cypress-io/cypress/issues/25713) + +# [@cypress/webpack-dev-server-v3.2.4](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.2.3...@cypress/webpack-dev-server-v3.2.4) (2023-02-20) + + +### Bug Fixes + +* **webpack-dev-server:** touch component-index during onSpecsChange to avoid writing to app file ([#25861](https://github.com/cypress-io/cypress/issues/25861)) ([87816de](https://github.com/cypress-io/cypress/commit/87816de1b7c4c3873ad791d71ac2af5aaa88e889)) + # [@cypress/webpack-dev-server-v3.2.3](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.2.2...@cypress/webpack-dev-server-v3.2.3) (2023-01-24) diff --git a/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts b/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts index 4f587e24b6f1..06e85369ea81 100644 --- a/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts @@ -39,4 +39,22 @@ describe('Config options', () => { expect(verifyFile).to.eq('OK') }) }) + + it('recompiles with new spec and custom indexHtmlFile', () => { + cy.scaffoldProject('webpack5_wds4-react') + cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-dev-server-custom-index.config.ts']) + cy.startAppServer('component') + + cy.visitApp() + + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject( + ctx.path.join('src', 'New.cy.js'), + await ctx.file.readFileInProject(ctx.path.join('src', 'App.cy.jsx')), + ) + }) + + cy.contains('New.cy.js').click() + cy.waitForSpecToFinish({ passCount: 2 }) + }) }) diff --git a/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts b/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts index 8cb128b2bbca..9aef4e5aa676 100644 --- a/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts +++ b/npm/webpack-dev-server/src/CypressCTWebpackPlugin.ts @@ -13,6 +13,7 @@ export interface CypressCTWebpackPluginOptions { supportFile: string | false devServerEvents: EventEmitter webpack: Function + indexHtmlFile: string } export type CypressCTContextOptions = Omit @@ -47,6 +48,7 @@ export class CypressCTWebpackPlugin { private supportFile: string | false private compilation: Webpack45Compilation | null = null private webpack: Function + private indexHtmlFile: string private readonly projectRoot: string private readonly devServerEvents: EventEmitter @@ -57,6 +59,7 @@ export class CypressCTWebpackPlugin { this.projectRoot = options.projectRoot this.devServerEvents = options.devServerEvents this.webpack = options.webpack + this.indexHtmlFile = options.indexHtmlFile } private addLoaderContext = (loaderContext: object, module: any) => { @@ -64,6 +67,7 @@ export class CypressCTWebpackPlugin { files: this.files, projectRoot: this.projectRoot, supportFile: this.supportFile, + indexHtmlFile: this.indexHtmlFile, } }; @@ -93,11 +97,16 @@ export class CypressCTWebpackPlugin { } /* - * `webpack --watch` watches the existing specs and their dependencies for changes, - * but we also need to add additional dependencies to our dynamic "browser.js" (generated - * using loader.ts) when new specs are created. This hook informs webpack that browser.js - * has been "updated on disk", causing a recompliation (and pulling the new specs in as - * dependencies). + * `webpack --watch` watches the existing specs and their dependencies for changes. + * When new specs are created, we need to trigger a recompilation to add the new specs + * as dependencies. This hook informs webpack that `component-index.html` has been "updated on disk", + * causing a recompilation (and pulling the new specs in as dependencies). We use the component + * index file because we know that it will be there since the project is using Component Testing. + * + * We were using `browser.js` before to cause a recompilation but we ran into an + * issue with MacOS Ventura that will not allow us to write to files inside of our application bundle. + * + * See https://github.com/cypress-io/cypress/issues/24398 */ private onSpecsChange = async (specs: Cypress.Cypress['spec'][]) => { if (!this.compilation || _.isEqual(specs, this.files)) { @@ -110,7 +119,7 @@ export class CypressCTWebpackPlugin { // eslint-disable-next-line no-restricted-syntax const utimesSync: UtimesSync = inputFileSystem.fileSystem.utimesSync ?? fs.utimesSync - utimesSync(path.resolve(__dirname, 'browser.js'), new Date(), new Date()) + utimesSync(path.join(this.projectRoot, this.indexHtmlFile), new Date(), new Date()) } /** diff --git a/npm/webpack-dev-server/src/devServer.ts b/npm/webpack-dev-server/src/devServer.ts index e7cabad50c17..675328144e22 100644 --- a/npm/webpack-dev-server/src/devServer.ts +++ b/npm/webpack-dev-server/src/devServer.ts @@ -114,7 +114,25 @@ export type PresetHandlerResult = { frameworkConfig: Configuration, sourceWebpac type Optional = Pick, K> & Omit +const thirdPartyDefinitionPrefixes = { + // matches @org/cypress-ct-* + namespacedPrefixRe: /^@.+?\/cypress-ct-.+/, + globalPrefix: 'cypress-ct-', +} + +export function isThirdPartyDefinition (framework: string) { + return framework.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) || + thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(framework) +} + async function getPreset (devServerConfig: WebpackDevServerConfig): Promise> { + const defaultWebpackModules = () => ({ sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig) }) + + // Third party library (eg solid-js, lit, etc) + if (devServerConfig.framework && isThirdPartyDefinition(devServerConfig.framework)) { + return defaultWebpackModules() + } + switch (devServerConfig.framework) { case 'create-react-app': return createReactAppHandler(devServerConfig) @@ -134,7 +152,7 @@ async function getPreset (devServerConfig: WebpackDevServerConfig): Promise { @@ -170,7 +170,7 @@ describe('#devServer', () => { await closeServer(close) }) - it('touches browser.js when a spec file is added and recompile', async function () { + it('touches component index when a spec file is added and recompile', async function () { // File watching only enabled when running in `open` mode cypressConfig.isTextTerminal = false const devServerEvents = new EventEmitter() @@ -187,13 +187,13 @@ describe('#devServer', () => { absolute: `${root}/test/fixtures/bar.spec.js`, } - const oldmtime = fs.statSync('./dist/browser.js').mtimeMs + const oldmtime = fs.statSync(cypressConfig.indexHtmlFile).mtimeMs await once(devServerEvents, 'dev-server:compile:success') devServerEvents.emit('dev-server:specs:changed', [newSpec]) await once(devServerEvents, 'dev-server:compile:success') - const updatedmtime = fs.statSync('./dist/browser.js').mtimeMs + const updatedmtime = fs.statSync(cypressConfig.indexHtmlFile).mtimeMs expect(oldmtime).to.not.equal(updatedmtime) diff --git a/npm/webpack-preprocessor/CHANGELOG.md b/npm/webpack-preprocessor/CHANGELOG.md index ea3f01d1b8eb..02be04fbf2ae 100644 --- a/npm/webpack-preprocessor/CHANGELOG.md +++ b/npm/webpack-preprocessor/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/webpack-preprocessor-v5.17.0](https://github.com/cypress-io/cypress/compare/@cypress/webpack-preprocessor-v5.16.3...@cypress/webpack-preprocessor-v5.17.0) (2023-02-15) + + +### Features + +* Bundle cy.origin() dependencies at runtime ([#25626](https://github.com/cypress-io/cypress/issues/25626)) ([41512c4](https://github.com/cypress-io/cypress/commit/41512c416a80e5158752fef9ffbe722402a5ada4)) + # [@cypress/webpack-preprocessor-v5.16.3](https://github.com/cypress-io/cypress/compare/@cypress/webpack-preprocessor-v5.16.2...@cypress/webpack-preprocessor-v5.16.3) (2023-02-06) # [@cypress/webpack-preprocessor-v5.16.2](https://github.com/cypress-io/cypress/compare/@cypress/webpack-preprocessor-v5.16.1...@cypress/webpack-preprocessor-v5.16.2) (2023-02-02) 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/package.json b/package.json index 1c9ab667368b..605285f3023e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "12.5.1", + "version": "12.6.0", "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": { diff --git a/packages/app/cypress/e2e/debug.cy.ts b/packages/app/cypress/e2e/debug.cy.ts index b39c4ca19ddd..76fce07b3ac0 100644 --- a/packages/app/cypress/e2e/debug.cy.ts +++ b/packages/app/cypress/e2e/debug.cy.ts @@ -77,7 +77,7 @@ describe('App - Debug Page', () => { cy.findByTestId('debug-header-branch').contains('main') cy.findByTestId('debug-header-commitHash').contains('e9d176f') cy.findByTestId('debug-header-author').contains('Lachlan Miller') - cy.findByTestId('debug-header-createdAt').contains('01:18') + cy.findByTestId('debug-header-createdAt').contains('02h 00m 10s') }) cy.findByTestId('debug-passed').contains('Well Done!') @@ -148,7 +148,7 @@ describe('App - Debug Page', () => { cy.findByTestId('debug-header-branch').contains('main') cy.findByTestId('debug-header-commitHash').contains('commit1') cy.findByTestId('debug-header-author').contains('Lachlan Miller') - cy.findByTestId('debug-header-createdAt').contains('00:19') + cy.findByTestId('debug-header-createdAt').contains('00m 19s') }) cy.findByTestId('spec-contents').within(() => { 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/app/cypress/e2e/runner/reporter-ct-mount-hover.cy.ts b/packages/app/cypress/e2e/runner/reporter-ct-mount-hover.cy.ts index e48055f7e349..4c4190036b10 100644 --- a/packages/app/cypress/e2e/runner/reporter-ct-mount-hover.cy.ts +++ b/packages/app/cypress/e2e/runner/reporter-ct-mount-hover.cy.ts @@ -42,13 +42,9 @@ for (const { projectName, test } of PROJECTS) { cy.contains(`${test}`).click() cy.waitForSpecToFinish(undefined) cy.get('.command.command-name-mount > .command-wrapper').click().then(() => { - if (`${projectName}` === 'angular-14') { - cy.get('iframe.aut-iframe').its('0.contentDocument.body').children().should('have.length.at.least', 2) - } else { - cy.get('iframe.aut-iframe').its('0.contentDocument.body').then(cy.wrap).within(() => { - cy.get('[data-cy-root]').children().should('have.length.at.least', 1) - }) - } + cy.get('iframe.aut-iframe').its('0.contentDocument.body').then(cy.wrap).within(() => { + cy.get('[data-cy-root]').children().should('have.length.at.least', 1) + }) }) } }) diff --git a/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json b/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json index b3af1afc3f90..c522ac0f0f98 100644 --- a/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json +++ b/packages/app/cypress/fixtures/debug-Failing/gql-Debug.json @@ -14,6 +14,7 @@ "commitInfo": { "sha": "commit1", "authorName": "Lachlan Miller", + "authorEmail": "hello@cypress.io", "summary": "chore: testing cypress", "branch": "main", "__typename": "CloudRunCommitInfo" diff --git a/packages/app/cypress/fixtures/debug-Passing/gql-Debug.json b/packages/app/cypress/fixtures/debug-Passing/gql-Debug.json index 770ddfb7f052..82ce76f8c171 100644 --- a/packages/app/cypress/fixtures/debug-Passing/gql-Debug.json +++ b/packages/app/cypress/fixtures/debug-Passing/gql-Debug.json @@ -10,10 +10,11 @@ "runNumber": 2, "createdAt": "2023-01-30T08:10:59.720Z", "status": "PASSED", - "totalDuration": 78898, + "totalDuration": 7210000, "commitInfo": { "sha": "e9d176f0c00c0428c9945577aec37cb6d48c5a26", "authorName": "Lachlan Miller", + "authorEmail": "asdf", "summary": "update projectId", "branch": "main", "__typename": "CloudRunCommitInfo" diff --git a/packages/app/src/debug/DebugArtifactLink.cy.tsx b/packages/app/src/debug/DebugArtifactLink.cy.tsx index 919abc703d88..196da01d36ce 100644 --- a/packages/app/src/debug/DebugArtifactLink.cy.tsx +++ b/packages/app/src/debug/DebugArtifactLink.cy.tsx @@ -11,7 +11,7 @@ describe('', () => { it('mounts correctly, provides expected tooltip content, and emits correct event', () => { artifactMapping.forEach((artifact) => { cy.mount(() => ( - + )) cy.findByTestId(`artifact-for-${artifact.icon}`).should('have.length', 1) diff --git a/packages/app/src/debug/DebugArtifactLink.vue b/packages/app/src/debug/DebugArtifactLink.vue index 635705840128..b1fa3cc31160 100644 --- a/packages/app/src/debug/DebugArtifactLink.vue +++ b/packages/app/src/debug/DebugArtifactLink.vue @@ -2,9 +2,10 @@ - -