diff --git a/.circleci/config.yml b/.circleci/config.yml
index e9ffc5385a..78215fbba5 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -55,7 +55,6 @@ linuxWorkflowExcludeFilters: &linux-x64-workflow-exclude-filters
unless:
or:
- false
- # - equal: [ 'tgriesser/chore/fix-windows-build', << pipeline.git.branch >> ]
# windows is slow and expensive in CI, so it normally only runs on main branches
# add your branch to this list to run the full Windows build on your PR
@@ -902,7 +901,7 @@ commands:
fi
curl -L https://raw.githubusercontent.com/cypress-io/cypress/$branch/scripts/ensure-node.sh --output ci-ensure-node.sh
- else
+ else
# if no .node-version file exists, we no-op the node script and use the global yarn
echo '' > ci-ensure-node.sh
fi
@@ -1031,7 +1030,7 @@ commands:
# by default, electron-builder will NOT sign app built in a pull request
# even our internal one (!)
# Usually this is not a problem, since we only build and test binary
- # built on "develop" and "master" branches
+ # built on the "develop" branch
# but if you need to really build and sign a binary in a PR
# set variable CSC_FOR_PULL_REQUEST=true
command: |
@@ -1874,6 +1873,20 @@ jobs:
name: Build
command: yarn workspace @cypress/mount-utils build
- store-npm-logs
+
+ npm-xpath:
+ <<: *defaults
+ resource_class: small
+ steps:
+ - restore_cached_workspace
+ - run:
+ name: Run tests
+ command: yarn workspace @cypress/xpath cy:run
+ - store_test_results:
+ path: npm/xpath/test_results
+ - store_artifacts:
+ path: npm/xpath/test_results
+ - store-npm-logs
npm-create-cypress-tests:
<<: *defaults
diff --git a/.github/workflows/merge-master-into-develop.yml b/.github/workflows/merge-master-into-develop.yml
deleted file mode 100644
index 2b15adabc1..0000000000
--- a/.github/workflows/merge-master-into-develop.yml
+++ /dev/null
@@ -1,79 +0,0 @@
-name: Merge master into develop
-on:
- push:
- branches:
- - master
-jobs:
- merge-master-into-develop:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- with:
- fetch-depth: 0
- # the default `GITHUB_TOKEN` cannot push to protected branches, so use `cypress-app-bot`'s token instead
- token: ${{ secrets.BOT_GITHUB_TOKEN }}
- - name: Set committer info
- run: |
- git config --local user.email "$(git log --format='%ae' HEAD^!)"
- git config --local user.name "$(git log --format='%an' HEAD^!)"
- - name: Checkout develop branch
- run: git checkout develop
- - name: Check for merge conflict
- id: check-conflict
- run: echo "::set-output name=merge_conflict::$(git merge-tree $(git merge-base HEAD master) master HEAD | egrep '<<<<<<<')"
- - name: Merge master into develop
- id: merge-master
- run: git merge master
- if: ${{ !steps.check-conflict.outputs.merge_conflict }}
- - name: Failed merge, set merged status as failed
- run: echo "::set-output name=merge_conflict::'failed merge'"
- if: ${{ steps.merge-master.outcome != 'success' }}
- - name: Push
- run: git push
- if: ${{ !steps.check-conflict.outputs.merge_conflict }}
- - name: Checkout master
- run: git checkout master
- if: ${{ steps.check-conflict.outputs.merge_conflict }}
- - name: Determine name of new branch
- id: gen-names
- run: |
- echo "::set-output name=sha::$(git rev-parse --short HEAD)"
- echo "::set-output name=branch_name::$(git rev-parse --short HEAD)-master-into-develop"
- if: ${{ steps.check-conflict.outputs.merge_conflict }}
- - name: Create a copy of master on a new branch
- run: git checkout -b ${{ steps.gen-names.outputs.branch_name }} master
- if: ${{ steps.check-conflict.outputs.merge_conflict }}
- - name: Push branch to remote
- run: git push origin ${{ steps.gen-names.outputs.branch_name }}
- if: ${{ steps.check-conflict.outputs.merge_conflict }}
- - name: Create Pull Request
- uses: actions/github-script@v3
- with:
- script: |
- const pull = await github.pulls.create({
- owner: context.repo.owner,
- repo: context.repo.repo,
- base: 'develop',
- head: '${{ steps.gen-names.outputs.branch_name }}',
- title: 'chore: merge master (${{ steps.gen-names.outputs.sha }}) into develop',
- body: `There was a merge conflict when trying to automatically merge master into develop. Please resolve the conflict and complete the merge.
-
- DO NOT SQUASH AND MERGE
-
- @${context.actor}`,
- maintainer_can_modify: true,
- })
- await github.pulls.requestReviewers({
- owner: context.repo.owner,
- repo: context.repo.repo,
- pull_number: pull.data.number,
- reviewers: [context.actor],
- })
- await github.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: pull.data.number,
- labels: ['auto-merge'],
- })
- if: ${{ steps.check-conflict.outputs.merge_conflict }}
diff --git a/.github/workflows/snyk_sca_scan.yaml b/.github/workflows/snyk_sca_scan.yaml
index d9b21b0ab8..4fd117ba3c 100644
--- a/.github/workflows/snyk_sca_scan.yaml
+++ b/.github/workflows/snyk_sca_scan.yaml
@@ -1,14 +1,13 @@
name: Snyk Software Composition Analysis Scan
# This git workflow leverages Snyk actions to perform a Software Composition
-# Analysis scan on our Opensource libraries upon Pull Requests to Master &
-# Develop branches. We use this as a control to prevent vulnerable packages
+# Analysis scan on our Opensource libraries upon Pull Requests to the
+# "develop" branch. We use this as a control to prevent vulnerable packages
# from being introduced into the codebase.
on:
pull_request_target:
types:
- opened
branches:
- - master
- develop
jobs:
Snyk_SCA_Scan:
diff --git a/.github/workflows/snyk_static_analysis_scan.yaml b/.github/workflows/snyk_static_analysis_scan.yaml
index f34b3de41e..50ce41e5f1 100644
--- a/.github/workflows/snyk_static_analysis_scan.yaml
+++ b/.github/workflows/snyk_static_analysis_scan.yaml
@@ -1,14 +1,13 @@
name: Snyk Static Analysis Scan
# This git workflow leverages Snyk actions to perform a Static Application
-# Testing scan (SAST) on our first-party code upon Pull Requests to Master &
-# Develop branches. We use this as a control to prevent vulnerabilities
+# Testing scan (SAST) on our first-party code upon Pull Requests to the
+# "develop" branch. We use this as a control to prevent vulnerabilities
# from being introduced into the codebase.
on:
pull_request_target:
types:
- opened
branches:
- - master
- develop
jobs:
Snyk_SAST_Scan :
diff --git a/.gitignore b/.gitignore
index 5f8a277767..5f24cb5146 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,6 +78,9 @@ system-tests/lib/fixtureDirs.ts
# from npm/webpack-dev-server
/npm/webpack-dev-server/cypress/videos
+# from npm/xpath
+/npm/xpath/cypress/videos
+
# from errors
/packages/errors/__snapshot-images__
/packages/errors/__snapshot-md__
diff --git a/.releaserc.base.js b/.releaserc.base.js
index 95f6bbf940..e858fcbc41 100644
--- a/.releaserc.base.js
+++ b/.releaserc.base.js
@@ -15,6 +15,6 @@ module.exports = {
],
extends: 'semantic-release-monorepo',
branches: [
- 'master',
+ { name: 'develop', channel: 'latest' },
],
}
diff --git a/.releaserc.js b/.releaserc.js
index 4025bd1ff5..1800a9cf37 100644
--- a/.releaserc.js
+++ b/.releaserc.js
@@ -1,7 +1,3 @@
module.exports = {
...require('./.releaserc.base'),
- branches: [
- 'master',
- { name: 'chore/webpack-5', channel: 'channel-next' },
- ],
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2a007ead1b..0fe28547aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,17 @@
-https://on.cypress.io/changelog
+# Changelogs
+
+- [Cypress App](https://on.cypress.io/changelog)
+- [`@cypress/angular`](https://github.com/cypress-io/cypress/blob/develop/npm/angular/CHANGELOG.md)
+- [`@cypress/create-cypress-tests`](https://github.com/cypress-io/cypress/blob/develop/npm/create-cypress-tests/CHANGELOG.md)
+- [`@cypress/eslint-plugin-dev`](https://github.com/cypress-io/cypress/blob/develop/npm/eslint-plugin-dev/CHANGELOG.md)
+- [`@cypress/mount-utils`](https://github.com/cypress-io/cypress/blob/develop/npm/mount-utils/CHANGELOG.md)
+- [`@cypress/react`](https://github.com/cypress-io/cypress/blob/develop/npm/react/CHANGELOG.md)
+- [`@cypress/react18`](https://github.com/cypress-io/cypress/blob/develop/npm/react18/CHANGELOG.md)
+- [`@cypress/svelte`](https://github.com/cypress-io/cypress/blob/develop/npm/svelte/CHANGELOG.md)
+- [`@cypress/vite-dev-server`](https://github.com/cypress-io/cypress/blob/develop/npm/vite-dev-server/CHANGELOG.md)
+- [`@cypress/vue`](https://github.com/cypress-io/cypress/blob/develop/npm/vue/CHANGELOG.md)
+- [`@cypress/vue2`](https://github.com/cypress-io/cypress/blob/develop/npm/vue2/CHANGELOG.md)
+- [`@cypress/webpack-batteries-included-preprocessor`](https://github.com/cypress-io/cypress/blob/develop/npm/webpack-batteries-included-preprocessor/CHANGELOG.md)
+- [`@cypress/webpack-dev-server`](https://github.com/cypress-io/cypress/blob/develop/npm/webpack-dev-server/CHANGELOG.md)
+- [`@cypress/webpack-preprocessor`](https://github.com/cypress-io/cypress/blob/develop/npm/webpack-preprocessor/CHANGELOG.md)
+- [`@cypress/xpath`](https://github.com/cypress-io/cypress/blob/develop/npm/xpath/CHANGELOG.md)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7e6ddbf461..57460ad081 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,8 +4,8 @@ Thanks for taking the time to contribute! :smile:
**Once you learn how to use Cypress, you can contribute in many ways:**
-- Join the [Cypress Discord](https://on.cypress.io/discord) and answer questions. Teaching others how to use Cypress is a great way to learn more about how it works.
-- Blog about Cypress. We display blogs featuring Cypress on our [Examples](https://on.cypress.io/examples) page. If you'd like your blog featured, [open a PR to add it to our docs](https://github.com/cypress-io/cypress-documentation/blob/develop/CONTRIBUTING.md#adding-examples).
+- Join the [Cypress Discord](https://on.cypress.io/chat) and answer questions. Teaching others how to use Cypress is a great way to learn more about how it works.
+- Blog about Cypress. We display blogs featuring Cypress on our [Examples](https://on.cypress.io/examples) page. If you'd like your blog featured, [open a PR to add it to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#adding-examples).
- Write some documentation or improve our existing docs. See our [guide to contributing to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md).
- Give a talk about Cypress. [Contact us](mailto:support@cypress.io) ahead of time and we'll send you some swag. :shirt:
@@ -20,7 +20,6 @@ Thanks for taking the time to contribute! :smile:
- [Code of Conduct](#code-of-conduct)
- [Opening Issues](#opening-issues)
-- [Triaging Issues](#triaging-issues)
- [Writing Documentation](#writing-documentation)
- [Writing Code](#writing-code)
- [What you need to know before getting started](#what-you-need-to-know-before-getting-started)
@@ -111,129 +110,6 @@ test execution | Running tests inside a single spec | [open](https://github.com/
typescript | Transpiling or bundling TypeScript | [open](https://github.com/cypress-io/cypress/labels/topic%3A%20typescript), [closed](https://github.com/cypress-io/cypress/issues?q=label%3A%22topic%3A+typescript%22+is%3Aclosed)
video | Problems with video recordings | [open](https://github.com/cypress-io/cypress/labels/topic%3A%20video%20%F0%9F%93%B9), [closed](https://github.com/cypress-io/cypress/issues?q=label%3A%22topic%3A+video+%F0%9F%93%B9%22+is%3Aclosed)
-## Triaging Issues
-
-When an issue is opened in [cypress](https://github.com/cypress-io/cypress), we need to evaluate the issue to determine what steps should be taken next. So, when approaching new issues, there are some steps that should be taken.
-
-### Is this a question?
-
-Some opened issues are questions, not bug reports or feature requests. Issues are reserved for potential bugs or feature requests *only*. If this is the case, you should:
-
-- Explain that issues in our GitHub repo are reserved for potential bugs or feature requests and that the issue will be closed since it appears to be neither a bug nor a feature request.
-- Guide them to existing resources where their questions can be asked like our [Discussions](https://github.com/cypress-io/cypress/discussions), [community chat](https://on.cypress.io/chat), [Discord](https://on.cypress.io/discord), or [Stack Overflow](https://stackoverflow.com/questions/tagged/cypress).
-- Cypress offers support via email when signing up for any of our [paid plans](https://www.cypress.io/pricing/), so remind them that this is an option if they already have a paid account.
-- Move the issue to [Discussions](https://github.com/cypress-io/cypress/discussions).
-
-### Does this issue belong in this repository?
-
-#### Other open source repos
-
-Issues may be opened about wanting changes to our [documentation](https://github.com/cypress-io/cypress-documentation), our [example-kitchensink app](https://github.com/cypress-io/cypress-example-kitchensink), or [another repository](https://github.com/cypress-io). In this case you should:
-
-- Thank them for their contribution.
-- Explain that this repo is only for bugs or feature requests of the Cypress App.
-- If you have permission to 'Transfer the issue', do so. If not, explain that they can open an issue in our other repository and link to the repository.
-- Close the issue (if not already transferred).
-
-#### Cypress Dashboard
-
-Issues may be opened about wanting features in our Dashboard Service. In this case you should:
-
-- Thank them for opening an issue.
-- Add the `external: dashboard` label.
-
-#### Component Testing
-
-Issues may be opened about wanting features in Component Testing. In this case you should:
-
-- Thank them for opening an issue.
-- Add the `component testing` label.
-
-### Is this already an open issue?
-
-Search [all issues](https://github.com/cypress-io/cypress/issues) for keywords from the issue to ensure there isn't already an issue open for this. GitHub has some [search tips](https://help.github.com/articles/searching-issues-and-pull-requests/) that may help you better find the relevant issue.
-
-If an issue already exists you should:
-
-- Thank them for their contribution.
-- Explain that this issue is a duplicate of another issue, linking to the relevant issue (`#1234`).
-- Add the `type: duplicate` label to the issue.
-- Close the issue.
-
-### Does the issue provide all the information from our issue template?
-
-When opening an issue, there is a provided issue template based on the type of issue. If the opened issue does not provide enough information asked from the issue template you should:
-
-- Explain that we require new issues follow our provided issue template and that issues that are opened without this information are automatically closed per our [contributing guidelines](#fill-out-our-issue-template).
-- Close the issue.
-
-### Are they running the current version of Cypress?
-
-If they listed an older version of Cypress in their issue. We don't want to spend the time to set up a reproducible project (which can be time consuming) only to find that bumping the Cypress version fixes it. You should:
-
-- Ask them to update to the newest version of Cypress and comment about the results.
-- Add the `stage: awaiting response` label to the issue.
-
-### Is the fix or feature within our vision for Cypress?
-
-There will inevitably be suggestions that will not fit within the scope of Cypress's vision for our product. If an issue or pull request falls under this category you should:
-
-- Thank them for their contribution.
-- Explain why it doesn't fit into the scope at Cypress, and offer clear suggestions for improvement, if you're able. Be kind, but firm.
-- Link to relevant documentation, if there is any. If you notice repeated requests for things that are not within scope, add them into the [documentation](https://github.com/cypress-io/cypress-documentation) to avoid repeating yourself.
-- Add the `stage: wontfix` label to the issue.
-- Close the issue/pull request.
-
-### Is what they're describing actually happening?
-
-The best way to determine the validity of a bug is to recreate it yourself. Follow the directions or information provided to recreate the bug that is described. Did they provide a repository that demonstrates the bug? Great - fork it and run the project and steps required. If they didn't provide a repository, the best way to reproduce the issue is to have a 'sandbox' project up and running locally for Cypress. This is just a simple project with Cypress installed where you can freely edit the application under test and the tests themselves to recreate the problem.
-
-**Attempting to recreate the bug will lead to a few scenarios:**
-
-#### 1. You can't recreate the bug
-
- If you can't recreate the situation happening you should:
-
-- Thank them for their contribution.
-- Explain that there isn't enough information to reproduce the bug. Provide information on how you went about recreating the scenario, if you're able. Note your OS, Browser, Cypress version and any other information.
-- Note that if no reproducible example is provided, we will unfortunately have to close the issue.
-- Add the `stage: needs information` label to the issue.
-
-#### 2. You can recreate the bug
-
-If you can recreate the bug you should:
-
-- Thank them for their contribution.
-- Explain that you're able to recreate the bug. Provide the exact test code ran and the versions of Cypress, OS, and browser you used to recreate it.
-- If you know where the code is that could possibly fix this issue - link to the file or line of code from the [cypress](https://github.com/cypress-io/cypress) repo and remind the user that we are open source and that we gladly accept PRs, even if they are a work in progress.
-- Add the `stage: ready for work` label to the issue.
-
-#### 3. You can tell the problem is a user error
-
-In recreating the issue, you may realize that they had a typo or used the Cypress API incorrectly, etc. In this case you should:
-
-- Leave a comment informing the user of their error.
-- Link to relevant documentation, if there is any. If you notice repeated user errors for the same situation, add them into the [documentation](https://github.com/cypress-io/cypress-documentation) to avoid repeating yourself.
-- Close the issue.
-
-### Has the issue gone stale?
-
-Some issues are opened and sadly forgotten about by the person originally opening the issue.
-
-#### Not enough information ever provided
-
-Sometimes we request more information to be provided (label `stage: needs information`) for an open issue, but no one is able to provide a reproducible example or they simply never respond. **This does not mean that we don't believe that there is a bug!** We just, unfortunately, don't have a path forward to fix it without this information. In this case you should:
-
-- Add a comment reminding them or our request for more information and that the issue will be closed if it is not provided. Sometimes issues get forgotten about, and all the person needs is a gentle reminder.
-- If there is still no response after a weeks time, explain that you are closing the issue due to not enough information or inactivity and that they can comment in the issue with a reproducible example and we will reopen the issue.
-- Close the issue.
-
-#### They already solved their issue
-
-Some issues are resolved by the community, by giving some guidance or a workaround, but the original opener of the issue forgets to close the issue. In this case you should:
-
-- Explain that you are closing the issue as resolved and that they can comment if they are still having the issue and we will consider reopening it.
-- Close the issue.
## Writing Documentation
@@ -259,22 +135,33 @@ Here is a list of the core packages in this repository with a short description,
| Folder Name | Package Name | Purpose |
| :------------------------------------ | :---------------------- | :--------------------------------------------------------------------------- |
| [cli](./cli) | `cypress` | The command-line tool that is packaged as an `npm` module. |
+ | [app](./packages/app) | `@packages/app` | The the front-end for the Cypress App that renders in the launched browser instance. |
+ | [config](./packages/config) | `@packages/config` | The Cypress configuration types and validation used in the server, data-context and driver. |
+ | [data-context](./packages/data-context) | `@packages/data-context` | Centralized data access for the Cypress application. |
| [driver](./packages/driver) | `@packages/driver` | The code that is used to drive the behavior of the API commands. |
| [electron](./packages/electron) | `@packages/electron` | The Cypress implementation of Electron. |
| [example](./packages/example) | `@packages/example` | Our example kitchen-sink application. |
| [extension](./packages/extension) | `@packages/extension` | The Cypress Chrome browser extension |
+ | [frontend-shared](./packages/frontend-shared) | `@packages/frontend-shared` | Shared components and styles used in the `app` and `launchpad`. |
+ | [graphql](./packages/graphql) | `@packages/graphql` | The GraphQL layer that the `launchpad` and `app` use to interact with the `server`. |
| [https-proxy](./packages/https-proxy) | `@packages/https-proxy` | This does https proxy for handling http certs and traffic. |
+ | [icons](./packages/icons) | `@packages/icons` | The Cypress icons. |
+ | [launcher](./packages/launcher) | `@packages/launcher` | Finds and launches browsers installed on your system. |
+ | [launchpad](./packages/launchpad) | `@packages/launcher` | The portal to running Cypress that displays in `open` mode. |
| [net-stubbing](./packages/net-stubbing) | `@packages/net-stubbing` | Contains server side code for Cypress' network stubbing features. |
| [network](./packages/network) | `@packages/network` | Various utilities related to networking. |
| [proxy](./packages/proxy) | `@packages/proxy` | Code for Cypress' network proxy layer. |
- | [launcher](./packages/launcher) | `@packages/launcher` | Finds and launches browsers installed on your system. |
| [reporter](./packages/reporter) | `@packages/reporter` | The reporter shows the running results of the tests (The Command Log UI). |
+ | [resolve-dist](./packages/resolve-dist) | `@packages/resolve-dist` | Centralizes the resolution of paths to compiled/static assets from server-side code.. |
+ | [rewriter](./packages/rewriter) | `@packages/rewriter` | The logic to rewrite JS and HTML that flows through the Cypress proxy.
| [root](./packages/root) | `@packages/root` | Dummy package pointing at the root of the repository. |
- | [runner](./packages/runner) | `@packages/runner` | The runner is the minimal "chrome" around the user's application under test. |
+ | [runner](./packages/runner) | `@packages/runner` | (deprecated) The runner is the minimal "chrome" around the user's application under test. |
+ | [scaffold-config](./packages/scaffold-config) | `@packages/scaffold-config` | The logic related to scaffolding new projects using launchpad. |
| [server](./packages/server) | `@packages/server` | The <3 of Cypress. This orchestrates everything. The backend node process. |
- | [server-ct](./packages/server-ct) | `@packages/server-ct` | Some Component Testing specific overrides. Mostly extends functionality from `@packages/server` |
| [socket](./packages/socket) | `@packages/socket` | A wrapper around socket.io to provide common libraries. |
| [ts](./packages/ts) | `@packages/ts` | A centralized version of typescript. |
+ | [types](./packages/types) | `@packages/types` | The shared internal Cypress types. |
+ | [web-config](./packages/web-config) | `@packages/ui-components` | The web-related configuration. |
Public packages live within the [`npm`](./npm) folder and are standalone modules that get independently published to npm under the `@cypress/` namespace. These packages generally contain extensions, plugins, or other packages that are complementary to, yet independent of, the main Cypress app.
@@ -288,11 +175,14 @@ Here is a list of the npm packages in this repository:
| [mount-utils](./npm/mount-utils) | `@cypress/mount-utils` | Common functionality for Vue/React/Angular adapters. |
| [react](./npm/react) | `@cypress/react` | Cypress component testing for React. |
| [react18](./npm/react18) | `@cypress/react18` | Cypress component testing for React 18. |
+ | [svelte](./npm/svelte) | `@cypress/svelte` | Cypress component testing for Svelte. |
| [vite-dev-server](./npm/vite-dev-server) | `@cypress/vite-dev-server` | Vite powered dev server for Component Testing. |
- | [webpack-preprocessor](./npm/webpack-preprocessor) | `@cypress/webpack-preprocessor` | Cypress preprocessor for bundling JavaScript via webpack. |
- | [webpack-dev-server](./npm/webpack-dev-server) | `@cypress/webpack-dev-server` | Webpack powered dev server for Component Testing. |
| [vue](./npm/vue) | `@cypress/vue` | Cypress component testing for Vue 3. |
| [vue2](./npm/vue2) | `@cypress/vue2` | Cypress component testing for Vue 2. |
+ | [webpack-batteries-included-preprocessor](./npm/webpack-batteries-included-preprocessor) | `@cypress/webpack-batteries-included-preprocessor` | Cypress preprocessor for bundling JavaScript via webpack with dependencies included and support for various ES features, TypeScript, and CoffeeScript. |
+ | [webpack-dev-server](./npm/webpack-dev-server) | `@cypress/webpack-dev-server` | Webpack powered dev server for Component Testing. |
+ | [webpack-preprocessor](./npm/webpack-preprocessor) | `@cypress/webpack-preprocessor` | Cypress preprocessor for bundling JavaScript via webpack. |
+ | [xpath](./npm/xpath) | `@cypress/xpath` | Adds XPath command to Cypress.io test runner. |
We try to tag all issues with a `pkg/` or `npm/` tag describing the appropriate package the work is required in. For public packages, we use their qualified package name: For example, issues relating to the webpack preprocessor are tagged under [`npm: @cypress/webpack-preprocessor`](https://github.com/cypress-io/cypress/labels/npm%3A%20%40cypress%2Fwebpack-preprocessor) label and issues related to the `driver` package are tagged with the [`pkg/driver`](https://github.com/cypress-io/cypress/labels/pkg%2Fdriver) label.
@@ -508,28 +398,13 @@ They will outline development and test procedures. When in doubt just look at th
### Branches
-The repository is setup with two main (protected) branches.
+The repository has one protected branch:
-- `master` is the code already published, both for the main Cypress app and independent npm packages.
-- `develop` is the current latest "pre-release" code. This branch is set as the default branch, and all pull requests that update the main Cypress binary should be made against this branch.
+- `develop` contains the current latest "pre-release" code for the Cypress app and contains the already published code of all [standalone npm packages](./npm) Cypress maintains. This branch is set as the default branch, and all pull requests should be made against this branch.
-In general, we want to publish our [standalone npm packages](./npm) continuously as new features are added. Therefore, any pull requests that only change independent `@cypress/` packages in the [`npm`](./npm) directory should be made directly off the `master` branch. We use [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/) to automatically publish these packages to npm when a PR is merged directly into master.
+We want to publish our [standalone npm packages](./npm) continuously as new features are added. Therefore, after any pull request that changes independent `@cypress/` packages in the [`npm`](./npm) directory will automatically publish when a PR is merged directly into `develop` and the entire build passes. We used [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/) to automate the release of these packages to npm.
-When updating the main Cypress app, pull requests should be made against the `develop` branch. We do not continuously deploy the Cypress binary, so `develop` contains all of the new features and fixes that are staged to go out in the next update of the main Cypress app. In addition, if you make changes to an npm package that can't be published until the binary is also updated, you should make a pull request against the `develop` branch.
-
-Essentially, if you only change files within the [`npm`](./npm) folder, then you should make a pull request against `master`. Otherwise, make it against `develop`.
-
-All updates to `master` are automatically merged into `develop`, so `develop` always has the latest version of every package.
-
-#### Workflow Diagrams
-
-
-
-
-
-### Independent Packages CI Workflow
-
-Independent packages are automatically released when code is merged into `master` and the entire build passes.
+We do not continuously deploy the Cypress binary, so `develop` contains all of the new features and fixes that are staged to go out in the next update of the main Cypress app. If you make changes to an npm package that can't be published until the binary is also updated, you should make a pull request against specifying this is not be merged until the scheduled Cypress app release date.
### Pull Requests
@@ -636,11 +511,13 @@ Below are some guidelines Cypress uses when reviewing dependency updates.
- [ ] 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)
-## Deployment
+## Releases
-We will try to review and merge pull requests quickly. If you want to know our build process or build your own Cypress binary, read [the "Release Process" guide](./guides/release-process.md).
+[Standalone npm packages](./npm) are deployed immediately when a PR is merged into `develop` and the entire build passes.
-Independent packages are deployed immediately upon being merged into master. You can read more [above](#independent-packages-ci-workflow).
+The Cypress app is typically released every two weeks. All PRs merged to `develop` will build a "pre-released" Cypress app which can be installed to verify or leverage your changes before the scheduled release. Read these instructions for [installing pre-release versions](https://docs.cypress.io/guides/references/advanced-installation#Install-pre-release-version).
+
+If you want to know our build process or build your own Cypress binary, read [the "Release Process" guide](./guides/release-process.md).
## Known problems
diff --git a/README.md b/README.md
index d10a708934..59de2bd82b 100644
--- a/README.md
+++ b/README.md
@@ -64,13 +64,16 @@ yarn add cypress --dev
## Contributing
- [](https://circleci.com/gh/cypress-io/cypress/tree/develop) - `develop` branch
-- [](https://circleci.com/gh/cypress-io/cypress/tree/master) - `master` branch
Please see our [Contributing Guideline](./CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps.
+## How we work
+
+At Cypress we value our community and strive to be as open and transparent with them as possible. Check out [our guide](./cypress-prioritization-and-triage.md) on how we prioritize community issues.
+
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/assets/DIAGRAMS.md b/assets/DIAGRAMS.md
deleted file mode 100644
index bf1145597d..0000000000
--- a/assets/DIAGRAMS.md
+++ /dev/null
@@ -1,16 +0,0 @@
-## Diagram assets in this repo
-
-> :warning: These will eventually move to the docs site, link with caution
-
-### Updating Diagrams
-
-1. Visit a diagram link
-1. Make your changes
-1. Go to Export -> URL... and paste the new URL in this document
-1. Go to Export -> PNG and replace the asset
-
-### List of Diagrams
-
-**Branching and Contributing**
-1. [Choosing a Branch - Develop or Master](https://viewer.diagrams.net/?highlight=0000ff&edit=_blank&layers=1&nav=1&title=git-2.drawio#RzVltc9o4EP41%2FtiO38EfgYS2c%2B1NrulMm4%2FCFrYutuXIcsD3629lS%2Fg9QBpCYQBrLa2l3WefXQnNWiX7Twxl0Tca4Fgz9WCvWTeaaRqW68KPkJS1xHOkIGQkkJ0awT35D0uhLqUFCXDe6cgpjTnJukKfpin2eUeGGKO7brctjbtPzVCIB4J7H8VD6U8S8KiWzh29kX%2FGJIzUkw1d3kmQ6iwFeYQCumuJrFvNWjFKeX2V7Fc4FsZTdqnHrSfuHibGcMpPGfDX0%2B33z%2Ff7X1m42j39mH%2F6JzazD1LLM4oLuWA5WV4qCzBapAEWSnTNWu4iwvF9hnxxdwc%2BB1nEkxhaBlxKdZhxvJ%2Bcp3FYPcAG0wRzVkIXNcCrR5TKonVz11jfsma1LGpZ3lbAQtLj4UFzYxS4kHY5w0ZDk%2BAAMCKblPGIhjRF8W0jXTbSr5Rm0jj%2FYs5LCXFUcNo13ZamfI0SEouF%2F0ARTRBI%2FYI9V8ZvrCuefq5tGY4RJ8%2FdcWOWkkPvKAGNjU%2Fm849O1y1zvauEIxZiLse1YXhcldNTldOC%2BXigasEYKlvdMtEhP2vSrn7u3Loj4KKeRYOmg5FfDzBrJAjdGBy6zDOUdqDnPhWCLyqwfMgrKC2gg2Fk%2B%2BYmXIXidxWhNATyFMFTzSKk8EULsWIi9FZxhfGj4M%2FSh0eYsABdPbxQejYMpX4khm631Td83eBnHAOyZV9Y96F7S1bPX4l7UXSESsbjYQsrWdGYskqHtd1uTd8XluKMPuLWncDduEAJ1rLhrzciKMvqgcRSnN%2FiKEPRfpuj3B7Q34yi7CsgaBdhofnuu5ic6JJgYIDgNAx9QznH7Prowe4EembeRtcvgR6Ayx8HH%2Bd9MhxYiJW%2FRCEBFpDNB1lXVI2bfadVytYJmRHvCa81z2eObD80T4JWo1s0ylbjDjMCdsRMyk7NsmCwKlG9YFhZytS58Vj8%2Fm7WPjc%2Fmvash0TTNl7Mj%2Ba8j93eiMvkR3eS3QQyquJfBW5DYBBV4jXktAfBZweOqTVMcIzAwFe0gS1NB8coJmEK1z7gQaBmKQiBwJ5hIW8kJAiqIGEYGBZtKn0CWtIloNxZas6NXEGLd5yVeL%2FENHKjI5Vqh%2B1FG57TQT5JS%2FpHw%2Fa6lbeihdfWjqoLMH6OudYnqTcAxuydeOtALobb5ZbzyeWCHHgF3rKuwVveERI6YYD3DlX9fMBaf9NrMcwQOSfWNa9mm9kRttFNddRQdh74mxtVdaChlJofHbur43Js5A0cvmBY1MVi4b6qpOvMs2Eq6eQ%2BzUTVrHNa2dV%2FRKP9SJqTAKvtF4%2FwsItfZgAHGLtOs0SrD7sEetZD2MUxyXKBnzxCmRD6MS2Cer%2FgkzQEiXn8tOe0ItsR77Ei261ewyRYvy5RfHuTxxjt2ns2UnsbxqWKb6W4hZufEa6QEwhIfIHPYfe0ZTQZcyiYhF%2BOQkRKrLeKL7kdpDKFGhfZdw9pfMR33ojvzIu5buT4dK1r3lpb2Jpnr2i6LXJhgvUwWL%2BIMC4hEbfJQfer3foOEcFpfEflAY24tUE5riM%2FmTiKOYqJbgSmNMW9cJWi07EzRhBXgoc72Fd7Q3yY9gg%2BZhfDhzmFD8%2FQFp52a2rAP%2FMbJTS15XAT8u6hLb3kTJedb%2B47w%2Bk7b4SXx2j5cq6bPpSVm87jR2r26JEadGJkA2Znp25Dr4UAw35HBNh9BMyGCHgjcodm87dbXf01f15at%2F8D)
-1. [Sample Branching Workflow](https://viewer.diagrams.net/?highlight=0000ff&edit=_blank&layers=1&nav=1#R7Vzdc9o4EP9rPHP3QMff2I8JhLYzl7vO5W7S9k3YMrgxliuLEPrXV7Ilf4NNwJhJeAF7Ja3W%2B9tda1cCSZusXj5iEC3vkQsDSZXdF0mbSqqq6PaYfjHKNqXYspoSFth3U5KcEx78X5CPFNS178KY01ISQSggflQmOigMoUNKNIAx2pS7eShwS4QILGCN8OCAoE599F2yTKmWIef0T9BfLMXMisxbVkB05oR4CVy0KZC0O0mbYIRIerV6mcCAKa%2Bsl9mO1kwwDEPSZcBPH1kPNzb%2BNP%2F0%2FfN38v8%2F44%2FGSFE5Ps8gWPNH5uKSrdABRuvQhYyNLGm3m6VP4EMEHNa6oahT2pKsAnqn0Mu6WFzSZ4gJfCmQuJgfIVpBgre0i2i10xHcZHRLS%2B83OQC6wbkuC8rXNJMDz0FfZKxzvdALrpoD1GSoNS09gFVEFaTKjwg%2FeQFFllkXpp9%2Ff7lPNOA8UeOIa9qkSiBllYHAX4T02qEqg5gSmKp8aoM3vGHluy4bfoth7P8C84QVwyJCfkiShzVuJWPKeK0JilMvYqw9FJIZWPkB0%2BV%2FYIlWgFO5qykmY5sBfCIE7QqCtlJDUB3LdQSFr5weQL1u5jNZsmcSFe3Gku5UyZIlayqIqnR7ewByAfTIyXHjCBnyThxPjptmlXAzjDpuTbCNe4PNaIhOZkC4RkrwmD%2FXSDSMUlXe0A6KHr3kjfRqwb4ntBP251TpWDCkAqY80x4Xgr6iD4e%2BXkffPqfT2idAX6XoJ7BXDOARMAjYYiJ5Vz%2BxSA7YOmIJwiRqJy3pZHN81FSSLjvbiBoB5TrbwDl7M4zofYSRQ4kXb4HqYBZoqnULVJpMUOvLBJXsGeo2iILjzIKzCfyj2EyW0Hli9orWzCPuQczWELlFMfYnnfA%2Bd5Y5BqGzlFgAEL1mEJA1Zh2U0tDexJmixFtjCjgzFLoY63O2CabPl0zInv%2FLv0wJIKYuQPXvecnnWUB4XMJQCOAzcVYQL5gYEwENIonfJCaeNBbkkqn3s7g3hc8wQFGfgn5m%2BiBLmJiELxQnBKJZmxf4NG3L5M6fyElU7e4VjhKZF3aOlzHB6AlOUECDrjYNUciCo0eVVCF1jatNuVD7klvtZcldjpzjhhW32RA5s3y1h9BZT5pqEC2oHqIj1JEl%2FvzlJhVz6yY1qUZZUU2LHFUIWnrFGOO%2BFKVo7YqCoXvDyhm5jdbMWtQntE5GyNaVlOmMRYh0UQCDecJfWDsjuQv4wIVAmCzRAoUguMupt%2FDFJ18Zgw8Gv%2FvGhWLX0xfBm91s%2BU3R4SRV8zxPdZyad9IW15ybBktQnTV%2B3mMd%2B%2B2PPcReDypAbzS4iKBhGADiP5cLQ03WwGf4whZZhbWN%2FsEom17F82K0xg7ko4qFnDZGNU4E0PBKapwS68ye%2BxiDbcim35HBuga0XL3JYC11rplvxWAVc1wys2qW39VeKZ%2BKwSpKN4OlBgS2hW48bdkncWUmWz5QtPIAepHKcGL%2FaUoq3o%2F%2FeB40dwT8sT2X5WP9JzXMPT2t8WU5mlK1wte%2BGuwqo3O%2FGsQSsyFdpqx5gQXHkJSSiTXxRtbuDDmOQNiYhDip5bAEBC%2Fmf8hpGiG%2B%2FkxYyEmm4nHXYF2J8A7RWCkWFhrSGVlLiPAq8RDRtuF2wBr1xGbpRQAJTbBGVGDHDxf1kSwjGfl0pRvykbKYL2khNJ%2BOPdpfjEz8Pk1r3TLXbOAcOE%2BLZPk8qqhD1a1UE6pu8wtDKMX14ygAXCF%2BGPhiJi9AgFSmr1cAKultCtD%2BLKxl26hbyfOAnZKWhKIlZemcdpmq8LiWGnlvJSpzd4XqAMfJfSd3H1Xe4UGUEVhFSaOmMbS4S1XJZQ7Vmry819NKzVVnU9v9TXRpdLlSY5PXiQ7NjidaW3xPLrifXPBAMbzJCbPHrvhhve5Vq928Ry8cyunGHWoblI0fxbvKREWt78iHu63pamjsTqPPFRTV8iJEoFAssZxzH2ncJV29gsVfaEOD1bTpdwWrGSx7aLDMK1idwVLUgdGyri%2BtA9Ayhkary67AFS3e2Roaresaozta2enewdC6LjIOQEsfGq0OZ5RfjxavRZ8MLVHCPhdaVrXmO3S6ZVlXuA6Aa%2BiEy7KvcB0A19Apl6KYdXhad%2B1%2BrFeR6MHr%2BnnHvxCKuCJ%2FQEK2HBJ2JLSMLtUu3n4t3rC9P74TyG7z7b%2FkbpvdVfcyyxuRr93OvLR9RWEb6Sbbno4ib27dgOy8s3jcQeumXbtrCNgVAgZP5O0u5weueF1MKm%2F3Wnh5e3gNnczbvZZe3hxeg6fzdq%2FFl7eH19AJfXbo%2FHpm4V2dWSj%2FOOg9n1sQrdrgx4cU8YcMfQTP7KT0qYJndsD6bAgZl1avURSjjs%2FZCgD9JPKvLSyc%2BWB%2BawFAkfnKsbUCIFLu01UAOh%2B%2B73gk%2BPjD94p2jsP0co%2BL9bcYvwYvYMpdCs79%2FPqBR5lCYPlWiis7y5fH%2FmrizIGqNfxkb%2F1L%2BQmEXDVTvWJ%2FXX8CMdJeG%2FDaIxK9zf%2BvKe2e%2F%2BuVdvcb)
diff --git a/assets/branching-diagram.png b/assets/branching-diagram.png
deleted file mode 100644
index 3d404987d9..0000000000
Binary files a/assets/branching-diagram.png and /dev/null differ
diff --git a/assets/sample-workflow.png b/assets/sample-workflow.png
deleted file mode 100644
index bb9e567e7f..0000000000
Binary files a/assets/sample-workflow.png and /dev/null differ
diff --git a/browser-versions.json b/browser-versions.json
index 3152d13386..54fa8578db 100644
--- a/browser-versions.json
+++ b/browser-versions.json
@@ -1,5 +1,5 @@
{
- "chrome:beta": "105.0.5195.28",
- "chrome:stable": "104.0.5112.101",
+ "chrome:beta": "106.0.5249.30",
+ "chrome:stable": "105.0.5195.125",
"chrome:minimum": "64.0.3282.0"
}
diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts
index d5bec83594..3087e4074e 100644
--- a/cli/types/cypress.d.ts
+++ b/cli/types/cypress.d.ts
@@ -641,6 +641,12 @@ declare namespace Cypress {
*/
off: Actions
+ /**
+ * Used to import dependencies within the cy.origin() callback
+ * @see https://on.cypress.io/origin
+ */
+ require: (id: string) => any
+
/**
* Trigger action
* @private
@@ -3008,6 +3014,7 @@ declare namespace Cypress {
// Internal or Unlisted at server/lib/config_options
namespace: string
projectRoot: string
+ repoRoot: string | null
devServerPublicPathRoute: string
cypressBinaryRoot: string
}
@@ -3081,18 +3088,21 @@ declare namespace Cypress {
type DevServerFn = (cypressDevServerConfig: DevServerConfig, devServerConfig: ComponentDevServerOpts) => ResolvedDevServerConfig | Promise
+ type ConfigHandler = T
+ | (() => T | Promise)
+
type DevServerConfigOptions = {
bundler: 'webpack'
framework: 'react' | 'vue' | 'vue-cli' | 'nuxt' | 'create-react-app' | 'next' | 'svelte'
- webpackConfig?: PickConfigOpt<'webpackConfig'>
+ webpackConfig?: ConfigHandler>
} | {
bundler: 'vite'
framework: 'react' | 'vue' | 'svelte'
- viteConfig?: Omit, undefined>, 'base' | 'root'>
+ viteConfig?: ConfigHandler, undefined>, 'base' | 'root'>>
} | {
bundler: 'webpack',
framework: 'angular',
- webpackConfig?: PickConfigOpt<'webpackConfig'>,
+ webpackConfig?: ConfigHandler>,
options?: {
projectConfig: AngularDevServerProjectConfig
}
diff --git a/cypress-prioritization-and-triage.md b/cypress-prioritization-and-triage.md
new file mode 100644
index 0000000000..43cd974135
--- /dev/null
+++ b/cypress-prioritization-and-triage.md
@@ -0,0 +1,139 @@
+# The Cypress App Prioritization and Triage Methodology
+
+At Cypress, we love our open source community. We work every day to grow our partnership with the community through open, honest, and straightforward communication about our processes and plans for the platform. The goal of this document is to provide resources that show our plans for the future and serve as a guide to our processes around handling issues that are raised by the open source community.
+
+## Table of Contents
+
+- [The Cypress App Priorities](#the-cypress-app-priorities)
+- [The Cypress App Roadmap](#the-cypress-app-roadmap)
+- [The Cypress App Issue Triage](#the-cypress-app-issue-triage)
+ - [The Cypress App Triage Process](#the-cypress-app-triage-process)
+ - [The Power of a Great Ticket](#the-power-of-a-great-ticket)
+- [How We Prioritize Issues](#how-we-prioritize-issues)
+ - [Cypress Relative Priority Score](#cypress-relative-priority-score)
+ - [Cypress Relative Priority to Effort Score](#cypress-relative-priority-to-effort-score)
+- [FAQs](#faqs)
+ - [Why Isn't Anyone Working on My Issue?](#why-isnt-anyone-working-on-my-issue)
+ - [Why Did You Close My Ticket?](#why-did-you-close-my-ticket)
+ - [Backpatching Strategy/Limitations](#backpatching-strategylimitations)
+- [How You Can Help](#how-you-can-help)
+
+
+## The Cypress App Priorities
+
+The Cypress app is constantly looking to improve and grow our testing platform to meet the ever-growing needs of the testing world. Like any software platform, we must balance research, development, and maintenance against real-world resource constraints. We feel that transparency about our future plans will help us grow a stronger relationship with end users who understand that all choices involve trade-offs.
+
+## The Cypress App Roadmap
+
+[The Cypress App Priorities](https://github.com/orgs/cypress-io/projects/13/views/1) board shows the high-level roadmap at Cypress.
+
+The board flows from left to right as a project moves from the ideation and feasibility phase all the way to General Availability (GA) release.
+
+Here is a guide to what each column on the board represents:
+
+| Column Name | Description |
+ | :------------------------------------ | :---------------------- |
+ | **Under Consideration** | These tickets are currently being discussed for future work. We are investigating the complexity of the change, the amount of resources necessary to complete it, and how it aligns with other goals for the platform. It is important to note that **not all** items in this column make their way into our scheduled work. It is possible that, upon investigation, a ticket falls in priority due to other goals or constraints. |
+| **Planned** | These are the tickets we are planning to work on, but the work has not yet started. |
+| **In Progress** | These tickets are actively being worked on. |
+| **Experimental** | If a ticket involves a substantial refactoring of the codebase, is large in scope or is a complex feature, we often release them in a turned-off state. End users can opt in to experiment with these changes via feature flags. Work is still ongoing, but we feel it is important to get this feature in users' hands. Feedback is always welcome, but it is especially helpful during this phase of the development cycle. Not all issues are released under the experimental flag before GA. |
+| **Released** | Once an issue has been fully implemented, tested, and bundled into an official release it moves to this column. Features in this column are no longer experimental, feature flags are removed and default behaviors are implemented. |
+
+## The Cypress App Issue Triage
+
+Whenever an issue is created in the [Cypress repo](https://github.com/cypress-io/cypress/) it is added to the [Cypress App Triage Board](https://github.com/orgs/cypress-io/projects/9/views/1). This board represents all issues we are actively working to investigate and reproduce before routing them to the appropriate team for [prioritization](#how-we-prioritize-issues). Prioritization does not necessarily mean that an issue will be worked on - priority scoring simply helps us create a relative ordering of potential work that is ranked according to standards we have defined as most important to Cypress and the community.
+
+### The Cypress App Triage Process
+
+At Cypress, we use two-week sprints. During each sprint, a rotating group of developers is assigned to the triage team. Their responsibilities are only to focus on triaging issues entered into the [Cypress repo](https://github.com/cypress-io/cypress/). Each issue is assigned to a team member for analysis and assessment and follows this general path:
+
+- **Assessment** - The goal of this step is to do a high-level analysis of the problem. If the issue is clear and the probable impact of the ticket is large in both scope and severity and affecting many users, we will internally escalate the issue and work to get it resolved as quickly as possible. During this phase we will often try and suggest workarounds that may help you get around the issue in the short term while we investigate further. This may not always be possible, but we will try our best to make sure you are not totally impeded.
+
+- **Reproduction** - The goal of this phase is to fully understand the issue described in the ticket and, most importantly, replicate the issue. The reason issue replication is so important to us at Cypress is that we believe our teams' development time is best spent on issues we can verify. In order to know if any solution solves a specific problem, we first need to be able to reliably reproduce it. A vast majority of our time in triage is spent trying to reproduce the issues our users are encountering. With a huge spectrum of users using countless permutations of hardware, operating systems, versions of Cypress, Node.js, CI configurations, unique web applications they are testing, and more, it can be very challenging to narrow down problems into something that is ready for prioritization by a team. This is where our end users can help us the most. The easier it is for us to recreate your issue, the sooner we can route it to our teams for prioritization. The best way to provide a reproducible example of your problem is to fork our [cypress-test-tiny repo](https://github.com/cypress-io/cypress-test-tiny) and replicate the issue there. At a minimum we will need a [short, self contained, correct example](http://sscce.org/) in order to assist in most cases.
+
+- **Routing** - Once we have clarified any questions we have on a submitted ticket and we are able to replicate the problem internally, the ticket is ready to be routed to the appropriate team at Cypress.
+
+- **Prioritization** - Teams meet regularly to review tickets that have been routed to them via triage. They evaluate each ticket based on a number of criteria outlined in our [prioritization rubric](#prioritization-rubric). Each dimension of our prioritization rubric carries a weight. The following equation is then used to give us a **relative priority**. The relative priority score is then divided by the estimated effort to address the ticket to give us a **priority-to-effort** score which we then use to determine which tickets are the highest priority.
+**Important note - These priorities do not dictate when a team will be able to start work. This simply gives teams the data they need to make informed prioritization choices.**
+
+- **Resolution and Verification** - Once an issue has been picked up for work, it will have the label **Stage: Under Development** attached. Developers will then begin work on the issue. Sometimes the scope of the work will be greater than the ticket submitted, and a parent ticket will be created to encapsulate the entire scope of the work. A link to the original ticket will be created in the new ticket.
+
+- **Release** - Once work has been finished, the ticket will be scheduled for release. If the changes are non-breaking, we generally aim to include the work in the next minor release every-other Tuesday. If the changes made to address the issue require breaking changes, the issue will be scheduled for our next major release roughly every quarter.
+
+### The Power of a Great Ticket
+
+One of the best ways to help get your ticket validated, replicated, routed and prioritized is to include as much information as possible. Our issue template will walk you through the most common information that we will need to best troubleshoot the problem. **Please do not open issues from GitHub discussion comments.**
+
+Here are some tips for providing a [Short, Self Contained, Correct, Example](http://sscce.org/) and our own [Troubleshooting Cypress](https://on.cypress.io/troubleshooting) guide. Another great way to assist us in replicating your issue is to fork our [cypress-test-tiny repo](https://github.com/cypress-io/cypress-test-tiny) and recreate the issue there.
+
+We will always need replication steps, so please include them when submitting an issue. Going back and forth to gather the basic data is all time we could be using to investigate and address the issue, so please be considerate of our developers' time and include the requested details when submitting an issue.
+
+## How We Prioritize Issues
+
+At Cypress, we use the following guidelines to help us standardize the importance of every ticket submitted. Of course there are subjective interpretations for each of these fields, but the goal is that each ticket is considered and examined thoroughly in a standardized way. Once values have been determined for each field, a total priority is calculated. It is important to note that these values are **a guide** to make informed decisions around what issues bring the most value to the community and Cypress as an organization. They are relative and fuzzy and not to be treated as gospel. It is also important to understand that these values may change as future factors and circumstances change.
+
+### Prioritization Rubric
+
+Here are the criteria we use to gauge relative issue priority:
+
+| Criteria | Weight | Description |
+|:---------------------|:---------:|:-----------------------------------------------------------------------------------|
+| Scope | 0.7 | How many users does this affect? |
+| Severity | 1.0 | What does the problem prevent users from doing? Is it an edge case? A primary test flow? A minor annoyance? |
+| Visibility | 0.5 | Is this an important issue to our community? Is there a lot of discussion around it? |
+| Does Workaround Exist | 0.4 | Does a work around exist that is reasonable? |
+| Regression | 0.7 | Did this previously work? How long ago did it last work? Was this a regression within this last (current) major version? |
+| Cypress Priority | 0.75 | Does Cypress have a vested interest in resolving this issue? For example, does it ease the support burden for our staff? Is this part of a corporate milestone or objective? |
+| Effort | 0.75 | How much effort is needed to resolve the issue? Estimates are for a single developer being assigned to the issue. |
+
+### Cypress Relative Priority Score
+
+The formula for calculating the Cypress Relative Priority Score (CRPS) is the weighted sum of our priority criteria:
+
+$$ \text{Cypress Relative Priority Score} = {{(Scope \times 0.7) + (Severity \times 1.0) + (Visibility \times 0.5) + (WorkAround \times 0.4) + (Regression \times 0.7) + (CypressPriority \times 0.75)}} $$
+
+### Cypress Relative Priority to Effort Score
+
+It is important to remember that just because an issue has a high CRPS, that doesn't mean it is necessarily the best way to allocate limited resources. To determine which gives us the greatest bang for the proverbial buck, we weigh CRPS versus the effort required to address the issue.
+
+$$ \text{Cypress Relative Priority to Effort Score} = {\text{Cypress Relative Priority Score} \over {Effort}} $$
+
+**Cypress Relative Priority to Effort Score** gives us a metric that better represents the effort-to-reward ratio for any proposed work.
+
+## FAQs
+
+### Why isn't anyone working on my issue?!
+
+We understand that it can be frustrating to take time to submit an issue, only to see it sit in the backlog untouched for a long period of time. We truly wish we could take up every single issue that comes in, but given the relatively small size of our internal teams and the large and varied user base of Cypress it just isn't possible to solve every issue. Our prioritization process helps us float the most important issues to the top based on the impact vs effort ratio of any given issue. It is important to remember that, just because an issue is not being actively worked on, does not mean we are ignoring the issue; it means that we have other issues we feel are more impactful to the user base and are prioritizing those issues first.
+
+Even when our internal developers cannot work on your issue, that does not mean all hope is lost! Being an open source project means you can be part of the solution. We love when our community commits code and want to encourage everyone to feel empowered to contribute and make the product we all love and use even better. We have guides on [how to contribute](https://github.com/cypress-io/cypress/blob/develop/CONTRIBUTING.md) and a very active [Discord community](https://discord.gg/cypress) which can help if you are interested in opening a PR.
+
+### Why did you close my ticket?
+
+There are a number of reasons why a ticket may be closed without any change or PR being opened.
+
+- **No Response From Author** - The most common reason is lack of response from the author. Our issue creation template prompts the user for many details that are vital in debugging and replicating an issue. It is not uncommon for issues to be entered with insufficient information for our teams to properly investigate an issue. We will often reach out for more details, but we do not have the bandwidth to chase down users for information. **If we do not receive a response within 7 days, we will close your ticket.** The best way to help get your issue worked on by a Cypress team member is to provide the information requested and give as much detail as possible (or, even better, a reproducible example in our [cypress-tiny repo](https://github.com/cypress-io/cypress-test-tiny)) in a timely manner.
+
+- **Not a bug or feature request** - Issues entered into the [Cypress repo](https://github.com/cypress-io/cypress) are for bugs and feature requests for the Cypress App only. Updates to [documentation](https://github.com/cypress-io/cypress-documentation), our [example-kitchensink app](https://github.com/cypress-io/cypress-example-kitchensink), or [another repository](https://github.com/cypress-io) should be made in the appropriate repository.
+The best place for asking questions is our [Discord server](https://discord.gg/cypress) which has a very active community of folks with a diverse set of knowledge. Other available channels to explore include [Cypress GitHub discussions](https://github.com/cypress-io/cypress/discussions), [community chat](https://on.cypress.io/chat), and [Stack Overflow](https://stackoverflow.com/questions/tagged/cypress).
+We also offer support via email with our [paid plans](https://www.cypress.io/pricing/).
+
+- **Feature request for Cypress Dashboard** - Thank you for your support as a Cypress Dashboard user! These issues are routed to our Cypress Dashboard team's ticketing system. Your customer success representative is available for follow-up and will reach out you directly via email if more information is needed.
+
+- **The fix or feature is not within our vision for Cypress** - There will inevitably be suggestions that will not fit within the scope of Cypress' vision for our product. We will do our best to explain why we will not be addressing this issue.
+
+- **It's a dupe** - The issue you have entered has already been logged by another person. We will link the appropriate ticket and mark the issue as a duplicate.
+
+- **Cannot reproduce ecosystem** - Another common issue that can lead to a ticket being closed is that the setup involved in reproducing the problem is complex and specific to your implementation of Cypress. As any developer can imagine, the variety of ecosystems that run Cypress are as varied as the number of flowers in the world. And not surprisingly, we do not have infrastructure setup to mimic every possible scenario. It is possible, even likely, that we are not set up to investigate your specific use case of Cypress. This does not mean that the issue is not real and that it is not important. It simply means we are not equipped to investigate it any further. We are more than happy to point you in the direction of resources that may help you dive into your problem further on your own but we will not be able to replicate your entire stack internally to properly reproduce your problem. Without a consistent reproduction of the issue, it is highly unlikely an issue will be prioritized by a team for inclusion in their sprint work. These types of issues are a fantastic opportunity for our community to contribute. By opening a PR to address an issue you are encountering that is otherwise not being worked on, you are not only helping yourself and your organization but potentially anyone else who may be encountering your issue as well but has not spoken up about it. Cypress developers are always willing to help clean and prep a PR from the community to help you get it over the line and merged into the code base. Just open a PR and we will automatically see it on our triage board! If you have questions along the way, always remember we have a very active and helpful community in [Discord](https://discord.gg/cypress).
+
+- **Stumped, for now** - Sometimes an author provides all the details we could ask for, is very responsive, and uses a straightforward setup - and it still stumps the Cypress devs! The reality is that there are some issues we just can't figure out because of limited resources. We love our large and active community, but in order to support it, sometimes we must move on. We understand this is not something an active issue author wants to hear, but we do not have the resources to put infinite time and energy into every single issue until it is resolved. During the course of our investigation we will attempt to understand the scope of the users being affected by the problem in the issue. If we determine this is an edge case or of low impact to effected users, we will close the issue until such a time that more information comes to light that sheds new insight into the potential root of the problem. This is another example of a great opportunity for folks in the community to give back and open a PR. Nothing makes our day better than seeing a PR from the community! Opening a PR will automatically add that issue to our triage board where a Cypress dev will help you get the PR over the line and merged into the repo for release. And remember to drop into [Discord](https://discord.gg/cypress) if you have questions or need a helping hand.
+
+### Backpatching Strategy/Limitations
+
+At Cypress, we have a roll-forward approach to support. If you are encountering an issue while using an older version of Cypress, our first step will be to verify the problem is still happening on the latest version of the app. If you are unable to upgrade, we will want to understand what blockers are keeping you from upgrading. We want to understand friction points so that we can build a tool that is easy to stay current on. As such, we will only be backpatching fixes on an ad hoc basis. We will use the [prioritization rubric](#prioritization-rubric) to assess the issue's severity/impact and we will consider the reasons that users may be blocked from upgrading to a newer version of Cypress.
+
+## How You Can Help
+
+One of the pillars of Cypress' success is our community. Without your input and support we would not be the platform we are today. We wish we had the bandwidth to address every single issue that comes in, but the reality is that there simply isn't enough time in the day for our internal teams to give every single ticket the love and attention it deserves.
+
+This is where we hope the community can help us. As an open source project, our issue backlogs and source code are all out in the open. Please feel empowered to search those backlogs for issues important to you and add your input. Maybe you can add a reproducible example to a ticket that needs one, or can verify a problem is also happening for you with more detail, or even better - maybe you can [contribute a fix to the repo](https://github.com/cypress-io/cypress/blob/develop/CONTRIBUTING.md)! Your input and engagement is always appreciated.
diff --git a/guides/release-process.md b/guides/release-process.md
index 10f52f058f..dd309fea4c 100644
--- a/guides/release-process.md
+++ b/guides/release-process.md
@@ -2,7 +2,7 @@
These procedures concern the release process for the Cypress binary and `cypress` npm module.
-The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) directory are automatically published to npm (with [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/)) upon being merged into `master`. You can read more about this in [CONTRIBUTING.md](../CONTRIBUTING.md#independent-packages-ci-workflow).
+The `@cypress/`-namespaced NPM packages that live inside the [`/npm`](../npm) directory are automatically published to npm (with [`semantic-release`](https://semantic-release.gitbook.io/semantic-release/)) upon being merged into `develop`. You can read more about this in [CONTRIBUTING.md](../CONTRIBUTING.md#releases).
[Anyone can build the binary and npm package locally](./building-release-artifacts.md), but you can only deploy the Cypress application and publish the npm module `cypress` if you are a member of the `cypress` npm organization.
@@ -68,18 +68,13 @@ of Cypress. You can see the progress of the test projects by opening the status
In the following instructions, "X.Y.Z" is used to denote the [next version of Cypress being published](./next-version.md).
-1. `develop` should contain all of the changes made in `master`. However, this occasionally may not be the case.
- - Ensure that `master` does not have any additional commits that are not on `develop`.
- - Ensure all auto-generated pull requests designed to merge master into develop have been successfully merged.
- - If there are additional commits necessary to merge `master` to `develop`, submit, get approvals on, and merge a new PR
+1. 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. 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. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version.
-3. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](../packages/example) to that new version.
+3. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/develop-/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-/cypress.tgz`, publishing can proceed.
-4. Once the `develop` branch is passing for all test projects with the new changes and the `linux-x64` binary is present at `https://cdn.cypress.io/beta/binary/X.Y.Z/linux-x64/develop-/cypress.zip`, and the `linux-x64` cypress npm package is present at `https://cdn.cypress.io/beta/npm/X.Y.Z/linux-x64/develop-/cypress.tgz`, publishing can proceed.
-
-5. Install and test the pre-release version to make sure everything is working.
+4. Install and test the pre-release version to make sure everything is working.
- Get the pre-release version that matches your system from the latest develop commit.
- Install the new version: `npm install -g `
- Run a quick, manual smoke test:
@@ -89,9 +84,9 @@ 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 dashboard repo.
-6. Log into AWS SSO with `aws sso login --profile `. If you have setup your credentials under a different profile than `prod`, be sure to set the `AWS_PROFILE` environment variable to that profile name for the remaining steps. For example, if you are using `production` instead of `prod`, do `export AWS_PROFILE=production`.
+5. Log into AWS SSO with `aws sso login --profile `. If you have setup your credentials under a different profile than `prod`, be sure to set the `AWS_PROFILE` environment variable to that profile name for the remaining steps. For example, if you are using `production` instead of `prod`, do `export AWS_PROFILE=production`.
-7. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens:
+6. Use the `prepare-release-artifacts` script (Mac/Linux only) to prepare the latest commit to a stable release. When you run this script, the following happens:
* the binaries for `` are moved from `beta` to the `desktop` folder for `` in S3
* the Cloudflare cache for this version is purged
* the pre-prod `cypress.tgz` NPM package is converted to a stable NPM package ready for release
@@ -102,22 +97,22 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
You can pass `--dry-run` to see the commands this would run under the hood.
-8. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`.
+7. Validate you are logged in to `npm` with `npm whoami`. Otherwise log in with `npm login`.
-9. Publish the generated npm package under the `dev` tag, using your personal npm account.
+8. Publish the generated npm package under the `dev` tag, using your personal npm account.
```shell
npm publish /tmp/cypress-prod.tgz --tag dev
```
-10. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output:
+9. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output:
```shell
dist-tags:
dev: 3.4.0 latest: 3.3.2
```
-11. Test `cypress@X.Y.Z` to make sure everything is working.
+10. Test `cypress@X.Y.Z` to make sure everything is working.
- Install the new version: `npm install -g cypress@X.Y.Z`
- Run a quick, manual smoke test:
- `cypress open`
@@ -126,7 +121,7 @@ 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 dashboard repo.
-12. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. This PR must be merged, built, and deployed before moving to the next step.
+11. Create or review the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). If there is not already a release-specific PR open, create one. This PR must be merged, built, and deployed before moving to the next step.
- Use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub:
```shell
cd packages/issues-in-release
@@ -136,33 +131,35 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
- Merge any release-specific documentation changes into the main release PR.
- You can view the doc's [branch deploy preview](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md#pull-requests) by clicking 'Details' on the PR's `netlify-cypress-docs/deploy-preview` GitHub status check.
-13. Create a PR for a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version. Instructions for updating `cypress-docker-images` can be found [here](https://github.com/cypress-io/cypress-docker-images/blob/master/CONTRIBUTING.md#add-new-included-image). Ensure the docker image is reviewed and has passing tests before preceeding.
+12. Create a PR for a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. Note: we use the base image with the Node version matching the bundled Node version. Instructions for updating `cypress-docker-images` can be found [here](https://github.com/cypress-io/cypress-docker-images/blob/master/CONTRIBUTING.md#add-new-included-image). Ensure the docker image is reviewed and has passing tests before preceeding.
-14. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
+13. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version:
```shell
npm dist-tag add cypress@X.Y.Z
```
-15. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system.
+14. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json). This will also ensure the binary for the version is downloadable for each system.
```shell
yarn binary-release --version X.Y.Z
```
-16. 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. 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).
-17. Merge the new docker image PR created in step 13 to release the image.
+16. Merge the new docker image PR created in step 13 to release the image.
-18. 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).
-19. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release):
+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.
-20. Bump `version` in [`package.json`](package.json), submit, get approvals on, and merge a new PR for the change. After it merges:
+19. Bump `version` in [`package.json`](package.json), submit, get approvals on, and merge a new PR for the change.
+
+20. After the PR to bump the [`package.json`](package.json) version merges:
```shell
git checkout develop
@@ -173,19 +170,17 @@ In the following instructions, "X.Y.Z" is used to denote the [next version of Cy
git push origin vX.Y.Z
```
-21. Submit, get approvals on, and merge a new PR that merges `develop` to `master`. **Important**: make sure to use a merge commit, not a squash merge.
+21. 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.
-22. 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.
-
-23. 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:
+22. 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:
```shell
cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z
```
-24. 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
+23. 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
-25. 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 `master`. 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:
+24. 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)
- [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1)
- [cypress-realworld-app](https://github.com/cypress-io/cypress-realworld-app/issues/41)
diff --git a/guides/testing-other-projects.md b/guides/testing-other-projects.md
index 43a3e5edd1..4864ecc0d0 100644
--- a/guides/testing-other-projects.md
+++ b/guides/testing-other-projects.md
@@ -1,6 +1,6 @@
# Testing other projects
-In `develop`, `master`, and any other branch configured in [the CircleCI config](../.circleci/config.yml), the Cypress binary and npm package are built and uploaded to `cdn.cypress.io`. Then, tests are run, using a variety of real-world example repositories.
+In `develop` and any other branch configured in [the CircleCI config](../.circleci/config.yml), the Cypress binary and npm package are built and uploaded to `cdn.cypress.io`. Then, tests are run, using a variety of real-world example repositories.
Two main strategies are used to spawn these test projects:
diff --git a/npm/README.md b/npm/README.md
index 44d7a303b9..c9fdf73222 100644
--- a/npm/README.md
+++ b/npm/README.md
@@ -2,5 +2,5 @@
This directory contains packages that are both used internally inside the Cypress monorepo [`packages`](../packages) and also published independently on npm under the Cypress organization using the `@cypress` prefix. For example, `vite-dev-server` is published as `@cypress/vite-dev-server`.
-These are automatically released based on [Semantic Version](https://semver.org) commit message prefixes (`feat`, `chore` etc). A package is automatically released when changes are merged into master. You can read more about this process in [`CONTRIBUTING`](../CONTRIBUTING.md#committing-code).
+These are automatically released based on [Semantic Version](https://semver.org) commit message prefixes (`feat`, `chore` etc). A package is automatically released when changes are merged into `develop`. You can read more about this process in [`CONTRIBUTING`](../CONTRIBUTING.md#committing-code).
diff --git a/npm/angular/.releaserc.js b/npm/angular/.releaserc.js
index 7b15992ed7..0ee9f3f0b5 100644
--- a/npm/angular/.releaserc.js
+++ b/npm/angular/.releaserc.js
@@ -1,6 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
- branches: [
- { name: 'master', channel: 'latest' },
- ],
}
diff --git a/npm/angular/README.md b/npm/angular/README.md
index ef4d366a44..8537799fab 100644
--- a/npm/angular/README.md
+++ b/npm/angular/README.md
@@ -78,7 +78,7 @@ Run `yarn build` to compile and sync packages to the `cypress` cli package.
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/angular/package.json b/npm/angular/package.json
index 44d04e3989..72cc5afaec 100644
--- a/npm/angular/package.json
+++ b/npm/angular/package.json
@@ -34,7 +34,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/angular/#readme",
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/angular/#readme",
"author": "Jordan Powell",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fangular&template=1-bug-report.md&title=",
"keywords": [
diff --git a/npm/create-cypress-tests/package.json b/npm/create-cypress-tests/package.json
index 7ecf5f176e..d6b0456c8f 100644
--- a/npm/create-cypress-tests/package.json
+++ b/npm/create-cypress-tests/package.json
@@ -52,5 +52,5 @@
},
"license": "MIT",
"repository": "https://github.com/cypress-io/cypress.git",
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/create-cypress-tests/#readme"
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/create-cypress-tests/#readme"
}
diff --git a/npm/cypress-schematic/.releaserc.js b/npm/cypress-schematic/.releaserc.js
index 7b15992ed7..0ee9f3f0b5 100644
--- a/npm/cypress-schematic/.releaserc.js
+++ b/npm/cypress-schematic/.releaserc.js
@@ -1,6 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
- branches: [
- { name: 'master', channel: 'latest' },
- ],
}
diff --git a/npm/cypress-schematic/package.json b/npm/cypress-schematic/package.json
index 85b9e64250..e1e8341e5e 100644
--- a/npm/cypress-schematic/package.json
+++ b/npm/cypress-schematic/package.json
@@ -35,7 +35,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/cypress-schematic#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/cypress-schematic#readme",
"author": "Cypress DX Team",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fcypress-schematics&template=1-bug-report.md&title=",
"keywords": [
diff --git a/npm/eslint-plugin-dev/README.md b/npm/eslint-plugin-dev/README.md
index 2672a9fe80..dec57fdfa7 100644
--- a/npm/eslint-plugin-dev/README.md
+++ b/npm/eslint-plugin-dev/README.md
@@ -3,7 +3,7 @@
[Internal] Cypress Developer ESLint Plugin
-
+
Common ESLint rules shared by Cypress packages.
diff --git a/npm/eslint-plugin-dev/package.json b/npm/eslint-plugin-dev/package.json
index f01cdbb88b..af268acbf7 100644
--- a/npm/eslint-plugin-dev/package.json
+++ b/npm/eslint-plugin-dev/package.json
@@ -44,7 +44,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/eslint-plugin-dev#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/eslint-plugin-dev#readme",
"bugs": {
"url": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Feslint-plugin-dev&template=bug-report.md"
},
diff --git a/npm/mount-utils/package.json b/npm/mount-utils/package.json
index 04747ef6a3..b171f80da7 100644
--- a/npm/mount-utils/package.json
+++ b/npm/mount-utils/package.json
@@ -27,7 +27,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/mount-utils#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/mount-utils#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?template=1-bug-report.md",
"publishConfig": {
"access": "public"
diff --git a/npm/react/.releaserc.js b/npm/react/.releaserc.js
index 7b15992ed7..0ee9f3f0b5 100644
--- a/npm/react/.releaserc.js
+++ b/npm/react/.releaserc.js
@@ -1,6 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
- branches: [
- { name: 'master', channel: 'latest' },
- ],
}
diff --git a/npm/react/README.md b/npm/react/README.md
index c99ab05b6f..23582443e4 100644
--- a/npm/react/README.md
+++ b/npm/react/README.md
@@ -112,7 +112,7 @@ Run `yarn test` to execute headless Cypress tests.
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/react/package.json b/npm/react/package.json
index 32ef2e880c..2447db256c 100644
--- a/npm/react/package.json
+++ b/npm/react/package.json
@@ -45,7 +45,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/react/#readme",
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/react/#readme",
"author": "Gleb Bahmutov ",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Freact&template=1-bug-report.md&title=",
"keywords": [
diff --git a/npm/react18/.releaserc.js b/npm/react18/.releaserc.js
index 7b15992ed7..0ee9f3f0b5 100644
--- a/npm/react18/.releaserc.js
+++ b/npm/react18/.releaserc.js
@@ -1,6 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
- branches: [
- { name: 'master', channel: 'latest' },
- ],
}
diff --git a/npm/react18/package.json b/npm/react18/package.json
index 88cda1925d..527c1dd278 100644
--- a/npm/react18/package.json
+++ b/npm/react18/package.json
@@ -38,7 +38,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/react18/#readme",
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/react18/#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Freact18&template=1-bug-report.md&title=",
"keywords": [
"react",
diff --git a/npm/svelte/README.md b/npm/svelte/README.md
index 3f1e6be7b4..9c487846b8 100644
--- a/npm/svelte/README.md
+++ b/npm/svelte/README.md
@@ -76,7 +76,7 @@ Run `yarn test` to execute headless Cypress tests.
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/svelte/package.json b/npm/svelte/package.json
index 14ffdaf8dc..232eb02a95 100644
--- a/npm/svelte/package.json
+++ b/npm/svelte/package.json
@@ -28,7 +28,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/svelte/#readme",
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/svelte/#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fsvelte&template=1-bug-report.md&title=",
"keywords": [
"cypress",
diff --git a/npm/vite-dev-server/README.md b/npm/vite-dev-server/README.md
index 2c818dae12..967a18f6a7 100644
--- a/npm/vite-dev-server/README.md
+++ b/npm/vite-dev-server/README.md
@@ -58,7 +58,7 @@ We then merge the sourced config with the user's vite config, and layer on our o
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts b/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts
index dcd43fb8fd..55a660e566 100644
--- a/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts
+++ b/npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts
@@ -58,4 +58,20 @@ describe('Config options', () => {
cy.waitForSpecToFinish()
cy.get('.passed > .num').should('contain', 1)
})
+
+ it('supports viteConfig as an async function', () => {
+ cy.scaffoldProject('vite2.9.1-react')
+ cy.openProject('vite2.9.1-react', ['--config-file', 'cypress-vite-async-function-config.config.ts'])
+ cy.startAppServer('component')
+
+ cy.visitApp()
+ cy.contains('App.cy.jsx').click()
+ cy.waitForSpecToFinish()
+ cy.get('.passed > .num').should('contain', 1)
+ cy.withCtx(async (ctx) => {
+ const verifyFile = await ctx.file.readFileInProject('wrote-to-file')
+
+ expect(verifyFile).to.eq('OK')
+ })
+ })
})
diff --git a/npm/vite-dev-server/package.json b/npm/vite-dev-server/package.json
index 51fbb3a171..cafa1ab593 100644
--- a/npm/vite-dev-server/package.json
+++ b/npm/vite-dev-server/package.json
@@ -41,7 +41,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/vite-dev-server#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/vite-dev-server#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?template=1-bug-report.md",
"module": "dist/index.js",
"publishConfig": {
diff --git a/npm/vite-dev-server/src/devServer.ts b/npm/vite-dev-server/src/devServer.ts
index 287a94b1b8..ab47c05c28 100644
--- a/npm/vite-dev-server/src/devServer.ts
+++ b/npm/vite-dev-server/src/devServer.ts
@@ -1,4 +1,5 @@
import debugFn from 'debug'
+import type { InlineConfig, UserConfig } from 'vite'
import { getVite, Vite } from './getVite'
import { createViteDevServerConfig } from './resolveConfig'
@@ -6,6 +7,8 @@ const debug = debugFn('cypress:vite-dev-server:devServer')
const ALL_FRAMEWORKS = ['react', 'vue'] as const
+type ConfigHandler = UserConfig | (() => UserConfig | Promise)
+
export type ViteDevServerConfig = {
specs: Cypress.Spec[]
cypressConfig: Cypress.PluginConfigOptions
@@ -13,7 +16,7 @@ export type ViteDevServerConfig = {
onConfigNotFound?: (devServer: 'vite', cwd: string, lookedIn: string[]) => void
} & {
framework?: typeof ALL_FRAMEWORKS[number] // Add frameworks here as we implement
- viteConfig?: unknown // Derived from the user's webpack
+ viteConfig?: ConfigHandler // Derived from the user's vite config
}
export async function devServer (config: ViteDevServerConfig): Promise {
diff --git a/npm/vite-dev-server/src/resolveConfig.ts b/npm/vite-dev-server/src/resolveConfig.ts
index 2b798bb150..ecc56c8a4c 100644
--- a/npm/vite-dev-server/src/resolveConfig.ts
+++ b/npm/vite-dev-server/src/resolveConfig.ts
@@ -6,7 +6,7 @@
import debugFn from 'debug'
import { importModule } from 'local-pkg'
import { relative, resolve } from 'pathe'
-import type { InlineConfig } from 'vite'
+import type { InlineConfig, UserConfig } from 'vite'
import path from 'path'
import { configFiles } from './constants'
@@ -90,7 +90,15 @@ export const createViteDevServerConfig = async (config: ViteDevServerConfig, vit
].filter((p) => p != null),
}
- const finalConfig = vite.mergeConfig(viteBaseConfig, viteOverrides as Record)
+ let resolvedOverrides: UserConfig = {}
+
+ if (typeof viteOverrides === 'function') {
+ resolvedOverrides = await viteOverrides()
+ } else if (typeof viteOverrides === 'object') {
+ resolvedOverrides = viteOverrides
+ }
+
+ const finalConfig = vite.mergeConfig(viteBaseConfig, resolvedOverrides)
debug('The resolved server config is', JSON.stringify(finalConfig, null, 2))
diff --git a/npm/vite-dev-server/test/resolveConfig.spec.ts b/npm/vite-dev-server/test/resolveConfig.spec.ts
index 53f932a7ee..c4182607e5 100644
--- a/npm/vite-dev-server/test/resolveConfig.spec.ts
+++ b/npm/vite-dev-server/test/resolveConfig.spec.ts
@@ -1,10 +1,14 @@
-import { expect } from 'chai'
+import Chai, { expect } from 'chai'
import { EventEmitter } from 'events'
import * as vite from 'vite'
import { scaffoldSystemTestProject } from './test-helpers/scaffoldProject'
import { createViteDevServerConfig } from '../src/resolveConfig'
+import sinon from 'sinon'
+import SinonChai from 'sinon-chai'
import type { ViteDevServerConfig } from '../src/devServer'
+Chai.use(SinonChai)
+
const getViteDevServerConfig = (projectRoot: string) => {
return {
specs: [],
@@ -21,6 +25,29 @@ const getViteDevServerConfig = (projectRoot: string) => {
describe('resolveConfig', function () {
this.timeout(1000 * 60)
+ it('calls viteConfig if it is a function, passing in the base config', async () => {
+ const viteConfigFn = sinon.spy(async () => {
+ return {
+ server: {
+ fs: {
+ allow: ['some/other/file'],
+ },
+ },
+ }
+ })
+
+ const projectRoot = await scaffoldSystemTestProject('vite-inspect')
+ const viteDevServerConfig = {
+ ...getViteDevServerConfig(projectRoot),
+ viteConfig: viteConfigFn,
+ }
+
+ const viteConfig = await createViteDevServerConfig(viteDevServerConfig, vite)
+
+ expect(viteConfigFn).to.be.called
+ expect(viteConfig.server.fs.allow).to.include('some/other/file')
+ })
+
context('inspect plugin', () => {
it('should not include inspect plugin by default', async () => {
const projectRoot = await scaffoldSystemTestProject('vite-inspect')
diff --git a/npm/vue/.releaserc.js b/npm/vue/.releaserc.js
index 4cb1091bc4..0ee9f3f0b5 100644
--- a/npm/vue/.releaserc.js
+++ b/npm/vue/.releaserc.js
@@ -1,7 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
- branches: [
- // this one releases v3 on master on the latest channel
- 'master',
- ],
}
diff --git a/npm/vue/README.md b/npm/vue/README.md
index 378c7ef0d7..2ac9c98ba1 100644
--- a/npm/vue/README.md
+++ b/npm/vue/README.md
@@ -81,7 +81,7 @@ Run `yarn test` to execute headless Cypress tests.
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/vue/package.json b/npm/vue/package.json
index ade741d517..67ce209c99 100644
--- a/npm/vue/package.json
+++ b/npm/vue/package.json
@@ -50,7 +50,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/vue/#readme",
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/vue/#readme",
"author": "Gleb Bahmutov ",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fvue&template=1-bug-report.md&title=",
"keywords": [
diff --git a/npm/vue/src/index.ts b/npm/vue/src/index.ts
index e00871f668..9a5778c574 100644
--- a/npm/vue/src/index.ts
+++ b/npm/vue/src/index.ts
@@ -17,8 +17,9 @@ import type {
ComponentPropsOptions,
ComponentOptionsWithArrayProps,
ComponentOptionsWithoutProps,
+ Prop,
} from 'vue'
-import type { MountingOptions, VueWrapper } from '@vue/test-utils'
+import type { MountingOptions as VTUMountingOptions, VueWrapper } from '@vue/test-utils'
import {
injectStylesBeforeElement,
StyleOptions,
@@ -44,7 +45,7 @@ export { VueTestUtils }
const DEFAULT_COMP_NAME = 'unknown'
-type GlobalMountOptions = Required>['global']
+type GlobalMountOptions = Required>['global']
// when we mount a Vue component, we add it to the global Cypress object
// so here we extend the global Cypress namespace and its Cypress interface
@@ -58,7 +59,7 @@ declare global {
}
}
-export type CyMountOptions = Omit, 'attachTo'> & {
+type MountingOptions = Omit, 'attachTo'> & {
log?: boolean
/**
* @deprecated use vue-test-utils `global` instead
@@ -69,6 +70,8 @@ export type CyMountOptions = Omit,
}
} & Partial
+export type CyMountOptions = MountingOptions
+
Cypress.on('run:start', () => {
// `mount` is designed to work with component testing only.
// it assumes ROOT_SELECTOR exists, which is not the case in e2e.
@@ -89,38 +92,79 @@ Cypress.on('run:start', () => {
})
/**
- * the types for mount have been copied directly from the VTU mount
- * https://github.com/vuejs/vue-test-utils-next/blob/master/src/mount.ts
+ * The types for mount have been copied directly from the VTU mount
+ * https://github.com/vuejs/vue-test-utils-next/blob/master/src/mount.ts.
*
- * If they are updated please copy and pase them again here.
+ * There isn't a good way to make them generic enough that we can extend them.
+ *
+ * In addition, we modify the types slightly.
+ *
+ * `MountOptions` are modifying, including some Cypress specific options like `styles`.
+ * The return type is different. Instead of VueWrapper, it's Cypress.Chainable>.
*/
+type PublicProps = VNodeProps & AllowedComponentProps & ComponentCustomProps
-type PublicProps = VNodeProps & AllowedComponentProps & ComponentCustomProps;
+type ComponentMountingOptions = T extends DefineComponent<
+ infer PropsOrPropOptions,
+ any,
+ infer D,
+ any,
+ any
+>
+ ? MountingOptions<
+ Partial> &
+ Omit<
+ Readonly> & PublicProps,
+ keyof ExtractDefaultPropTypes
+ >,
+ D
+ > &
+ Record
+ : MountingOptions
+
+// Class component (without vue-class-component) - no props
+export function mount(
+ originalComponent: {
+ new (...args: any[]): V
+ __vccOpts: any
+ },
+ options?: MountingOptions & Record
+): Cypress.Chainable>>
+
+// Class component (without vue-class-component) - props
+export function mount(
+ originalComponent: {
+ new (...args: any[]): V
+ __vccOpts: any
+ defaultProps?: Record> | string[]
+ },
+ options?: MountingOptions
& Record
+): Cypress.Chainable>>
// Class component - no props
-export function mount(
+export function mount(
originalComponent: {
new (...args: any[]): V
registerHooks(keys: string[]): void
},
- options?: MountingOptions
-): Cypress.Chainable
+ options?: MountingOptions & Record
+): Cypress.Chainable>>
// Class component - props
-export function mount(
+export function mount(
originalComponent: {
new (...args: any[]): V
props(Props: P): any
registerHooks(keys: string[]): void
},
- options?: CyMountOptions
-): Cypress.Chainable
+ options?: MountingOptions
& Record
+): Cypress.Chainable>>
// Functional component with emits
-export function mount(
+export function mount(
originalComponent: FunctionalComponent,
- options?: CyMountOptions
-): Cypress.Chainable
+ options?: MountingOptions & Record
+): Cypress.Chainable>>
// Component declared with defineComponent
export function mount<
@@ -135,7 +179,7 @@ export function mount<
EE extends string = string,
PP = PublicProps,
Props = Readonly>,
- Defaults = ExtractDefaultPropTypes
+ Defaults extends {} = ExtractDefaultPropTypes
>(
component: DefineComponent<
PropsOrPropOptions,
@@ -151,17 +195,43 @@ export function mount<
Props,
Defaults
>,
- options?: CyMountOptions<
+ options?: MountingOptions<
Partial & Omit,
D
+ > &
+ Record
+): Cypress.Chainable<
+ VueWrapper<
+ InstanceType<
+ DefineComponent<
+ PropsOrPropOptions,
+ RawBindings,
+ D,
+ C,
+ M,
+ Mixin,
+ Extends,
+ E,
+ EE,
+ PP,
+ Props,
+ Defaults
+ >
+ >
>
-): Cypress.Chainable
+>
+
+// component declared by vue-tsc ScriptSetup
+export function mount>(
+ component: T,
+ options?: ComponentMountingOptions
+): Cypress.Chainable>>
// Component declared with no props
export function mount<
Props = {},
RawBindings = {},
- D = {},
+ D extends {} = {},
C extends ComputedOptions = {},
M extends Record = {},
E extends EmitsOptions = Record,
@@ -172,25 +242,31 @@ export function mount<
componentOptions: ComponentOptionsWithoutProps<
Props,
RawBindings,
- D
+ D,
+ C,
+ M,
+ E,
+ Mixin,
+ Extends,
+ EE
>,
- options?: CyMountOptions
-): Cypress.Chainable
+ options?: MountingOptions
+): Cypress.Chainable>> & Record
// Component declared with { props: [] }
export function mount<
PropNames extends string,
RawBindings,
- D,
+ D extends {},
C extends ComputedOptions = {},
M extends Record = {},
E extends EmitsOptions = Record,
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
EE extends string = string,
- Props extends Readonly<{ [key in PropNames]?: any }> = Readonly<
- { [key in PropNames]?: any }
- >
+ Props extends Readonly<{ [key in PropNames]?: any }> = Readonly<{
+ [key in PropNames]?: any
+ }>
>(
componentOptions: ComponentOptionsWithArrayProps<
PropNames,
@@ -204,8 +280,8 @@ export function mount<
EE,
Props
>,
- options?: CyMountOptions
-): Cypress.Chainable
+ options?: MountingOptions
+): Cypress.Chainable>>
// Component declared with { props: { ... } }
export function mount<
@@ -213,7 +289,7 @@ export function mount<
// as constant instead of boolean.
PropsOptions extends Readonly,
RawBindings,
- D,
+ D extends {},
C extends ComputedOptions = {},
M extends Record = {},
E extends EmitsOptions = Record,
@@ -232,14 +308,23 @@ export function mount<
Extends,
EE
>,
- options?: CyMountOptions & PublicProps, D>
-): Cypress.Chainable
+ options?: MountingOptions & PublicProps, D>
+): Cypress.Chainable<
+ VueWrapper<
+ ComponentPublicInstance<
+ ExtractPropTypes,
+ RawBindings,
+ D,
+ C,
+ M,
+ E,
+ VNodeProps & ExtractPropTypes
+ >
+ >
+>
// implementation
-export function mount (
- componentOptions: any,
- options: CyMountOptions = {},
-) {
+export function mount (componentOptions: any, options: any) {
// TODO: get the real displayName and props from VTU shallowMount
const componentName = getComponentDisplayName(componentOptions)
diff --git a/npm/vue/test-tsd/Slots.vue b/npm/vue/test-tsd/Slots.vue
new file mode 100644
index 0000000000..e4c666f0a6
--- /dev/null
+++ b/npm/vue/test-tsd/Slots.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/npm/vue/test-tsd/mount-test.ts b/npm/vue/test-tsd/mount-test.ts
index d787014f22..f4ddecfa0f 100644
--- a/npm/vue/test-tsd/mount-test.ts
+++ b/npm/vue/test-tsd/mount-test.ts
@@ -1,4 +1,4 @@
-import { expectError, expectType } from './index'
+import { expectType } from './index'
import { mount, VueTestUtils } from '../dist'
import * as VTU from '@vue/test-utils'
import { defineComponent } from 'vue'
diff --git a/npm/vue/test-tsd/test.ts b/npm/vue/test-tsd/test.ts
new file mode 100644
index 0000000000..bf7f9d8e93
--- /dev/null
+++ b/npm/vue/test-tsd/test.ts
@@ -0,0 +1,4 @@
+import { mount } from '../dist'
+import Slots from './Slots.vue'
+
+mount(Slots)
diff --git a/npm/vue2/.releaserc.js b/npm/vue2/.releaserc.js
index 4cb1091bc4..0ee9f3f0b5 100644
--- a/npm/vue2/.releaserc.js
+++ b/npm/vue2/.releaserc.js
@@ -1,7 +1,3 @@
module.exports = {
...require('../../.releaserc.base'),
- branches: [
- // this one releases v3 on master on the latest channel
- 'master',
- ],
}
diff --git a/npm/vue2/README.md b/npm/vue2/README.md
index 7a527f2332..8b07e1a1c2 100644
--- a/npm/vue2/README.md
+++ b/npm/vue2/README.md
@@ -70,7 +70,7 @@ the `options`.
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/vue2/package.json b/npm/vue2/package.json
index 108650b579..1863c34e58 100644
--- a/npm/vue2/package.json
+++ b/npm/vue2/package.json
@@ -40,7 +40,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/blob/master/npm/vue/#readme",
+ "homepage": "https://github.com/cypress-io/cypress/blob/develop/npm/vue/#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fvue&template=1-bug-report.md&title=",
"keywords": [
"cypress",
diff --git a/npm/vue2/src/index.ts b/npm/vue2/src/index.ts
index 4f8ea0d226..0966bdf0d7 100644
--- a/npm/vue2/src/index.ts
+++ b/npm/vue2/src/index.ts
@@ -142,14 +142,14 @@ type VuePlugins = VuePlugin[]
* local components, plugins, etc.
*
* @interface MountOptionsExtensions
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
*/
interface MountOptionsExtensions {
/**
* Extra local components
*
* @memberof MountOptionsExtensions
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
* @example
* import Hello from './Hello.vue'
* // imagine Hello needs AppComponent
@@ -167,7 +167,7 @@ interface MountOptionsExtensions {
* Optional Vue filters to install while mounting the component
*
* @memberof MountOptionsExtensions
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
* @example
* const filters = {
* reverse: (s) => s.split('').reverse().join(''),
@@ -181,7 +181,7 @@ interface MountOptionsExtensions {
*
* @memberof MountOptionsExtensions
* @alias mixins
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
*/
mixin?: VueMixins
@@ -190,14 +190,14 @@ interface MountOptionsExtensions {
*
* @memberof MountOptionsExtensions
* @alias mixin
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
*/
mixins?: VueMixins
/**
* A single plugin or multiple plugins.
*
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
* @alias plugins
* @memberof MountOptionsExtensions
*/
@@ -206,7 +206,7 @@ interface MountOptionsExtensions {
/**
* A single plugin or multiple plugins.
*
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
* @alias use
* @memberof MountOptionsExtensions
*/
@@ -233,7 +233,7 @@ interface MountOptions {
* mounting this component
*
* @memberof MountOptions
- * @see https://github.com/cypress-io/cypress/tree/master/npm/vue#examples
+ * @see https://github.com/cypress-io/cypress/tree/develop/npm/vue#examples
*/
extensions: MountOptionsExtensions
}
diff --git a/npm/webpack-batteries-included-preprocessor/README.md b/npm/webpack-batteries-included-preprocessor/README.md
index 5603b1e686..0d34e70a9c 100644
--- a/npm/webpack-batteries-included-preprocessor/README.md
+++ b/npm/webpack-batteries-included-preprocessor/README.md
@@ -8,7 +8,7 @@ Cypress preprocessor for bundling JavaScript via webpack, with dependencies incl
## Why?
-This preprocessor is a wrapper for [@cypress/webpack-preprocessor](https://github.com/cypress-io/cypress/tree/master/npm/webpack-preprocessor#readme). The webpack preprocessor does not include any extra dependencies (e.g. `babel-loader`, `ts-loader`), since most users will use their own `webpack.config.js` with it and already have the necessary dependencies installed. This preprocessor is for users who do not have those dependencies installed and would prefer not to configure the preprocessor to handle things like TypeScript and CoffeeScript.
+This preprocessor is a wrapper for [@cypress/webpack-preprocessor](https://github.com/cypress-io/cypress/tree/develop/npm/webpack-preprocessor#readme). The webpack preprocessor does not include any extra dependencies (e.g. `babel-loader`, `ts-loader`), since most users will use their own `webpack.config.js` with it and already have the necessary dependencies installed. This preprocessor is for users who do not have those dependencies installed and would prefer not to configure the preprocessor to handle things like TypeScript and CoffeeScript.
## Installation
@@ -42,7 +42,7 @@ module.exports = (on) => {
}
```
-Other than the `typescript` option, this preprocessor supports the same options as [@cypress/webpack-preprocessor](https://github.com/cypress-io/cypress/tree/master/npm/webpack-preprocessor#readme), so see its README for more information.
+Other than the `typescript` option, this preprocessor supports the same options as [@cypress/webpack-preprocessor](https://github.com/cypress-io/cypress/tree/develop/npm/webpack-preprocessor#readme), so see its README for more information.
## Contributing
diff --git a/npm/webpack-batteries-included-preprocessor/package.json b/npm/webpack-batteries-included-preprocessor/package.json
index d7837ea194..f6b1274840 100644
--- a/npm/webpack-batteries-included-preprocessor/package.json
+++ b/npm/webpack-batteries-included-preprocessor/package.json
@@ -55,7 +55,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/webpack-batteries-included-preprocessor#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/webpack-batteries-included-preprocessor#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20webpack-batteries-included-preprocessor&template=1-bug-report.md&title=",
"keywords": [
"cypress",
diff --git a/npm/webpack-dev-server/README.md b/npm/webpack-dev-server/README.md
index ef3ca503b1..91a641e5be 100644
--- a/npm/webpack-dev-server/README.md
+++ b/npm/webpack-dev-server/README.md
@@ -71,7 +71,7 @@ We then merge the sourced config with the user's webpack config, and layer on ou
## License
-[](https://github.com/cypress-io/cypress/blob/master/LICENSE)
+[](https://github.com/cypress-io/cypress/blob/develop/LICENSE)
This project is licensed under the terms of the [MIT license](/LICENSE).
diff --git a/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts b/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts
index 792b44b35e..af9bc5a095 100644
--- a/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts
+++ b/npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts
@@ -22,4 +22,21 @@ describe('Config options', () => {
cy.waitForSpecToFinish()
cy.get('.passed > .num').should('contain', 1)
})
+
+ it('supports webpackConfig as an async function', () => {
+ cy.scaffoldProject('webpack5_wds4-react')
+ cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-dev-server-async-config.config.ts'])
+ cy.startAppServer('component')
+
+ cy.visitApp()
+ cy.contains('App.cy.jsx').click()
+ cy.waitForSpecToFinish()
+ cy.get('.passed > .num').should('contain', 1)
+
+ cy.withCtx(async (ctx) => {
+ const verifyFile = await ctx.file.readFileInProject('wrote-to-file')
+
+ expect(verifyFile).to.eq('OK')
+ })
+ })
})
diff --git a/npm/webpack-dev-server/package.json b/npm/webpack-dev-server/package.json
index 086e9880a0..810ddf973f 100644
--- a/npm/webpack-dev-server/package.json
+++ b/npm/webpack-dev-server/package.json
@@ -50,7 +50,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/webpack-dev-server#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/webpack-dev-server#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?template=1-bug-report.md",
"publishConfig": {
"access": "public"
diff --git a/npm/webpack-dev-server/src/devServer.ts b/npm/webpack-dev-server/src/devServer.ts
index a3a9f0d368..9fcc9240e9 100644
--- a/npm/webpack-dev-server/src/devServer.ts
+++ b/npm/webpack-dev-server/src/devServer.ts
@@ -27,12 +27,16 @@ type FrameworkConfig = {
}
}
+export type ConfigHandler =
+ Partial
+ | (() => Partial | Promise>)
+
export type WebpackDevServerConfig = {
specs: Cypress.Spec[]
cypressConfig: Cypress.PluginConfigOptions
devServerEvents: NodeJS.EventEmitter
onConfigNotFound?: (devServer: 'webpack', cwd: string, lookedIn: string[]) => void
- webpackConfig?: unknown // Derived from the user's webpack
+ webpackConfig?: ConfigHandler // Derived from the user's webpack config
} & FrameworkConfig
/**
diff --git a/npm/webpack-dev-server/src/makeWebpackConfig.ts b/npm/webpack-dev-server/src/makeWebpackConfig.ts
index 2ca6c09549..7bdb954fdf 100644
--- a/npm/webpack-dev-server/src/makeWebpackConfig.ts
+++ b/npm/webpack-dev-server/src/makeWebpackConfig.ts
@@ -80,7 +80,7 @@ export async function makeWebpackConfig (
config: CreateFinalWebpackConfig,
) {
const { module: webpack } = config.sourceWebpackModulesResult.webpack
- let userWebpackConfig = config.devServerConfig.webpackConfig as Partial
+ let userWebpackConfig = config.devServerConfig.webpackConfig
const frameworkWebpackConfig = config.frameworkConfig as Partial
const {
cypressConfig: {
@@ -125,6 +125,10 @@ export async function makeWebpackConfig (
}
}
+ userWebpackConfig = typeof userWebpackConfig === 'function'
+ ? await userWebpackConfig()
+ : userWebpackConfig
+
const userAndFrameworkWebpackConfig = modifyWebpackConfigForCypress(
merge(frameworkWebpackConfig ?? {}, userWebpackConfig ?? {}),
)
diff --git a/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts b/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts
index a76e8182ec..0afab14ee8 100644
--- a/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts
+++ b/npm/webpack-dev-server/test/makeWebpackConfig.spec.ts
@@ -1,9 +1,14 @@
-import { expect } from 'chai'
+import Chai, { expect } from 'chai'
import EventEmitter from 'events'
import snapshot from 'snap-shot-it'
+import { IgnorePlugin } from 'webpack'
import { WebpackDevServerConfig } from '../src/devServer'
import { CYPRESS_WEBPACK_ENTRYPOINT, makeWebpackConfig } from '../src/makeWebpackConfig'
import { createModuleMatrixResult } from './test-helpers/createModuleMatrixResult'
+import sinon from 'sinon'
+import SinonChai from 'sinon-chai'
+
+Chai.use(SinonChai)
describe('makeWebpackConfig', () => {
it('ignores userland webpack `output.publicPath` and `devServer.overlay` with webpack-dev-server v3', async () => {
@@ -144,4 +149,45 @@ describe('makeWebpackConfig', () => {
'cypress-entry': CYPRESS_WEBPACK_ENTRYPOINT,
})
})
+
+ it('calls webpackConfig if it is a function, passing in the base config', async () => {
+ const testPlugin = new IgnorePlugin({
+ contextRegExp: /aaa/,
+ resourceRegExp: /bbb/,
+ })
+
+ const modifyConfig = sinon.spy(async () => {
+ return {
+ plugins: [testPlugin],
+ }
+ })
+
+ const devServerConfig: WebpackDevServerConfig = {
+ specs: [],
+ cypressConfig: {
+ isTextTerminal: false,
+ projectRoot: '.',
+ supportFile: '/support.js',
+ devServerPublicPathRoute: '/test-public-path', // This will be overridden by makeWebpackConfig.ts
+ } as Cypress.PluginConfigOptions,
+ webpackConfig: modifyConfig,
+ devServerEvents: new EventEmitter(),
+ }
+
+ const actual = await makeWebpackConfig({
+ devServerConfig,
+ sourceWebpackModulesResult: createModuleMatrixResult({
+ webpack: 4,
+ webpackDevServer: 4,
+ }),
+ })
+
+ expect(actual.plugins.length).to.eq(3)
+ expect(modifyConfig).to.have.been.called
+ // merged plugins get added at the top of the chain by default
+ // should be merged, not overriding existing plugins
+ expect(actual.plugins[0].constructor.name).to.eq('IgnorePlugin')
+ expect(actual.plugins[1].constructor.name).to.eq('HtmlWebpackPlugin')
+ expect(actual.plugins[2].constructor.name).to.eq('CypressCTWebpackPlugin')
+ })
})
diff --git a/npm/webpack-preprocessor/deferred.ts b/npm/webpack-preprocessor/deferred.ts
deleted file mode 100644
index 86bda35d1f..0000000000
--- a/npm/webpack-preprocessor/deferred.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import * as Promise from 'bluebird'
-
-export function createDeferred () {
- let resolve: (thenableOrResult?: T | PromiseLike | undefined) => void
- let reject: any
- const promise = new Promise(function (_resolve, _reject) {
- resolve = _resolve
- reject = _reject
- })
-
- return {
- //@ts-ignore
- resolve,
- reject,
- promise,
- }
-}
diff --git a/npm/webpack-preprocessor/index.ts b/npm/webpack-preprocessor/index.ts
index 7376f4f8db..c11d27f7c6 100644
--- a/npm/webpack-preprocessor/index.ts
+++ b/npm/webpack-preprocessor/index.ts
@@ -1,19 +1,29 @@
-import { overrideSourceMaps } from './lib/typescript-overrides'
-
-import * as Promise from 'bluebird'
+import Bluebird from 'bluebird'
+import Debug from 'debug'
+import _ from 'lodash'
import * as events from 'events'
-import * as _ from 'lodash'
-import * as webpack from 'webpack'
-import { createDeferred } from './deferred'
+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 path = require('path')
-const debug = require('debug')('cypress:webpack')
-const debugStats = require('debug')('cypress:webpack:stats')
+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 absense 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: Promise
- deferreds: Array<{ resolve: (filePath: string) => void, reject: (error: Error) => void, promise: Promise }>
+ promise: Bluebird
+ deferreds: Array<{ resolve: (filePath: string) => void, reject: (error: Error) => void, promise: Bluebird }>
initial: boolean
}
@@ -114,7 +124,7 @@ interface FileEvent extends events.EventEmitter {
* Cypress asks file preprocessor to bundle the given file
* and return the full path to produced bundle.
*/
-type FilePreprocessor = (file: FileEvent) => Promise
+type FilePreprocessor = (file: FileEvent) => Bluebird
type WebpackPreprocessorFn = (options: PreprocessorOptions) => FilePreprocessor
@@ -153,6 +163,8 @@ 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'
//
@@ -229,6 +241,24 @@ 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)$/,
+ use: [{
+ loader: path.join(__dirname, 'lib/cross-origin-callback-loader'),
+ options: {
+ commands: callbackReplacementCommands,
+ },
+ }],
+ })
+
+ crossOriginCallbackLoaderAdded = true
+ }
+
debug('webpackOptions: %o', webpackOptions)
debug('watchOptions: %o', watchOptions)
if (options.typescript) debug('typescript: %s', options.typescript)
@@ -238,7 +268,7 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
const compiler = webpack(webpackOptions)
- let firstBundle = createDeferred()
+ let firstBundle = utils.createDeferred()
// cache the bundle promise, so it can be returned if this function
// is invoked again with the same filePath
@@ -301,29 +331,78 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
console.error(stats.toString({ colors: true }))
}
- // resolve with the outputPath so Cypress knows where to serve
- // the file from
- // Seems to be a race condition where changing file before next tick
- // does not cause build to rerun
- Promise.delay(0).then(() => {
- if (!bundles[filePath]) {
- return
- }
-
+ 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 cy.origin() callback
+ // functions that contains Cypress.require() 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(() => {
+ if (!bundles[filePath]) {
+ return
+ }
+
+ if (!callbackReplacementCommands) {
+ return resolveAllBundles()
+ }
+
+ handleCrossOriginCallbackFiles()
})
}
- // this event is triggered when watching and a file is saved
const plugin = { name: 'CypressWebpackPreprocessor' }
+ // this event is triggered when watching and a file is saved
const onCompile = () => {
debug('compile', filePath)
- const nextBundle = createDeferred()
+ const nextBundle = utils.createDeferred()
bundles[filePath].promise = nextBundle.promise
bundles[filePath].deferreds.push(nextBundle)
@@ -374,6 +453,17 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F
bundler.close(cb)
}
}
+
+ // clean up temp dir where cross-origin callback files are output
+ const tmpdir = utils.tmpdir(utils.hash(filePath))
+
+ debug('remove temp directory:', tmpdir)
+
+ utils.rmdir(tmpdir).catch((err) => {
+ // not the end of the world if removing the tmpdir fails, but we
+ // don't want it to crash the whole process by going uncaught
+ debug('failed removing temp directory: %s', err.stack)
+ })
})
// return the promise, which will resolve with the outputPath or reject
diff --git a/npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts b/npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts
new file mode 100644
index 0000000000..293b9538d4
--- /dev/null
+++ b/npm/webpack-preprocessor/lib/cross-origin-callback-compile.ts
@@ -0,0 +1,104 @@
+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 contains Cypress.require() and stores their sources in the
+// CrossOriginCallbackStore. this sends those sources through webpack again
+// to process any dependencies and create bundles for each callback function
+export const compileCrossOriginCallbackFiles = (files: CrossOriginCallbackStoreFile[], options: CompileOptions): Promise => {
+ debug('compile cross-origin callback files: %o', files)
+
+ const { originalFilePath, webpackOptions } = options
+ const outputDir = path.dirname(files[0].outputFilePath)
+ const { entry, virtualConfig } = getConfig({ files, originalFilePath })
+ const modifiedWebpackOptions = getWebpackOptions({
+ webpackOptions,
+ entry,
+ virtualConfig,
+ outputDir,
+ })
+
+ return new Promise((resolve, reject) => {
+ const compiler = webpack(modifiedWebpackOptions)
+
+ const handle = (err: Error) => {
+ if (err) {
+ debug('errored compiling cross-origin callback files with: %s', err.stack)
+
+ return reject(err)
+ }
+
+ debug('successfully compiled cross-origin callback files')
+
+ resolve()
+ }
+
+ compiler.run(handle)
+ })
+}
diff --git a/npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts b/npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts
new file mode 100644
index 0000000000..deddb99ae0
--- /dev/null
+++ b/npm/webpack-preprocessor/lib/cross-origin-callback-loader.ts
@@ -0,0 +1,188 @@
+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 { crossOriginCallbackStore } from './cross-origin-callback-store'
+import utils from './utils'
+
+const debug = Debug('cypress:webpack')
+
+// this loader makes supporting dependencies within the cy.origin() callbacks
+// possible. it does this by doing the following:
+// - extracting callback(s)
+// - the callback(s) is/are kept in memory and then run back through webpack
+// once the initial file compilation is complete
+// - users use Cypress.require() in their test code instead of require().
+// this is because we don't want require()s nested within the callback
+// to be processed in the initial compilation. this both improves
+// performance and prevents errors (when the dependency has ES import
+// statements, babel will error because they're not top-level since
+// the require is not top-level)
+// - replacing Cypress.require() with require()
+// - this allows the require()s to be processed normally during the
+// compilation of the callback itself.
+// - replacing the callback(s) with object(s)
+// - 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',
+ })
+ } catch (err) {
+ // it's unlikely there will be a parsing error, since that should have
+ // already been caught by a previous loader, but if there is and it isn't
+ // possible to get the AST, there's nothing we can do, so just callback
+ // with the original source
+ debug('parsing error for file (%s): %s', resourcePath, err.stack)
+
+ this.callback(null, source, map)
+
+ return
+ }
+
+ let hasDependencies = false
+
+ traverse(ast, {
+ CallExpression (path) {
+ const callee = path.get('callee') as NodePath
+
+ if (!callee.isMemberExpression()) return
+
+ // bail if we're not inside a supported command
+ if (!commands.includes((callee.node.property as t.Identifier).name)) {
+ return
+ }
+
+ const lastArg = _.last(path.get('arguments'))
+
+ // the user could try an invalid signature for cy.origin() 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
+ }
+
+ // replace instances of Cypress.require('dep') with require('dep')
+ lastArg.traverse({
+ CallExpression (path) {
+ const callee = path.get('callee') as NodePath
+
+ // e.g. const dep = Cypress.require('../path/to/dep')
+ if (callee.matchesPattern('Cypress.require')) {
+ hasDependencies = true
+
+ path.replaceWith(
+ t.callExpression(
+ callee.node.property as t.Expression, // 'require'
+ path.get('arguments').map((arg) => arg.node), // ['../path/to/dep']
+ ),
+ )
+ }
+ },
+ }, this)
+
+ if (!hasDependencies) return
+
+ // generate the extracted callback function from an AST into a string
+ // and assign it to a variable. we wrap this generated code when we
+ // eval the code, so the variable is set up and then invoked. it ends up
+ // like this:
+ //
+ // let __cypressCrossOriginCallback 】added at runtime
+ // (function () { ┓ added by webpack
+ // // ... webpack stuff stuff ... â”›
+ // __cypressCrossOriginCallback = (args) => { ┓ extracted callback
+ // const dep = require('../path/to/dep') ┃
+ // // ... test stuff ... ┃
+ // } â”›
+ // // ... webpack stuff stuff ... ┓ added by webpack
+ // }()) â”›
+ // __cypressCrossOriginCallback(args) 】added at runtime
+ //
+ const callbackName = '__cypressCrossOriginCallback'
+ const generatedCode = generate(lastArg.node, {}).code
+ const modifiedGeneratedCode = `${callbackName} = ${generatedCode}`
+ // the tmpdir path uses a hashed version of the source file path
+ // so that it can be cleaned up without removing other in-use tmpdirs
+ // (notably the support file persists between specs, so its cross-origin
+ // callback output files need to persist as well)
+ const sourcePathHash = utils.hash(resourcePath)
+ const outputDir = utils.tmpdir(sourcePathHash)
+ // use a hash of the contents in file name to ensure it's unique. if
+ // the contents happen to be the same, it's okay if they share a file
+ const codeHash = utils.hash(modifiedGeneratedCode)
+ const inputFileName = `cross-origin-cb-${codeHash}`
+ const outputFilePath = `${pathUtil.join(outputDir, inputFileName)}.js`
+
+ store.addFile(resourcePath, {
+ inputFileName,
+ outputFilePath,
+ source: modifiedGeneratedCode,
+ })
+
+ // replaces callback function with object referencing the extracted
+ // function's callback name and output file path in the form
+ // { callbackName: , outputFilePath: }
+ // this is used at runtime when cy.origin() 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 we found Cypress.require()s, re-generate the code from the AST
+ if (hasDependencies) {
+ debug('callback with modified source')
+
+ // TODO: handle sourcemaps for this correctly
+ // https://github.com/cypress-io/cypress/issues/23365
+ // the following causes error "Cannot read property 'replace' of undefined"
+ // return generate(ast, { sourceMaps: true }, source).code
+ // and can't pass in original map or the output ends up with
+ // `undefinedundefined` appended, which is a syntax error
+ this.callback(null, generate(ast, {}).code)
+
+ return
+ }
+
+ debug('callback with original source')
+ // if no Cypress.require()s were found, callback with the original source/map
+ this.callback(null, source, map)
+}
diff --git a/npm/webpack-preprocessor/lib/cross-origin-callback-store.ts b/npm/webpack-preprocessor/lib/cross-origin-callback-store.ts
new file mode 100644
index 0000000000..1167f26ab3
--- /dev/null
+++ b/npm/webpack-preprocessor/lib/cross-origin-callback-store.ts
@@ -0,0 +1,29 @@
+export interface CrossOriginCallbackStoreFile {
+ inputFileName: string
+ outputFilePath: string
+ source: string
+}
+
+export class CrossOriginCallbackStore {
+ private files: { [key: string]: CrossOriginCallbackStoreFile[] } = {}
+
+ addFile (sourceFilePath: string, file: CrossOriginCallbackStoreFile) {
+ this.files[sourceFilePath] = (this.files[sourceFilePath] || []).concat(file)
+ }
+
+ hasFilesFor (sourceFiles: string[]) {
+ return !!this.getFilesFor(sourceFiles)?.length
+ }
+
+ getFilesFor (sourceFiles: string[]) {
+ return Object.keys(this.files).reduce((files, sourceFilePath) => {
+ return sourceFiles.includes(sourceFilePath) ? files.concat(this.files[sourceFilePath]) : files
+ }, [] as CrossOriginCallbackStoreFile[])
+ }
+
+ reset (sourceFilePath: string) {
+ this.files[sourceFilePath] = []
+ }
+}
+
+export const crossOriginCallbackStore = new CrossOriginCallbackStore()
diff --git a/npm/webpack-preprocessor/lib/utils.ts b/npm/webpack-preprocessor/lib/utils.ts
new file mode 100644
index 0000000000..e51a2e723d
--- /dev/null
+++ b/npm/webpack-preprocessor/lib/utils.ts
@@ -0,0 +1,43 @@
+import _ from 'lodash'
+import * as os from 'os'
+import path from 'path'
+import md5 from 'md5'
+import Bluebird from 'bluebird'
+import fs from 'fs-extra'
+
+function createDeferred () {
+ let resolve: (thenableOrResult?: T | PromiseLike | undefined) => void
+ let reject: any
+ const promise = new Bluebird(function (_resolve, _reject) {
+ resolve = _resolve
+ reject = _reject
+ })
+
+ return {
+ //@ts-ignore
+ resolve,
+ reject,
+ promise,
+ }
+}
+
+function hash (contents: string) {
+ return md5(contents)
+}
+
+function rmdir (dirPath: string) {
+ return fs.emptyDir(dirPath)
+}
+
+function tmpdir (dirname?: string) {
+ const pathParts = _.compact([os.tmpdir(), 'cypress', 'webpack-preprocessor', dirname])
+
+ return path.join(...pathParts)
+}
+
+export default {
+ createDeferred,
+ hash,
+ rmdir,
+ tmpdir,
+}
diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json
index 894b78f576..ff118d8142 100644
--- a/npm/webpack-preprocessor/package.json
+++ b/npm/webpack-preprocessor/package.json
@@ -17,12 +17,15 @@
"test-unit": "mocha test/unit/*.spec.*",
"test-watch": "yarn test-unit & chokidar '**/*.(js|ts)' 'test/unit/*.(js|ts)' -c 'yarn test-unit'",
"check-ts": "tsc --noEmit",
- "watch": "yarn build --watch"
+ "watch": "rimraf dist && tsc --watch"
},
"dependencies": {
"bluebird": "3.7.1",
"debug": "^4.3.2",
- "lodash": "^4.17.20"
+ "fs-extra": "^10.1.0",
+ "loader-utils": "^2.0.0",
+ "lodash": "^4.17.20",
+ "webpack-virtual-modules": "^0.4.4"
},
"devDependencies": {
"@babel/core": "^7.0.1",
@@ -37,6 +40,7 @@
"chai": "4.1.2",
"chalk": "3.0.0",
"chokidar-cli": "2.1.0",
+ "common-tags": "^1.8.2",
"cypress": "0.0.0-development",
"dependency-check": "2.9.1",
"deps-ok": "1.2.1",
@@ -46,7 +50,7 @@
"eslint-plugin-mocha": "8.1.0",
"fast-glob": "3.1.1",
"find-webpack": "1.5.0",
- "fs-extra": "9.1.0",
+ "md5": "2.3.0",
"mocha": "^7.1.0",
"mockery": "2.1.0",
"proxyquire": "2.1.3",
@@ -76,7 +80,7 @@
"type": "git",
"url": "https://github.com/cypress-io/cypress.git"
},
- "homepage": "https://github.com/cypress-io/cypress/tree/master/npm/webpack-preprocessor#readme",
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/webpack-preprocessor#readme",
"bugs": "https://github.com/cypress-io/cypress/issues/new?assignees=&labels=npm%3A%20%40cypress%2Fwebpack-preprocessor&template=1-bug-report.md&title=",
"keywords": [
"cypress",
diff --git a/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts b/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts
new file mode 100644
index 0000000000..8bf7ac4b2a
--- /dev/null
+++ b/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts
@@ -0,0 +1,275 @@
+'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 = { sourcesContent: [] }
+
+ 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 Cypress.require()', () => {
+ const source = `it('test', () => {
+ cy.origin('http://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://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', () => {
+ const { resultingSource, resultingMap } = callLoader(stripIndent`
+ it('test', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ Cypress.require('../support/utils')
+ })
+ })`)
+
+ expect(resultingSource).to.equal(stripIndent`
+ it('test', () => {
+ cy.origin('http://foobar.com:3500', {
+ "callbackName": "__cypressCrossOriginCallback",
+ "outputFilePath": "/path/to/tmp/cross-origin-cb-abc123.js"
+ });
+ });`)
+
+ expect(resultingMap).to.be.undefined
+ })
+
+ it('replaces cy.other() when specified in commands', () => {
+ const { resultingSource, resultingMap } = callLoader(stripIndent`
+ it('test', () => {
+ cy.other('http://foobar.com:3500', () => {
+ Cypress.require('../support/utils')
+ })
+ })`,
+ ['other'])
+
+ expect(resultingSource).to.equal(stripIndent`
+ it('test', () => {
+ cy.other('http://foobar.com:3500', {
+ "callbackName": "__cypressCrossOriginCallback",
+ "outputFilePath": "/path/to/tmp/cross-origin-cb-abc123.js"
+ });
+ });`)
+
+ expect(resultingMap).to.be.undefined
+ })
+
+ it('adds the file to the store, replacing Cypress.require() with require()', () => {
+ const { store } = callLoader(
+ `it('test', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ Cypress.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://foobar.com:3500', function () {
+ Cypress.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://foobar.com:3500', () => {
+ Cypress.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://foobar.com:3500', () => {
+ const utils = Cypress.require('../support/utils')
+ utils.foo()
+ })
+ })`,
+ )
+
+ expectAddFileSource(store).to.equal(stripIndent`
+ __cypressCrossOriginCallback = () => {
+ const utils = require('../support/utils');
+
+ utils.foo();
+ }`)
+ })
+
+ it('works with multiple Cypress.require()s', () => {
+ const { store } = callLoader(
+ `it('test', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ Cypress.require('../support/commands')
+ const utils = Cypress.require('../support/utils')
+ const _ = Cypress.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://foobar.com:3500', () => {
+ Cypress.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://foobar.com:3500', () => {
+ const someVar = 'someValue'
+ const result = Cypress.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://foobar.com:3500', { args: { foo: 'foo'}}, ({ foo }) => {
+ const result = Cypress.require('./fn')(foo)
+ expect(result).to.equal('mutated someVar')
+ })
+ })`,
+ )
+
+ expectAddFileSource(store).to.equal(stripIndent`
+ __cypressCrossOriginCallback = ({
+ foo
+ }) => {
+ const result = require('./fn')(foo);
+
+ expect(result).to.equal('mutated someVar');
+ }`)
+ })
+ })
+})
diff --git a/npm/webpack-preprocessor/test/unit/index.spec.js b/npm/webpack-preprocessor/test/unit/index.spec.js
index 37d4247aae..b22f310026 100644
--- a/npm/webpack-preprocessor/test/unit/index.spec.js
+++ b/npm/webpack-preprocessor/test/unit/index.spec.js
@@ -23,7 +23,10 @@ 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 () {
@@ -65,6 +68,9 @@ 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)
}
@@ -161,6 +167,79 @@ 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')
@@ -177,7 +256,7 @@ describe('webpack preprocessor', function () {
})
it('does not enable inline source maps when devtool is false', function () {
- const options = { webpackOptions: { devtool: false } }
+ const options = { webpackOptions: { devtool: false, module: { rules: [] } } }
return this.run(options).then(() => {
expect(webpack).to.be.calledWithMatch({
@@ -189,7 +268,7 @@ describe('webpack preprocessor', function () {
})
it('always sets devtool even when mode is "production"', function () {
- const options = { webpackOptions: { mode: 'production' } }
+ const options = { webpackOptions: { mode: 'production', module: { rules: [] } } }
return this.run(options).then(() => {
expect(webpack).to.be.calledWithMatch({
@@ -211,7 +290,7 @@ describe('webpack preprocessor', function () {
})
it('follows user mode if present', function () {
- const options = { webpackOptions: { mode: 'production' } }
+ const options = { webpackOptions: { mode: 'production', module: { rules: [] } } }
return this.run(options).then(() => {
expect(webpack).to.be.calledWithMatch({
@@ -307,6 +386,15 @@ describe('webpack preprocessor', function () {
})
})
+ it('deletes temp dir when `close` is emitted', function () {
+ this.compilerApi.watch.yields(null, this.statsApi)
+
+ return this.run().then(() => {
+ this.file.on.withArgs('close').yield()
+ expect(utils.rmdir).to.be.calledWith(utils.tmpdir())
+ })
+ })
+
it('uses default webpack options when no user options', function () {
return this.run().then(() => {
expect(webpack.lastCall.args[0].module.rules[0].use).to.have.length(1)
diff --git a/npm/webpack-preprocessor/tsconfig.json b/npm/webpack-preprocessor/tsconfig.json
index 032ac5f098..8d5c7cc20d 100644
--- a/npm/webpack-preprocessor/tsconfig.json
+++ b/npm/webpack-preprocessor/tsconfig.json
@@ -45,7 +45,7 @@
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
- // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
@@ -62,5 +62,5 @@
/* Advanced Options */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
- "include": ["./*.ts"]
+ "include": ["./*.ts", "./lib/*.ts"]
}
diff --git a/npm/xpath/.eslintrc b/npm/xpath/.eslintrc
new file mode 100644
index 0000000000..220212842e
--- /dev/null
+++ b/npm/xpath/.eslintrc
@@ -0,0 +1,17 @@
+{
+ "plugins": [
+ "cypress"
+ ],
+ "extends": [
+ "plugin:@cypress/dev/tests"
+ ],
+ "env": {
+ "cypress/globals": true
+ },
+ "rules": {
+ "mocha/no-global-tests": "off",
+ "no-unused-vars": "off",
+ "no-console": "off",
+ "@typescript-eslint/no-unused-vars": "off"
+ }
+}
diff --git a/npm/xpath/.releaserc.js b/npm/xpath/.releaserc.js
new file mode 100644
index 0000000000..03ddcf9ab4
--- /dev/null
+++ b/npm/xpath/.releaserc.js
@@ -0,0 +1,6 @@
+module.exports = {
+ ...require('../../.releaserc.base'),
+ branches: [
+ { name: 'develop', channel: 'latest' },
+ ],
+}
diff --git a/npm/xpath/README.md b/npm/xpath/README.md
new file mode 100644
index 0000000000..339fce4e35
--- /dev/null
+++ b/npm/xpath/README.md
@@ -0,0 +1,82 @@
+# @cypress/xpath
+
+> Adds XPath command to [Cypress.io](https://www.cypress.io) test runner
+
+## Install with npm
+
+```shell
+npm install -D @cypress/xpath
+```
+
+## Install with Yarn
+
+```shell
+yarn add @cypress/xpath --dev
+```
+
+Then include in your project's [support file](https://on.cypress.io/support-file)
+
+```js
+require('@cypress/xpath');
+```
+
+## Use
+
+After installation your `cy` object will have `xpath` command.
+
+```js
+it('finds list items', () => {
+ cy.xpath('//ul[@class="todo-list"]//li').should('have.length', 3);
+});
+```
+
+You can also chain `xpath` off of another command.
+
+```js
+it('finds list items', () => {
+ cy.xpath('//ul[@class="todo-list"]').xpath('./li').should('have.length', 3);
+});
+```
+
+As with other cy commands, it is scoped by `cy.within()`.
+
+```js
+it('finds list items', () => {
+ cy.xpath('//ul[@class="todo-list"]').within(() => {
+ cy.xpath('./li').should('have.length', 3);
+ });
+});
+```
+
+**note:** you can test XPath expressions from DevTools console using `$x(...)` function, for example `$x('//div')` to find all divs.
+
+See [cypress/e2e/spec.cy.js](cypress/e2e/spec.cy.js)
+
+## Beware the XPath // trap
+
+In XPath the expression // means something very specific, and it might not be what you think. Contrary to common belief, // means "anywhere in the document" not "anywhere in the current context". As an example:
+
+```js
+cy.xpath('//body').xpath('//script');
+```
+
+You might expect this to find all script tags in the body, but actually, it finds all script tags in the entire document, not only those in the body! What you're looking for is the .// expression which means "any descendant of the current node":
+
+```js
+cy.xpath('//body').xpath('.//script');
+```
+
+The same thing goes for within:
+
+```js
+cy.xpath('//body').within(() => {
+ cy.xpath('.//script');
+});
+```
+
+
+For more, see [Intelligent Code Completion](https://on.cypress.io/intellisense)
+
+## License
+
+This project is licensed under the terms of the [MIT license](/LICENSE.md).
diff --git a/npm/xpath/cypress.config.js b/npm/xpath/cypress.config.js
new file mode 100644
index 0000000000..e7a2ffb6dc
--- /dev/null
+++ b/npm/xpath/cypress.config.js
@@ -0,0 +1,12 @@
+const { defineConfig } = require('cypress')
+
+module.exports = defineConfig({
+ e2e: {
+ excludeSpecPattern: '*.html',
+ supportFile: 'cypress/support/e2e.js',
+ },
+ component: {
+ excludeSpecPattern: '*.html',
+ supportFile: 'cypress/support/e2e.js',
+ },
+})
diff --git a/npm/xpath/cypress/e2e/index.html b/npm/xpath/cypress/e2e/index.html
new file mode 100644
index 0000000000..ebe8a49935
--- /dev/null
+++ b/npm/xpath/cypress/e2e/index.html
@@ -0,0 +1,26 @@
+
+
+
cypress-xpath
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/npm/xpath/cypress/e2e/spec.cy.js b/npm/xpath/cypress/e2e/spec.cy.js
new file mode 100644
index 0000000000..c88881f93a
--- /dev/null
+++ b/npm/xpath/cypress/e2e/spec.cy.js
@@ -0,0 +1,183 @@
+///
+///
+
+describe('cypress-xpath', () => {
+ it('adds xpath command', () => {
+ expect(cy).property('xpath').to.be.a('function')
+ })
+
+ context('elements', () => {
+ beforeEach(() => {
+ cy.visit('cypress/e2e/index.html')
+ })
+
+ it('finds h1', () => {
+ cy.xpath('//h1').should('have.length', 1)
+ })
+
+ it('returns jQuery wrapped elements', () => {
+ cy.xpath('//h1').then((el$) => {
+ expect(el$).to.have.property('jquery')
+ })
+ })
+
+ it('returns primitives as is', () => {
+ cy.xpath('string(//h1)').then((el$) => {
+ expect(el$).to.not.have.property('jquery')
+ })
+ })
+
+ it('provides jQuery wrapped elements to assertions', () => {
+ cy.xpath('//h1').should((el$) => {
+ expect(el$).to.have.property('jquery')
+ })
+ })
+
+ it('gets h1 text', () => {
+ cy.xpath('//h1/text()')
+ .its('0.textContent')
+ .should('equal', 'cypress-xpath')
+ })
+
+ it('retries until element is inserted', () => {
+ // the element will be inserted after 1 second
+ cy.xpath('string(//*[@id="inserted"])').should('equal', 'inserted text')
+ })
+
+ describe('chaining', () => {
+ it('finds h1 within main', () => {
+ // first assert that h1 doesn't exist as a child of the implicit document subject
+ cy.xpath('./h1').should('not.exist')
+
+ cy.xpath('//main').xpath('./h1').should('exist')
+ })
+
+ it('finds body outside of main when succumbing to // trap', () => {
+ // first assert that body doesn't actually exist within main
+ cy.xpath('//main').xpath('.//body').should('not.exist')
+
+ cy.xpath('//main').xpath('//body').should('exist')
+ })
+
+ it('finds h1 within document', () => {
+ cy.document().xpath('//h1').should('exist')
+ })
+
+ it('throws when subject is more than a single element', (done) => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.eq(
+ 'xpath() can only be called on a single element. Your subject contained 2 elements.',
+ )
+
+ done()
+ })
+
+ cy.get('main, div').xpath('foo')
+ })
+ })
+
+ describe('within()', () => {
+ it('finds h1 within within-subject', () => {
+ // first assert that h1 doesn't exist as a child of the implicit document subject
+ cy.xpath('./h1').should('not.exist')
+
+ cy.xpath('//main').within(() => {
+ cy.xpath('./h1').should('exist')
+ })
+ })
+
+ it('finds body outside of within-subject when succumbing to // trap', () => {
+ // first assert that body doesn't actually exist within main
+ cy.xpath('//main').within(() => {
+ cy.xpath('.//body').should('not.exist')
+ })
+
+ cy.xpath('//main').within(() => {
+ cy.xpath('//body').should('exist')
+ })
+ })
+ })
+
+ describe('primitives', () => {
+ it('counts h1 elements', () => {
+ cy.xpath('count(//h1)').should('equal', 1)
+ })
+
+ it('returns h1 text content', () => {
+ cy.xpath('string(//h1)').should('equal', 'cypress-xpath')
+ })
+
+ it('returns boolean', () => {
+ cy.xpath('boolean(//h1)').should('be.true')
+ cy.xpath('boolean(//h2)').should('be.false')
+ })
+ })
+
+ describe('typing', () => {
+ it('works on text input', () => {
+ cy.xpath('//*[@id="name"]').type('World')
+ cy.contains('span#greeting', 'Hello, World')
+ })
+ })
+
+ describe('clicking', () => {
+ it('on button', () => {
+ // this button invokes window.alert when clicked
+ const alert = cy.stub()
+
+ cy.on('window:alert', alert)
+ cy.xpath('//*[@id="first-button"]')
+ .click()
+ .then(() => {
+ expect(alert).to.have.been.calledOnce
+ })
+ })
+ })
+ })
+
+ context('logging', () => {
+ beforeEach(() => {
+ cy.visit('cypress/e2e/index.html')
+ })
+
+ it('should log by default', () => {
+ cy.spy(Cypress, 'log').log(false)
+
+ cy.xpath('//h1').then(() => {
+ expect(Cypress.log).to.be.calledWithMatch({ name: 'xpath' })
+ })
+ })
+
+ it('logs the selector when not found', (done) => {
+ cy.xpath('//h1') // does exist
+ cy.on('fail', (e) => {
+ const isExpectedErrorMessage = (message) => {
+ return message.includes('Timed out retrying') &&
+ message.includes(
+ 'Expected to find element: `//h2`, but never found it.',
+ )
+ }
+
+ if (!isExpectedErrorMessage(e.message)) {
+ console.error('Cypress test failed with an unexpected error message')
+ console.error(e)
+
+ return done(e)
+ }
+
+ // no errors, the error message for not found selector is correct
+ done()
+ })
+
+ cy.xpath('//h2', { timeout: 100 }) // does not exist
+ })
+
+ it('should not log when provided log: false', () => {
+ cy.spy(Cypress, 'log').log(false)
+
+ cy.xpath('//h1', { log: false }).then(() => {
+ expect(Cypress.log).to.not.be.calledWithMatch({ name: 'xpath' })
+ })
+ })
+ })
+})
diff --git a/npm/xpath/cypress/fixtures/example.json b/npm/xpath/cypress/fixtures/example.json
new file mode 100644
index 0000000000..da18d9352a
--- /dev/null
+++ b/npm/xpath/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+ "name": "Using fixtures to represent data",
+ "email": "hello@cypress.io",
+ "body": "Fixtures are a great way to mock data for responses to routes"
+}
\ No newline at end of file
diff --git a/npm/xpath/cypress/support/e2e.js b/npm/xpath/cypress/support/e2e.js
new file mode 100644
index 0000000000..10ea61f87d
--- /dev/null
+++ b/npm/xpath/cypress/support/e2e.js
@@ -0,0 +1,16 @@
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+import '../../src'
diff --git a/npm/xpath/images/cypress-xpath-reference.gif b/npm/xpath/images/cypress-xpath-reference.gif
new file mode 100644
index 0000000000..97ebfa8f41
Binary files /dev/null and b/npm/xpath/images/cypress-xpath-reference.gif differ
diff --git a/npm/xpath/package.json b/npm/xpath/package.json
new file mode 100644
index 0000000000..8d64100997
--- /dev/null
+++ b/npm/xpath/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@cypress/xpath",
+ "version": "0.0.0-development",
+ "description": "Adds XPath command to Cypress.io test runner",
+ "main": "scripts",
+ "scripts": {
+ "cy:run": "node ../../scripts/cypress.js run --e2e",
+ "cy:open": "node ../../scripts/cypress.js open --e2e --project ${PWD}"
+ },
+ "files": [
+ "src"
+ ],
+ "types": "src",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/cypress-io/cypress.git"
+ },
+ "homepage": "https://github.com/cypress-io/cypress/tree/develop/npm/xpath#readme",
+ "author": "Cypress Tools Team",
+ "bugs": {
+ "url": "https://github.com/cypress-io/cypress/issues"
+ },
+ "keywords": [
+ "cypress",
+ "cypress-io",
+ "xpath"
+ ],
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/npm/xpath/src/index.d.ts b/npm/xpath/src/index.d.ts
new file mode 100644
index 0000000000..e93ada6e34
--- /dev/null
+++ b/npm/xpath/src/index.d.ts
@@ -0,0 +1,15 @@
+///
+
+declare namespace Cypress {
+ interface Chainable {
+ /**
+ * Get one or more DOM elements by an XPath selector.
+ * **Note:** you can test XPath expressions from DevTools console using $x(...) function, for example $x('//div') to find all divs.
+ * @see https://github.com/cypress-io/cypress-xpath
+ * @example
+ * cy.xpath(`//ul[@class="todo-list"]//li`)
+ * .should('have.length', 3)
+ */
+ xpath(selector: string, options?: Partial): Chainable>
+ }
+}
\ No newline at end of file
diff --git a/npm/xpath/src/index.js b/npm/xpath/src/index.js
new file mode 100644
index 0000000000..62001bebf9
--- /dev/null
+++ b/npm/xpath/src/index.js
@@ -0,0 +1,165 @@
+/* eslint-disable no-redeclare */
+///
+
+/**
+ * Adds XPath support to Cypress using a custom command.
+ *
+ * @see https://devhints.io/xpath
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_using_XPath_in_JavaScript
+ * @example
+ ```js
+ it('finds list items', () => {
+ cy.xpath('//ul[@class="todo-list"]//li')
+ .should('have.length', 3)
+ })
+ ```
+ */
+const xpath = (subject, selector, options = {}) => {
+ /* global XPathResult */
+ const isNumber = (xpathResult) => {
+ return xpathResult.resultType === XPathResult.NUMBER_TYPE
+ }
+ const numberResult = (xpathResult) => xpathResult.numberValue
+
+ const isString = (xpathResult) => {
+ return xpathResult.resultType === XPathResult.STRING_TYPE
+ }
+ const stringResult = (xpathResult) => xpathResult.stringValue
+
+ const isBoolean = (xpathResult) => {
+ return xpathResult.resultType === XPathResult.BOOLEAN_TYPE
+ }
+ const booleanResult = (xpathResult) => xpathResult.booleanValue
+
+ const isPrimitive = (x) => {
+ return Cypress._.isNumber(x) || Cypress._.isString(x) || Cypress._.isBoolean(x)
+ }
+
+ // options to log later
+ const log = {
+ name: 'xpath',
+ message: selector,
+ }
+
+ if (Cypress.dom.isElement(subject) && subject.length > 1) {
+ throw new Error(
+ `xpath() can only be called on a single element. Your subject contained ${
+ subject.length
+ } elements.`,
+ )
+ }
+
+ const getValue = () => {
+ let nodes = []
+ let contextNode
+ let withinSubject = cy.state('withinSubject')
+
+ if (Cypress.dom.isElement(subject)) {
+ contextNode = subject[0]
+ } else if (Cypress.dom.isDocument(subject)) {
+ contextNode = subject
+ } else if (withinSubject) {
+ contextNode = withinSubject[0]
+ } else {
+ contextNode = cy.state('window').document
+ }
+
+ let iterator = (contextNode.ownerDocument || contextNode).evaluate(
+ selector,
+ contextNode,
+ )
+
+ if (isNumber(iterator)) {
+ const result = numberResult(iterator)
+
+ log.consoleProps = () => {
+ return {
+ XPath: selector,
+ type: 'number',
+ result,
+ }
+ }
+
+ return result
+ }
+
+ if (isString(iterator)) {
+ const result = stringResult(iterator)
+
+ log.consoleProps = () => {
+ return {
+ XPath: selector,
+ type: 'string',
+ result,
+ }
+ }
+
+ return result
+ }
+
+ if (isBoolean(iterator)) {
+ const result = booleanResult(iterator)
+
+ log.consoleProps = () => {
+ return {
+ XPath: selector,
+ type: 'boolean',
+ result,
+ }
+ }
+
+ return result
+ }
+
+ try {
+ let node = iterator.iterateNext()
+
+ while (node) {
+ nodes.push(node)
+ node = iterator.iterateNext()
+ }
+
+ log.consoleProps = () => {
+ return {
+ XPath: selector,
+ result: nodes.length === 1 ? nodes[0] : nodes,
+ }
+ }
+
+ return nodes
+ } catch (e) {
+ console.error('Document tree modified during iteration', e)
+
+ return null
+ }
+ }
+
+ const resolveValue = () => {
+ return Cypress.Promise.try(getValue).then((value) => {
+ if (!isPrimitive(value)) {
+ value = Cypress.$(value)
+ // Add the ".selector" property because Cypress uses it for error messages
+ value.selector = selector
+ }
+
+ return cy.verifyUpcomingAssertions(value, options, {
+ onRetry: resolveValue,
+ })
+ })
+ }
+
+ return resolveValue().then((value) => {
+ if (options.log !== false) {
+ // TODO set found elements on the command log?
+ Cypress.log(log)
+ }
+
+ return value
+ })
+}
+
+Cypress.Commands.add(
+ 'xpath',
+ { prevSubject: ['optional', 'element', 'document'] },
+ xpath,
+)
diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
index db4757616f..e8cef2166d 100644
--- a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
+++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
@@ -31,21 +31,17 @@ function assertCorrectRunsLink (specFileName: string, status: string) {
}
function validateTooltip (status: string) {
- cy.validateExternalLink({
- // TODO: (#23778) This name is so long because the entire tooltip is wrapped in a link,
- // we can make this more accessible by having the name of the link describe the destination
- // (which is currently not described) and keeping the other content separate.
- name: `accounts_new.spec.js ${status} 4 months ago 2:23 - 2:39 skipped pending passed failed`,
- // the main thing about testing this link is that is gets composed with the expected UTM params
- href: makeTestingCloudLink(status),
- })
+ cy.get(`a[href="${makeTestingCloudLink(status)}"]`)
.should('contain.text', 'accounts_new.spec.js')
- .and('contain.text', '4 months ago')
.and('contain.text', '2:23 - 2:39')
.and('contain.text', 'skipped 0')
.and('contain.text', 'pending 1-2')
.and('contain.text', `passed 22-23`)
.and('contain.text', 'failed 1-2')
+ .invoke('text')
+ .should((text) => {
+ expect(text).to.match(/\d+ (day|week|month|year)s? ago/)
+ })
}
function specShouldShow (specFileName: string, runDotsClasses: string[], latestRunStatus: CloudRunStatus|'PLACEHOLDER') {
diff --git a/packages/config/README.md b/packages/config/README.md
index 76a33c9c77..8e05385ad8 100644
--- a/packages/config/README.md
+++ b/packages/config/README.md
@@ -1,6 +1,6 @@
# Config
-The `config` package contains the configuration types and validation used in both the `server` and the `driver` for setting the Cypress configuration values.
+The `config` package contains the configuration types and validation used in both the `server`, the `data-context` and the `driver` for setting the Cypress configuration values.
## Testing
diff --git a/packages/config/__snapshots__/index.spec.ts.js b/packages/config/__snapshots__/index.spec.ts.js
index 0a367f0dbc..286c8a73ad 100644
--- a/packages/config/__snapshots__/index.spec.ts.js
+++ b/packages/config/__snapshots__/index.spec.ts.js
@@ -52,6 +52,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1
"port": null,
"projectId": null,
"redirectionLimit": 20,
+ "repoRoot": null,
"reporter": "spec",
"reporterOptions": null,
"requestTimeout": 5000,
@@ -136,6 +137,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f
"port": null,
"projectId": null,
"redirectionLimit": 20,
+ "repoRoot": null,
"reporter": "spec",
"reporterOptions": null,
"requestTimeout": 5000,
diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts
index dd39341ae1..8173d48d92 100644
--- a/packages/config/src/options.ts
+++ b/packages/config/src/options.ts
@@ -501,6 +501,11 @@ const runtimeOptions: Array = [
defaultValue: '__cypress',
validation: validate.isString,
isInternal: true,
+ }, {
+ name: 'repoRoot',
+ defaultValue: null,
+ validation: validate.isString,
+ isInternal: true,
}, {
name: 'reporterRoute',
defaultValue: '/__cypress/reporter',
diff --git a/packages/config/src/project/index.ts b/packages/config/src/project/index.ts
index 9c68ce2f7c..d582ad856b 100644
--- a/packages/config/src/project/index.ts
+++ b/packages/config/src/project/index.ts
@@ -21,7 +21,7 @@ const debug = Debug('cypress:config:project')
// TODO: any -> SetupFullConfigOptions in data-context/src/data/ProjectConfigManager.ts
export function setupFullConfigWithDefaults (obj: any = {}, getFilesByGlob: any): Promise {
debug('setting config object %o', obj)
- let { projectRoot, projectName, config, envFile, options, cliConfig } = obj
+ let { projectRoot, projectName, config, envFile, options, cliConfig, repoRoot } = obj
// just force config to be an object so we dont have to do as much
// work in our tests
@@ -35,6 +35,7 @@ export function setupFullConfigWithDefaults (obj: any = {}, getFilesByGlob: any)
config.envFile = envFile
config.projectRoot = projectRoot
config.projectName = projectName
+ config.repoRoot = repoRoot
// @ts-ignore
return mergeDefaults(config, options, cliConfig, getFilesByGlob)
diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts
index d89e31f617..0eb7304a0a 100644
--- a/packages/data-context/src/DataContext.ts
+++ b/packages/data-context/src/DataContext.ts
@@ -98,6 +98,10 @@ export class DataContext {
this.lifecycleManager = new ProjectLifecycleManager(this)
}
+ get git () {
+ return this.coreData.currentProjectGitInfo
+ }
+
get schema () {
return this._config.schema
}
diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts
index b5d882920a..f9d16e47dd 100644
--- a/packages/data-context/src/data/ProjectConfigManager.ts
+++ b/packages/data-context/src/data/ProjectConfigManager.ts
@@ -461,6 +461,16 @@ export class ProjectConfigManager {
)
}
+ get repoRoot () {
+ /*
+ Used to detect the correct file path when a test fails.
+ It is derived and assigned in the packages/driver in stack_utils.
+ It's needed to show the correct link to files in repo mgmt tools like GitHub in the dashboard.
+ Right now we assume the repoRoot is where the `.git` dir is located.
+ */
+ return this.options.ctx.git?.gitBaseDir
+ }
+
private async buildBaseFullConfig (configFileContents: Cypress.ConfigOptions, envFile: Cypress.ConfigOptions, options: Partial, withBrowsers = true) {
assert(this._testingType, 'Cannot build base full config without a testing type')
this.validateConfigRoot(configFileContents, this._testingType)
@@ -479,6 +489,7 @@ export class ProjectConfigManager {
cliConfig: options.config ?? {},
projectName: path.basename(this.options.projectRoot),
projectRoot: this.options.projectRoot,
+ repoRoot: this.repoRoot,
config: _.cloneDeep(configFileContents),
envFile: _.cloneDeep(envFile),
options: {
diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts
index 2dfdc3de73..00e2fc1eaf 100644
--- a/packages/data-context/src/data/ProjectLifecycleManager.ts
+++ b/packages/data-context/src/data/ProjectLifecycleManager.ts
@@ -395,19 +395,17 @@ export class ProjectLifecycleManager {
this.ctx.update((s) => {
s.currentProject = projectRoot
s.currentProjectGitInfo?.destroy()
- if (!this.ctx.isRunMode) {
- s.currentProjectGitInfo = new GitDataSource({
- isRunMode: this.ctx.isRunMode,
- projectRoot,
- onError: this.ctx.onError,
- onBranchChange: () => {
- this.ctx.emitter.branchChange()
- },
- onGitInfoChange: (specPaths) => {
- this.ctx.emitter.gitInfoChange(specPaths)
- },
- })
- }
+ s.currentProjectGitInfo = new GitDataSource({
+ isRunMode: this.ctx.isRunMode,
+ projectRoot,
+ onError: this.ctx.onError,
+ onBranchChange: () => {
+ this.ctx.emitter.branchChange()
+ },
+ onGitInfoChange: (specPaths) => {
+ this.ctx.emitter.gitInfoChange(specPaths)
+ },
+ })
s.diagnostics = { error: null, warnings: [] }
s.packageManager = packageManagerUsed
diff --git a/packages/data-context/src/sources/GitDataSource.ts b/packages/data-context/src/sources/GitDataSource.ts
index e39e985e8b..1750f4ad4f 100644
--- a/packages/data-context/src/sources/GitDataSource.ts
+++ b/packages/data-context/src/sources/GitDataSource.ts
@@ -93,7 +93,13 @@ export class GitDataSource {
debug('exception caught when loading git client')
}
- if (!config.isRunMode) {
+ // don't watch/refresh git data in run mode since we only
+ // need it to detect the .git directory to set `repoRoot`
+ if (config.isRunMode) {
+ this.#verifyGitRepo().catch(() => {
+ // Empty catch for no-floating-promises rule
+ })
+ } else {
this.#refreshAllGitData()
}
}
diff --git a/packages/driver/cross-origin-testing.md b/packages/driver/cross-origin-testing.md
index d486edc26b..b88fa12be3 100644
--- a/packages/driver/cross-origin-testing.md
+++ b/packages/driver/cross-origin-testing.md
@@ -125,6 +125,10 @@ Having the **AUT** on a different origin than **top** causes issues with cookies
In order to counteract this, we utilize the [proxy](../proxy) to capture cookies from cross-origin responses, store them in our own server-side cookie jar, set them in the browser with automation, and then attach them to cross-origin requests where appropriate. This simulates how cookies behave outside of Cypress.
+## Dependencies
+
+Users can utilize `Cypress.require()` to include dependencies. It's functionally the same as the CommonJs `require()`. We handle the dependency resolution and bundling with the webpack preprocessor. We add a webpack loader that runs last. If we find a `Cypress.require()` call inside a `cy.origin()` callback, we extract that callback from the output code and replace references to `Cypress.require()` with `require()` calls. 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.
+
## Unsupported APIs
Certain APIs are currently not supported in the **cy.origin()** callback. Depending on the API, we may or may not implement support for them in the future.
diff --git a/packages/driver/cypress/e2e/cypress/stack_utils.cy.js b/packages/driver/cypress/e2e/cypress/stack_utils.cy.js
index 4b6e069c53..9bf68e3b97 100644
--- a/packages/driver/cypress/e2e/cypress/stack_utils.cy.js
+++ b/packages/driver/cypress/e2e/cypress/stack_utils.cy.js
@@ -23,6 +23,58 @@ describe('driver/src/cypress/stack_utils', () => {
})
})
+ context('getRelativePathFromRoot', () => {
+ const relativeFile = 'relative/path/to/file.js'
+ const absoluteFile = 'User/ruby/cypress/packages/driver/relative/path/to/file.js'
+ const repoRoot = 'User/ruby/cypress'
+ const relativePathFromRoot = 'packages/driver/relative/path/to/file.js'
+
+ const actualPlatform = Cypress.config('platform')
+ const actualRepoRoot = Cypress.config('repoRoot')
+
+ after(() => {
+ // restore config values to prevent bleeding into subsequent tests
+ Cypress.config('platform', actualPlatform)
+ Cypress.config('repoRoot', actualRepoRoot)
+ })
+
+ it('returns relativeFile if absoluteFile is empty', () => {
+ const result = $stackUtils.getRelativePathFromRoot(relativeFile, undefined)
+
+ expect(result).to.equal(relativeFile)
+ })
+
+ it('returns relativeFile if `repoRoot` is not set in the config', () => {
+ const result = $stackUtils.getRelativePathFromRoot(relativeFile, absoluteFile)
+
+ expect(result).to.equal(relativeFile)
+ })
+
+ it('returns relativeFile if absoluteFile does not start with `repoRoot`', () => {
+ Cypress.config('repoRoot', 'User/ruby/test-repo')
+ const result = $stackUtils.getRelativePathFromRoot(relativeFile, absoluteFile)
+
+ expect(result).to.equal(relativeFile)
+ })
+
+ it('returns the relative path from root if the absoluteFile starts with `repoRoot`', () => {
+ Cypress.config('repoRoot', repoRoot)
+ const result = $stackUtils.getRelativePathFromRoot(relativeFile, absoluteFile)
+
+ expect(result).to.equal(relativePathFromRoot)
+ })
+
+ it('uses posix on windows', () => {
+ Cypress.config('repoRoot', 'C:/Users/Administrator/Documents/GitHub/cypress')
+ Cypress.config('platform', 'win32')
+ const absoluteFile = 'C:\\Users\\Administrator\\Documents\\GitHub\\cypress\\packages\\app/cypress/e2e/reporter_header.cy.ts'
+ const relativeFile = 'cypress/e2e/reporter_header.cy.ts'
+ const result = $stackUtils.getRelativePathFromRoot(relativeFile, absoluteFile)
+
+ expect(result).to.equal('packages/app/cypress/e2e/reporter_header.cy.ts')
+ })
+ })
+
context('.getCodeFrame', () => {
let originalErr
const sourceCode = `it('is a failing test', () => {
@@ -93,6 +145,14 @@ describe('driver/src/cypress/stack_utils', () => {
expect($stackUtils.getCodeFrame(originalErr)).to.be.undefined
})
+
+ it('relativeFile is relative to the repo root when `absoluteFile` starts with `repoRoot`', () => {
+ Cypress.config('repoRoot', '/dev')
+ cy.stub($sourceMapUtils, 'getSourceContents').returns(sourceCode)
+ const codeFrame = $stackUtils.getCodeFrame(originalErr)
+
+ expect(codeFrame.relativeFile).to.equal('app/cypress/integration/features/source_map_spec.js')
+ })
})
context('.getSourceStack when http links', () => {
diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
index 5aca750195..e929d07d61 100644
--- a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts
@@ -195,9 +195,10 @@ context('cy.origin misc', () => {
})
it('verifies number of cy commands', () => {
+ // remove custom commands we added for our own testing
+ const customCommands = ['getAll', 'shouldWithTimeout', 'originLoadUtils']
// @ts-ignore
- // remove 'getAll' and 'shouldWithTimeout' commands since they are custom commands we added for our own testing and are not actual cy commands
- const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => command === 'getAll' || command === 'shouldWithTimeout')
+ const actualCommands = Cypress._.reject(Object.keys(cy.commandFns), (command) => customCommands.includes(command))
const expectedCommands = [
'check', 'uncheck', 'click', 'dblclick', 'rightclick', 'focus', 'blur', 'hover', 'scrollIntoView', 'scrollTo', 'select',
'selectFile', 'submit', 'type', 'clear', 'trigger', 'as', 'ng', 'should', 'and', 'clock', 'tick', 'spread', 'each', 'then',
diff --git a/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx
new file mode 100644
index 0000000000..c542249196
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.jsx
@@ -0,0 +1,14 @@
+describe('cy.origin dependencies - jsx', () => {
+ beforeEach(() => {
+ cy.visit('/fixtures/primary-origin.html')
+ cy.get('a[data-cy="cross-origin-secondary-link"]').click()
+ })
+
+ it('works with a jsx file', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ const lodash = Cypress.require('lodash')
+
+ expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
+ })
+ })
+})
diff --git a/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts
new file mode 100644
index 0000000000..687a38e992
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/origin/dependencies.cy.ts
@@ -0,0 +1,131 @@
+describe('cy.origin dependencies', () => {
+ beforeEach(() => {
+ cy.visit('/fixtures/primary-origin.html')
+ cy.get('a[data-cy="cross-origin-secondary-link"]').click()
+ })
+
+ it('works with an arrow function', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ 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')
+
+ cy.log('command log')
+ })
+ })
+
+ it('works with a function expression', () => {
+ cy.origin('http://foobar.com:3500', function () {
+ const lodash = Cypress.require('lodash')
+
+ expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
+ })
+ })
+
+ it('works with options object + args', () => {
+ cy.origin('http://foobar.com:3500', { args: ['arg1'] }, ([arg1]) => {
+ const lodash = Cypress.require('lodash')
+
+ expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
+ expect(arg1).to.equal('arg1')
+ })
+ })
+
+ it('works with a yielded value', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ const lodash = Cypress.require('lodash')
+
+ expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
+
+ cy.wrap('yielded value')
+ })
+ .should('equal', 'yielded value')
+ })
+
+ it('works with a returned value', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ const lodash = Cypress.require('lodash')
+
+ expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
+
+ return 'returned value'
+ })
+ .should('equal', 'returned value')
+ })
+
+ it('works with multiple cy.origin calls', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ const lodash = Cypress.require('lodash')
+
+ expect(lodash.get({ foo: 'foo' }, 'foo')).to.equal('foo')
+
+ cy.get('[data-cy="cross-origin-tertiary-link"]').click()
+ })
+
+ cy.origin('http://idp.com:3500', () => {
+ const dayjs = Cypress.require('dayjs')
+
+ expect(dayjs('2022-07-29 12:00:00').format('MMMM D, YYYY')).to.equal('July 29, 2022')
+ })
+ })
+
+ it('works with a relative esm dependency', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ const { add } = Cypress.require('./dependencies.support-esm')
+
+ expect(add(1, 2)).to.equal(3)
+ })
+ })
+
+ it('works with a relative commonjs dependency', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ const { add } = Cypress.require('./dependencies.support-commonjs')
+
+ expect(add(1, 2)).to.equal(3)
+ })
+ })
+
+ it('works with args passed to require result', () => {
+ const args = ['some string']
+
+ cy.origin('http://foobar.com:3500', { args }, ([arg1]) => {
+ const result = Cypress.require('./dependencies.support-commonjs')(arg1)
+
+ expect(result).to.equal('some_string')
+ })
+ })
+
+ it('works in support file', () => {
+ cy.origin('http://foobar.com:3500', () => {
+ expect(cy.getAll).to.be.undefined
+ })
+
+ cy.originLoadUtils('http://foobar.com:3500')
+
+ cy.origin('http://foobar.com:3500', () => {
+ expect(cy.getAll).to.be.a('function')
+ })
+ })
+
+ describe('errors', () => {
+ it('when dependency does not exist', () => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.include('Cannot find module')
+ })
+
+ cy.origin('http://foobar.com:3500', () => {
+ Cypress.require('./does-not-exist')
+ })
+ })
+
+ it('when Cypress.require() is used outside cy.origin() callback', () => {
+ cy.on('fail', (err) => {
+ expect(err.message).to.equal('`Cypress.require()` can only be used inside the `cy.origin()` callback.')
+ })
+
+ Cypress.require('./does-not-exist')
+ })
+ })
+})
diff --git a/packages/driver/cypress/e2e/e2e/origin/dependencies.support-commonjs.ts b/packages/driver/cypress/e2e/e2e/origin/dependencies.support-commonjs.ts
new file mode 100644
index 0000000000..27c23b84be
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/origin/dependencies.support-commonjs.ts
@@ -0,0 +1,7 @@
+function snakeCase (string) {
+ return _.snakeCase(string)
+}
+
+snakeCase.add = (a, b) => a + b
+
+module.exports = snakeCase
diff --git a/packages/driver/cypress/e2e/e2e/origin/dependencies.support-esm.ts b/packages/driver/cypress/e2e/e2e/origin/dependencies.support-esm.ts
new file mode 100644
index 0000000000..86d2d27fd8
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/origin/dependencies.support-esm.ts
@@ -0,0 +1,7 @@
+import _ from 'lodash'
+
+export const add = (a, b) => a + b
+
+export default (string) => {
+ return _.snakeCase(string)
+}
diff --git a/packages/driver/cypress/fixtures/secondary-origin.html b/packages/driver/cypress/fixtures/secondary-origin.html
index e8b77e5e2d..7342927fc3 100644
--- a/packages/driver/cypress/fixtures/secondary-origin.html
+++ b/packages/driver/cypress/fixtures/secondary-origin.html
@@ -14,8 +14,8 @@
hashChange
- /fixtures/primary-origin.html
+ /fixtures/primary-origin.html
+ http://www.idp.com:3500/fixtures/generic.html
@@ -279,6 +279,17 @@ const originalWithModifyObstructiveThirdPartyCode = `\
dynamicIntegrityScript.setAttribute('integrity', "sha384-XiV6bRRw9OEpsWSumtD1J7rElgTrNQro4MY/O4IYjhH+YGCf1dHaNGZ3A2kzYi/C")
document.querySelector('head').appendChild(dynamicIntegrityScript)
+