mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-03 05:20:38 -05:00
Merge develop --> release/13.0.0 (#25901)
* fix: update newProject ref when switching between organizations in SelectCloudProjectModal (#25730) * chore: debug page tooltip distance and artifact border (#25727) * misc: debug page tooltip distance and artifact border * add changelog entry * fix CT test * fix: Improve error handling around calls to `this.next` in middleware (#25702) * chore: update changelog validation example (#25742) * misc: improve debug loading text wrap responsiveness (#25703) * misc: Increase max failures in IATR badge to 99 (#25737) * chore: exclude collaborator issues/PRs from triage project (#25769) * feat: add --auto-cancel-after-failures flag (#25237) Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: Matt Schile <mschile@cypress.io> Co-authored-by: Ryan Pei <ryanppei@gmail.com> Co-authored-by: Emily Rohrbough <emilyrohrbough@yahoo.com> * chore: Update v8 snapshot cache (#25592) * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * Update update_v8_snapshot_cache.yml * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache * chore: updating v8 snapshot cache --------- Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io> Co-authored-by: cypress-bot[bot] <47117332+cypress-bot[bot]@users.noreply.github.com> * fix: implement new graphql fields for spec counts (#25757) Co-authored-by: Stokes Player <stokes@cypress.io> Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com> * feat: Bundle cy.origin() dependencies at runtime (#25626) Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io> * chore: remove zenhub from release process (#25701) Co-authored-by: Matt Schile <mschile@cypress.io> * feat: add Cypress.Commands.overwriteQuery (#25674) * feat: add Cypress.Commands.overwriteQuery Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: Zach Bloomquist <git@chary.us> * fix: spawn child process with process.env in macOS arm64 (#25753) Co-authored-by: Matt Schile <mschile@cypress.io> Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: Zach Bloomquist <github@chary.us> * chore: lint system tests in CI (#25673) * fix: Suppress filesystem errors during glob search (#25774) * chore: issue with ts-loader missing in binary and problematic esbuild norewrite construct (#25797) * chore: update changelog linting (#25809) * docs(guides): add more detail to code-signing (#25794) Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> * chore: update workflows.yml to include the v8 snapshot update branch (#25784) Co-authored-by: cypress-bot[bot] <+cypress-bot[bot]@users.noreply.github.com> * chore: internal request preflight (#25772) --------- Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io> Co-authored-by: Matt Henkes <mjhenkes@gmail.com> Co-authored-by: Zach Bloomquist <git@chary.us> * chore: bump for 12.6.0 release (#25812) * chore: release @cypress/webpack-batteries-included-preprocessor-v2.4.0 [skip ci] * chore: release @cypress/webpack-preprocessor-v5.17.0 [skip ci] * test: skip flaky GitDataSource test (#25825) * chore: making our add-to-triage-board workflow reusable within the Cypress-io org (#25820) * chore: Making our add to triage workflow callable from other projects inside the Cypress-io org in Github * chore: updated cypress-example-kitchensink version (#25828) * fix: duplicate and expired cookies (#25761) * chore: add regression tests for duplicate cookies and bad expiry times * avoid prepending domain with dot for cookies that are set with the server side jar. This is to avoid the cookie being duplicated if it is set or overridden in a different context (request that can actually set the cookie or via document.domain) * feat: use cookie.toString() in the cookie patch to more accurately set cookies on the document, which should include other properties besides key=value * fix: add logic to handle expired cookies in the document.cookie patch, as well as in CDP * chore: build binary for cookie fixes for users to test * chore: change name of fixture to something more accurate * chore: comment why we are using the toughCookie toString method in the patch * [run ci] * chore: add changelog entry * [run ci] * fix: revert back to key=value when getting document.cookie as those are the only values are displayed (oversight on my end) * [run ci] * chore: make compatible with cypress.require * fix: add tests for hostOnly/non hostOnly cookies to make sure property gets sent up to automation client correctly. No longer need custom cookie prop to determine destination * [run ci] * fix: stale unit test * chore: adjust comments * [run ci] * fix: bad domain logic * [run ci] * chore: remove irrelevant comment * [run ci] * fix: adjust cookie login text to spec hostOnly cookie within the cookie patch. This should yield the same behavior as we are bound to same origin within the spec bridge * [run ci] * [run ci] * fix: allow for cookies on request of same key to take precedence over cookies in the jar, regardless of how many hierachy cookies exist in the jar * chore: fix cookie misc tests for cy.origin (dont run cy.origin) * [run ci] * chore: skip misc cookie tests in webkit as headless behavior doesn't clear cookies between tests correctly * Revert "fix: allow for cookies on request of same key to take precedence over cookies in the jar, regardless of how many hierachy cookies exist in the jar" This reverts commit17de1883ab. * [run ci] * chore: split changelog entry into two parts * chore: update logic to remove else statement and add comments * [run ci] * chore: readd windows snapshot branch in workflows * [run ci] * chore: fix workflows from bad merge * [run ci] * Revert "chore: split changelog entry into two parts" This reverts commit4352ef5f3e. * [run ci] * fix: Fix type definitions for cy.reload() (#25779) Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> * misc: Debug header updates (#25823) * fix: allow running tests outside Vite project root folder (#25801) * fix: allow running tests outside Vite project root folder * update snapshots * add changelog entry --------- Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com> * fix: mount component in [data-cy-root] (#25807) * fix(angular): mount component in [data-cy-root] * fix e2e test * add changelog entry * changelog [skip ci] * changelog --------- Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com> * chore: updating add to triage baord github action to use org secret (#25868) * chore: updating add to triage board github action to use org secret * chore: release @cypress/angular-v2.0.2 [skip ci] * chore: release @cypress/vite-dev-server-v5.0.3 [skip ci] * chore: Update v8 snapshot cache (#25822) Co-authored-by: cypress-bot[bot] <+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io> * feat: support host only cookies (#25853) * feat: allow setCookie API to take a hostOnly option * chore: add jsdoc/typescript description to render to users * chore: add changelog entry * [run ci] * chore: fix types * chore: fix cookie login tests * chore: update e2e cookie system tests * [run ci] * chore: fix cookie command tests. localhost cookies are calculated as hostOnly, which is consistent with how cypress works today * chore: fix system tests for cookies. * [run ci] * chore: fix system tests * chore: skip hostOnly assertions in webkit (for now) * [run ci] * chore: add property definitions to setCookieOptions * [run ci] * chore: add comments to hostOnly prop in firefox when setting a cookie * fix(webpack-dev-server): touch component-index during onSpecsChange to avoid writing to app file (#25861) * testing: try disabling uTimesSync and see what happens * build binaries [run ci] * fix: touch component index file instead of browser.js * build binaries [run ci] * update test * fix test * add test for custom HTML file in config * use existing component index in webpack-dev-server unit tests --------- Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com> * chore: release @cypress/webpack-dev-server-v3.2.4 [skip ci] * chore: improve types for server automation cookie client (#25836) * chore: improve types for automation cookies * [run ci] * fix: the cookie_behavior tests by syncing cookies immediately if … (#25855) * fix: fix the cookie_behavior tests by syncing cookies immediately if the application is already stable * chore: add changelog entry * [run ci] * chore: address comments from code review * feat: Public API for CT Framework Definitions (#25780) * chore: rework component onboarding in launchpad (#25713) * chore: refactoring and types * rework source of frameworks * revert rename * fix tests * fix more tests * types * update code * use same public API internally * rename interfaces * rename * work on dev server api * fix types * fix test * attempt to support getDevServerConfig * tests * add function to define framework [skip ci] * rework a lot of types * fix test * update tests and types * refactor * revert changes * lint * fix test * revert * remove * add "community" label [skip ci] * refactor * types * lint * fix bug * update function name * address feedback * improve types with Pick * refactor using type guard * correct label --------- Co-authored-by: Zachary Williams <ZachJW34@gmail.com> * chore: typing error * feat: scan for 3rd party ct plugins (#25749) * chore: refactoring and types * rework source of frameworks * revert rename * fix tests * fix more tests * types * update code * use same public API internally * rename interfaces * rename * work on dev server api * fix types * fix test * attempt to support getDevServerConfig * tests * add function to define framework [skip ci] * rework a lot of types * fix test * update tests and types * refactor * revert changes * lint * fix test * revert * remove * add "community" label [skip ci] * refactor * types * lint * fix bug * update function name * address feedback * feat: scan for 3rd party ct plugins * add e2e test * unit tests [run ci] * tweak resolution * rebase, address comments * fix windows paths * remove .gitignore * fix test --------- Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com> * lint config * spacing * try fix race cond * fix import error * build binary * try update snapshot * try using require * support namespaced definitions (#25804) * remove category * add icon prop * support esm -> cjs compiled typescript * fix test * misc: add CTA footer to launchpad framework dropdown (#25831) * remove test project dependencies * rebase * windows * windows again * add changelog entry * changelog * revert workflow * remove worklfow --------- Co-authored-by: Zachary Williams <ZachJW34@gmail.com> Co-authored-by: Adam Stone-Lord <adams@cypress.io> * chore: release @cypress/webpack-dev-server-v3.3.0 [skip ci] * fix: Add missing error message when `req.continue` is used incorrectly (#25884) --------- Co-authored-by: Adam Stone-Lord <adams@cypress.io> Co-authored-by: Zachary Williams <ZachJW34@gmail.com> Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com> Co-authored-by: Matt Schile <mschile@cypress.io> Co-authored-by: Alejandro Estrada <estrada9166@gmail.com> Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> Co-authored-by: Ryan Pei <ryanppei@gmail.com> Co-authored-by: Emily Rohrbough <emilyrohrbough@yahoo.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Ryan Manuel <ryanm@cypress.io> Co-authored-by: cypress-bot[bot] <47117332+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Mark Noonan <mark@cypress.io> Co-authored-by: Stokes Player <stokes@cypress.io> Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com> Co-authored-by: Zach Bloomquist <git@chary.us> Co-authored-by: willmsC <50909991+willmsC@users.noreply.github.com> Co-authored-by: Zach Bloomquist <github@chary.us> Co-authored-by: cypress-bot[bot] <+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Tim Griesser <tgriesser10@gmail.com> Co-authored-by: Matt Henkes <mjhenkes@gmail.com> Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net> Co-authored-by: Ben M <benm@cypress.io> Co-authored-by: Bill Glesias <bglesias@gmail.com> Co-authored-by: Podles <78863563+podlesny-j@users.noreply.github.com> Co-authored-by: Paolo Caleffi <p.caleffi@dreamonkey.com> Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
This commit is contained in:
+13
-6
@@ -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
|
||||
|
||||
@@ -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)? <!-- Link to PR here -->
|
||||
- [ ] Have API changes been updated in the [`type definitions`](https://github.com/cypress-io/cypress/blob/develop/cli/types/cypress.d.ts)?
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||

|
||||
- 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.
|
||||

|
||||
- 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
|
||||
|
||||
+39
-11
@@ -1,33 +1,61 @@
|
||||
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
|
||||
## 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).
|
||||
|
||||
## 12.6.0
|
||||
|
||||
_Released 02/14/2023 (PENDING)_
|
||||
|
||||
**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).
|
||||
- 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:**
|
||||
|
||||
- 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).
|
||||
- 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/15/2023_
|
||||
|
||||
**Features:**
|
||||
|
||||
- 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 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:**
|
||||
|
||||
|
||||
@@ -67,29 +67,30 @@ exports['shows help for run --foo 1'] = `
|
||||
Runs Cypress tests from the CLI without the GUI
|
||||
|
||||
Options:
|
||||
-b, --browser <browser-name-or-path> 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 <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 <config> sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}.
|
||||
-C, --config-file <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 <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 <name> a named group for recorded runs in Cypress Cloud
|
||||
-k, --key <record-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 <port> runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}.
|
||||
-P, --project <project-path> 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 <reporter> runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"
|
||||
-o, --reporter-options <reporter-options> options for the mocha reporter. defaults to "null"
|
||||
-s, --spec <spec> runs specific spec file(s). defaults to "all"
|
||||
-t, --tag <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 <test-failure-count || false> 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 <browser-name-or-path> 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 <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 <config> sets configuration values. separate multiple values with a comma. overrides any value in cypress.config.{js,ts,mjs,cjs}.
|
||||
-C, --config-file <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 <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 <name> a named group for recorded runs in Cypress Cloud
|
||||
-k, --key <record-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 <port> runs Cypress on a specific port. overrides any value in cypress.config.{js,ts,mjs,cjs}.
|
||||
-P, --project <project-path> 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 <reporter> runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"
|
||||
-o, --reporter-options <reporter-options> options for the mocha reporter. defaults to "null"
|
||||
-s, --spec <spec> runs specific spec file(s). defaults to "all"
|
||||
-t, --tag <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:
|
||||
-------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <test-failure-count || false>', text('autoCancelAfterFailures'))
|
||||
.option('-b, --browser <browser-name-or-path>', text('browser'))
|
||||
.option('--ci-build-id <id>', text('ciBuildId'))
|
||||
.option('--component', text('component'))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -192,6 +192,7 @@ const dequote = (str) => {
|
||||
|
||||
const parseOpts = (opts) => {
|
||||
opts = _.pick(opts,
|
||||
'autoCancelAfterFailures',
|
||||
'browser',
|
||||
'cachePath',
|
||||
'cacheList',
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Vendored
+19
@@ -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<ComponentDevServerOpts = any>(config: Cypress.ConfigOptions<ComponentDevServerOpts>): 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
|
||||
|
||||
Vendored
+254
-3
@@ -53,6 +53,9 @@ declare namespace Cypress {
|
||||
interface QueryFn<T extends keyof ChainableMethods> {
|
||||
(this: Command, ...args: Parameters<ChainableMethods[T]>): (subject: any) => any
|
||||
}
|
||||
interface QueryFnWithOriginalFn<T extends keyof Chainable> {
|
||||
(this: Command, originalFn: QueryFn<T>, ...args: Parameters<ChainableMethods[T]>): (subject: any) => any
|
||||
}
|
||||
interface ObjectLike {
|
||||
[key: string]: any
|
||||
}
|
||||
@@ -648,6 +651,12 @@ declare namespace Cypress {
|
||||
* @see https://on.cypress.io/api/custom-queries
|
||||
*/
|
||||
addQuery<T extends keyof Chainable>(name: T, fn: QueryFn<T>): void
|
||||
|
||||
/**
|
||||
* Overwrite an existing Cypress query with a new implementation
|
||||
* @see https://on.cypress.io/api/custom-queries
|
||||
*/
|
||||
overwriteQuery<T extends keyof Chainable>(name: T, fn: QueryFnWithOriginalFn<T>): 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: <T = any>(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>>) => 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<Loggable & Timeoutable>): Chainable<AUTWindow>
|
||||
reload(): Chainable<AUTWindow>
|
||||
/**
|
||||
* Reload the page.
|
||||
*
|
||||
* @see https://on.cypress.io/reload
|
||||
* @param {Partial<Loggable & Timeoutable>} 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<Loggable & Timeoutable>): Chainable<AUTWindow>
|
||||
/**
|
||||
* Reload the page without cache
|
||||
*
|
||||
@@ -1815,6 +1842,18 @@ declare namespace Cypress {
|
||||
* cy.reload(true)
|
||||
*/
|
||||
reload(forceReload: boolean): Chainable<AUTWindow>
|
||||
/**
|
||||
* 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<Loggable & Timeoutable>} 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<Loggable & Timeoutable>): Chainable<AUTWindow>
|
||||
|
||||
/**
|
||||
* 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> = 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 <package_name>@<version>.
|
||||
*
|
||||
* @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<DependencyToInstall[]>
|
||||
|
||||
/**
|
||||
* 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 <mount_module>,
|
||||
* @example: `cypress-ct-solidjs/src/mount`
|
||||
*/
|
||||
mountModule: (projectPath: string) => Promise<string>
|
||||
|
||||
/**
|
||||
* 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<ResolvedComponentFrameworkDefinition, 'dependencies'> & {
|
||||
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<ComponentFrameworkDefinition, 'type' | 'name' | 'supportedBundlers' | 'detectors' | 'dependencies'> & {
|
||||
/**
|
||||
* @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
|
||||
|
||||
@@ -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<CypressRunOptions> | undefined) => Promise<CypressRunResult | CypressFailedRunResult>
|
||||
cypress.open // $ExpectType (options?: Partial<CypressOpenOptions> | undefined) => Promise<void>
|
||||
@@ -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: '<svg>...</svg>'
|
||||
})
|
||||
|
||||
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: {
|
||||
|
||||
@@ -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<JQuery<HTMLElement>>
|
||||
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.SinonStatic>('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
|
||||
}
|
||||
|
||||
+27
-10
@@ -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
|
||||
➜ openssl pkcs12 -export -inkey key.pem -in cert.pem -out encrypted.pfx
|
||||
# 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 win-code-signing.key -in win-code-signing.crt -out encrypted-win-code-signing.pfx
|
||||
Enter Export Password: <password>
|
||||
Verifying - Enter Export Password: <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.
|
||||
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
|
||||
|
||||
+16
-25
@@ -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-<sha>/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-<sha>/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 <name_of_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).
|
||||
15. Merge the documentation PR from step 11 and the new docker image PR created in step 12 to release the image.
|
||||
|
||||
16. Merge the documentation PR from step 11 and the new docker image PR created in step 12 to release the image.
|
||||
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).
|
||||
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
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 <sha>
|
||||
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 <path_to_releaseData.json>
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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<T> (
|
||||
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<T> (
|
||||
...bootstrapModule(componentFixture, config),
|
||||
})
|
||||
|
||||
getTestBed().overrideProvider(TestComponentRenderer, { useValue: new CypressTestComponentRenderer() })
|
||||
|
||||
return componentFixture
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface CypressCTWebpackPluginOptions {
|
||||
supportFile: string | false
|
||||
devServerEvents: EventEmitter
|
||||
webpack: Function
|
||||
indexHtmlFile: string
|
||||
}
|
||||
|
||||
export type CypressCTContextOptions = Omit<CypressCTWebpackPluginOptions, 'devServerEvents' | 'webpack'>
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -114,7 +114,25 @@ export type PresetHandlerResult = { frameworkConfig: Configuration, sourceWebpac
|
||||
|
||||
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
||||
|
||||
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<Optional<PresetHandlerResult, 'frameworkConfig'>> {
|
||||
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<Opti
|
||||
case 'vue':
|
||||
case 'svelte':
|
||||
case undefined:
|
||||
return { sourceWebpackModulesResult: sourceDefaultWebpackDependencies(devServerConfig) }
|
||||
return defaultWebpackModules()
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected framework ${(devServerConfig as any).framework}, please visit https://on.cypress.io/component-framework-configuration to see a list of supported frameworks`)
|
||||
|
||||
@@ -89,6 +89,7 @@ export function makeCypressWebpackConfig (
|
||||
devServerEvents,
|
||||
supportFile,
|
||||
webpack,
|
||||
indexHtmlFile,
|
||||
}),
|
||||
],
|
||||
devtool: 'inline-source-map',
|
||||
|
||||
@@ -68,7 +68,7 @@ const cypressConfig = {
|
||||
supportFile: '',
|
||||
isTextTerminal: true,
|
||||
devServerPublicPathRoute: root,
|
||||
indexHtmlFile: path.join(__dirname, 'component-index.html'),
|
||||
indexHtmlFile: 'test/component-index.html',
|
||||
} as any as Cypress.PluginConfigOptions
|
||||
|
||||
describe('#devServer', () => {
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<FilePath>
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void>((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)
|
||||
})
|
||||
}
|
||||
@@ -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<t.MemberExpression>
|
||||
|
||||
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: <callbackName>, outputFilePath: <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)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -1,95 +0,0 @@
|
||||
// https://github.com/keik/merge-source-map
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
|
||||
// Copyright (c) keik <k4t0.kei@gmail.com>
|
||||
|
||||
// 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())
|
||||
}
|
||||
@@ -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<T> () {
|
||||
let resolve: (thenableOrResult?: T | PromiseLike<T> | undefined) => void
|
||||
@@ -21,23 +16,6 @@ function createDeferred<T> () {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"commitInfo": {
|
||||
"sha": "commit1",
|
||||
"authorName": "Lachlan Miller",
|
||||
"authorEmail": "hello@cypress.io",
|
||||
"summary": "chore: testing cypress",
|
||||
"branch": "main",
|
||||
"__typename": "CloudRunCommitInfo"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('<DebugArtifacts />', () => {
|
||||
it('mounts correctly, provides expected tooltip content, and emits correct event', () => {
|
||||
artifactMapping.forEach((artifact) => {
|
||||
cy.mount(() => (
|
||||
<DebugArtifactLink icon={artifact.icon} popperText={artifact.text} url={artifact.url}/>
|
||||
<DebugArtifactLink class="m-24px inline-flex" icon={artifact.icon} popperText={artifact.text} url={artifact.url}/>
|
||||
))
|
||||
|
||||
cy.findByTestId(`artifact-for-${artifact.icon}`).should('have.length', 1)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
:data-cy="`artifact-for-${icon}`"
|
||||
:distance="8"
|
||||
>
|
||||
<ExternalLink
|
||||
class="flex h-full w-full items-center justify-center"
|
||||
class="flex h-24px w-24px justify-center items-center hocus:rounded-md group hocus:border-1px hocus:border-indigo-500"
|
||||
:data-cy="`${icon}-button`"
|
||||
:href="props.url || '#'"
|
||||
:use-default-hocus="true"
|
||||
@@ -16,6 +17,7 @@
|
||||
fill-color="gray-100"
|
||||
hocus-stroke-color="indigo-500"
|
||||
hocus-fill-color="indigo-100"
|
||||
interactive-colors-on-group
|
||||
/>
|
||||
</ExternalLink>
|
||||
<template #popper>
|
||||
|
||||
@@ -241,6 +241,8 @@ describe('<DebugContainer />', () => {
|
||||
result.currentProject.cloudProject.runByNumber = {
|
||||
...CloudRunStubs.running,
|
||||
runNumber: 1,
|
||||
completedInstanceCount: 2,
|
||||
totalInstanceCount: 3,
|
||||
} as typeof test
|
||||
}
|
||||
},
|
||||
@@ -255,6 +257,51 @@ describe('<DebugContainer />', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not render DebugPendingRunSplash and DebugNewRelevantRunBar at the same time', () => {
|
||||
cy.mountFragment(DebugSpecsFragmentDoc, {
|
||||
variableTypes: DebugSpecVariableTypes,
|
||||
variables: {
|
||||
hasNextRun: false,
|
||||
runNumber: 1,
|
||||
nextRunNumber: -1,
|
||||
},
|
||||
onResult: (result) => {
|
||||
if (result.currentProject?.cloudProject?.__typename === 'CloudProject') {
|
||||
const test = result.currentProject.cloudProject.runByNumber
|
||||
|
||||
// Testing this to confirm we are "making impossible states impossible" in the UI,
|
||||
// and document the expectation in this scenario. For clarity,
|
||||
// we do not expect a 'RUNNING` current and next run at the same time, so
|
||||
// the data below represents an invalid state.
|
||||
|
||||
result.currentProject.cloudProject.runByNumber = {
|
||||
...CloudRunStubs.running,
|
||||
runNumber: 1,
|
||||
completedInstanceCount: 2,
|
||||
totalInstanceCount: 3,
|
||||
} as typeof test
|
||||
|
||||
result.currentProject.cloudProject.nextRun = {
|
||||
...CloudRunStubs.running,
|
||||
runNumber: 1,
|
||||
completedInstanceCount: 5,
|
||||
totalInstanceCount: 6,
|
||||
} as typeof test
|
||||
}
|
||||
},
|
||||
render: (gqlVal) => <DebugContainer gql={gqlVal} />,
|
||||
})
|
||||
|
||||
cy.findByTestId('debug-header').should('be.visible')
|
||||
cy.findByTestId('debug-pending-splash')
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.findByTestId('debug-pending-counts').should('have.text', '0 of 0 specs completed')
|
||||
})
|
||||
|
||||
cy.findByTestId('newer-relevant-run').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders specs and tests when completed run available', () => {
|
||||
cy.mountFragment(DebugSpecsFragmentDoc, {
|
||||
variableTypes: DebugSpecVariableTypes,
|
||||
|
||||
@@ -28,15 +28,15 @@
|
||||
:gql="run"
|
||||
:commits-ahead="props.commitsAhead"
|
||||
/>
|
||||
<DebugNewRelevantRunBar
|
||||
v-if="newerRelevantRun"
|
||||
:gql="newerRelevantRun"
|
||||
/>
|
||||
|
||||
<DebugPendingRunSplash
|
||||
v-if="isFirstPendingRun"
|
||||
class="mt-12"
|
||||
/>
|
||||
<DebugNewRelevantRunBar
|
||||
v-else-if="newerRelevantRun"
|
||||
:gql="newerRelevantRun"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<DebugPageDetails
|
||||
v-if="shouldDisplayDetails(run.status, run.isHidden)"
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div
|
||||
v-if="!props.expandable"
|
||||
data-cy="debug-artifacts"
|
||||
class="flex flex-grow space-x-4.5 opacity-0 px-18px justify-end test-row-artifacts"
|
||||
class="flex flex-grow opacity-0 px-18px gap-16px justify-end test-row-artifacts"
|
||||
>
|
||||
<div
|
||||
v-for="result, i in failedTestData.debugArtifacts"
|
||||
|
||||
@@ -6,7 +6,7 @@ const defaults = [
|
||||
{ attr: 'debug-header-branch', text: 'Branch Name: feature/DESIGN-183' },
|
||||
{ attr: 'debug-header-commitHash', text: 'Commit Hash: b5e6fde' },
|
||||
{ attr: 'debug-header-author', text: 'Commit Author: cypressDTest' },
|
||||
{ attr: 'debug-header-createdAt', text: 'Run Total Duration: 01:00 (an hour ago) ' },
|
||||
{ attr: 'debug-header-createdAt', text: 'Run Total Duration: 01m 00s (an hour ago) ' },
|
||||
]
|
||||
|
||||
describe('<DebugPageHeader />', {
|
||||
@@ -22,6 +22,7 @@ describe('<DebugPageHeader />', {
|
||||
result.commitInfo.summary = 'Adding a hover state to the button component'
|
||||
result.commitInfo.branch = 'feature/DESIGN-183'
|
||||
result.commitInfo.authorName = 'cypressDTest'
|
||||
result.commitInfo.authorEmail = 'adams@cypress.io'
|
||||
result.commitInfo.sha = 'b5e6fde'
|
||||
}
|
||||
}
|
||||
@@ -144,4 +145,22 @@ describe('<DebugPageHeader />', {
|
||||
|
||||
cy.findByTestId('debug-commitsAhead').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders duration over 1 hour', () => {
|
||||
cy.mountFragment(DebugPageHeaderFragmentDoc, {
|
||||
onResult (result) {
|
||||
if (result) {
|
||||
result.totalDuration = 3602000000
|
||||
}
|
||||
},
|
||||
render: (gqlVal) => {
|
||||
return (
|
||||
<DebugPageHeader gql={gqlVal} commitsAhead={0}/>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
cy.findByTestId('debug-header-createdAt')
|
||||
.should('have.text', 'Run Total Duration: 16h 33m 20s (an hour ago) ')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,8 +81,9 @@
|
||||
v-if="debug?.commitInfo?.authorName"
|
||||
data-cy="debug-header-author"
|
||||
>
|
||||
<i-cy-general-user_x16
|
||||
class="mr-1 mr-11px icon-dark-gray-500 icon-light-gray-100 icon-secondary-light-gray-200"
|
||||
<UserAvatar
|
||||
class="h-16px mr-8px w-16px"
|
||||
:email="debug?.commitInfo?.authorEmail"
|
||||
data-cy="debug-header-avatar"
|
||||
/>
|
||||
<span class="sr-only">Commit Author:</span> {{ debug.commitInfo.authorName }}
|
||||
@@ -113,8 +114,8 @@ import CommitIcon from '~icons/cy/commit_x14'
|
||||
import { gql } from '@urql/core'
|
||||
import { dayjs } from '../runs/utils/day.js'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDurationFormat } from '../composables/useDurationFormat'
|
||||
import DebugRunNumber from './DebugRunNumber.vue'
|
||||
import UserAvatar from '@cy/gql-components/topnav/UserAvatar.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -132,6 +133,7 @@ fragment DebugPageHeader on CloudRun {
|
||||
...RunResults
|
||||
commitInfo {
|
||||
authorName
|
||||
authorEmail
|
||||
summary
|
||||
branch
|
||||
}
|
||||
@@ -147,7 +149,14 @@ const debug = computed(() => props.gql)
|
||||
|
||||
const relativeCreatedAt = computed(() => dayjs(new Date(debug.value.createdAt!)).fromNow())
|
||||
|
||||
const totalDuration = useDurationFormat(debug.value.totalDuration ?? 0)
|
||||
/*
|
||||
Format duration to in HH[h] mm[m] ss[s] format. The `totalDuration` field is milliseconds. Remove the leading "00h" if the value is less
|
||||
than an hour. Currently, there is no expectation that a run duration will be greater 24 hours or greater, so it is okay that
|
||||
this format would "roll-over" in that scenario.
|
||||
Ex: 1 second which is 1000ms = 00m 01s
|
||||
Ex: 1 hour and 1 second which is 3601000ms = 01h 00m 01s
|
||||
*/
|
||||
const totalDuration = computed(() => dayjs.duration(debug.value.totalDuration ?? 0).format('HH[h] mm[m] ss[s]').replace(/^0+h /, ''))
|
||||
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
color="dark"
|
||||
:is-interactive="!!(runAllFailuresState.cta)"
|
||||
:disabled="!runAllFailuresState.disabled"
|
||||
:distance="8"
|
||||
>
|
||||
<Button
|
||||
data-cy="run-failures"
|
||||
@@ -186,9 +187,9 @@
|
||||
<div
|
||||
v-for="thumbprint in Object.keys(specData.failedTests)"
|
||||
:key="`test-${thumbprint}`"
|
||||
:data-cy="`test-group`"
|
||||
data-cy="test-group"
|
||||
class="flex flex-col flex-start border-b-gray-100 border-b-1px w-full pr-16px pl-16px justify-center"
|
||||
:class="Object.keys(specData.groups).length > 1 ? 'pb-16px': 'hover:bg-gray-50'"
|
||||
:class="Object.keys(specData.groups).length > 1 ? 'pb-16px': 'hover:bg-gray-50 focus-within:bg-gray-50'"
|
||||
>
|
||||
<DebugFailedTest
|
||||
v-if="specData.failedTests[thumbprint].length >= 1"
|
||||
|
||||
@@ -33,6 +33,10 @@ describe('Debug page empty states', () => {
|
||||
cy.findByRole('link', { name: 'Learn about project setup in Cypress' }).should('have.attr', 'href', 'https://on.cypress.io/adding-new-project?utm_source=Binary%3A+Launchpad&utm_medium=Debug+Tab&utm_campaign=Learn+More')
|
||||
|
||||
cy.percySnapshot()
|
||||
|
||||
cy.viewport(600, 600)
|
||||
|
||||
cy.percySnapshot('responsive')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -24,18 +24,16 @@
|
||||
:rows="loadingRows"
|
||||
>
|
||||
<template #header>
|
||||
<div class="bg-white border rounded-md flex border-gray-100 w-max p-5px text-14px text-gray-700">
|
||||
<div><i-cy-status-failed_x12 /></div>
|
||||
<div class="bg-gray-700 h-1px mx-5px mt-7px w-5px" />
|
||||
<div
|
||||
v-if="exampleTestName"
|
||||
class="bg-gray-100 h-13px mx-1 pb-1px w-1px"
|
||||
/>
|
||||
<div
|
||||
v-if="exampleTestName"
|
||||
class="mx-1 text-14px text-gray-700"
|
||||
>
|
||||
{{ exampleTestName }}
|
||||
<div class="flex">
|
||||
<div class="bg-white border rounded-md flex border-gray-100 py-4px px-8px text-14px text-gray-700 gap-8px items-center">
|
||||
<i-cy-status-failed_x12 class="h-12px w-12px" />
|
||||
<span>-</span>
|
||||
<div
|
||||
v-if="exampleTestName"
|
||||
class="border-l border-gray-100 pl-8px"
|
||||
>
|
||||
{{ exampleTestName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -170,10 +170,14 @@ describe('SidebarNavigation', () => {
|
||||
it('renders failure badge', () => {
|
||||
mountComponent({ cloudProject: { status: 'FAILED', numFailedTests: 1 } })
|
||||
cy.findByLabelText('Relevant run had 1 test failure').should('be.visible').contains('1')
|
||||
cy.percySnapshot('Debug Badge:failed')
|
||||
cy.percySnapshot('Debug Badge:failed:single-digit')
|
||||
|
||||
mountComponent({ cloudProject: { status: 'FAILED', numFailedTests: 10 } })
|
||||
cy.findByLabelText('Relevant run had 10 test failures').should('be.visible').contains('9+')
|
||||
cy.findByLabelText('Relevant run had 10 test failures').should('be.visible').contains('10')
|
||||
cy.percySnapshot('Debug Badge:failed:double-digit')
|
||||
|
||||
mountComponent({ cloudProject: { status: 'FAILED', numFailedTests: 100 } })
|
||||
cy.findByLabelText('Relevant run had 100 test failures').should('be.visible').contains('99+')
|
||||
cy.percySnapshot('Debug Badge:failed:truncated')
|
||||
})
|
||||
|
||||
|
||||
@@ -202,9 +202,9 @@ watchEffect(() => {
|
||||
let countToDisplay = '0'
|
||||
|
||||
if (totalFailed) {
|
||||
countToDisplay = totalFailed < 9
|
||||
countToDisplay = totalFailed < 99
|
||||
? String(totalFailed)
|
||||
: '9+'
|
||||
: '99+'
|
||||
}
|
||||
|
||||
if (status === 'FAILED') {
|
||||
|
||||
@@ -82,7 +82,23 @@ const props = withDefaults(defineProps <{
|
||||
})
|
||||
|
||||
const badgeVariant = computed(() => {
|
||||
return props.isNavBarExpanded ? 'ml-16px h-20px text-sm leading-3' : 'absolute outline-gray-1000 outline-2px outline bottom-0 left-36px text-xs h-16px leading-2'
|
||||
const classes: string[] = []
|
||||
|
||||
if (props.isNavBarExpanded) {
|
||||
classes.push('ml-16px', 'h-20px', 'text-sm', 'leading-3')
|
||||
} else {
|
||||
classes.push('absolute', 'outline-gray-1000', 'outline-2px', 'outline', 'bottom-0', 'text-xs', 'h-16px', 'leading-2')
|
||||
|
||||
// Keep failure count from overflowing sidebar (#25662)
|
||||
if ((props.badge.status === 'failed' || props.badge.status === 'error') && props.badge.value.length >= 3) {
|
||||
classes.push('right-4px')
|
||||
} else {
|
||||
// Anything else should left-align and overflow sidebar if needed
|
||||
classes.push('left-36px')
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
})
|
||||
|
||||
const badgeColorStyles = {
|
||||
|
||||
@@ -231,7 +231,8 @@ const driverConfigOptions: Array<DriverConfigOption> = [
|
||||
defaultValue: false,
|
||||
validation: validate.isBoolean,
|
||||
isExperimental: true,
|
||||
requireRestartOnChange: 'server',
|
||||
overrideLevel: 'any',
|
||||
requireRestartOnChange: 'browser',
|
||||
}, {
|
||||
name: 'experimentalSourceRewriting',
|
||||
defaultValue: false,
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { DataContext } from '..'
|
||||
import { SpecOptions, codeGenerator } from '../codegen'
|
||||
import templates from '../codegen/templates'
|
||||
import type { CodeGenType } from '../gen/graphcache-config.gen'
|
||||
import { WizardFrontendFramework, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { parse as parseReactComponent, resolver as reactDocgenResolvers } from 'react-docgen'
|
||||
import { visit } from 'ast-types'
|
||||
|
||||
@@ -152,7 +151,7 @@ export class CodegenActions {
|
||||
})
|
||||
}
|
||||
|
||||
getWizardFrameworkFromConfig (): WizardFrontendFramework | undefined {
|
||||
getWizardFrameworkFromConfig (): Cypress.ResolvedComponentFrameworkDefinition | undefined {
|
||||
const config = this.ctx.lifecycleManager.loadedConfigFile
|
||||
|
||||
// If devServer is a function, they are using a custom dev server.
|
||||
@@ -161,7 +160,7 @@ export class CodegenActions {
|
||||
}
|
||||
|
||||
// @ts-ignore - because of the conditional above, we know that devServer isn't a function
|
||||
return WIZARD_FRAMEWORKS.find((framework) => framework.configFramework === config?.component?.devServer.framework)
|
||||
return this.ctx.coreData.wizard.frameworks.find((framework) => framework.configFramework === config?.component?.devServer.framework)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import { detectFramework, WIZARD_FRAMEWORKS, commandsFileBody, supportFileComponent, supportFileE2E, WizardBundler, WizardFrontendFramework } from '@packages/scaffold-config'
|
||||
import { detectFramework, commandsFileBody, supportFileComponent, supportFileE2E, getBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition, detectThirdPartyCTFrameworks } from '@packages/scaffold-config'
|
||||
import assert from 'assert'
|
||||
import path from 'path'
|
||||
import Debug from 'debug'
|
||||
@@ -9,6 +9,7 @@ const debug = Debug('cypress:data-context:wizard-actions')
|
||||
|
||||
import type { DataContext } from '..'
|
||||
import { addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions } from '@packages/config'
|
||||
import componentIndexHtmlGenerator from '@packages/scaffold-config/src/component-index-template'
|
||||
|
||||
export class WizardActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
@@ -23,14 +24,14 @@ export class WizardActions {
|
||||
return this.ctx.wizardData
|
||||
}
|
||||
|
||||
setFramework (framework: WizardFrontendFramework | null): void {
|
||||
const next = WIZARD_FRAMEWORKS.find((x) => x.type === framework?.type)
|
||||
setFramework (framework: Cypress.ResolvedComponentFrameworkDefinition | null): void {
|
||||
const next = this.ctx.coreData.wizard.frameworks.find((x) => x.type === framework?.type)
|
||||
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.wizard.chosenFramework = framework
|
||||
})
|
||||
|
||||
if (next?.supportedBundlers?.length === 1) {
|
||||
if (next?.supportedBundlers?.[0] && next.supportedBundlers.length === 1) {
|
||||
this.setBundler(next?.supportedBundlers?.[0])
|
||||
|
||||
return
|
||||
@@ -41,7 +42,7 @@ export class WizardActions {
|
||||
// if the previous bundler was incompatible with the
|
||||
// new framework that was selected, we need to reset it
|
||||
const doesNotSupportChosenBundler = (chosenBundler && !new Set(
|
||||
this.ctx.coreData.wizard.chosenFramework?.supportedBundlers.map((x) => x.type) || [],
|
||||
this.ctx.coreData.wizard.chosenFramework?.supportedBundlers ?? [],
|
||||
).has(chosenBundler.type)) ?? false
|
||||
|
||||
const prevFramework = this.ctx.coreData.wizard.chosenFramework?.type ?? null
|
||||
@@ -51,9 +52,21 @@ export class WizardActions {
|
||||
}
|
||||
}
|
||||
|
||||
setBundler (bundler: WizardBundler | null) {
|
||||
getNullableBundler (bundler: 'vite' | 'webpack' | null) {
|
||||
if (!bundler) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return getBundler(bundler)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
setBundler (bundler: 'vite' | 'webpack' | null) {
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.wizard.chosenBundler = bundler
|
||||
coreData.wizard.chosenBundler = this.getNullableBundler(bundler)
|
||||
})
|
||||
|
||||
return this.ctx.coreData.wizard
|
||||
@@ -87,7 +100,7 @@ export class WizardActions {
|
||||
|
||||
this.resetWizard()
|
||||
|
||||
const detected = await detectFramework(this.ctx.currentProject)
|
||||
const detected = await detectFramework(this.ctx.currentProject, this.ctx.coreData.wizard.frameworks)
|
||||
|
||||
debug('detected %o', detected)
|
||||
|
||||
@@ -100,8 +113,8 @@ export class WizardActions {
|
||||
return
|
||||
}
|
||||
|
||||
coreData.wizard.detectedBundler = detected.bundler || detected.framework.supportedBundlers[0]
|
||||
coreData.wizard.chosenBundler = detected.bundler || detected.framework.supportedBundlers[0]
|
||||
coreData.wizard.detectedBundler = this.getNullableBundler(detected.bundler || detected.framework.supportedBundlers[0])
|
||||
coreData.wizard.chosenBundler = this.getNullableBundler(detected.bundler || detected.framework.supportedBundlers[0])
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -148,6 +161,20 @@ export class WizardActions {
|
||||
}
|
||||
}
|
||||
|
||||
async detectFrameworks () {
|
||||
if (!this.ctx.currentProject) {
|
||||
return
|
||||
}
|
||||
|
||||
const officialFrameworks = CT_FRAMEWORKS.map((framework) => resolveComponentFrameworkDefinition(framework))
|
||||
const thirdParty = await detectThirdPartyCTFrameworks(this.ctx.currentProject)
|
||||
const resolvedThirdPartyFrameworks = thirdParty.map(resolveComponentFrameworkDefinition)
|
||||
|
||||
this.ctx.update((d) => {
|
||||
d.wizard.frameworks = officialFrameworks.concat(resolvedThirdPartyFrameworks)
|
||||
})
|
||||
}
|
||||
|
||||
private async scaffoldE2E () {
|
||||
// Order of the scaffoldedFiles is intentional, confirm before changing
|
||||
const scaffoldedFiles = await Promise.all([
|
||||
@@ -295,14 +322,16 @@ export class WizardActions {
|
||||
}
|
||||
}
|
||||
|
||||
private async scaffoldComponentIndexHtml (chosenFramework: WizardFrontendFramework): Promise<NexusGenObjects['ScaffoldedFile']> {
|
||||
private async scaffoldComponentIndexHtml (chosenFramework: Cypress.ResolvedComponentFrameworkDefinition): Promise<NexusGenObjects['ScaffoldedFile']> {
|
||||
const componentIndexHtmlPath = path.join(this.projectRoot, 'cypress', 'support', 'component-index.html')
|
||||
|
||||
await this.ensureDir('support')
|
||||
|
||||
const defaultComponentIndex = componentIndexHtmlGenerator()
|
||||
|
||||
return this.scaffoldFile(
|
||||
componentIndexHtmlPath,
|
||||
chosenFramework.componentIndexHtml(),
|
||||
chosenFramework.componentIndexHtml?.() ?? defaultComponentIndex(),
|
||||
'The HTML wrapper that each component is served with. Used for global fonts, CSS, JS, HTML, etc.',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ParsedPath } from 'path'
|
||||
import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { WizardFrontendFramework } from '@packages/scaffold-config'
|
||||
import fs from 'fs-extra'
|
||||
import { uniq, upperFirst } from 'lodash'
|
||||
import path from 'path'
|
||||
@@ -14,7 +13,7 @@ interface CodeGenOptions {
|
||||
isDefaultSpecPattern: boolean
|
||||
specPattern: string[]
|
||||
currentProject: string | null
|
||||
framework?: WizardFrontendFramework
|
||||
framework?: Cypress.ResolvedComponentFrameworkDefinition
|
||||
specs?: FoundSpec[]
|
||||
componentName?: string
|
||||
isDefault?: boolean
|
||||
|
||||
@@ -18,7 +18,7 @@ import { CypressEnv } from './CypressEnv'
|
||||
import { autoBindDebug } from '../util/autoBindDebug'
|
||||
import type { EventRegistrar } from './EventRegistrar'
|
||||
import type { DataContext } from '../DataContext'
|
||||
import { DependencyToInstall, isDependencyInstalled, WIZARD_BUNDLERS, WIZARD_DEPENDENCIES, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { isDependencyInstalled, WIZARD_BUNDLERS } from '@packages/scaffold-config'
|
||||
|
||||
const debug = debugLib(`cypress:lifecycle:ProjectConfigManager`)
|
||||
|
||||
@@ -191,14 +191,20 @@ export class ProjectConfigManager {
|
||||
|
||||
// Use a map since sometimes the same dependency can appear in `bundler` and `framework`,
|
||||
// for example webpack appears in both `bundler: 'webpack', framework: 'react-scripts'`
|
||||
const unsupportedDeps = new Map<DependencyToInstall['dependency']['type'], DependencyToInstall>()
|
||||
const unsupportedDeps = new Map<Cypress.DependencyToInstall['dependency']['type'], Cypress.DependencyToInstall>()
|
||||
|
||||
if (!bundler) {
|
||||
return
|
||||
}
|
||||
|
||||
const isFrameworkSatisfied = async (bundler: typeof WIZARD_BUNDLERS[number], framework: typeof WIZARD_FRAMEWORKS[number]) => {
|
||||
for (const dep of await (framework.dependencies(bundler.type, this.options.projectRoot))) {
|
||||
const isFrameworkSatisfied = async (bundler: typeof WIZARD_BUNDLERS[number], framework: Cypress.ResolvedComponentFrameworkDefinition) => {
|
||||
const deps = await framework.dependencies(bundler.type, this.options.projectRoot)
|
||||
|
||||
debug('deps are %o', deps)
|
||||
|
||||
for (const dep of deps) {
|
||||
debug('detecting %s in %s', dep.dependency.name, this.options.projectRoot)
|
||||
|
||||
const res = await isDependencyInstalled(dep.dependency, this.options.projectRoot)
|
||||
|
||||
if (!res.satisfied) {
|
||||
@@ -209,9 +215,9 @@ export class ProjectConfigManager {
|
||||
return true
|
||||
}
|
||||
|
||||
const frameworks = WIZARD_FRAMEWORKS.filter((x) => x.configFramework === devServerOptions.framework)
|
||||
const frameworks = this.options.ctx.coreData.wizard.frameworks.filter((x) => x.configFramework === devServerOptions.framework)
|
||||
|
||||
const mismatchedFrameworkDeps = new Map<typeof WIZARD_DEPENDENCIES[number]['type'], DependencyToInstall>()
|
||||
const mismatchedFrameworkDeps = new Map<string, Cypress.DependencyToInstall>()
|
||||
|
||||
let isSatisfied = false
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep, BannerState } from '@packages/types'
|
||||
import type { WizardFrontendFramework, WizardBundler } from '@packages/scaffold-config'
|
||||
import { WizardBundler, CT_FRAMEWORKS, resolveComponentFrameworkDefinition } from '@packages/scaffold-config'
|
||||
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { App, BrowserWindow } from 'electron'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
@@ -66,10 +66,11 @@ export interface AppDataShape {
|
||||
|
||||
export interface WizardDataShape {
|
||||
chosenBundler: WizardBundler | null
|
||||
chosenFramework: WizardFrontendFramework | null
|
||||
chosenFramework: Cypress.ResolvedComponentFrameworkDefinition | null
|
||||
chosenManualInstall: boolean
|
||||
detectedBundler: WizardBundler | null
|
||||
detectedFramework: WizardFrontendFramework | null
|
||||
detectedFramework: Cypress.ResolvedComponentFrameworkDefinition | null
|
||||
frameworks: Cypress.ResolvedComponentFrameworkDefinition[]
|
||||
}
|
||||
|
||||
export interface MigrationDataShape {
|
||||
@@ -197,6 +198,8 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
|
||||
chosenManualInstall: false,
|
||||
detectedBundler: null,
|
||||
detectedFramework: null,
|
||||
// TODO: API to add third party frameworks to this list.
|
||||
frameworks: CT_FRAMEWORKS.map((framework) => resolveComponentFrameworkDefinition(framework)),
|
||||
},
|
||||
migration: {
|
||||
step: 'renameAuto',
|
||||
|
||||
@@ -34,7 +34,7 @@ export class FileDataSource {
|
||||
return this.ctx.fs.readFile(path.join(this.ctx.currentProject, relative), 'utf-8')
|
||||
}
|
||||
|
||||
async getFilesByGlob (cwd: string, glob: string | string[], globOptions?: GlobbyOptions) {
|
||||
async getFilesByGlob (cwd: string, glob: string | string[], globOptions: GlobbyOptions = {}): Promise<string[]> {
|
||||
const globs = ([] as string[]).concat(glob).map((globPattern) => {
|
||||
const workingDirectoryPrefix = path.join(cwd, path.sep)
|
||||
|
||||
@@ -49,7 +49,7 @@ export class FileDataSource {
|
||||
return globPattern
|
||||
})
|
||||
|
||||
const ignoreGlob = (globOptions?.ignore ?? []).concat('**/node_modules/**')
|
||||
const ignoreGlob = (globOptions.ignore ?? []).concat('**/node_modules/**')
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
// globby can't work with backwards slashes
|
||||
@@ -72,7 +72,15 @@ export class FileDataSource {
|
||||
|
||||
return files
|
||||
} catch (e) {
|
||||
debug('error in getFilesByGlob %o', e)
|
||||
if (!globOptions.suppressErrors) {
|
||||
// Log error and retry with filesystem errors suppressed - this allows us to find partial
|
||||
// results even if the glob search hits permission issues (#24109)
|
||||
debug('Error in getFilesByGlob %o, retrying with filesystem errors suppressed', e)
|
||||
|
||||
return await this.getFilesByGlob(cwd, glob, { ...globOptions, suppressErrors: true })
|
||||
}
|
||||
|
||||
debug('Non-suppressible error in getFilesByGlob %o', e)
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import debugLib from 'debug'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
import type { DataContext } from '../DataContext'
|
||||
import type { CloudSpecStatus, Query, RelevantRun, CurrentProjectRelevantRunSpecs, CloudSpecRun, CloudRun } from '../gen/graphcache-config.gen'
|
||||
import type { Query, RelevantRun, CurrentProjectRelevantRunSpecs, CloudRun } from '../gen/graphcache-config.gen'
|
||||
import { Poller } from '../polling'
|
||||
import type { CloudRunStatus } from '@packages/graphql/src/gen/cloud-source-types.gen'
|
||||
|
||||
@@ -15,6 +15,8 @@ const RELEVANT_RUN_SPEC_OPERATION_DOC = gql`
|
||||
id
|
||||
runNumber
|
||||
status
|
||||
completedInstanceCount
|
||||
totalInstanceCount
|
||||
specs {
|
||||
id
|
||||
status
|
||||
@@ -55,8 +57,6 @@ export const SPECS_EMPTY_RETURN: RunSpecReturn = {
|
||||
statuses: {},
|
||||
}
|
||||
|
||||
const INCOMPLETE_STATUSES: CloudSpecStatus[] = ['RUNNING', 'UNCLAIMED']
|
||||
|
||||
export type RunSpecReturn = {
|
||||
runSpecs: CurrentProjectRelevantRunSpecs
|
||||
statuses: {
|
||||
@@ -86,23 +86,9 @@ export class RelevantRunSpecsDataSource {
|
||||
return this.#cached.runSpecs
|
||||
}
|
||||
|
||||
#calculateSpecMetadata (specs: CloudSpecRun[]) {
|
||||
//mimic logic in Cloud to sum up the count of groups per spec to give the total spec counts
|
||||
const countGroupsForSpec = (specs: CloudSpecRun[]) => {
|
||||
return specs.map((spec) => spec.groupIds?.length || 0).reduce((acc, curr) => acc += curr, 0)
|
||||
}
|
||||
|
||||
return {
|
||||
totalSpecs: countGroupsForSpec(specs),
|
||||
completedSpecs: countGroupsForSpec(specs.filter((spec) => !INCOMPLETE_STATUSES.includes(spec.status || 'UNCLAIMED'))),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls runs from the current Cypress Cloud account and determines which runs are considered:
|
||||
* - "current" the most recent completed run, or if not found, the most recent running run
|
||||
* - "next" the most recent running run if a completed run is found
|
||||
* @param shas list of Git commit shas to query the Cloud with for matching runs
|
||||
* Pulls the specs that match the relevant run.
|
||||
* @param runs - the current and (optionally) next relevant run
|
||||
*/
|
||||
async getRelevantRunSpecs (runs: RelevantRun): Promise<RunSpecReturn> {
|
||||
const projectSlug = await this.ctx.project.projectId()
|
||||
@@ -147,28 +133,40 @@ export class RelevantRunSpecsDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
function isValidNumber (value: unknown): value is number {
|
||||
return Number.isFinite(value)
|
||||
}
|
||||
|
||||
if (cloudProject?.__typename === 'CloudProject') {
|
||||
const runSpecsToReturn: RunSpecReturn = {
|
||||
runSpecs: {},
|
||||
statuses: {},
|
||||
}
|
||||
|
||||
if (cloudProject.current && cloudProject.current.runNumber && cloudProject.current.status) {
|
||||
runSpecsToReturn.runSpecs.current = {
|
||||
...this.#calculateSpecMetadata(cloudProject.current.specs || []),
|
||||
runNumber: cloudProject.current.runNumber,
|
||||
const { current, next } = cloudProject
|
||||
|
||||
const formatCloudRunInfo = (cloudRunDetails: Partial<CloudRun>) => {
|
||||
const { runNumber, totalInstanceCount, completedInstanceCount } = cloudRunDetails
|
||||
|
||||
if (runNumber && isValidNumber(totalInstanceCount) && isValidNumber(completedInstanceCount)) {
|
||||
return {
|
||||
totalSpecs: totalInstanceCount,
|
||||
completedSpecs: completedInstanceCount,
|
||||
runNumber,
|
||||
}
|
||||
}
|
||||
|
||||
runSpecsToReturn.statuses.current = cloudProject.current.status
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (cloudProject.next && cloudProject.next.runNumber && cloudProject.next.status) {
|
||||
runSpecsToReturn.runSpecs.next = {
|
||||
...this.#calculateSpecMetadata(cloudProject.next.specs || []),
|
||||
runNumber: cloudProject.next.runNumber,
|
||||
}
|
||||
if (current && current.status) {
|
||||
runSpecsToReturn.runSpecs.current = formatCloudRunInfo(current)
|
||||
runSpecsToReturn.statuses.current = current.status
|
||||
}
|
||||
|
||||
runSpecsToReturn.statuses.next = cloudProject.next.status
|
||||
if (next && next.status) {
|
||||
runSpecsToReturn.runSpecs.next = formatCloudRunInfo(next)
|
||||
runSpecsToReturn.statuses.next = next.status
|
||||
}
|
||||
|
||||
return runSpecsToReturn
|
||||
@@ -193,6 +191,7 @@ export class RelevantRunSpecsDataSource {
|
||||
|
||||
debug(`Spec data is `, specs)
|
||||
|
||||
const wasWatchingCurrentProject = this.#cached.statuses.current === 'RUNNING'
|
||||
const specCountsChanged = !isEqual(specs.runSpecs, this.#cached.runSpecs)
|
||||
const statusesChanged = !isEqual(specs.statuses, this.#cached.statuses)
|
||||
|
||||
@@ -208,7 +207,7 @@ export class RelevantRunSpecsDataSource {
|
||||
debug('Run statuses changed')
|
||||
const projectSlug = await this.ctx.project.projectId()
|
||||
|
||||
if (projectSlug) {
|
||||
if (projectSlug && wasWatchingCurrentProject) {
|
||||
debug(`Invalidate cloudProjectBySlug ${projectSlug}`)
|
||||
await this.ctx.cloud.invalidate('Query', 'cloudProjectBySlug', { slug: projectSlug })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
WIZARD_DEPENDENCY_TYPESCRIPT,
|
||||
DependencyToInstall,
|
||||
isDependencyInstalled,
|
||||
} from '@packages/scaffold-config'
|
||||
import type { DataContext } from '..'
|
||||
@@ -8,12 +7,12 @@ import type { DataContext } from '..'
|
||||
export class WizardDataSource {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
async packagesToInstall (): Promise<DependencyToInstall[]> {
|
||||
async packagesToInstall (): Promise<Cypress.DependencyToInstall[]> {
|
||||
if (!this.ctx.coreData.wizard.chosenFramework || !this.ctx.coreData.wizard.chosenBundler || !this.ctx.currentProject) {
|
||||
return []
|
||||
}
|
||||
|
||||
const packages: DependencyToInstall[] = [
|
||||
const packages: Cypress.DependencyToInstall[] = [
|
||||
...(await this.ctx.coreData.wizard.chosenFramework.dependencies(
|
||||
this.ctx.coreData.wizard.chosenBundler.type, this.ctx.currentProject,
|
||||
)),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { SpecOptions } from '../../../src/codegen/spec-options'
|
||||
import templates from '../../../src/codegen/templates'
|
||||
import { createTestDataContext } from '../helper'
|
||||
import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { CT_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { defaultSpecPattern } from '@packages/config'
|
||||
|
||||
const tmpPath = path.join(__dirname, 'tmp/test-code-gen')
|
||||
@@ -352,7 +352,7 @@ describe('code-generator', () => {
|
||||
currentProject: 'path/to/myProject',
|
||||
codeGenPath: path.join(__dirname, 'files', 'react', 'Button.jsx'),
|
||||
codeGenType: 'component',
|
||||
framework: WIZARD_FRAMEWORKS[1],
|
||||
framework: CT_FRAMEWORKS[1],
|
||||
isDefaultSpecPattern: true,
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultSpecPattern } from '@packages/config'
|
||||
import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { CT_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { expect } from 'chai'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
@@ -89,7 +89,7 @@ describe('spec-options', () => {
|
||||
codeGenPath: `${tmpPath}/MyComponent.vue`,
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: true,
|
||||
framework: WIZARD_FRAMEWORKS[1],
|
||||
framework: CT_FRAMEWORKS[1],
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
})
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('spec-options', () => {
|
||||
codeGenPath: `${tmpPath}/MyComponent.vue`,
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: true,
|
||||
framework: WIZARD_FRAMEWORKS[1],
|
||||
framework: CT_FRAMEWORKS[1],
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
})
|
||||
|
||||
@@ -127,7 +127,7 @@ describe('spec-options', () => {
|
||||
codeGenPath: `${tmpPath}/Counter.tsx`,
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: true,
|
||||
framework: WIZARD_FRAMEWORKS[0],
|
||||
framework: CT_FRAMEWORKS[0],
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
componentName: 'Counter',
|
||||
isDefault: true,
|
||||
@@ -146,7 +146,7 @@ describe('spec-options', () => {
|
||||
codeGenPath: `${tmpPath}/Counter.tsx`,
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: true,
|
||||
framework: WIZARD_FRAMEWORKS[0],
|
||||
framework: CT_FRAMEWORKS[0],
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
componentName: 'View',
|
||||
})
|
||||
@@ -166,7 +166,7 @@ describe('spec-options', () => {
|
||||
codeGenPath: `${tmpPath}/Counter.tsx`,
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: true,
|
||||
framework: WIZARD_FRAMEWORKS[0],
|
||||
framework: CT_FRAMEWORKS[0],
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
componentName: 'View',
|
||||
})
|
||||
@@ -212,7 +212,7 @@ describe('spec-options', () => {
|
||||
codeGenPath: `${tmpPath}/${componentPath}`,
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: false,
|
||||
framework: WIZARD_FRAMEWORKS[1],
|
||||
framework: CT_FRAMEWORKS[1],
|
||||
specPattern,
|
||||
specs,
|
||||
})
|
||||
@@ -322,7 +322,7 @@ describe('spec-options', () => {
|
||||
codeGenType: 'component',
|
||||
isDefaultSpecPattern: true,
|
||||
specPattern: [defaultSpecPattern.component],
|
||||
framework: WIZARD_FRAMEWORKS[1],
|
||||
framework: CT_FRAMEWORKS[1],
|
||||
})
|
||||
|
||||
await fs.outputFile(`${tmpPath}/${fileName}`, '// foo')
|
||||
|
||||
@@ -272,6 +272,35 @@ describe('FileDataSource', () => {
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it('should retry search with `suppressErrors` if non-suppressed attempt fails', async () => {
|
||||
matchGlobsStub.onFirstCall().rejects(new Error('mocked filesystem error'))
|
||||
matchGlobsStub.onSecondCall().resolves(mockMatches)
|
||||
|
||||
const files = await fileDataSource.getFilesByGlob(
|
||||
'/',
|
||||
'/cypress/e2e/**.cy.js',
|
||||
{ absolute: false, objectMode: true },
|
||||
)
|
||||
|
||||
expect(files).to.eq(mockMatches)
|
||||
expect(matchGlobsStub).to.have.callCount(2)
|
||||
expect(matchGlobsStub.getCall(0).args[1].suppressErrors).to.be.undefined
|
||||
expect(matchGlobsStub.getCall(1).args[1].suppressErrors).to.equal(true)
|
||||
})
|
||||
|
||||
it('should return empty array if retry with suppression fails', async () => {
|
||||
matchGlobsStub.rejects(new Error('mocked filesystem error'))
|
||||
|
||||
const files = await fileDataSource.getFilesByGlob(
|
||||
'/',
|
||||
'/cypress/e2e/**.cy.js',
|
||||
{ absolute: false, objectMode: true },
|
||||
)
|
||||
|
||||
expect(files).to.eql([])
|
||||
expect(matchGlobsStub).to.have.callCount(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,9 @@ describe('GitDataSource', () => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it(`gets correct status for files on ${os.platform()}`, async function () {
|
||||
// TODO: Very flaky on CI, even with a bunch of retries it still fails more often than nought
|
||||
// see: https://github.com/cypress-io/cypress/issues/25824
|
||||
it.skip(`gets correct status for files on ${os.platform()}`, async function () {
|
||||
// TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23317
|
||||
this.retries(15)
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
import { DataContext } from '../../../src'
|
||||
import { createTestDataContext } from '../helper'
|
||||
import { RelevantRunSpecsDataSource, SPECS_EMPTY_RETURN } from '../../../src/sources'
|
||||
import { FAKE_PROJECT_ONE_RUNNING_RUN_ONE_COMPLETED_THREE_SPECS, FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC } from './fixtures/graphqlFixtures'
|
||||
|
||||
describe('RelevantRunSpecsDataSource', () => {
|
||||
let ctx: DataContext
|
||||
let dataSource: RelevantRunSpecsDataSource
|
||||
|
||||
beforeEach(() => {
|
||||
ctx = createTestDataContext('open')
|
||||
dataSource = new RelevantRunSpecsDataSource(ctx)
|
||||
sinon.stub(ctx.project, 'projectId').resolves('test123')
|
||||
})
|
||||
|
||||
describe('getRelevantRunSpecs()', () => {
|
||||
it('returns no specs or statuses when no specs found for run', async () => {
|
||||
const result = await dataSource.getRelevantRunSpecs({ current: 11111, next: 22222, commitsAhead: 0 })
|
||||
|
||||
expect(result).to.eql(SPECS_EMPTY_RETURN)
|
||||
})
|
||||
|
||||
it('returns expected specs and statuses when one run is found', async () => {
|
||||
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC)
|
||||
|
||||
const result = await dataSource.getRelevantRunSpecs({ current: 1, next: null, commitsAhead: 0 })
|
||||
|
||||
expect(result).to.eql({
|
||||
runSpecs: {
|
||||
current: {
|
||||
runNumber: 1,
|
||||
completedSpecs: 1,
|
||||
totalSpecs: 1,
|
||||
},
|
||||
},
|
||||
statuses: { current: 'RUNNING' },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns expected specs and statuses when one run is completed and one is running', async () => {
|
||||
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_COMPLETED_THREE_SPECS)
|
||||
|
||||
const result = await dataSource.getRelevantRunSpecs({ current: 1, next: null, commitsAhead: 0 })
|
||||
|
||||
expect(result).to.eql({
|
||||
runSpecs: {
|
||||
current: {
|
||||
runNumber: 1,
|
||||
completedSpecs: 3,
|
||||
totalSpecs: 3,
|
||||
},
|
||||
next: {
|
||||
runNumber: 2,
|
||||
completedSpecs: 0,
|
||||
totalSpecs: 3,
|
||||
},
|
||||
},
|
||||
statuses: {
|
||||
current: 'PASSED',
|
||||
next: 'RUNNING',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,9 +1,9 @@
|
||||
import { WizardBundler, WizardFrontendFramework, WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { WizardBundler, WIZARD_BUNDLERS, CT_FRAMEWORKS, resolveComponentFrameworkDefinition } from '@packages/scaffold-config'
|
||||
import { expect } from 'chai'
|
||||
import { createTestDataContext, scaffoldMigrationProject, removeCommonNodeModules } from '../helper'
|
||||
|
||||
function findFramework (type: WizardFrontendFramework['type']) {
|
||||
return WIZARD_FRAMEWORKS.find((x) => x.type === type)!
|
||||
function findFramework (type: Cypress.ResolvedComponentFrameworkDefinition['type']) {
|
||||
return resolveComponentFrameworkDefinition(CT_FRAMEWORKS.find((x) => x.type === type)!)
|
||||
}
|
||||
|
||||
function findBundler (type: WizardBundler['type']) {
|
||||
|
||||
@@ -49,6 +49,42 @@ export const FAKE_PROJECT_NO_RUNS = { data: { cloudProjectBySlug: { __typename:
|
||||
|
||||
export const FAKE_PROJECT_ONE_RUNNING_RUN = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [{ runNumber: 1, status: 'RUNNING', commitInfo: { sha: FAKE_SHAS[0] } }] } } }
|
||||
|
||||
export const FAKE_PROJECT_MULTIPLE_COMPLETED = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [{ runNumber: 4, status: 'FAILED', commitInfo: { sha: FAKE_SHAS[1] } }, { runNumber: 1, status: 'PASSED', commitInfo: { sha: FAKE_SHAS[0] } }] } } }
|
||||
export const FAKE_PROJECT_MULTIPLE_COMPLETED = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [
|
||||
{ runNumber: 4, status: 'FAILED', commitInfo: { sha: FAKE_SHAS[1] } }, { runNumber: 1, status: 'PASSED', commitInfo: { sha: FAKE_SHAS[0] } },
|
||||
] } } }
|
||||
|
||||
export const FAKE_PROJECT_MULTIPLE_COMPLETED_PLUS_RUNNING = { data: { cloudProjectBySlug: { __typename: 'CloudProject', runsByCommitShas: [{ runNumber: 5, status: 'RUNNING', commitInfo: { sha: FAKE_SHAS[2] } }, { runNumber: 4, status: 'FAILED', commitInfo: { sha: FAKE_SHAS[1] } }, { runNumber: 1, status: 'PASSED', commitInfo: { sha: FAKE_SHAS[0] } }] } } }
|
||||
|
||||
export const FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC = {
|
||||
data: {
|
||||
cloudProjectBySlug: {
|
||||
__typename: 'CloudProject',
|
||||
current: {
|
||||
runNumber: 1,
|
||||
completedInstanceCount: 1,
|
||||
totalInstanceCount: 1,
|
||||
status: 'RUNNING',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const FAKE_PROJECT_ONE_RUNNING_RUN_ONE_COMPLETED_THREE_SPECS = {
|
||||
data: {
|
||||
cloudProjectBySlug: {
|
||||
__typename: 'CloudProject',
|
||||
current: {
|
||||
runNumber: 1,
|
||||
status: 'PASSED',
|
||||
completedInstanceCount: 3,
|
||||
totalInstanceCount: 3,
|
||||
},
|
||||
next: {
|
||||
runNumber: 2,
|
||||
status: 'RUNNING',
|
||||
completedInstanceCount: 0,
|
||||
totalInstanceCount: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
"noUnusedLocals": false,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"types": [],
|
||||
"types": ["cypress"],
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ We patch `XMLHttpRequest` and `fetch` client-side to capture their `withCredenti
|
||||
|
||||
## Dependencies
|
||||
|
||||
Users can utilize `require()` or (dynamic) `import()` to include dependencies. We handle the dependency resolution and bundling with the webpack preprocessor. We add a webpack loader that runs last. If we find a `require()` or `import()` call inside a `cy.origin()` callback, we extract that callback from the output code. We then run that extracted callback through webpack again, so that it gets its own output bundle with all dependencies included. The original callback is replaced with an object that references the output bundle. At runtime, when executing `cy.origin()`, it loads and executes the callback bundle.
|
||||
Users can utilize `Cypress.require()` to include dependencies. It's functionally the same as the CommonJS `require()`. At runtime, before evaluating the **cy.origin()** callback, we send it to the server, replace references to `Cypress.require()` with `require()`, and run it through the default preprocessor (currently webpack) to bundle any dependencies with it. We send that bundle back to the cross-origin driver and evaluate it.
|
||||
|
||||
## Unsupported APIs
|
||||
|
||||
|
||||
@@ -562,6 +562,40 @@ describe('src/cy/commands/cookies - no stub', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the cookie on the specified domain as hostOnly and validates hostOnly property persists through related commands that fetch cookies', () => {
|
||||
const isWebkit = Cypress.browser.name.includes('webkit')
|
||||
|
||||
cy.visit('http://www.barbaz.com:3500/fixtures/generic.html')
|
||||
cy.setCookie('foo', 'bar', { hostOnly: true })
|
||||
|
||||
cy.getCookie('foo').its('domain').should('eq', 'www.barbaz.com')
|
||||
if (!isWebkit) {
|
||||
cy.getCookie('foo').its('hostOnly').should('eq', true)
|
||||
}
|
||||
|
||||
cy.getCookies().then((cookies) => {
|
||||
expect(cookies).to.have.lengthOf(1)
|
||||
|
||||
const cookie = cookies[0]
|
||||
|
||||
expect(cookie).to.have.property('domain', 'www.barbaz.com')
|
||||
if (!isWebkit) {
|
||||
expect(cookie).to.have.property('hostOnly', true)
|
||||
}
|
||||
})
|
||||
|
||||
cy.getAllCookies().then((cookies) => {
|
||||
expect(cookies).to.have.lengthOf(1)
|
||||
|
||||
const cookie = cookies[0]
|
||||
|
||||
expect(cookie).to.have.property('domain', 'www.barbaz.com')
|
||||
if (!isWebkit) {
|
||||
expect(cookie).to.have.property('hostOnly', true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('src/cy/commands/cookies', () => {
|
||||
@@ -785,7 +819,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
|
||||
it('#consoleProps', () => {
|
||||
cy.getCookies().then(function (cookies) {
|
||||
expect(cookies).to.deep.eq([{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false }])
|
||||
expect(cookies).to.deep.eq([{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: false }])
|
||||
const c = this.lastLog.invoke('consoleProps')
|
||||
|
||||
expect(c['Yielded']).to.deep.eq(cookies)
|
||||
@@ -938,7 +972,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
|
||||
it('#consoleProps', () => {
|
||||
cy.getCookies().then(function (cookies) {
|
||||
expect(cookies).to.deep.eq([{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false }])
|
||||
expect(cookies).to.deep.eq([{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: false }])
|
||||
const c = this.lastLog.invoke('consoleProps')
|
||||
|
||||
expect(c['Yielded']).to.deep.eq(cookies)
|
||||
@@ -955,7 +989,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('deep.eq', {
|
||||
name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false,
|
||||
name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: true,
|
||||
})
|
||||
.then(() => {
|
||||
expect(Cypress.automation).to.be.calledWith(
|
||||
@@ -1196,7 +1230,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
.then(() => {
|
||||
expect(Cypress.automation).to.be.calledWith(
|
||||
'set:cookie',
|
||||
{ domain: 'localhost', name: 'foo', value: 'bar', path: '/', secure: false, httpOnly: false, expiry: 12345, sameSite: undefined },
|
||||
{ domain: 'localhost', name: 'foo', value: 'bar', path: '/', secure: false, httpOnly: false, hostOnly: false, expiry: 12345, sameSite: undefined },
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1212,7 +1246,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
.then(() => {
|
||||
expect(Cypress.automation).to.be.calledWith(
|
||||
'set:cookie',
|
||||
{ domain: 'brian.dev.local', name: 'foo', value: 'bar', path: '/foo', secure: true, httpOnly: true, expiry: 987, sameSite: undefined },
|
||||
{ domain: 'brian.dev.local', name: 'foo', value: 'bar', path: '/foo', secure: true, httpOnly: true, hostOnly: false, expiry: 987, sameSite: undefined },
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1461,7 +1495,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
|
||||
Cypress.automation
|
||||
.withArgs('set:cookie', {
|
||||
domain: 'localhost', name: 'foo', value: 'bar', path: '/', secure: false, httpOnly: false, expiry: 12345, sameSite: undefined,
|
||||
domain: 'localhost', name: 'foo', value: 'bar', path: '/', secure: false, httpOnly: false, hostOnly: false, expiry: 12345, sameSite: undefined,
|
||||
})
|
||||
.resolves({
|
||||
name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: true,
|
||||
@@ -1494,7 +1528,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
|
||||
it('#consoleProps', () => {
|
||||
cy.setCookie('foo', 'bar').then(function (cookie) {
|
||||
expect(cookie).to.deep.eq({ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false })
|
||||
expect(cookie).to.deep.eq({ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: true })
|
||||
const c = this.lastLog.invoke('consoleProps')
|
||||
|
||||
expect(c['Yielded']).to.deep.eq(cookie)
|
||||
@@ -1688,7 +1722,7 @@ describe('src/cy/commands/cookies', () => {
|
||||
const c = this.lastLog.invoke('consoleProps')
|
||||
|
||||
expect(c['Yielded']).to.eq('null')
|
||||
expect(c['Cleared Cookie']).to.deep.eq({ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false })
|
||||
expect(c['Cleared Cookie']).to.deep.eq({ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: false })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2496,6 +2496,30 @@ describe('network stubbing', { retries: 15 }, function () {
|
||||
}).visit('/dump-method')
|
||||
})
|
||||
|
||||
it('fails test if both req.reply and req.continue are called in req handler', function (done) {
|
||||
testFail((err) => {
|
||||
expect(err.message).to.contain('`req.reply()` and/or `req.continue()` were called to signal request completion multiple times, but a request can only be completed once')
|
||||
done()
|
||||
})
|
||||
|
||||
cy.intercept('/dump-method', function (req) {
|
||||
req.reply()
|
||||
|
||||
req.continue()
|
||||
}).visit('/dump-method')
|
||||
})
|
||||
|
||||
it('fails test if req.continue is called with a non-function parameter', function (done) {
|
||||
testFail((err) => {
|
||||
expect(err.message).to.contain('\`req.continue\` requires the parameter to be a function')
|
||||
done()
|
||||
})
|
||||
|
||||
cy.intercept('/dump-method', function (req) {
|
||||
req.continue({} as any)
|
||||
}).visit('/dump-method')
|
||||
})
|
||||
|
||||
it('fails test if req.reply is called after req handler finishes', function (done) {
|
||||
testFail((err) => {
|
||||
expect(err.message).to.contain('> `req.reply()` was called after the request handler finished executing')
|
||||
|
||||
@@ -565,5 +565,54 @@ describe('driver/src/cypress/cy', () => {
|
||||
|
||||
cy.get('body').find('#specific-contains').children().should('have.class', 'active')
|
||||
})
|
||||
|
||||
context('overwriting queries', () => {
|
||||
it('does not allow commands to overwrite queries', () => {
|
||||
const fn = () => Cypress.Commands.overwrite('get', () => {})
|
||||
|
||||
expect(fn).to.throw().with.property('message')
|
||||
.and.include('Cannot overwite the `get` query. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`.')
|
||||
|
||||
expect(fn).to.throw().with.property('docsUrl')
|
||||
.and.include('https://on.cypress.io/api')
|
||||
})
|
||||
|
||||
it('does not allow queries to overwrite commands', () => {
|
||||
const fn = () => Cypress.Commands.overwriteQuery('click', () => {})
|
||||
|
||||
expect(fn).to.throw().with.property('message')
|
||||
.and.include('Cannot overwite the `click` command. Commands can only be overwritten with `Cypress.Commands.overwrite()`.')
|
||||
|
||||
expect(fn).to.throw().with.property('docsUrl')
|
||||
.and.include('https://on.cypress.io/api')
|
||||
})
|
||||
|
||||
it('can call the originalFn', () => {
|
||||
// Ensure nothing gets confused when we overwrite the same query multiple times.
|
||||
// Both overwrites should succeed, layered on top of each other.
|
||||
|
||||
let overwriteCalled = 0
|
||||
|
||||
Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
|
||||
overwriteCalled++
|
||||
|
||||
return originalFn.call(this, ...args)
|
||||
})
|
||||
|
||||
let secondOverwriteCalled = 0
|
||||
|
||||
Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
|
||||
secondOverwriteCalled++
|
||||
|
||||
return originalFn.call(this, ...args)
|
||||
})
|
||||
|
||||
cy.get('button').should('have.length', 24)
|
||||
cy.then(() => {
|
||||
expect(overwriteCalled).to.eq(1)
|
||||
expect(secondOverwriteCalled).to.eq(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { makeRequestForCookieBehaviorTests as makeRequest } from '../../../support/utils'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
makeRequest: (
|
||||
win: Cypress.AUTWindow,
|
||||
url: string,
|
||||
client?: 'fetch' | 'xmlHttpRequest',
|
||||
credentials?: 'same-origin' | 'include' | 'omit' | boolean,
|
||||
) => Promise<any>
|
||||
}
|
||||
}
|
||||
|
||||
describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
const serverConfig = {
|
||||
http: {
|
||||
@@ -29,12 +40,8 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
before(() => {
|
||||
originUrl = `${scheme}://www.foobar.com:${sameOriginPort}`
|
||||
|
||||
// add httpClient here globally until Cypress.require PR is merged
|
||||
cy.origin(`${scheme}://www.foobar.com:${sameOriginPort}`, () => {
|
||||
const { makeRequestForCookieBehaviorTests: makeRequest } = require('../../../support/utils')
|
||||
|
||||
// @ts-ignore
|
||||
window.makeRequest = makeRequest
|
||||
window.makeRequest = Cypress.require('../../../support/utils').makeRequestForCookieBehaviorTests
|
||||
})
|
||||
})
|
||||
|
||||
@@ -59,11 +66,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
args: originUrl,
|
||||
}, (originUrl) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${originUrl}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${originUrl}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${originUrl}/test-request`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${originUrl}/test-request`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -87,11 +94,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
// cookie jar should now mimic http://www.foobar.com:3500 / https://foobar.com:3502 as top
|
||||
cy.origin(originUrl, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/test-request', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/test-request', 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -114,11 +121,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
// cookie jar should now mimic http://www.foobar.com:3500 / https://foobar.com:3502 as top
|
||||
cy.origin(originUrl, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/test-request', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/test-request', 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -141,12 +148,12 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
cy.origin(originUrl, () => {
|
||||
cy.window().then((win) => {
|
||||
// set the cookie in the browser
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
// but omit the cookies in the request
|
||||
return cy.wrap(makeRequest(win, '/test-request', 'fetch', 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, '/test-request', 'fetch', 'omit'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -169,12 +176,12 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
cy.origin(originUrl, () => {
|
||||
cy.window().then((win) => {
|
||||
// do NOT set the cookie in the browser
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch', 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo1=bar1; Domain=foobar.com', 'fetch', 'omit'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
// but send the cookies in the request
|
||||
return cy.wrap(makeRequest(win, '/test-request', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/test-request', 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -206,12 +213,12 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
// do NOT set the cookie in the browser
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
// but send the cookies in the request
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -239,26 +246,16 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
// do NOT set the cookie in the browser
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true))
|
||||
})
|
||||
|
||||
// though request is cross origin, site should have access directly to cookie because it is same site
|
||||
// assert cookie value is actually set in the browser
|
||||
// current expected assertion. NOTE: This SHOULD be consistent
|
||||
if (Cypress.isBrowser('firefox')) {
|
||||
// firefox actually sets the cookie correctly
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
} else {
|
||||
cy.getCookie('foo1').should('equal', null)
|
||||
}
|
||||
|
||||
// FIXME: Ideally, browser should have access to this cookie. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
// future expected assertion
|
||||
// cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
|
||||
cy.window().then((win) => {
|
||||
// but send the cookies in the request
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -286,26 +283,16 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
// do NOT set the cookie in the browser
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'xmlHttpRequest', true))
|
||||
})
|
||||
|
||||
// though request is cross origin, site should have access directly to cookie because it is same site
|
||||
// assert cookie value is actually set in the browser
|
||||
// current expected assertion. NOTE: This SHOULD be consistent
|
||||
if (Cypress.isBrowser('firefox')) {
|
||||
// firefox actually sets the cookie correctly
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
} else {
|
||||
cy.getCookie('foo1').should('equal', null)
|
||||
}
|
||||
|
||||
// FIXME: Ideally, browser should have access to this cookie. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
// future expected assertion
|
||||
// cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
|
||||
cy.window().then((win) => {
|
||||
// but send the cookies in the request
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'xmlHttpRequest', true))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'xmlHttpRequest', true))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -334,11 +321,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -365,24 +352,14 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
// assert cookie value is actually set in the browser
|
||||
// current expected assertion.
|
||||
if (Cypress.isBrowser('firefox')) {
|
||||
// firefox actually sets the cookie correctly
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
} else {
|
||||
cy.getCookie('foo1').should('equal', null)
|
||||
}
|
||||
|
||||
// FIXME: Ideally, browser should have access to this cookie. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
// future expected assertion
|
||||
// cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'include'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -407,24 +384,14 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
// assert cookie value is actually set in the browser
|
||||
// current expected assertion. NOTE: This SHOULD be consistent
|
||||
if (Cypress.isBrowser('firefox')) {
|
||||
// firefox actually sets the cookie correctly
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
} else {
|
||||
cy.getCookie('foo1').should('equal', null)
|
||||
}
|
||||
|
||||
// FIXME: Ideally, browser should have access to this cookie. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
// future expected assertion
|
||||
// cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
cy.getCookie('foo1').its('value').should('equal', 'bar1')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -451,11 +418,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=foo1=bar1; Domain=foobar.com`, 'fetch', 'omit'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request-credentials`, 'fetch', 'omit'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -486,11 +453,11 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -519,23 +486,20 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true))
|
||||
})
|
||||
|
||||
// assert cookie value is actually set in the browser
|
||||
if (scheme === 'https') {
|
||||
// FIXME: cy.getCookie does not believe this cookie exists. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
// can only set third-party SameSite=None with Secure attribute, which is only possibly over https
|
||||
|
||||
//expected future assertion
|
||||
// cy.getCookie('bar1').its('value').should('equal', 'baz1')
|
||||
// assert cookie value is actually set in the browser, even if in a different domain
|
||||
cy.getCookie('bar1', {
|
||||
domain: 'barbaz.com',
|
||||
}).its('value').should('equal', 'baz1')
|
||||
} else {
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
}
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'xmlHttpRequest'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -562,18 +526,16 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'xmlHttpRequest', true))
|
||||
})
|
||||
|
||||
// FIXME: cy.getCookie does not believe this cookie exists. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
// can only set third-party SameSite=None with Secure attribute, which is only possibly over https
|
||||
|
||||
//expected future assertion
|
||||
// cy.getCookie('bar1').its('value').should('equal', 'baz1')
|
||||
// assert cookie value is actually set in the browser, even if in a different domain
|
||||
cy.getCookie('bar1', {
|
||||
domain: 'barbaz.com',
|
||||
}).its('value').should('equal', 'baz1')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'xmlHttpRequest', true))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'xmlHttpRequest', true))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -605,13 +567,13 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort, credentialOption }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'fetch', credentialOption as 'same-origin' | 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie?cookie=bar1=baz1; Domain=barbaz.com`, 'fetch', credentialOption as 'same-origin' | 'omit'))
|
||||
})
|
||||
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -639,23 +601,20 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort, credentialOption }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
// assert cookie value is actually set in the browser
|
||||
if (scheme === 'https') {
|
||||
// FIXME: cy.getCookie does not believe this cookie exists. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
// can only set third-party SameSite=None with Secure attribute, which is only possibly over https
|
||||
|
||||
//expected future assertion
|
||||
// cy.getCookie('bar1').its('value').should('equal', 'baz1')
|
||||
// assert cookie value is actually set in the browser, even if in a different domain
|
||||
cy.getCookie('bar1', {
|
||||
domain: 'barbaz.com',
|
||||
}).its('value').should('equal', 'baz1')
|
||||
} else {
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
}
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request`, 'fetch', credentialOption as 'same-origin' | 'omit'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -685,20 +644,16 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/set-cookie-credentials?cookie=bar1=baz1; Domain=barbaz.com; SameSite=None; Secure`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
// assert cookie value is actually set in the browser
|
||||
|
||||
// FIXME: cy.getCookie does not believe this cookie exists, though it is set in the browser. Should be fixed in https://github.com/cypress-io/cypress/pull/23643.
|
||||
cy.getCookie('bar1').should('equal', null)
|
||||
// can only set third-party SameSite=None with Secure attribute, which is only possibly over https
|
||||
|
||||
//expected future assertion
|
||||
// cy.getCookie('bar1').its('value').should('equal', 'baz1')
|
||||
// assert cookie value is actually set in the browser, even if in a different domain
|
||||
cy.getCookie('bar1', {
|
||||
domain: 'barbaz.com',
|
||||
}).its('value').should('equal', 'baz1')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.barbaz.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -730,20 +685,20 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, crossOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch', 'include'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=bar=baz; Domain=.foobar.com`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=bar=baz; Domain=.foobar.com`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
// Cookie should not be sent with app.foobar.com:3500/test as it does NOT fit the domain
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=baz=quux; Domain=app.foobar.com`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/set-cookie-credentials?cookie=baz=quux; Domain=app.foobar.com`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${crossOriginPort}/test-request`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -770,15 +725,15 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com', 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://app.foobar.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://app.foobar.com:${sameOriginPort}/test-request-credentials`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -803,15 +758,15 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
// cookie jar should now mimic http://www.foobar.com:3500 / https://foobar.com:3502 as top
|
||||
cy.origin(originUrl, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com; Path=/', 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, '/set-cookie?cookie=foo=bar; Domain=www.foobar.com; Path=/', 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com; Path=/test-request`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com; Path=/test-request`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `/test-request`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `/test-request`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
@@ -840,17 +795,17 @@ describe('Cookie Behavior', { browser: '!webkit' }, () => {
|
||||
},
|
||||
}, ({ scheme, sameOriginPort }) => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=www.foobar.com`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=www.foobar.com`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.wait(200)
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch'))
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=bar=baz; Domain=.foobar.com`, 'fetch'))
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(makeRequest(win, `${scheme}://www.foobar.com:${sameOriginPort}/test-request`, 'fetch', 'include'))
|
||||
return cy.wrap(window.makeRequest(win, `${scheme}://www.foobar.com:${sameOriginPort}/test-request`, 'fetch', 'include'))
|
||||
})
|
||||
|
||||
cy.wait('@cookieCheck')
|
||||
|
||||
@@ -723,8 +723,9 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
|
||||
cy.getCookie('key').then((cookie) => {
|
||||
expect(Cypress._.omit(cookie, 'expiry')).to.deep.equal({
|
||||
domain: '.www.foobar.com',
|
||||
domain: 'www.foobar.com',
|
||||
httpOnly: false,
|
||||
hostOnly: true,
|
||||
name: 'key',
|
||||
path: '/fixtures',
|
||||
sameSite: 'strict',
|
||||
@@ -851,8 +852,9 @@ describe('cy.origin - cookie login', { browser: '!webkit' }, () => {
|
||||
cy.get('[data-cy="doc-cookie"]').invoke('text').should('equal', 'name=value')
|
||||
cy.getCookie('name').then((cookie) => {
|
||||
expect(Cypress._.omit(cookie, 'expiry')).to.deep.equal({
|
||||
domain: '.www.foobar.com',
|
||||
domain: 'www.foobar.com',
|
||||
httpOnly: false,
|
||||
hostOnly: true,
|
||||
name: 'name',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
// FIXME: currently cookies aren't cleared properly in headless mode with webkit between tests, as the below tests (excluding cy.origin) pass headfully locally.
|
||||
describe('misc cookie tests', { browser: '!webkit' }, () => {
|
||||
// NOTE: For this test to work correctly, we need to have a FQDN, not localhost (www.foobar.com).
|
||||
// FIXES: https://github.com/cypress-io/cypress/issues/25174 (cookies are duplicated with prepended dot (.))
|
||||
it('does not duplicate cookies with a prepended dot for cookies that are stored inside the server side cookie jar (host only)', () => {
|
||||
cy.visit('https://www.foobar.com:3502/fixtures/trigger-cross-origin-redirect-to-self.html')
|
||||
|
||||
// does a 302 redirect back to www.foobar.com primary-origin page, but sets a sameSite=None cookie
|
||||
cy.get('[data-cy="cookie-cross-origin-redirects-host-only"]').click()
|
||||
|
||||
cy.getCookies({ domain: 'www.foobar.com' }).then((cookies) => {
|
||||
expect(cookies).to.have.length(1)
|
||||
|
||||
const singleCookie = cookies[0]
|
||||
|
||||
expect(singleCookie).to.have.property('name', 'foo')
|
||||
expect(singleCookie).to.have.property('value', 'bar')
|
||||
expect(singleCookie).to.have.property('domain', 'www.foobar.com')
|
||||
})
|
||||
})
|
||||
|
||||
it('does not duplicate cookies with a prepended dot for cookies that are stored inside the server side cookie jar (non-host only)', () => {
|
||||
cy.visit('https://www.foobar.com:3502/fixtures/trigger-cross-origin-redirect-to-self.html')
|
||||
|
||||
// does a 302 redirect back to www.foobar.com primary-origin page, but sets a sameSite=None cookie
|
||||
cy.get('[data-cy="cookie-cross-origin-redirects"]').click()
|
||||
|
||||
cy.getCookies({ domain: 'www.foobar.com' }).then((cookies) => {
|
||||
expect(cookies).to.have.length(1)
|
||||
|
||||
const singleCookie = cookies[0]
|
||||
|
||||
expect(singleCookie).to.have.property('name', 'foo')
|
||||
expect(singleCookie).to.have.property('value', 'bar')
|
||||
expect(singleCookie).to.have.property('domain', '.www.foobar.com')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* FIXES:
|
||||
* https://github.com/cypress-io/cypress/issues/25205 (cookies set with expired time with value deleted show up as set with value deleted)
|
||||
* https://github.com/cypress-io/cypress/issues/25495 (session cookies set with expired time with value deleted show up as set with value deleted)
|
||||
* https://github.com/cypress-io/cypress/issues/25148 (cannot log into azure, shows cookies are disabled/blocked)
|
||||
*/
|
||||
describe('expiring cookies', { browser: '!webkit' }, () => {
|
||||
before(() => {
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
window.makeRequest = Cypress.require('../../../support/utils').makeRequestForCookieBehaviorTests
|
||||
})
|
||||
})
|
||||
|
||||
describe('removes cookies that are set with an expired expiry time from the server side cookie jar / browser via CDP', () => {
|
||||
it('works with Max-Age=0', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; Max-Age=0;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
|
||||
it('works with expires=Thu, 01-Jan-1970 00:00:01 GMT', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; expires=Thu, 01-Jan-1970 00:00:01 GMT;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
|
||||
it('works with expires=Tues, 01-Jan-1980 00:00:01 GMT', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; expires=Tues, 01-Jan-1980 00:00:01 GMT; Max-Age=0;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
|
||||
it('work with expires=Thu, 01-Jan-1970 00:00:01 GMT and Max-Age=0', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=bar; Domain=.foobar.com;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
return cy.wrap(window.makeRequest(win, `/set-cookie?cookie=foo=deleted; Domain=.foobar.com; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0;`))
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removes cookies that are set with an expired expiry time from the document.cookie patch / browser via CDP', () => {
|
||||
it('works with Max-Age=0', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=bar'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0;'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
|
||||
it('works with expires=Thu, 01-Jan-1970 00:00:01 GMT', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=bar'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=deleted; Max-Age=0'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
|
||||
it('works with expires=Tues, 01-Jan-1980 00:00:01 GMT', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=bar'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=deleted; expires=Tues, 01-Jan-1980 00:00:01 GMT'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
|
||||
it('expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0', () => {
|
||||
cy.visit(`https://www.foobar.com:3502/fixtures/primary-origin.html`)
|
||||
|
||||
cy.visit(`https://app.foobar.com:3503/fixtures/secondary-origin.html`)
|
||||
cy.origin(`https://app.foobar.com:3503`, () => {
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=bar'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').its('value').should('eq', 'bar')
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.document.cookie = 'foo=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0'
|
||||
})
|
||||
|
||||
cy.getCookie('foo').should('eq', null)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,7 @@ describe('cy.origin dependencies - jsx', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with a jsx file', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const lodash = require('lodash')
|
||||
const lodash = Cypress.require('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
})
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import type { LoDashStatic } from 'lodash'
|
||||
|
||||
describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
})
|
||||
|
||||
it('works with require()', () => {
|
||||
it('works', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const lodash = require('lodash')
|
||||
// default type: any
|
||||
const lodash1 = Cypress.require('lodash')
|
||||
// 2 ways of getting the proper type
|
||||
const lodash2 = Cypress.require('lodash') as typeof import('lodash')
|
||||
const lodash3 = Cypress.require<LoDashStatic>('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
})
|
||||
})
|
||||
|
||||
it('works with dynamic import()', () => {
|
||||
cy.origin('http://www.foobar.com:3500', async () => {
|
||||
const lodash = await import('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
expect(lodash1.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
expect(lodash2.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
expect(lodash3.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
})
|
||||
})
|
||||
|
||||
it('works with an arrow function', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const lodash = require('lodash')
|
||||
const dayjs = require('dayjs')
|
||||
const lodash = Cypress.require('lodash')
|
||||
const dayjs = Cypress.require('dayjs')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
expect(dayjs('2022-07-29 12:00:00').format('MMMM D, YYYY')).to.equal('July 29, 2022')
|
||||
@@ -34,7 +34,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with a function expression', () => {
|
||||
cy.origin('http://www.foobar.com:3500', function () {
|
||||
const lodash = require('lodash')
|
||||
const lodash = Cypress.require('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
})
|
||||
@@ -42,7 +42,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with options object + args', () => {
|
||||
cy.origin('http://www.foobar.com:3500', { args: ['arg1'] }, ([arg1]) => {
|
||||
const lodash = require('lodash')
|
||||
const lodash = Cypress.require('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
expect(arg1).to.equal('arg1')
|
||||
@@ -51,7 +51,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with a yielded value', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const lodash = require('lodash')
|
||||
const lodash = Cypress.require('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
|
||||
@@ -62,18 +62,16 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with a returned value', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const lodash = require('lodash')
|
||||
const { add } = Cypress.require('./dependencies.support-esm')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
|
||||
return 'returned value'
|
||||
return add(1, 2)
|
||||
})
|
||||
.should('equal', 'returned value')
|
||||
.should('equal', 3)
|
||||
})
|
||||
|
||||
it('works with multiple cy.origin calls', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const lodash = require('lodash')
|
||||
const lodash = Cypress.require('lodash')
|
||||
|
||||
expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
|
||||
|
||||
@@ -81,7 +79,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
})
|
||||
|
||||
cy.origin('http://www.idp.com:3500', () => {
|
||||
const dayjs = require('dayjs')
|
||||
const dayjs = Cypress.require('dayjs')
|
||||
|
||||
expect(dayjs('2022-07-29 12:00:00').format('MMMM D, YYYY')).to.equal('July 29, 2022')
|
||||
})
|
||||
@@ -89,7 +87,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with a relative esm dependency', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const { add } = require('./dependencies.support-esm')
|
||||
const { add } = Cypress.require('./dependencies.support-esm')
|
||||
|
||||
expect(add(1, 2)).to.equal(3)
|
||||
})
|
||||
@@ -97,7 +95,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
|
||||
it('works with a relative commonjs dependency', () => {
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
const { add } = require('./dependencies.support-commonjs')
|
||||
const { add } = Cypress.require('./dependencies.support-commonjs')
|
||||
|
||||
expect(add(1, 2)).to.equal(3)
|
||||
})
|
||||
@@ -107,7 +105,7 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
const args = ['some string']
|
||||
|
||||
cy.origin('http://www.foobar.com:3500', { args }, ([arg1]) => {
|
||||
const result = require('./dependencies.support-commonjs')(arg1)
|
||||
const result = Cypress.require('./dependencies.support-commonjs')(arg1)
|
||||
|
||||
expect(result).to.equal('some_string')
|
||||
})
|
||||
@@ -126,13 +124,26 @@ describe('cy.origin dependencies', { browser: '!webkit' }, () => {
|
||||
})
|
||||
|
||||
describe('errors', () => {
|
||||
it('when dependency does not exist', () => {
|
||||
it('when dependency does not exist', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('Cannot find module')
|
||||
done()
|
||||
})
|
||||
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
require('./does-not-exist')
|
||||
Cypress.require('./does-not-exist')
|
||||
})
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
it('when experimental flag is disabled', { experimentalOriginDependencies: false }, (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
expect(err.message).to.include('Using `Cypress.require()` requires enabling the `experimentalOriginDependencies` flag.')
|
||||
done()
|
||||
})
|
||||
|
||||
cy.origin('http://www.foobar.com:3500', () => {
|
||||
Cypress.require('lodash')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<a data-cy="cookie-cross-origin-redirects-host-only">Trigger Cross Origin Redirect Loop that sets host only cookie</a>
|
||||
<a data-cy="cookie-cross-origin-redirects">Trigger Cross Origin Redirect Loop that sets non host only cookie</a>
|
||||
<script>
|
||||
function setHref (dataCy, redirect, cookie) {
|
||||
const redirectEncoded = encodeURIComponent(redirect)
|
||||
const cookieEncoded = encodeURIComponent(cookie)
|
||||
|
||||
document.querySelector(`[data-cy="${dataCy}"]`).href = (
|
||||
`/set-same-site-none-cookie-on-redirect?redirect=${redirectEncoded}&cookie=${cookieEncoded}`
|
||||
)
|
||||
}
|
||||
setHref('cookie-cross-origin-redirects-host-only', `${window.location.origin}/fixtures/primary-origin.html`, 'foo=bar')
|
||||
setHref('cookie-cross-origin-redirects', `${window.location.origin}/fixtures/primary-origin.html`, `foo=bar; domain=.${window.location.hostname}`)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -296,6 +296,17 @@ const createApp = (port) => {
|
||||
.sendStatus(200)
|
||||
})
|
||||
|
||||
app.get('/set-same-site-none-cookie-on-redirect', (req, res) => {
|
||||
const { redirect, cookie } = req.query
|
||||
const cookieDecoded = decodeURIComponent(cookie)
|
||||
|
||||
const cookieVal = `${cookieDecoded}; SameSite=None; Secure`
|
||||
|
||||
res
|
||||
.header('Set-Cookie', cookieVal)
|
||||
.redirect(302, redirect)
|
||||
})
|
||||
|
||||
app.get('/test-request-credentials', (req, res) => {
|
||||
const origin = cors.getOrigin(req['headers']['referer'])
|
||||
|
||||
|
||||
@@ -36,6 +36,6 @@ beforeEach(() => {
|
||||
// support file work properly
|
||||
Cypress.Commands.add('originLoadUtils', (origin) => {
|
||||
cy.origin(origin, () => {
|
||||
require('./utils')
|
||||
Cypress.require('./utils')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import $Cypress from '../cypress'
|
||||
import { $Cy } from '../cypress/cy'
|
||||
import { $Location } from '../cypress/location'
|
||||
import $Commands from '../cypress/commands'
|
||||
import $errUtils from '../cypress/error_utils'
|
||||
import { create as createLog } from '../cypress/log'
|
||||
import { bindToListeners } from '../cy/listeners'
|
||||
import { handleOriginFn } from './origin_fn'
|
||||
@@ -135,6 +136,20 @@ const setup = (cypressConfig: Cypress.Config, env: Cypress.ObjectLike) => {
|
||||
// @ts-ignore
|
||||
Cypress.isCy = cy.isCy
|
||||
|
||||
// this is "valid" inside the cy.origin() callback (as long as the experimental
|
||||
// flag is enabled), but it should be replaced by the preprocessor at runtime
|
||||
// with an actual require() before it's run in the browser. if it's not,
|
||||
// something unexpected has gone wrong
|
||||
// @ts-expect-error
|
||||
Cypress.require = () => {
|
||||
// @ts-ignore
|
||||
if (!Cypress.config('experimentalOriginDependencies')) {
|
||||
$errUtils.throwErrByPath('require.invalid_without_flag')
|
||||
}
|
||||
|
||||
$errUtils.throwErrByPath('require.invalid_inside_origin')
|
||||
}
|
||||
|
||||
handleOriginFn(Cypress, cy)
|
||||
handleLogs(Cypress)
|
||||
handleSocketEvents(Cypress)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AutomationCookie } from '@packages/server/lib/automation/cookies'
|
||||
import type { SerializableAutomationCookie } from '@packages/server/lib/util/cookies'
|
||||
import type { ICypress } from '../../cypress'
|
||||
|
||||
// cross-origin cookies collected by the the proxy are sent down to the driver
|
||||
@@ -10,10 +10,10 @@ export const handleCrossOriginCookies = (Cypress: ICypress) => {
|
||||
// multiple requests could set cookies while the page is loading, so we
|
||||
// collect all cookies and only send set them via automation once after
|
||||
// the page has loaded
|
||||
let cookiesToSend: AutomationCookie[] = []
|
||||
let cookiesToSend: SerializableAutomationCookie[] = []
|
||||
let waitingToSend = false
|
||||
|
||||
Cypress.on('cross:origin:cookies', (cookies: AutomationCookie[]) => {
|
||||
Cypress.on('cross:origin:cookies', (cookies: SerializableAutomationCookie[]) => {
|
||||
cookiesToSend = cookiesToSend.concat(cookies)
|
||||
|
||||
Cypress.backend('cross:origin:cookies:received')
|
||||
@@ -22,11 +22,7 @@ export const handleCrossOriginCookies = (Cypress: ICypress) => {
|
||||
|
||||
waitingToSend = true
|
||||
|
||||
// this event allows running a handler before stability is released.
|
||||
// this prevents subsequent commands from running until the cookies
|
||||
// are set via automation
|
||||
// @ts-ignore
|
||||
Cypress.once('before:stability:release', () => {
|
||||
const syncCookiesViaAutomation = () => {
|
||||
const cookies = cookiesToSend
|
||||
|
||||
cookiesToSend = []
|
||||
@@ -37,6 +33,18 @@ export const handleCrossOriginCookies = (Cypress: ICypress) => {
|
||||
.catch(() => {
|
||||
// errors here can be ignored as they're not user-actionable
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// if the application is already stable, sync the cookies to the automation client immediately
|
||||
if (cy.state('isStable')) {
|
||||
syncCookiesViaAutomation()
|
||||
} else {
|
||||
// otherwise, wait until stability is achieved
|
||||
// this event allows running a handler before stability is released.
|
||||
// this prevents subsequent commands from running until the cookies
|
||||
// are set via automation
|
||||
// @ts-ignore
|
||||
Cypress.once('before:stability:release', syncCookiesViaAutomation)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,18 +5,13 @@ import { $Location } from '../cypress/location'
|
||||
import { syncConfigToCurrentOrigin, syncEnvToCurrentOrigin } from '../util/config'
|
||||
import type { Runnable, Test } from 'mocha'
|
||||
import { LogUtils } from '../cypress/log'
|
||||
import $networkUtils from '../cypress/network_utils'
|
||||
|
||||
interface CrossOriginCallbackObject {
|
||||
callbackName: string
|
||||
outputFilePath: string
|
||||
}
|
||||
|
||||
interface RunOriginFnOptions {
|
||||
config: Cypress.Config
|
||||
args: any
|
||||
env: Cypress.ObjectLike
|
||||
fn: string | CrossOriginCallbackObject
|
||||
file?: string
|
||||
fn: string
|
||||
skipConfigValidation: boolean
|
||||
state: {}
|
||||
logCounter: number
|
||||
@@ -66,6 +61,49 @@ const rehydrateRunnable = (data: serializedRunnable): Runnable|Test => {
|
||||
return runnable
|
||||
}
|
||||
|
||||
// Callback function handling / preprocessing for dependencies
|
||||
// ---
|
||||
// 1. If experimentalOriginDependencies is disabled or the string "Cypress.require"
|
||||
// does not exist in the callback, just eval the callback as-is
|
||||
// 2. Otherwise, we send it to the server
|
||||
// 3. The server webpacks the callback to bundle in all the deps, then returns
|
||||
// that bundle
|
||||
// 4. Eval the callback like normal
|
||||
const getCallbackFn = async (fn: string, file?: string) => {
|
||||
if (
|
||||
// @ts-expect-error
|
||||
!Cypress.config('experimentalOriginDependencies')
|
||||
|| !fn.includes('Cypress.require')
|
||||
) {
|
||||
return fn
|
||||
}
|
||||
|
||||
// Since webpack will wrap everything up in a closure, we create a variable
|
||||
// in the outer scope (see the return value below), assign the function to it
|
||||
// in the inner scope, then call the function with the args
|
||||
const callbackName = '__cypressCallback'
|
||||
const response = await fetch('/__cypress/process-origin-callback', {
|
||||
body: JSON.stringify({ file, fn: `${callbackName} = ${fn};` }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
const result = await response.json() as GetFileResult
|
||||
|
||||
if (result.error) {
|
||||
$errUtils.throwErrByPath('origin.failed_to_get_callback', {
|
||||
args: { error: result.error },
|
||||
})
|
||||
}
|
||||
|
||||
return `(args) => {
|
||||
let ${callbackName};
|
||||
${result.contents};
|
||||
return ${callbackName}(args);
|
||||
}`
|
||||
}
|
||||
|
||||
export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
|
||||
const reset = (state) => {
|
||||
cy.reset({})
|
||||
@@ -98,7 +136,7 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
|
||||
}
|
||||
|
||||
Cypress.specBridgeCommunicator.on('run:origin:fn', async (options: RunOriginFnOptions) => {
|
||||
const { config, args, env, fn, state, skipConfigValidation, logCounter } = options
|
||||
const { config, args, env, file, fn, state, skipConfigValidation, logCounter } = options
|
||||
|
||||
let queueFinished = false
|
||||
|
||||
@@ -139,27 +177,8 @@ export const handleOriginFn = (Cypress: Cypress.Cypress, cy: $Cy) => {
|
||||
})
|
||||
|
||||
try {
|
||||
let value
|
||||
|
||||
if (_.isString(fn)) {
|
||||
value = window.eval(`(${fn})`)(args)
|
||||
} else {
|
||||
const { callbackName, outputFilePath } = fn
|
||||
const rawResult = await $networkUtils.fetch(`/__cypress/get-file/${encodeURIComponent(outputFilePath)}`) as string
|
||||
const result = JSON.parse(rawResult) as GetFileResult
|
||||
|
||||
if (result.error) {
|
||||
$errUtils.throwErrByPath('origin.failed_to_get_callback', {
|
||||
args: { error: result.error },
|
||||
})
|
||||
}
|
||||
|
||||
value = window.eval(`(args) => {
|
||||
let ${callbackName};
|
||||
${result.contents}
|
||||
return ${callbackName}(args);
|
||||
}`)(args)
|
||||
}
|
||||
const callback = await getCallbackFn(fn, file)
|
||||
const value = window.eval(`(${callback})`)(args)
|
||||
|
||||
// If we detect a non promise value with commands in queue, throw an error
|
||||
if (value && cy.queue.length > 0 && !value.then) {
|
||||
|
||||
@@ -4,10 +4,7 @@ import Promise from 'bluebird'
|
||||
import $utils from '../../cypress/utils'
|
||||
import $errUtils from '../../cypress/error_utils'
|
||||
|
||||
// TODO: add hostOnly to COOKIE_PROPS
|
||||
// https://github.com/cypress-io/cypress/issues/363
|
||||
// https://github.com/cypress-io/cypress/issues/17527
|
||||
const COOKIE_PROPS = 'name value path secure httpOnly expiry domain sameSite'.split(' ')
|
||||
const COOKIE_PROPS = 'name value path secure hostOnly httpOnly expiry domain sameSite'.split(' ')
|
||||
|
||||
function pickCookieProps (cookie) {
|
||||
if (!cookie) return cookie
|
||||
@@ -359,6 +356,7 @@ export default function (Commands, Cypress: InternalCypress.Cypress, cy, state,
|
||||
path: '/',
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
hostOnly: false,
|
||||
log: true,
|
||||
expiry: $utils.addTwentyYears(),
|
||||
})
|
||||
|
||||
@@ -183,6 +183,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
|
||||
communicator.toSpecBridge(origin, 'attach:to:window')
|
||||
|
||||
const fn = _.isFunction(callbackFn) ? callbackFn.toString() : callbackFn
|
||||
const file = $stackUtils.getSourceDetailsForFirstLine(userInvocationStack, config('projectRoot'))?.absoluteFile
|
||||
|
||||
// once the secondary origin page loads, send along the
|
||||
// user-specified callback to run in that origin
|
||||
@@ -190,6 +191,7 @@ export default (Commands, Cypress: Cypress.Cypress, cy: Cypress.cy, state: State
|
||||
communicator.toSpecBridge(origin, 'run:origin:fn', {
|
||||
args: options?.args || undefined,
|
||||
fn,
|
||||
file,
|
||||
// let the spec bridge version of Cypress know if config read-only values can be overwritten since window.top cannot be accessed in cross-origin iframes
|
||||
// this should only be used for internal testing. Cast to boolean to guarantee serialization
|
||||
// @ts-ignore
|
||||
|
||||
@@ -772,6 +772,11 @@ class $Cypress {
|
||||
return throwPrivateCommandInterface('addUtilityCommand')
|
||||
}
|
||||
|
||||
// Cypress.require() is only valid inside the cy.origin() callback
|
||||
require () {
|
||||
$errUtils.throwErrByPath('require.invalid_outside_origin')
|
||||
}
|
||||
|
||||
get currentTest () {
|
||||
const r = this.cy.state('runnable')
|
||||
|
||||
|
||||
@@ -27,11 +27,9 @@ const getTypeByPrevSubject = (prevSubject) => {
|
||||
return 'parent'
|
||||
}
|
||||
|
||||
const internalError = (path, name) => {
|
||||
const internalError = (path, args) => {
|
||||
$errUtils.throwErrByPath(path, {
|
||||
args: {
|
||||
name,
|
||||
},
|
||||
args,
|
||||
stack: (new cy.state('specWindow').Error('add command stack')).stack,
|
||||
errProps: {
|
||||
appendToStack: {
|
||||
@@ -88,11 +86,11 @@ export default {
|
||||
|
||||
add (name, options, fn) {
|
||||
if (builtInCommandNames[name]) {
|
||||
internalError('miscellaneous.invalid_new_command', name)
|
||||
internalError('miscellaneous.invalid_new_command', { name })
|
||||
}
|
||||
|
||||
if (reservedCommandNames.has(name)) {
|
||||
internalError('miscellaneous.reserved_command', name)
|
||||
internalError('miscellaneous.reserved_command', { name })
|
||||
}
|
||||
|
||||
// .hover & .mount are special case commands. allow as builtins so users
|
||||
@@ -126,11 +124,11 @@ export default {
|
||||
const original = commands[name]
|
||||
|
||||
if (queries[name]) {
|
||||
internalError('miscellaneous.invalid_overwrite_query_with_command', name)
|
||||
internalError('miscellaneous.invalid_overwrite_query_with_command', { name })
|
||||
}
|
||||
|
||||
if (!original) {
|
||||
internalError('miscellaneous.invalid_overwrite', name)
|
||||
internalError('miscellaneous.invalid_overwrite', { name, type: 'command' })
|
||||
}
|
||||
|
||||
function originalFn (...args) {
|
||||
@@ -157,13 +155,13 @@ export default {
|
||||
return cy.addCommand(overridden)
|
||||
},
|
||||
|
||||
addQuery (name: string, fn: () => QueryFunction) {
|
||||
addQuery (name: string, fn: (...args: any[]) => QueryFunction) {
|
||||
if (reservedCommandNames.has(name)) {
|
||||
internalError('miscellaneous.reserved_command_query', name)
|
||||
internalError('miscellaneous.reserved_command_query', { name })
|
||||
}
|
||||
|
||||
if (cy[name]) {
|
||||
internalError('miscellaneous.invalid_new_query', name)
|
||||
internalError('miscellaneous.invalid_new_query', { name })
|
||||
}
|
||||
|
||||
if (addingBuiltIns) {
|
||||
@@ -173,6 +171,26 @@ export default {
|
||||
queries[name] = fn
|
||||
cy.addQuery({ name, fn })
|
||||
},
|
||||
|
||||
overwriteQuery (name: string, fn: (...args: any[]) => QueryFunction) {
|
||||
if (commands[name]) {
|
||||
internalError('miscellaneous.invalid_overwrite_command_with_query', { name })
|
||||
}
|
||||
|
||||
const original = queries[name]
|
||||
|
||||
if (!original) {
|
||||
internalError('miscellaneous.invalid_overwrite', { name, type: 'command' })
|
||||
}
|
||||
|
||||
queries[name] = function overridden (...args) {
|
||||
args.unshift(original)
|
||||
|
||||
return fn.apply(this, args)
|
||||
}
|
||||
|
||||
cy.addQuery({ name, fn: queries[name] })
|
||||
},
|
||||
}
|
||||
|
||||
addingBuiltIns = true
|
||||
|
||||
@@ -868,12 +868,16 @@ export default {
|
||||
docsUrl: 'https://on.cypress.io/api/custom-queries',
|
||||
},
|
||||
invalid_overwrite: {
|
||||
message: 'Cannot overwite command for: `{{name}}`. An existing command does not exist by that name.',
|
||||
docsUrl: 'https://on.cypress.io/api',
|
||||
message: 'Cannot overwite command for: `{{name}}`. An existing {{type}} does not exist by that name.',
|
||||
docsUrl: 'https://on.cypress.io/api/custom-commands',
|
||||
},
|
||||
invalid_overwrite_command_with_query: {
|
||||
message: 'Cannot overwite the `{{name}}` command. Commands can only be overwritten with `Cypress.Commands.overwrite()`.',
|
||||
docsUrl: 'https://on.cypress.io/api/custom-commands',
|
||||
},
|
||||
invalid_overwrite_query_with_command: {
|
||||
message: 'Cannot overwite the `{{name}}` query. Queries cannot be overwritten.',
|
||||
docsUrl: 'https://on.cypress.io/api',
|
||||
message: 'Cannot overwite the `{{name}}` query. Queries can only be overwritten with `Cypress.Commands.overwriteQuery()`.',
|
||||
docsUrl: 'https://on.cypress.io/api/custom-queries',
|
||||
},
|
||||
invoking_child_without_parent (obj) {
|
||||
return stripIndent`\
|
||||
@@ -1137,6 +1141,7 @@ export default {
|
||||
|
||||
You passed: ${format(eventName)}`, 10)
|
||||
},
|
||||
req_continue_fn_only: '\`req.continue\` requires the parameter to be a function',
|
||||
event_needs_handler: `\`req.on()\` requires the second parameter to be a function.`,
|
||||
defineproperty_is_not_allowed: `\`defineProperty()\` is not allowed.`,
|
||||
setprototypeof_is_not_allowed: `\`setPrototypeOf()\` is not allowed.`,
|
||||
@@ -1238,9 +1243,7 @@ export default {
|
||||
|
||||
Variables must either be defined within the ${cmd('origin')} command or passed in using the args option.
|
||||
|
||||
Using \`require()\` or \`import()\` to include dependencies requires enabling the \`experimentalOriginDependencies\` flag and using the latest version of \`@cypress/webpack-preprocessor\`.
|
||||
|
||||
Note: Using \`require()\` or \`import()\` within ${cmd('origin')} from a \`node_modules\` plugin is not currently supported.`,
|
||||
Using \`require()\` or \`import()\` within the ${cmd('origin')} callback is not supported. Use ${cmd('Cypress.require')} to include dependencies instead, but note that it currently requires enabling the \`experimentalOriginDependencies\` flag.`,
|
||||
},
|
||||
callback_mixes_sync_and_async: {
|
||||
message: stripIndent`\
|
||||
@@ -1496,6 +1499,21 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
require: {
|
||||
invalid_inside_origin: {
|
||||
message: `${cmd('Cypress.require')} is supposed to be replaced with a \`require()\` statement before the test code using it is run. If this error is being thrown, something unexpected has occurred. Please submit an issue.`,
|
||||
docsUrl: 'https://on.cypress.io/origin',
|
||||
},
|
||||
invalid_outside_origin: {
|
||||
message: `${cmd('Cypress.require')} can only be used inside the ${cmd('origin')} callback and requires enabling the \`experimentalOriginDependencies\` flag.`,
|
||||
docsUrl: 'https://on.cypress.io/origin',
|
||||
},
|
||||
invalid_without_flag: {
|
||||
message: `Using ${cmd('Cypress.require')} requires enabling the \`experimentalOriginDependencies\` flag.`,
|
||||
docsUrl: 'https://on.cypress.io/origin',
|
||||
},
|
||||
},
|
||||
|
||||
route: {
|
||||
removed (obj) {
|
||||
return {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user