diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 000000000000..4168a3cdeed9 --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,2 @@ +# Always validate the PR title, and ignore the commits +titleOnly: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e27132ea97d..48158f793f01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -117,7 +117,7 @@ Some opened issue are questions, not bug reports or feature requests. Issues are - Explain that issues in our GitHub repo are reserved for potential bugs or feature requests and that the issue will be closed since it appears to be neither a bug nor a feature request. - Guide them to existing resources where their questions can be asked like our [community chat](https://on.cypress.io/chat), our [documentation](https://docs.cypress.io), or [Stack Overflow](https://stackoverflow.com/questions/tagged/cypress). -- Cypress offers support via email when signing up for any of our our [paid plans](https://www.cypress.io/pricing/), so remind them that this is an option. Cypress also offers screen sharing and workshops with our [premium support options](https://www.cypress.io/support/) if they would like something higher-touch. +- Cypress offers support via email when signing up for any of our our [paid plans](https://www.cypress.io/pricing/), so remind them that this is an option if they already have a paid account. - Add the `type: question` label to the issue. - Close the issue. @@ -518,6 +518,7 @@ The repository is setup with two main (protected) branches. - 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](./.github/ISSUE_TEMPLATE.md)**. - PR's can be opened before all the work is finished. In fact we encourage this! Please create a [Draft Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) if your PR is not ready for review. [Mark the PR as **Ready for Review**](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review) when you're ready for a Cypress team member to review the PR. +- Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format as defined [here](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type). For example, if your PR is fixing a bug, you should prefix the PR title with `fix:`. - Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. 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) diff --git a/DEPLOY.md b/DEPLOY.md index ce44fc4fe85d..7c29be7a8fd7 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,25 +1,24 @@ # 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 the `cypress` NPM organization. +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 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. +new official version of the binary and `cypress` npm package. ## Building Locally -### Building the NPM package +### Building the npm package > :warning: Note: The steps in this section are automated in CI, and you should not need to do them yourself when publishing. -Building a new NPM package is very quick. +Building a new npm package is very quick. - Increment the version in the root `package.json` - `yarn build --scope cypress` The steps above: -- Build the `cypress` NPM package +- Build the `cypress` npm package - Transpile the code into ES5 to be compatible with the common Node versions - Put the result into the [`cli/build`](./cli/build) folder. @@ -27,7 +26,7 @@ The steps above: > :warning: Note: The steps in this section are automated in CI, and you should not need to do them yourself when publishing. -The NPM package requires a corresponding binary of the same version. In production, it will try to retrieve the binary from the Cypress CDN if it is not cached locally. +The npm package requires a corresponding binary of the same version. In production, it will try to retrieve the binary from the Cypress CDN if it is not cached locally. You can build the Cypress binary locally by running `yarn binary-build`. You can use Docker to build a Linux binary by running `yarn binary-build-linux`. @@ -35,38 +34,15 @@ You can build the Cypress binary locally by running `yarn binary-build`. You can ## Publishing -### 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`: - -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 `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 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 the `develop` branch for all test projects are reliably passing with the new changes, publishing can proceed. +### Prerequisites -### Steps to Publish a New Version - -In the following instructions, "X.Y.Z" is used to denote the version of Cypress being published. - -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 you have the correct permissions set up before proceeding: +- Ensure you have the following permissions set up: - An AWS account with permission to create AWS access keys for the Cypress CDN. - Permissions for your npm account to publish the `cypress` package. - Permissions to modify environment variables for `cypress` on CircleCI and AppVeyor. - Permissions to update releases in ZenHub. -2. Make sure that you have the correct environment variables set up before proceeding: + +- Set up the following environment variables: - Cypress AWS access key and secret in `aws_credentials_json`, which looks like this: ```text aws_credentials_json={"bucket":"cdn.cypress.io","folder":"desktop","key":"...","secret":"..."} @@ -83,27 +59,57 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress ``` - The `cypress-bot` GitHub app credentials are also needed. Ask another team member who has done a deploy for those. - Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations. -3. Use the `move-binaries` script to move the binaries for `` from `beta` to the `desktop` folder - for `` + +### 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`: + +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 `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 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 the `develop` branch for all test projects are reliably passing with the new changes, publishing can proceed. + +### Steps to Publish a New Version + +In the following instructions, "X.Y.Z" is used to denote the version of Cypress being published. + +1. If there is a new [`cypress-example-kitchensink`](https://github.com/cypress-io/cypress-example-kitchensink/releases) version, update the corresponding dependency in [`packages/example`](./packages/example) to that new version. + +2. Use the `move-binaries` script to move the binaries for `` from `beta` to the `desktop` folder for `` ```shell yarn move-binaries --sha --version ``` -4. Publish the new NPM package under the `dev` tag, using your personal NPM account. + +3. Publish the new npm package under the `dev` tag, using your personal npm account. - To find the link to the package file `cypress.tgz`: 1. In GitHub, go to the latest commit (the one whose sha you used in the last step). ![commit-link](https://user-images.githubusercontent.com/1157043/80608728-33fe6100-8a05-11ea-8b53-375303757b67.png) 2. Scroll down past the changes to the comments. The first comment should be a `cypress-bot` comment that includes a line beginning `npm install ...`. Grab the `https://cdn.../npm/X.Y.Z//cypress.tgz` link. ![cdn-tgz-link](https://user-images.githubusercontent.com/1157043/80608736-3791e800-8a05-11ea-8d75-e4f80128e857.png) - - Publish to the NPM registry straight from the URL: + - Publish to the npm registry straight from the URL: ```shell npm publish https://cdn.../npm/X.Y.Z//cypress.tgz --tag dev ``` -5. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions). `latest` should still point to the previous version. Example output: + +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). `latest` should still point to the previous version. Example output: ```shell dist-tags: dev: 3.4.0 latest: 3.3.2 ``` -6. Test `cypress@X.Y.Z` to make sure everything is working. + +5. Test `cypress@X.Y.Z` to make sure everything is working. - Install the new version: `npm install -g cypress@X.Y.Z` - Run a quick, manual smoke test: - `cypress open` @@ -114,7 +120,8 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress node scripts/test-other-projects.js --npm cypress@X.Y.Z --binary X.Y.Z ``` - Test the new version of Cypress against the Cypress dashboard repo. -7. Deploy the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). + +6. Deploy the release-specific documentation and changelog in [cypress-documentation](https://github.com/cypress-io/cypress-documentation). - If there is not already a release-specific PR open, create one. You can use [`release-automations`](https://github.com/cypress-io/release-automations)'s `issues-in-release` tool to generate a starting point for the changelog, based off of ZenHub: ``` cd packages/issues-in-release @@ -123,37 +130,44 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress - Ensure the changelog is up-to-date and has the correct date. - Merge any release-specific documentation changes into the main release PR. - Merging this PR into `develop` will deploy to `docs-staging` and then a PR will be automatically created against `master`. It will be automatically merged after it passes and will deploy to production. -8. Make the new NPM version the "latest" version by updating the dist-tag `latest` to point to the new version: + +7. Make the new npm version the "latest" version by updating the dist-tag `latest` to point to the new version: ```shell npm dist-tag add cypress@X.Y.Z ``` -9. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json) and set the next CI version: + +8. Run `binary-release` to update the [download server's manifest](https://download.cypress.io/desktop.json) and set the next CI version: ```shell yarn run binary-release --version X.Y.Z ``` - > Note: Currently, there is an [issue setting the next CI version](https://github.com/cypress-io/cypress/issues/7176) that will cause this command to fail after setting the download manifest. You will need to manually update NEXT_DEV_VERSION by logging in to CircleCI and AppVeyor. -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): + > Note: Currently, there is an [issue setting the next CI version](https://github.com/cypress-io/cypress/issues/7176) that will cause this command to fail after setting the download manifest. You will need to manually update NEXT_DEV_VERSION by logging in to CircleCI and AppVeyor. This is noted in Step 16 below. + +9. 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). + +10. 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). + +11. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release): - Close the current release in ZenHub. - Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date. - Move all issues that are still open from the current release to the appropriate future release. -13. Bump `version` in [`package.json`](package.json), commit it to `develop`, and tag it with version: - ```shell - # commit and tag at the same time - git commit -a vX.Y.Z -m "release X.Y.Z [skip ci]" - # OR if you don't tag it with the commit, you can tag it after - git commit -m "release X.Y.Z [skip ci]" - git log --pretty=oneline # copy sha of the previous commit +12. Bump `version` in [`package.json`](package.json), commit it to `develop`, tag it with the version, and push the tag up: + ```shell + git commit -am "release X.Y.Z [skip ci]" + git log --pretty=oneline + # copy sha of the previous commit git tag -a vX.Y.Z + git push origin vX.Y.Z ``` -14. Push the tag up: +13. Merge `develop` into `master` and push both branches up. ```shell - git push origin vX.Y.Z + git push origin develop + git checkout master + git merge develop + git push origin master ``` -15. Merge `develop` into `master` and push that branch up. -16. Inside of [cypress-io/release-automations][release-automations]: + +14. Inside of [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`: ```shell cd packages/set-releases && npm run release-log -- --version X.Y.Z @@ -162,9 +176,12 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress ```shell cd packages/issues-in-release && npm run do:comment -- --release X.Y.Z ``` -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. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects: + +15. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version. + +16. 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). + +17. Update example projects to the new version. For most projects, you can go to the Renovate dependency issue and check the box next to `Update dependency cypress to X.Y.Z`. It will automatically create a PR. Once it passes, you can merge it. Try updating at least the following projects: - [cypress-example-todomvc](https://github.com/cypress-io/cypress-example-todomvc/issues/99) - [cypress-example-todomvc-redux](https://github.com/cypress-io/cypress-example-todomvc-redux/issues/1) - [cypress-example-realworld](https://github.com/cypress-io/cypress-example-realworld/issues/2) @@ -175,7 +192,8 @@ In the following instructions, "X.Y.Z" is used to denote the version of Cypress - [cypress-example-piechopper](https://github.com/cypress-io/cypress-example-piechopper/issues/75) - [cypress-documentation](https://github.com/cypress-io/cypress-documentation/issues/1313) - [cypress-example-docker-compose](https://github.com/cypress-io/cypress-example-docker-compose) - Doesn't have a Renovate issue, but will auto-create and auto-merge non-major Cypress updates as long as the tests pass. -20. Check if any test or example repositories have a branch for testing the features or fixes from the newly published version `x.y.z`. The branch should also be named `x.y.z`. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z`, merge it into `master`. + +18. Check if any test or example repositories have a branch for testing the features or fixes from the newly published version `x.y.z`. The branch should also be named `x.y.z`. Check all `cypress-test-*` and `cypress-example-*` repositories, and if there is a branch named `x.y.z`, merge it into `master`. **Test Repos** diff --git a/circle.yml b/circle.yml index 89e6a46666cc..209fefc1c55e 100644 --- a/circle.yml +++ b/circle.yml @@ -812,6 +812,40 @@ jobs: path: /tmp/artifacts - store-npm-logs + desktop-gui-visual-tests: + <<: *defaults + parallelism: 1 + steps: + - attach_workspace: + at: ~/ + - run: + command: yarn build-prod + working_directory: packages/desktop-gui + - run: + name: Desktop GUI server + command: yarn start + working_directory: packages/desktop-gui + background: true + - run: + # will use PERCY_TOKEN environment variable if available + # to tie Percy builds together, use environment variables + # https://docs.percy.io/docs/parallel-test-suites + # we can use workflow id and list number of jobs with Percy screenshots + # if we allow Percy snapshots in more jobs, use the same + # PERCY_* variables in all of them + # NOTE: we only execute specs with "cy.percySnapshot" commands in them + command: | + CYPRESS_KONFIG_ENV=production \ + PERCY_PARALLEL_NONCE=$CIRCLE_WORKFLOW_ID \ + PERCY_PARALLEL_TOTAL=1 \ + yarn percy exec -- \ + yarn cypress:run \ + --spec cypress/integration/settings_spec.js,cypress/integration/specs_list_spec.js,cypress/integration/runs_list_spec.js + working_directory: packages/desktop-gui + - verify-mocha-results + # we don't really need any artifacts - we are only interested in visual screenshots + - store-npm-logs + desktop-gui-component-tests: <<: *defaults parallelism: 1 diff --git a/cli/package.json b/cli/package.json index 2a6d632fb468..eaa56736fd91 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,44 +20,43 @@ "unit": "cross-env BLUEBIRD_DEBUG=1 NODE_ENV=test mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json" }, "dependencies": { - "@cypress/listr-verbose-renderer": "0.4.1", - "@cypress/request": "2.88.5", - "@cypress/xvfb": "1.2.4", - "@types/sinonjs__fake-timers": "6.0.1", - "@types/sizzle": "2.3.2", - "arch": "2.1.2", - "blob-util": "2.0.2", - "bluebird": "3.7.2", - "cachedir": "2.3.0", - "chalk": "4.1.0", - "check-more-types": "2.24.0", - "cli-table3": "0.6.0", - "commander": "4.1.1", - "common-tags": "1.8.0", - "debug": "4.1.1", - "eventemitter2": "6.4.2", - "execa": "4.0.2", - "executable": "4.1.1", - "extract-zip": "1.7.0", - "fs-extra": "9.0.1", - "getos": "3.2.1", - "is-ci": "2.0.0", - "is-installed-globally": "0.3.2", - "lazy-ass": "1.6.0", - "listr": "0.14.3", - "lodash": "4.17.19", - "log-symbols": "4.0.0", - "minimist": "1.2.5", - "moment": "2.26.0", - "ospath": "1.2.2", - "pretty-bytes": "5.3.0", - "ramda": "0.26.1", - "request-progress": "3.0.0", - "supports-color": "7.1.0", - "tmp": "0.2.1", - "untildify": "4.0.0", - "url": "0.11.0", - "yauzl": "2.10.0" + "@cypress/listr-verbose-renderer": "^0.4.1", + "@cypress/request": "^2.88.5", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "^6.0.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.1.2", + "bluebird": "^3.7.2", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-table3": "~0.6.0", + "commander": "^4.1.1", + "common-tags": "^1.8.0", + "debug": "^4.1.1", + "eventemitter2": "^6.4.2", + "execa": "^4.0.2", + "executable": "^4.1.1", + "extract-zip": "^1.7.0", + "fs-extra": "^9.0.1", + "getos": "^3.2.1", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.2", + "lazy-ass": "^1.6.0", + "listr": "^0.14.3", + "lodash": "^4.17.19", + "log-symbols": "^4.0.0", + "minimist": "^1.2.5", + "moment": "^2.27.0", + "ospath": "^1.2.2", + "pretty-bytes": "^5.3.0", + "ramda": "~0.26.1", + "request-progress": "^3.0.0", + "supports-color": "^7.1.0", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "url": "^0.11.0", + "yauzl": "^2.10.0" }, "devDependencies": { "@babel/cli": "7.8.4", diff --git a/cli/schema/cypress.schema.json b/cli/schema/cypress.schema.json index 70874a6dde08..12b7c37052e3 100644 --- a/cli/schema/cypress.schema.json +++ b/cli/schema/cypress.schema.json @@ -124,6 +124,11 @@ "default": "cypress/plugins/index.js", "description": "Path to plugins file. (Pass false to disable)" }, + "screenshotOnRunFailure": { + "type": "boolean", + "default": true, + "description": "Whether Cypress will take a screenshot when a test fails during cypress run" + }, "screenshotsFolder": { "type": "string", "default": "cypress/screenshots", diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index 6d962cf473e6..805bb2ca3ca0 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -6,7 +6,7 @@ // in the future the NPM module itself will be in TypeScript // but for now describe it as an ambient module -declare module 'cypress' { +declare namespace CypressCommandLine { interface TestError { name: string message: string @@ -332,7 +332,9 @@ declare module 'cypress' { */ parseRunArguments(args: string[]): Promise> } +} +declare module 'cypress' { /** * Cypress NPM module interface. * @see https://on.cypress.io/module-api @@ -357,19 +359,19 @@ declare module 'cypress' { }) ``` */ - run(options?: Partial): Promise, + run(options?: Partial): Promise, /** * Opens Cypress GUI. Resolves with void when the * GUI is closed. * @see https://on.cypress.io/module-api#cypress-open */ - open(options?: Partial): Promise + open(options?: Partial): Promise /** * Utility functions for parsing CLI arguments the same way * Cypress does */ - cli: CypressCliParser + cli: CypressCommandLine.CypressCliParser } // export Cypress NPM module interface diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index b9dd61c25860..a37ca35278f2 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -228,6 +228,7 @@ declare namespace Cypress { name: string // "config_passing_spec.coffee" relative: string // "cypress/integration/config_passing_spec.coffee" or "__all" if clicked all specs button absolute: string + specFilter?: string // optional spec filter used by the user } /** @@ -2333,6 +2334,54 @@ declare namespace Cypress { * @default false */ multiple: boolean + /** + * Activates the control key during click + * + * @default false + */ + ctrlKey: boolean + /** + * Activates the control key during click + * + * @default false + */ + controlKey: boolean + /** + * Activates the alt key (option key for Mac) during click + * + * @default false + */ + altKey: boolean + /** + * Activates the alt key (option key for Mac) during click + * + * @default false + */ + optionKey: boolean + /** + * Activates the shift key during click + * + * @default false + */ + shiftKey: boolean + /** + * Activates the meta key (Windows key or command key for Mac) during click + * + * @default false + */ + metaKey: boolean + /** + * Activates the meta key (Windows key or command key for Mac) during click + * + * @default false + */ + commandKey: boolean + /** + * Activates the meta key (Windows key or command key for Mac) during click + * + * @default false + */ + cmdKey: boolean } interface ResolvedConfigOptions { @@ -2436,6 +2485,11 @@ declare namespace Cypress { * @example 1.2.3 */ resolvedNodeVersion: string + /** + * Whether Cypress will take a screenshot when a test fails during cypress run. + * @default true + */ + screenshotOnRunFailure: boolean /** * Path to folder where screenshots will be saved from [cy.screenshot()](https://on.cypress.io/screenshot) command or after a headless or CI run’s test failure * @default "cypress/screenshots" @@ -2849,7 +2903,7 @@ declare namespace Cypress { encoding: Encodings } - // Kind of onerous, but has a nice auto-complete. Also fallbacks at the end for custom stuff + // Kind of onerous, but has a nice auto-complete. /** * @see https://on.cypress.io/should * @@ -3040,6 +3094,22 @@ declare namespace Cypress { * @see https://on.cypress.io/assertions */ (chainer: 'be.undefined'): Chainable + /** + * Asserts that the target is strictly (`===`) equal to null. + * @example + * cy.wrap(null).should('be.null') + * @see http://chaijs.com/api/bdd/#method_null + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.null'): Chainable + /** + * Asserts that the target is strictly (`===`) equal to NaN. + * @example + * cy.wrap(NaN).should('be.NaN') + * @see http://chaijs.com/api/bdd/#method_null + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.NaN'): Chainable /** * Asserts that the target is a number or a date greater than or equal to the given number or date `start`, and less than or equal to the given number or date `finish` respectively. * However, it’s often best to assert that the target is equal to its expected value. @@ -3168,7 +3238,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_all * @see https://on.cypress.io/assertions */ - (chainer: 'have.all.keys', ...value: string[]): Chainable + (chainer: 'have.all.keys' | 'have.keys' | 'have.deep.keys' | 'have.all.deep.keys', ...value: string[]): Chainable /** * Causes all `.keys` assertions that follow in the chain to only require that the target have at least one of the given keys. This is the opposite of `.all`, which requires that the target have all of the given keys. * @example @@ -3176,7 +3246,15 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_any * @see https://on.cypress.io/assertions */ - (chainer: 'have.any.keys', ...value: string[]): Chainable + (chainer: 'have.any.keys' | 'include.any.keys', ...value: string[]): Chainable + /** + * Causes all `.keys` assertions that follow in the chain to require the target to be a superset of the expected set, rather than an identical set. + * @example + * cy.wrap({ a: 1, b: 2 }).should('include.all.keys', 'a', 'b') + * @see http://chaijs.com/api/bdd/#method_keys + * @see https://on.cypress.io/assertions + */ + (chainer: 'include.all.keys', ...value: string[]): Chainable /** * Asserts that the target has a property with the given key `name`. See the `deep-eql` project page for info on the deep equality algorithm: https://github.com/chaijs/deep-eql. * @example @@ -3194,7 +3272,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length', value: number): Chainable + (chainer: 'have.length' | 'have.lengthOf', value: number): Chainable /** * Asserts that the target’s `length` property is greater than to the given number `n`. * @example @@ -3203,7 +3281,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.greaterThan', value: number): Chainable + (chainer: 'have.length.greaterThan' | 'have.lengthOf.greaterThan', value: number): Chainable /** * Asserts that the target’s `length` property is greater than to the given number `n`. * @example @@ -3212,7 +3290,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.gt', value: number): Chainable + (chainer: 'have.length.gt' | 'have.lengthOf.gt' | 'have.length.above' | 'have.lengthOf.above', value: number): Chainable /** * Asserts that the target’s `length` property is greater than or equal to the given number `n`. * @example @@ -3221,7 +3299,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.gte', value: number): Chainable + (chainer: 'have.length.gte' | 'have.lengthOf.gte' | 'have.length.at.least' | 'have.lengthOf.at.least', value: number): Chainable /** * Asserts that the target’s `length` property is less than to the given number `n`. * @example @@ -3230,7 +3308,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.lessThan', value: number): Chainable + (chainer: 'have.length.lessThan' | 'have.lengthOf.lessThan', value: number): Chainable /** * Asserts that the target’s `length` property is less than to the given number `n`. * @example @@ -3239,7 +3317,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.lt', value: number): Chainable + (chainer: 'have.length.lt' | 'have.lengthOf.lt' | 'have.length.below' | 'have.lengthOf.below', value: number): Chainable /** * Asserts that the target’s `length` property is less than or equal to the given number `n`. * @example @@ -3248,7 +3326,15 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.lte', value: number): Chainable + (chainer: 'have.length.lte' | 'have.lengthOf.lte' | 'have.length.at.most' | 'have.lengthOf.at.most', value: number): Chainable + /** + * Asserts that the target’s `length` property is within `start` and `finish`. + * @example + * cy.wrap([1, 2, 3]).should('have.length.within', 1, 5) + * @see http://chaijs.com/api/bdd/#method_lengthof + * @see https://on.cypress.io/assertions + */ + (chainer: 'have.length.within' | 'have.lengthOf.within', start: number, finish: number): Chainable /** * Asserts that the target array has the same members as the given array `set`. * @example @@ -3256,7 +3342,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_members * @see https://on.cypress.io/assertions */ - (chainer: 'have.members', values: any[]): Chainable + (chainer: 'have.members' | 'have.deep.members', values: any[]): Chainable /** * Asserts that the target array has the same members as the given array where order matters. * @example @@ -3282,7 +3368,15 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_property * @see https://on.cypress.io/assertions */ - (chainer: 'have.property', property: string, value?: any): Chainable + (chainer: 'have.property' | 'have.nested.property' | 'have.own.property' | 'have.a.property' | 'have.deep.property' | 'have.deep.own.property' | 'have.deep.nested.property', property: string, value?: any): Chainable + /** + * Asserts that the target has its own property descriptor with the given key name. + * @example + * cy.wrap({a: 1}).should('have.ownPropertyDescriptor', 'a', { value: 1 }) + * @see http://chaijs.com/api/bdd/#method_ownpropertydescriptor + * @see https://on.cypress.io/assertions + */ + (chainer: 'have.ownPropertyDescriptor' | 'haveOwnPropertyDescriptor', name: string, descriptor?: PropertyDescriptor): Chainable /** * Asserts that the target string contains the given substring `str`. * @example @@ -3298,7 +3392,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_include * @see https://on.cypress.io/assertions */ - (chainer: 'include', value: any): Chainable + (chainer: 'include' | 'deep.include' | 'nested.include' | 'own.include' | 'deep.own.include' | 'deep.nested.include', value: any): Chainable /** * When the target is a string, `.include` asserts that the given string `val` is a substring of the target. * @example @@ -3306,21 +3400,29 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_members * @see https://on.cypress.io/assertions */ - (chainer: 'include.members', value: any[]): Chainable + (chainer: 'include.members' | 'include.ordered.members' | 'include.deep.ordered.members', value: any[]): Chainable /** * When one argument is provided, `.increase` asserts that the given function `subject` returns a greater number when it’s * invoked after invoking the target function compared to when it’s invoked beforehand. * `.increase` also causes all `.by` assertions that follow in the chain to assert how much greater of a number is returned. * It’s often best to assert that the return value increased by the expected amount, rather than asserting it increased by any amount. + * + * When two arguments are provided, `.increase` asserts that the value of the given object `subject`’s `prop` property is greater after + * invoking the target function compared to beforehand. + * * @example * let val = 1 * function addTwo() { val += 2 } * function getVal() { return val } * cy.wrap(addTwo).should('increase', getVal) + * + * const myObj = { val: 1 } + * function addTwo() { myObj.val += 2 } + * cy.wrap(addTwo).should('increase', myObj, 'val') * @see http://chaijs.com/api/bdd/#method_increase * @see https://on.cypress.io/assertions */ - (chainer: 'increase', value: object, property: string): Chainable + (chainer: 'increase', value: object, property?: string): Chainable /** * Asserts that the target matches the given regular expression `re`. * @example @@ -3373,6 +3475,50 @@ declare namespace Cypress { */ // tslint:disable-next-line ban-types (chainer: 'throw', error: Error | Function, expected?: string | RegExp): Chainable + /** + * Asserts that the target is a member of the given array list. + * @example + * cy.wrap(1).should('be.oneOf', [1, 2, 3]) + * @see http://chaijs.com/api/bdd/#method_oneof + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.oneOf', list: ReadonlyArray): Chainable + /** + * Asserts that the target is extensible, which means that new properties can be added to it. + * @example + * cy.wrap({a: 1}).should('be.extensible') + * @see http://chaijs.com/api/bdd/#method_extensible + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.extensible'): Chainable + /** + * Asserts that the target is sealed, which means that new properties can’t be added to it, and its existing properties can’t be reconfigured or deleted. + * @example + * let sealedObject = Object.seal({}) + * let frozenObject = Object.freeze({}) + * cy.wrap(sealedObject).should('be.sealed') + * cy.wrap(frozenObject).should('be.sealed') + * @see http://chaijs.com/api/bdd/#method_sealed + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.sealed'): Chainable + /** + * Asserts that the target is frozen, which means that new properties can’t be added to it, and its existing properties can’t be reassigned to different values, reconfigured, or deleted. + * @example + * let frozenObject = Object.freeze({}) + * cy.wrap(frozenObject).should('be.frozen') + * @see http://chaijs.com/api/bdd/#method_frozen + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.frozen'): Chainable + /** + * Asserts that the target is a number, and isn’t `NaN` or positive/negative `Infinity`. + * @example + * cy.wrap(1).should('be.finite') + * @see http://chaijs.com/api/bdd/#method_finite + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.finite'): Chainable // chai.not /** @@ -3557,6 +3703,22 @@ declare namespace Cypress { * @see https://on.cypress.io/assertions */ (chainer: 'not.be.undefined'): Chainable + /** + * Asserts that the target is strictly (`===`) equal to null. + * @example + * cy.wrap(null).should('not.be.null') + * @see http://chaijs.com/api/bdd/#method_null + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.null'): Chainable + /** + * Asserts that the target is strictly (`===`) equal to NaN. + * @example + * cy.wrap(NaN).should('not.be.NaN') + * @see http://chaijs.com/api/bdd/#method_nan + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.NaN'): Chainable /** * Asserts that the target is not a number or a date greater than or equal to the given number or date `start`, and less than or equal to the given number or date `finish` respectively. * However, it’s often best to assert that the target is equal to its expected value. @@ -3668,7 +3830,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_all * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.all.keys', ...value: string[]): Chainable + (chainer: 'not.have.all.keys' | 'not.have.keys' | 'not.have.deep.keys' | 'not.have.all.deep.keys', ...value: string[]): Chainable /** * Causes all `.keys` assertions that follow in the chain to only require that the target not have at least one of the given keys. This is the opposite of `.all`, which requires that the target have all of the given keys. * @example @@ -3676,7 +3838,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_any * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.any.keys', ...value: string[]): Chainable + (chainer: 'not.have.any.keys' | 'not.include.any.keys', ...value: string[]): Chainable /** * Asserts that the target does not have a property with the given key `name`. See the `deep-eql` project page for info on the deep equality algorithm: https://github.com/chaijs/deep-eql. * @example @@ -3694,7 +3856,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.length', value: number): Chainable + (chainer: 'not.have.length' | 'not.have.lengthOf', value: number): Chainable /** * Asserts that the target’s `length` property is not greater than to the given number `n`. * @example @@ -3703,7 +3865,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.length.greaterThan', value: number): Chainable + (chainer: 'not.have.length.greaterThan' | 'not.have.lengthOf.greaterThan', value: number): Chainable /** * Asserts that the target’s `length` property is not greater than to the given number `n`. * @example @@ -3712,7 +3874,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.length.gt', value: number): Chainable + (chainer: 'not.have.length.gt' | 'not.have.lengthOf.gt' | 'not.have.length.above' | 'not.have.lengthOf.above', value: number): Chainable /** * Asserts that the target’s `length` property is not greater than or equal to the given number `n`. * @example @@ -3721,7 +3883,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'have.length.gte', value: number): Chainable + (chainer: 'not.have.length.gte' | 'not.have.lengthOf.gte' | 'not.have.length.at.least' | 'not.have.lengthOf.at.least', value: number): Chainable /** * Asserts that the target’s `length` property is less than to the given number `n`. * @example @@ -3730,7 +3892,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.length.lessThan', value: number): Chainable + (chainer: 'not.have.length.lessThan' | 'not.have.lengthOf.lessThan', value: number): Chainable /** * Asserts that the target’s `length` property is not less than to the given number `n`. * @example @@ -3739,16 +3901,24 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.length.lt', value: number): Chainable + (chainer: 'not.have.length.lt' | 'not.have.lengthOf.lt' | 'not.have.length.below' | 'not.have.lengthOf.below', value: number): Chainable /** * Asserts that the target’s `length` property is not less than or equal to the given number `n`. * @example - * cy.wrap([1, 2, 3]).should('not.have.length.let', 2) + * cy.wrap([1, 2, 3]).should('not.have.length.lte', 2) * cy.wrap('foo').should('not.have.length.lte', 2) * @see http://chaijs.com/api/bdd/#method_lengthof * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.length.lte', value: number): Chainable + (chainer: 'not.have.length.lte' | 'not.have.lengthOf.lte' | 'not.have.length.at.most' | 'not.have.lengthOf.at.most', value: number): Chainable + /** + * Asserts that the target’s `length` property is within `start` and `finish`. + * @example + * cy.wrap([1, 2, 3]).should('not.have.length.within', 6, 12) + * @see http://chaijs.com/api/bdd/#method_lengthof + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.have.length.within' | 'not.have.lengthOf.within', start: number, finish: number): Chainable /** * Asserts that the target array does not have the same members as the given array `set`. * @example @@ -3756,7 +3926,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_members * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.members', values: any[]): Chainable + (chainer: 'not.have.members' | 'not.have.deep.members', values: any[]): Chainable /** * Asserts that the target array does not have the same members as the given array where order matters. * @example @@ -3782,7 +3952,15 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_property * @see https://on.cypress.io/assertions */ - (chainer: 'not.have.property', property: string, value?: any): Chainable + (chainer: 'not.have.property' | 'not.have.nested.property' | 'not.have.own.property' | 'not.have.a.property' | 'not.have.deep.property' | 'not.have.deep.own.property' | 'not.have.deep.nested.property', property: string, value?: any): Chainable + /** + * Asserts that the target has its own property descriptor with the given key name. + * @example + * cy.wrap({a: 1}).should('not.have.ownPropertyDescriptor', 'a', { value: 2 }) + * @see http://chaijs.com/api/bdd/#method_ownpropertydescriptor + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.have.ownPropertyDescriptor' | 'not.haveOwnPropertyDescriptor', name: string, descriptor?: PropertyDescriptor): Chainable /** * Asserts that the target string does not contains the given substring `str`. * @example @@ -3798,7 +3976,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_include * @see https://on.cypress.io/assertions */ - (chainer: 'not.include', value: any): Chainable + (chainer: 'not.include' | 'not.deep.include' | 'not.nested.include' | 'not.own.include' | 'not.deep.own.include' | 'not.deep.nested.include', value: any): Chainable /** * When the target is a string, `.include` asserts that the given string `val` is not a substring of the target. * @example @@ -3806,21 +3984,29 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_members * @see https://on.cypress.io/assertions */ - (chainer: 'not.include.members', value: any[]): Chainable + (chainer: 'not.include.members' | 'not.include.ordered.members' | 'not.include.deep.ordered.members', value: any[]): Chainable /** * When one argument is provided, `.increase` asserts that the given function `subject` returns a greater number when it’s * invoked after invoking the target function compared to when it’s invoked beforehand. * `.increase` also causes all `.by` assertions that follow in the chain to assert how much greater of a number is returned. * It’s often best to assert that the return value increased by the expected amount, rather than asserting it increased by any amount. + * + * When two arguments are provided, `.increase` asserts that the value of the given object `subject`’s `prop` property is greater after + * invoking the target function compared to beforehand. + * * @example * let val = 1 * function addTwo() { val += 2 } * function getVal() { return val } * cy.wrap(() => {}).should('not.increase', getVal) + * + * const myObj = { val: 1 } + * function addTwo() { myObj.val += 2 } + * cy.wrap(addTwo).should('increase', myObj, 'val') * @see http://chaijs.com/api/bdd/#method_increase * @see https://on.cypress.io/assertions */ - (chainer: 'not.increase', value: object, property: string): Chainable + (chainer: 'not.increase', value: object, property?: string): Chainable /** * Asserts that the target does not match the given regular expression `re`. * @example @@ -3859,7 +4045,7 @@ declare namespace Cypress { * @see http://chaijs.com/api/bdd/#method_throw * @see https://on.cypress.io/assertions */ - (chainer: 'throw', value?: string | RegExp): Chainable + (chainer: 'not.throw', value?: string | RegExp): Chainable /** * When no arguments are provided, `.throw` invokes the target function and asserts that no error is thrown. * When one argument is provided, and it’s a string, `.throw` invokes the target function and asserts that no error is thrown with a message that contains that string. @@ -3872,7 +4058,50 @@ declare namespace Cypress { * @see https://on.cypress.io/assertions */ // tslint:disable-next-line ban-types - (chainer: 'throw', error: Error | Function, expected?: string | RegExp): Chainable + (chainer: 'not.throw', error: Error | Function, expected?: string | RegExp): Chainable + /** + * Asserts that the target is a member of the given array list. + * @example + * cy.wrap(42).should('not.be.oneOf', [1, 2, 3]) + * @see http://chaijs.com/api/bdd/#method_oneof + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.oneOf', list: ReadonlyArray): Chainable + /** + * Asserts that the target is extensible, which means that new properties can be added to it. + * @example + * let o = Object.seal({}) + * cy.wrap(o).should('not.be.extensible') + * @see http://chaijs.com/api/bdd/#method_extensible + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.extensible'): Chainable + /** + * Asserts that the target is sealed, which means that new properties can’t be added to it, and its existing properties can’t be reconfigured or deleted. + * @example + * cy.wrap({a: 1}).should('be.sealed') + * cy.wrap({a: 1}).should('be.sealed') + * @see http://chaijs.com/api/bdd/#method_sealed + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.sealed'): Chainable + /** + * Asserts that the target is frozen, which means that new properties can’t be added to it, and its existing properties can’t be reassigned to different values, reconfigured, or deleted. + * @example + * cy.wrap({a: 1}).should('not.be.frozen') + * @see http://chaijs.com/api/bdd/#method_frozen + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.frozen'): Chainable + /** + * Asserts that the target is a number, and isn’t `NaN` or positive/negative `Infinity`. + * @example + * cy.wrap(NaN).should('not.be.finite') + * cy.wrap(Infinity).should('not.be.finite') + * @see http://chaijs.com/api/bdd/#method_finite + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.finite'): Chainable // sinon-chai /** @@ -3881,80 +4110,80 @@ declare namespace Cypress { * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew * @see https://on.cypress.io/assertions */ - (chainer: 'be.always.calledWithNew'): Chainable + (chainer: 'be.always.calledWithNew' | 'always.have.been.calledWithNew'): Chainable /** * Assert if spy was always called with matching arguments (and possibly others). * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledwithmatcharg1-arg2- * @see https://on.cypress.io/assertions */ - (chainer: 'be.always.calledWithMatch', ...args: any[]): Chainable + (chainer: 'be.always.calledWithMatch' | 'always.have.been.calledWithMatch', ...args: any[]): Chainable /** * Assert spy always returned the provided value. * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwaysreturnedobj * @see https://on.cypress.io/assertions */ - (chainer: 'always.returned', value: any): Chainable + (chainer: 'always.returned' | 'have.always.returned', value: any): Chainable /** * `true` if the spy was called at least once * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalled * @see https://on.cypress.io/assertions */ - (chainer: 'be.called'): Chainable + (chainer: 'be.called' | 'have.been.called'): Chainable /** * Assert spy was called after `anotherSpy` * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledafteranotherspy * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledAfter', spy: sinon.SinonSpy): Chainable + (chainer: 'be.calledAfter' | 'have.been.calledAfter', spy: sinon.SinonSpy): Chainable /** * Assert spy was called before `anotherSpy` * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledbeforeanotherspy * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledBefore', spy: sinon.SinonSpy): Chainable + (chainer: 'be.calledBefore' | 'have.been.calledBefore', spy: sinon.SinonSpy): Chainable /** * Assert spy was called at least once with `obj` as `this`. `calledOn` also accepts a matcher (see [matchers](http://sinonjs.org/releases/v4.1.3/spies/#matchers)). * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledonobj * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledOn', context: any): Chainable + (chainer: 'be.calledOn' | 'have.been.calledOn', context: any): Chainable /** * Assert spy was called exactly once * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledonce * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledOnce'): Chainable + (chainer: 'be.calledOnce' | 'have.been.calledOnce'): Chainable /** * Assert spy was called exactly three times * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledthrice * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledThrice'): Chainable + (chainer: 'be.calledThrice' | 'have.been.calledThrice'): Chainable /** * Assert spy was called exactly twice * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledtwice * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledTwice'): Chainable + (chainer: 'be.calledTwice' | 'have.been.calledTwice'): Chainable /** * Assert spy was called at least once with the provided arguments and no others. * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithexactlyarg1-arg2- * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledWithExactly', ...args: any[]): Chainable + (chainer: 'be.calledWithExactly' | 'have.been.calledWithExactly', ...args: any[]): Chainable /** * Assert spy was called with matching arguments (and possibly others). * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithmatcharg1-arg2- * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledWithMatch', ...args: any[]): Chainable + (chainer: 'be.calledWithMatch' | 'have.been.calledWithMatch', ...args: any[]): Chainable /** * Assert spy/stub was called the `new` operator. * Beware that this is inferred based on the value of the this object and the spy function’s prototype, so it may give false positives if you actively return the right kind of object. * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew * @see https://on.cypress.io/assertions */ - (chainer: 'be.calledWithNew'): Chainable + (chainer: 'be.calledWithNew' | 'have.been.calledWithNew'): Chainable /** * Assert spy always threw an exception. * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwaysthrew @@ -3978,7 +4207,61 @@ declare namespace Cypress { * @see http://sinonjs.org/releases/v4.1.3/spies/#spyreturnedobj * @see https://on.cypress.io/assertions */ - (chainer: 'returned', value: any): Chainable + (chainer: 'returned' | 'have.returned', value: any): Chainable + /** + * Assert spy was called before anotherSpy, and no spy calls occurred between spy and anotherSpy. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledimmediatelybeforeanotherspy + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.calledImmediatelyBefore' | 'have.been.calledImmediatelyBefore', anotherSpy: sinon.SinonSpy): Chainable + /** + * Assert spy was called after anotherSpy, and no spy calls occurred between anotherSpy and spy. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledimmediatelyafteranotherspy + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.calledImmediatelyAfter' | 'have.been.calledImmediatelyAfter', anotherSpy: sinon.SinonSpy): Chainable + /** + * Assert the spy was always called with obj as this + * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledonobj + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.always.calledOn' | 'always.have.been.calledOn', obj: any): Chainable + /** + * Assert spy was called at least once with the provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwitharg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.calledWith' | 'have.been.calledWith', ...args: any[]): Chainable + /** + * Assert spy was always called with the provided arguments (and possibly others). + * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledwitharg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.always.calledWith' | 'always.have.been.calledWith', ...args: any[]): Chainable + /** + * Assert spy was called at exactly once with the provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwitharg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.calledOnceWith' | 'have.been.calledOnceWith', ...args: any[]): Chainable + /** + * Assert spy was always called with the exact provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledwithexactlyarg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.always.calledWithExactly' | 'have.been.calledWithExactly', ...args: any[]): Chainable + /** + * Assert spy was called at exactly once with the provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/# + * @see https://on.cypress.io/assertions + */ + (chainer: 'be.calledOnceWithExactly' | 'have.been.calledOnceWithExactly', ...args: any[]): Chainable + /** + * Assert spy always returned the provided value. + * @see http://sinonjs.org/releases/v4.1.3/spies/# + * @see https://on.cypress.io/assertions + */ + (chainer: 'have.always.returned', obj: any): Chainable // sinon-chai.not /** @@ -3987,80 +4270,80 @@ declare namespace Cypress { * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.always.calledWithNew'): Chainable + (chainer: 'not.be.always.calledWithNew' | 'not.always.have.been.calledWithNew'): Chainable /** * Assert if spy was not always called with matching arguments (and possibly others). * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledwithmatcharg1-arg2- * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.always.calledWithMatch', ...args: any[]): Chainable + (chainer: 'not.be.always.calledWithMatch' | 'not.always.have.been.calledWithMatch', ...args: any[]): Chainable /** * Assert spy not always returned the provided value. * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwaysreturnedobj * @see https://on.cypress.io/assertions */ - (chainer: 'not.always.returned', value: any): Chainable + (chainer: 'not.always.returned' | 'not.have.always.returned', value: any): Chainable /** * `true` if the spy was not called at least once * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalled * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.called'): Chainable + (chainer: 'not.be.called' | 'not.have.been.called'): Chainable /** * Assert spy was not.called after `anotherSpy` * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledafteranotherspy * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledAfter', spy: sinon.SinonSpy): Chainable + (chainer: 'not.be.calledAfter' | 'not.have.been.calledAfter', spy: sinon.SinonSpy): Chainable /** * Assert spy was not called before `anotherSpy` * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledbeforeanotherspy * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledBefore', spy: sinon.SinonSpy): Chainable + (chainer: 'not.be.calledBefore' | 'not.have.been.calledBefore', spy: sinon.SinonSpy): Chainable /** * Assert spy was not called at least once with `obj` as `this`. `calledOn` also accepts a matcher (see [matchers](http://sinonjs.org/releases/v4.1.3/spies/#matchers)). * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledonobj * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledOn', context: any): Chainable + (chainer: 'not.be.calledOn' | 'not.have.been.calledOn', context: any): Chainable /** * Assert spy was not called exactly once * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledonce * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledOnce'): Chainable + (chainer: 'not.be.calledOnce' | 'not.have.been.calledOnce'): Chainable /** * Assert spy was not called exactly three times * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledthrice * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledThrice'): Chainable + (chainer: 'not.be.calledThrice' | 'not.have.been.calledThrice'): Chainable /** * Assert spy was not called exactly twice * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledtwice * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledTwice'): Chainable + (chainer: 'not.be.calledTwice' | 'not.have.been.calledTwice'): Chainable /** * Assert spy was not called at least once with the provided arguments and no others. * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithexactlyarg1-arg2- * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledWithExactly', ...args: any[]): Chainable + (chainer: 'not.be.calledWithExactly' | 'not.have.been.calledWithExactly', ...args: any[]): Chainable /** * Assert spy was not called with matching arguments (and possibly others). * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithmatcharg1-arg2- * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledWithMatch', ...args: any[]): Chainable + (chainer: 'not.be.calledWithMatch' | 'not.have.been.calledWithMatch', ...args: any[]): Chainable /** * Assert spy/stub was not called the `new` operator. * Beware that this is inferred based on the value of the this object and the spy function’s prototype, so it may give false positives if you actively return the right kind of object. * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwithnew * @see https://on.cypress.io/assertions */ - (chainer: 'not.be.calledWithNew'): Chainable + (chainer: 'not.be.calledWithNew' | 'not.have.been.calledWithNew'): Chainable /** * Assert spy did not always throw an exception. * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwaysthrew @@ -4084,7 +4367,61 @@ declare namespace Cypress { * @see http://sinonjs.org/releases/v4.1.3/spies/#spyreturnedobj * @see https://on.cypress.io/assertions */ - (chainer: 'not.returned', value: any): Chainable + (chainer: 'not.returned' | 'not.have.returned', value: any): Chainable + /** + * Assert spy was called before anotherSpy, and no spy calls occurred between spy and anotherSpy. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledimmediatelybeforeanotherspy + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.calledImmediatelyBefore' | 'not.have.been.calledImmediatelyBefore', anotherSpy: sinon.SinonSpy): Chainable + /** + * Assert spy was called after anotherSpy, and no spy calls occurred between anotherSpy and spy. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledimmediatelyafteranotherspy + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.calledImmediatelyAfter' | 'not.have.been.calledImmediatelyAfter', anotherSpy: sinon.SinonSpy): Chainable + /** + * Assert the spy was always called with obj as this + * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledonobj + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.always.calledOn' | 'not.always.have.been.calledOn', obj: any): Chainable + /** + * Assert spy was called at least once with the provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwitharg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.calledWith' | 'not.have.been.calledWith', ...args: any[]): Chainable + /** + * Assert spy was always called with the provided arguments (and possibly others). + * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledwitharg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.always.calledWith' | 'not.always.have.been.calledWith', ...args: any[]): Chainable + /** + * Assert spy was called at exactly once with the provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spycalledwitharg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.calledOnceWith' | 'not.have.been.calledOnceWith', ...args: any[]): Chainable + /** + * Assert spy was always called with the exact provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/#spyalwayscalledwithexactlyarg1-arg2- + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.always.calledWithExactly' | 'not.have.been.calledWithExactly', ...args: any[]): Chainable + /** + * Assert spy was called at exactly once with the provided arguments. + * @see http://sinonjs.org/releases/v4.1.3/spies/# + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.be.calledOnceWithExactly' | 'not.have.been.calledOnceWithExactly', ...args: any[]): Chainable + /** + * Assert spy always returned the provided value. + * @see http://sinonjs.org/releases/v4.1.3/spies/# + * @see https://on.cypress.io/assertions + */ + (chainer: 'not.have.always.returned', obj: any): Chainable // jquery-chai /** @@ -4837,7 +5174,7 @@ declare namespace Cypress { domain: string httpOnly: boolean secure: boolean - expiry?: string + expiry?: number sameSite?: SameSiteStatus } diff --git a/cli/types/tests/chainer-examples.ts b/cli/types/tests/chainer-examples.ts index f73532e20dd4..6f72b6f5af1a 100644 --- a/cli/types/tests/chainer-examples.ts +++ b/cli/types/tests/chainer-examples.ts @@ -97,7 +97,7 @@ cy.wrap({ x: {a: 1 }}).should('have.deep.property', 'x', { a: 1 }) cy.wrap([1, 2, 3]).should('have.length', 3) cy.wrap('foo').should('have.length', 3) -cy.wrap([1, 2, 3]).should('have.length.greaterThan') +cy.wrap([1, 2, 3]).should('have.length.greaterThan', 2) cy.wrap('foo').should('have.length.greaterThan', 2) cy.wrap([1, 2, 3]).should('have.length.gt', 2) @@ -128,19 +128,19 @@ cy.wrap('foobar').should('have.string', 'bar') cy.wrap('foobar').should('include', 'foo') -cy.wrap('foo').should('contain.value') -cy.wrap('foo').should('contain.text') -cy.wrap('foo').should('contain.html') -cy.wrap('foo').should('not.contain.value') -cy.wrap('foo').should('not.contain.text') -cy.wrap('foo').should('not.contain.html') +cy.wrap('foo').should('contain.value', 'foo') +cy.wrap('foo').should('contain.text', 'foo') +cy.wrap('foo').should('contain.html', 'foo') +cy.wrap('foo').should('not.contain.value', 'foo') +cy.wrap('foo').should('not.contain.text', 'foo') +cy.wrap('foo').should('not.contain.html', 'foo') -cy.wrap('foo').should('include.value') -cy.wrap('foo').should('include.text') -cy.wrap('foo').should('include.html') -cy.wrap('foo').should('not.include.value') -cy.wrap('foo').should('not.include.text') -cy.wrap('foo').should('not.incldue.html') +cy.wrap('foo').should('include.value', 'foo') +cy.wrap('foo').should('include.text', 'foo') +cy.wrap('foo').should('include.html', 'foo') +cy.wrap('foo').should('not.include.value', 'foo') +cy.wrap('foo').should('not.include.text', 'foo') +cy.wrap('foo').should('not.include.html', 'foo') // Ensure we've extended chai.Includes correctly expect('foo').to.include.value('foo') @@ -157,6 +157,9 @@ cy.wrap([1, 2, 3]).should('include.members', [1, 2]) function addTwo() { val += 2 } function getVal() { return val } cy.wrap(addTwo).should('increase', getVal) + + const myObj = { val: 1 } + cy.wrap(addTwo).should('increase', myObj, 'val') } cy.wrap('foobar').should('match', /^foo/) @@ -226,6 +229,12 @@ cy.wrap(true).should('not.be.undefined') cy.wrap(3).should('not.be.within', 5, 10) +cy.wrap(null).should('be.null') +cy.wrap(123).should('not.be.null') + +cy.wrap(NaN).should('be.NaN') +cy.wrap('cypress').should('not.be.NaN') + ; () => { let dots = '' @@ -256,7 +265,7 @@ cy.wrap('tester').should('not.contain', 'foo') cy.wrap(() => {}).should('not.decrease', myObj, 'val') } -cy.wrap({ a: 1 }).should('not.deep.equal', { b: 1 }) +cy.wrap<{a?: number, b?: number }>({ a: 1 }).should('not.deep.equal', { b: 1 }) cy.wrap(null).should('not.exist') @@ -287,12 +296,12 @@ cy.wrap('foo').should('have.length.lessThan', 2) cy.wrap([1, 2, 3]).should('not.have.length.lt', 2) cy.wrap('foo').should('not.have.length.lt', 2) -cy.wrap([1, 2, 3]).should('not.have.length.let', 2) +cy.wrap([1, 2, 3]).should('not.have.length.lte', 2) cy.wrap('foo').should('not.have.length.lte', 2) cy.wrap([1, 2, 3]).should('not.have.members', [4, 5, 6]) -cy.wrap([1, 2, 3]).should('not. have.ordered.members', [4, 5, 6]) +cy.wrap([1, 2, 3]).should('not.have.ordered.members', [4, 5, 6]) ; (Object as any).prototype.b = 2 @@ -361,7 +370,7 @@ cy.get('#result').should('not.be.focused') cy.get('#result').should('have.focus') cy.get('#result').should('not.have.focus') -cy.get('#result').should('be.contain', 'text') +cy.get('#result').should('contain', 'text') cy.get('#result').should('have.attr', 'role') cy.get('#result').should('have.attr', 'role', 'menu') @@ -458,6 +467,9 @@ cy.writeFile('../file.path', '', { }) cy.get('foo').click() +cy.get('foo').click({ + ctrlKey: true, +}) cy.get('foo').rightclick() cy.get('foo').dblclick() diff --git a/cli/types/tests/cypress-npm-api-test.ts b/cli/types/tests/cypress-npm-api-test.ts index e4ae6fa3ba5f..8fbcd703c35d 100644 --- a/cli/types/tests/cypress-npm-api-test.ts +++ b/cli/types/tests/cypress-npm-api-test.ts @@ -38,3 +38,7 @@ const runConfig: Cypress.ConfigOptions = { }, } cypress.run({ config: runConfig }) + +cypress.run({}).then((results) => { + results as CypressCommandLine.CypressRunResult // $ExpectType CypressRunResult +}) diff --git a/cli/types/tests/kitchen-sink.ts b/cli/types/tests/kitchen-sink.ts index 85a1509d2dfc..af59141f74f0 100644 --- a/cli/types/tests/kitchen-sink.ts +++ b/cli/types/tests/kitchen-sink.ts @@ -62,6 +62,7 @@ expect(stub).to.not.have.been.called stub() expect(stub).to.have.been.calledOnce cy.wrap(stub).should('have.been.calledOnce') +cy.wrap(stub).should('be.calledOnce') namespace EventInterfaceTests { // window:confirm stubbing diff --git a/package.json b/package.json index 3f21e4eb5a5f..546cfe200c91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "4.11.0", + "version": "4.12.1", "description": "Cypress.io end to end testing tool", "private": true, "scripts": { @@ -81,7 +81,7 @@ "@cypress/questions-remain": "1.0.1", "@cypress/request": "2.88.5", "@cypress/request-promise": "4.2.6", - "@cypress/webpack-preprocessor": "5.4.1", + "@cypress/webpack-preprocessor": "5.4.2", "@fellow/eslint-plugin-coffee": "0.4.13", "@percy/cypress": "2.3.1", "@types/bluebird": "3.5.29", diff --git a/packages/desktop-gui/cypress/integration/runs_list_spec.js b/packages/desktop-gui/cypress/integration/runs_list_spec.js index c47ceeade608..eb6999052d8e 100644 --- a/packages/desktop-gui/cypress/integration/runs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/runs_list_spec.js @@ -195,7 +195,7 @@ describe('Runs List', function () { }) }) - it('shows \'cannot connect to api server\' message', function () { + it('shows "cannot connect to api server" message', function () { cy.contains('Cannot connect to API server') cy.contains('http://api.server') cy.contains('ECONNREFUSED') @@ -327,8 +327,8 @@ describe('Runs List', function () { this.goToRuns() }) - it('displays \'need to set up\' message', () => { - cy.contains('You have no recorded runs') + it('displays "need to set up" message', () => { + cy.contains('You could see test recordings here') }) }) @@ -342,13 +342,13 @@ describe('Runs List', function () { this.goToRuns() }) - it('displays \'need to set up\' message', () => { - cy.contains('You have no recorded runs') + it('displays "need to set up" message', () => { + cy.contains('You could see test recordings here') }) describe('click setup project', function () { beforeEach(() => { - cy.contains('Set up project').click() + cy.contains('Connect to Dashboard').click() }) it('shows login message', () => { @@ -439,10 +439,10 @@ describe('Runs List', function () { cy.contains('Request access') }) - it('displays \'need to set up\' message', function () { + it('displays "need to set up" message', function () { this.ipcError({ type: 'NO_PROJECT_ID' }) - cy.contains('You have no recorded runs') + cy.contains('You could see test recordings here') }) it('displays old runs if another error', function () { @@ -541,7 +541,7 @@ describe('Runs List', function () { cy.get('@requestAccessBtn').should('be.disabled') }) - it('hides \'Request access\' text', () => { + it('hides "Request access" text', () => { cy.get('@requestAccessBtn').find('span').should('not.be.visible') }) @@ -558,7 +558,7 @@ describe('Runs List', function () { cy.contains('Request sent') }) - it('\'persists\' request state (until app is reloaded at least)', function () { + it('persists request state (until app is reloaded at least)', function () { this.ipc.getRuns.onCall(1).rejects({ name: 'foo', message: 'There\'s an error', statusCode: 403 }) cy.get('.navbar-default a').contains('Tests').click() @@ -613,7 +613,7 @@ describe('Runs List', function () { cy.get('@requestAccessBtn').should('not.be.disabled') }) - it('shows \'Request access\' text', () => { + it('shows "Request access" text', () => { cy.get('@requestAccessBtn').find('span').should('be.visible') }) @@ -627,7 +627,7 @@ describe('Runs List', function () { this.requestAccess.reject({ type: 'DENIED', name: 'foo', message: 'There\'s an error' }) }) - it('shows \'success\' message', () => { + it('shows "success" message', () => { cy.contains('Request sent') }) }) @@ -637,7 +637,7 @@ describe('Runs List', function () { this.requestAccess.reject({ type: 'ALREADY_REQUESTED', name: 'foo', message: 'There\'s an error' }) }) - it('shows \'success\' message', () => { + it('shows "success" message', () => { cy.contains('Request sent') }) }) @@ -653,6 +653,7 @@ describe('Runs List', function () { it('shows login message', () => { cy.get('.empty h4').should('contain', 'Log in') + cy.percySnapshot() }) it('clicking Log In to Dashboard opens login', () => { @@ -723,12 +724,13 @@ describe('Runs List', function () { }) }) - it('displays \'need to set up\' message', () => { - cy.contains('You have no recorded runs') + it('displays "need to set up" message', () => { + cy.contains('You could see test recordings here') + cy.percySnapshot() }) - it('clears message after setting up to record', function () { - cy.contains('.btn', 'Set up project').click() + it('clears message after setting up to record', () => { + cy.contains('.btn', 'Connect to Dashboard').click() cy.get('.organizations-select__dropdown-indicator').click() cy.get('.organizations-select__menu').should('be.visible') cy.get('.organizations-select__option') @@ -747,11 +749,7 @@ describe('Runs List', function () { this.openProject.resolve(this.config) this.goToRuns().then(() => { - this.getRuns.reject({ name: 'foo', message: `\ -{ - "no runs": "for you" -}\ -`, type: 'UNKNOWN' }) + this.getRuns.reject({ name: 'foo', message: `{"no runs": "for you"}`, type: 'UNKNOWN' }) }) }) @@ -824,8 +822,8 @@ describe('Runs List', function () { }) }) - it('displays \'need to set up\' message', () => { - cy.contains('You have no recorded runs') + it('displays "need to set up" message', () => { + cy.contains('You could see test recordings here') }) }) }) @@ -843,14 +841,14 @@ describe('Runs List', function () { cy.contains('To record your first') }) - it('opens project id guide on clicking \'Why?\'', () => { + it('opens project id guide on clicking "Why?"', () => { cy.contains('Why?').click() .then(function () { expect(this.ipc.externalOpen).to.be.calledWith('https://on.cypress.io/what-is-a-project-id') }) }) - it('opens dashboard on clicking \'Cypress Dashboard\'', () => { + it('opens dashboard on clicking "Cypress Dashboard"', () => { cy.contains('Cypress Dashboard').click() .then(function () { expect(this.ipc.externalOpen).to.be.calledWith(`https://on.cypress.io/dashboard/projects/${this.config.projectId}/runs`) diff --git a/packages/desktop-gui/cypress/integration/setup_project_modal_spec.js b/packages/desktop-gui/cypress/integration/setup_project_modal_spec.js index de5d70dde293..fe20ce50d674 100644 --- a/packages/desktop-gui/cypress/integration/setup_project_modal_spec.js +++ b/packages/desktop-gui/cypress/integration/setup_project_modal_spec.js @@ -1,4 +1,4 @@ -describe('Set Up Project', function () { +describe('Connect to Dashboard', function () { beforeEach(function () { cy.fixture('user').as('user') cy.fixture('projects').as('projects') @@ -46,8 +46,8 @@ describe('Set Up Project', function () { }) }) - it('displays \'need to set up\' message', () => { - cy.contains('You have no recorded runs') + it('displays "need to set up" message', () => { + cy.contains('You could see test recordings here') }) describe('when there is a current user', function () { @@ -59,7 +59,7 @@ describe('Set Up Project', function () { beforeEach(function () { this.getOrgs.resolve(this.orgs) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) it('clicking link opens setup project window', () => { @@ -90,7 +90,7 @@ describe('Set Up Project', function () { describe('loading behavior', function () { beforeEach(function () { - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) it('calls getOrgs', function () { @@ -111,7 +111,7 @@ describe('Set Up Project', function () { beforeEach(function () { this.getOrgs.resolve(this.orgs) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() cy.get('.modal-content') cy.get('.organizations-select__dropdown-indicator').click() cy.get('.organizations-select__menu').should('be.visible') @@ -135,7 +135,7 @@ describe('Set Up Project', function () { context('with orgs', function () { beforeEach(function () { this.getOrgs.resolve(this.orgs) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() cy.get('.modal-content') }) @@ -176,7 +176,7 @@ describe('Set Up Project', function () { context('orgs with no default org', function () { beforeEach(function () { this.getOrgs.resolve(Cypress._.filter(this.orgs, { 'default': false })) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) it('lists organizations to assign to project', function () { @@ -212,7 +212,7 @@ describe('Set Up Project', function () { context('without orgs', function () { beforeEach(function () { this.getOrgs.resolve([]) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) it('displays empty message', () => { @@ -236,7 +236,7 @@ describe('Set Up Project', function () { 'default': true, }]) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() cy.get('.modal-content') }) @@ -264,7 +264,7 @@ describe('Set Up Project', function () { beforeEach(function () { cy.clock() this.getOrgs.resolve(this.orgs) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) it('polls for orgs twice in 10+sec on click of org', function () { @@ -308,7 +308,7 @@ describe('Set Up Project', function () { describe('on submit', function () { beforeEach(function () { this.getOrgs.resolve(this.orgs) - cy.contains('.btn', 'Set up project').click() + cy.contains('.btn', 'Connect to Dashboard').click() cy.get('.organizations-select__dropdown-indicator').click() cy.get('.organizations-select__menu').should('be.visible') cy.get('.organizations-select__option') @@ -343,7 +343,7 @@ describe('Set Up Project', function () { orgId: '000', }) - cy.contains('.btn', 'Set up project').click() + cy.contains('.btn', 'Connect to Dashboard').click() }) it('sends project name, org id, and public flag to ipc event', function () { @@ -450,7 +450,7 @@ describe('Set Up Project', function () { describe('errors', function () { beforeEach(function () { this.getOrgs.resolve(this.orgs) - cy.contains('.btn', 'Set up project').click() + cy.contains('.btn', 'Connect to Dashboard').click() cy.get('.organizations-select__dropdown-indicator').click() cy.get('.organizations-select__menu').should('be.visible') cy.get('.organizations-select__option') @@ -471,12 +471,7 @@ describe('Set Up Project', function () { it('displays error name and message when unexpected', function () { this.setupDashboardProject.reject({ name: 'Fatal Error!', - message: `\ -{ - "system": "down", - "toxicity": "of the city" -}\ -`, + message: `{ "system": "down", "toxicity": "of the city" }`, }) cy.contains('"system": "down"') @@ -485,7 +480,7 @@ describe('Set Up Project', function () { describe('when get orgs 401s', function () { beforeEach(function () { - cy.contains('.btn', 'Set up project').click() + cy.contains('.btn', 'Connect to Dashboard').click() .then(() => { this.getOrgs.reject({ name: '', message: '', statusCode: 401 }) }) @@ -501,7 +496,7 @@ describe('Set Up Project', function () { beforeEach(function () { this.getCurrentUser.resolve(null) - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) it('shows login', () => { @@ -511,7 +506,7 @@ describe('Set Up Project', function () { it('closes login modal', () => { cy.get('.modal').contains('Log In to Dashboard') cy.get('.close').click() - cy.get('.btn').contains('Set up project').click() + cy.get('.btn').contains('Connect to Dashboard').click() }) describe('when login succeeds', function () { diff --git a/packages/desktop-gui/cypress/integration/specs_list_spec.js b/packages/desktop-gui/cypress/integration/specs_list_spec.js index 313905c70f9d..44f9c7420fcb 100644 --- a/packages/desktop-gui/cypress/integration/specs_list_spec.js +++ b/packages/desktop-gui/cypress/integration/specs_list_spec.js @@ -201,12 +201,15 @@ describe('Specs List', function () { it('triggers browser launch on click of button', () => { cy.contains('.all-tests', 'Run all specs').click() + .find('.fa-dot-circle') .then(function () { const launchArgs = this.ipc.launchBrowser.lastCall.args - expect(launchArgs[0].browser.name).to.eq('chrome') + expect(launchArgs[0].browser.name, 'browser name').to.eq('chrome') - expect(launchArgs[0].spec.name).to.eq('All Specs') + expect(launchArgs[0].spec.name, 'spec name').to.eq('All Specs') + + expect(launchArgs[0].specFilter, 'spec filter').to.eq(null) }) }) @@ -406,13 +409,23 @@ describe('Specs List', function () { this.ipc.getSpecs.yields(null, this.specs) this.openProject.resolve(this.config) + cy.contains('.all-tests', 'Run all specs') cy.get('.filter').type('new') }) - it('displays only matching spec', () => { + it('displays only matching spec', function () { cy.get('.specs-list .file') .should('have.length', 1) .and('contain', 'account_new_spec.coffee') + + cy.contains('.all-tests', 'Run 1 spec').click() + .find('.fa-dot-circle') + .then(() => { + expect(this.ipc.launchBrowser).to.have.property('called').equal(true) + const launchArgs = this.ipc.launchBrowser.lastCall.args + + expect(launchArgs[0].specFilter, 'spec filter').to.eq('new') + }) }) it('only shows matching folders', () => { @@ -427,6 +440,8 @@ describe('Specs List', function () { cy.get('.specs-list .file') .should('have.length', this.numSpecs) + + cy.contains('.all-tests', 'Run all specs') }) it('clears the filter if the user press ESC key', function () { @@ -435,6 +450,9 @@ describe('Specs List', function () { cy.get('.specs-list .file') .should('have.length', this.numSpecs) + + cy.contains('.all-tests', 'Run all specs') + .find('.fa-play') }) it('shows empty message if no results', function () { @@ -442,6 +460,17 @@ describe('Specs List', function () { cy.get('.specs-list').should('not.exist') cy.get('.empty-well').should('contain', 'No specs match your search: "foobarbaz"') + + cy.contains('.all-tests', 'No specs') + }) + + it('disables run all tests if no results', function () { + cy.get('.filter').clear().type('foobarbaz') + + cy.contains('.all-tests', 'No specs').should('be.disabled').click({ force: true }) + .then(function () { + expect(this.ipc.launchBrowser).to.have.property('called').equal(false) + }) }) it('clears and focuses the filter field when clear search is clicked', function () { @@ -460,6 +489,29 @@ describe('Specs List', function () { expect(JSON.parse(win.localStorage[`specsFilter-${this.config.projectId}-/foo/bar`])).to.equal('new') }) }) + + it('does not update run button label while running', function () { + cy.contains('.all-tests', 'Run 1 spec').click() + // mock opened browser and running tests + // to force "Stop" button to show up + cy.window().its('__project').then((project) => { + project.browserOpened() + }) + + // the button has its its label reflect the running specs + cy.contains('.all-tests', 'Running 1 spec') + .should('have.class', 'active') + + // the button has its label unchanged while the specs are running + cy.get('.filter').clear() + cy.contains('.all-tests', 'Running 1 spec') + .should('have.class', 'active') + + // but once the project stops running tests, the button gets updated + cy.get('.close-browser').click() + cy.contains('.all-tests', 'Run all specs') + .should('not.have.class', 'active') + }) }) describe('when there\'s a saved filter', function () { @@ -475,6 +527,7 @@ describe('Specs List', function () { this.openProject.resolve(this.config) cy.get('.filter').should('have.value', 'app') + cy.contains('.all-tests', 'Run 1 spec') }) it('does not apply it for a different project', function () { @@ -525,7 +578,7 @@ describe('Specs List', function () { cy.get('@firstSpec') .click() .then(function () { - expect(this.ipc.closeBrowser).to.be.called + expect(this.ipc.closeBrowser).to.have.property('called', true) const launchArgs = this.ipc.launchBrowser.lastCall.args @@ -544,6 +597,12 @@ describe('Specs List', function () { .should('have.class', 'active') }) + it('shows the running spec label', () => { + cy.get('@firstSpec').click() + cy.contains('.all-tests', 'Running 1 spec') + .find('.fa-dot-circle') + }) + it('maintains active selection if specs change', function () { cy.get('@firstSpec').click().then(() => { this.ipc.getSpecs.yield(null, this.specs) @@ -682,7 +741,8 @@ describe('Specs List', function () { }) it('opens in preferred opener', function () { - cy.get('@button').click().then(() => { + cy.get('@button').click() + .then(() => { expect(this.ipc.openFile).to.be.calledWith({ where: this.availableEditors[4], file: '/user/project/cypress/integration/app_spec.coffee', diff --git a/packages/desktop-gui/package.json b/packages/desktop-gui/package.json index bb6a382e3864..d910c1b499c8 100644 --- a/packages/desktop-gui/package.json +++ b/packages/desktop-gui/package.json @@ -31,17 +31,17 @@ "gravatar": "1.8.0", "human-interval": "1.0.0", "lodash": "4.17.19", - "markdown-it": "8.4.2", + "markdown-it": "11.0.0", "mobx": "5.15.4", "mobx-react": "6.1.8", "mobx-react-devtools": "6.1.1", - "moment": "2.26.0", + "moment": "2.27.0", "prop-types": "15.7.2", "rc-collapse": "2.0.0", "react": "16.8.6", "react-bootstrap-modal": "4.2.0", "react-dom": "16.8.6", - "react-inspector": "4.0.0", + "react-inspector": "5.0.1", "react-loader": "2.4.7", "react-select": "3.1.0", "webpack": "4.35.3", diff --git a/packages/desktop-gui/src/auth/login-form.jsx b/packages/desktop-gui/src/auth/login-form.jsx index 4d159f29084e..6f95c5c54f7c 100644 --- a/packages/desktop-gui/src/auth/login-form.jsx +++ b/packages/desktop-gui/src/auth/login-form.jsx @@ -25,7 +25,7 @@ class LoginForm extends Component {
{this._error()} +

