diff --git a/.eslintignore b/.eslintignore index c50ce0052813..45d0e83020f6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,7 +7,12 @@ **/dist **/dist-test **/node_modules -**/support/fixtures +**/support/fixtures/* +!**/support/fixtures/projects +**/support/fixtures/projects/**/_fixtures/* +**/support/fixtures/projects/**/*.jsx +**/support/fixtures/projects/**/jquery.js +**/support/fixtures/projects/**/fail.js **/test/fixtures **/vendor diff --git a/.eslintrc.json b/.eslintrc.json index 22f00333db5e..8a1dd583eed9 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,5 +5,7 @@ "extends": [ "plugin:@cypress/dev/general" ], - "rules": {} + "rules": { + "prefer-spread": "off" + } } diff --git a/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md similarity index 100% rename from ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE.md diff --git a/.github/PULL_REQUEST_REVIEW_COMMENT.md b/.github/PULL_REQUEST_REVIEW_COMMENT.md new file mode 100644 index 000000000000..3eb329d1f77a --- /dev/null +++ b/.github/PULL_REQUEST_REVIEW_COMMENT.md @@ -0,0 +1,43 @@ + +Thanks for the contribution! Below are some guidelines Cypress uses when doing PR reviews. + +- Please write \`[WIP]\` in the title of your Pull Request if your PR is not ready for review - someone will review your PR as soon as the \`[WIP]\` is removed. +- Please familiarize yourself with the PR Review Checklist and feel free to make updates on your PR based on these guidelines. + +## PR Review Checklist + +If any of the following requirements can't be met, leave a comment in the review selecting 'Request changes', otherwise 'Approve'. + +### User Experience + +- The feature/bugfix is self-documenting from within the product. +- The change provides the end user with a way to fix their problem (no dead ends). + +### Functionality + +- The code works and performs its intended function with the correct logic. +- Performance has been factored in (for example, the code cleans up after itself to not cause memory leaks). +- The code guards against edge cases and invalid input and has tests to cover it. + +### Maintainability + +- The code is readable (too many nested 'if's are a bad sign). +- Names used for variables, methods, etc, clearly describe their function. +- The code is easy to understood and there are relevant comments explaining. +- New algorithms are documented in the code with link(s) to external docs (flowcharts, w3c, chrome, firefox). +- There are comments containing link(s) to the addressed issue (in tests and code). + +### Quality + +- The change does not reimplement code. +- There's not a module from the ecosystem that should be used instead. +- There is no redundant or duplicate code. +- There are no irrelevant comments left in the code. +- Tests are testing the code’s intended functionality in the best way possible. + +### Internal + +- The original issue has been tagged with a release in ZenHub. diff --git a/.github/PULL_REQUEST_REVIEW_COMMENT_FOR_DEPENDENCY_UPDATE.md b/.github/PULL_REQUEST_REVIEW_COMMENT_FOR_DEPENDENCY_UPDATE.md new file mode 100644 index 000000000000..0ffdf0609910 --- /dev/null +++ b/.github/PULL_REQUEST_REVIEW_COMMENT_FOR_DEPENDENCY_UPDATE.md @@ -0,0 +1,18 @@ + +Below are some guidelines Cypress uses when reviewing dependency updates. + +## Dependency Update Instructions + +- Read through the entire changelog of the dependency's changes. If a changelog is not available, check every commit made to the dependency. **NOTE** - do not rely on semver to indicate breaking changes - every product does not follow this standard. +- Add a PR review comment noting any relevant changes in the dependency. +- If any of the following requirements cannot be met, leave a comment in the review selecting 'Request changes', otherwise 'Approve'. + +## Dependency Updates Checklist + +- Code using the dependency has been updated to accommodate any breaking changes +- The dependency still supports the version of Node that the package requires. +- The PR been tagged with a release in ZenHub. +- Appropriate labels have been added to the PR (for example: label \`type: breaking change\` if it is a breaking change) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000000..704c3a5a26a9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,44 @@ + + + + +- Closes + +### User facing changelog + + + +### Additional details + + + +### How has the user experience changed? + + + +### PR Tasks + + + +- [ ] Have tests been added/updated? +- [ ] Has the original issue been tagged with a release in ZenHub? +- [ ] Has a PR for user-facing changes been opened in [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation)? +- [ ] Have API changes been updated in the [`type definitions`](cli/types/index.d.ts)? +- [ ] Have new configuration options been added to the [`cypress.schema.json`](cli/schema/cypress.schema.json)? diff --git a/.gitignore b/.gitignore index 80e31ba98136..7f8a5d66f382 100644 --- a/.gitignore +++ b/.gitignore @@ -22,19 +22,14 @@ Cached Theme Material Design.pak packages/https-proxy/ca/ # from desktop-gui +packages/desktop-gui/cypress/videos packages/desktop-gui/src/jsconfig.json -# from driver -packages/driver/test/cypress/videos - # from example packages/example/app packages/example/build packages/example/cypress -# from driver -packages/driver/test/cypress/videos - # from server packages/server/.cy packages/server/.projects diff --git a/.node-version b/.node-version index 22333f1ec56f..4044f90867df 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -8.9.3 +12.0.0 diff --git a/.vscode/settings.json b/.vscode/settings.json index ae8c20a1d0d1..3f6006f21b08 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,4 +28,14 @@ }, ], "eslint.enable": true, + // this project does not use Prettier + // thus set all settings to disable accidentally running Prettier + "prettier.requireConfig": true, + "prettier.disableLanguages": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "json" + ] } diff --git a/.vscode/terminals.json b/.vscode/terminals.json index c566965a74aa..6f5b1d776bf0 100644 --- a/.vscode/terminals.json +++ b/.vscode/terminals.json @@ -29,7 +29,7 @@ "onlySingle": true, "execute": false, "cwd": "[cwd]/packages/server", - "command": "npm run test-e2e -- --spec name" + "command": "npm run test-e2e -- --spec [fileBasename]" }, { "name": "packages/runner watch", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3aa2a5558d09..7f0c7448a994 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,16 +4,17 @@ Thanks for taking the time to contribute! :smile: **Once you learn how to use Cypress, you can contribute in many ways:** -- Join the [Cypress Gitter chat](https://gitter.im/cypress-io/cypress) 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, [contact us](mailto:support@cypress.io). -- Write some documentation or improve our existing docs. Know another language? You can help us translate them. See our [guide to contributing to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). +- Join the [Cypress Gitter chat](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/develop/.github/CONTRIBUTING.md#adding-examples). +- Write some documentation or improve our existing docs. Know another language? You can help us translate them. See our [guide to contributing to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/.github/CONTRIBUTING.md). - Give a talk about Cypress. [Contact us](mailto:support@cypress.io) ahead of time and we'll send you some swag. :shirt: **Want to dive deeper into how Cypress works? There are several ways you can help with the development of Cypress:** - [Report bugs](https://github.com/cypress-io/cypress/issues/new) by opening an issue. - [Request features](https://github.com/cypress-io/cypress/issues/new) by opening an issue. -- Write code for one of our core packages. [Please thoroughly read our writing code guide](#writing-code). +- [Help triage existing issue](#triaging-issues). +- Write code to address an issue. We have some issues labeled as [`first-timers-only`](https://github.com/cypress-io/cypress/labels/first-timers-only) that are good place to start. [Please thoroughly read our writing code guide](#writing-code). ## Table of Contents @@ -52,7 +53,7 @@ Build status | Description ## Code of Conduct -All contributors are expecting to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). +All contributors are expecting to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). ## Opening Issues @@ -66,7 +67,7 @@ All contributors are expecting to abide by our [Code of Conduct](CODE_OF_CONDUCT - [Describe your problem, not your solution](#describe-problems) - [Explain how to reproduce the issue](#reproducibility). -Finally, if you are up to date, supported, have collected information about the problem, and have the best reproduction instructions you can come up with, you are ready to [open an issue](https://github.com/cypress-io/cypress/issues/new). +Finally, if you are up to date, supported, have collected information about the problem, and have the best reproduction instructions you can give, you are ready to [open an issue](https://github.com/cypress-io/cypress/issues/new). ### Update Cypress @@ -116,6 +117,8 @@ Some opened issue are questions, not bug reports or feature requests. Issues are ### Does this issue belong in this repository? +#### Other open source repos + Issues may be opened about wanting changes to our [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. @@ -123,6 +126,15 @@ Issues may be opened about wanting changes to our [documentation](), our [exampl - 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). +#### Our Dashboard Service + +Issues may be opened about wanting features in our Dashboard Service. In this case you should: + +- Thank them for expressing interest in a new feature. +- Refer them to the Dashboard ProductBoard: "You can express interest and see progress for this feature on our Roadmap from our Dashboard's product board here: https://portal.productboard.com/cypress-io/1-cypress-dashboard All related work for the Dashboard features is handled in that ProductBoard and will be handled by the Dashboard team directly when you comment there." +- Close the issue +- Close the issue to comments + ### 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. @@ -138,7 +150,7 @@ If an issue already exists you should: When opening an issue, there is a provided [issue template](./ISSUE_TEMPLATE.md). 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](./ISSUE_TEMPLATE.md) and that issues that are opened without this information are automatically closed per our [contributing guidelines](#fill-out-our-issue-template). +- Explain that we require new issues follow our provided [issue template](./ISSUE_TEMPLATE.md) 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? @@ -170,7 +182,7 @@ The best way to determine the validity of a bug is to recreate it yourself. Foll - Thank them for their contribution. - Explain that there is not 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. -- Link them to our contributing guideline for [opening issues](#opening-issues). +- Link them to our contributing guideline for [opening issues](#opening-issues). - 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. @@ -213,7 +225,7 @@ Some issues are resolved by the community, by giving some guidance or a workarou ## Writing Documentation Cypress documentation lives in a separate repository with its own dependencies and build tools. -See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). +See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/.github/CONTRIBUTING.md). ## Writing code @@ -362,9 +374,11 @@ When you edit files, you can quickly fix all changed files before you commit usi npm run lint-changed-fix ``` -When committing files, we run a Git pre-commit hook to lint the staged JS files. See the [`lint-staged` project](https://github.com/okonet/lint-staged). +When committing files, we run a Git pre-commit hook to lint the staged JS files. See the [`lint-staged` project](https://github.com/okonet/lint-staged). If this command fails, you may need to run `npm run lint-changed-fix` and commit those changes. +We **DO NOT** use Prettier to format code. You can find [.prettierignore](.prettierignore) file that ignores all files in this repository. To ensure this file is loaded, please always open _the root repository folder_ in your text editor, otherwise your code formatter might execute, reformatting lots of source files. + ### Tests For most packages there are typically unit and some integration tests. @@ -377,11 +391,22 @@ If you're curious how we manage all of these tests in CI check out our [`circle. #### Docker -Sometimes tests pass locally, but fail on CI. Our CI environment should be dockerized. In order to run the same image locally, there is script [scripts/run-docker-local.sh](scripts/run-docker-local.sh) that assumes that you have pulled the image `cypress/internal:chrome61` (see [circle.yml](circle.yml) for the current image name). +Sometimes tests pass locally, but fail in CI. Our CI environment is dockerized. In order to run the image used in CI locally: + +1. [Install Docker](https://docs.docker.com/install/) and get it running on your machine. +2. Run the following command from the root of the project: + + ```shell + npm run docker + ``` + +There is a script [scripts/run-docker-local.sh](scripts/run-docker-local.sh) that runs the cypress image (see [circle.yml](circle.yml) for the current image name). The image will start and will map the root of the repository to `/cypress` inside the image. Now you can modify the files using your favorite environment and rerun tests inside the docker environment. -**hint** sometimes building inside the image has problems with `node-sass` library. +##### Troubleshooting + +Sometimes building inside the image has problems with `node-sass` library. ```text Error: Missing binding /cypress/packages/desktop-gui/node_modules/node-sass/vendor/linux-x64-48/binding.node @@ -404,13 +429,13 @@ npm rebuild node-sass #### Docker for built binary -You can also use Docker to simulate and debug built binary. In a temp folder (for example from the folder `/tmp/test-folder/`) start a Docker image +You can also use Docker to simulate and debug the built binary. In a temporary folder (for example from the folder `/tmp/test-folder/`) start a Docker image: ```shell $ docker run -it -w /app -v $PWD:/app cypress/base:8 /bin/bash ``` -Point installation at a specific binary and NPM (if needed) and _set local cache folder_ to unzip downloaded binary into a subfolder. +Point the installation at a specific binary and npm (if needed) and _set local cache folder_ to unzip the downloaded binary into a subfolder. ```shell $ export CYPRESS_INSTALL_BINARY=https://cdn.cypress.io/beta/.../cypress.zip @@ -418,13 +443,13 @@ $ export CYPRESS_CACHE_FOLDER=./cypress-cache $ npm i https://cdn.cypress.io/beta/npm/.../cypress.tgz ``` -Note that unzipping Linux binary inside Docker container onto a mapped volume drive is slow. But once this is done you can modify application resource folder in local folder `/tmp/test-folder/node_modules/cypress/cypress-cache/3.3.0/Cypress/resources/app` to debug issues. +Note that unzipping the Linux binary inside a Docker container onto a mapped volume drive is *slow*. But once this is done you can modify the application resource folder in the local folder `/tmp/test-folder/node_modules/cypress/cypress-cache/3.3.0/Cypress/resources/app` to debug issues. ### Packages Generally when making contributions, you are typically making them to a small number of packages. Most of your local development work will be inside a single package at a time. -Each package documents how to best work with it, so simply consult the `README.md` of each package. +Each package documents how to best work with it, so consult the `README.md` of each package. They will outline development and test procedures. When in doubt just look at the `scripts` of each `package.json` file. Everything we do at Cypress is contained there. @@ -435,16 +460,33 @@ They will outline development and test procedures. When in doubt just look at th The repository is setup with two main (protected) branches. - `master` is the code already published in the last Cypress version. -- `develop` is the current latest "edge" code. This branch is set as the default branch, and all pull requests should be made against this branch. +- `develop` is the current latest "pre-release" code. This branch is set as the default branch, and all pull requests should be made against this branch. ### Pull Requests -- When opening a PR for a specific issue already open, please name the branch you are working on using the convention `issue-[issue number]`. For example, if your PR fixes Issue #803, name your branch `issue-803`. If there is not an associated open issue, **create an issue using our [Issue Template](./ISSUE_TEMPLATE.md)**. -- Please use the `address #[issue number]` or `close #[issue number]` syntax in the pull request description. This will automatically close the issue once the issue is merged. -- Add [tests](#tests)! We are a testing product afterall. 😉 +- When opening a PR for a specific issue already open, please name the branch you are working on using the convention `issue-[issue number]`. For example, if your PR fixes Issue #803, name your branch `issue-803`. If the PR is a larger issue, you can add more context like `issue-803-new-scrollable-area` If there is not an associated open issue, **create an issue using our [Issue Template](./ISSUE_TEMPLATE.md)**. +- PR's can be opened before all the work is finished. In fact we encourage this! Please write `[WIP]` in the title of your Pull Request if your PR is not ready for review - someone will review your PR as soon as the `[WIP]` is removed. +- Fill out the [Pull Request Tempalte](./PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleteing those sections. PR's will not be reviewed if this template is not filled in. - Please check the "Allow edits from maintainers" checkbox when submitting your PR. This will make it easier for the maintainers to make minor adjustments, to help with tests or any other changes we may need. ![Allow edits from maintainers checkbox](https://user-images.githubusercontent.com/1271181/31393427-b3105d44-ada9-11e7-80f2-0dac51e3919e.png) +### Pull Request Reviews + +After a PR has been opened, our `cypress-bot` will comment on the PR detailing the guidelines to be used to review Pull Requests. Please read these guidelines carefully and make any updates where you see the PR may not be meeting the quality of these guidelines. + +**Some rules about Pull Requests Reviews:** + +1. The contributor opening the pull request may not approve their own PR. +2. The PR will not be merged if some reviewers have voted "Needs changes". + +If any of the Pull Request Review guidelines can't be met, a comment will be left by the reviewer with 'Request changes'. Please make any updates as appropriate and we will rereview once those changes are addressed. + +**During a Pull Request Review, the following should be done:** + +- Run the code and use it as the end user would. Double check issue and PR description to ensure it is meeting requirements. +- Read through every line of changed code (Yes, we know this could be a LOT). +- If you don’t understand why some piece of code is required, ask for clarification! Likely the contributor had a reason and can provide the answer quicker than investigating yourself. + ### Testing This repository is exhaustively tested by [CircleCI](https://circleci.com/gh/cypress-io/cypress). Additionally we test the code by running it against various other example projects. See CI badges and links at the top of this document. @@ -455,13 +497,14 @@ To run local tests, consult the `README.md` of each package. We use [RenovateBot](https://renovatebot.com/) to automatically upgrade our dependencies. The bot uses the settings in [renovate.json](renovate.json) to maintain our [Update Dependencies](https://github.com/cypress-io/cypress/issues/3777) issue and open PRs. You can manually select a package to open a PR from our [Update Dependencies](https://github.com/cypress-io/cypress/issues/3777) issue. -Every PR for a package upgrade requires a review of the packages changes either from their changelog or their commits as well as all of the existing Cypress tests to pass. +After a PR has been opened for a dependency update, our `cypress-bot` will comment on the PR detailing the guidelines to be used to review the dependency update. Please read these guidelines carefully and make any updates where you see the PR may not be meeting the quality of these guidelines. + +## Deployment -#### If there are test failures or breaking changes: +We will try to review and merge pull requests quickly. After merging we will try releasing a new version. If you want to know our build process or build your own Cypress binary, read [DEPLOY.md](./DEPLOY.md) -- Note the breaking changes in a PR comment and note where the breaking change occured. -- Edit the PR to fix any breaking changes, if you are able. If you are not able, mark the PR review as 'changes requested' and note that there are breaking changes. +## Known problems -## Deployment +### ENFILE or EMFILE -We will try to review and merge pull requests quickly. After merging we will try releasing a new version. If you want to know our build process or build your own Cypress binary, read [DEPLOY.md](DEPLOY.md) +If you get `ENFILE: file table overflow`, `ENFILE: too many open files` or any other `ENFILE` or `EMFILE` errors on Mac, that means you are doing synchronous file system operations. Cypress should **NEVER** do them. Instead we should use async file system operations and let `graceful-fs` retry them. Find the place where the synchronous `fs` operation is done from the stacktrace and make it async. diff --git a/DEPLOY.md b/DEPLOY.md index 95b68b510af2..ac9d8ebfca34 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,7 +1,7 @@ # Deployment Anyone can build the binary and NPM package, but you can only deploy the Cypress application -and publish the NPM module `cypress` if you are a member of `cypress` NPM organization. +and publish the NPM module `cypress` if you are a member of the `cypress` NPM organization. > :information_source: See the [publishing](#publishing) section for how to build, test and publish a new official version of the binary and `cypress` NPM package. @@ -12,7 +12,11 @@ We build the NPM package and binary on all major platforms (Linux, Mac, Windows) providers. In order to set the version while building we have to set the environment variable with the new version on each CI provider *before starting the build*. -Use script command `npm run set-next-ci-version` to do this. +Use the script command below to to do this. + +```shell +npm run set-next-ci-version +``` ## Building @@ -28,12 +32,12 @@ Building a new NPM package is very quick. The steps above: - Build the `cypress` NPM package -- Transpiles the code into ES5 version to be compatible with the common Node versions -- Puts the result into the `cli/build` folder. +- Transpile the code into ES5 to be compatible with the common Node versions +- Put the result into the [`cli/build`](./cli/build) folder. You could publish from there, but first you need to build and upload the binary with the *same version*; this guarantees that when users do `npm i cypress@` they can download the binary -with the same version `x.y.z` from Cypress CDN service. +with the same version `x.y.z` from Cypress's CDN service. ### Building the binary @@ -43,13 +47,13 @@ First, you need to build, zip and upload the application binary to the Cypress s You can use a single command to do all tasks at once: -``` +```shell npm run binary-deploy ``` -You can also specify each command separately: +Or you can specify each command separately: -``` +```shell npm run binary-build npm run binary-zip npm run binary-upload @@ -57,7 +61,7 @@ npm run binary-upload You can pass options to each command to avoid answering questions, for example -``` +```shell npm run binary-deploy -- --platform darwin --version 0.20.0 npm run binary-upload -- --platform darwin --version 0.20.0 --zip cypress.zip ``` @@ -65,15 +69,13 @@ npm run binary-upload -- --platform darwin --version 0.20.0 --zip cypress.zip If something goes wrong, see the debug messages using the `DEBUG=cypress:binary ...` environment variable. -Because we had many problems reliably zipping the built binary, for now we need -to build both the Mac and Linux binary from Mac (Linux binary is built using -a Docker container), then zip it **from Mac**, then upload it. +Because we had many problems reliably zipping the built binary, for now we need to build both the Mac and Linux binary from Mac (Linux binary is built using a Docker container), then zip it **from Mac**, then upload it. ### Building Linux binary in Docker If you are using a Mac you can build the linux binary if you have docker installed. -``` +```shell npm run binary-build-linux ``` @@ -81,29 +83,28 @@ npm run binary-build-linux ### Before Publishing a New Version -In order to publish a new `cypress` package to the NPM registry, we must build and test it across -multiple platforms and test projects. This makes publishing *directly* into the NPM registry -impossible. Instead, we have CI set up to do the following on every commit to `develop`: +In order to publish a new `cypress` package to the NPM registry, we must build and test it across multiple platforms and test projects. This makes publishing *directly* into the NPM registry impossible. Instead, we have CI set up to do the following on every commit to `develop`: 1. Build the NPM package with the new target version baked in. 2. Build the Linux/Mac binaries on CircleCI and build Windows on AppVeyor. -3. Upload the binaries and the new NPM package to the `cdn.cypress.io` under the "beta" folder. +3. Upload the binaries and the new NPM package to `cdn.cypress.io` under the "beta" folder. 4. Launch the test projects like [cypress-test-node-versions](https://github.com/cypress-io/cypress-test-node-versions) and [cypress-test-example-repos](https://github.com/cypress-io/cypress-test-example-repos) using the newly-uploaded package & binary instead of installing from the NPM registry. That installation looks like this: - ``` + ```shell export CYPRESS_INSTALL_BINARY=https://cdn.../binary///cypress.zip npm i https://cdn.../npm///cypress.tgz ``` -Multiple test projects are launched for each target operating system, and the results are reported -back to GitHub using status checks so that it is easy to see if a change has broken real-world usage +Multiple test projects are launched for each target operating system and the results are reported +back to GitHub using status checks so that you can see if a change has broken real-world usage of Cypress. You can see the progress of the test projects by opening the status checks on GitHub: ![Screenshot of status checks](https://i.imgur.com/AsQwzgO.png) -Once all test projects are reliably working with new changes, publishing can proceed. +Once the `develop` branch for all test projects are reliably passing with the new changes, publishing can proceed. ### Steps to Publish a New Version +0. Make sure that if there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, the corresponding dependency in [`packages/example`](./packages/example) has been updated to that new version. 1. Make sure that you have the correct environment variables set up before proceeding. - You'll need Cypress AWS access keys in `aws_credentials_json`, which looks like this: ```text @@ -117,42 +118,58 @@ Once all test projects are reliably working with new changes, publishing can pro - Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations. 2. Use the `move-binaries` script to move the binaries for `` from `beta` to the `desktop` folder for `` - ``` + ```shell npm run move-binaries -- --sha --version ``` -3. Publish the new NPM package under the dev tag. The unique link to the package file `cypress.tgz` - is the one already tested above. You can publish to the NPM registry straight from the URL: - ``` +3. Publish the new NPM package under the `dev` tag. The unique link to the package file `cypress.tgz` is the one already tested above. You can publish to the NPM registry straight from the URL: + ```shell npm publish https://cdn.../npm/3.4.0//cypress.tgz --tag dev ``` 4. 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): - ``` + ```shell dist-tags: dev: 3.4.0 latest: 3.3.2 ``` -5. Test `cypress@3.4.0` again to make sure everything is working. You can trigger test projects - from the command line (if you have the appropriate permissions) +5. Test `cypress@3.4.0` again to make sure everything is working. You can trigger test projects from the command line (if you have the appropriate permissions) ``` node scripts/test-other-projects.js --npm cypress@3.4.0 --binary 3.4.0 ``` -6. Update and publish the changelog and any release-specific documentation changes in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). -7. Make the new NPM version the "latest" version by updating the dist-tag `latest` to point to the new version: - ``` +6. Test the new version of Cypress against the Cypress dashboard repo. +7. Update and publish the changelog and any release-specific documentation changes in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). +8. 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@3.4.0 ``` -8. Run `binary-release` to update the download server's manifest, set the next CI version, and create an empty version commit: - ``` +9. Run `binary-release` to update the download the server's manifest, set the next CI version, and create an empty version commit: + ```shell npm run binary-release -- --version 3.4.0 --commit` ``` -9. Tag the current commit with `v3.4.0` and push that tag up. -10. If needed, push out the updated changes to the docs manifest to `on.cypress.io`. -11. If needed, push out an updated kitchen sink. -12. Close the release in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release). -13. Bump `version` in `package.json` from `develop` branch and then merge into `master`. -14. Using [cypress-io/release-automations][release-automations]: +10. 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). +11. 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). +12. 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. +13. Bump `version` in [`package.json`](package.json) and commit it to `develop` using a commit message like `release 3.4.0 [skip ci]` +14. Tag this commit with `v3.4.0` and push that tag up. +15. Merge `develop` into `master` and push that branch up. +16. Using [cypress-io/release-automations][release-automations]: - Publish GitHub release to [cypress-io/cypress/releases](https://github.com/cypress-io/cypress/releases) using package `set-releases` (see its README for details). - Add a comment to each GH issue that has been resolved with the new published version using package `issues-in-release` (see its README for details) +17. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. +18. Decide on the next version that we will work on. For example, if we have just released `3.7.0` we probably will work on `3.7.1` next. Set it on [CI machines](#set-next-version-on-cis). +19. Try updating as many example projects to the new version. You probably want to update by using Renovate dependency issue like [`cypress-example-todomvc` "Update Dependencies (Renovate Bot)](https://github.com/cypress-io/cypress-example-todomvc/issues/99). Try updating at least the following projects: + - https://github.com/cypress-io/cypress-example-todomvc + - https://github.com/cypress-io/cypress-example-todomvc-redux + - https://github.com/cypress-io/cypress-example-realworld + - https://github.com/cypress-io/cypress-example-recipes + - https://github.com/cypress-io/cypress-example-docker-compose + - https://github.com/cypress-io/cypress-example-api-testing + - https://github.com/cypress-io/angular-pizza-creator + - https://github.com/cypress-io/cypress-fiddle + - https://github.com/cypress-io/cypress-example-piechopper + - https://github.com/cypress-io/cypress-documentation Take a break, you deserve it! :sunglasses: [release-automations]: https://github.com/cypress-io/release-automations +[cypress-example-kitchensink]: https://github.com/cypress-io/cypress-example-kitchensink diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 00497ede6876..000000000000 --- a/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ - - -- Closes - -### Pre-merge Tasks - - - -- [ ] Have tests been added/updated for the changes in this PR? -- [ ] Has a PR to [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation) been submitted to document any user-facing changes? -- [ ] Have the [type definitions](cli/types/index.d.ts) been updated with any user-facing API changes? -- [ ] Has the [cypress.schema.json](cli/schema/cypress.schema.json) been updated with any new configuration options? -- [ ] Has the original issue been tagged with a release in ZenHub? diff --git a/README.md b/README.md index f02051cb342b..216065392d5b 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ Gitter chat -
- - + + StackShare +

## What is Cypress? @@ -53,13 +53,13 @@ npm install cypress --save-dev - [![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/develop.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress/tree/develop) - `develop` branch - [![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/master.svg?style=svg)](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. +Please see our [Contributing Guideline](./CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps. ## License -[![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cypress-io/cypress/blob/master/LICENSE.md) +[![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cypress-io/cypress/blob/master/LICENSE) -This project is licensed under the terms of the [MIT license](/LICENSE.md). +This project is licensed under the terms of the [MIT license](/LICENSE). ## Badges diff --git a/appveyor.yml b/appveyor.yml index a0ab300d40e2..10dcf01cb3ca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,30 +7,29 @@ branches: # https://www.appveyor.com/docs/lang/nodejs-iojs/ environment: # use matching version of Node.js - nodejs_version: "8.9.3" + nodejs_version: "12.0.0" # encode secure variables which will NOT be used # in pull requests # https://www.appveyor.com/docs/build-configuration/#secure-variables # the variables can be encrypted at # https://ci.appveyor.com/tools/encrypt ci_json: - secure: tf3fK5S5Gh5HGUcDo3eGw7nqdcFU/4A+2s3JJovmn/eA0p9dEspjPFp7G1I9BxdUc4OoCeJ3dSSrCQ1BPIzb/bzK6aQqAZQWNcJ1sanoIiF0QUPbiW40Js3xpWylIh8qutVoaWtZz5a1ygg9sJmAYR7qB5+aQqQNA55TBKkUCydXpnDBfWuagb6d/7cblULsXasvvji3RIoxWTKd8HmaD/xxqONjPAJ3IJsiDTaWc5S9bAgV8/IYa7YZaQm5vpTTsWU5IGwkA1l9yMu7j+7BSNK9esvAYyKsx7kUV9jiVFo= + secure: uOM7Bj+6MfQA/wiUzA4MolZDlcdhIqrOWLN0LdR+Lg4olc1onF3IpWfRf+3B6Q5uT98OTnIU71OOqSRY7inGQg== # for uploading built binary to S3 bucket aws_credentials_json: - secure: ttGzd2/rW+i8H+pozcFxzZKU07B5INL8+LjD4vCOKes+tI6EaKhrLvAQ9xT7r+e1oTWbC8olZQ96ZZ8P5Ve8pIpG8oe1ITMs5f50iXaKULfwIcJOm+G8a3pkMRZOWa0wGs7/sKtRSyIpMFRfCOIl8TePBKEgeRtVzixBqSuyYLn/u2dz0z8uHeJDq/H1kJlI + secure: ttGzd2/rW+i8H+pozcFxzZKU07B5INL8+LjD4vCOKes+tI6EaKhrLvAQ9xT7r+e1p8My8f4LrqvT+i37kbRCUPY4DHmUgagj3aj0OghsT0eX/Vr/6T4v1yndB7SX7FnG07eVcGb63r9f5kT7xu7ElJ9WXh1Ok5K69W0zDPsa1RGCCYqsTi4tH2h5EAZwjY1b # CDN control - CF_DOMAIN: "cypress.io" - CF_EMAIL: - secure: +kZOcImCZVZJv/e/hQc3gvJ6xXSH88qg46cMwKn8mRo= CF_TOKEN: - secure: d8SQfJ2r6qrKDjYWoFg3AzgY7aL6hTuE5OIlRr0TXkcXkZzdmYCujfzIYcCQfpZg + secure: nVGxcWxXGvrT621HmgMf9Mwm84dqo+cKHZj94OvyyWEGIZOJJkbWHsD/l2/r4a6Y + CF_ZONEID: + secure: SrZnd4BYW9PILEEi7y9VyXuC16C+qMEDT3QuV1PZZJa47TRkjcaYpAXhAHz0j0jT # authenticate as Cypress bot when posting commit status checks GH_APP_ID: secure: oR0RVDbv6GKej4wwjkz7Zw== GH_INSTALLATION_ID: secure: tAoqu4zIgZUxOfW0u9YQgw== GH_PRIVATE_KEY: - secure: msLmlIBnkNovqrqTeCqa7ZPjETyS8Xn4JLuiRMWYK7gZBTO66pNnFaoeqwPFwH+o9fhC3NYlr9IFIeChXEtMJljqRfZbvfKbgbQGlVsFrg4GdFOKE24w7Hq/M0pjeFsffJ4gr+tFEya4Hri010Z8tD8kFeAv6S6y4rnOz20wambNNMb3C10Jrw5kHk4ED0h+KxggFFgmC8E+23bLuYqyXhgWHiDMDuFYuZTg6di/YExVNTo54bv8vakbdpAiX6VA4sy8jcu9134vygwPyEgDbJVIHYTbQQdiEcKc5V7+7ccF5dOZCDu3YQ0gG5CTcCKlMSts/nYeTnyRL1nGMK63HgzejGy7w7uUigo50tEIt0NQNpx03S8A65LjTJsPKjmIrh3KTPsxmrnFBhlDE4kdLNsQYptRe3oGtIJ/8DpyinAmbLVkq1xV66lQ9EqQjiJlKdK+X56EZYYBu9F49i9MfcZTtlK9uDHdzZAZJagZzIEJ4T+6sl2Q/gDMXwB7OO1+CAe3oUpaLNda9T4APzplzpFdSePTjtDI/5bzXD6DUs5xhuWqCDo+tZCHW7uBYdGTD3mNf1H1Su+HTHB0G6goHyljwBrKvRB5dm4jEYGiTt8ndxmVhZd/bzI9Uftd4mx0hJeselkXCFrmzTVGpFa9wbpbD6A1TVKwwBSbXlj11ON7ktAaPClvRAjCcwyWt0YglXlUC8tMXCml7mmFNZx8moqAAC8uf8ZG8+wI9ip1vjCsPs/yE/RY/yayWkjfYFWnh/LDmLV20gYnlk0YCkezM43lm1+dcNfI1DrAyu3sMh3xxnGXUZt2CdKfYCDRn8u32MnXkgdHre//6oIdlg5yVCHLwfBkO0Tc+qr2O+jlWYNK2k1UvghSS+w10iVbMi2XHIKKvnQ2+dkpTiDX7AIzCZvDG4lWreQMMZzuAkUvSM8qESBP4txHUVAYQy9bvToLPQVz1pTQaFasoczMOgwDidfO6+z45pm/hv3IHgEVVV/LD2B3pQlUNgMYYfn2F5Qj81wsGCudV0foznIwcLWA7dCW6KkGzK5+Sn0qS2FOcPbOPorJRLLNLbOO5oIS3EcnOOxgSwouXFzag0twpI7bfeqjeKqkd7ung3G36Mq6qrlTq4mAOJXfiETFhE7WCQz8ecz2LqV1dz0vcwsQeeoVCoK4RCNI2BxwvEwHrk8BLLgSFr8zwoOCAiViWVqphZ0SfIM/Jov7lNKls5ZJmRlyuJuI825wxEZTogNe9ZCUAA8R8CnfmaHfaJAMbEKbs4SXV57H5M33XKf9QWSvHoHusn4yVXcpIxftI7SQ14NaiOHBrAcuRMA6ZQe1/s0fN6aAnx96ToT4shKn0TOq84DJm2P48FjwI+avZ5FDrdZVu9nZ99MiZIw6sVE9BW/LTyZOchEAJvZGA0yTiJyCHhAspQqn8vS1RUZFy1vSfymyHuh76z0rzZMfwHZTmdDUrBFouc1bSi16/QbOq2ZrmO4B7GEPInJRSLB7MPqkv+W8F3v7h/YW9tS75ZIROwj/EYfhfsyjpgSSeDImQN4YYpG1fv5Ty7GPG5i4TXaxz0wtyElRnY22fxMUEWXboyvbxseB2QWo5RaUXcOe5X1bL3UynTZmZJGcOgp9tLv350D43t3eJ/c6i6lXDzBWiJOcHeXkwjTY1WT14qZfpZ0owYF+9Wk8tMOPWBUTwCioXLs8uckIHJzdZHPUGaIzFITlcrDMuDaknS5cTHRLWbiD/eJuvey827JtYOz0/yx3FCTZsbVTNdBAaxZcNrJE13UzTq58XKwAu+O7eQYn0/wuHuWkZj7dHdoafg6jJG3ArxrCO31a2aQvSVn5jfKzLz3Y42hBl1USAC8bQ2Dx5vl0rFyhc9YfwqO5tIRcW1Z8a+e8n91WFms1Y77fAEW4s8Q6BccvmLrsocOFhDrgTeJPlf0LGfj5lvR3EFfFOO4m97pQSmt0tjsP0aaro6Afsx67np37LztcdleedqcyvLA/LVWgWf0xCTbX81p94ri5g2pH1KQwT/ETRSv4pomBgU3EG1C19Jnld0L3N9aYVxEDfJyfITakO1LxZFNuDUdNu4eu4H67Fv0Lvm0vBMg2IQ19jB0yvqrMMcDwC5OEc+YmduVSuR7Tj6DH8+KBANFm5xQi4q1H1QKNJlrwjNlDjJST1Qd1Qam14NJ4buJpf0t6XS+RAvppPSp2LiDjON9LE3qkEQNTP+Qpdf0jTOVPTzp9L+O5W0lm/1dX0khcvsZoRghq/tHZS9PeIIxKLmwBS3sPL+G9ao2xcQp9D1bvAcQPECWswp5ndfi+fUmRgnHxKqE0UAUB+sLb4rE8QkWwijjVZDOf9BpnJ5wQO36w3ZqC1z3bKFMraDLnS/gBjaKx3HAwesSLEzWIgcyyfhpTZJMDDnKcbSk3whzs2RSYL7uu41jCMebo6x+JCTiJX7ZYvlJybbHTQCcHK+iuV7Z7T2ss4ibHTs2w94nYutiKOSIN9+LMffrG20PhS9qlJbk6c9UkeT0VKGb278QAVcgm1w1GQSy/OKXxPcvb6s/A/uhrAbLo2H5qIX0pqjlLSuynm++3u0ocDDUp9aBl0qJZQT5ONPBJsy3UCbawNFcXu8DcLOMcu/v4LCwCJQPt0fdi+luZG1VPKlHTmzE2m0VP0uf3zEGquUBYzM0YTT9MxyyBRCZIfBWdIIVMsjg8KyEGdyK2RKNRD3oANoTKa4RbxiXk6CJwzhB8d/9cmt2ra7M1aH9T4N+GhXP3tzaNxem1tWAy3l0n5StpCYQYQ5fsgw32RSHYcmwCmGobbONwauU4V1qHqtYLKsrlVpB8A9owgqE1Hq9s6LEAeH5Bu7X6+jhx9nTWGF03TZw+X8PJgJcC3Fx8gvOfhOZ6T+MeoFoSKFoJfpJmgGsw6QQPigdSegbf3a1/1/TypWWKxKPu4149g5R8lf0hTRg6deaYQ19W3S09oYIHVSF9e0XBUNDDr4DQ8QKI9wxAm6V+DhyvmlAonXGq + secure: msLmlIBnkNovqrqTeCqa7ZPjETyS8Xn4JLuiRMWYK7gZBTO66pNnFaoeqwPFwH+ooO0cDFhAOPTToLisgTLXCo4hnw38zuBuKq+ywCh5mtk5uZn4x4F8G2XyRLD/ViZm+VuD2yZzaTWF11upDqC4xbXDe32yD6OSLKhA5ms5F5ke83zEuWSLTqVVCIpVH12rVTJHl3QHaWPwZbBBE3SFN8D6uiclvI06y3pEg2bVShU8YqlwearYTRuErsYXNCUmT0SrDd2kHznlYf08edQDHpydnQvvTViZMgomvYp5wDCXFD+/FxtTMuTptJFpspirXL8w/xjYy1/JaTd/K01oUUD2Xwl/v0cS28OpdcraETyrQxQhEgTCXfg9ONbZ5mRvQlkaRROaTqDSGMmEPs4N91zarpA7RLxu7PPvxXQcbDW4GiJvH5BhVWu8lY/QBZsr8It1dhLYSzTPNIh9ey8xNaUbZ3oQhPBoreRi36B+FSPBsrZpB8Q8aa97gd+lCa8br2RfaEpzx8gA0pSK44odqcGuJe7T8MHOqYo0cUEUb2UypPPG7mWyjGip+x3Z9P/vSrZzDV+YFFvEzQAMoyRMp/456V+YL8iduryMRIadkJcB4ZVZz2hsxY5Gv6Eeh9NhwzyM64Rz5NP5fJ9Kw8E5Vm+ddEmft8Ec6dajcURoVN0i+s8t7h/e3Hzrr62UjWr0FpUx5fPBC/Tldn3+h4Rr9/HFI2RCZAI5wHOrx/aQ/HknA9UCEdqdod8ix5yAdSpTxp3aCGEoS97STXU43CjLEiQFyLaReoHOOwFp5EqaAiAqiORJaKuShWoir+OqSk7rucU7kFvIlU9GDfLuKUpxcQoDq/8fKT3lcG3Pr4MVV79BJ6EcjcsEf4ukQ3IfwMY+2RbwYWEowsQP18k4HztZpMEOuYPlSCiAPL7Cz4dcE5oybSURr9QQbSqVMoiCKZBn344KxpvH59KW90wt8CYyoeLSlPpM9s73g9My4fwbB3W9lcbw/AteRGer01VYEHY+1MyQwhqgHoXQ//op4gztFbpSLcli88v1IOopcr0Dw5NrylcjCTKuVWmQs0uIAfOr7zxqCZ8DCXG6spdipjF1jx+bxp318ZgH56pmmTOTMbj5Cmdpr3KlCFbYB4JI7lexnZmti1NcHtOglDSq+XT4092myAiarSzQLA6smB+gk68M50W492+QNuc+6LAOfev+Da4geLiErqMpuIqfA3jw4h5+9Ns6mf3JnOLZd1c/X/xvnV3JjBzSJ6f9xGMLBcMTQm/wVfkHM9tO1oZrHswDiBlE1AkQrj6kqT9Kznu/rbAUGRnWL65FoCwdMbYVEhQQvLbLvVCRGBJfB01oD2xs80jyZ2YYZFRZCl/d0lGrVVVZsq6XM7CsxR5WlpJy5JLxCQ4kliG8cjexh0GkVYJoRYneJifw8yThMlyAnMQ88iNS2p2MnYk0WZgTJOIHliIhPRFY4z6BtrxmL8SR1no1vhaQCdbE5RI/rYbk8NpOmQunkjcDwp7nTKn1d8bMTfKGUH+DzhvmqwxA5PW37P84FFSK+3ePY9+oKXcInkAaxiXUpzcZJ4KzUGEZaZCB6irU+sxs6QLDzsq05PprwVz2DGtEn1TcY8qQ6ezeMGxJMRgDvEGq2J0nEgOEZ98CJ7XiPJRlnvUjGUzBlcjnbfFH8zzl/0p189YtENhE6Fyr5bD9MAI6NpVHjLLlg3yjmQ6X95fUtiNCmSpCUveEqIQCRtHCY2E/RrulGqTWE+vCvbM6IJV3WnatPOtWZfXEntWHmS08j6aUkUDM9TodBuzG8TRhW2Kgv8b4pfoejuMa4WkvwRAUU7V+clTWG26dT9UHdk+QuOIQDUiCewWk3PmpIJI4WdcxpBWwDvIgojob7uaGzhkabFKi77RJRc5/Ulxm6yM2MX79jgJxrQprWxxkjlsQnJk186nQZQqpuwziH/ZxV82n1bmI9zCqMXgE1Yr86gvyZpk2UbWhlFdtXEPapge9Cfo/fWUBCIbVcd77Bk98E88Y5Y372YWW+D8oHZed8l+0tCeyZmoHQNCYykcf6w77C+8C+bVdJplPns96vyLgbWIr0cpqZBK4qmkAxHuKZoG0AKRw4U379lnXOsI+02TaTzGOMlFTg4ME5miCbxo/2pUnjrydyTE5evdImLzKAK50Fhy1XASaPxgLrkjhGZebwf1UD2kYg6A1NCHchQId25vSEwGRkMPWvY3a5KOmgsMmRoOUJ17uo/r57p7nLgZV9c1+YEdZxu+GmgwQDLNGpgW1cpEN6GSVpx8xhaGKeYSuqd4lh6H9U5/P8masNckrsz+EHv+w5plzx8nJ/Fx/H50OdOm1KUjo66m26aITX7EjJB/U1qtqNfiK6dt8EttJ5iRXlCbfOkj2biRYeKbXQ2Ezr+61/Mu/W/nhLqmLFDtM6K3xf2bSJnEXQFZOOXTRkKXnRDP7Y47ZgG3563fJQjSfoU4Hsw5xnegTOKlJsoEm95Rnq0esdMTA450Ki2wBOeIsOycljoApACBYLAlSe+ewxEaOjrLtnIR0LfzcKXlCRYbM31YWOCtMhMRehJbX9qWGNPTQHmjabYz7/IhLKtJuaMIpj3pfYgS/oQQ36g6ItCo7vLQAq+rgU99IUyQROOGXMUgK/8umL71oijA9dht0LmH9E7EGwih0WuLO2SndovTJODDfK9YrRTEocbo3B9S05O4fpGoQ32TK99mXjoQdlyxd/dn9Q9uDD27u/fGgUoYdt9VzAIigbRIQuRx430n33V0ZyXv90QuD4ESOLxVI1vnLj6JKAS4PGRz66rouYG6U+1syDWpf5Y6DzC/2KOfdLPwmuwjMQxuhf+6+tGeJbeotNX/eJF0LkRfyieRwEGKxIo0PaxdmVwsF7vKR6ZnOpr5BuLm/+44Rg3bQdJ4bcRW6i6dIhOyHWniLvsAPLu1NZDVN6jA13KTChhcrNnSGddjRFLekawl80E3KhG1p+KvItIZX3kzG4QjJ platform: - x64 @@ -54,6 +53,7 @@ install: - node --version - node --print process.arch - npm --version + - npm run check-next-dev-version # prints all public variables relevant to the build - print-env Platform - npm run check-node-version diff --git a/bulk-decaffeinate.config.js b/bulk-decaffeinate.config.js index 7f81e1d7f506..e7af5f254a2d 100644 --- a/bulk-decaffeinate.config.js +++ b/bulk-decaffeinate.config.js @@ -13,5 +13,10 @@ module.exports = { path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-multi-assign-class-export.js'), path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-implicit-return-assignment.js'), path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-existential-conditional-assignment.js'), + './scripts/decaff/remove-comment-sharp.js', + './scripts/decaff/switch-false.js', + './scripts/decaff/empty-catch.js', + './scripts/decaff/no-cond-assign.js', + './scripts/decaff/arrow-comment.js', ], } diff --git a/circle.yml b/circle.yml index d1824b323ac9..7f0b1f59a685 100644 --- a/circle.yml +++ b/circle.yml @@ -1,5 +1,14 @@ version: 2.1 +# usually we don't build Mac app - it takes a long time +# but sometimes we want to really confirm we are doing the right thing +# so just add your branch to the list here to build and test on Mac +macBuildFilters: &macBuildFilters + filters: + branches: + only: + - develop + defaults: &defaults parallelism: 1 working_directory: ~/cypress @@ -19,11 +28,20 @@ defaults: &defaults COLUMNS: 100 LINES: 24 + executors: # the Docker image with Cypress dependencies and Chrome browser cy-doc: docker: - - image: cypress/browsers:node8.9.3-npm6.10.1-chrome75 + - image: cypress/browsers:node12.8.1-chrome78-ff70 + environment: + PLATFORM: linux + + # Docker image with non-root "node" user + non-root-docker-user: + docker: + - image: cypress/base:12.0.0 + user: node environment: PLATFORM: linux @@ -37,6 +55,30 @@ executors: PLATFORM: mac commands: + run-e2e-tests: + parameters: + browser: + description: browser shortname to target + type: string + chunk: + description: e2e test chunk number + type: integer + steps: + - attach_workspace: + at: ~/ + - run: + command: npm run test-e2e -- --chunk << parameters.chunk >> --browser << parameters.browser >> + working_directory: packages/server + - store_test_results: + path: /tmp/cypress + - store-npm-logs + + store-npm-logs: + description: Saves any NPM debug logs as artifacts in case there is a problem + steps: + - store_artifacts: + path: ~/.npm/_logs + # for caching node modules use project environment variable # like CACHE_VERSION=15 save-cache: @@ -217,7 +259,8 @@ jobs: # Install the root packages # Link sup packages in ./node_modules/@packages/* # Install sub packages dependencies and build all sub packages via postinstall script - - run: npm install + # try several times, because flaky NPM installs ... + - run: npm install || npm install - run: name: Top level packages command: npm ls --depth=0 || true @@ -226,6 +269,7 @@ jobs: - run: npm run all prune - save-caches + - store-npm-logs ## save entire folder as artifact for other jobs to run without reinstalling - persist_to_workspace: @@ -240,6 +284,7 @@ jobs: at: ~/ ## this will catch .only's in js/coffee as well - run: npm run lint-all + - store-npm-logs unit-tests: <<: *defaults @@ -255,6 +300,8 @@ jobs: - run: npm run test-mocha-snapshot # make sure packages with TypeScript can be transpiled to JS - run: npm run all build-js + # test codemods + - run: npm run test-jscodeshift # run unit tests from individual packages - run: npm run all test -- --package cli - run: npm run all test -- --package electron @@ -262,13 +309,14 @@ jobs: - run: npm run all test -- --package https-proxy - run: npm run all test -- --package launcher - run: npm run all test -- --package network - # how to pass Mocha reporter through zunder? + - run: npm run all test -- --package proxy - run: npm run all test -- --package reporter - run: npm run all test -- --package runner - run: npm run all test -- --package socket - run: npm run all test -- --package static - store_test_results: path: /tmp/cypress + - store-npm-logs lint-types: <<: *defaults @@ -285,6 +333,7 @@ jobs: - run: command: npm run dtslint working_directory: cli + - store-npm-logs "server-unit-tests": <<: *defaults @@ -295,6 +344,7 @@ jobs: - run: npm run all test-unit -- --package server - store_test_results: path: /tmp/cypress + - store-npm-logs "server-integration-tests": <<: *defaults @@ -305,6 +355,7 @@ jobs: - run: npm run all test-integration -- --package server - store_test_results: path: /tmp/cypress + - store-npm-logs "server-performance-tests": <<: *defaults @@ -317,98 +368,123 @@ jobs: path: /tmp/cypress - store_artifacts: path: /tmp/artifacts + - store-npm-logs - "server-e2e-tests-1": + "server-e2e-tests-chrome-1": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 1 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 1 - "server-e2e-tests-2": + "server-e2e-tests-chrome-2": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 2 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 2 - "server-e2e-tests-3": + "server-e2e-tests-chrome-3": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 3 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 3 - "server-e2e-tests-4": + "server-e2e-tests-chrome-4": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 4 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 4 - "server-e2e-tests-5": + "server-e2e-tests-chrome-5": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 5 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 5 - "server-e2e-tests-6": + "server-e2e-tests-chrome-6": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 6 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 6 - "server-e2e-tests-7": + "server-e2e-tests-chrome-7": <<: *defaults steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 7 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + - run-e2e-tests: + browser: chrome + chunk: 7 - "server-e2e-tests-8": - <<: *defaults - steps: - - attach_workspace: - at: ~/ - - run: - command: npm run test-e2e -- --chunk 8 - working_directory: packages/server - - store_test_results: - path: /tmp/cypress + "server-e2e-tests-chrome-8": + <<: *defaults + steps: + - run-e2e-tests: + browser: chrome + chunk: 8 + + "server-e2e-tests-electron-1": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 1 + + "server-e2e-tests-electron-2": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 2 + + "server-e2e-tests-electron-3": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 3 + + "server-e2e-tests-electron-4": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 4 + + "server-e2e-tests-electron-5": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 5 + + "server-e2e-tests-electron-6": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 6 + + "server-e2e-tests-electron-7": + <<: *defaults + steps: + - run-e2e-tests: + browser: electron + chunk: 7 - "driver-integration-tests-3x": + "server-e2e-tests-electron-8": <<: *defaults - parallelism: 3 + steps: + - run-e2e-tests: + browser: electron + chunk: 8 + + "driver-integration-tests-chrome": + <<: *defaults + parallelism: 5 steps: - attach_workspace: at: ~/ @@ -423,12 +499,38 @@ jobs: command: | CYPRESS_KONFIG_ENV=production \ CYPRESS_RECORD_KEY=$PACKAGES_RECORD_KEY \ - npm run cypress:run -- --record --parallel --group 3x-driver-chrome --browser chrome + npm run cypress:run -- --record --parallel --group 5x-driver-chrome --browser chrome working_directory: packages/driver - store_test_results: path: /tmp/cypress - store_artifacts: path: /tmp/artifacts + - store-npm-logs + + # "driver-integration-tests-electron": + # <<: *defaults + # parallelism: 5 + # steps: + # - attach_workspace: + # at: ~/ + # - run: + # command: npm start + # background: true + # working_directory: packages/driver + # - run: + # command: $(npm bin)/wait-on http://localhost:3500 + # working_directory: packages/driver + # - run: + # command: | + # CYPRESS_KONFIG_ENV=production \ + # CYPRESS_RECORD_KEY=$PACKAGES_RECORD_KEY \ + # npm run cypress:run -- --record --parallel --group 5x-driver-electron --browser electron + # working_directory: packages/driver + # - store_test_results: + # path: /tmp/cypress + # - store_artifacts: + # path: /tmp/artifacts + # - store-npm-logs "desktop-gui-integration-tests-2x": <<: *defaults @@ -449,6 +551,7 @@ jobs: path: /tmp/cypress - store_artifacts: path: /tmp/artifacts + - store-npm-logs "reporter-integration-tests": <<: *defaults @@ -468,6 +571,7 @@ jobs: path: /tmp/cypress - store_artifacts: path: /tmp/artifacts + - store-npm-logs "run-launcher": <<: *defaults @@ -515,9 +619,25 @@ jobs: - run: environment: DEBUG: electron-builder,electron-osx-sign* - command: npm run binary-build -- --platform $PLATFORM --version $NEXT_DEV_VERSION + # if this is a forked pull request, the NEXT_DEV_VERSION environment variable + # won't be set and we will use default version, since we are not going to + # upload the dev binary build anywhere + command: npm run binary-build -- --platform $PLATFORM --version ${NEXT_DEV_VERSION:-0.0.0-development} - run: npm run binary-zip -- --platform $PLATFORM + # Cypress binary file should be zipped to cypress.zip - run: ls -l *.zip + - store-npm-logs + - persist_to_workspace: + root: ~/ + paths: + - cypress/cypress.zip + + upload-binary: + <<: *defaults + steps: + - attach_workspace: + at: ~/ + - run: ls -l - run: name: upload unique binary command: | @@ -525,15 +645,37 @@ jobs: --file cypress.zip \ --version $NEXT_DEV_VERSION - run: cat binary-url.json - - run: mkdir /tmp/urls - - run: cp binary-url.json /tmp/urls - - run: cp cypress.zip /tmp/urls - - run: ls /tmp/urls + - store-npm-logs - persist_to_workspace: - root: /tmp/urls + root: ~/ paths: - - binary-url.json - - cypress.zip + - cypress/binary-url.json + + test-kitchensink: + <<: *defaults + steps: + - attach_workspace: + at: ~/ + - run: + name: Cloning test project + command: git clone https://github.com/cypress-io/cypress-example-kitchensink.git /tmp/repo + - run: + name: Install prod dependencies + command: npm install --production + working_directory: /tmp/repo + - run: + name: Example server + command: npm start + working_directory: /tmp/repo + background: true + - run: + name: Run Kitchensink example project + command: npm run cypress:run -- --project /tmp/repo + - store_artifacts: + path: /tmp/repo/cypress/screenshots + - store_artifacts: + path: /tmp/repo/cypress/videos + - store-npm-logs "test-kitchensink-against-staging": <<: *defaults @@ -560,6 +702,7 @@ jobs: CYPRESS_ENV=staging \ CYPRESS_video=false \ npm run cypress:run -- --project /tmp/repo --record + - store-npm-logs "test-against-staging": <<: *defaults @@ -576,15 +719,17 @@ jobs: CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \ CYPRESS_ENV=staging \ npm run cypress:run -- --project /tmp/repo --record + - store-npm-logs - "build-npm-package": + build-npm-package: <<: *defaults steps: - attach_workspace: at: ~/ + - run: npm run check-next-dev-version - run: name: bump NPM version - command: npm --no-git-tag-version version $NEXT_DEV_VERSION + command: npm --no-git-tag-version --allow-same-version version ${NEXT_DEV_VERSION:-0.0.0-development} - run: name: build NPM package working_directory: cli @@ -605,32 +750,50 @@ jobs: working_directory: cli/build command: ls -l # created file should have filename cypress-.tgz + - run: mkdir /tmp/urls + - run: cp cli/build/cypress-${NEXT_DEV_VERSION:-0.0.0-development}.tgz cypress.tgz + - run: cp cli/build/cypress-${NEXT_DEV_VERSION:-0.0.0-development}.tgz /tmp/urls/cypress.tgz + - run: ls -l /tmp/urls + - store-npm-logs + - run: pwd + - run: ls -l + - persist_to_workspace: + root: ~/ + paths: + - cypress/cypress.tgz + + upload-npm-package: + <<: *defaults + steps: + - attach_workspace: + at: ~/ + - run: ls -l + # NPM package file should have filename cypress-.tgz - run: name: upload NPM package command: | node scripts/binary.js upload-npm-package \ - --file cli/build/cypress-$NEXT_DEV_VERSION.tgz \ + --file cypress.tgz \ --version $NEXT_DEV_VERSION + - store-npm-logs + - run: ls -l - run: cat npm-package-url.json - - run: mkdir /tmp/urls - - run: cp cli/build/cypress-$NEXT_DEV_VERSION.tgz /tmp/urls/cypress.tgz - - run: cp npm-package-url.json /tmp/urls - - run: ls /tmp/urls - persist_to_workspace: - root: /tmp/urls + root: ~/ paths: - - npm-package-url.json - - cypress.tgz + - cypress/npm-package-url.json "test-binary-and-npm-against-other-projects": <<: *defaults steps: + # needs uploaded NPM and test binary - attach_workspace: at: ~/ - - attach_workspace: - at: /tmp/urls - - run: ls -la /tmp/urls - - run: cat /tmp/urls/*.json + - run: ls -la + # make sure JSON files with uploaded urls are present + - run: ls -la binary-url.json npm-package-url.json + - run: cat binary-url.json + - run: cat npm-package-url.json - run: mkdir /tmp/testing - run: name: create dummy package @@ -641,8 +804,8 @@ jobs: name: Install Cypress command: | node scripts/test-unique-npm-and-binary.js \ - --npm /tmp/urls/npm-package-url.json \ - --binary /tmp/urls/binary-url.json \ + --npm npm-package-url.json \ + --binary binary-url.json \ --cwd /tmp/testing - run: name: Verify Cypress binary @@ -652,29 +815,25 @@ jobs: name: Post pre-release install comment command: | node scripts/add-install-comment.js \ - --npm /tmp/urls/npm-package-url.json \ - --binary /tmp/urls/binary-url.json + --npm npm-package-url.json \ + --binary binary-url.json - run: name: Running other test projects with new NPM package and binary command: | node scripts/test-other-projects.js \ - --npm /tmp/urls/npm-package-url.json \ - --binary /tmp/urls/binary-url.json \ + --npm npm-package-url.json \ + --binary binary-url.json \ --provider circle + - store-npm-logs "test-npm-module-and-verify-binary": <<: *defaults steps: - attach_workspace: at: ~/ - - attach_workspace: - at: /tmp/urls # make sure we have cypress.zip received - - run: ls -l /tmp/urls/cypress.zip - # build NPM package - - run: - command: npm run build - working_directory: cli + - run: ls -l + - run: ls -l cypress.zip cypress.tgz - run: mkdir test-binary - run: name: Create new NPM package @@ -685,11 +844,12 @@ jobs: name: Install Cypress working_directory: test-binary # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=/tmp/urls/cypress.zip npm i /tmp/urls/cypress.tgz + command: CYPRESS_INSTALL_BINARY=/root/cypress/cypress.zip npm i /root/cypress/cypress.tgz - run: name: Verify Cypress binary working_directory: test-binary command: $(npm bin)/cypress verify + - store-npm-logs # install NPM + binary zip and run against staging API "test-binary-against-staging": @@ -697,12 +857,9 @@ jobs: steps: - attach_workspace: at: ~/ - - attach_workspace: - at: /tmp/urls - # make sure we have the binary - - run: ls -l /tmp/urls/cypress.zip - # make sure we have the NPM package - - run: ls -l /tmp/urls/cypress.tgz + - run: ls -l + # make sure we have the binary and NPM package + - run: ls -l cypress.zip cypress.tgz - run: name: Cloning test project command: git clone https://github.com/cypress-io/cypress-test-tiny.git /tmp/cypress-test-tiny @@ -710,7 +867,7 @@ jobs: name: Install Cypress working_directory: /tmp/cypress-test-tiny # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=/tmp/urls/cypress.zip npm i /tmp/urls/cypress.tgz + command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz - run: name: Run test project working_directory: /tmp/cypress-test-tiny @@ -719,14 +876,16 @@ jobs: CYPRESS_RECORD_KEY=$TEST_TINY_RECORD_KEY \ CYPRESS_ENV=staging \ $(npm bin)/cypress run --record + - store-npm-logs "test-binary-against-kitchensink": <<: *defaults steps: - attach_workspace: at: ~/ - - attach_workspace: - at: /tmp/urls + # make sure the binary and NPM package files are present + - run: ls -l + - run: ls -l cypress.zip cypress.tgz - run: name: Cloning kitchensink project command: git clone --depth 1 https://github.com/cypress-io/cypress-example-kitchensink.git /tmp/kitchensink @@ -737,7 +896,7 @@ jobs: name: Install Cypress working_directory: /tmp/kitchensink # force installing the freshly built binary - command: CYPRESS_INSTALL_BINARY=/tmp/urls/cypress.zip npm i /tmp/urls/cypress.tgz + command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz - run: working_directory: /tmp/kitchensink command: npm run build @@ -748,6 +907,47 @@ jobs: - run: working_directory: /tmp/kitchensink command: npm run e2e + - store-npm-logs + + test-binary-as-specific-user: + <<: *defaults + steps: + - attach_workspace: + at: ~/ + # the user should be "node" + - run: whoami + - run: pwd + # prints the current user's effective user id + # for root it is 0 + # for other users it is a positive integer + - run: node -e 'console.log(process.geteuid())' + # make sure the binary and NPM package files are present + - run: ls -l + - run: ls -l cypress.zip cypress.tgz + - run: mkdir test-binary + - run: + name: Create new NPM package + working_directory: test-binary + command: npm init -y + - run: + # install NPM from built NPM package folder + name: Install Cypress + working_directory: test-binary + # force installing the freshly built binary + command: CYPRESS_INSTALL_BINARY=~/cypress/cypress.zip npm i ~/cypress/cypress.tgz + - run: + name: Add Cypress demo + working_directory: test-binary + command: npx @bahmutov/cly init + - run: + name: Verify Cypress binary + working_directory: test-binary + command: DEBUG=cypress:cli $(npm bin)/cypress verify + - run: + name: Run Cypress binary + working_directory: test-binary + command: DEBUG=cypress:cli $(npm bin)/cypress run + - store-npm-logs linux-workflow: &linux-workflow jobs: @@ -772,33 +972,61 @@ linux-workflow: &linux-workflow - server-performance-tests: requires: - build - - server-e2e-tests-1: + - server-e2e-tests-chrome-1: requires: - build - - server-e2e-tests-2: + - server-e2e-tests-chrome-2: requires: - build - - server-e2e-tests-3: + - server-e2e-tests-chrome-3: requires: - build - - server-e2e-tests-4: + - server-e2e-tests-chrome-4: requires: - build - - server-e2e-tests-5: + - server-e2e-tests-chrome-5: requires: - build - - server-e2e-tests-6: + - server-e2e-tests-chrome-6: requires: - build - - server-e2e-tests-7: + - server-e2e-tests-chrome-7: requires: - build - - server-e2e-tests-8: + - server-e2e-tests-chrome-8: requires: - build - - driver-integration-tests-3x: + - server-e2e-tests-electron-1: requires: - build + - server-e2e-tests-electron-2: + requires: + - build + - server-e2e-tests-electron-3: + requires: + - build + - server-e2e-tests-electron-4: + requires: + - build + - server-e2e-tests-electron-5: + requires: + - build + - server-e2e-tests-electron-6: + requires: + - build + - server-e2e-tests-electron-7: + requires: + - build + - server-e2e-tests-electron-8: + requires: + - build + - driver-integration-tests-chrome: + requires: + - build + ## TODO: add these back in when flaky tests are fixed + # - driver-integration-tests-electron: + # requires: + # - build - desktop-gui-integration-tests-2x: requires: - build @@ -811,13 +1039,18 @@ linux-workflow: &linux-workflow # various testing scenarios, like building full binary # and testing it on a real project - test-against-staging: + context: test-runner:record-tests filters: branches: only: - develop requires: - build + - test-kitchensink: + requires: + - build - test-kitchensink-against-staging: + context: test-runner:record-tests filters: branches: only: @@ -825,27 +1058,36 @@ linux-workflow: &linux-workflow requires: - build - build-npm-package: + requires: + - build + - upload-npm-package: + context: test-runner:upload filters: branches: only: - develop requires: - - build + - build-npm-package - build-binary: + requires: + - build + - upload-binary: + context: test-runner:upload filters: branches: only: - develop requires: - - build + - build-binary - test-binary-and-npm-against-other-projects: + context: test-runner:trigger-test-jobs filters: branches: only: - develop requires: - - build-npm-package - - build-binary + - upload-npm-package + - upload-binary - test-npm-module-and-verify-binary: filters: branches: @@ -855,6 +1097,7 @@ linux-workflow: &linux-workflow - build-npm-package - build-binary - test-binary-against-staging: + context: test-runner:record-tests filters: branches: only: @@ -870,15 +1113,29 @@ linux-workflow: &linux-workflow requires: - build-npm-package - build-binary + - test-binary-as-specific-user: + name: "test binary as a non-root user" + executor: non-root-docker-user + requires: + - build-npm-package + - build-binary + - test-binary-as-specific-user: + name: "test binary as a root user" + requires: + - build-npm-package + - build-binary mac-workflow: &mac-workflow jobs: - build: name: Mac build executor: mac + <<: *macBuildFilters + - lint: name: Mac lint executor: mac + <<: *macBuildFilters requires: - Mac build @@ -887,24 +1144,47 @@ mac-workflow: &mac-workflow - build-npm-package: name: Mac NPM package executor: mac + requires: + - Mac build + filters: + branches: + only: + - develop + + - upload-npm-package: + name: Mac NPM package upload + context: test-runner:upload + executor: mac filters: branches: only: - develop - - test-example-repos-on-mac-4526 requires: - - Mac build + - Mac NPM package - build-binary: name: Mac binary - executor: mac context: org-global + executor: mac + <<: *macBuildFilters + requires: + - Mac build + + - upload-binary: + name: Mac binary upload + executor: mac + context: test-runner:upload filters: branches: only: - develop - - binary-metadata - - test-example-repos-on-mac-4526 + requires: + - Mac binary + + - test-kitchensink: + name: Test Mac Kitchensink + executor: mac + <<: *macBuildFilters requires: - Mac build @@ -920,6 +1200,7 @@ mac-workflow: &mac-workflow - Mac binary - test-binary-against-staging: + context: test-runner:record-tests name: Test Mac binary against staging executor: mac filters: @@ -931,16 +1212,16 @@ mac-workflow: &mac-workflow - Mac binary - test-binary-and-npm-against-other-projects: + context: test-runner:trigger-test-jobs name: Test Mac binary against other projects executor: mac filters: branches: only: - develop - - test-example-repos-on-mac-4526 requires: - - Mac NPM package - - Mac binary + - Mac NPM package upload + - Mac binary upload workflows: linux: diff --git a/cli/README.md b/cli/README.md index d1c8809bf535..36b756ec8069 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,6 +1,6 @@ # CLI -The CLI is used to build Cypress npm module to be run within a terminal. +The CLI is used to build the [cypress npm module](https://www.npmjs.com/package/cypress) to be run within a terminal. **The CLI has the following responsibilities:** @@ -8,9 +8,10 @@ The CLI is used to build Cypress npm module to be run within a terminal. - Allow users to install the Cypress executable - Allow users to print their current Cypress version - Allow users to run Cypress tests from the terminal -- Allow users to open Cypress in the interactive GUI. -- Allow users to verifies that Cypress is installed correctly and executable +- Allow users to open Cypress in the interactive Test Runner. +- Allow users to verify that Cypress is installed correctly and executable - Allow users to manages the Cypress binary cache +- Allow users to pass in options that change way tests are ran or recorded (browsers used, specfiles ran, grouping, parallelization) ## Installing @@ -23,7 +24,7 @@ npm install ## Building -See `scripts/build.js`. Note that the built NPM package will include [NPM_README.md](NPM_README.md) as its public README file. +See `scripts/build.js`. Note that the built npm package will include [NPM_README.md](NPM_README.md) as its public README file. ## Testing diff --git a/cli/__snapshots__/cli_spec.js b/cli/__snapshots__/cli_spec.js index d471f77b5da0..cd42cfdadf4c 100644 --- a/cli/__snapshots__/cli_spec.js +++ b/cli/__snapshots__/cli_spec.js @@ -1,26 +1,180 @@ -exports['cli --version no binary version 1'] = ` -Cypress package version: 1.2.3 -Cypress binary version: not installed +exports['shows help for open --foo 1'] = ` + + command: bin/cypress open --foo + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + error: unknown option: --foo + + + Usage: open [options] + + Opens Cypress in the interactive GUI. + + Options: + + -b, --browser path to a custom browser to be added to the list of available browsers in Cypress + -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. + -C, --config-file path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable. + -d, --detached [bool] runs Cypress application in detached mode + -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json + --global force Cypress into global mode as if its globally installed + -p, --port runs Cypress on a specific port. overrides any value in cypress.json. + -P, --project path to the project + --dev runs cypress in development and bypasses binary check + -h, --help output usage information + ------- + stderr: + ------- + + ------- + ` -exports['cli -v no binary version 1'] = ` -Cypress package version: 1.2.3 -Cypress binary version: not installed +exports['shows help for run --foo 1'] = ` + + command: bin/cypress run --foo + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + error: unknown option: --foo + + + Usage: run [options] + + Runs Cypress tests from the CLI without the GUI + + Options: + + -b, --browser runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path. + --ci-build-id the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers + -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. + -C, --config-file path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable. + -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json + --group a named group for recorded runs in the Cypress Dashboard + -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. + --headed displays the browser instead of running headlessly (defaults to true for Chrome-family browsers) + --headless hide the browser instead of running headed (defaults to true for Electron) + --no-exit keep the browser open after tests finish + --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes + -p, --port runs Cypress on a specific port. overrides any value in cypress.json. + -P, --project path to the project + --record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard. + -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" + -o, --reporter-options options for the mocha reporter. defaults to "null" + -s, --spec runs specific spec file(s). defaults to "all" + -t, --tag named tag(s) for recorded runs in the Cypress Dashboard + --dev runs cypress in development and bypasses binary check + -h, --help output usage information + ------- + stderr: + ------- + + ------- + ` -exports['cli cypress run warns with space-separated --specs 1'] = ` -⚠ Warning: It looks like you're passing --spec a space-separated list of files: +exports['cli unknown option shows help for cache command - unknown option --foo 1'] = ` -"a b c d e f g" + command: bin/cypress cache --foo + code: 1 + failed: true + killed: false + signal: null + timedOut: false -This will work, but it's not recommended. + stdout: + ------- + error: unknown option: --foo -The most common cause of this warning is using an unescaped glob pattern. If you are -trying to pass a glob pattern, escape it using quotes: - cypress run --spec "**/*.spec.js" -If you are trying to pass multiple spec filenames, separate them by commas instead: - cypress run --spec spec1,spec2,spec3 + Usage: cache [command] + + Manages the Cypress binary cache + + Options: + + list list cached binary versions + path print the path to the binary cache + clear delete all cached binaries + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` + +exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = ` + + command: bin/cypress cache foo + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + error: unknown command: cache foo + + + Usage: cache [command] + + Manages the Cypress binary cache + + Options: + + list list cached binary versions + path print the path to the binary cache + clear delete all cached binaries + -h, --help output usage information + ------- + stderr: + ------- + + ------- + +` + +exports['cli unknown option shows help for cache command - no sub-command 1'] = ` + + command: bin/cypress cache + code: 1 + failed: true + killed: false + signal: null + timedOut: false + + stdout: + ------- + Usage: cache [command] + + Manages the Cypress binary cache + + Options: + + list list cached binary versions + path print the path to the binary cache + clear delete all cached binaries + -h, --help output usage information + ------- + stderr: + ------- + + ------- + ` exports['cli help command shows help 1'] = ` @@ -58,9 +212,9 @@ exports['cli help command shows help 1'] = ` ` -exports['cli help command shows help for --help 1'] = ` +exports['cli help command shows help for -h 1'] = ` - command: bin/cypress --help + command: bin/cypress -h code: 0 failed: false killed: false @@ -93,9 +247,9 @@ exports['cli help command shows help for --help 1'] = ` ` -exports['cli help command shows help for -h 1'] = ` +exports['cli help command shows help for --help 1'] = ` - command: bin/cypress -h + command: bin/cypress --help code: 0 failed: false killed: false @@ -165,97 +319,33 @@ exports['cli unknown command shows usage and exits 1'] = ` ` -exports['cli unknown option shows help for cache command - no sub-command 1'] = ` - - command: bin/cypress cache - code: 1 - failed: true - killed: false - signal: null - timedOut: false - - stdout: - ------- - Usage: cache [command] - - Manages the Cypress binary cache - - Options: - - list list cached binary versions - path print the path to the binary cache - clear delete all cached binaries - -h, --help output usage information - ------- +exports['cli CYPRESS_ENV allows staging environment 1'] = ` + code: 0 stderr: ------- ------- - -` - -exports['cli unknown option shows help for cache command - unknown option --foo 1'] = ` - command: bin/cypress cache --foo - code: 1 - failed: true - killed: false - signal: null - timedOut: false - - stdout: - ------- - error: unknown option: --foo - - - Usage: cache [command] - - Manages the Cypress binary cache - - Options: - - list list cached binary versions - path print the path to the binary cache - clear delete all cached binaries - -h, --help output usage information - ------- - stderr: - ------- - - ------- - ` -exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = ` - - command: bin/cypress cache foo - code: 1 - failed: true - killed: false - signal: null - timedOut: false - - stdout: +exports['cli CYPRESS_ENV catches environment "foo" 1'] = ` + code: 11 + stderr: ------- - error: unknown command: cache foo + The environment variable with the reserved name "CYPRESS_ENV" is set. + Unset the "CYPRESS_ENV" environment variable and run Cypress again. - Usage: cache [command] + ---------- - Manages the Cypress binary cache + CYPRESS_ENV=foo - Options: + ---------- - list list cached binary versions - path print the path to the binary cache - clear delete all cached binaries - -h, --help output usage information + Platform: xxx + Cypress Version: 1.2.3 ------- - stderr: - ------- - - ------- - + ` exports['cli version and binary version 1'] = ` @@ -273,84 +363,38 @@ Cypress package version: 1.2.3 Cypress binary version: not installed ` -exports['shows help for open --foo 1'] = ` - - command: bin/cypress open --foo - code: 1 - failed: true - killed: false - signal: null - timedOut: false - - stdout: - ------- - error: unknown option: --foo +exports['cli --version no binary version 1'] = ` +Cypress package version: 1.2.3 +Cypress binary version: not installed +` +exports['cli -v no binary version 1'] = ` +Cypress package version: 1.2.3 +Cypress binary version: not installed +` - Usage: open [options] +exports['cli cypress run warns with space-separated --spec 1'] = ` +⚠ Warning: It looks like you're passing --spec a space-separated list of arguments: - Opens Cypress in the interactive GUI. +"a b c d e f g" - Options: +This will work, but it's not recommended. - -p, --port runs Cypress on a specific port. overrides any value in cypress.json. - -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json - -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. - -d, --detached [bool] runs Cypress application in detached mode - -b, --browser path to a custom browser to be added to the list of available browsers in Cypress - -P, --project path to the project - --global force Cypress into global mode as if its globally installed - --dev runs cypress in development and bypasses binary check - -h, --help output usage information - ------- - stderr: - ------- - - ------- +If you are trying to pass multiple arguments, separate them with commas instead: + cypress run --spec arg1,arg2,arg3 +The most common cause of this warning is using an unescaped glob pattern. If you are +trying to pass a glob pattern, escape it using quotes: + cypress run --spec "**/*.spec.js" ` -exports['shows help for run --foo 1'] = ` - - command: bin/cypress run --foo - code: 1 - failed: true - killed: false - signal: null - timedOut: false - - stdout: - ------- - error: unknown option: --foo - - - Usage: run [options] +exports['cli cypress run warns with space-separated --tag 1'] = ` +⚠ Warning: It looks like you're passing --tag a space-separated list of arguments: - Runs Cypress tests from the CLI without the GUI +"a b c d e f g" - Options: +This will work, but it's not recommended. - --record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard. - --headed displays the Electron browser instead of running headlessly - -k, --key your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable. - -s, --spec runs a specific spec file. defaults to "all" - -r, --reporter runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec" - -o, --reporter-options options for the mocha reporter. defaults to "null" - -p, --port runs Cypress on a specific port. overrides any value in cypress.json. - -e, --env sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json - -c, --config sets configuration values. separate multiple values with a comma. overrides any value in cypress.json. - -b, --browser runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path. - -P, --project path to the project - --parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes - --group a named group for recorded runs in the Cypress dashboard - --ci-build-id the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers - --no-exit keep the browser open after tests finish - --dev runs cypress in development and bypasses binary check - -h, --help output usage information - ------- - stderr: - ------- - - ------- - +If you are trying to pass multiple arguments, separate them with commas instead: + cypress run --tag arg1,arg2,arg3 ` diff --git a/cli/__snapshots__/errors_spec.js b/cli/__snapshots__/errors_spec.js index f8333d37acba..e04b953cee2e 100644 --- a/cli/__snapshots__/errors_spec.js +++ b/cli/__snapshots__/errors_spec.js @@ -29,9 +29,12 @@ Cypress Version: 1.2.3 exports['errors individual has the following errors 1'] = [ "CYPRESS_RUN_BINARY", "binaryNotExecutable", + "childProcessKilled", "failedDownload", "failedUnzip", + "incompatibleHeadlessFlags", "invalidCacheDirectory", + "invalidCypressEnv", "invalidSmokeTestDisplayError", "missingApp", "missingDependency", @@ -70,3 +73,27 @@ If you are using Docker, we provide containers with all required dependencies in Platform: test platform (Foo-OsVersion) Cypress Version: 1.2.3 ` + +exports['child kill error object'] = { + "description": "The Test Runner unexpectedly exited via a exit event with signal SIGKILL", + "solution": "Please search Cypress documentation for possible solutions:\n\n https://on.cypress.io\n\nCheck if there is a GitHub issue describing this crash:\n\n https://github.com/cypress-io/cypress/issues\n\nConsider opening a new issue." +} + +exports['Error message'] = ` +The Test Runner unexpectedly exited via a exit event with signal SIGKILL + +Please search Cypress documentation for possible solutions: + +https://on.cypress.io + +Check if there is a GitHub issue describing this crash: + +https://github.com/cypress-io/cypress/issues + +Consider opening a new issue. + +---------- + +Platform: test platform (Foo-OsVersion) +Cypress Version: 1.2.3 +` diff --git a/cli/__snapshots__/run_spec.js b/cli/__snapshots__/run_spec.js index b83f3addbe46..8b4021564a65 100644 --- a/cli/__snapshots__/run_spec.js +++ b/cli/__snapshots__/run_spec.js @@ -1,10 +1,10 @@ exports['exec run .processRunOptions does not remove --record option when using --browser 1'] = [ "--run-project", null, - "--record", - "foo", "--browser", - "test browser" + "test browser", + "--record", + "foo" ] exports['exec run .processRunOptions passes --browser option 1'] = [ @@ -20,3 +20,10 @@ exports['exec run .processRunOptions passes --record option 1'] = [ "--record", "my record id" ] + +exports['exec run .processRunOptions passes --config-file false option 1'] = [ + "--run-project", + null, + "--config-file", + false +] diff --git a/cli/__snapshots__/spawn_spec.js b/cli/__snapshots__/spawn_spec.js new file mode 100644 index 000000000000..df92946387cd --- /dev/null +++ b/cli/__snapshots__/spawn_spec.js @@ -0,0 +1,18 @@ +exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = ` +The Test Runner unexpectedly exited via a exit event with signal SIGKILL + +Please search Cypress documentation for possible solutions: + +https://on.cypress.io + +Check if there is a GitHub issue describing this crash: + +https://github.com/cypress-io/cypress/issues + +Consider opening a new issue. + +---------- + +Platform: darwin (Foo-OsVersion) +Cypress Version: 0.0.0 +` diff --git a/cli/__snapshots__/verify_spec.js b/cli/__snapshots__/verify_spec.js index 9b7e43f9257a..32089782d5b3 100644 --- a/cli/__snapshots__/verify_spec.js +++ b/cli/__snapshots__/verify_spec.js @@ -159,7 +159,7 @@ Error: Cypress verification timed out. This command failed with the following output: -/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 +/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222 ---------- @@ -181,7 +181,7 @@ Error: Cypress verification failed. This command failed with the following output: -/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 +/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222 ---------- @@ -203,7 +203,7 @@ Error: Cypress verification failed. This command failed with the following output: -/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --smoke-test --ping=222 +/cache/Cypress/1.2.3/Cypress.app/Contents/MacOS/Cypress --no-sandbox --smoke-test --ping=222 ---------- diff --git a/cli/index.js b/cli/index.js index 7da84f9ec87b..250d3d4b52b1 100644 --- a/cli/index.js +++ b/cli/index.js @@ -23,6 +23,6 @@ switch (args.exec) { break default: - // export our node module interface + debug('exporting Cypress module interface') module.exports = require('./lib/cypress') } diff --git a/cli/lib/cli.js b/cli/lib/cli.js index dc6a6e82e7c8..69b9b958f539 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -2,9 +2,10 @@ const _ = require('lodash') const commander = require('commander') const { stripIndent } = require('common-tags') const logSymbols = require('log-symbols') -const debug = require('debug')('cypress:cli') +const debug = require('debug')('cypress:cli:cli') const util = require('./util') const logger = require('./logger') +const errors = require('./errors') const cache = require('./tasks/cache') // patch "commander" method called when a user passed an unknown option @@ -24,95 +25,111 @@ const coerceFalse = (arg) => { return arg !== 'false' } -const spaceDelimitedSpecsMsg = (files) => { - logger.log() - logger.warn(stripIndent` - ${logSymbols.warning} Warning: It looks like you're passing --spec a space-separated list of files: +const spaceDelimitedArgsMsg = (flag, args) => { + let msg = ` + ${logSymbols.warning} Warning: It looks like you're passing --${flag} a space-separated list of arguments: - "${files.join(' ')}" + "${args.join(' ')}" This will work, but it's not recommended. + If you are trying to pass multiple arguments, separate them with commas instead: + cypress run --${flag} arg1,arg2,arg3 + ` + + if (flag === 'spec') { + msg += ` The most common cause of this warning is using an unescaped glob pattern. If you are trying to pass a glob pattern, escape it using quotes: cypress run --spec "**/*.spec.js" + ` + } - If you are trying to pass multiple spec filenames, separate them by commas instead: - cypress run --spec spec1,spec2,spec3 - `) - + logger.log() + logger.warn(stripIndent(msg)) logger.log() } const parseVariableOpts = (fnArgs, args) => { const opts = fnArgs.pop() - if (fnArgs.length && opts.spec) { - // this will capture space-delimited specs after --spec spec1 but before the next option - - const argIndex = _.indexOf(args, '--spec') + 2 - const nextOptOffset = _.findIndex(_.slice(args, argIndex), (arg) => { - return _.startsWith(arg, '--') + if (fnArgs.length && (opts.spec || opts.tag)) { + // this will capture space-delimited args after + // flags that could have possible multiple args + // but before the next option + // --spec spec1 spec2 or --tag foo bar + + const multiArgFlags = _.compact([ + opts.spec ? 'spec' : opts.spec, + opts.tag ? 'tag' : opts.tag, + ]) + + _.forEach(multiArgFlags, (flag) => { + const argIndex = _.indexOf(args, `--${flag}`) + 2 + const nextOptOffset = _.findIndex(_.slice(args, argIndex), (arg) => { + return _.startsWith(arg, '--') + }) + const endIndex = nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length + + const maybeArgs = _.slice(args, argIndex, endIndex) + const extraArgs = _.intersection(maybeArgs, fnArgs) + + if (extraArgs.length) { + opts[flag] = [opts[flag]].concat(extraArgs) + spaceDelimitedArgsMsg(flag, opts[flag]) + opts[flag] = opts[flag].join(',') + } }) - const endIndex = nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length - - const maybeSpecs = _.slice(args, argIndex, endIndex) - const extraSpecs = _.intersection(maybeSpecs, fnArgs) - - if (extraSpecs.length) { - opts.spec = [opts.spec].concat(extraSpecs) - spaceDelimitedSpecsMsg(opts.spec) - opts.spec = opts.spec.join(',') - } } - return parseOpts(opts) -} + debug('variable-length opts parsed %o', { args, opts }) -const parseOpts = (opts) => { - opts = _.pick(opts, - 'project', 'spec', 'reporter', 'reporterOptions', 'path', 'destination', - 'port', 'env', 'cypressVersion', 'config', 'record', 'key', - 'browser', 'detached', 'headed', 'global', 'dev', 'force', 'exit', - 'cachePath', 'cacheList', 'cacheClear', 'parallel', 'group', 'ciBuildId') - - if (opts.exit) { - opts = _.omit(opts, 'exit') - } - - debug('parsed cli options', opts) - - return opts + return util.parseOpts(opts) } const descriptions = { - record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.', - key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.', - spec: 'runs a specific spec file. defaults to "all"', - reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"', - reporterOptions: 'options for the mocha reporter. defaults to "null"', - port: 'runs Cypress on a specific port. overrides any value in cypress.json.', - env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json', - config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.', - browserRunMode: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.', browserOpenMode: 'path to a custom browser to be added to the list of available browsers in Cypress', + browserRunMode: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.', + cacheClear: 'delete all cached binaries', + cacheList: 'list cached binary versions', + cachePath: 'print the path to the binary cache', + ciBuildId: 'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers', + config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.', + configFile: 'path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.', detached: 'runs Cypress application in detached mode', - project: 'path to the project', - global: 'force Cypress into global mode as if its globally installed', - version: 'prints Cypress version', - headed: 'displays the Electron browser instead of running headlessly', dev: 'runs cypress in development and bypasses binary check', - forceInstall: 'force install the Cypress binary', + env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json', exit: 'keep the browser open after tests finish', - cachePath: 'print the path to the binary cache', - cacheList: 'list cached binary versions', - cacheClear: 'delete all cached binaries', - group: 'a named group for recorded runs in the Cypress dashboard', + forceInstall: 'force install the Cypress binary', + global: 'force Cypress into global mode as if its globally installed', + group: 'a named group for recorded runs in the Cypress Dashboard', + headed: 'displays the browser instead of running headlessly (defaults to true for Chrome-family browsers)', + headless: 'hide the browser instead of running headed (defaults to true for Electron)', + key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.', parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes', - ciBuildId: 'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers', + port: 'runs Cypress on a specific port. overrides any value in cypress.json.', + project: 'path to the project', + record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.', + reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"', + reporterOptions: 'options for the mocha reporter. defaults to "null"', + spec: 'runs specific spec file(s). defaults to "all"', + tag: 'named tag(s) for recorded runs in the Cypress Dashboard', + version: 'prints Cypress version', } -const knownCommands = ['version', 'run', 'open', 'install', 'verify', '-v', '--version', 'help', '-h', '--help', 'cache'] +const knownCommands = [ + 'cache', + 'help', + '-h', + '--help', + 'install', + 'open', + 'run', + 'verify', + '-v', + '--version', + 'version', +] const text = (description) => { if (!descriptions[description]) { @@ -123,9 +140,11 @@ const text = (description) => { } function includesVersion (args) { - return _.includes(args, 'version') || + return ( + _.includes(args, 'version') || _.includes(args, '--version') || _.includes(args, '-v') + ) } function showVersions () { @@ -147,9 +166,17 @@ module.exports = { args = process.argv } + if (!util.isValidCypressEnvValue(process.env.CYPRESS_ENV)) { + debug('invalid CYPRESS_ENV value', process.env.CYPRESS_ENV) + + return errors.exitWithError(errors.errors.invalidCypressEnv)( + `CYPRESS_ENV=${process.env.CYPRESS_ENV}` + ) + } + const program = new commander.Command() - // bug in commaner not printing name + // bug in commander not printing name // in usage help docs program._name = 'cypress' @@ -172,24 +199,27 @@ module.exports = { .command('run') .usage('[options]') .description('Runs Cypress tests from the CLI without the GUI') - .option('--record [bool]', text('record'), coerceFalse) - .option('--headed', text('headed')) - .option('-k, --key ', text('key')) - .option('-s, --spec ', text('spec')) - .option('-r, --reporter ', text('reporter')) - .option('-o, --reporter-options ', text('reporterOptions')) - .option('-p, --port ', text('port')) - .option('-e, --env ', text('env')) - .option('-c, --config ', text('config')) .option('-b, --browser ', text('browserRunMode')) - .option('-P, --project ', text('project')) - .option('--parallel', text('parallel')) - .option('--group ', text('group')) .option('--ci-build-id ', text('ciBuildId')) + .option('-c, --config ', text('config')) + .option('-C, --config-file ', text('configFile')) + .option('-e, --env ', text('env')) + .option('--group ', text('group')) + .option('-k, --key ', text('key')) + .option('--headed', text('headed')) + .option('--headless', text('headless')) .option('--no-exit', text('exit')) + .option('--parallel', text('parallel')) + .option('-p, --port ', text('port')) + .option('-P, --project ', text('project')) + .option('--record [bool]', text('record'), coerceFalse) + .option('-r, --reporter ', text('reporter')) + .option('-o, --reporter-options ', text('reporterOptions')) + .option('-s, --spec ', text('spec')) + .option('-t, --tag ', text('tag')) .option('--dev', text('dev'), coerceFalse) .action((...fnArgs) => { - debug('running Cypress') + debug('running Cypress with args %o', fnArgs) require('./exec/run') .start(parseVariableOpts(fnArgs, args)) .then(util.exit) @@ -200,40 +230,45 @@ module.exports = { .command('open') .usage('[options]') .description('Opens Cypress in the interactive GUI.') - .option('-p, --port ', text('port')) - .option('-e, --env ', text('env')) + .option('-b, --browser ', text('browserOpenMode')) .option('-c, --config ', text('config')) + .option('-C, --config-file ', text('configFile')) .option('-d, --detached [bool]', text('detached'), coerceFalse) - .option('-b, --browser ', text('browserOpenMode')) - .option('-P, --project ', text('project')) + .option('-e, --env ', text('env')) .option('--global', text('global')) + .option('-p, --port ', text('port')) + .option('-P, --project ', text('project')) .option('--dev', text('dev'), coerceFalse) .action((opts) => { debug('opening Cypress') require('./exec/open') - .start(parseOpts(opts)) + .start(util.parseOpts(opts)) .catch(util.logErrorExit1) }) program .command('install') .usage('[options]') - .description('Installs the Cypress executable matching this package\'s version') + .description( + 'Installs the Cypress executable matching this package\'s version' + ) .option('-f, --force', text('forceInstall')) .action((opts) => { require('./tasks/install') - .start(parseOpts(opts)) + .start(util.parseOpts(opts)) .catch(util.logErrorExit1) }) program .command('verify') .usage('[options]') - .description('Verifies that Cypress is installed correctly and executable') + .description( + 'Verifies that Cypress is installed correctly and executable' + ) .option('--dev', text('dev'), coerceFalse) .action((opts) => { const defaultOpts = { force: true, welcomeMessage: false } - const parsedOpts = parseOpts(opts) + const parsedOpts = util.parseOpts(opts) const options = _.extend(parsedOpts, defaultOpts) require('./tasks/verify') diff --git a/cli/lib/errors.js b/cli/lib/errors.js index 1a83b96ce434..2c394c0d076b 100644 --- a/cli/lib/errors.js +++ b/cli/lib/errors.js @@ -36,7 +36,9 @@ const failedUnzip = { const missingApp = (binaryDir) => { return { - description: `No version of Cypress is installed in: ${chalk.cyan(binaryDir)}`, + description: `No version of Cypress is installed in: ${chalk.cyan( + binaryDir + )}`, solution: stripIndent` \nPlease reinstall Cypress by running: ${chalk.cyan('cypress install')} `, @@ -59,7 +61,8 @@ const binaryNotExecutable = (executable) => { const notInstalledCI = (executable) => { return { - description: 'The cypress npm package is installed, but the Cypress binary is missing.', + description: + 'The cypress npm package is installed, but the Cypress binary is missing.', solution: stripIndent`\n We expected the binary to be installed here: ${chalk.cyan(executable)} @@ -114,7 +117,7 @@ const smokeTestFailure = (smokeTestCommand, timedOut) => { const invalidSmokeTestDisplayError = { code: 'INVALID_SMOKE_TEST_DISPLAY_ERROR', description: 'Cypress verification failed.', - solution (msg) { + solution (msg) { return stripIndent` Cypress failed to start after spawning a new Xvfb server. @@ -152,7 +155,8 @@ const missingDependency = { } const invalidCacheDirectory = { - description: 'Cypress cannot write to the cache directory due to file permissions', + description: + 'Cypress cannot write to the cache directory due to file permissions', solution: stripIndent` See discussion and possible solutions at ${chalk.blue(util.getGitHubIssueUrl(1281))} @@ -164,25 +168,55 @@ const versionMismatch = { solution: 'Install Cypress and verify app again', } -const unexpected = { - description: 'An unexpected error occurred while verifying the Cypress executable.', - solution: stripIndent` - Please search Cypress documentation for possible solutions: +const incompatibleHeadlessFlags = { + description: '`--headed` and `--headless` cannot both be passed.', + solution: 'Either pass `--headed` or `--headless`, but not both.', +} - ${chalk.blue(docsUrl)} +const solutionUnknown = stripIndent` + Please search Cypress documentation for possible solutions: - Check if there is a GitHub issue describing this crash: + ${chalk.blue(docsUrl)} - ${chalk.blue(util.issuesUrl)} + Check if there is a GitHub issue describing this crash: - Consider opening a new issue. - `, + ${chalk.blue(util.issuesUrl)} + + Consider opening a new issue. +` +const unexpected = { + description: + 'An unexpected error occurred while verifying the Cypress executable.', + solution: solutionUnknown, +} + +const invalidCypressEnv = { + description: + chalk.red('The environment variable with the reserved name "CYPRESS_ENV" is set.'), + solution: chalk.red('Unset the "CYPRESS_ENV" environment variable and run Cypress again.'), + exitCode: 11, +} + +/** + * This error happens when CLI detects that the child Test Runner process + * was killed with a signal, like SIGBUS + * @see https://github.com/cypress-io/cypress/issues/5808 + * @param {'close'|'event'} eventName Child close event name + * @param {string} signal Signal that closed the child process, like "SIGBUS" +*/ +const childProcessKilled = (eventName, signal) => { + return { + description: `The Test Runner unexpectedly exited via a ${chalk.cyan(eventName)} event with signal ${chalk.cyan(signal)}`, + solution: solutionUnknown, + } } const removed = { CYPRESS_BINARY_VERSION: { description: stripIndent` - The environment variable CYPRESS_BINARY_VERSION has been renamed to CYPRESS_INSTALL_BINARY as of version ${chalk.green('3.0.0')} + The environment variable CYPRESS_BINARY_VERSION has been renamed to CYPRESS_INSTALL_BINARY as of version ${chalk.green( + '3.0.0' + )} `, solution: stripIndent` You should set CYPRESS_INSTALL_BINARY instead. @@ -190,7 +224,9 @@ const removed = { }, CYPRESS_SKIP_BINARY_INSTALL: { description: stripIndent` - The environment variable CYPRESS_SKIP_BINARY_INSTALL has been removed as of version ${chalk.green('3.0.0')} + The environment variable CYPRESS_SKIP_BINARY_INSTALL has been removed as of version ${chalk.green( + '3.0.0' + )} `, solution: stripIndent` To skip the binary install, set CYPRESS_INSTALL_BINARY=0 @@ -210,8 +246,7 @@ const CYPRESS_RUN_BINARY = { } function getPlatformInfo () { - return util.getOsVersionAsync() - .then((version) => { + return util.getOsVersionAsync().then((version) => { return stripIndent` Platform: ${os.platform()} (${version}) Cypress Version: ${util.pkgVersion()} @@ -220,29 +255,50 @@ function getPlatformInfo () { } function addPlatformInformation (info) { - return getPlatformInfo() - .then((platform) => { + return getPlatformInfo().then((platform) => { return merge(info, { platform }) }) } +/** + * Given an error object (see the errors above), forms error message text with details, + * then resolves with Error instance you can throw or reject with. + * @param {object} errorObject + * @returns {Promise} resolves with an Error + * @example + ```js + // inside a Promise with "resolve" and "reject" + const errorObject = childProcessKilled('exit', 'SIGKILL') + return getError(errorObject).then(reject) + ``` + */ +function getError (errorObject) { + return formErrorText(errorObject).then((errorMessage) => { + const err = new Error(errorMessage) + + err.known = true + + return err + }) +} + /** * Forms nice error message with error and platform information, * and if possible a way to solve it. Resolves with a string. */ function formErrorText (info, msg, prevMessage) { - return addPlatformInformation(info) - .then((obj) => { + return addPlatformInformation(info).then((obj) => { const formatted = [] function add (msg) { - formatted.push( - stripIndents(msg) - ) + formatted.push(stripIndents(msg)) } - la(is.unemptyString(obj.description), - 'expected error description to be text', obj.description) + la( + is.unemptyString(obj.description), + 'expected error description to be text', + obj.description + ) // assuming that if there the solution is a function it will handle // error message and (optional previous error message) @@ -258,8 +314,11 @@ function formErrorText (info, msg, prevMessage) { `) } else { - la(is.unemptyString(obj.solution), - 'expected error solution to be text', obj.solution) + la( + is.unemptyString(obj.solution), + 'expected error solution to be text', + obj.solution + ) add(` ${obj.description} @@ -312,15 +371,33 @@ const raise = (info) => { const throwFormErrorText = (info) => { return (msg, prevMessage) => { - return formErrorText(info, msg, prevMessage) - .then(raise(info)) + return formErrorText(info, msg, prevMessage).then(raise(info)) + } +} + +/** + * Forms full error message with error and OS details, prints to the error output + * and then exits the process. + * @param {ErrorInformation} info Error information {description, solution} + * @example return exitWithError(errors.invalidCypressEnv)('foo') + */ +const exitWithError = (info) => { + return (msg) => { + return formErrorText(info, msg).then((text) => { + // eslint-disable-next-line no-console + console.error(text) + process.exit(info.exitCode || 1) + }) } } module.exports = { raise, + exitWithError, + // formError, formErrorText, throwFormErrorText, + getError, hr, errors: { nonZeroExitCodeXvfb, @@ -334,9 +411,12 @@ module.exports = { unexpected, failedDownload, failedUnzip, + invalidCypressEnv, invalidCacheDirectory, removed, CYPRESS_RUN_BINARY, smokeTestFailure, + childProcessKilled, + incompatibleHeadlessFlags, }, } diff --git a/cli/lib/exec/open.js b/cli/lib/exec/open.js index 5187d5c35631..786ae9a6e9dc 100644 --- a/cli/lib/exec/open.js +++ b/cli/lib/exec/open.js @@ -11,18 +11,22 @@ module.exports = { const args = [] - if (options.env) { - args.push('--env', options.env) - } - if (options.config) { args.push('--config', options.config) } + if (options.configFile !== undefined) { + args.push('--config-file', options.configFile) + } + if (options.browser) { args.push('--browser', options.browser) } + if (options.env) { + args.push('--env', options.env) + } + if (options.port) { args.push('--port', options.port) } diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index a3b29c2884c0..28fc4f352db7 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -1,95 +1,117 @@ const _ = require('lodash') -const debug = require('debug')('cypress:cli') +const debug = require('debug')('cypress:cli:run') const util = require('../util') const spawn = require('./spawn') const verify = require('../tasks/verify') +const { exitWithError, errors } = require('../errors') // maps options collected by the CLI // and forms list of CLI arguments to the server const processRunOptions = (options = {}) => { - debug('processing run options') + debug('processing run options %o', options) const args = ['--run-project', options.project] - //// if key is set use that - else attempt to find it by environment variable - if (options.key == null) { - debug('--key is not set, looking up environment variable CYPRESS_RECORD_KEY') - options.key = util.getEnv('CYPRESS_RECORD_KEY') || util.getEnv('CYPRESS_CI_KEY') + if (options.browser) { + args.push('--browser', options.browser) } - if (options.env) { - args.push('--env', options.env) + if (options.ci) { + // push to display the deprecation message + args.push('--ci') + + // also automatically record + args.push('--record', true) + } + + if (options.ciBuildId) { + args.push('--ci-build-id', options.ciBuildId) } if (options.config) { args.push('--config', options.config) } - if (options.port) { - args.push('--port', options.port) + if (options.configFile !== undefined) { + args.push('--config-file', options.configFile) } - // if we have specific spec(s) push that into the args - if (options.spec) { - args.push('--spec', options.spec) + if (options.env) { + args.push('--env', options.env) } - //// if we have a specific reporter push that into the args - if (options.reporter) { - args.push('--reporter', options.reporter) + if (options.exit === false) { + args.push('--no-exit') } - //// if we have a specific reporter push that into the args - if (options.reporterOptions) { - args.push('--reporter-options', options.reporterOptions) + if (options.group) { + args.push('--group', options.group) } - if (options.ci) { - //// push to display the deprecation message - args.push('--ci') + if (options.headed) { + args.push('--headed', options.headed) + } - //// also automatically record - args.push('--record', true) + if (options.headless) { + if (options.headed) { + // throw this error synchronously, it will be caught later on and + // the details will be propagated to the promise chain + const err = new Error() + + err.details = errors.incompatibleHeadlessFlags + throw err + } + + args.push('--headed', !options.headless) } - //// if we have a key assume we're in record mode + // if key is set use that - else attempt to find it by environment variable + if (options.key == null) { + debug('--key is not set, looking up environment variable CYPRESS_RECORD_KEY') + options.key = util.getEnv('CYPRESS_RECORD_KEY') || util.getEnv('CYPRESS_CI_KEY') + } + + // if we have a key assume we're in record mode if (options.key) { args.push('--key', options.key) } - //// if record is defined and we're not - //// already in ci mode, then send it up - if (options.record != null && !options.ci) { - args.push('--record', options.record) + if (options.outputPath) { + args.push('--output-path', options.outputPath) } if (options.parallel) { args.push('--parallel') } - if (options.group) { - args.push('--group', options.group) + if (options.port) { + args.push('--port', options.port) } - if (options.ciBuildId) { - args.push('--ci-build-id', options.ciBuildId) + // if record is defined and we're not + // already in ci mode, then send it up + if (options.record != null && !options.ci) { + args.push('--record', options.record) } - if (options.outputPath) { - args.push('--output-path', options.outputPath) + // if we have a specific reporter push that into the args + if (options.reporter) { + args.push('--reporter', options.reporter) } - if (options.browser) { - args.push('--browser', options.browser) + // if we have a specific reporter push that into the args + if (options.reporterOptions) { + args.push('--reporter-options', options.reporterOptions) } - if (options.headed) { - args.push('--headed', options.headed) + // if we have specific spec(s) push that into the args + if (options.spec) { + args.push('--spec', options.spec) } - if (options.exit === false) { - args.push('--no-exit') + if (options.tag) { + args.push('--tag', options.tag) } return args @@ -108,7 +130,17 @@ module.exports = { }) function run () { - const args = processRunOptions(options) + let args + + try { + args = processRunOptions(options) + } catch (err) { + if (err.details) { + return exitWithError(err.details)() + } + + throw err + } debug('run to spawn.start args %j', args) diff --git a/cli/lib/exec/spawn.js b/cli/lib/exec/spawn.js index dbb9a6507473..88eb8bc2ffa9 100644 --- a/cli/lib/exec/spawn.js +++ b/cli/lib/exec/spawn.js @@ -9,7 +9,8 @@ const debugElectron = require('debug')('cypress:electron') const util = require('../util') const state = require('../tasks/state') const xvfb = require('./xvfb') -const { throwFormErrorText, errors } = require('../errors') +const verify = require('../tasks/verify') +const errors = require('../errors') const isXlibOrLibudevRe = /^(?:Xlib|libudev)/ const isHighSierraWarningRe = /\*\*\* WARNING/ @@ -72,10 +73,15 @@ module.exports = { debug('needs to start own Xvfb?', needsXvfb) - // always push cwd into the args + // 1. Start arguments with "--" so Electron knows these are OUR + // arguments and does not try to sanitize them. Otherwise on Windows + // an url in one of the arguments crashes it :( + // https://github.com/cypress-io/cypress/issues/5466 + + // 2. Always push cwd into the args // which additionally acts as a signal to the // binary that it was invoked through the NPM module - args = [].concat(args, '--cwd', process.cwd()) + args = ['--'].concat(args, '--cwd', process.cwd()) _.defaults(options, { dev: false, @@ -98,6 +104,8 @@ module.exports = { args.unshift( path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js') ) + + debug('in dev mode the args became %o', args) } const { onStderrData, electronLogging } = overrides @@ -105,6 +113,12 @@ module.exports = { const electronArgs = _.clone(args) const node11WindowsFix = isPlatform('win32') + if (!options.dev && verify.needsSandbox()) { + // this is one of the Electron's command line switches + // thus it needs to be before "--" separator + electronArgs.unshift('--no-sandbox') + } + // strip dev out of child process options let stdioOptions = _.pick(options, 'env', 'detached', 'stdio') @@ -132,15 +146,43 @@ module.exports = { const child = cp.spawn(executable, electronArgs, stdioOptions) - child.on('close', resolve) + function resolveOn (event) { + return function (code, signal) { + debug('child event fired %o', { event, code, signal }) + + if (code === null) { + const errorObject = errors.errors.childProcessKilled(event, signal) + + return errors.getError(errorObject).then(reject) + } + + resolve(code) + } + } + + child.on('close', resolveOn('close')) + child.on('exit', resolveOn('exit')) child.on('error', reject) - child.stdin && child.stdin.pipe(process.stdin) - child.stdout && child.stdout.pipe(process.stdout) + // if stdio options is set to 'pipe', then + // we should set up pipes: + // process STDIN (read stream) => child STDIN (writeable) + // child STDOUT => process STDOUT + // child STDERR => process STDERR with additional filtering + if (child.stdin) { + debug('piping process STDIN into child STDIN') + process.stdin.pipe(child.stdin) + } + + if (child.stdout) { + debug('piping child STDOUT to process STDOUT') + child.stdout.pipe(process.stdout) + } // if this is defined then we are manually piping for linux // to filter out the garbage - child.stderr && + if (child.stderr) { + debug('piping child STDERR to process STDERR') child.stderr.on('data', (data) => { const str = data.toString() @@ -158,15 +200,17 @@ module.exports = { // else pass it along! process.stderr.write(data) }) + } // https://github.com/cypress-io/cypress/issues/1841 + // https://github.com/cypress-io/cypress/issues/5241 // In some versions of node, it will throw on windows // when you close the parent process after piping // into the child process. unpiping does not seem // to have any effect. so we're just catching the // error here and not doing anything. process.stdin.on('error', (err) => { - if (err.code === 'EPIPE') { + if (['EPIPE', 'ENOTCONN'].includes(err.code)) { return } @@ -223,7 +267,9 @@ module.exports = { return code }) - .catch(throwFormErrorText(errors.unexpected)) + // we can format and handle an error message from the code above + // prevent wrapping error again by using "known: undefined" filter + .catch({ known: undefined }, errors.throwFormErrorText(errors.errors.unexpected)) } if (needsXvfb) { diff --git a/cli/lib/exec/versions.js b/cli/lib/exec/versions.js index 58ed2e1726b9..bb3589ef7494 100644 --- a/cli/lib/exec/versions.js +++ b/cli/lib/exec/versions.js @@ -8,7 +8,6 @@ const { throwFormErrorText, errors } = require('../errors') const getVersions = () => { return Promise.try(() => { - if (util.getEnv('CYPRESS_RUN_BINARY')) { let envBinaryPath = path.resolve(util.getEnv('CYPRESS_RUN_BINARY')) diff --git a/cli/lib/exec/xvfb.js b/cli/lib/exec/xvfb.js index c5e9ce4656f9..463a69f5631b 100644 --- a/cli/lib/exec/xvfb.js +++ b/cli/lib/exec/xvfb.js @@ -8,7 +8,7 @@ const { throwFormErrorText, errors } = require('../errors') const util = require('../util') const xvfb = Promise.promisifyAll(new Xvfb({ - timeout: 5000, // milliseconds + timeout: 30000, // milliseconds onStderrData (data) { if (debugXvfb.enabled) { debugXvfb(data.toString()) diff --git a/cli/lib/tasks/download.js b/cli/lib/tasks/download.js index 3bdda4ffd474..5afd2a7b95c3 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -176,7 +176,6 @@ const verifyDownloadedFile = (filename, expectedSize, expectedChecksum) => { debug('downloaded file lacks checksum or size to verify') return Promise.resolve() - } // downloads from given url diff --git a/cli/lib/tasks/install.js b/cli/lib/tasks/install.js index 6215a08e8e18..26cb7b23ccf8 100644 --- a/cli/lib/tasks/install.js +++ b/cli/lib/tasks/install.js @@ -27,7 +27,6 @@ const alreadyInstalledMsg = () => { } const displayCompletionMsg = () => { - // check here to see if we are globally installed if (util.isInstalledGlobally()) { // if we are display a warning @@ -103,7 +102,6 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }) => { { title: util.titleize('Finishing Installation'), task: (ctx, task) => { - const cleanup = () => { debug('removing zip file %s', downloadDestination) @@ -129,7 +127,6 @@ const downloadAndUnzip = ({ version, installDir, downloadDir }) => { } const start = (options = {}) => { - // handle deprecated / removed if (util.getEnv('CYPRESS_BINARY_VERSION')) { return throwFormErrorText(errors.removed.CYPRESS_BINARY_VERSION)() @@ -152,7 +149,6 @@ const start = (options = {}) => { // let this environment variable reset the binary version we need if (util.getEnv('CYPRESS_INSTALL_BINARY')) { - // because passed file paths are often double quoted // and might have extra whitespace around, be robust and trim the string const trimAndRemoveDoubleQuotes = true @@ -175,7 +171,6 @@ const start = (options = {}) => { // if this doesn't match the expected version // then print warning to the user if (envVarVersion !== needVersion) { - // reset the version to the env var version needVersion = envVarVersion } @@ -211,7 +206,6 @@ const start = (options = {}) => { return state.getBinaryPkgVersionAsync(binaryDir) }) .then((binaryVersion) => { - if (!binaryVersion) { debug('no binary installed under cli version') diff --git a/cli/lib/tasks/state.js b/cli/lib/tasks/state.js index 4f449c4f47f2..0b284e9808af 100644 --- a/cli/lib/tasks/state.js +++ b/cli/lib/tasks/state.js @@ -1,6 +1,7 @@ const _ = require('lodash') const os = require('os') const path = require('path') +const untildify = require('untildify') const debug = require('debug')('cypress:cli') const fs = require('../fs') @@ -53,14 +54,35 @@ const getVersionDir = (version = util.pkgVersion()) => { return path.join(getCacheDir(), version) } +/** + * When executing "npm postinstall" hook, the working directory is set to + * "/node_modules/cypress", which can be surprising when using relative paths. + */ +const isInstallingFromPostinstallHook = () => { + // individual folders + const cwdFolders = process.cwd().split(path.sep) + const length = cwdFolders.length + + return cwdFolders[length - 2] === 'node_modules' && cwdFolders[length - 1] === 'cypress' +} + const getCacheDir = () => { let cache_directory = util.getCacheDir() if (util.getEnv('CYPRESS_CACHE_FOLDER')) { - const envVarCacheDir = util.getEnv('CYPRESS_CACHE_FOLDER') + const envVarCacheDir = untildify(util.getEnv('CYPRESS_CACHE_FOLDER')) debug('using environment variable CYPRESS_CACHE_FOLDER %s', envVarCacheDir) - cache_directory = path.resolve(envVarCacheDir) + + if (!path.isAbsolute(envVarCacheDir) && isInstallingFromPostinstallHook()) { + const packageRootFolder = path.join('..', '..', envVarCacheDir) + + cache_directory = path.resolve(packageRootFolder) + debug('installing from postinstall hook, original root folder is %s', packageRootFolder) + debug('and resolved cache directory is %s', cache_directory) + } else { + cache_directory = path.resolve(envVarCacheDir) + } } return cache_directory diff --git a/cli/lib/tasks/unzip.js b/cli/lib/tasks/unzip.js index a7a7124290ec..be526194221c 100644 --- a/cli/lib/tasks/unzip.js +++ b/cli/lib/tasks/unzip.js @@ -3,7 +3,7 @@ const is = require('check-more-types') const cp = require('child_process') const os = require('os') const yauzl = require('yauzl') -const debug = require('debug')('cypress:cli') +const debug = require('debug')('cypress:cli:unzip') const extract = require('extract-zip') const Promise = require('bluebird') const readline = require('readline') @@ -12,9 +12,12 @@ const { throwFormErrorText, errors } = require('../errors') const fs = require('../fs') const util = require('../util') +const unzipTools = { + extract, +} + // expose this function for simple testing const unzip = ({ zipFilePath, installDir, progress }) => { - debug('unzipping from %s', zipFilePath) debug('into', installDir) @@ -22,15 +25,17 @@ const unzip = ({ zipFilePath, installDir, progress }) => { throw new Error('Missing zip filename') } + const startTime = Date.now() + let yauzlDoneTime = 0 + return fs.ensureDirAsync(installDir) .then(() => { return new Promise((resolve, reject) => { return yauzl.open(zipFilePath, (err, zipFile) => { + yauzlDoneTime = Date.now() + if (err) return reject(err) - // debug('zipfile.paths:', zipFile) - // zipFile.on('entry', debug) - // debug(zipFile.readEntry()) const total = zipFile.entryCount debug('zipFile entries count', total) @@ -58,6 +63,8 @@ const unzip = ({ zipFilePath, installDir, progress }) => { } const unzipWithNode = () => { + debug('unzipping with node.js (slow)') + const endFn = (err) => { if (err) { return reject(err) @@ -71,15 +78,50 @@ const unzip = ({ zipFilePath, installDir, progress }) => { onEntry: tick, } - return extract(zipFilePath, opts, endFn) + return unzipTools.extract(zipFilePath, opts, endFn) + } + + const unzipWithUnzipTool = () => { + debug('unzipping via `unzip`') + + const inflatingRe = /inflating:/ + + const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]) + + sp.on('error', unzipWithNode) + + sp.on('close', (code) => { + if (code === 0) { + percent = 100 + notify(percent) + + return resolve() + } + + debug('`unzip` failed %o', { code }) + + return unzipWithNode() + }) + + sp.stdout.on('data', (data) => { + if (inflatingRe.test(data)) { + return tick() + } + }) + + sp.stderr.on('data', (data) => { + debug('`unzip` stderr %s', data) + }) } - //# we attempt to first unzip with the native osx - //# ditto because its less likely to have problems - //# with corruption, symlinks, or icons causing failures - //# and can handle resource forks - //# http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/ + // we attempt to first unzip with the native osx + // ditto because its less likely to have problems + // with corruption, symlinks, or icons causing failures + // and can handle resource forks + // http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/ const unzipWithOsx = () => { + debug('unzipping via `ditto`') + const copyingFileRe = /^copying file/ const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]) @@ -97,6 +139,8 @@ const unzip = ({ zipFilePath, installDir, progress }) => { return resolve() } + debug('`ditto` failed %o', { code }) + return unzipWithNode() }) @@ -114,6 +158,7 @@ const unzip = ({ zipFilePath, installDir, progress }) => { case 'darwin': return unzipWithOsx() case 'linux': + return unzipWithUnzipTool() case 'win32': return unzipWithNode() default: @@ -121,6 +166,12 @@ const unzip = ({ zipFilePath, installDir, progress }) => { } }) }) + .tap(() => { + debug('unzip completed %o', { + yauzlMs: yauzlDoneTime - startTime, + unzipMs: Date.now() - yauzlDoneTime, + }) + }) }) } @@ -148,4 +199,8 @@ const start = ({ zipFilePath, installDir, progress }) => { module.exports = { start, + utils: { + unzip, + unzipTools, + }, } diff --git a/cli/lib/tasks/verify.js b/cli/lib/tasks/verify.js index bfafcb34c272..9e359abd2714 100644 --- a/cli/lib/tasks/verify.js +++ b/cli/lib/tasks/verify.js @@ -7,6 +7,7 @@ const { stripIndent } = require('common-tags') const Promise = require('bluebird') const logSymbols = require('log-symbols') const path = require('path') +const os = require('os') const { throwFormErrorText, errors } = require('../errors') const util = require('../util') @@ -80,6 +81,12 @@ const runSmokeTest = (binaryDir, options) => { const random = _.random(0, 1000) const args = ['--smoke-test', `--ping=${random}`] + if (needsSandbox()) { + // electron requires --no-sandbox to run as root + debug('disabling Electron sandbox') + args.unshift('--no-sandbox') + } + if (options.dev) { executable = 'node' args.unshift( @@ -112,14 +119,18 @@ const runSmokeTest = (binaryDir, options) => { .then((result) => { // TODO: when execa > 1.1 is released // change this to `result.all` for both stderr and stdout - const smokeTestReturned = result.stdout + // use lodash to be robust during tests against null result or missing stdout + const smokeTestStdout = _.get(result, 'stdout', '') - debug('smoke test stdout "%s"', smokeTestReturned) + debug('smoke test stdout "%s"', smokeTestStdout) - if (!util.stdoutLineMatches(String(random), smokeTestReturned)) { + if (!util.stdoutLineMatches(String(random), smokeTestStdout)) { debug('Smoke test failed because could not find %d in:', random, result) - return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, false))(result.stderr || result.stdout) + const smokeTestStderr = _.get(result, 'stderr', '') + const errorText = smokeTestStderr || smokeTestStdout + + return throwFormErrorText(errors.smokeTestFailure(smokeTestCommand, false))(errorText) } }) } @@ -212,7 +223,6 @@ function testBinary (version, binaryDir, options) { const maybeVerify = (installedVersion, binaryDir, options) => { return state.getBinaryVerifiedAsync(binaryDir) .then((isVerified) => { - debug('is Verified ?', isVerified) let shouldVerify = !isVerified @@ -308,7 +318,6 @@ const start = (options = {}) => { return state.getBinaryPkgVersionAsync(binaryDir) }) .then((binaryVersion) => { - if (!binaryVersion) { debug('no Cypress binary found for cli version ', packageVersion) @@ -347,7 +356,21 @@ const start = (options = {}) => { }) } +const isLinuxLike = () => os.platform() !== 'win32' + +/** + * Returns true if running on a system where Electron needs "--no-sandbox" flag. + * @see https://crbug.com/638180 + * + * On Debian we had problems running in sandbox even for non-root users. + * @see https://github.com/cypress-io/cypress/issues/5434 + * Seems there is a lot of discussion around this issue among Electron users + * @see https://github.com/electron/electron/issues/17972 +*/ +const needsSandbox = () => isLinuxLike() + module.exports = { start, VERIFY_TEST_RUNNER_TIMEOUT_MS, + needsSandbox, } diff --git a/cli/lib/util.js b/cli/lib/util.js index 9d0044835675..458569bfdcd4 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -119,6 +119,25 @@ function stdoutLineMatches (expectedLine, stdout) { return lines.some(lineMatches) } +/** + * Confirms if given value is a valid CYPRESS_ENV value. Undefined values + * are valid, because the system can set the default one. + * + * @param {string} value + * @example util.isValidCypressEnvValue(process.env.CYPRESS_ENV) + */ +function isValidCypressEnvValue (value) { + if (_.isUndefined(value)) { + // will get default value + return true + } + + // names of config environments, see "packages/server/config/app.yml" + const names = ['development', 'test', 'staging', 'production'] + + return _.includes(names, value) +} + /** * Prints NODE_OPTIONS using debug() module, but only * if DEBUG=cypress... is set @@ -139,6 +158,8 @@ function printNodeOptions (log = debug) { * Removes double quote characters * from the start and end of the given string IF they are both present * + * @param {string} str Input string + * @returns {string} Trimmed string or the original string if there are no double quotes around it. * @example ``` dequote('"foo"') @@ -156,9 +177,58 @@ const dequote = (str) => { return str } +const parseOpts = (opts) => { + opts = _.pick(opts, + 'browser', + 'cachePath', + 'cacheList', + 'cacheClear', + 'ciBuildId', + 'config', + 'configFile', + 'cypressVersion', + 'destination', + 'detached', + 'dev', + 'exit', + 'env', + 'force', + 'global', + 'group', + 'headed', + 'headless', + 'key', + 'path', + 'parallel', + 'port', + 'project', + 'reporter', + 'reporterOptions', + 'record', + 'spec', + 'tag') + + if (opts.exit) { + opts = _.omit(opts, 'exit') + } + + // some options might be quoted - which leads to unexpected results + // remove double quotes from certain options + const removeQuotes = { + group: dequote, + ciBuildId: dequote, + } + const cleanOpts = R.evolve(removeQuotes, opts) + + debug('parsed cli options %o', cleanOpts) + + return cleanOpts +} + const util = { normalizeModuleOptions, - + parseOpts, + isValidCypressEnvValue, printNodeOptions, isCi () { @@ -307,7 +377,6 @@ const util = { } return os.release() - }) }, diff --git a/cli/package.json b/cli/package.json index 30e1e8fa1780..a3c01aa9cb01 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,7 +13,7 @@ "lint": "bin-up eslint --fix *.js scripts/*.js bin/* lib/*.js lib/**/*.js test/*.js test/**/*.js", "prerelease": "npm run build", "release": "cd build && releaser --no-node --no-changelog", - "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", + "size": "npm pack --dry", "pretest": "npm run check-deps-pre", "test": "npm run test-unit", "test-debug": "node --inspect --debug-brk $(bin-up _mocha)", @@ -23,11 +23,12 @@ "pretest-watch": "npm run check-deps-pre", "test-watch": "npm run unit -- --watch", "types": "npm run dtslint", - "unit": "BLUEBIRD_DEBUG=1 NODE_ENV=test bin-up mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json" + "unit": "cross-env BLUEBIRD_DEBUG=1 NODE_ENV=test ../node_modules/.bin/mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json" }, "dependencies": { "@cypress/listr-verbose-renderer": "0.4.1", "@cypress/xvfb": "1.2.4", + "@types/sizzle": "2.3.2", "arch": "2.1.1", "bluebird": "3.5.0", "cachedir": "1.3.0", @@ -54,6 +55,7 @@ "request-progress": "3.0.0", "supports-color": "5.5.0", "tmp": "0.1.0", + "untildify": "3.0.3", "url": "0.11.0", "yauzl": "2.10.0" }, @@ -63,7 +65,7 @@ "@types/bluebird": "3.5.18", "@types/chai": "4.0.8", "@types/chai-jquery": "1.1.38", - "@types/jquery": "3.3.6", + "@types/jquery": "3.3.31", "@types/lodash": "4.14.122", "@types/minimatch": "3.0.3", "@types/mocha": "5.2.7", @@ -71,10 +73,11 @@ "@types/sinon-chai": "3.2.2", "babel-cli": "6.26.0", "babel-preset-es2015": "6.24.1", - "bin-up": "1.2.0", + "bin-up": "1.2.2", "chai": "3.5.0", "chai-as-promised": "7.1.1", - "chai-string": "1.4.0", + "chai-string": "1.5.0", + "cross-env": "6.0.3", "dependency-check": "3.4.1", "dtslint": "0.9.0", "execa-wrap": "1.4.0", @@ -85,8 +88,9 @@ "proxyquire": "2.1.0", "shelljs": "0.8.3", "sinon": "7.2.2", - "snap-shot-it": "7.8.0", - "spawn-mock": "1.0.0" + "snap-shot-it": "7.9.0", + "spawn-mock": "1.0.0", + "strip-ansi": "4.0.0" }, "files": [ "bin", diff --git a/cli/schema/cypress.schema.json b/cli/schema/cypress.schema.json index 304696118779..1cf147dae475 100644 --- a/cli/schema/cypress.schema.json +++ b/cli/schema/cypress.schema.json @@ -43,9 +43,12 @@ "description": "The reporter options used. Supported options depend on the reporter. See https://on.cypress.io/reporters#Reporter-Options" }, "testFiles": { - "type": "string", + "type": [ + "string", + "array" + ], "default": "**/*.*", - "description": "A String glob pattern of the test files to load" + "description": "A String or Array of string glob patterns of the test files to load. See https://on.cypress.io/configuration#Global" }, "watchForFileChanges": { "type": "boolean", @@ -199,6 +202,14 @@ "type": "string", "default": null, "description": "A 6 character string use to identify this project in the Cypress Dashboard. See https://on.cypress.io/dashboard-service#Identification" + }, + "nodeVersion": { + "enum": [ + "system", + "bundled" + ], + "default": "bundled", + "description": "If set to 'system', Cypress will try to find a Node.js executable on your path to use when executing your plugins. Otherwise, Cypress will use the Node version bundled with Cypress." } } } diff --git a/cli/scripts/post-install.js b/cli/scripts/post-install.js index 394cea6aefda..81784fcd60cc 100644 --- a/cli/scripts/post-install.js +++ b/cli/scripts/post-install.js @@ -20,6 +20,12 @@ includeTypes.forEach((folder) => { shell.cp('-R', source, 'types') }) +// jQuery v3.3.x includes "dist" folder that just references back to itself +// causing dtslint to think there are double definitions. Remove that folder. +const typesJqueryDistFolder = join('types', 'jquery', 'dist') + +shell.rm('-rf', typesJqueryDistFolder) + // fix paths to Chai, jQuery and other types to be relative shell.sed( '-i', diff --git a/cli/test/.eslintrc b/cli/test/.eslintrc.json similarity index 100% rename from cli/test/.eslintrc rename to cli/test/.eslintrc.json diff --git a/cli/test/lib/cli_spec.js b/cli/test/lib/cli_spec.js index 952441ad1132..c23cda5c287d 100644 --- a/cli/test/lib/cli_spec.js +++ b/cli/test/lib/cli_spec.js @@ -73,6 +73,58 @@ describe('cli', () => { }) }) + context('CYPRESS_ENV', () => { + /** + * Replaces line "Platform: ..." with "Platform: xxx" + * @param {string} s + */ + const replacePlatform = (s) => { + return s.replace(/Platform: .+/, 'Platform: xxx') + } + + /** + * Replaces line "Cypress Version: ..." with "Cypress Version: 1.2.3" + * @param {string} s + */ + const replaceCypressVersion = (s) => { + return s.replace(/Cypress Version: .+/, 'Cypress Version: 1.2.3') + } + + const sanitizePlatform = (text) => { + return text + .split(os.eol) + .map(replacePlatform) + .map(replaceCypressVersion) + .join(os.eol) + } + + it('allows staging environment', () => { + const options = { + env: { + CYPRESS_ENV: 'staging', + }, + // we are only interested in the exit code + filter: ['code', 'stderr'], + } + + return execa('bin/cypress', ['help'], options).then(snapshot) + }) + + it('catches environment "foo"', () => { + const options = { + env: { + CYPRESS_ENV: 'foo', + }, + // we are only interested in the exit code + filter: ['code', 'stderr'], + } + + return execa('bin/cypress', ['help'], options) + .then(sanitizePlatform) + .then(snapshot) + }) + }) + context('cypress version', () => { const binaryDir = '/binary/dir' @@ -172,13 +224,6 @@ describe('cli', () => { expect(run.start).to.be.calledWith({ port: '7878' }) }) - it('calls run with spec', () => { - this.exec('run --spec cypress/integration/foo_spec.js') - expect(run.start).to.be.calledWith({ - spec: 'cypress/integration/foo_spec.js', - }) - }) - it('calls run with port with -p arg', () => { this.exec('run -p 8989') expect(run.start).to.be.calledWith({ port: '8989' }) @@ -248,19 +293,74 @@ describe('cli', () => { expect(run.start).to.be.calledWith({ group: 'staging' }) }) - it('calls run with space-separated --specs', () => { + it('calls run with spec', () => { + this.exec('run --spec cypress/integration/foo_spec.js') + expect(run.start).to.be.calledWith({ + spec: 'cypress/integration/foo_spec.js', + }) + }) + + it('calls run with space-separated --spec', () => { this.exec('run --spec a b c d e f g') expect(run.start).to.be.calledWith({ spec: 'a,b,c,d,e,f,g' }) this.exec('run --dev bang --spec foo bar baz -P ./') expect(run.start).to.be.calledWithMatch({ spec: 'foo,bar,baz' }) }) - it('warns with space-separated --specs', (done) => { + it('warns with space-separated --spec', (done) => { sinon.spy(logger, 'warn') this.exec('run --spec a b c d e f g --dev') snapshot(logger.warn.getCall(0).args[0]) done() }) + + it('calls run with --tag', () => { + this.exec('run --tag nightly') + expect(run.start).to.be.calledWith({ tag: 'nightly' }) + }) + + it('calls run comma-separated --tag', () => { + this.exec('run --tag nightly,staging') + expect(run.start).to.be.calledWith({ tag: 'nightly,staging' }) + }) + + it('does not remove double quotes from --tag', () => { + // I think it is a good idea to lock down this behavior + // to make sure we either preserve it or change it in the future + this.exec('run --tag "nightly"') + expect(run.start).to.be.calledWith({ tag: '"nightly"' }) + }) + + it('calls run comma-separated --spec', () => { + this.exec('run --spec main_spec.js,view_spec.js') + expect(run.start).to.be.calledWith({ spec: 'main_spec.js,view_spec.js' }) + }) + + it('calls run with space-separated --tag', () => { + this.exec('run --tag a b c d e f g') + expect(run.start).to.be.calledWith({ tag: 'a,b,c,d,e,f,g' }) + this.exec('run --dev bang --tag foo bar baz -P ./') + expect(run.start).to.be.calledWithMatch({ tag: 'foo,bar,baz' }) + }) + + it('warns with space-separated --tag', (done) => { + sinon.spy(logger, 'warn') + this.exec('run --tag a b c d e f g --dev') + snapshot(logger.warn.getCall(0).args[0]) + done() + }) + + it('calls run with space-separated --tag and --spec', () => { + this.exec('run --tag a b c d e f g --spec h i j k l') + expect(run.start).to.be.calledWith({ tag: 'a,b,c,d,e,f,g', spec: 'h,i,j,k,l' }) + this.exec('run --dev bang --tag foo bar baz -P ./ --spec fizz buzz --headed false') + expect(run.start).to.be.calledWithMatch({ tag: 'foo,bar,baz', spec: 'fizz,buzz' }) + }) + + it('removes stray double quotes from --ci-build-id and --group', () => { + this.exec('run --ci-build-id "123" --group "staging"') + expect(run.start).to.be.calledWith({ ciBuildId: '123', group: 'staging' }) + }) }) context('cypress open', () => { diff --git a/cli/test/lib/cypress_spec.js b/cli/test/lib/cypress_spec.js index 404878dbf98a..506c5e3ab386 100644 --- a/cli/test/lib/cypress_spec.js +++ b/cli/test/lib/cypress_spec.js @@ -54,6 +54,18 @@ describe('cypress', function () { expect(args).to.deep.eq({ config: JSON.stringify(config) }) }) }) + + it('passes configFile: false', () => { + const opts = { + configFile: false, + } + + return cypress.open(opts) + .then(getStartArgs) + .then((args) => { + expect(args).to.deep.eq(opts) + }) + }) }) context('.run', function () { @@ -123,5 +135,17 @@ describe('cypress', function () { it('resolves with contents of tmp file', () => { return cypress.run().then(snapshot) }) + + it('passes configFile: false', () => { + const opts = { + configFile: false, + } + + return cypress.run(opts) + .then(getStartArgs) + .then((args) => { + expect(args).to.deep.eq(opts) + }) + }) }) }) diff --git a/cli/test/lib/errors_spec.js b/cli/test/lib/errors_spec.js index 4310da3a2f84..fbf90797b5d9 100644 --- a/cli/test/lib/errors_spec.js +++ b/cli/test/lib/errors_spec.js @@ -2,7 +2,7 @@ require('../spec_helper') const os = require('os') const snapshot = require('../support/snapshot') -const { errors, formErrorText } = require(`${lib}/errors`) +const { errors, getError, formErrorText } = require(`${lib}/errors`) const util = require(`${lib}/util`) describe('errors', function () { @@ -19,6 +19,20 @@ describe('errors', function () { }) }) + context('getError', () => { + it('forms full message and creates Error object', () => { + const errObject = errors.childProcessKilled('exit', 'SIGKILL') + + snapshot('child kill error object', errObject) + + return getError(errObject).then((e) => { + expect(e).to.be.an('Error') + expect(e).to.have.property('known', true) + snapshot('Error message', e.message) + }) + }) + }) + context('.errors.formErrorText', function () { it('returns fully formed text message', () => { expect(missingXvfb).to.be.an('object') diff --git a/cli/test/lib/exec/.eslintrc b/cli/test/lib/exec/.eslintrc.json similarity index 100% rename from cli/test/lib/exec/.eslintrc rename to cli/test/lib/exec/.eslintrc.json diff --git a/cli/test/lib/exec/open_spec.js b/cli/test/lib/exec/open_spec.js index f91844184dc7..62315f5e3383 100644 --- a/cli/test/lib/exec/open_spec.js +++ b/cli/test/lib/exec/open_spec.js @@ -56,6 +56,24 @@ describe('exec open', function () { }) }) + it('spawns with --config-file false', function () { + return open.start({ configFile: false }) + .then(() => { + expect(spawn.start).to.be.calledWith( + ['--config-file', false] + ) + }) + }) + + it('spawns with --config-file set', function () { + return open.start({ configFile: 'special-cypress.json' }) + .then(() => { + expect(spawn.start).to.be.calledWith( + ['--config-file', 'special-cypress.json'] + ) + }) + }) + it('spawns with cwd as --project if not installed globally', function () { util.isInstalledGlobally.returns(false) diff --git a/cli/test/lib/exec/run_spec.js b/cli/test/lib/exec/run_spec.js index a4c3d9f11bf1..a2e89bd32686 100644 --- a/cli/test/lib/exec/run_spec.js +++ b/cli/test/lib/exec/run_spec.js @@ -1,5 +1,6 @@ require('../../spec_helper') +const os = require('os') const snapshot = require('../../support/snapshot') const util = require(`${lib}/util`) @@ -30,6 +31,27 @@ describe('exec run', function () { snapshot(args) }) + it('passes --config-file false option', () => { + const args = run.processRunOptions({ + configFile: false, + }) + + snapshot(args) + }) + + it('does not allow setting paradoxical --headed and --headless flags', () => { + os.platform.returns('linux') + process.exit.returns() + + expect(() => run.processRunOptions({ headed: true, headless: true })).to.throw() + }) + + it('passes --headed according to --headless', () => { + expect(run.processRunOptions({ headless: true })).to.deep.eq([ + '--run-project', undefined, '--headed', false, + ]) + }) + it('does not remove --record option when using --browser', () => { const args = run.processRunOptions({ record: 'foo', @@ -74,6 +96,24 @@ describe('exec run', function () { }) }) + it('spawns with --config-file false', function () { + return run.start({ configFile: false }) + .then(() => { + expect(spawn.start).to.be.calledWith( + ['--run-project', process.cwd(), '--config-file', false] + ) + }) + }) + + it('spawns with --config-file set', function () { + return run.start({ configFile: 'special-cypress.json' }) + .then(() => { + expect(spawn.start).to.be.calledWith( + ['--run-project', process.cwd(), '--config-file', 'special-cypress.json'] + ) + }) + }) + it('spawns with --record false', function () { return run.start({ record: false }) .then(() => { @@ -105,5 +145,23 @@ describe('exec run', function () { expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--output-path', '/path/to/output']) }) }) + + it('spawns with --tag value', function () { + return run.start({ tag: 'nightly' }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--tag', 'nightly', + ]) + }) + }) + + it('spawns with several --tag words unchanged', function () { + return run.start({ tag: 'nightly, sanity' }) + .then(() => { + expect(spawn.start).to.be.calledWith([ + '--run-project', process.cwd(), '--tag', 'nightly, sanity', + ]) + }) + }) }) }) diff --git a/cli/test/lib/exec/spawn_spec.js b/cli/test/lib/exec/spawn_spec.js index 75f46650ef5b..a090a95e9181 100644 --- a/cli/test/lib/exec/spawn_spec.js +++ b/cli/test/lib/exec/spawn_spec.js @@ -7,12 +7,15 @@ const tty = require('tty') const path = require('path') const EE = require('events') const mockedEnv = require('mocked-env') +const debug = require('debug')('test') const state = require(`${lib}/tasks/state`) const xvfb = require(`${lib}/exec/xvfb`) const spawn = require(`${lib}/exec/spawn`) +const verify = require(`${lib}/tasks/verify`) const util = require(`${lib}/util.js`) const expect = require('chai').expect +const snapshot = require('../../support/snapshot') const cwd = process.cwd() @@ -39,7 +42,10 @@ describe('lib/exec/spawn', function () { }, } - sinon.stub(process, 'stdin').value(new EE) + // process.stdin is both an event emitter and a readable stream + this.processStdin = new EE() + this.processStdin.pipe = sinon.stub().returns(undefined) + sinon.stub(process, 'stdin').value(this.processStdin) sinon.stub(cp, 'spawn').returns(this.spawnedProcess) sinon.stub(xvfb, 'start').resolves() sinon.stub(xvfb, 'stop').resolves() @@ -73,12 +79,22 @@ describe('lib/exec/spawn', function () { }) context('.start', function () { + // ️️⚠️ NOTE ⚠️ + // when asserting the calls made to spawn the child Cypress process + // we have to be _very_ careful. Spawn uses process.env object, if an assertion + // fails, it will print the entire process.env object to the logs, which + // might contain sensitive environment variables. Think about what the + // failed assertion might print to the public CI logs and limit + // the environment variables when running tests on CI. + it('passes args + options to spawn', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + sinon.stub(verify, 'needsSandbox').returns(false) return spawn.start('--foo', { foo: 'bar' }) .then(() => { expect(cp.spawn).to.be.calledWithMatch('/path/to/cypress', [ + '--', '--foo', '--cwd', cwd, @@ -89,8 +105,53 @@ describe('lib/exec/spawn', function () { }) }) + it('uses --no-sandbox when needed', function () { + this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + sinon.stub(verify, 'needsSandbox').returns(true) + + return spawn.start('--foo', { foo: 'bar' }) + .then(() => { + // skip the options argument: we do not need anything about it + // and also less risk that a failed assertion would dump the + // entire ENV object with possible sensitive variables + const args = cp.spawn.firstCall.args.slice(0, 2) + // it is important for "--no-sandbox" to appear before "--" separator + const expectedCliArgs = [ + '--no-sandbox', + '--', + '--foo', + '--cwd', + cwd, + ] + + expect(args).to.deep.equal(['/path/to/cypress', expectedCliArgs]) + }) + }) + it('uses npm command when running in dev mode', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + sinon.stub(verify, 'needsSandbox').returns(false) + + const p = path.resolve('..', 'scripts', 'start.js') + + return spawn.start('--foo', { dev: true, foo: 'bar' }) + .then(() => { + expect(cp.spawn).to.be.calledWithMatch('node', [ + p, + '--', + '--foo', + '--cwd', + cwd, + ], { + detached: false, + stdio: ['inherit', 'inherit', 'pipe'], + }) + }) + }) + + it('does not pass --no-sandbox when running in dev mode', function () { + this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + sinon.stub(verify, 'needsSandbox').returns(true) const p = path.resolve('..', 'scripts', 'start.js') @@ -98,6 +159,7 @@ describe('lib/exec/spawn', function () { .then(() => { expect(cp.spawn).to.be.calledWithMatch('node', [ p, + '--', '--foo', '--cwd', cwd, @@ -119,6 +181,37 @@ describe('lib/exec/spawn', function () { }) }) + context('closes', function () { + ['close', 'exit'].forEach((event) => { + it(`if '${event}' event fired`, function () { + this.spawnedProcess.on.withArgs(event).yieldsAsync(0) + + return spawn.start('--foo') + }) + }) + + it('if exit event fired and close event fired', function () { + this.spawnedProcess.on.withArgs('exit').yieldsAsync(0) + this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + + return spawn.start('--foo') + }) + }) + + context('detects kill signal', function () { + it('exits with error on SIGKILL', function () { + this.spawnedProcess.on.withArgs('exit').yieldsAsync(null, 'SIGKILL') + + return spawn.start('--foo') + .then(() => { + throw new Error('should have hit error handler but did not') + }, (e) => { + debug('error message', e.message) + snapshot(e.message) + }) + }) + }) + it('does not start xvfb when its not needed', function () { this.spawnedProcess.on.withArgs('close').yieldsAsync(0) @@ -194,6 +287,7 @@ describe('lib/exec/spawn', function () { .then(() => { throw new Error('should have hit error handler but did not') }, (e) => { + debug('error message', e.message) expect(e.message).to.include(msg) }) }) @@ -292,6 +386,9 @@ describe('lib/exec/spawn', function () { return spawn.start() .then(() => { expect(cp.spawn.firstCall.args[2].stdio).to.deep.eq('pipe') + // parent process STDIN was piped to child process STDIN + expect(this.processStdin.pipe, 'process.stdin').to.have.been.calledOnce + .and.to.have.been.calledWith(this.spawnedProcess.stdin) }) }) @@ -401,24 +498,28 @@ describe('lib/exec/spawn', function () { }) }) - it('catches process.stdin errors and returns when code=EPIPE', function () { - this.spawnedProcess.on.withArgs('close').yieldsAsync(0) + // https://github.com/cypress-io/cypress/issues/1841 + // https://github.com/cypress-io/cypress/issues/5241 + ;['EPIPE', 'ENOTCONN'].forEach((errCode) => { + it(`catches process.stdin errors and returns when code=${errCode}`, function () { + this.spawnedProcess.on.withArgs('close').yieldsAsync(0) - return spawn.start() - .then(() => { - let called = false + return spawn.start() + .then(() => { + let called = false - const fn = () => { - called = true - const err = new Error() + const fn = () => { + called = true + const err = new Error() - err.code = 'EPIPE' + err.code = errCode - return process.stdin.emit('error', err) - } + return process.stdin.emit('error', err) + } - expect(fn).not.to.throw() - expect(called).to.be.true + expect(fn).not.to.throw() + expect(called).to.be.true + }) }) }) diff --git a/cli/test/lib/exec/xvfb_spec.js b/cli/test/lib/exec/xvfb_spec.js index 55db48b0f5dd..a5da2853ef4a 100644 --- a/cli/test/lib/exec/xvfb_spec.js +++ b/cli/test/lib/exec/xvfb_spec.js @@ -71,7 +71,6 @@ describe('lib/exec/xvfb', function () { }) context('#isNeeded', function () { - it('does not need xvfb on osx', function () { os.platform.returns('darwin') diff --git a/cli/test/lib/tasks/install_spec.js b/cli/test/lib/tasks/install_spec.js index c9c50bae9b85..8e9a9a5b229d 100644 --- a/cli/test/lib/tasks/install_spec.js +++ b/cli/test/lib/tasks/install_spec.js @@ -59,7 +59,6 @@ describe('/lib/tasks/install', function () { }) describe('skips install', function () { - it('when environment variable is set', function () { process.env.CYPRESS_INSTALL_BINARY = '0' @@ -76,7 +75,6 @@ describe('/lib/tasks/install', function () { }) describe('override version', function () { - it('warns when specifying cypress version in env', function () { const version = '0.12.1' @@ -182,7 +180,6 @@ describe('/lib/tasks/install', function () { describe('when version is already installed', function () { beforeEach(function () { state.getBinaryPkgVersionAsync.resolves(packageVersion) - }) it('doesn\'t attempt to download', function () { @@ -191,7 +188,6 @@ describe('/lib/tasks/install', function () { expect(download.start).not.to.be.called expect(state.getBinaryPkgVersionAsync).to.be.calledWith('/cache/Cypress/1.2.3/Cypress.app') }) - }) it('logs \'skipping install\' when explicit cypress install', function () { @@ -202,7 +198,6 @@ describe('/lib/tasks/install', function () { normalize(this.stdout.toString()) ) }) - }) it('logs when already installed when run from postInstall', function () { @@ -249,7 +244,6 @@ describe('/lib/tasks/install', function () { }) it('logs message and starts download', function () { - expect(download.start).to.be.calledWithMatch({ version: packageVersion, }) @@ -301,7 +295,6 @@ describe('/lib/tasks/install', function () { }) it('logs message and starts download', function () { - expect(download.start).to.be.calledWithMatch({ version: packageVersion, }) @@ -467,5 +460,4 @@ describe('/lib/tasks/install', function () { }) }) }) - }) diff --git a/cli/test/lib/tasks/state_spec.js b/cli/test/lib/tasks/state_spec.js index 435ce8c770ae..b998027b263c 100644 --- a/cli/test/lib/tasks/state_spec.js +++ b/cli/test/lib/tasks/state_spec.js @@ -5,6 +5,7 @@ const path = require('path') const Promise = require('bluebird') const proxyquire = require('proxyquire') const mockfs = require('mock-fs') +const debug = require('debug')('test') const fs = require(`${lib}/fs`) const logger = require(`${lib}/logger`) @@ -258,6 +259,39 @@ describe('lib/tasks/state', function () { expect(ret).to.eql(path.resolve('local-cache/folder')) }) + + it('CYPRESS_CACHE_FOLDER resolves from relative path during postinstall', () => { + process.env.CYPRESS_CACHE_FOLDER = './local-cache/folder' + // simulates current folder when running "npm postinstall" hook + sinon.stub(process, 'cwd').returns('/my/project/folder/node_modules/cypress') + const ret = state.getCacheDir() + + debug('returned cache dir %s', ret) + expect(ret).to.eql(path.resolve('/my/project/folder/local-cache/folder')) + }) + + it('CYPRESS_CACHE_FOLDER resolves from absolute path during postinstall', () => { + process.env.CYPRESS_CACHE_FOLDER = '/cache/folder/Cypress' + // simulates current folder when running "npm postinstall" hook + sinon.stub(process, 'cwd').returns('/my/project/folder/node_modules/cypress') + const ret = state.getCacheDir() + + debug('returned cache dir %s', ret) + expect(ret).to.eql(path.resolve('/cache/folder/Cypress')) + }) + + it('resolves ~ with user home folder', () => { + const homeDir = os.homedir() + + process.env.CYPRESS_CACHE_FOLDER = '~/.cache/Cypress' + + const ret = state.getCacheDir() + + debug('cache dir is "%s"', ret) + expect(path.isAbsolute(ret), ret).to.be.true + expect(ret, '~ has been resolved').to.not.contain('~') + expect(ret, 'replaced ~ with home directory').to.equal(`${homeDir}/.cache/Cypress`) + }) }) context('.parseRealPlatformBinaryFolderAsync', function () { diff --git a/cli/test/lib/tasks/unzip_spec.js b/cli/test/lib/tasks/unzip_spec.js index 86260b043464..20035c0cd968 100644 --- a/cli/test/lib/tasks/unzip_spec.js +++ b/cli/test/lib/tasks/unzip_spec.js @@ -3,6 +3,7 @@ require('../../spec_helper') const os = require('os') const path = require('path') const snapshot = require('../../support/snapshot') +const cp = require('child_process') const fs = require(`${lib}/fs`) const util = require(`${lib}/util`) @@ -63,4 +64,41 @@ describe('lib/tasks/unzip', function () { return fs.statAsync(installDir) }) }) + + // NOTE: hmm, running this test for some reason breaks 4 tests in verify_spec.js with very weird errors + context.skip('on linux', () => { + beforeEach(() => { + os.platform.returns('linux') + }) + + it('can try unzip first then fall back to node unzip', function () { + sinon.stub(unzip.utils.unzipTools, 'extract').resolves() + + const unzipChildProcess = { + on: sinon.stub(), + stdout: { + on () {}, + }, + stderr: { + on () {}, + }, + } + + unzipChildProcess.on.withArgs('error').yieldsAsync(0) + unzipChildProcess.on.withArgs('close').yieldsAsync(0) + sinon.stub(cp, 'spawn').withArgs('unzip').returns(unzipChildProcess) + + const zipFilePath = path.join('test', 'fixture', 'example.zip') + + return unzip + .start({ + zipFilePath, + installDir, + }) + .then(() => { + expect(cp.spawn).to.have.been.calledWith('unzip') + expect(unzip.utils.unzipTools.extract).to.be.calledWith(zipFilePath) + }) + }) + }) }) diff --git a/cli/test/lib/tasks/verify_spec.js b/cli/test/lib/tasks/verify_spec.js index 007dc6e05260..73169f5c57e7 100644 --- a/cli/test/lib/tasks/verify_spec.js +++ b/cli/test/lib/tasks/verify_spec.js @@ -53,11 +53,12 @@ context('lib/tasks/verify', () => { sinon.stub(xvfb, 'stop').resolves() sinon.stub(xvfb, 'isNeeded').returns(false) sinon.stub(Promise.prototype, 'delay').resolves() + sinon.stub(process, 'geteuid').returns(1000) sinon.stub(_, 'random').returns('222') util.exec - .withArgs(executablePath, ['--smoke-test', '--ping=222']) + .withArgs(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222']) .resolves(spawnedProcess) }) @@ -70,7 +71,6 @@ context('lib/tasks/verify', () => { }) it('logs error and exits when no version of Cypress is installed', () => { - return verify .start() .then(() => { @@ -86,6 +86,46 @@ context('lib/tasks/verify', () => { }) }) + it('adds --no-sandbox when user is root', () => { + // make it think the executable exists + createfs({ + alreadyVerified: false, + executable: mockfs.file({ mode: 0o777 }), + packageVersion, + }) + + process.geteuid.returns(0) // user is root + util.exec.resolves({ + stdout: '222', + stderr: '', + }) + + return verify.start() + .then(() => { + expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222']) + }) + }) + + it('adds --no-sandbox when user is non-root', () => { + // make it think the executable exists + createfs({ + alreadyVerified: false, + executable: mockfs.file({ mode: 0o777 }), + packageVersion, + }) + + process.geteuid.returns(1000) // user is non-root + util.exec.resolves({ + stdout: '222', + stderr: '', + }) + + return verify.start() + .then(() => { + expect(util.exec).to.be.calledWith(executablePath, ['--no-sandbox', '--smoke-test', '--ping=222']) + }) + }) + it('is noop when binary is already verified', () => { // make it think the executable exists and is verified createfs({ @@ -158,7 +198,6 @@ context('lib/tasks/verify', () => { .then(() => { snapshot(normalize(slice(stdout.toString()))) }) - }) it('logs error when child process returns incorrect stdout (stderr when exists)', () => { @@ -185,7 +224,6 @@ context('lib/tasks/verify', () => { .then(() => { snapshot(normalize(slice(stdout.toString()))) }) - }) it('logs error when child process returns incorrect stdout (stdout when no stderr)', () => { @@ -211,7 +249,6 @@ context('lib/tasks/verify', () => { .then(() => { snapshot(normalize(slice(stdout.toString()))) }) - }) it('sets ELECTRON_ENABLE_LOGGING without mutating process.env', () => { @@ -651,7 +688,7 @@ context('lib/tasks/verify', () => { }) util.exec - .withArgs(realEnvBinaryPath, ['--smoke-test', '--ping=222']) + .withArgs(realEnvBinaryPath, ['--no-sandbox', '--smoke-test', '--ping=222']) .resolves(spawnedProcess) return verify.start().then(() => { @@ -680,8 +717,35 @@ context('lib/tasks/verify', () => { }) }) }) + + // tests for when Electron needs "--no-sandbox" CLI flag + context('.needsSandbox', () => { + it('needs --no-sandbox on Linux as a root', () => { + os.platform.returns('linux') + process.geteuid.returns(0) // user is root + expect(verify.needsSandbox()).to.be.true + }) + + it('needs --no-sandbox on Linux as a non-root', () => { + os.platform.returns('linux') + process.geteuid.returns(1000) // user is non-root + expect(verify.needsSandbox()).to.be.true + }) + + it('needs --no-sandbox on Mac as a non-root', () => { + os.platform.returns('darwin') + process.geteuid.returns(1000) // user is non-root + expect(verify.needsSandbox()).to.be.true + }) + + it('does not need --no-sandbox on Windows', () => { + os.platform.returns('win32') + expect(verify.needsSandbox()).to.be.false + }) + }) }) +// TODO this needs documentation with examples badly. function createfs ({ alreadyVerified, executable, packageVersion, customDir }) { let mockFiles = { [customDir ? customDir : '/cache/Cypress/1.2.3/Cypress.app']: { diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index 73238da52e60..7b9ef06db7dd 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -321,7 +321,6 @@ describe('util', () => { context('.printNodeOptions', () => { describe('NODE_OPTIONS is not set', () => { - it('does nothing if debug is not enabled', () => { const log = sinon.spy() @@ -498,4 +497,54 @@ describe('util', () => { }) }) }) + + context('parseOpts', () => { + it('passes normal options and strips unknown ones', () => { + const result = util.parseOpts({ + unknownOptions: true, + group: 'my group name', + ciBuildId: 'my ci build id', + }) + + expect(result).to.deep.equal({ + group: 'my group name', + ciBuildId: 'my ci build id', + }) + }) + + it('removes leftover double quotes', () => { + const result = util.parseOpts({ + group: '"my group name"', + ciBuildId: '"my ci build id"', + }) + + expect(result).to.deep.equal({ + group: 'my group name', + ciBuildId: 'my ci build id', + }) + }) + + it('leaves unbalanced double quotes', () => { + const result = util.parseOpts({ + group: 'my group name"', + ciBuildId: '"my ci build id', + }) + + expect(result).to.deep.equal({ + group: 'my group name"', + ciBuildId: '"my ci build id', + }) + }) + + it('works with unspecified options', () => { + const result = util.parseOpts({ + // notice that "group" option is missing + ciBuildId: '"my ci build id"', + }) + + expect(result).to.deep.equal({ + ciBuildId: 'my ci build id', + }) + }) + }) }) diff --git a/cli/test/spec_helper.js b/cli/test/spec_helper.js index e795b33e8731..a5df843f7981 100644 --- a/cli/test/spec_helper.js +++ b/cli/test/spec_helper.js @@ -9,9 +9,7 @@ const { MockChildProcess } = require('spawn-mock') const _kill = MockChildProcess.prototype.kill const patchMockSpawn = () => { - MockChildProcess.prototype.kill = function (...args) { - this.emit('exit') return _kill.apply(this, args) diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index 2faea0e9c0e4..37409b7a0dac 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -43,9 +43,9 @@ declare module 'cypress' { */ reporter: string, /** - * A String glob pattern of the test files to load. + * A String or Array of string glob pattern of the test files to load. */ - testFiles: string + testFiles: string | string[] // // timeouts @@ -224,7 +224,7 @@ declare module 'cypress' { }) ``` */ - interface CypressRunOptions { + interface CypressRunOptions extends CypressCommonOptions { /** * Specify different browser to run tests in, either by name or by filesystem path */ @@ -233,22 +233,18 @@ declare module 'cypress' { * Specify a unique identifier for a run to enable grouping or parallelization */ ciBuildId: string - /** - * Specify configuration - */ - config: Partial - /** - * Specify environment variables - */ - env: object /** * Group recorded tests together under a single run name */ group: string /** - * Display the Electron browser instead of running headlessly + * Display the browser instead of running headlessly */ headed: boolean + /** + * Hide the browser instead of running headed + */ + headless: boolean /** * Specify your secret record key */ @@ -265,10 +261,6 @@ declare module 'cypress' { * Override default port */ port: number - /** - * Path to a specific project - */ - project: string /** * Whether to record the test run */ @@ -302,23 +294,15 @@ declare module 'cypress' { }) ``` */ - interface CypressOpenOptions { + interface CypressOpenOptions extends CypressCommonOptions { /** * Specify a filesystem path to a custom browser */ browser: string - /** - * Specify configuration - */ - config: Partial /** * Open Cypress in detached mode */ detached: boolean - /** - * Specify environment variables - */ - env: object /** * Run in global mode */ @@ -327,6 +311,28 @@ declare module 'cypress' { * Override default port */ port: number + } + + /** + * Options available for `cypress.open` and `cypress.run` + */ + interface CypressCommonOptions { + /** + * Specify configuration + */ + config: Partial + /** + * Path to the config file to be used. + * + * If `false` is passed, no config file will be used. + * + * @default "cypress.json" + */ + configFile: string | false + /** + * Specify environment variables + */ + env: object /** * Path to a specific project */ @@ -443,6 +449,7 @@ declare module 'cypress' { /** * Results returned by the test run. + * @see https://on.cypress.io/module-api */ interface CypressRunResult { startedTestsAt: dateTimeISO @@ -463,6 +470,29 @@ declare module 'cypress' { cypressVersion: string // TODO add resolved object to the configuration config: CypressConfiguration + /** + * If Cypress fails to run at all (for example, if there are no spec files to run), + * then it will set failures to 1 and will have actual error message in the + * property "message". Check this property before checking other properties. + * + * @type {number} + * @example + ``` + const result = await cypress.run() + if (result.failures) { + console.error(result.message) + process.exit(result.failures) + } + ``` + */ + failures?: number + /** + * If returned result has "failures" set, then this property gives + * the error message. + * + * @type {string} + */ + message?: string } /** diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index a9566af6fee9..2f4b04317298 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -1,4474 +1,4579 @@ -// Project: https://www.cypress.io -// GitHub: https://github.com/cypress-io/cypress -// Definitions by: Gert Hengeveld -// Mike Woudenberg -// Robbert van Markus -// Nicholas Boll -// TypeScript Version: 3.4 -// Updated by the Cypress team: https://www.cypress.io/about/ - -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// - -// "moment" types are with "node_modules/moment" -/// - -// load ambient declaration for "cypress" NPM module -// hmm, how to load it better? -/// - -// Cypress adds chai expect and assert to global -declare const expect: Chai.ExpectStatic -declare const assert: Chai.AssertStatic - -declare namespace Cypress { - type FileContents = string | any[] | object - type HistoryDirection = "back" | "forward" - type HttpMethod = string - type RequestBody = string | object - type ViewportOrientation = "portrait" | "landscape" - type PrevSubject = "optional" | "element" | "document" | "window" - - interface CommandOptions { - prevSubject: boolean | PrevSubject | PrevSubject[] - } - interface ObjectLike { - [key: string]: any - } - interface Auth { - username: string - password: string - } - - /** - * Describes a browser Cypress can control - */ - interface Browser { - name: "electron" | "chrome" | "canary" | "chromium" | "firefox" - displayName: "Electron" | "Chrome" | "Canary" | "Chromium" | "FireFox" - version: string - majorVersion: string - path: string - isHeaded: boolean - isHeadless: boolean - } - - interface LocalStorage { - /** - * Called internally to clear `localStorage` in two situations. - * - * 1. Before every test, this is called with no argument to clear all keys. - * 2. On `cy.clearLocalStorage(keys)` this is called with `keys` as an argument. - * - * You should not call this method directly to clear `localStorage`; instead, use `cy.clearLocalStorage(key)`. - * - * @see https://on.cypress.io/clearlocalstorage - */ - clear: (keys?: string[]) => void - } - - /** - * Several libraries are bundled with Cypress by default. - * - * @see https://on.cypress.io/api - */ - interface Cypress { - /** - * Lodash library - * - * @see https://on.cypress.io/_ - * @example - * Cypress._.keys(obj) - */ - _: _.LoDashStatic - /** - * jQuery library - * - * @see https://on.cypress.io/$ - * @example - * Cypress.$('p') - */ - $: JQueryStatic - /** - * Cypress automatically includes a Blob library and exposes it as Cypress.Blob. - * - * @see https://on.cypress.io/blob - * @see https://github.com/nolanlawson/blob-util - * @example - * Cypress.Blob.method() - */ - Blob: BlobUtil.BlobUtilStatic - /** - * Cypress automatically includes minimatch and exposes it as Cypress.minimatch. - * - * @see https://on.cypress.io/minimatch - */ - minimatch: typeof Minimatch.minimatch - /** - * Cypress automatically includes moment.js and exposes it as Cypress.moment. - * - * @see https://on.cypress.io/moment - * @see http://momentjs.com/ - * @example - * const todaysDate = Cypress.moment().format("MMM DD, YYYY") - */ - moment: Moment.MomentStatic - /** - * Cypress automatically includes Bluebird and exposes it as Cypress.Promise. - * - * @see https://on.cypress.io/promise - * @see https://github.com/petkaantonov/bluebird - * @example - * new Cypress.Promise((resolve, reject) => { ... }) - */ - Promise: Bluebird.BluebirdStatic - /** - * Cypress version string. i.e. "1.1.2" - * @see https://on.cypress.io/version - * @example - ``` - expect(Cypress.version).to.be.a('string') - if (Cypress.version === '1.2.0') { - // test something specific - } - ``` - */ - version: string - - /** - * OS platform name, from Node `os.platform()` - * - * @see https://nodejs.org/api/os.html#os_os_platform - * @example - * Cypress.platform // "darwin" - */ - platform: string - - /** - * CPU architecture, from Node `os.arch()` - * - * @see https://nodejs.org/api/os.html#os_os_arch - * @example - * Cypress.arch // "x64" - */ - arch: string - - /** - * Currently executing spec file. - * @example - ``` - Cypress.spec - // { - // name: "config_passing_spec.coffee", - // relative: "cypress/integration/config_passing_spec.coffee", - // absolute: "/users/smith/projects/web/cypress/integration/config_passing_spec.coffee" - // } - ``` - */ - spec: { - name: string // "config_passing_spec.coffee" - relative: string | null // "cypress/integration/config_passing_spec.coffee" - absolute: string | null - } - - /** - * Information about the browser currently running the tests - */ - browser: Browser - - /** - * Internal class for LocalStorage management. - */ - LocalStorage: LocalStorage - - /** - * Returns all configuration objects. - * @see https://on.cypress.io/config - * @example - ``` - Cypress.config() - // {defaultCommandTimeout: 10000, pageLoadTimeout: 30000, ...} - ``` - */ - config(): ConfigOptions - /** - * Returns one configuration value. - * @see https://on.cypress.io/config - * @example - ``` - Cypress.config('pageLoadTimeout') - // 60000 - ``` - */ - config(key: K): ConfigOptions[K] - /** - * Sets one configuration value. - * @see https://on.cypress.io/config - * @example - ``` - Cypress.config('viewportWidth', 800) - ``` - */ - config(key: K, value: ConfigOptions[K]): void - /** - * Sets multiple configuration values at once. - * @see https://on.cypress.io/config - * @example - ``` - Cypress.config({ - defaultCommandTimeout: 10000, - viewportHeight: 900 - }) - ``` - */ - config(Object: Partial): void - - // no real way to type without generics - /** - * Returns all environment variables set with CYPRESS_ prefix or in "env" object in "cypress.json" - * - * @see https://on.cypress.io/env - */ - env(): ObjectLike - /** - * Returns specific environment variable or undefined - * @see https://on.cypress.io/env - * @example - * // cypress.json - * { "env": { "foo": "bar" } } - * Cypress.env("foo") // => bar - */ - env(key: string): any - /** - * Set value for a variable. - * Any value you change will be permanently changed for the remainder of your tests. - * @see https://on.cypress.io/env - * @example - * Cypress.env("host", "http://server.dev.local") - */ - env(key: string, value: any): void - /** - * Set values for multiple variables at once. Values are merged with existing values. - * @see https://on.cypress.io/env - * @example - * Cypress.env({ host: "http://server.dev.local", foo: "foo" }) - */ - env(object: ObjectLike): void - - /** - * Checks if a variable is a valid instance of `cy` or a `cy` chainable. - * - * @see https://on.cypress.io/iscy - * @example - * Cypress.isCy(cy) // => true - */ - isCy(obj: Chainable): obj is Chainable - isCy(obj: any): obj is Chainable - - /** - * Internal options for "cy.log" used in custom commands. - * - * @see https://on.cypress.io/cypress-log - */ - log(options: Partial): Log - - /** - * @see https://on.cypress.io/api/commands - */ - Commands: { - add(name: string, fn: (...args: any[]) => void): void - add(name: string, options: CommandOptions, fn: (...args: any[]) => void): void - overwrite(name: string, fn: (...args: any[]) => void): void - overwrite(name: string, options: CommandOptions, fn: (...args: any[]) => void): void - } - - /** - * @see https://on.cypress.io/cookies - */ - Cookies: { - debug(enabled: boolean, options?: Partial): void - preserveOnce(...names: string[]): void - defaults(options: Partial): void - } - - /** - * @see https://on.cypress.io/dom - */ - dom: { - isHidden(element: JQuery | HTMLElement): boolean - } - - /** - * @see https://on.cypress.io/api/api-server - */ - Server: { - defaults(options: Partial): void - } - - /** - * @see https://on.cypress.io/api/screenshot-api - */ - Screenshot: { - defaults(options: Partial): void - } - - /** - * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - on: Actions - - /** - * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - once: Actions - - /** - * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - off: Actions - } - - /** - * Chainable interface for non-array Subjects - */ - interface Chainable { - /** - * Create an assertion. Assertions are automatically retried until they pass or time out. - * - * @alias should - * @see https://on.cypress.io/and - */ - and: Chainer - - /** - * Assign an alias for later use. Reference the alias later within a - * [cy.get()](https://on.cypress.io/get) or - * [cy.wait()](https://on.cypress.io/wait) command with a `@` prefix. - * You can alias DOM elements, routes, stubs and spies. - * - * @see https://on.cypress.io/as - * @see https://on.cypress.io/variables-and-aliases - * @see https://on.cypress.io/get - * @example - ``` - // Get the aliased ‘todos’ elements - cy.get('ul#todos').as('todos') - //...hack hack hack... - // later retrieve the todos - cy.get('@todos') - ``` - */ - as(alias: string): Chainable - - /** - * Blur a focused element. This element must currently be in focus. - * If you want to ensure an element is focused before blurring, - * try using .focus() before .blur(). - * - * @see https://on.cypress.io/blur - */ - blur(options?: Partial): Chainable - - /** - * Check checkbox(es) or radio(s). This element must be an `` with type `checkbox` or `radio`. - * - * @see https://on.cypress.io/check - * @example - * // Check checkbox element - * cy.get('[type="checkbox"]').check() - * // Check first radio element - * cy.get('[type="radio"]').first().check() - */ - check(options?: Partial): Chainable - /** - * Check checkbox(es) or radio(s). This element must be an `` with type `checkbox` or `radio`. - * - * @see https://on.cypress.io/check - * @example - * // Select the radio with the value of ‘US’ - * cy.get('[type="radio"]').check('US') - * // Check the checkboxes with the values ‘ga’ and ‘ca’ - * cy.get('[type="checkbox"]').check(['ga', 'ca']) - */ - check(value: string | string[], options?: Partial): Chainable - - /** - * Get the children of each DOM element within a set of DOM elements. - * - * @see https://on.cypress.io/children - */ - children(options?: Partial): Chainable> - children(selector: K, options?: Partial): Chainable> - children(selector: string, options?: Partial): Chainable> - - /** - * Clear the value of an `input` or `textarea`. - * An alias for `.type({selectall}{backspace})` - * - * @see https://on.cypress.io/clear - */ - clear(options?: Partial): Chainable - - /** - * Clear a specific browser cookie. - * Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn’t need to use this command unless you’re using it to clear a specific cookie inside a single test. - * - * @see https://on.cypress.io/clearcookie - */ - clearCookie(name: string, options?: Partial): Chainable - - /** - * Clear all browser cookies. - * Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn’t need to use this command unless you’re using it to clear a specific cookie inside a single test. - * - * @see https://on.cypress.io/clearcookies - */ - clearCookies(options?: Partial): Chainable - - /** - * Clear data in local storage. - * Cypress automatically runs this command before each test to prevent state from being - * shared across tests. You shouldn’t need to use this command unless you’re using it - * to clear localStorage inside a single test. Yields `localStorage` object. - * - * @see https://on.cypress.io/clearlocalstorage - * @param {string} [key] - name of a particular item to remove (optional). - * @example - ``` - // removes all local storage keys - cy.clearLocalStorage() - .should(ls => { - expect(ls.getItem('prop1')).to.be.null - }) - // removes item "todos" - cy.clearLocalStorage("todos") - ``` - */ - clearLocalStorage(key?: string): Chainable - /** - * Clear keys in local storage that match given regular expression. - * - * @see https://on.cypress.io/clearlocalstorage - * @param {RegExp} re - regular expression to match. - * @example - ``` - // Clear all local storage matching /app-/ - cy.clearLocalStorage(/app-/) - ``` - */ - clearLocalStorage(re: RegExp): Chainable - - /** - * Click a DOM element. - * - * @see https://on.cypress.io/click - * @example - * cy.get('button').click() // Click on button - * cy.focused().click() // Click on el with focus - * cy.contains('Welcome').click() // Click on first el containing 'Welcome' - */ - click(options?: Partial): Chainable - /** - * Click a DOM element at specific corner / side. - * - * @param {String} position - The position where the click should be issued. - * The `center` position is the default position. - * @see https://on.cypress.io/click - * @example - * cy.get('button').click('topRight') - */ - click(position: string, options?: Partial): Chainable - /** - * Click a DOM element at specific coordinates - * - * @param {number} x The distance in pixels from the element’s left to issue the click. - * @param {number} y The distance in pixels from the element’s top to issue the click. - * @see https://on.cypress.io/click - * @example - ``` - // The click below will be issued inside of the element - // (15px from the left and 40px from the top). - cy.get('button').click(15, 40) - ``` - */ - click(x: number, y: number, options?: Partial): Chainable - - /** - * `cy.clock()` overrides native global functions related to time allowing them to be controlled - * synchronously via [cy.tick()](https://on.cypress.io/tick) or the yielded clock object. - * This includes controlling: - * * `setTimeout` - * * `clearTimeout` - * * `setInterval` - * * `clearInterval` - * * `Date` Objects - * - * The clock starts at the unix epoch (timestamp of 0). - * This means that when you instantiate new Date in your application, - * it will have a time of January 1st, 1970. - * - * @see https://on.cypress.io/clock - */ - clock(): Chainable - /** - * Mocks global clock and sets current timestamp to the given value. - * Overrides all functions that deal with time. - * - * @see https://on.cypress.io/clock - * @example - * // your app code - * $('#date').text(new Date().toJSON()) - * // from spec file - * const now = new Date(2017, 3, 14).getTime() // March 14, 2017 timestamp - * cy.clock(now) - * cy.visit('/index.html') - * cy.get('#date').contains('2017-03-14') - */ - clock(now: number, options?: Loggable): Chainable - /** - * Mocks global clock but only overrides specific functions. - * - * @see https://on.cypress.io/clock - * @example - * // keep current date but override "setTimeout" and "clearTimeout" - * cy.clock(null, ['setTimeout', 'clearTimeout']) - */ - clock(now: number, functions?: Array<'setTimeout' | 'clearTimeout' | 'setInterval' | 'clearInterval' | 'Date'>, options?: Loggable): Chainable - /** - * Mocks global clock and all functions. - * - * @see https://on.cypress.io/clock - * @example - * // mock clock but do not log this command - * cy.clock({ log: false }) - */ - clock(options: Loggable): Chainable - - /** - * Get the first DOM element that matches the selector (whether it be itself or one of its ancestors). - * - * @see https://on.cypress.io/closest - */ - closest(selector: K, options?: Partial): Chainable> - closest(selector: string, options?: Partial): Chainable> - - /** - * Get the DOM element containing the text. - * DOM elements can contain more than the desired text and still match. - * Additionally, Cypress prefers some DOM elements over the deepest element found. - * - * @see https://on.cypress.io/contains - * @example - * // Yield el in .nav containing 'About' - * cy.get('.nav').contains('About') - * // Yield first el in document containing 'Hello' - * cy.contains('Hello') - * // you can use regular expression - * cy.contains(/^b\w+/) - * // yields
    ...
- * cy.contains('ul', 'apples') - * // tries to find the given text for up to 1 second - * cy.contains('my text to find', {timeout: 1000}) - */ - contains(content: string | number | RegExp, options?: Partial): Chainable - /** - * Get the child DOM element that contains given text. - * - * @see https://on.cypress.io/contains - * @example - * // Yield el in .nav containing 'About' - * cy.get('.nav').contains('About') - */ - contains(content: string | number | RegExp): Chainable> - /** - * Get the DOM element with name "selector" containing the text or regular expression. - * - * @see https://on.cypress.io/contains - * @example - * // yields
    ...
- * cy.contains('ul', 'apples') - */ - contains(selector: K, text: string | number | RegExp, options?: Partial): Chainable> - /** - * Get the DOM element using CSS "selector" containing the text or regular expression. - * - * @see https://on.cypress.io/contains - * @example - * // yields <... class="foo">... apples ... - * cy.contains('.foo', 'apples') - */ - contains(selector: string, text: string | number | RegExp, options?: Partial): Chainable> - - /** - * Double-click a DOM element. - * - * @see https://on.cypress.io/dblclick - */ - dblclick(options?: Partial): Chainable - - /** - * Set a debugger and log what the previous command yields. - * - * @see https://on.cypress.io/debug - */ - debug(options?: Partial): Chainable - - /** - * Get the window.document of the page that is currently active. - * - * @see https://on.cypress.io/document - * @example - * cy.document() - * .its('contentType') - * .should('eq', 'text/html') - */ - document(options?: Partial): Chainable - - /** - * Iterate through an array like structure (arrays or objects with a length property). - * - * @see https://on.cypress.io/each - */ - each(fn: (element: JQuery, index: number, $list: E[]) => void): Chainable> // Can't properly infer type without breaking down Chainable - each(fn: (item: any, index: number, $list: any[]) => void): Chainable - - /** - * End a chain of commands - * - * @see https://on.cypress.io/end - */ - end(): Chainable - - /** - * Get A DOM element at a specific index in an array of elements. - * - * @see https://on.cypress.io/eq - * @param {Number} index A number indicating the index to find the element at within an array of elements. A negative number counts index from the end of the list. - * @example - * cy.get('tbody>tr').eq(0) // Yield first 'tr' in 'tbody' - * cy.get('ul>li').eq('4') // Yield fifth 'li' in 'ul' - * cy.get('li').eq(-2) // Yields second from last 'li' element - */ - eq(index: number, options?: Partial): Chainable> - - /** - * Execute a system command. - * @see https://on.cypress.io/exec - */ - exec(command: string, options?: Partial): Chainable - - /** - * Get the DOM elements that match a specific selector. Opposite of `.not()` - * - * @see https://on.cypress.io/filter - */ - filter(selector: K, options?: Partial): Chainable> // automatically returns the correct HTMLElement type - /** - * Get the DOM elements that match a specific selector. Opposite of `.not()` - * - * @see https://on.cypress.io/filter - */ - filter(selector: string, options?: Partial): Chainable> - /** - * Get the DOM elements that match a specific selector. Opposite of `.not()` - * - * @see https://on.cypress.io/filter - */ - filter(fn: (index: number, element: E) => boolean, options?: Partial): Chainable> - - /** - * Get the descendent DOM elements of a specific selector. - * - * @see https://on.cypress.io/find - * @example - * cy.get('.article').find('footer') // Yield 'footer' within '.article' - */ - find(selector: K, options?: Partial): Chainable> - /** - * Finds the descendent DOM elements with the given selector. - * - * @see https://on.cypress.io/find - * @example - * // Find the li’s within the nav - * cy.get('.left-nav>.nav').find('>li') - */ - find(selector: string, options?: Partial): Chainable> - - /** - * Get the first DOM element within a set of DOM elements. - * - * @see https://on.cypress.io/first - */ - first(options?: Partial): Chainable - - /** - * Load a fixed set of data located in a file. - * - * @see https://on.cypress.io/fixture - */ - fixture(path: string, options?: Partial): Chainable // no log? - /** - * Load a fixed set of data located in a file with given encoding. - * - * @see https://on.cypress.io/fixture - */ - fixture(path: string, encoding: Encodings, options?: Partial): Chainable // no log? - - /** - * Focus on a DOM element. - * - * @see https://on.cypress.io/focus - * @example - * cy.get('input').first().focus() // Focus on the first input - */ - focus(options?: Partial): Chainable - - /** - * Get the DOM element that is currently focused. - * - * @see https://on.cypress.io/focused - * @example - * // Get the element that is focused - * cy.focused().then(function($el) { - * // do something with $el - * }) - * // Blur the element with focus - * cy.focused().blur() - * // Make an assertion on the focused element - * cy.focused().should('have.attr', 'name', 'username') - */ - focused(options?: Partial): Chainable - - /** - * Get one or more DOM elements by node name: input, button, etc. - * @see https://on.cypress.io/get - * @example - * cy.get('input').should('be.disabled') - * cy.get('button').should('be.visible') - */ - get(selector: K, options?: Partial): Chainable> - /** - * Get one or more DOM elements by selector. - * The querying behavior of this command matches exactly how $(…) works in jQuery. - * @see https://on.cypress.io/get - * @example - * cy.get('.list>li') // Yield the
  • 's in <.list> - * cy.get('ul li:first').should('have.class', 'active') - * cy.get('.dropdown-menu').click() - */ - get(selector: string, options?: Partial): Chainable> - /** - * Get one or more DOM elements by alias. - * @see https://on.cypress.io/get#Alias - * @example - * // Get the aliased ‘todos’ elements - * cy.get('ul#todos').as('todos') - * //...hack hack hack... - * //later retrieve the todos - * cy.get('@todos') - */ - get(alias: string, options?: Partial): Chainable - - /** - * Get a browser cookie by its name. - * - * @see https://on.cypress.io/getcookie - */ - getCookie(name: string, options?: Partial): Chainable - - /** - * Get all of the browser cookies. - * - * @see https://on.cypress.io/getcookies - */ - getCookies(options?: Partial): Chainable - - /** - * Navigate back or forward to the previous or next URL in the browser’s history. - * - * @see https://on.cypress.io/go - */ - go(direction: HistoryDirection | number, options?: Partial): Chainable - - /** - * Get the current URL hash of the page that is currently active. - * - * @see https://on.cypress.io/hash - */ - hash(options?: Partial): Chainable - - /** - * Invoke a function on the previously yielded subject, yielding the result. - * - * @see https://on.cypress.io/invoke - * @example - * // Invoke the 'add' function, passing in parameters - * cy.wrap({ add: (a, b) => a + b }).invoke('add', 1, 2).should('eq', 3) - * // DOM elements are automatically wrapped with jQuery, - * // so jQuery functions (including 3rd party plugins) can be invoked - * cy.get('.modal').invoke('show').should('be.visible') // jQuery's 'show' functions - */ - invoke< - TActualSubject extends (Subject extends Node | Node[] ? JQuery : Subject), - TName extends FunctionKeys, - TArgs extends OverloadedParameters, - TReturn extends OverloadedReturnType, - >(functionName: TName, ...args: TArgs): Chainable - - /** - * Get a property’s value on the previously yielded subject. - * - * @see https://on.cypress.io/its - * @example - * // Get the 'width' property - * cy.wrap({width: '50'}).its('width') - * // Drill into nested properties by using dot notation - * cy.wrap({foo: {bar: {baz: 1}}}).its('foo.bar.baz') - */ - its(propertyName: K): Chainable - - /** - * Get the last DOM element within a set of DOM elements. - * - * @see https://on.cypress.io/last - */ - last(options?: Partial): Chainable> - - /** - * Get the global `window.location` object of the page that is currently active. - * - * @see https://on.cypress.io/location - * @example - * cy.location() // Get location object - */ - location(options?: Partial): Chainable - /** - * Get a part of the global `window.location` object of the page that is currently active. - * - * @see https://on.cypress.io/location - * @example - * cy.location('host') // Get the host of the location object - * cy.location('port') // Get the port of the location object - * // Assert on the href of the location - * cy.location('href').should('contain', '/tag/tutorials') - */ - location(key: string, options?: Partial): Chainable - - /** - * Print a message to the Cypress Command Log. - * - * @see https://on.cypress.io/log - */ - log(message: string, ...args: any[]): Chainable - - /** - * Get the immediately following sibling of each DOM element within a set of DOM elements. - * - * @see https://on.cypress.io/next - */ - next(selector: K, options?: Partial): Chainable> - /** - * Get the immediately following sibling of each DOM element within a set of DOM elements. - * - * @see https://on.cypress.io/next - * @example - * cy.get('nav a:first').next() - */ - next(options?: Partial): Chainable> - /** - * Get the immediately following sibling of each DOM element within a set of DOM elements that match selector - * - * @see https://on.cypress.io/next - * @example - * cy.get('nav a:first').next('.menu-item) - */ - next(selector: string, options?: Partial): Chainable> - - /** - * Get all following siblings of each DOM element in a set of matched DOM elements. - * - * @see https://on.cypress.io/nextall - */ - nextAll(selector: K, options?: Partial): Chainable> - /** - * Get all following siblings of each DOM element in a set of matched DOM elements. - * - * @see https://on.cypress.io/nextall - */ - nextAll(options?: Partial): Chainable> - /** - * Get all following siblings of each DOM element in a set of matched DOM elements. - * - * @see https://on.cypress.io/nextall - */ - nextAll(selector: string, options?: Partial): Chainable> - - /** - * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * - * @see https://on.cypress.io/nextuntil - */ - nextUntil(selector: K, options?: Partial): Chainable> - /** - * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * - * @see https://on.cypress.io/nextuntil - */ - nextUntil(options?: Partial): Chainable> - /** - * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * - * @see https://on.cypress.io/nextuntil - */ - nextUntil(selector: string, options?: Partial): Chainable> - - /** - * Filter DOM element(s) from a set of DOM elements. Opposite of `.filter()` - * - * @see https://on.cypress.io/not - */ - not(selector: string, options?: Partial): Chainable - - /** - * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - on: Actions - - /** - * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - once: Actions - - /** - * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. - * @see https://on.cypress.io/catalog-of-events#App-Events - */ - off: Actions - - /** - * Get the parent DOM element of a set of DOM elements. - * - * @see https://on.cypress.io/parent - */ - parent(selector: K, options?: Partial): Chainable> - /** - * Get the parent DOM element of a set of DOM elements. - * - * @see https://on.cypress.io/parent - */ - parent(options?: Partial): Chainable> - /** - * Get the parent DOM element of a set of DOM elements. - * - * @see https://on.cypress.io/parent - */ - parent(selector: string, options?: Partial): Chainable> - - /** - * Get the parent DOM elements of a set of DOM elements. - * - * @see https://on.cypress.io/parents - */ - parents(selector: K, options?: Partial): Chainable> - /** - * Get the parent DOM elements of a set of DOM elements. - * - * @see https://on.cypress.io/parents - */ - parents(options?: Partial): Chainable> - /** - * Get the parent DOM elements of a set of DOM elements. - * - * @see https://on.cypress.io/parents - */ - parents(selector: string, options?: Partial): Chainable> - - /** - * Get all ancestors of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * - * @see https://on.cypress.io/parentsuntil - */ - parentsUntil(selector: K, filter?: string, options?: Partial): Chainable> - /** - * Get all ancestors of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * - * @see https://on.cypress.io/parentsuntil - */ - parentsUntil(selector: string, filter?: string, options?: Partial): Chainable> - /** - * Get all ancestors of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * - * @see https://on.cypress.io/parentsuntil - */ - parentsUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> - - /** - * Stop cy commands from running and allow interaction with the application under test. You can then “resume” running all commands or choose to step through the “next” commands from the Command Log. - * This does not set a `debugger` in your code, unlike `.debug()` - * - * @see https://on.cypress.io/pause - */ - pause(options?: Partial): Chainable - - /** - * Get the immediately preceding sibling of each element in a set of the elements. - * - * @example - * cy.get('nav').prev('a') // Yield previous 'a' - * @see https://on.cypress.io/prev - */ - prev(selector: K, options?: Partial): Chainable> - /** - * Get the immediately preceding sibling of each element in a set of the elements. - * - * @example - * cy.get('li').prev() // Yield previous 'li' - * @see https://on.cypress.io/prev - */ - prev(options?: Partial): Chainable> - /** - * Get the immediately preceding sibling of each element in a set of the elements that match selector. - * - * @example - * cy.get('nav').prev('.menu-item') // Yield previous '.menu-item' - * @see https://on.cypress.io/prev - */ - prev(selector: string, options?: Partial): Chainable> - - /** - * Get all previous siblings of each DOM element in a set of matched DOM elements. - * > The querying behavior of this command matches exactly how [.prevAll()](http://api.jquery.com/prevAll) works in jQuery. - * - * @see https://on.cypress.io/prevall - */ - prevAll(selector: K, options?: Partial): Chainable> - /** - * Get all previous siblings of each DOM element in a set of matched DOM elements. - * > The querying behavior of this command matches exactly how [.prevAll()](http://api.jquery.com/prevAll) works in jQuery. - * - * @see https://on.cypress.io/prevall - */ - prevAll(options?: Partial): Chainable> - /** - * Get all previous siblings of each DOM element in a set of matched DOM elements. - * > The querying behavior of this command matches exactly how [.prevAll()](http://api.jquery.com/prevAll) works in jQuery. - * - * @see https://on.cypress.io/prevall - */ - prevAll(selector: string, options?: Partial): Chainable> - - /** - * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. - * - * @see https://on.cypress.io/prevall - */ - prevUntil(selector: K, filter?: string, options?: Partial): Chainable> - /** - * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. - * - * @see https://on.cypress.io/prevall - */ - prevUntil(selector: string, filter?: string, options?: Partial): Chainable> - /** - * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. - * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. - * - * @see https://on.cypress.io/prevall - */ - prevUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> - - /** - * Read a file and yield its contents. - * - * @see https://on.cypress.io/readfile - */ - readFile(filePath: string, options?: Partial): Chainable - /** - * Read a file with given encoding and yield its contents. - * - * @see https://on.cypress.io/readfile - * @example - * cy.readFile('foo.json', 'utf8') - */ - readFile(filePath: string, encoding: Encodings, options?: Partial): Chainable - - /** - * Reload the page. - * - * @see https://on.cypress.io/reload - * @example - * cy.reload() - */ - reload(options?: Partial): Chainable - /** - * Reload the page without cache - * - * @see https://on.cypress.io/reload - * @param {Boolean} forceReload Whether to reload the current page without using the cache. true forces the reload without cache. - * @example - * // Reload the page without using the cache - * cy.visit('http://localhost:3000/admin') - * cy.reload(true) - */ - reload(forceReload: boolean): Chainable - - /** - * Make an HTTP GET request. - * - * @see https://on.cypress.io/request - * @example - * cy.request('http://dev.local/seed') - */ - request(url: string, body?: RequestBody): Chainable - /** - * Make an HTTP request with specific method. - * - * @see https://on.cypress.io/request - * @example - * cy.request('POST', 'http://localhost:8888/users', {name: 'Jane'}) - */ - request(method: HttpMethod, url: string, body?: RequestBody): Chainable - /** - * Make an HTTP request with specific behavior. - * - * @see https://on.cypress.io/request - * @example - * cy.request({ - * url: '/dashboard', - * followRedirect: false // turn off following redirects - * }) - */ - request(options: Partial): Chainable - - /** - * Get the root DOM element. - * The root element yielded is `` by default. - * However, when calling `.root()` from a `.within()` command, - * the root element will point to the element you are “within”. - * - * @see https://on.cypress.io/root - */ - root(options?: Partial): Chainable> // can't do better typing unless we ignore the `.within()` case - - /** - * Use `cy.route()` to manage the behavior of network requests. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route('https://localhost:7777/users', [{id: 1, name: 'Pat'}]) - */ - route(url: string | RegExp, response?: string | object): Chainable - /** - * Spy or stub request with specific method and url. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * // spy on POST /todos requests - * cy.route('POST', '/todos').as('add-todo') - */ - route(method: string, url: string | RegExp, response?: string | object): Chainable - /** - * Set a route by returning an object literal from a callback function. - * Functions that return a Promise will automatically be awaited. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route(() => { - * // your logic here - * // return an appropriate routing object here - * return { - * method: 'POST', - * url: '/comments', - * response: this.commentsFixture - * } - * }) - */ - route(fn: () => RouteOptions): Chainable - /** - * Spy or stub a given route. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route({ - * method: 'DELETE', - * url: '/users', - * status: 412, - * delay: 1000 - * // and other options, see documentation - * }) - */ - route(options: Partial): Chainable - - /** - * Take a screenshot of the application under test and the Cypress Command Log. - * - * @see https://on.cypress.io/screenshot - * @example - * cy.screenshot() - * cy.get(".post").screenshot() - */ - screenshot(options?: Partial): Chainable - /** - * Take a screenshot of the application under test and the Cypress Command Log and save under given filename. - * - * @see https://on.cypress.io/screenshot - * @example - * cy.get(".post").screenshot("post-element") - */ - screenshot(fileName: string, options?: Partial): Chainable - - /** - * Scroll an element into view. - * - * @see https://on.cypress.io/scrollintoview - */ - scrollIntoView(options?: Partial): Chainable - - /** - * Scroll to a specific position. - * - * @see https://on.cypress.io/scrollto - */ - scrollTo(position: PositionType, options?: Partial): Chainable - /** - * Scroll to a specific X,Y position. - * - * @see https://on.cypress.io/scrollto - */ - scrollTo(x: number | string, y: number | string, options?: Partial): Chainable - - /** - * Select an `