After logging in, you'll see recorded test runs here and in your Cypress Dashboard.

) } - _visitDashboard = (e) => { - e.preventDefault() - ipc.externalOpen('https://on.cypress.io/dashboard') - } - _invalidProject () { return (
diff --git a/packages/desktop-gui/src/runs/runs-list.jsx b/packages/desktop-gui/src/runs/runs-list.jsx index 28633151f917..7cc0588a6063 100644 --- a/packages/desktop-gui/src/runs/runs-list.jsx +++ b/packages/desktop-gui/src/runs/runs-list.jsx @@ -18,6 +18,7 @@ import LoginForm from '../auth/login-form' import Run from './runs-list-item' import PermissionMessage from './permission-message' import ProjectNotSetup from './project-not-setup' +import DashboardBanner from './dashboard-banner' @observer class RunsList extends Component { @@ -274,16 +275,9 @@ class RunsList extends Component { _loginMessage () { return (
-

Log in to view runs

-

- After logging in, you will see recorded runs here and on the Cypress Dashboard Service. -

-
- - - -
- + +

Log in to see test recordings here!

+
After logging in, you will see recorded runs here and on the Cypress Dashboard.
) diff --git a/packages/desktop-gui/src/runs/runs.scss b/packages/desktop-gui/src/runs/runs.scss index 52283bc83b72..17930f782941 100644 --- a/packages/desktop-gui/src/runs/runs.scss +++ b/packages/desktop-gui/src/runs/runs.scss @@ -529,3 +529,142 @@ margin-top: 10px; } } + +.dashboard-banner { + background-color: #311C56; + box-shadow: 0 0 3px 0 rgba(0,0,0,0.20); + display: flex; + flex-direction: column; + justify-content: flex-end; + height: 185px; + position: relative; + overflow: hidden; + + .left-vector { + z-index: 900; + position: absolute; + width: 250px; + height: 225px; + left: -100px; + bottom: -140px; + border-radius: 100px; + + background-color: #3d2665; + transform: matrix(-0.97, -0.08, 0.24, -1, 0, 0); + } + + .right-vector { + z-index: 900; + position: absolute; + width: 250px; + height: 225px; + right: -65px; + top: -100px; + border-radius: 100px; + + background-color: #3d2665; + transform: matrix(0.42, 0.82, -1.09, 0.18, 0, 0); + } +} + +.dashboard-banner-db { + z-index: 1000; + background-color: #fff; + width: 600px; + margin: 0 auto; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} + +.dashboard-banner-test { + padding: 10px 20px; + display: flex; + justify-content: space-between; + color: #888B91; + height: 50px; + align-items: center; + border-bottom: 1px solid #f4f5f7; + font-size: 11px; + + i { + padding-right: 8px; + } + + &:first-of-type { + border-top-left-radius: 3px; + } + + .test-title-top { + margin-bottom: 4px; + display: flex; + } + + .test-title-bottom { + display: flex; + margin-left: 20px; + + &>div:nth-child(1) { + width: 45px; + } + + &>div:nth-child(2) { + width: 60px; + } + + &>div:nth-child(3) { + width: 20px; + } + } + + .fake-text { + background-color: #E0E0E0; + height: 6px; + border-radius: 1px; + margin-right: 6px; + margin-top: 3px; + } + + .dashboard-banner-test-title { + font-weight: bold; + display: flex; + flex-direction: column; + padding-right: 100px; + + i { + margin-top: 3px; + } + } + + &.pass { + border-left: 5px solid #48996b; + + .dashboard-banner-test-title { + color: #48996b; + } + } + + &.fail { + border-left: 4px solid #DB7177; + + .dashboard-banner-test-title { + color: #DB7177; + } + } +} + +.dashboard-banner-test-mid, .dashboard-banner-test-end { + display: flex; + flex-direction: row; +} + +.dashboard-banner-test-mid { + &>div:nth-of-type(1) { + width: 70px; + } +} + +.dashboard-banner-test-end { + &>div:nth-of-type(1) { + width: 60px; + } +} \ No newline at end of file diff --git a/packages/desktop-gui/src/specs/specs-list.jsx b/packages/desktop-gui/src/specs/specs-list.jsx index b88c3f729667..f201e709a0cd 100644 --- a/packages/desktop-gui/src/specs/specs-list.jsx +++ b/packages/desktop-gui/src/specs/specs-list.jsx @@ -15,12 +15,56 @@ class SpecsList extends Component { constructor (props) { super(props) this.filterRef = React.createRef() + // when the specs are running and the user changes the search filter + // we still want to show the previous button label to reflect what + // is currently running + this.runAllSavedLabel = null + + if (window.Cypress) { + // expose project object for testing + window.__project = this.props.project + } } render () { if (specsStore.isLoading) return - if (!specsStore.filter && !specsStore.specs.length) return this._empty() + const filteredSpecs = specsStore.getFilteredSpecs() + + const hasSpecFilter = specsStore.filter + const numberOfShownSpecs = filteredSpecs.length + const hasNoSpecs = !hasSpecFilter && !numberOfShownSpecs + + if (hasNoSpecs) { + return this._empty() + } + + const areTestsRunning = this._areTestsRunning() + let runSpecsLabel = allSpecsSpec.displayName + let runButtonDisabled = false + + if (areTestsRunning && this.runAllSavedLabel) { + runSpecsLabel = this.runAllSavedLabel + } else { + if (hasSpecFilter) { + if (numberOfShownSpecs < 1) { + runSpecsLabel = 'No specs' + runButtonDisabled = true + } else { + const specLabel = numberOfShownSpecs === 1 ? 'spec' : 'specs' + + runSpecsLabel = `Run ${numberOfShownSpecs} ${specLabel}` + } + } + } + + const runTestsButton = () return (
@@ -47,12 +91,7 @@ class SpecsList extends Component {
- - {' '} - {allSpecsSpec.displayName} - + {runTestsButton} {this._specsList()}
@@ -86,8 +125,17 @@ class SpecsList extends Component { return spec.hasChildren ? this._folderContent(spec, nestingLevel) : this._specContent(spec, nestingLevel) } - _allSpecsIcon (allSpecsChosen) { - return allSpecsChosen ? 'far fa-dot-circle green' : 'fas fa-play' + _allSpecsIcon () { + return this._areTestsRunning() ? 'far fa-dot-circle green' : 'fas fa-play' + } + + _areTestsRunning () { + if (!this.props.project) { + return false + } + + return this.props.project.browserState === 'opening' + || this.props.project.browserState === 'opened' } _specIcon (isChosen) { @@ -117,7 +165,21 @@ class SpecsList extends Component { const { project } = this.props - return projectsApi.runSpec(project, spec, project.chosenBrowser) + if (spec.relative === '__all') { + if (specsStore.filter) { + const filteredSpecs = specsStore.getFilteredSpecs() + const numberOfShownSpecs = filteredSpecs.length + + this.runAllSavedLabel = numberOfShownSpecs === 1 + ? 'Running 1 spec' : `Running ${numberOfShownSpecs} specs` + } else { + this.runAllSavedLabel = 'Running all specs' + } + } else { + this.runAllSavedLabel = 'Running 1 spec' + } + + return projectsApi.runSpec(project, spec, project.chosenBrowser, specsStore.filter) } _setExpandRootFolder (specFolderPath, isExpanded, e) { diff --git a/packages/desktop-gui/src/specs/specs-store.js b/packages/desktop-gui/src/specs/specs-store.js index 74ee745519b5..1968c24643d1 100644 --- a/packages/desktop-gui/src/specs/specs-store.js +++ b/packages/desktop-gui/src/specs/specs-store.js @@ -25,7 +25,27 @@ const pathsEqual = (path1, path2) => { return path1.replace(pathSeparatorRe, '') === path2.replace(pathSeparatorRe, '') } +/** + * Filters give file objects by spec name substring +*/ +const filterSpecs = (filter, files) => { + if (!filter) { + return files + } + + const filteredFiles = _.filter(files, (spec) => { + return spec.name.toLowerCase().includes(filter.toLowerCase()) + }) + + return filteredFiles +} + export class SpecsStore { + /** + * All spec files + * + * @memberof SpecsStore + */ @observable _files = [] @observable chosenSpecPath @observable error @@ -75,7 +95,9 @@ export class SpecsStore { } @action setFilter (project, filter = null) { - if (!filter) return this.clearFilter(project) + if (!filter) { + return this.clearFilter(project) + } localData.set(this.getSpecsFilterId(project), filter) @@ -106,12 +128,17 @@ export class SpecsStore { return specOrFolder.children.some((child) => child.isFolder) } + /** + * Returns only specs matching the current filter + * + * @memberof SpecsStore + */ + getFilteredSpecs () { + return filterSpecs(this.filter, this._files) + } + _tree (files) { - if (this.filter) { - files = _.filter(files, (spec) => { - return spec.name.toLowerCase().includes(this.filter.toLowerCase()) - }) - } + files = filterSpecs(this.filter, files) const tree = _.reduce(files, (root, file) => { const segments = [file.type].concat(file.name.split(pathSeparatorRe)) diff --git a/packages/desktop-gui/src/specs/specs.scss b/packages/desktop-gui/src/specs/specs.scss index be2da3974a87..1eed0bb0efcb 100644 --- a/packages/desktop-gui/src/specs/specs.scss +++ b/packages/desktop-gui/src/specs/specs.scss @@ -17,6 +17,8 @@ $max-nesting-level: 14; height: 42px; background: #f5f5f5; border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; } .search { @@ -24,7 +26,7 @@ $max-nesting-level: 14; margin-right: 15px; display: inline-block; position: relative; - width: calc(100% - 140px); + width: calc(100% - 160px); label { position: absolute; @@ -91,10 +93,14 @@ $max-nesting-level: 14; pointer-events: none; color: #4c4e63; text-decoration: none; - } } + &:disabled { + cursor: not-allowed; + color: #ddd; + } + i { font-size: 8px; position: relative; diff --git a/packages/desktop-gui/src/styles/components/_buttons.scss b/packages/desktop-gui/src/styles/components/_buttons.scss index bc2adcdfc97e..960fec703dca 100644 --- a/packages/desktop-gui/src/styles/components/_buttons.scss +++ b/packages/desktop-gui/src/styles/components/_buttons.scss @@ -12,3 +12,23 @@ outline: 0 } } + +.btn-primary { + background-color: #3385d4; + border-color: #3385d4; + + &:hover, &:focus { + background-color: darken(#3385d4, 10%); + border-color: darken(#3385d4, 10%); + } + + &:active, &:focus { + outline: 0 + } +} + +.btn-wide { + padding: 8px 30px; + font-weight: 600; + font-size: 12px; +} diff --git a/packages/desktop-gui/src/styles/components/_elements.scss b/packages/desktop-gui/src/styles/components/_elements.scss index 09f40130ec5c..cd1d29a48917 100644 --- a/packages/desktop-gui/src/styles/components/_elements.scss +++ b/packages/desktop-gui/src/styles/components/_elements.scss @@ -22,6 +22,7 @@ a { &:hover, &:focus, &:active { cursor: pointer; + color: darken($brand-primary, 10%); } } diff --git a/packages/desktop-gui/src/styles/components/_empty.scss b/packages/desktop-gui/src/styles/components/_empty.scss index 92bcde2f53d1..385d5f24b616 100644 --- a/packages/desktop-gui/src/styles/components/_empty.scss +++ b/packages/desktop-gui/src/styles/components/_empty.scss @@ -12,12 +12,39 @@ } } -.empty-no-runs { - margin-top: 70px; +.empty-no-runs, .empty-log-in { + h4 { + margin-top: 40px; + font-size: 22px; + color: #000; + } + + .empty-no-runs-details { + text-align: left; + margin: 15px auto 0; + width: 440px; + } + + ul { + color: #272B2F; + padding-left: 25px; + font-size: 13px; + } + + h5 { + color: #272B2F; + font-size: 15px; + } + + p { + font-size: 11px; + margin-top: 10px; + color: #272B2F; + font-weight: 200; + } } -.empty-no-api-server, -.empty-log-in { +.empty-no-api-server { padding: 40px 100px 20px; } diff --git a/packages/desktop-gui/src/styles/variables.scss b/packages/desktop-gui/src/styles/variables.scss index afdc54dc107d..fbb8920d85c1 100644 --- a/packages/desktop-gui/src/styles/variables.scss +++ b/packages/desktop-gui/src/styles/variables.scss @@ -1,7 +1,7 @@ $red-primary: #e94f5f; $light-gray: #EFF0F3; -$brand-primary: #476fc9; +$brand-primary: #3284d4; $brand-success: #21C799; $pass: #08c18d; diff --git a/packages/driver/cypress/fixtures/custom-elements.html b/packages/driver/cypress/fixtures/custom-elements.html new file mode 100644 index 000000000000..611bddc44f48 --- /dev/null +++ b/packages/driver/cypress/fixtures/custom-elements.html @@ -0,0 +1,29 @@ + + + + Custom Elements + + + + + + + diff --git a/packages/driver/cypress/fixtures/issue-486.html b/packages/driver/cypress/fixtures/issue-486.html new file mode 100644 index 000000000000..0f5954a16764 --- /dev/null +++ b/packages/driver/cypress/fixtures/issue-486.html @@ -0,0 +1,33 @@ + + + + Issue 486 + + + +
Result
+ + + diff --git a/packages/driver/cypress/integration/commands/actions/click_spec.js b/packages/driver/cypress/integration/commands/actions/click_spec.js index deb601cfa0a3..674c299ade04 100644 --- a/packages/driver/cypress/integration/commands/actions/click_spec.js +++ b/packages/driver/cypress/integration/commands/actions/click_spec.js @@ -882,6 +882,105 @@ describe('src/cy/commands/actions/click', () => { }) }) + describe('modifier options', () => { + beforeEach(() => { + cy.visit('/fixtures/issue-486.html') + }) + + it('ctrl', () => { + cy.get('#button').click({ + ctrlKey: true, + }) + + cy.get('#result').should('contain', '{Ctrl}') + + // ctrl should be released + cy.get('#button').click() + cy.get('#result').should('not.contain', '{Ctrl}') + + cy.get('#button').click({ + controlKey: true, + }) + + cy.get('#result').should('contain', '{Ctrl}') + }) + + it('alt', () => { + cy.get('#button').click({ + altKey: true, + }) + + cy.get('#result').should('contain', '{Alt}') + + // alt should be released + cy.get('#button').click() + cy.get('#result').should('not.contain', '{Alt}') + + cy.get('#button').click({ + optionKey: true, + }) + + cy.get('#result').should('contain', '{Alt}') + }) + + it('shift', () => { + cy.get('#button').click({ + shiftKey: true, + }) + + cy.get('#result').should('contain', '{Shift}') + + // shift should be released + cy.get('#button').click() + cy.get('#result').should('not.contain', '{Shift}') + }) + + it('meta', () => { + cy.get('#button').click({ + metaKey: true, + }) + + cy.get('#result').should('contain', '{Meta}') + + // shift should be released + cy.get('#button').click() + cy.get('#result').should('not.contain', '{Meta}') + + cy.get('#button').click({ + commandKey: true, + }) + + cy.get('#result').should('contain', '{Meta}') + + cy.get('#button').click({ + cmdKey: true, + }) + + cy.get('#result').should('contain', '{Meta}') + }) + + it('multiple', () => { + cy.get('#button').click({ + ctrlKey: true, + altKey: true, + shiftKey: true, + metaKey: true, + }) + + cy.get('#result').should('contain', '{Ctrl}') + cy.get('#result').should('contain', '{Alt}') + cy.get('#result').should('contain', '{Shift}') + cy.get('#result').should('contain', '{Meta}') + + // modifiers should be released + cy.get('#button').click() + cy.get('#result').should('not.contain', '{Ctrl}') + cy.get('#result').should('not.contain', '{Alt}') + cy.get('#result').should('not.contain', '{Shift}') + cy.get('#result').should('not.contain', '{Meta}') + }) + }) + describe('pointer-events:none', () => { beforeEach(function () { cy.$$('
behind #ptrNone
').appendTo(cy.$$('#dom')) diff --git a/packages/driver/cypress/integration/commands/fixtures_spec.js b/packages/driver/cypress/integration/commands/fixtures_spec.js index f8a547ed411f..254afb7879b9 100644 --- a/packages/driver/cypress/integration/commands/fixtures_spec.js +++ b/packages/driver/cypress/integration/commands/fixtures_spec.js @@ -50,6 +50,10 @@ describe('src/cy/commands/fixtures', () => { cy.fixture('example').should('deep.eq', { example: true }) }) + it('works with null.json', () => { + cy.fixture('null.json').should('equal', null) + }) + it('can read a fixture without extension with multiple dots in the name', () => { cy.fixture('foo.bar.baz').should('deep.eq', { quux: 'quuz' }) }) diff --git a/packages/driver/cypress/integration/commands/screenshot_spec.js b/packages/driver/cypress/integration/commands/screenshot_spec.js index 108ddd58931d..8e65dcd7315e 100644 --- a/packages/driver/cypress/integration/commands/screenshot_spec.js +++ b/packages/driver/cypress/integration/commands/screenshot_spec.js @@ -95,6 +95,25 @@ describe('src/cy/commands/screenshot', () => { }) }) + it('is noop when screenshotOnRunFailure is false', () => { + Cypress.config('isInteractive', false) + Cypress.config('screenshotOnRunFailure', false) + + cy.spy(Cypress, 'action').log(false) + + const test = { + err: new Error, + } + + const runnable = cy.state('runnable') + + Cypress.action('runner:runnable:after:run:async', test, runnable) + .then(() => { + expect(Cypress.action).not.to.be.calledWith('cy:test:set:state') + expect(Cypress.automation).not.to.be.called + }) + }) + it('sends before/after events', function () { Cypress.config('isInteractive', false) this.screenshotConfig.scale = false diff --git a/packages/driver/cypress/integration/commands/xhr_spec.js b/packages/driver/cypress/integration/commands/xhr_spec.js index b3df743a316b..295eb11f67bc 100644 --- a/packages/driver/cypress/integration/commands/xhr_spec.js +++ b/packages/driver/cypress/integration/commands/xhr_spec.js @@ -2031,6 +2031,19 @@ describe('src/cy/commands/xhr', () => { .wrap({ foo: 'bar' }).as('foo') .route(/foo/, '@bar') }) + + // https://github.com/cypress-io/cypress/issues/7818 + it('throws when fixture cannot be found', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contains('A fixture file could not be found at any of the following paths:') + done() + }) + + cy.route(/foo/, 'fx:NOT_EXISTING_FILE_FIXTURE') + cy.window().then((win) => { + win.$.get('/foo') + }) + }) }) describe('.log', () => { diff --git a/packages/driver/cypress/integration/cy/snapshot_spec.js b/packages/driver/cypress/integration/cy/snapshot_spec.js index 0faa9b7960ca..0e916033351b 100644 --- a/packages/driver/cypress/integration/cy/snapshot_spec.js +++ b/packages/driver/cypress/integration/cy/snapshot_spec.js @@ -166,4 +166,28 @@ describe('driver/src/cy/snapshots', () => { }) }) }) + + context('custom elements', () => { + beforeEach(() => { + cy.visit('/fixtures/custom-elements.html') + }) + + // https://github.com/cypress-io/cypress/issues/7187 + it('does not trigger constructor', () => { + const constructor = cy.stub(cy.state('window'), 'shadowScreenshotConstructor') + + cy.createSnapshot() + + expect(constructor).not.to.be.called + }) + + // https://github.com/cypress-io/cypress/issues/7187 + it('does not trigger attributeChangedCallback', () => { + const attributeChanged = cy.stub(cy.state('window'), 'shadowScreenshotAttributeChanged') + + cy.createSnapshot() + + expect(attributeChanged).not.to.be.called + }) + }) }) diff --git a/packages/driver/cypress/integration/dom/visibility_spec.ts b/packages/driver/cypress/integration/dom/visibility_spec.ts index c989d76bac9a..27beb750a03b 100644 --- a/packages/driver/cypress/integration/dom/visibility_spec.ts +++ b/packages/driver/cypress/integration/dom/visibility_spec.ts @@ -244,6 +244,12 @@ describe('src/cypress/dom/visibility', () => { `) + this.$childPointerEventsNone = add(`\ +
+ child pointer-events: none +
\ +`) + this.$descendentPosAbs = add(`\
@@ -289,6 +295,19 @@ describe('src/cypress/dom/visibility', () => { \ +`) + + this.$parentPointerEventsNone = add(`\ +
+ parent pointer-events: none +
\ +`) + + this.$parentPointerEventsNoneCovered = add(`\ +
+ parent pointer-events: none +
+covering the element with pointer-events: none\ `) this.$elOutOfParentBoundsToLeft = add(`\ @@ -688,7 +707,7 @@ describe('src/cypress/dom/visibility', () => { expect(this.$coveredUpPosFixed.find('#coveredUpPosFixed')).not.to.be.visible }) - it('is hidden if position: fixed and off screent', function () { + it('is hidden if position: fixed and off screen', function () { expect(this.$offScreenPosFixed).to.be.hidden expect(this.$offScreenPosFixed).not.to.be.visible }) @@ -702,6 +721,21 @@ describe('src/cypress/dom/visibility', () => { expect(this.$parentPosAbs.find('span')).to.be.hidden expect(this.$parentPosAbs.find('span')).to.not.be.visible }) + + it('is visible if position: fixed and parent has pointer-events: none', function () { + expect(this.$parentPointerEventsNone.find('span')).to.be.visible + expect(this.$parentPointerEventsNone.find('span')).to.not.be.hidden + }) + + it('is not visible if covered when position: fixed and parent has pointer-events: none', function () { + expect(this.$parentPointerEventsNoneCovered.find('span')).to.be.hidden + expect(this.$parentPointerEventsNoneCovered.find('span')).to.not.be.visible + }) + + it('is visible if pointer-events: none and parent has position: fixed', function () { + expect(this.$childPointerEventsNone.find('span')).to.be.visible + expect(this.$childPointerEventsNone.find('span')).to.not.be.hidden + }) }) describe('css overflow', () => { diff --git a/packages/driver/package.json b/packages/driver/package.json index ea995e46af6e..4b5d00a0b637 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -44,20 +44,20 @@ "error-stack-parser": "2.0.6", "errorhandler": "1.5.1", "eventemitter2": "6.4.2", - "express": "4.16.4", + "express": "4.17.1", "jquery": "3.1.1", "jquery.scrollto": "2.1.2", "js-cookie": "2.2.1", "jsdom": "14.1.0", "lodash": "4.17.19", "lolex": "4.1.0", - "md5": "2.2.1", + "md5": "2.3.0", "method-override": "3.0.0", "methods": "1.1.2", "minimatch": "3.0.4", "minimist": "1.2.5", "mocha": "7.0.1", - "moment": "2.26.0", + "moment": "2.27.0", "morgan": "1.9.1", "ordinal": "1.0.3", "react-15.6.1": "npm:react@15.6.1", diff --git a/packages/driver/src/cy/commands/actions/click.js b/packages/driver/src/cy/commands/actions/click.js index b564e2f99350..09ead09029f5 100644 --- a/packages/driver/src/cy/commands/actions/click.js +++ b/packages/driver/src/cy/commands/actions/click.js @@ -34,7 +34,7 @@ const formatMouseEvents = (events) => { } module.exports = (Commands, Cypress, cy, state, config) => { - const { mouse } = cy.devices + const { mouse, keyboard } = cy.devices const mouseAction = (eventName, { subject, positionOrX, y, userOptions, onReady, onTable, defaultOptions }) => { let position @@ -54,6 +54,14 @@ module.exports = (Commands, Cypress, cy, state, config) => { errorOnSelect: true, waitForAnimations: config('waitForAnimations'), animationDistanceThreshold: config('animationDistanceThreshold'), + ctrlKey: false, + controlKey: false, + altKey: false, + optionKey: false, + shiftKey: false, + metaKey: false, + commandKey: false, + cmdKey: false, ...defaultOptions, }) @@ -65,6 +73,24 @@ module.exports = (Commands, Cypress, cy, state, config) => { }) } + const flagModifiers = (press) => { + if (options.ctrlKey || options.controlKey) { + keyboard.flagModifier({ key: 'Control' }, press) + } + + if (options.altKey || options.optionKey) { + keyboard.flagModifier({ key: 'Alt' }, press) + } + + if (options.shiftKey) { + keyboard.flagModifier({ key: 'Shift' }, press) + } + + if (options.metaKey || options.commandKey || options.cmdKey) { + keyboard.flagModifier({ key: 'Meta' }, press) + } + } + const perform = (el) => { let deltaOptions const $el = $dom.wrap(el) @@ -143,7 +169,7 @@ module.exports = (Commands, Cypress, cy, state, config) => { } // must use callbacks here instead of .then() - // because we're issuing the clicks synchonrously + // because we're issuing the clicks synchronously // once we establish the coordinates and the element // passes all of the internal checks return $actionability.verify(cy, $el, options, { @@ -158,8 +184,12 @@ module.exports = (Commands, Cypress, cy, state, config) => { const moveEvents = mouse.move(fromElViewport, forceEl) + flagModifiers(true) + const onReadyProps = onReady(fromElViewport, forceEl) + flagModifiers(false) + return createLog({ moveEvents, ...onReadyProps, diff --git a/packages/driver/src/cy/commands/fixtures.js b/packages/driver/src/cy/commands/fixtures.js index 6e088516f3e1..c357e68ff3bb 100644 --- a/packages/driver/src/cy/commands/fixtures.js +++ b/packages/driver/src/cy/commands/fixtures.js @@ -58,10 +58,8 @@ module.exports = (Commands, Cypress, cy, state, config) => { return Cypress.backend('get:fixture', fixture, _.pick(options, 'encoding')) .timeout(timeout) .then((response) => { - const err = response.__error - - if (err) { - return $errUtils.throwErr(err) + if (response && response.__error) { + return $errUtils.throwErr(response.__error) } // add the fixture to the cache diff --git a/packages/driver/src/cy/commands/screenshot.js b/packages/driver/src/cy/commands/screenshot.js index 98bc64d7e09f..d924bcbae8e2 100644 --- a/packages/driver/src/cy/commands/screenshot.js +++ b/packages/driver/src/cy/commands/screenshot.js @@ -391,7 +391,7 @@ module.exports = function (Commands, Cypress, cy, state, config) { Cypress.on('runnable:after:run:async', (test, runnable) => { const screenshotConfig = $Screenshot.getConfig() - if (!test.err || !screenshotConfig.screenshotOnRunFailure || config('isInteractive') || test.err.isPending) { + if (!test.err || !screenshotConfig.screenshotOnRunFailure || config('isInteractive') || test.err.isPending || !config('screenshotOnRunFailure')) { return } diff --git a/packages/driver/src/cy/commands/xhr.js b/packages/driver/src/cy/commands/xhr.js index 4a20a13b7a52..b2a26ac7bb15 100644 --- a/packages/driver/src/cy/commands/xhr.js +++ b/packages/driver/src/cy/commands/xhr.js @@ -200,7 +200,7 @@ const startXhrServer = (cy, state, config) => { }, onFixtureError (xhr, err) { - err = $errUtils.cypressErr(err) + err = $errUtils.cypressErr({ message: err }) return this.onError(xhr, err) }, @@ -471,6 +471,15 @@ module.exports = (Commands, Cypress, cy, state, config) => { }) } + // look ahead to see if fixture exists + const fixturesRe = /^(fx:|fixture:)/ + + if (hasResponse && fixturesRe.test(options.response)) { + const fixtureName = options.response.replace(fixturesRe, '') + + return cy.now('fixture', fixtureName).then(() => route()) + } + return route() } diff --git a/packages/driver/src/cy/focused.js b/packages/driver/src/cy/focused.js index e0c3c224ef4c..913ddf369760 100644 --- a/packages/driver/src/cy/focused.js +++ b/packages/driver/src/cy/focused.js @@ -89,9 +89,12 @@ const create = (state) => { const win = $window.getWindowByElement(el) - // store the current focused element - // since when we call .focus() it will change - const $focused = getFocused() + // store the current focused element, since it will change when we call .focus() + // + // need to pass in el.ownerDocument to get the correct focused element + // when el is in an iframe and the browser is not + // in focus (https://github.com/cypress-io/cypress/issues/8111) + const $focused = getFocused(el.ownerDocument) let hasFocused = false @@ -220,8 +223,8 @@ const create = (state) => { return false } - const getFocused = () => { - const { activeElement } = state('document') + const getFocused = (document = state('document')) => { + const { activeElement } = document if ($dom.isFocused(activeElement)) { return $dom.wrap(activeElement) diff --git a/packages/driver/src/cy/snapshots.js b/packages/driver/src/cy/snapshots.js index 316ff0514288..52d4004f5729 100644 --- a/packages/driver/src/cy/snapshots.js +++ b/packages/driver/src/cy/snapshots.js @@ -147,7 +147,12 @@ const create = ($$, state) => { // TODO: throw error here if cy is undefined! - const $body = $$('body').clone() + // cloneNode can actually trigger functions attached to custom elements + // so we have to use importNode to clone the element + // to the outer doc and then reassign ownership to the original doc + // https://github.com/cypress-io/cypress/issues/7187 + // https://github.com/cypress-io/cypress/issues/1068 + const $body = $$(state('document').adoptNode(document.importNode($$('body')[0], true))) // for the head and body, get an array of all CSS, // whether it's links or style tags diff --git a/packages/driver/src/cypress/mocha.js b/packages/driver/src/cypress/mocha.js index acecb0567610..bb44c2ef8313 100644 --- a/packages/driver/src/cypress/mocha.js +++ b/packages/driver/src/cypress/mocha.js @@ -106,7 +106,7 @@ function getInvocationDetails (specWindow, config) { // firefox throws a different stack than chromium // which includes this file (mocha.js) and mocha/.../common.js at the top - if (specWindow.Cypress && specWindow.Cypress.browser.family === 'firefox') { + if (specWindow.Cypress && specWindow.Cypress.isBrowser('firefox')) { stack = $stackUtils.stackWithLinesDroppedFromMarker(stack, 'mocha/lib/interfaces/common.js') } @@ -123,7 +123,9 @@ function overloadMochaHook (fnName, suite, specWindow, config) { this._createHook = function (title, fn) { const hook = _createHook.call(this, title, fn) - hook.invocationDetails = getInvocationDetails(specWindow, config) + if (!hook.invocationDetails) { + hook.invocationDetails = getInvocationDetails(specWindow, config) + } return hook } @@ -140,7 +142,9 @@ function overloadMochaTest (suite, specWindow, config) { const _fn = suite.addTest suite.addTest = function (test) { - test.invocationDetails = getInvocationDetails(specWindow, config) + if (!test.invocationDetails) { + test.invocationDetails = getInvocationDetails(specWindow, config) + } return _fn.call(this, test) } diff --git a/packages/driver/src/cypress/runner.js b/packages/driver/src/cypress/runner.js index c2aaac84303e..5776b1c8dec2 100644 --- a/packages/driver/src/cypress/runner.js +++ b/packages/driver/src/cypress/runner.js @@ -687,7 +687,7 @@ const _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, se // hooks do not have their own id, their // commands need to grouped with the test // and we can only associate them by this id - const test = getTest() || getTestFromHookOrFindTest(hook) + const test = getTestFromHookOrFindTest(hook) if (!test) { // we couldn't find a test to run with this hook @@ -912,7 +912,6 @@ const create = (specWindow, mocha, Cypress, cy) => { } // hold onto the _runnables for faster lookup later - let _stopped = false let _test = null let _tests = [] let _testsById = {} @@ -1109,8 +1108,13 @@ const create = (specWindow, mocha, Cypress, cy) => { return _runner.run((failures) => { // if we happen to make it all the way through - // the run, then just set _stopped to true here - _stopped = true + // the run, then just set _runner.stopped to true here + _runner.stopped = true + + // remove all the listeners + // so no more events fire + // since a test failure may 'leak' after a run completes + _runner.removeAllListeners() // TODO this functions is not correctly // synchronized with the 'end' event that @@ -1381,11 +1385,11 @@ const create = (specWindow, mocha, Cypress, cy) => { }, stop () { - if (_stopped) { + if (_runner.stopped) { return } - _stopped = true + _runner.stopped = true // abort the run _runner.abort() @@ -1398,7 +1402,7 @@ const create = (specWindow, mocha, Cypress, cy) => { // remove all the listeners // so no more events fire - return _runner.removeAllListeners() + _runner.removeAllListeners() }, getDisplayPropsForLog: $Log.getDisplayProps, diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index 1c4d4881a590..ae6e941601d6 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -240,7 +240,11 @@ const elIsNotElementFromPoint = function ($el) { // if the element at point is not a descendent // of our $el then we know it's being covered or its // not visible - return !$elements.isDescendent($el, $elAtPoint) + + // we also check if the element at point is a + // parent since pointer-events: none + // will cause elAtCenterPoint to fall through to parent + return !($elements.isDescendent($el, $elAtPoint) || ($elAtPoint && $elements.isAncestor($el, $elAtPoint))) } const elIsOutOfBoundsOfAncestorsOverflow = function ($el, $ancestor = $el.parent()) { diff --git a/packages/network/package.json b/packages/network/package.json index 659d2a0f3271..f31503436964 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -28,7 +28,7 @@ "@packages/socket": "*", "@packages/ts": "*", "@types/concat-stream": "1.6.0", - "express": "4.16.4", + "express": "4.17.1", "mocha": "6.2.2", "sinon": "7.3.1", "sinon-chai": "3.3.0", diff --git a/packages/reporter/cypress/integration/tests_spec.ts b/packages/reporter/cypress/integration/tests_spec.ts index e2bcf0736036..5647c32073b4 100644 --- a/packages/reporter/cypress/integration/tests_spec.ts +++ b/packages/reporter/cypress/integration/tests_spec.ts @@ -2,179 +2,240 @@ import { EventEmitter } from 'events' import { itHandlesFileOpening } from '../support/utils' describe('controls', function () { - beforeEach(function () { - cy.fixture('runnables').as('runnables') - - this.runner = new EventEmitter() - - cy.visit('/dist').then((win) => { - win.render({ - runner: this.runner, - spec: { - name: 'foo.js', - relative: 'relative/path/to/foo.js', - absolute: '/absolute/path/to/foo.js', - }, + context('all specs', function () { + beforeEach(function () { + cy.fixture('runnables').as('runnables') + + this.runner = new EventEmitter() + + cy.visit('/dist').then((win) => { + win.render({ + runner: this.runner, + spec: { + relative: '__all', + name: '', + absolute: '', + }, + }) }) - }) - cy.get('.reporter').then(() => { - this.runner.emit('runnables:ready', this.runnables) + cy.get('.reporter').then(() => { + this.runner.emit('runnables:ready', this.runnables) - this.runner.emit('reporter:start', {}) + this.runner.emit('reporter:start', {}) + }) }) - }) - describe('tests', function () { - beforeEach(function () { - this.passingTestTitle = this.runnables.suites[0].tests[0].title - this.failingTestTitle = this.runnables.suites[0].tests[1].title + it('shows header', () => { + cy.contains('.runnable-header', 'All Specs') }) + }) - describe('expand and collapse', function () { - it('is collapsed by default', function () { - cy.contains(this.passingTestTitle) - .parents('.collapsible').first() - .should('not.have.class', 'is-open') - .find('.collapsible-content') - .should('not.be.visible') - }) - - describe('expand/collapse test manually', function () { - beforeEach(function () { - cy.contains(this.passingTestTitle) - .parents('.collapsible').first().as('testWrapper') - .should('not.have.class', 'is-open') - .find('.collapsible-content') - .should('not.be.visible') + context('filtered specs', function () { + beforeEach(function () { + cy.fixture('runnables').as('runnables') + + this.runner = new EventEmitter() + + cy.visit('/dist').then((win) => { + win.render({ + runner: this.runner, + spec: { + relative: '__all', + name: '', + absolute: '', + specFilter: 'cof', + }, }) + }) - it('expands/collapses on click', function () { - cy.contains(this.passingTestTitle) - .click() + cy.get('.reporter').then(() => { + this.runner.emit('runnables:ready', this.runnables) - cy.get('@testWrapper') - .should('have.class', 'is-open') - .find('.collapsible-content').should('be.visible') + this.runner.emit('reporter:start', {}) + }) + }) - cy.contains(this.passingTestTitle) - .click() + it('shows header', () => { + cy.contains('.runnable-header', 'Specs matching "cof"') + }) + }) - cy.get('@testWrapper') - .should('not.have.class', 'is-open') - .find('.collapsible-content').should('not.be.visible') + context('single spec', function () { + beforeEach(function () { + cy.fixture('runnables').as('runnables') + + this.runner = new EventEmitter() + + cy.visit('/dist').then((win) => { + win.render({ + runner: this.runner, + spec: { + name: 'foo.js', + relative: 'relative/path/to/foo.js', + absolute: '/absolute/path/to/foo.js', + }, }) + }) - it('expands/collapses on enter', function () { - cy.contains(this.passingTestTitle) - .parents('.collapsible-header').first() - .focus().type('{enter}') + cy.get('.reporter').then(() => { + this.runner.emit('runnables:ready', this.runnables) - cy.get('@testWrapper') - .should('have.class', 'is-open') - .find('.collapsible-content').should('be.visible') + this.runner.emit('reporter:start', {}) + }) + }) - cy.contains(this.passingTestTitle) - .parents('.collapsible-header').first() - .focus().type('{enter}') + describe('tests', function () { + beforeEach(function () { + this.passingTestTitle = this.runnables.suites[0].tests[0].title + this.failingTestTitle = this.runnables.suites[0].tests[1].title + }) - cy.get('@testWrapper') + describe('expand and collapse', function () { + it('is collapsed by default', function () { + cy.contains(this.passingTestTitle) + .parents('.collapsible').first() .should('not.have.class', 'is-open') - .find('.collapsible-content').should('not.be.visible') + .find('.collapsible-content') + .should('not.be.visible') }) - it('expands/collapses on space', function () { - cy.contains(this.passingTestTitle) - .parents('.collapsible-header').first() - .focus().type(' ') - - cy.get('@testWrapper') - .should('have.class', 'is-open') - .find('.collapsible-content').should('be.visible') - - cy.contains(this.passingTestTitle) - .parents('.collapsible-header').first() - .focus().type(' ') - - cy.get('@testWrapper') - .should('not.have.class', 'is-open') - .find('.collapsible-content').should('not.be.visible') + describe('expand/collapse test manually', function () { + beforeEach(function () { + cy.contains(this.passingTestTitle) + .parents('.collapsible').first().as('testWrapper') + .should('not.have.class', 'is-open') + .find('.collapsible-content') + .should('not.be.visible') + }) + + it('expands/collapses on click', function () { + cy.contains(this.passingTestTitle) + .click() + + cy.get('@testWrapper') + .should('have.class', 'is-open') + .find('.collapsible-content').should('be.visible') + + cy.contains(this.passingTestTitle) + .click() + + cy.get('@testWrapper') + .should('not.have.class', 'is-open') + .find('.collapsible-content').should('not.be.visible') + }) + + it('expands/collapses on enter', function () { + cy.contains(this.passingTestTitle) + .parents('.collapsible-header').first() + .focus().type('{enter}') + + cy.get('@testWrapper') + .should('have.class', 'is-open') + .find('.collapsible-content').should('be.visible') + + cy.contains(this.passingTestTitle) + .parents('.collapsible-header').first() + .focus().type('{enter}') + + cy.get('@testWrapper') + .should('not.have.class', 'is-open') + .find('.collapsible-content').should('not.be.visible') + }) + + it('expands/collapses on space', function () { + cy.contains(this.passingTestTitle) + .parents('.collapsible-header').first() + .focus().type(' ') + + cy.get('@testWrapper') + .should('have.class', 'is-open') + .find('.collapsible-content').should('be.visible') + + cy.contains(this.passingTestTitle) + .parents('.collapsible-header').first() + .focus().type(' ') + + cy.get('@testWrapper') + .should('not.have.class', 'is-open') + .find('.collapsible-content').should('not.be.visible') + }) }) }) - }) - describe('failed tests', function () { - it('expands automatically', function () { - cy.contains(this.failingTestTitle) - .parents('.collapsible').first() - .should('have.class', 'is-open') - .find('.collapsible-content') - .should('be.visible') + describe('failed tests', function () { + it('expands automatically', function () { + cy.contains(this.failingTestTitle) + .parents('.collapsible').first() + .should('have.class', 'is-open') + .find('.collapsible-content') + .should('be.visible') + }) }) - }) - describe('header', function () { - it('displays', function () { - cy.get('.runnable-header').find('a').should('have.text', 'relative/path/to/foo.js') - }) + describe('header', function () { + it('displays', function () { + cy.get('.runnable-header').find('a').should('have.text', 'relative/path/to/foo.js') + }) - it('displays tooltip on hover', () => { - cy.get('.runnable-header a').first().trigger('mouseover') - cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE') - }) + it('displays tooltip on hover', () => { + cy.get('.runnable-header a').first().trigger('mouseover') + cy.get('.cy-tooltip').first().should('have.text', 'Open in IDE') + }) - itHandlesFileOpening('.runnable-header a', { - file: '/absolute/path/to/foo.js', - line: 0, - column: 0, + itHandlesFileOpening('.runnable-header a', { + file: '/absolute/path/to/foo.js', + line: 0, + column: 0, + }) }) - }) - describe('progress bar', function () { - it('displays', function () { - cy.get('.runnable-active').click() - cy.get('.command-progress').should('be.visible') - }) + describe('progress bar', function () { + it('displays', function () { + cy.get('.runnable-active').click() + cy.get('.command-progress').should('be.visible') + }) - it('calculates correct width', function () { - const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0] + it('calculates correct width', function () { + const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0] - // take the wallClockStartedAt of this command and add 2500 milliseconds to it - // in order to simulate the command having run for 2.5 seconds of the total 4000 timeout - const date = new Date(wallClockStartedAt).setMilliseconds(2500) + // take the wallClockStartedAt of this command and add 2500 milliseconds to it + // in order to simulate the command having run for 2.5 seconds of the total 4000 timeout + const date = new Date(wallClockStartedAt).setMilliseconds(2500) - cy.clock(date, ['Date']) - cy.get('.runnable-active').click() - cy.get('.command-progress > span').should(($span) => { - expect($span.attr('style')).to.contain('animation-duration: 1500ms') - expect($span.attr('style')).to.contain('width: 37.5%') + cy.clock(date, ['Date']) + cy.get('.runnable-active').click() + cy.get('.command-progress > span').should(($span) => { + expect($span.attr('style')).to.contain('animation-duration: 1500ms') + expect($span.attr('style')).to.contain('width: 37.5%') - // ensures that actual width hits 0 within default timeout - expect($span).to.have.css('width', '0px') + // ensures that actual width hits 0 within default timeout + expect($span).to.have.css('width', '0px') + }) }) - }) - it('recalculates correct width after being closed', function () { - const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0] + it('recalculates correct width after being closed', function () { + const { wallClockStartedAt } = this.runnables.suites[0].suites[0].tests[1].commands[0] - // take the wallClockStartedAt of this command and add 1000 milliseconds to it - // in order to simulate the command having run for 1 second of the total 4000 timeout - const date = new Date(wallClockStartedAt).setMilliseconds(1000) + // take the wallClockStartedAt of this command and add 1000 milliseconds to it + // in order to simulate the command having run for 1 second of the total 4000 timeout + const date = new Date(wallClockStartedAt).setMilliseconds(1000) - cy.clock(date, ['Date']) - cy.get('.runnable-active').click() - cy.get('.command-progress > span').should(($span) => { - expect($span.attr('style')).to.contain('animation-duration: 3000ms') - expect($span.attr('style')).to.contain('width: 75%') - }) + cy.clock(date, ['Date']) + cy.get('.runnable-active').click() + cy.get('.command-progress > span').should(($span) => { + expect($span.attr('style')).to.contain('animation-duration: 3000ms') + expect($span.attr('style')).to.contain('width: 75%') + }) - // set the clock ahead as if time has passed - cy.tick(2000) + // set the clock ahead as if time has passed + cy.tick(2000) - cy.get('.runnable-active > .collapsible > .runnable-wrapper').click().click() - cy.get('.command-progress > span').should(($span) => { - expect($span.attr('style')).to.contain('animation-duration: 1000ms') - expect($span.attr('style')).to.contain('width: 25%') + cy.get('.runnable-active > .collapsible > .runnable-wrapper').click().click() + cy.get('.command-progress > span').should(($span) => { + expect($span.attr('style')).to.contain('animation-duration: 1000ms') + expect($span.attr('style')).to.contain('width: 25%') + }) }) }) }) diff --git a/packages/reporter/package.json b/packages/reporter/package.json index 40e046e92453..f2b064992a98 100644 --- a/packages/reporter/package.json +++ b/packages/reporter/package.json @@ -32,11 +32,11 @@ "enzyme-adapter-react-16": "1.15.2", "jsdom": "14.1.0", "lodash": "4.17.19", - "markdown-it": "8.4.2", + "markdown-it": "11.0.0", "mobx": "5.15.4", "mobx-react": "6.1.8", "mocha": "6.2.2", - "prismjs": "1.16.0", + "prismjs": "1.21.0", "prop-types": "15.7.2", "react": "16.8.6", "react-dom": "16.8.6", diff --git a/packages/reporter/src/agents/agents.tsx b/packages/reporter/src/agents/agents.tsx index 7a2db43a7f57..6e786ed81082 100644 --- a/packages/reporter/src/agents/agents.tsx +++ b/packages/reporter/src/agents/agents.tsx @@ -45,7 +45,7 @@ const Agents = observer(({ model }: AgentsProps) => (
  • diff --git a/packages/reporter/src/commands/commands.scss b/packages/reporter/src/commands/commands.scss index d44204db9f74..7694559e58d5 100644 --- a/packages/reporter/src/commands/commands.scss +++ b/packages/reporter/src/commands/commands.scss @@ -40,7 +40,7 @@ padding: 4px 0; &:focus { - outline: 1px dotted #6c6c6c; + outline: none; } > .collapsible-header-inner:focus { diff --git a/packages/reporter/src/routes/routes.tsx b/packages/reporter/src/routes/routes.tsx index dfcb1bb7274e..9b4d1d8b1040 100644 --- a/packages/reporter/src/routes/routes.tsx +++ b/packages/reporter/src/routes/routes.tsx @@ -55,7 +55,7 @@ const Routes = observer(({ model }: RoutesProps) => (
  • diff --git a/packages/reporter/src/runnables/runnable-header.tsx b/packages/reporter/src/runnables/runnable-header.tsx index 2f57fd2e5708..42a8bbb69982 100644 --- a/packages/reporter/src/runnables/runnable-header.tsx +++ b/packages/reporter/src/runnables/runnable-header.tsx @@ -2,7 +2,7 @@ import React, { Component, ReactElement } from 'react' import FileNameOpener from '../lib/file-name-opener' -const renderRunnableHeader = (children:ReactElement) =>
    {children}
    +const renderRunnableHeader = (children: ReactElement) =>
    {children}
    interface RunnableHeaderProps { spec: Cypress.Cypress['spec'] @@ -11,9 +11,16 @@ interface RunnableHeaderProps { class RunnableHeader extends Component { render () { const { spec } = this.props + const relativeSpecPath = spec.relative if (spec.relative === '__all') { + if (spec.specFilter) { + return renderRunnableHeader( + Specs matching "{spec.specFilter}", + ) + } + return renderRunnableHeader( All Specs, ) diff --git a/packages/reporter/src/runnables/runnables.scss b/packages/reporter/src/runnables/runnables.scss index 3a894012ade2..2ae964dba666 100644 --- a/packages/reporter/src/runnables/runnables.scss +++ b/packages/reporter/src/runnables/runnables.scss @@ -108,6 +108,7 @@ &.runnable-pending > div > .runnable-wrapper, &.runnable-pending > div > .runnable-instruments { border-left: 5px solid lighten($pending, 25%); + padding-bottom: 0; } &.runnable-passed > div > .runnable-wrapper, diff --git a/packages/rewriter/__snapshots__/html-spec.ts.js b/packages/rewriter/__snapshots__/html-spec.ts.js index 4b124d2903a3..2731b513b275 100644 --- a/packages/rewriter/__snapshots__/html-spec.ts.js +++ b/packages/rewriter/__snapshots__/html-spec.ts.js @@ -19,7 +19,7 @@ exports['html rewriter .rewriteHtmlJs rewrites a real-ish document with sourcema "url": "http://example.com/foo.html:0", "js": "\n if (top != self) run()\n if (top!=self) run()\n if (self !== top) run()\n if (self!==top) run()\n if (self === top) return\n if (top.location!=self.location&&(top.location.href=self.location.href)) run()\n if (top.location != self.location) run()\n if (top.location != location) run()\n if (self.location != top.location) run()\n if (parent.frames.length > 0) run()\n if (window != top) run()\n if (window.top !== window.self) run()\n if (window.top!==window.self) run()\n if (window.self != window.top) run()\n if (window.top != window.self) run()\n if (window[\"top\"] != window[\"parent\"]) run()\n if (window['top'] != window['parent']) run()\n if (window[\"top\"] != self['parent']) run()\n if (parent && parent != window) run()\n if (parent && parent != self) run()\n if (parent && window != parent) run()\n if (parent && self != parent) run()\n if (parent && parent.frames && parent.frames.length > 0) run()\n if ((self.parent && !(self.parent === self)) && (self.parent.frames.length != 0)) run()\n if (parent !== null && parent.tag !== 'HostComponent' && parent.tag !== 'HostRoot') { }\n if (null !== parent && parent.tag !== 'HostComponent' && parent.tag !== 'HostRoot') { }\n if (top===self) return\n if (top==self) return\n " }, - "html": "\n \n top1\n settop\n settopbox\n parent1\n grandparent\n grandparents\n topFoo\n topFoo.window\n topFoo.window != topFoo\n parentFoo\n parentFoo.window\n parentFoo.window != parentFoo\n\n
    \n
    \n
    \n\n parent()\n foo.parent()\n top()\n foo.top()\n foo(\"parent\")\n foo(\"top\")\n\n const parent = () => { bar: 'bar' }\n\n parent.bar\n\n \n \n" + "html": "\n \n top1\n settop\n settopbox\n parent1\n grandparent\n grandparents\n topFoo\n topFoo.window\n topFoo.window != topFoo\n parentFoo\n parentFoo.window\n parentFoo.window != parentFoo\n\n
    \n
    \n
    \n\n parent()\n foo.parent()\n top()\n foo.top()\n foo(\"parent\")\n foo(\"top\")\n\n const parent = () => { bar: 'bar' }\n\n parent.bar\n\n \n \n\n" } exports['html rewriter .rewriteHtmlJs with inline scripts rewrites inline JS with no type 1'] = ` @@ -33,3 +33,7 @@ exports['html rewriter .rewriteHtmlJs with inline scripts rewrites inline JS wit exports['html rewriter .rewriteHtmlJs with inline scripts does not rewrite non-JS inline 1'] = ` ` + +exports['html rewriter .rewriteHtmlJs with inline scripts rewrites extra long JS string 1'] = ` + +` diff --git a/packages/rewriter/lib/html.ts b/packages/rewriter/lib/html.ts index d3cab45b01e0..1a498323cbbf 100644 --- a/packages/rewriter/lib/html.ts +++ b/packages/rewriter/lib/html.ts @@ -14,7 +14,7 @@ export function HtmlJsRewriter (url: string, deferSourceMapRewrite?: DeferSource return rewriter } -export function rewriteHtmlJs (url: string, html: string, deferSourceMapRewrite?: DeferSourceMapRewriteFn): string { +export function rewriteHtmlJs (url: string, html: string, deferSourceMapRewrite?: DeferSourceMapRewriteFn): Promise { let out = '' const rewriter = HtmlJsRewriter(url, deferSourceMapRewrite) @@ -24,5 +24,9 @@ export function rewriteHtmlJs (url: string, html: string, deferSourceMapRewrite? rewriter.end(html) - return out + return new Promise((resolve) => { + rewriter.on('end', () => { + resolve(out) + }) + }) } diff --git a/packages/rewriter/lib/threads/worker.ts b/packages/rewriter/lib/threads/worker.ts index 24d029827906..dd4de569863d 100644 --- a/packages/rewriter/lib/threads/worker.ts +++ b/packages/rewriter/lib/threads/worker.ts @@ -16,7 +16,7 @@ parentPort!.postMessage(true) let _idCounter = 0 -parentPort!.on('message', (req: RewriteRequest) => { +parentPort!.on('message', async (req: RewriteRequest) => { if (req.shutdown) { return process.exit() } @@ -58,7 +58,7 @@ parentPort!.on('message', (req: RewriteRequest) => { } try { - const output = _getOutput() + const output = await _getOutput() _reply({ output, threadMs: _getThreadMs() }) } catch (error) { diff --git a/packages/rewriter/test/unit/html-spec.ts b/packages/rewriter/test/unit/html-spec.ts index 3d655049ad23..f53e2ae3f82f 100644 --- a/packages/rewriter/test/unit/html-spec.ts +++ b/packages/rewriter/test/unit/html-spec.ts @@ -10,21 +10,21 @@ const rewriteNoSourceMap = (html) => rewriteHtmlJs(URL, html) describe('html rewriter', function () { context('.rewriteHtmlJs', function () { // https://github.com/cypress-io/cypress/issues/2393 - it('strips SRI', function () { - snapshot(rewriteNoSourceMap('')) + snapshot(await rewriteNoSourceMap('')) // should preserve namespaced attrs and still rewrite if no `type` - snapshot(rewriteNoSourceMap('')) + it('rewrites inline JS with no type', async function () { + snapshot(await rewriteNoSourceMap('')) }) - it('rewrites inline JS with type', function () { - snapshot(rewriteNoSourceMap('')) + it('rewrites inline JS with type', async function () { + snapshot(await rewriteNoSourceMap('')) }) - it('does not rewrite non-JS inline', function () { - snapshot(rewriteNoSourceMap('')) + it('does not rewrite non-JS inline', async function () { + snapshot(await rewriteNoSourceMap('')) }) - it('ignores invalid inline JS', function () { + it('ignores invalid inline JS', async function () { const str = '' - expect(rewriteNoSourceMap(str)).to.eq(str) + expect(await rewriteHtmlJs(URL, str)).to.eq(str) + }) + + it('rewrites extra long JS string', async function () { + snapshot(await rewriteNoSourceMap('')) }) }) }) diff --git a/packages/runner/cypress/fixtures/hook_spec.js b/packages/runner/cypress/fixtures/hooks/basic_spec.js similarity index 100% rename from packages/runner/cypress/fixtures/hook_spec.js rename to packages/runner/cypress/fixtures/hooks/basic_spec.js diff --git a/packages/runner/cypress/fixtures/hooks/only_spec.js b/packages/runner/cypress/fixtures/hooks/only_spec.js new file mode 100644 index 000000000000..8bf00531c9c9 --- /dev/null +++ b/packages/runner/cypress/fixtures/hooks/only_spec.js @@ -0,0 +1,31 @@ +describe('test wrapper', () => { + it('test 1', () => { + cy.log('testBody 1') + }) + + describe('nested suite 1', () => { + beforeEach(() => { + cy.log('beforeEachHook 1') + }) + + it.only('test 2', () => { + cy.log('testBody 2') + }) + }) + + describe('nested suite 2', () => { + beforeEach(() => { + cy.log('beforeEachHook 2') + }) + + it('test 3', () => { + cy.log('testBody 3') + }) + }) + + describe('nested suite 3', () => { + it.only('test 4', () => { + cy.log('testBody 4') + }) + }) +}) diff --git a/packages/runner/cypress/fixtures/hooks/skip_spec.js b/packages/runner/cypress/fixtures/hooks/skip_spec.js new file mode 100644 index 000000000000..55fac958f4a6 --- /dev/null +++ b/packages/runner/cypress/fixtures/hooks/skip_spec.js @@ -0,0 +1,15 @@ +describe('outer suite', () => { + it.skip('test 1', () => { + cy.log('testBody 1') + }) + + describe('inner suite', () => { + before(() => { + cy.log('beforeHook 1') + }) + + it('test 2', () => { + cy.log('testBody 2') + }) + }) +}) diff --git a/packages/runner/cypress/integration/reporter.hooks.spec.js b/packages/runner/cypress/integration/reporter.hooks.spec.js index b5bf213b7c90..90ea52a664c0 100644 --- a/packages/runner/cypress/integration/reporter.hooks.spec.js +++ b/packages/runner/cypress/integration/reporter.hooks.spec.js @@ -4,67 +4,150 @@ const { createCypress } = helpers const { runIsolatedCypress } = createCypress() describe('hooks', function () { - beforeEach(function () { - this.editor = {} + describe('displays hooks', function () { + beforeEach(function () { + return runIsolatedCypress(`cypress/fixtures/hooks/basic_spec.js`) + }) - return runIsolatedCypress(`cypress/fixtures/hook_spec.js`, { - onBeforeRun: ({ win }) => { - this.win = win + it('displays commands under correct hook', function () { + cy.contains('tests 1').click() - win.runnerWs.emit.withArgs('get:user:editor') - .yields({ - preferredOpener: this.editor, - }) - }, + cy.contains('before all').closest('.collapsible').should('contain', 'beforeHook 1') + cy.contains('before each').closest('.collapsible').should('contain', 'beforeEachHook 1') + cy.contains('test body').closest('.collapsible').should('contain', 'testBody 1') + cy.contains('after each').closest('.collapsible').should('contain', 'afterEachHook 1') }) - }) - it('displays commands under correct hook', function () { - cy.contains('tests 1').click() + it('displays hooks without number when only one of type', function () { + cy.contains('tests 1').click() + + cy.contains('before all').should('not.contain', '(1)') + cy.contains('before each').should('not.contain', '(1)') + cy.contains('after each').should('not.contain', '(1)') + }) + + it('displays hooks separately with number when more than one of type', function () { + cy.contains('tests 2').click() - cy.contains('before all').closest('.collapsible').should('contain', 'beforeHook 1') - cy.contains('before each').closest('.collapsible').should('contain', 'beforeEachHook 1') - cy.contains('test body').closest('.collapsible').should('contain', 'testBody 1') - cy.contains('after each').closest('.collapsible').should('contain', 'afterEachHook 1') + cy.contains('before all (1)').closest('.collapsible').should('contain', 'beforeHook 2') + cy.contains('before all (2)').closest('.collapsible').should('contain', 'beforeHook 3') + cy.contains('before each (1)').closest('.collapsible').should('contain', 'beforeEachHook 1') + cy.contains('before each (2)').closest('.collapsible').should('contain', 'beforeEachHook 2') + cy.contains('test body').closest('.collapsible').should('contain', 'testBody 2') + cy.contains('after each (1)').closest('.collapsible').should('contain', 'afterEachHook 2') + cy.contains('after each (2)').closest('.collapsible').should('contain', 'afterEachHook 1') + cy.contains('after all (1)').closest('.collapsible').should('contain', 'afterHook 2') + cy.contains('after all (2)').closest('.collapsible').should('contain', 'afterHook 1') + }) }) - it('displays hooks without number when only one of type', function () { - cy.contains('tests 1').click() + describe('open in IDE', function () { + beforeEach(function () { + this.editor = {} + + return runIsolatedCypress(`cypress/fixtures/hooks/basic_spec.js`, { + onBeforeRun: ({ win }) => { + this.win = win - cy.contains('before all').should('not.contain', '(1)') - cy.contains('before each').should('not.contain', '(1)') - cy.contains('after each').should('not.contain', '(1)') + win.runnerWs.emit.withArgs('get:user:editor') + .yields({ + preferredOpener: this.editor, + }) + }, + }) + }) + + it('creates open in IDE button', function () { + cy.contains('tests 1').click() + + cy.get('.hook-open-in-ide').should('have.length', 4) + }) + + it('properly opens file in IDE at hook', function () { + cy.contains('tests 1').click() + + cy.contains('Open in IDE').invoke('show').click().then(function () { + expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include('basic_spec.js') + // chrome sets the column to right before "before(" + // while firefox sets it right after "before(" + expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].column).to.be.eq(Cypress.browser.family === 'firefox' ? 10 : 3) + expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].line).to.be.eq(2) + }) + }) }) - it('displays hooks separately with number when more than one of type', function () { - cy.contains('tests 2').click() - - cy.contains('before all (1)').closest('.collapsible').should('contain', 'beforeHook 2') - cy.contains('before all (2)').closest('.collapsible').should('contain', 'beforeHook 3') - cy.contains('before each (1)').closest('.collapsible').should('contain', 'beforeEachHook 1') - cy.contains('before each (2)').closest('.collapsible').should('contain', 'beforeEachHook 2') - cy.contains('test body').closest('.collapsible').should('contain', 'testBody 2') - cy.contains('after each (1)').closest('.collapsible').should('contain', 'afterEachHook 2') - cy.contains('after each (2)').closest('.collapsible').should('contain', 'afterEachHook 1') - cy.contains('after all (1)').closest('.collapsible').should('contain', 'afterHook 2') - cy.contains('after all (2)').closest('.collapsible').should('contain', 'afterHook 1') + describe('skipped tests', function () { + beforeEach(function () { + return runIsolatedCypress(`cypress/fixtures/hooks/skip_spec.js`) + }) + + it('does not display commands from skipped tests', function () { + cy.contains('test 1').click() + + cy.contains('test 1').parents('.collapsible').first().should('not.contain', 'testBody 1') + }) + + // https://github.com/cypress-io/cypress/issues/8086 + it('displays before hook when following it.skip', function () { + cy.contains('test 2').click() + + cy.contains('test 2').parents('.collapsible').first().should('contain', 'before all') + }) }) - it('creates open in IDE button', function () { - cy.contains('tests 1').click() + describe('only tests', function () { + beforeEach(function () { + return runIsolatedCypress(`cypress/fixtures/hooks/only_spec.js`) + }) + + it('only displays tests with .only', function () { + cy.contains('test wrapper').parents('.collapsible').first().should(($suite) => { + expect($suite).not.to.contain('test 1') + expect($suite).to.contain('nested suite 1') + expect($suite).to.contain('test 2') + expect($suite).not.to.contain('nested suite 2') + expect($suite).not.to.contain('test 3') + expect($suite).to.contain('nested suite 3') + expect($suite).to.contain('test 4') + }) - cy.get('.hook-open-in-ide').should('have.length', 4) + cy.contains('test 2').click() + + cy.contains('test 2').parents('.collapsible').first().should(($test) => { + expect($test).to.contain('before each') + expect($test).to.contain('test body') + }) + }) }) - it('properly opens file in IDE at hook', function () { - cy.contains('tests 1').click() + // https://github.com/cypress-io/cypress/issues/8189 + it('can rerun without timeout error leaking into next run (due to run restart)', () => { + runIsolatedCypress(() => { + const top = window.parent + + top.count = top.count || 0 + + Cypress.config('defaultCommandTimeout', 50) + afterEach(function () { + assert(true, `run ${top.count}`) + }) - cy.contains('Open in IDE').invoke('show').click().then(function () { - expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].file).to.include('hook_spec.js') - // chrome sets the column to right before "before(" - // while firefox sets it right after "before(" - expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].column).to.be.eq(Cypress.browser.family === 'firefox' ? 10 : 3) - expect(this.win.runnerWs.emit.withArgs('open:file').lastCall.args[1].line).to.be.eq(2) + describe('s1', () => { + it('foo', () => { + cy.once('test:after:run', () => { + if (!top.count) { + requestAnimationFrame(() => { + window.parent.eventManager.reporterBus.emit('runner:restart') + }) + } + + top.count++ + }) + }) + }) }) + + // wait until spec has run twice (due to one reload) + cy.window().its('count').should('eq', 2) }) }) diff --git a/packages/runner/cypress/support/helpers.js b/packages/runner/cypress/support/helpers.js index 21ebecd7015e..58b2a8716b80 100644 --- a/packages/runner/cypress/support/helpers.js +++ b/packages/runner/cypress/support/helpers.js @@ -196,8 +196,14 @@ function createCypress (defaultOptions = {}) { }) }) - cy.spy(cy.state('window').console, 'log').as('console_log').log(false) - cy.spy(cy.state('window').console, 'error').as('console_error').log(false) + // TODO: clean this up, sinon doesn't like wrapping things multiple times + // and this catches that error + try { + cy.spy(cy.state('window').console, 'log').as('console_log').log(false) + cy.spy(cy.state('window').console, 'error').as('console_error').log(false) + } catch (_e) { + // console was already wrapped, noop + } autCypress.run((failed) => { resolve({ failed, mochaStubs, autCypress, win }) diff --git a/packages/runner/package.json b/packages/runner/package.json index cf891f393dd8..ffd1b26f405c 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -37,7 +37,7 @@ "mobx": "5.15.4", "mobx-react": "6.1.8", "mocha": "7.0.1", - "prismjs": "1.16.0", + "prismjs": "1.21.0", "prop-types": "15.7.2", "react": "16.8.6", "react-dom": "16.8.6", diff --git a/packages/server/__snapshots__/4_request_spec.ts.js b/packages/server/__snapshots__/4_request_spec.ts.js index bcf2f1e075a8..ae6d4025ded2 100644 --- a/packages/server/__snapshots__/4_request_spec.ts.js +++ b/packages/server/__snapshots__/4_request_spec.ts.js @@ -356,7 +356,7 @@ The response we got was: Status: 404 - Not Found Headers: { "x-powered-by": "Express", - "content-security-policy": "default-src 'self'", + "content-security-policy": "default-src 'none'", "x-content-type-options": "nosniff", "content-type": "text/html; charset=utf-8", "content-length": "301", diff --git a/packages/server/__snapshots__/cypress_spec.js b/packages/server/__snapshots__/cypress_spec.js index c63d647a9a50..5e0b18126f41 100644 --- a/packages/server/__snapshots__/cypress_spec.js +++ b/packages/server/__snapshots__/cypress_spec.js @@ -60,6 +60,7 @@ The ciBuildId is automatically detected if you are running Cypress in any of the - appveyor - azure +- awsCodeBuild - bamboo - bitbucket - buildkite @@ -95,6 +96,7 @@ The ciBuildId is automatically detected if you are running Cypress in any of the - appveyor - azure +- awsCodeBuild - bamboo - bitbucket - buildkite @@ -131,6 +133,7 @@ The ciBuildId is automatically detected if you are running Cypress in any of the - appveyor - azure +- awsCodeBuild - bamboo - bitbucket - buildkite diff --git a/packages/server/lib/config.js b/packages/server/lib/config.js index 8be35b889359..5de52ab03c43 100644 --- a/packages/server/lib/config.js +++ b/packages/server/lib/config.js @@ -76,6 +76,7 @@ viewportHeight responseTimeout video taskTimeout videoCompression videoUploadOnPasses +screenshotOnRunFailure watchForFileChanges waitForAnimations resolvedNodeVersion nodeVersion resolvedNodePath @@ -144,6 +145,7 @@ const CONFIG_DEFAULTS = { video: true, videoCompression: 32, videoUploadOnPasses: true, + screenshotOnRunFailure: true, modifyObstructiveCode: true, chromeWebSecurity: true, waitForAnimations: true, @@ -214,6 +216,7 @@ const validationRules = { videoCompression: v.isNumberOrFalse, videosFolder: v.isString, videoUploadOnPasses: v.isBoolean, + screenshotOnRunFailure: v.isBoolean, viewportHeight: v.isNumber, viewportWidth: v.isNumber, waitForAnimations: v.isBoolean, diff --git a/packages/server/lib/controllers/files.js b/packages/server/lib/controllers/files.js index 6f7fe9ffa78a..8c0a95e64024 100644 --- a/packages/server/lib/controllers/files.js +++ b/packages/server/lib/controllers/files.js @@ -23,20 +23,25 @@ module.exports = { }) }, - handleIframe (req, res, config, getRemoteState) { + handleIframe (req, res, config, getRemoteState, extraOptions) { const test = req.params[0] const iframePath = cwd('lib', 'html', 'iframe.html') + const specFilter = _.get(extraOptions, 'specFilter') - debug('handle iframe %o', { test }) + debug('handle iframe %o', { test, specFilter }) - return this.getSpecs(test, config) + return this.getSpecs(test, config, extraOptions) .then((specs) => { return this.getJavascripts(config) .then((js) => { + const allFilesToSend = js.concat(specs) + + debug('all files to send %o', _.map(allFilesToSend, 'relative')) + const iframeOptions = { title: this.getTitle(test), domain: getRemoteState().domainName, - scripts: JSON.stringify(js.concat(specs)), + scripts: JSON.stringify(allFilesToSend), } debug('iframe %s options %o', test, iframeOptions) @@ -46,8 +51,8 @@ module.exports = { }) }, - getSpecs (spec, config) { - debug('get specs %o', { spec }) + getSpecs (spec, config, extraOptions = {}) { + debug('get specs %o', { spec, extraOptions }) const convertSpecPath = (spec) => { // get the absolute path to this spec and @@ -59,15 +64,30 @@ module.exports = { return this.prepareForBrowser(convertedSpec, config.projectRoot) } + const specFilter = _.get(extraOptions, 'specFilter') + + debug('specFilter %o', { specFilter }) + const specFilterContains = (spec) => { + // only makes sense if there is specFilter string + // the filter should match the logic in + // desktop-gui/src/specs/specs-store.js + return spec.relative.toLowerCase().includes(specFilter.toLowerCase()) + } + const specFilterFn = specFilter ? specFilterContains : R.T + const getSpecsHelper = () => { // grab all of the specs if this is ci const experimentalComponentTestingEnabled = _.get(config, 'resolved.experimentalComponentTesting.value', false) if (spec === '__all') { + debug('returning all specs') + return specsUtil.find(config) .then(R.tap((specs) => { return debug('found __all specs %o', specs) - })).filter((spec) => { + })) + .filter(specFilterFn) + .filter((spec) => { if (experimentalComponentTestingEnabled) { return spec.specType === 'integration' } diff --git a/packages/server/lib/gui/events.js b/packages/server/lib/gui/events.js index fdd30230c632..57038921675e 100644 --- a/packages/server/lib/gui/events.js +++ b/packages/server/lib/gui/events.js @@ -132,12 +132,18 @@ const handleEvent = function (options, bus, event, id, type, arg) { case 'launch:browser': // is there a way to lint the arguments received? debug('launching browser for \'%s\' spec: %o', arg.specType, arg.spec) + debug('full list of options %o', arg) + // the "arg" should have objects for // - browser // - spec (with fields) // name, absolute, relative // - specType: "integration" | "component" - const fullSpec = _.merge({}, arg.spec, { specType: arg.specType }) + // - specFilter (optional): the string user searched for + const fullSpec = _.merge({}, arg.spec, { + specType: arg.specType, + specFilter: arg.specFilter, + }) return openProject.launch(arg.browser, fullSpec, { projectRoot: options.projectRoot, diff --git a/packages/server/lib/project.js b/packages/server/lib/project.js index 60d6e2b4504a..b734863ec39e 100644 --- a/packages/server/lib/project.js +++ b/packages/server/lib/project.js @@ -501,6 +501,8 @@ class Project extends EE { } getSpecUrl (absoluteSpecPath, specType) { + debug('get spec url: %s for spec type %s', absoluteSpecPath, specType) + return this.getConfig() .then((cfg) => { // if we don't have a absoluteSpecPath or its __all diff --git a/packages/server/lib/routes.js b/packages/server/lib/routes.js index 6c69630b8739..21b571c7c20b 100644 --- a/packages/server/lib/routes.js +++ b/packages/server/lib/routes.js @@ -1,6 +1,7 @@ const path = require('path') const la = require('lazy-ass') const check = require('check-more-types') +const _ = require('lodash') const debug = require('debug')('cypress:server:routes') const AppData = require('./util/app_data') @@ -46,7 +47,17 @@ module.exports = ({ app, config, getRemoteState, networkProxy, project, onError // routing for the dynamic iframe html app.get('/__cypress/iframes/*', (req, res) => { - files.handleIframe(req, res, config, getRemoteState) + const extraOptions = { + specFilter: _.get(project, 'spec.specFilter'), + } + + debug('project %o', project) + debug('handling iframe for project spec %o', { + spec: project.spec, + extraOptions, + }) + + files.handleIframe(req, res, config, getRemoteState, extraOptions) }) app.all('/__cypress/xhrs/*', (req, res, next) => { diff --git a/packages/server/lib/util/ci_provider.js b/packages/server/lib/util/ci_provider.js index dfdf244b098b..0b84a6042e4e 100644 --- a/packages/server/lib/util/ci_provider.js +++ b/packages/server/lib/util/ci_provider.js @@ -30,6 +30,12 @@ const isAzureCi = () => { return process.env.TF_BUILD && process.env.AZURE_HTTP_USER_AGENT } +const isAWSCodeBuild = () => { + return _.some(process.env, (val, key) => { + return /^CODEBUILD_/.test(key) + }) +} + const isBamboo = () => { return process.env.bamboo_buildNumber } @@ -82,6 +88,7 @@ const isWercker = () => { const CI_PROVIDERS = { 'appveyor': 'APPVEYOR', 'azure': isAzureCi, + 'awsCodeBuild': isAWSCodeBuild, 'bamboo': isBamboo, 'bitbucket': 'BITBUCKET_BUILD_NUMBER', 'buildkite': 'BUILDKITE', @@ -139,6 +146,13 @@ const _providerCiParams = () => { 'BUILD_CONTAINERID', 'BUILD_REPOSITORY_URI', ]), + awsCodeBuild: extract([ + 'CODEBUILD_BUILD_ID', + 'CODEBUILD_BUILD_NUMBER', + 'CODEBUILD_RESOLVED_SOURCE_VERSION', + 'CODEBUILD_SOURCE_REPO_URL', + 'CODEBUILD_SOURCE_VERSION', + ]), bamboo: extract([ 'bamboo_buildNumber', 'bamboo_buildResultsUrl', @@ -375,6 +389,15 @@ const _providerCommitParams = () => { // remoteOrigin: ??? // defaultBranch: ??? }, + awsCodeBuild: { + sha: env.CODEBUILD_RESOLVED_SOURCE_VERSION, + // branch: ???, + // message: ??? + // authorName: ??? + // authorEmail: ??? + remoteOrigin: env.CODEBUILD_SOURCE_REPO_URL, + // defaultBranch: ??? + }, azure: { sha: env.BUILD_SOURCEVERSION, branch: env.BUILD_SOURCEBRANCHNAME, diff --git a/packages/server/lib/util/escape_filename.ts b/packages/server/lib/util/escape_filename.ts index 30216e4ff2ca..c75b0a7ddfbb 100644 --- a/packages/server/lib/util/escape_filename.ts +++ b/packages/server/lib/util/escape_filename.ts @@ -1,6 +1,7 @@ const ampersandRe = /&/g const percentRe = /%/g const questionRe = /\?/g +const plusRe = /\+/g export function escapeFilenameInUrl (url: string) { // escape valid file name characters that cannot be used in URL @@ -8,4 +9,5 @@ export function escapeFilenameInUrl (url: string) { .replace(percentRe, '%25') // % .replace(ampersandRe, '%26') // & .replace(questionRe, '%3F') // ? -> it's only valid in Linux + .replace(plusRe, '%2B') // + https://github.com/cypress-io/cypress/issues/5909 } diff --git a/packages/server/package.json b/packages/server/package.json index c23b45f001f9..153f5c2df9b5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -50,11 +50,11 @@ "debug": "4.1.1", "dependency-tree": "7.0.2", "duplexify": "4.1.1", - "electron-context-menu": "0.15.1", + "electron-context-menu": "2.2.0", "errorhandler": "1.5.1", "evil-dns": "0.2.0", "execa": "1.0.0", - "express": "4.16.4", + "express": "4.17.1", "find-process": "1.4.1", "firefox-profile": "2.0.0", "fix-path": "3.0.0", @@ -70,7 +70,7 @@ "image-size": "0.8.3", "is-fork-pr": "2.5.0", "is-html": "2.0.0", - "jimp": "0.13.0", + "jimp": "0.14.0", "jsonlint": "1.6.3", "konfig": "0.2.1", "launch-editor": "2.2.1", @@ -79,14 +79,14 @@ "lodash": "4.17.19", "log-symbols": "2.2.0", "marionette-client": "cypress-io/marionette-client#2cddf7d791cca7be5191d7fe103d58be7283957d", - "md5": "2.2.1", + "md5": "2.3.0", "mime": "2.4.4", "minimatch": "3.0.4", "minimist": "1.2.5", "mocha-7.0.1": "npm:mocha@7.0.1", "mocha-junit-reporter": "1.23.1", "mocha-teamcity-reporter": "3.0.0", - "moment": "2.26.0", + "moment": "2.27.0", "morgan": "1.9.1", "node-machine-id": "1.1.12", "node-webkit-updater": "cypress-io/node-webkit-updater#e74623726f381487f543e373e71515177a32daeb", diff --git a/packages/server/test/e2e/3_issue_8111_spec.js b/packages/server/test/e2e/3_issue_8111_spec.js new file mode 100644 index 000000000000..f775270ef993 --- /dev/null +++ b/packages/server/test/e2e/3_issue_8111_spec.js @@ -0,0 +1,16 @@ +const e2e = require('../support/helpers/e2e').default +const Fixtures = require('../support/helpers/fixtures') + +describe('e2e issue 8111 iframe input focus', function () { + e2e.setup() + + e2e.it('iframe input retains focus when browser is out of focus', { + // this test is dependent on the browser being Chrome headed + // and also having --auto-open-devtools-for-tabs plugins option + // (which pulls focus from main browser window) + project: Fixtures.projectPath('issue-8111-iframe-input'), + spec: 'iframe_input_spec.js', + browser: 'chrome', + headed: true, + }) +}) diff --git a/packages/server/test/e2e/4_controllers_spec.js b/packages/server/test/e2e/4_controllers_spec.js index 9d1c83ff4f84..d3d67403b412 100644 --- a/packages/server/test/e2e/4_controllers_spec.js +++ b/packages/server/test/e2e/4_controllers_spec.js @@ -20,8 +20,8 @@ describe('e2e plugins', () => { }) }) - it('handles specs with $, &, ? in file name', function () { - let relativeSpecPath = path.join('d?ir&1%', '%di?r2&', 's%pec&?.js') + it('handles specs with $, &, ?, + in file name', function () { + let relativeSpecPath = path.join('d?ir&1%', '%di?r2&', 's%p+ec&?.js') // windows doesn't support ? in file names if (process.platform === 'win32') { diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index 501aaaafe979..851835bfa5a7 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -159,7 +159,7 @@ describe('Routes', () => { } const open = () => { - const project = new Project('/path/to/project') + this.project = new Project('/path/to/project') return Promise.all([ // open our https server @@ -168,7 +168,7 @@ describe('Routes', () => { // and open our cypress server (this.server = new Server(new Watchers())), - this.server.open(cfg, project) + this.server.open(cfg, this.project) .spread((port) => { if (initialUrl) { this.server._onDomainSet(initialUrl) @@ -202,6 +202,7 @@ describe('Routes', () => { Fixtures.remove() this.session.destroy() preprocessor.close() + this.project = null return Promise.join( this.server.close(), @@ -1137,6 +1138,26 @@ describe('Routes', () => { expect(body).to.eq(contents) }) }) + + it('can send back tests matching spec filter', function () { + // only returns tests with "sub_test" in their names + const contents = removeWhitespace(Fixtures.get('server/expected_todos_filtered_tests_iframe.html')) + + this.project.spec = { + specFilter: 'sub_test', + } + + return this.rp('http://localhost:2020/__cypress/iframes/__all') + .then((res) => { + expect(res.statusCode).to.eq(200) + + const body = cleanResponseBody(res.body) + + console.log(body) + + expect(body).to.eq(contents) + }) + }) }) describe('no-server', () => { diff --git a/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress.json b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress.json new file mode 100644 index 000000000000..9c5417cd8268 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress.json @@ -0,0 +1,3 @@ +{ + "supportFolder": false +} diff --git a/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress/integration/iframe_input_spec.js b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress/integration/iframe_input_spec.js new file mode 100644 index 000000000000..c605183f66e3 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress/integration/iframe_input_spec.js @@ -0,0 +1,8 @@ +it('can type into an input in an iframe that calls auto focus', () => { + cy.visit('/outer.html') + cy.get('iframe') + .its('0.contentDocument.body').should('not.be.empty') + .then(cy.wrap) + .find('input') + .type(42) +}) diff --git a/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress/plugins/index.js b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress/plugins/index.js new file mode 100644 index 000000000000..f04098567ec3 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/cypress/plugins/index.js @@ -0,0 +1,7 @@ +module.exports = (on) => { + on('before:browser:launch', (browser, launchOptions) => { + launchOptions.args.push('--auto-open-devtools-for-tabs') + + return launchOptions + }) +} diff --git a/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/inner.html b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/inner.html new file mode 100644 index 000000000000..1394a7ef8fd2 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/inner.html @@ -0,0 +1,4 @@ + + diff --git a/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/outer.html b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/outer.html new file mode 100644 index 000000000000..7c17c2c77b97 --- /dev/null +++ b/packages/server/test/support/fixtures/projects/issue-8111-iframe-input/outer.html @@ -0,0 +1 @@ +