Merge branch 'develop' into v4.0-release

This commit is contained in:
Chris Breiding
2019-12-09 13:46:05 -05:00
441 changed files with 19808 additions and 16369 deletions
+1 -2
View File
@@ -4,6 +4,5 @@
],
"extends": [
"plugin:@cypress/dev/general"
],
"rules": {}
]
}
+43
View File
@@ -0,0 +1,43 @@
<!--
this comment will be posted automatically by Cypress bot whenever someone opens a pull request,
and it helps the reviewer from Cypress team to ensure the change is solid.
-->
Thanks for the contribution! Below are some guidelines Cypress uses when doing PR reviews.
- Please write \`[WIP]\` in the title of your Pull Request if your PR is not ready for review - someone will review your PR as soon as the \`[WIP]\` is removed.
- Please familiarize yourself with the PR Review Checklist and feel free to make updates on your PR based on these guidelines.
## PR Review Checklist
If any of the following requirements can't be met, leave a comment in the review selecting 'Request changes', otherwise 'Approve'.
### User Experience
- The feature/bugfix is self-documenting from within the product.
- The change provides the end user with a way to fix their problem (no dead ends).
### Functionality
- The code works and performs its intended function with the correct logic.
- Performance has been factored in (for example, the code cleans up after itself to not cause memory leaks).
- The code guards against edge cases and invalid input and has tests to cover it.
### Maintainability
- The code is readable (too many nested 'if's are a bad sign).
- Names used for variables, methods, etc, clearly describe their function.
- The code is easy to understood and there are relevant comments explaining.
- New algorithms are documented in the code with link(s) to external docs (flowcharts, w3c, chrome, firefox).
- There are comments containing link(s) to the addressed issue (in tests and code).
### Quality
- The change does not reimplement code.
- There's not a module from the ecosystem that should be used instead.
- There is no redundant or duplicate code.
- There are no irrelevant comments left in the code.
- Tests are testing the codes intended functionality in the best way possible.
### Internal
- The original issue has been tagged with a release in ZenHub.
@@ -0,0 +1,18 @@
<!--
this comment will be posted automatically by Cypress bot whenever a dependency update pull request is opened,
and it helps the reviewer from Cypress team to ensure the update won't have unexpected consequences.
-->
Below are some guidelines Cypress uses when reviewing dependency updates.
## Dependency Update Instructions
- Read through the entire changelog of the dependency's changes. If a changelog is not available, check every commit made to the dependency. **NOTE** - do not rely on semver to indicate breaking changes - every product does not follow this standard.
- Add a PR review comment noting any relevant changes in the dependency.
- If any of the following requirements cannot be met, leave a comment in the review selecting 'Request changes', otherwise 'Approve'.
## Dependency Updates Checklist
- Code using the dependency has been updated to accommodate any breaking changes
- The dependency still supports the version of Node that the package requires.
- The PR been tagged with a release in ZenHub.
- Appropriate labels have been added to the PR (for example: label \`type: breaking change\` if it is a breaking change)
@@ -1,7 +1,7 @@
<!--
Thanks for contributing!
Read our contribution guidelines here:
https://github.com/cypress-io/cypress/blob/develop/CONTRIBUTING.md
https://github.com/cypress-io/cypress/blob/develop/.github/CONTRIBUTING.md
-->
<!-- Example: "Closes #1234" -->
@@ -38,6 +38,7 @@ Delete tasks if they are not applicable.
-->
- [ ] Have tests been added/updated?
- [ ] Has the original issue been tagged with a release in ZenHub? <!-- (internal team only)-->
- [ ] Has a PR for user-facing changes been opened in [`cypress-documentation`](https://github.com/cypress-io/cypress-documentation)? <!-- Link to PR here -->
- [ ] Have API changes been updated in the [`type definitions`](cli/types/index.d.ts)?
- [ ] Have new configuration options been added to the [`cypress.schema.json`](cli/schema/cypress.schema.json)?
-3
View File
@@ -25,9 +25,6 @@ packages/https-proxy/ca/
packages/desktop-gui/cypress/videos
packages/desktop-gui/src/jsconfig.json
# from driver
packages/driver/test/cypress/videos
# from example
packages/example/app
packages/example/build
+5 -5
View File
@@ -5,8 +5,8 @@ Thanks for taking the time to contribute! :smile:
**Once you learn how to use Cypress, you can contribute in many ways:**
- Join the [Cypress Gitter chat](https://on.cypress.io/chat) and answer questions. Teaching others how to use Cypress is a great way to learn more about how it works.
- Blog about Cypress. We display blogs featuring Cypress on our [Examples](https://on.cypress.io/examples) page. If you'd like your blog featured, [open a PR to add it to our docs](https://github.com/cypress-io/cypress-documentation/blob/develop/CONTRIBUTING.md#adding-examples).
- Write some documentation or improve our existing docs. Know another language? You can help us translate them. See our [guide to contributing to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md).
- Blog about Cypress. We display blogs featuring Cypress on our [Examples](https://on.cypress.io/examples) page. If you'd like your blog featured, [open a PR to add it to our docs](https://github.com/cypress-io/cypress-documentation/blob/develop/.github/CONTRIBUTING.md#adding-examples).
- Write some documentation or improve our existing docs. Know another language? You can help us translate them. See our [guide to contributing to our docs](https://github.com/cypress-io/cypress-documentation/blob/master/.github/CONTRIBUTING.md).
- Give a talk about Cypress. [Contact us](mailto:support@cypress.io) ahead of time and we'll send you some swag. :shirt:
**Want to dive deeper into how Cypress works? There are several ways you can help with the development of Cypress:**
@@ -53,7 +53,7 @@ Build status | Description
## Code of Conduct
All contributors are expecting to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
All contributors are expecting to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md).
## Opening Issues
@@ -225,7 +225,7 @@ Some issues are resolved by the community, by giving some guidance or a workarou
## Writing Documentation
Cypress documentation lives in a separate repository with its own dependencies and build tools.
See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md).
See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/.github/CONTRIBUTING.md).
## Writing code
@@ -501,7 +501,7 @@ After a PR has been opened for a dependency update, our `cypress-bot` will comme
## Deployment
We will try to review and merge pull requests quickly. After merging we will try releasing a new version. If you want to know our build process or build your own Cypress binary, read [DEPLOY.md](DEPLOY.md)
We will try to review and merge pull requests quickly. After merging we will try releasing a new version. If you want to know our build process or build your own Cypress binary, read [DEPLOY.md](./DEPLOY.md)
## Known problems
+56 -39
View File
@@ -1,7 +1,7 @@
# Deployment
Anyone can build the binary and NPM package, but you can only deploy the Cypress application
and publish the NPM module `cypress` if you are a member of `cypress` NPM organization.
and publish the NPM module `cypress` if you are a member of the `cypress` NPM organization.
> :information_source: See the [publishing](#publishing) section for how to build, test and publish a
new official version of the binary and `cypress` NPM package.
@@ -12,7 +12,11 @@ We build the NPM package and binary on all major platforms (Linux, Mac, Windows)
providers. In order to set the version while building we have to set the environment variable
with the new version on each CI provider *before starting the build*.
Use script command `npm run set-next-ci-version` to do this.
Use the script command below to to do this.
```shell
npm run set-next-ci-version
```
## Building
@@ -28,12 +32,12 @@ Building a new NPM package is very quick.
The steps above:
- Build the `cypress` NPM package
- Transpiles the code into ES5 version to be compatible with the common Node versions
- Puts the result into the `cli/build` folder.
- Transpile the code into ES5 to be compatible with the common Node versions
- Put the result into the [`cli/build`](./cli/build) folder.
You could publish from there, but first you need to build and upload the binary with the *same version*;
this guarantees that when users do `npm i cypress@<x.y.z>` they can download the binary
with the same version `x.y.z` from Cypress CDN service.
with the same version `x.y.z` from Cypress's CDN service.
### Building the binary
@@ -43,13 +47,13 @@ First, you need to build, zip and upload the application binary to the Cypress s
You can use a single command to do all tasks at once:
```
```shell
npm run binary-deploy
```
You can also specify each command separately:
Or you can specify each command separately:
```
```shell
npm run binary-build
npm run binary-zip
npm run binary-upload
@@ -57,7 +61,7 @@ npm run binary-upload
You can pass options to each command to avoid answering questions, for example
```
```shell
npm run binary-deploy -- --platform darwin --version 0.20.0
npm run binary-upload -- --platform darwin --version 0.20.0 --zip cypress.zip
```
@@ -65,15 +69,13 @@ npm run binary-upload -- --platform darwin --version 0.20.0 --zip cypress.zip
If something goes wrong, see the debug messages using the `DEBUG=cypress:binary ...` environment
variable.
Because we had many problems reliably zipping the built binary, for now we need
to build both the Mac and Linux binary from Mac (Linux binary is built using
a Docker container), then zip it **from Mac**, then upload it.
Because we had many problems reliably zipping the built binary, for now we need to build both the Mac and Linux binary from Mac (Linux binary is built using a Docker container), then zip it **from Mac**, then upload it.
### Building Linux binary in Docker
If you are using a Mac you can build the linux binary if you have docker installed.
```
```shell
npm run binary-build-linux
```
@@ -81,29 +83,28 @@ npm run binary-build-linux
### Before Publishing a New Version
In order to publish a new `cypress` package to the NPM registry, we must build and test it across
multiple platforms and test projects. This makes publishing *directly* into the NPM registry
impossible. Instead, we have CI set up to do the following on every commit to `develop`:
In order to publish a new `cypress` package to the NPM registry, we must build and test it across multiple platforms and test projects. This makes publishing *directly* into the NPM registry impossible. Instead, we have CI set up to do the following on every commit to `develop`:
1. Build the NPM package with the new target version baked in.
2. Build the Linux/Mac binaries on CircleCI and build Windows on AppVeyor.
3. Upload the binaries and the new NPM package to the `cdn.cypress.io` under the "beta" folder.
3. Upload the binaries and the new NPM package to `cdn.cypress.io` under the "beta" folder.
4. Launch the test projects like [cypress-test-node-versions](https://github.com/cypress-io/cypress-test-node-versions) and [cypress-test-example-repos](https://github.com/cypress-io/cypress-test-example-repos) using the newly-uploaded package & binary instead of installing from the NPM registry. That installation looks like this:
```
```shell
export CYPRESS_INSTALL_BINARY=https://cdn.../binary/<new version>/<commit hash>/cypress.zip
npm i https://cdn.../npm/<new version>/<commit hash>/cypress.tgz
```
Multiple test projects are launched for each target operating system, and the results are reported
back to GitHub using status checks so that it is easy to see if a change has broken real-world usage
Multiple test projects are launched for each target operating system and the results are reported
back to GitHub using status checks so that you can see if a change has broken real-world usage
of Cypress. You can see the progress of the test projects by opening the status checks on GitHub:
![Screenshot of status checks](https://i.imgur.com/AsQwzgO.png)
Once all test projects are reliably working with new changes, publishing can proceed.
Once the `develop` branch for all test projects are reliably passing with the new changes, publishing can proceed.
### Steps to Publish a New Version
0. Make sure that if there is a new [`cypress-example-kitchensink`][https://github.com/cypress-io/cypress-example-kitchensink/releases] version, the corresponding dependency in [`packages/example`](./packages/example) has been updated to that new version.
1. Make sure that you have the correct environment variables set up before proceeding.
- You'll need Cypress AWS access keys in `aws_credentials_json`, which looks like this:
```text
@@ -117,42 +118,58 @@ Once all test projects are reliably working with new changes, publishing can pro
- Tip: Use [as-a](https://github.com/bahmutov/as-a) to manage environment variables for different situations.
2. Use the `move-binaries` script to move the binaries for `<commit sha>` from `beta` to the `desktop` folder
for `<new target version>`
```
```shell
npm run move-binaries -- --sha <commit sha> --version <new target version>
```
3. Publish the new NPM package under the dev tag. The unique link to the package file `cypress.tgz`
is the one already tested above. You can publish to the NPM registry straight from the URL:
```
3. Publish the new NPM package under the `dev` tag. The unique link to the package file `cypress.tgz` is the one already tested above. You can publish to the NPM registry straight from the URL:
```shell
npm publish https://cdn.../npm/3.4.0/<long sha>/cypress.tgz --tag dev
```
4. Double-check that the new version has been published under the `dev` tag using `npm info cypress` or [available-versions](https://github.com/bahmutov/available-versions):
```
```shell
dist-tags:
dev: 3.4.0 latest: 3.3.2
```
5. Test `cypress@3.4.0` again to make sure everything is working. You can trigger test projects
from the command line (if you have the appropriate permissions)
5. Test `cypress@3.4.0` again to make sure everything is working. You can trigger test projects from the command line (if you have the appropriate permissions)
```
node scripts/test-other-projects.js --npm cypress@3.4.0 --binary 3.4.0
```
6. Update and publish the changelog and any release-specific documentation changes in [cypress-documentation](https://github.com/cypress-io/cypress-documentation).
7. Make the new NPM version the "latest" version by updating the dist-tag `latest` to point to the new version:
```
6. Test the new version of Cypress against the Cypress dashboard repo.
7. Update and publish the changelog and any release-specific documentation changes in [cypress-documentation](https://github.com/cypress-io/cypress-documentation).
8. Make the new NPM version the "latest" version by updating the dist-tag `latest` to point to the new version:
```shell
npm dist-tag add cypress@3.4.0
```
8. Run `binary-release` to update the download server's manifest, set the next CI version, and create an empty version commit:
```
9. Run `binary-release` to update the download the server's manifest, set the next CI version, and create an empty version commit:
```shell
npm run binary-release -- --version 3.4.0 --commit`
```
9. Tag the current commit with `v3.4.0` and push that tag up.
10. If needed, push out the updated changes to the docs manifest to `on.cypress.io`.
11. If needed, push out an updated kitchen sink.
12. Close the release in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release).
13. Bump `version` in `package.json` from `develop` branch and then merge into `master`.
14. Using [cypress-io/release-automations][release-automations]:
10. If needed, push out any updated changes to the links manifest to [`on.cypress.io`](https://github.com/cypress-io/cypress-services/tree/develop/packages/on).
11. If needed, deploy the updated [`cypress-example-kitchensink`][cypress-example-kitchensink] to `example.cypress.io` by following [these instructions under "Deployment"](./packages/example/README.md).
12. Update the releases in [ZenHub](https://app.zenhub.com/workspaces/test-runner-5c3ea3baeb1e75374f7b0708/reports/release):
- Close the current release in ZenHub.
- Create a new patch release (and a new minor release, if this is a minor release) in ZenHub, and schedule them both to be completed 2 weeks from the current date.
13. Bump `version` in [`package.json`](package.json) and commit it to `develop` using a commit message like `release 3.4.0 [skip ci]`
14. Tag this commit with `v3.4.0` and push that tag up.
15. Merge `develop` into `master` and push that branch up.
16. Using [cypress-io/release-automations][release-automations]:
- Publish GitHub release to [cypress-io/cypress/releases](https://github.com/cypress-io/cypress/releases) using package `set-releases` (see its README for details).
- Add a comment to each GH issue that has been resolved with the new published version using package `issues-in-release` (see its README for details)
17. Publish a new docker image in [`cypress-docker-images`](https://github.com/cypress-io/cypress-docker-images) under `included` for the new cypress version.
18. Decide on the next version that we will work on. For example, if we have just released `3.7.0` we probably will work on `3.7.1` next. Set it on [CI machines](#set-next-version-on-cis).
19. Try updating as many example projects to the new version. You probably want to update by using Renovate dependency issue like [`cypress-example-todomvc` "Update Dependencies (Renovate Bot)](https://github.com/cypress-io/cypress-example-todomvc/issues/99). Try updating at least the following projects:
- https://github.com/cypress-io/cypress-example-todomvc
- https://github.com/cypress-io/cypress-example-todomvc-redux
- https://github.com/cypress-io/cypress-example-realworld
- https://github.com/cypress-io/cypress-example-recipes
- https://github.com/cypress-io/cypress-example-docker-compose
- https://github.com/cypress-io/cypress-example-api-testing
- https://github.com/cypress-io/angular-pizza-creator
- https://github.com/cypress-io/cypress-fiddle
- https://github.com/cypress-io/cypress-example-piechopper
- https://github.com/cypress-io/cypress-documentation
Take a break, you deserve it! :sunglasses:
[release-automations]: https://github.com/cypress-io/release-automations
[cypress-example-kitchensink]: https://github.com/cypress-io/cypress-example-kitchensink
+1 -1
View File
@@ -53,7 +53,7 @@ npm install cypress --save-dev
- [![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/develop.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress/tree/develop) - `develop` branch
- [![CircleCI](https://circleci.com/gh/cypress-io/cypress/tree/master.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress/tree/master) - `master` branch
Please see our [Contributing Guideline](/CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps.
Please see our [Contributing Guideline](./CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps.
## License
+1
View File
@@ -53,6 +53,7 @@ install:
- node --version
- node --print process.arch
- npm --version
- npm run check-next-dev-version
# prints all public variables relevant to the build
- print-env Platform
- npm run check-node-version
+5
View File
@@ -13,5 +13,10 @@ module.exports = {
path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-multi-assign-class-export.js'),
path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-implicit-return-assignment.js'),
path.resolve('node_modules', 'jscodemods', 'decaffeinate', 'fix-existential-conditional-assignment.js'),
'./scripts/decaff/remove-comment-sharp.js',
'./scripts/decaff/switch-false.js',
'./scripts/decaff/empty-catch.js',
'./scripts/decaff/no-cond-assign.js',
'./scripts/decaff/arrow-comment.js',
],
}
+6 -3
View File
@@ -290,6 +290,8 @@ jobs:
- run: npm run test-mocha-snapshot
# make sure packages with TypeScript can be transpiled to JS
- run: npm run all build-js
# test codemods
- run: npm run test-jscodeshift
# run unit tests from individual packages
- run: npm run all test -- --package cli
- run: npm run all test -- --package electron
@@ -297,7 +299,7 @@ jobs:
- run: npm run all test -- --package https-proxy
- run: npm run all test -- --package launcher
- run: npm run all test -- --package network
# how to pass Mocha reporter through zunder?
- run: npm run all test -- --package proxy
- run: npm run all test -- --package reporter
- run: npm run all test -- --package runner
- run: npm run all test -- --package socket
@@ -714,9 +716,10 @@ jobs:
steps:
- attach_workspace:
at: ~/
- run: npm run check-next-dev-version
- run:
name: bump NPM version
command: npm --no-git-tag-version version ${NEXT_DEV_VERSION:-0.0.0-development}
command: npm --no-git-tag-version --allow-same-version version ${NEXT_DEV_VERSION:-0.0.0-development}
- run:
name: build NPM package
working_directory: cli
@@ -1121,7 +1124,7 @@ mac-workflow: &mac-workflow
branches:
only:
- develop
- lint:
name: Mac lint
executor: mac
+265 -237
View File
@@ -1,26 +1,178 @@
exports['cli --version no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
exports['shows help for open --foo 1'] = `
command: bin/cypress open --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: cypress open [options]
Opens Cypress in the interactive GUI.
Options:
-b, --browser <browser-path> path to a custom browser to be added to the
list of available browsers in Cypress
-c, --config <config> sets configuration values. separate multiple
values with a comma. overrides any value in
cypress.json.
-C, --config-file <config-file> path to JSON file where configuration values
are set. defaults to "cypress.json". pass
"false" to disable.
-d, --detached [bool] runs Cypress application in detached mode
-e, --env <env> sets environment variables. separate
multiple values with a comma. overrides any
value in cypress.json or cypress.env.json
--global force Cypress into global mode as if its
globally installed
-p, --port <port> runs Cypress on a specific port. overrides
any value in cypress.json.
-P, --project <project-path> path to the project
--dev runs cypress in development and bypasses
binary check
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli -v no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
exports['shows help for run --foo 1'] = `
command: bin/cypress run --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: cypress run [options]
Runs Cypress tests from the CLI without the GUI
Options:
-b, --browser <browser-name-or-path> runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.
--ci-build-id <id> the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers
-c, --config <config> sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.
-C, --config-file <config-file> path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.
-e, --env <env> sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json
--group <name> a named group for recorded runs in the Cypress Dashboard
-k, --key <record-key> your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.
--headed displays the Electron browser instead of running headlessly
--no-exit keep the browser open after tests finish
--parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes
-p, --port <port> runs Cypress on a specific port. overrides any value in cypress.json.
-P, --project <project-path> path to the project
--record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard.
-r, --reporter <reporter> runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"
-o, --reporter-options <reporter-options> options for the mocha reporter. defaults to "null"
-s, --spec <spec> runs specific spec file(s). defaults to "all"
-t, --tag <tag> named tag(s) for recorded runs in the Cypress Dashboard
--dev runs cypress in development and bypasses binary check
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli cypress run warns with space-separated --specs 1'] = `
⚠ Warning: It looks like you're passing --spec a space-separated list of files:
exports['cli unknown option shows help for cache command - unknown option --foo 1'] = `
"a b c d e f g"
command: bin/cypress cache --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
This will work, but it's not recommended.
stdout:
-------
error: unknown option: --foo
The most common cause of this warning is using an unescaped glob pattern. If you are
trying to pass a glob pattern, escape it using quotes:
cypress run --spec "**/*.spec.js"
Usage: cypress cache [command]
If you are trying to pass multiple spec filenames, separate them by commas instead:
cypress run --spec spec1,spec2,spec3
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = `
command: bin/cypress cache foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli unknown option shows help for cache command - no sub-command 1'] = `
command: bin/cypress cache
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli help command shows help 1'] = `
@@ -45,41 +197,10 @@ exports['cli help command shows help 1'] = `
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
install [options] Installs the Cypress executable matching this package's version
verify [options] Verifies that Cypress is installed correctly and executable
cache [options] Manages the Cypress binary cache
-------
stderr:
-------
-------
`
exports['cli help command shows help for --help 1'] = `
command: bin/cypress --help
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help output usage information
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
install [options] Installs the Cypress executable matching this package's version
verify [options] Verifies that Cypress is installed correctly and executable
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
-------
stderr:
@@ -111,8 +232,45 @@ exports['cli help command shows help for -h 1'] = `
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
install [options] Installs the Cypress executable matching this package's version
verify [options] Verifies that Cypress is installed correctly and executable
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
-------
stderr:
-------
-------
`
exports['cli help command shows help for --help 1'] = `
command: bin/cypress --help
code: 0
failed: false
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cypress <command> [options]
Options:
-v, --version prints Cypress version
-h, --help output usage information
Commands:
help Shows CLI help and exits
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
-------
stderr:
@@ -145,8 +303,10 @@ exports['cli unknown command shows usage and exits 1'] = `
version prints Cypress version
run [options] Runs Cypress tests from the CLI without the GUI
open [options] Opens Cypress in the interactive GUI.
install [options] Installs the Cypress executable matching this package's version
verify [options] Verifies that Cypress is installed correctly and executable
install [options] Installs the Cypress executable matching this package's
version
verify [options] Verifies that Cypress is installed correctly and
executable
cache [options] Manages the Cypress binary cache
-------
stderr:
@@ -156,189 +316,6 @@ exports['cli unknown command shows usage and exits 1'] = `
`
exports['cli unknown option shows help for cache command - no sub-command 1'] = `
command: bin/cypress cache
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
Usage: cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli unknown option shows help for cache command - unknown option --foo 1'] = `
command: bin/cypress cache --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli unknown option shows help for cache command - unknown sub-command foo 1'] = `
command: bin/cypress cache foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown command: cache foo
Usage: cache [command]
Manages the Cypress binary cache
Options:
list list cached binary versions
path print the path to the binary cache
clear delete all cached binaries
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli version and binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
`
exports['cli version and binary version 2'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
`
exports['cli version no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
`
exports['shows help for open --foo 1'] = `
command: bin/cypress open --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: open [options]
Opens Cypress in the interactive GUI.
Options:
-p, --port <port> runs Cypress on a specific port. overrides any value in the configuration file.
-e, --env <env> sets environment variables. separate multiple values with a comma. overrides any value in the configuration file or cypress.env.json
-c, --config <config> sets configuration values. separate multiple values with a comma. overrides any value in the configuration file.
-C, --config-file <config-file> path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.
-d, --detached [bool] runs Cypress application in detached mode
-b, --browser <browser-path> path to a custom browser to be added to the list of available browsers in Cypress
-P, --project <project-path> path to the project
--global force Cypress into global mode as if its globally installed
--dev runs cypress in development and bypasses binary check
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['shows help for run --foo 1'] = `
command: bin/cypress run --foo
code: 1
failed: true
killed: false
signal: null
timedOut: false
stdout:
-------
error: unknown option: --foo
Usage: run [options]
Runs Cypress tests from the CLI without the GUI
Options:
--record [bool] records the run. sends test results, screenshots and videos to your Cypress Dashboard.
--headed displays the Electron browser instead of running headlessly
-k, --key <record-key> your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.
-s, --spec <spec> runs a specific spec file. defaults to "all"
-r, --reporter <reporter> runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"
-o, --reporter-options <reporter-options> options for the mocha reporter. defaults to "null"
-p, --port <port> runs Cypress on a specific port. overrides any value in the configuration file.
-e, --env <env> sets environment variables. separate multiple values with a comma. overrides any value in the configuration file or cypress.env.json
-c, --config <config> sets configuration values. separate multiple values with a comma. overrides any value in the configuration file.
-C, --config-file <config-file> path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.
-b, --browser <browser-name-or-path> runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.
-P, --project <project-path> path to the project
--parallel enables concurrent runs and automatic load balancing of specs across multiple machines or processes
--group <name> a named group for recorded runs in the Cypress dashboard
--ci-build-id <id> the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers
--no-exit keep the browser open after tests finish
--dev runs cypress in development and bypasses binary check
-h, --help output usage information
-------
stderr:
-------
-------
`
exports['cli CYPRESS_ENV allows staging environment 1'] = `
code: 0
stderr:
@@ -367,3 +344,54 @@ exports['cli CYPRESS_ENV catches environment "foo" 1'] = `
-------
`
exports['cli version and binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
`
exports['cli version and binary version 2'] = `
Cypress package version: 1.2.3
Cypress binary version: X.Y.Z
`
exports['cli version no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
`
exports['cli --version no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
`
exports['cli -v no binary version 1'] = `
Cypress package version: 1.2.3
Cypress binary version: not installed
`
exports['cli cypress run warns with space-separated --spec 1'] = `
⚠ Warning: It looks like you're passing --spec a space-separated list of arguments:
"a b c d e f g"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --spec arg1,arg2,arg3
The most common cause of this warning is using an unescaped glob pattern. If you are
trying to pass a glob pattern, escape it using quotes:
cypress run --spec "**/*.spec.js"
`
exports['cli cypress run warns with space-separated --tag 1'] = `
⚠ Warning: It looks like you're passing --tag a space-separated list of arguments:
"a b c d e f g"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --tag arg1,arg2,arg3
`
+25
View File
@@ -29,6 +29,7 @@ Cypress Version: 1.2.3
exports['errors individual has the following errors 1'] = [
"CYPRESS_RUN_BINARY",
"binaryNotExecutable",
"childProcessKilled",
"failedDownload",
"failedUnzip",
"invalidCacheDirectory",
@@ -71,3 +72,27 @@ If you are using Docker, we provide containers with all required dependencies in
Platform: test platform (Foo-OsVersion)
Cypress Version: 1.2.3
`
exports['child kill error object'] = {
"description": "The Test Runner unexpectedly exited via a exit event with signal SIGKILL",
"solution": "Please search Cypress documentation for possible solutions:\n\n https://on.cypress.io\n\nCheck if there is a GitHub issue describing this crash:\n\n https://github.com/cypress-io/cypress/issues\n\nConsider opening a new issue."
}
exports['Error message'] = `
The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: test platform (Foo-OsVersion)
Cypress Version: 1.2.3
`
+3 -3
View File
@@ -1,10 +1,10 @@
exports['exec run .processRunOptions does not remove --record option when using --browser 1'] = [
"--run-project",
null,
"--record",
"foo",
"--browser",
"test browser"
"test browser",
"--record",
"foo"
]
exports['exec run .processRunOptions passes --browser option 1'] = [
+18
View File
@@ -0,0 +1,18 @@
exports['lib/exec/spawn .start detects kill signal exits with error on SIGKILL 1'] = `
The Test Runner unexpectedly exited via a exit event with signal SIGKILL
Please search Cypress documentation for possible solutions:
https://on.cypress.io
Check if there is a GitHub issue describing this crash:
https://github.com/cypress-io/cypress/issues
Consider opening a new issue.
----------
Platform: darwin (Foo-OsVersion)
Cypress Version: 0.0.0
`
+93 -127
View File
@@ -25,139 +25,107 @@ const coerceFalse = (arg) => {
return arg !== 'false'
}
const spaceDelimitedSpecsMsg = (files) => {
logger.log()
logger.warn(stripIndent`
${
logSymbols.warning
} Warning: It looks like you're passing --spec a space-separated list of files:
const spaceDelimitedArgsMsg = (flag, args) => {
let msg = `
${logSymbols.warning} Warning: It looks like you're passing --${flag} a space-separated list of arguments:
"${files.join(' ')}"
"${args.join(' ')}"
This will work, but it's not recommended.
If you are trying to pass multiple arguments, separate them with commas instead:
cypress run --${flag} arg1,arg2,arg3
`
if (flag === 'spec') {
msg += `
The most common cause of this warning is using an unescaped glob pattern. If you are
trying to pass a glob pattern, escape it using quotes:
cypress run --spec "**/*.spec.js"
`
}
If you are trying to pass multiple spec filenames, separate them by commas instead:
cypress run --spec spec1,spec2,spec3
`)
logger.log()
logger.warn(stripIndent(msg))
logger.log()
}
const parseVariableOpts = (fnArgs, args) => {
const opts = fnArgs.pop()
if (fnArgs.length && opts.spec) {
// this will capture space-delimited specs after --spec spec1 but before the next option
if (fnArgs.length && (opts.spec || opts.tag)) {
// this will capture space-delimited args after
// flags that could have possible multiple args
// but before the next option
// --spec spec1 spec2 or --tag foo bar
const argIndex = _.indexOf(args, '--spec') + 2
const nextOptOffset = _.findIndex(_.slice(args, argIndex), (arg) => {
return _.startsWith(arg, '--')
const multiArgFlags = _.compact([
opts.spec ? 'spec' : opts.spec,
opts.tag ? 'tag' : opts.tag,
])
_.forEach(multiArgFlags, (flag) => {
const argIndex = _.indexOf(args, `--${flag}`) + 2
const nextOptOffset = _.findIndex(_.slice(args, argIndex), (arg) => {
return _.startsWith(arg, '--')
})
const endIndex = nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length
const maybeArgs = _.slice(args, argIndex, endIndex)
const extraArgs = _.intersection(maybeArgs, fnArgs)
if (extraArgs.length) {
opts[flag] = [opts[flag]].concat(extraArgs)
spaceDelimitedArgsMsg(flag, opts[flag])
opts[flag] = opts[flag].join(',')
}
})
const endIndex =
nextOptOffset !== -1 ? argIndex + nextOptOffset : args.length
const maybeSpecs = _.slice(args, argIndex, endIndex)
const extraSpecs = _.intersection(maybeSpecs, fnArgs)
if (extraSpecs.length) {
opts.spec = [opts.spec].concat(extraSpecs)
spaceDelimitedSpecsMsg(opts.spec)
opts.spec = opts.spec.join(',')
}
}
return parseOpts(opts)
}
const parseOpts = (opts) => {
opts = _.pick(
opts,
'project',
'spec',
'reporter',
'reporterOptions',
'path',
'destination',
'port',
'env',
'cypressVersion',
'config',
'record',
'key',
'configFile',
'browser',
'detached',
'headed',
'global',
'dev',
'force',
'exit',
'cachePath',
'cacheList',
'cacheClear',
'parallel',
'group',
'ciBuildId'
)
if (opts.exit) {
opts = _.omit(opts, 'exit')
}
debug('parsed cli options', opts)
return opts
return util.parseOpts(opts)
}
const descriptions = {
record:
'records the run. sends test results, screenshots and videos to your Cypress Dashboard.',
key:
'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
spec: 'runs a specific spec file. defaults to "all"',
reporter:
'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"',
reporterOptions: 'options for the mocha reporter. defaults to "null"',
port: 'runs Cypress on a specific port. overrides any value in the configuration file.',
env: 'sets environment variables. separate multiple values with a comma. overrides any value in the configuration file or cypress.env.json',
config: 'sets configuration values. separate multiple values with a comma. overrides any value in the configuration file.',
browserRunMode: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.',
browserOpenMode: 'path to a custom browser to be added to the list of available browsers in Cypress',
detached: 'runs Cypress application in detached mode',
project: 'path to the project',
global: 'force Cypress into global mode as if its globally installed',
configFile: 'path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.',
version: 'prints Cypress version',
headed: 'displays the Electron browser instead of running headlessly',
dev: 'runs cypress in development and bypasses binary check',
forceInstall: 'force install the Cypress binary',
exit: 'keep the browser open after tests finish',
cachePath: 'print the path to the binary cache',
cacheList: 'list cached binary versions',
browserRunMode: 'runs Cypress in the browser with the given name. if a filesystem path is supplied, Cypress will attempt to use the browser at that path.',
cacheClear: 'delete all cached binaries',
group: 'a named group for recorded runs in the Cypress dashboard',
parallel:
'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
ciBuildId:
'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers',
cacheList: 'list cached binary versions',
cachePath: 'print the path to the binary cache',
ciBuildId: 'the unique identifier for a run on your CI provider. typically a "BUILD_ID" env var. this value is automatically detected for most CI providers',
config: 'sets configuration values. separate multiple values with a comma. overrides any value in cypress.json.',
configFile: 'path to JSON file where configuration values are set. defaults to "cypress.json". pass "false" to disable.',
detached: 'runs Cypress application in detached mode',
dev: 'runs cypress in development and bypasses binary check',
env: 'sets environment variables. separate multiple values with a comma. overrides any value in cypress.json or cypress.env.json',
exit: 'keep the browser open after tests finish',
forceInstall: 'force install the Cypress binary',
global: 'force Cypress into global mode as if its globally installed',
group: 'a named group for recorded runs in the Cypress Dashboard',
headed: 'displays the Electron browser instead of running headlessly',
key: 'your secret Record Key. you can omit this if you set a CYPRESS_RECORD_KEY environment variable.',
parallel: 'enables concurrent runs and automatic load balancing of specs across multiple machines or processes',
port: 'runs Cypress on a specific port. overrides any value in cypress.json.',
project: 'path to the project',
record: 'records the run. sends test results, screenshots and videos to your Cypress Dashboard.',
reporter: 'runs a specific mocha reporter. pass a path to use a custom reporter. defaults to "spec"',
reporterOptions: 'options for the mocha reporter. defaults to "null"',
spec: 'runs specific spec file(s). defaults to "all"',
tag: 'named tag(s) for recorded runs in the Cypress Dashboard',
version: 'prints Cypress version',
}
const knownCommands = [
'version',
'run',
'open',
'install',
'verify',
'-v',
'--version',
'cache',
'help',
'-h',
'--help',
'cache',
'install',
'open',
'run',
'verify',
'-v',
'--version',
'version',
]
const text = (description) => {
@@ -205,7 +173,7 @@ module.exports = {
const program = new commander.Command()
// bug in commaner not printing name
// bug in commander not printing name
// in usage help docs
program._name = 'cypress'
@@ -228,25 +196,23 @@ module.exports = {
.command('run')
.usage('[options]')
.description('Runs Cypress tests from the CLI without the GUI')
.option('--record [bool]', text('record'), coerceFalse)
.option('--headed', text('headed'))
.option('-k, --key <record-key>', text('key'))
.option('-s, --spec <spec>', text('spec'))
.option('-r, --reporter <reporter>', text('reporter'))
.option(
'-o, --reporter-options <reporter-options>',
text('reporterOptions')
)
.option('-p, --port <port>', text('port'))
.option('-e, --env <env>', text('env'))
.option('-b, --browser <browser-name-or-path>', text('browserRunMode'))
.option('--ci-build-id <id>', text('ciBuildId'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('-b, --browser <browser-name-or-path>', text('browserRunMode'))
.option('-P, --project <project-path>', text('project'))
.option('--parallel', text('parallel'))
.option('-e, --env <env>', text('env'))
.option('--group <name>', text('group'))
.option('--ci-build-id <id>', text('ciBuildId'))
.option('-k, --key <record-key>', text('key'))
.option('--headed', text('headed'))
.option('--no-exit', text('exit'))
.option('--parallel', text('parallel'))
.option('-p, --port <port>', text('port'))
.option('-P, --project <project-path>', text('project'))
.option('--record [bool]', text('record'), coerceFalse)
.option('-r, --reporter <reporter>', text('reporter'))
.option('-o, --reporter-options <reporter-options>', text('reporterOptions'))
.option('-s, --spec <spec>', text('spec'))
.option('-t, --tag <tag>', text('tag'))
.option('--dev', text('dev'), coerceFalse)
.action((...fnArgs) => {
debug('running Cypress')
@@ -260,19 +226,19 @@ module.exports = {
.command('open')
.usage('[options]')
.description('Opens Cypress in the interactive GUI.')
.option('-p, --port <port>', text('port'))
.option('-e, --env <env>', text('env'))
.option('-b, --browser <browser-path>', text('browserOpenMode'))
.option('-c, --config <config>', text('config'))
.option('-C, --config-file <config-file>', text('configFile'))
.option('-d, --detached [bool]', text('detached'), coerceFalse)
.option('-b, --browser <browser-path>', text('browserOpenMode'))
.option('-P, --project <project-path>', text('project'))
.option('-e, --env <env>', text('env'))
.option('--global', text('global'))
.option('-p, --port <port>', text('port'))
.option('-P, --project <project-path>', text('project'))
.option('--dev', text('dev'), coerceFalse)
.action((opts) => {
debug('opening Cypress')
require('./exec/open')
.start(parseOpts(opts))
.start(util.parseOpts(opts))
.catch(util.logErrorExit1)
})
@@ -285,7 +251,7 @@ module.exports = {
.option('-f, --force', text('forceInstall'))
.action((opts) => {
require('./tasks/install')
.start(parseOpts(opts))
.start(util.parseOpts(opts))
.catch(util.logErrorExit1)
})
@@ -298,7 +264,7 @@ module.exports = {
.option('--dev', text('dev'), coerceFalse)
.action((opts) => {
const defaultOpts = { force: true, welcomeMessage: false }
const parsedOpts = parseOpts(opts)
const parsedOpts = util.parseOpts(opts)
const options = _.extend(parsedOpts, defaultOpts)
require('./tasks/verify')
+50 -11
View File
@@ -168,20 +168,21 @@ const versionMismatch = {
solution: 'Install Cypress and verify app again',
}
const solutionUnknown = stripIndent`
Please search Cypress documentation for possible solutions:
${chalk.blue(docsUrl)}
Check if there is a GitHub issue describing this crash:
${chalk.blue(util.issuesUrl)}
Consider opening a new issue.
`
const unexpected = {
description:
'An unexpected error occurred while verifying the Cypress executable.',
solution: stripIndent`
Please search Cypress documentation for possible solutions:
${chalk.blue(docsUrl)}
Check if there is a GitHub issue describing this crash:
${chalk.blue(util.issuesUrl)}
Consider opening a new issue.
`,
solution: solutionUnknown,
}
const invalidCypressEnv = {
@@ -191,6 +192,20 @@ const invalidCypressEnv = {
exitCode: 11,
}
/**
* This error happens when CLI detects that the child Test Runner process
* was killed with a signal, like SIGBUS
* @see https://github.com/cypress-io/cypress/issues/5808
* @param {'close'|'event'} eventName Child close event name
* @param {string} signal Signal that closed the child process, like "SIGBUS"
*/
const childProcessKilled = (eventName, signal) => {
return {
description: `The Test Runner unexpectedly exited via a ${chalk.cyan(eventName)} event with signal ${chalk.cyan(signal)}`,
solution: solutionUnknown,
}
}
const removed = {
CYPRESS_BINARY_VERSION: {
description: stripIndent`
@@ -240,6 +255,28 @@ function addPlatformInformation (info) {
})
}
/**
* Given an error object (see the errors above), forms error message text with details,
* then resolves with Error instance you can throw or reject with.
* @param {object} errorObject
* @returns {Promise<Error>} resolves with an Error
* @example
```js
// inside a Promise with "resolve" and "reject"
const errorObject = childProcessKilled('exit', 'SIGKILL')
return getError(errorObject).then(reject)
```
*/
function getError (errorObject) {
return formErrorText(errorObject).then((errorMessage) => {
const err = new Error(errorMessage)
err.known = true
return err
})
}
/**
* Forms nice error message with error and platform information,
* and if possible a way to solve it. Resolves with a string.
@@ -355,6 +392,7 @@ module.exports = {
// formError,
formErrorText,
throwFormErrorText,
getError,
hr,
errors: {
nonZeroExitCodeXvfb,
@@ -373,5 +411,6 @@ module.exports = {
removed,
CYPRESS_RUN_BINARY,
smokeTestFailure,
childProcessKilled,
},
}
+4 -4
View File
@@ -11,10 +11,6 @@ module.exports = {
const args = []
if (options.env) {
args.push('--env', options.env)
}
if (options.config) {
args.push('--config', options.config)
}
@@ -27,6 +23,10 @@ module.exports = {
args.push('--browser', options.browser)
}
if (options.env) {
args.push('--env', options.env)
}
if (options.port) {
args.push('--port', options.port)
}
+65 -61
View File
@@ -12,14 +12,20 @@ const processRunOptions = (options = {}) => {
const args = ['--run-project', options.project]
//// if key is set use that - else attempt to find it by environment variable
if (options.key == null) {
debug('--key is not set, looking up environment variable CYPRESS_RECORD_KEY')
options.key = util.getEnv('CYPRESS_RECORD_KEY') || util.getEnv('CYPRESS_CI_KEY')
if (options.browser) {
args.push('--browser', options.browser)
}
if (options.env) {
args.push('--env', options.env)
if (options.ci) {
// push to display the deprecation message
args.push('--ci')
// also automatically record
args.push('--record', true)
}
if (options.ciBuildId) {
args.push('--ci-build-id', options.ciBuildId)
}
if (options.config) {
@@ -30,70 +36,68 @@ const processRunOptions = (options = {}) => {
args.push('--config-file', options.configFile)
}
if (options.env) {
args.push('--env', options.env)
}
if (options.exit === false) {
args.push('--no-exit')
}
if (options.group) {
args.push('--group', options.group)
}
if (options.headed) {
args.push('--headed', options.headed)
}
// if key is set use that - else attempt to find it by environment variable
if (options.key == null) {
debug('--key is not set, looking up environment variable CYPRESS_RECORD_KEY')
options.key = util.getEnv('CYPRESS_RECORD_KEY') || util.getEnv('CYPRESS_CI_KEY')
}
// if we have a key assume we're in record mode
if (options.key) {
args.push('--key', options.key)
}
if (options.outputPath) {
args.push('--output-path', options.outputPath)
}
if (options.parallel) {
args.push('--parallel')
}
if (options.port) {
args.push('--port', options.port)
}
// if record is defined and we're not
// already in ci mode, then send it up
if (options.record != null && !options.ci) {
args.push('--record', options.record)
}
// if we have a specific reporter push that into the args
if (options.reporter) {
args.push('--reporter', options.reporter)
}
// if we have a specific reporter push that into the args
if (options.reporterOptions) {
args.push('--reporter-options', options.reporterOptions)
}
// if we have specific spec(s) push that into the args
if (options.spec) {
args.push('--spec', options.spec)
}
//// if we have a specific reporter push that into the args
if (options.reporter) {
args.push('--reporter', options.reporter)
}
//// if we have a specific reporter push that into the args
if (options.reporterOptions) {
args.push('--reporter-options', options.reporterOptions)
}
if (options.ci) {
//// push to display the deprecation message
args.push('--ci')
//// also automatically record
args.push('--record', true)
}
//// if we have a key assume we're in record mode
if (options.key) {
args.push('--key', options.key)
}
//// if record is defined and we're not
//// already in ci mode, then send it up
if (options.record != null && !options.ci) {
args.push('--record', options.record)
}
if (options.parallel) {
args.push('--parallel')
}
if (options.group) {
args.push('--group', options.group)
}
if (options.ciBuildId) {
args.push('--ci-build-id', options.ciBuildId)
}
if (options.outputPath) {
args.push('--output-path', options.outputPath)
}
if (options.browser) {
args.push('--browser', options.browser)
}
if (options.headed) {
args.push('--headed', options.headed)
}
if (options.exit === false) {
args.push('--no-exit')
if (options.tag) {
args.push('--tag', options.tag)
}
return args
+24 -6
View File
@@ -10,7 +10,7 @@ const util = require('../util')
const state = require('../tasks/state')
const xvfb = require('./xvfb')
const verify = require('../tasks/verify')
const { throwFormErrorText, errors } = require('../errors')
const errors = require('../errors')
const isXlibOrLibudevRe = /^(?:Xlib|libudev)/
const isHighSierraWarningRe = /\*\*\* WARNING/
@@ -73,10 +73,15 @@ module.exports = {
debug('needs to start own Xvfb?', needsXvfb)
// always push cwd into the args
// 1. Start arguments with "--" so Electron knows these are OUR
// arguments and does not try to sanitize them. Otherwise on Windows
// an url in one of the arguments crashes it :(
// https://github.com/cypress-io/cypress/issues/5466
// 2. Always push cwd into the args
// which additionally acts as a signal to the
// binary that it was invoked through the NPM module
args = [].concat(args, '--cwd', process.cwd())
args = ['--'].concat(args, '--cwd', process.cwd())
_.defaults(options, {
dev: false,
@@ -99,6 +104,8 @@ module.exports = {
args.unshift(
path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js')
)
debug('in dev mode the args became %o', args)
}
const { onStderrData, electronLogging } = overrides
@@ -106,8 +113,10 @@ module.exports = {
const electronArgs = _.clone(args)
const node11WindowsFix = isPlatform('win32')
if (verify.needsSandbox()) {
electronArgs.push('--no-sandbox')
if (!options.dev && verify.needsSandbox()) {
// this is one of the Electron's command line switches
// thus it needs to be before "--" separator
electronArgs.unshift('--no-sandbox')
}
// strip dev out of child process options
@@ -140,6 +149,13 @@ module.exports = {
function resolveOn (event) {
return function (code, signal) {
debug('child event fired %o', { event, code, signal })
if (code === null) {
const errorObject = errors.errors.childProcessKilled(event, signal)
return errors.getError(errorObject).then(reject)
}
resolve(code)
}
}
@@ -251,7 +267,9 @@ module.exports = {
return code
})
.catch(throwFormErrorText(errors.unexpected))
// we can format and handle an error message from the code above
// prevent wrapping error again by using "known: undefined" filter
.catch({ known: undefined }, errors.throwFormErrorText(errors.errors.unexpected))
}
if (needsXvfb) {
+66 -10
View File
@@ -3,7 +3,7 @@ const is = require('check-more-types')
const cp = require('child_process')
const os = require('os')
const yauzl = require('yauzl')
const debug = require('debug')('cypress:cli')
const debug = require('debug')('cypress:cli:unzip')
const extract = require('extract-zip')
const Promise = require('bluebird')
const readline = require('readline')
@@ -12,6 +12,10 @@ const { throwFormErrorText, errors } = require('../errors')
const fs = require('../fs')
const util = require('../util')
const unzipTools = {
extract,
}
// expose this function for simple testing
const unzip = ({ zipFilePath, installDir, progress }) => {
debug('unzipping from %s', zipFilePath)
@@ -21,15 +25,17 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
throw new Error('Missing zip filename')
}
const startTime = Date.now()
let yauzlDoneTime = 0
return fs.ensureDirAsync(installDir)
.then(() => {
return new Promise((resolve, reject) => {
return yauzl.open(zipFilePath, (err, zipFile) => {
yauzlDoneTime = Date.now()
if (err) return reject(err)
// debug('zipfile.paths:', zipFile)
// zipFile.on('entry', debug)
// debug(zipFile.readEntry())
const total = zipFile.entryCount
debug('zipFile entries count', total)
@@ -57,6 +63,8 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
}
const unzipWithNode = () => {
debug('unzipping with node.js (slow)')
const endFn = (err) => {
if (err) {
return reject(err)
@@ -70,15 +78,50 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
onEntry: tick,
}
return extract(zipFilePath, opts, endFn)
return unzipTools.extract(zipFilePath, opts, endFn)
}
//# we attempt to first unzip with the native osx
//# ditto because its less likely to have problems
//# with corruption, symlinks, or icons causing failures
//# and can handle resource forks
//# http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
const unzipWithUnzipTool = () => {
debug('unzipping via `unzip`')
const inflatingRe = /inflating:/
const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir])
sp.on('error', unzipWithNode)
sp.on('close', (code) => {
if (code === 0) {
percent = 100
notify(percent)
return resolve()
}
debug('`unzip` failed %o', { code })
return unzipWithNode()
})
sp.stdout.on('data', (data) => {
if (inflatingRe.test(data)) {
return tick()
}
})
sp.stderr.on('data', (data) => {
debug('`unzip` stderr %s', data)
})
}
// we attempt to first unzip with the native osx
// ditto because its less likely to have problems
// with corruption, symlinks, or icons causing failures
// and can handle resource forks
// http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
const unzipWithOsx = () => {
debug('unzipping via `ditto`')
const copyingFileRe = /^copying file/
const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir])
@@ -96,6 +139,8 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
return resolve()
}
debug('`ditto` failed %o', { code })
return unzipWithNode()
})
@@ -113,6 +158,7 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
case 'darwin':
return unzipWithOsx()
case 'linux':
return unzipWithUnzipTool()
case 'win32':
return unzipWithNode()
default:
@@ -120,6 +166,12 @@ const unzip = ({ zipFilePath, installDir, progress }) => {
}
})
})
.tap(() => {
debug('unzip completed %o', {
yauzlMs: yauzlDoneTime - startTime,
unzipMs: Date.now() - yauzlDoneTime,
})
})
})
}
@@ -147,4 +199,8 @@ const start = ({ zipFilePath, installDir, progress }) => {
module.exports = {
start,
utils: {
unzip,
unzipTools,
},
}
+50
View File
@@ -158,6 +158,8 @@ function printNodeOptions (log = debug) {
* Removes double quote characters
* from the start and end of the given string IF they are both present
*
* @param {string} str Input string
* @returns {string} Trimmed string or the original string if there are no double quotes around it.
* @example
```
dequote('"foo"')
@@ -175,8 +177,56 @@ const dequote = (str) => {
return str
}
const parseOpts = (opts) => {
opts = _.pick(opts,
'browser',
'cachePath',
'cacheList',
'cacheClear',
'ciBuildId',
'config',
'configFile',
'cypressVersion',
'destination',
'detached',
'dev',
'exit',
'env',
'force',
'global',
'group',
'headed',
'key',
'path',
'parallel',
'port',
'project',
'reporter',
'reporterOptions',
'record',
'spec',
'tag')
if (opts.exit) {
opts = _.omit(opts, 'exit')
}
// some options might be quoted - which leads to unexpected results
// remove double quotes from certain options
const removeQuotes = {
group: dequote,
ciBuildId: dequote,
}
const cleanOpts = R.evolve(removeQuotes, opts)
debug('parsed cli options %o', cleanOpts)
return cleanOpts
}
const util = {
normalizeModuleOptions,
parseOpts,
isValidCypressEnvValue,
printNodeOptions,
+3 -2
View File
@@ -13,7 +13,7 @@
"lint": "bin-up eslint --fix *.js scripts/*.js bin/* lib/*.js lib/**/*.js test/*.js test/**/*.js",
"prerelease": "npm run build",
"release": "cd build && releaser --no-node --no-changelog",
"size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";",
"size": "npm pack --dry",
"pretest": "npm run check-deps-pre",
"test": "npm run test-unit",
"test-debug": "node --inspect --debug-brk $(bin-up _mocha)",
@@ -23,7 +23,7 @@
"pretest-watch": "npm run check-deps-pre",
"test-watch": "npm run unit -- --watch",
"types": "npm run dtslint",
"unit": "BLUEBIRD_DEBUG=1 NODE_ENV=test bin-up mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json"
"unit": "cross-env BLUEBIRD_DEBUG=1 NODE_ENV=test ../node_modules/.bin/mocha --reporter mocha-multi-reporters --reporter-options configFile=../mocha-reporter-config.json"
},
"dependencies": {
"@cypress/listr-verbose-renderer": "0.4.1",
@@ -77,6 +77,7 @@
"chai": "3.5.0",
"chai-as-promised": "7.1.1",
"chai-string": "1.4.0",
"cross-env": "6.0.3",
"dependency-check": "3.4.1",
"dtslint": "0.9.0",
"execa-wrap": "1.4.0",
+5 -2
View File
@@ -43,9 +43,12 @@
"description": "The reporter options used. Supported options depend on the reporter. See https://on.cypress.io/reporters#Reporter-Options"
},
"testFiles": {
"type": "string",
"type": [
"string",
"array"
],
"default": "**/*.*",
"description": "A String glob pattern of the test files to load"
"description": "A String or Array of string glob patterns of the test files to load. See https://on.cypress.io/configuration#Global"
},
"watchForFileChanges": {
"type": "boolean",
+57 -9
View File
@@ -224,13 +224,6 @@ describe('cli', () => {
expect(run.start).to.be.calledWith({ port: '7878' })
})
it('calls run with spec', () => {
this.exec('run --spec cypress/integration/foo_spec.js')
expect(run.start).to.be.calledWith({
spec: 'cypress/integration/foo_spec.js',
})
})
it('calls run with port with -p arg', () => {
this.exec('run -p 8989')
expect(run.start).to.be.calledWith({ port: '8989' })
@@ -300,19 +293,74 @@ describe('cli', () => {
expect(run.start).to.be.calledWith({ group: 'staging' })
})
it('calls run with space-separated --specs', () => {
it('calls run with spec', () => {
this.exec('run --spec cypress/integration/foo_spec.js')
expect(run.start).to.be.calledWith({
spec: 'cypress/integration/foo_spec.js',
})
})
it('calls run with space-separated --spec', () => {
this.exec('run --spec a b c d e f g')
expect(run.start).to.be.calledWith({ spec: 'a,b,c,d,e,f,g' })
this.exec('run --dev bang --spec foo bar baz -P ./')
expect(run.start).to.be.calledWithMatch({ spec: 'foo,bar,baz' })
})
it('warns with space-separated --specs', (done) => {
it('warns with space-separated --spec', (done) => {
sinon.spy(logger, 'warn')
this.exec('run --spec a b c d e f g --dev')
snapshot(logger.warn.getCall(0).args[0])
done()
})
it('calls run with --tag', () => {
this.exec('run --tag nightly')
expect(run.start).to.be.calledWith({ tag: 'nightly' })
})
it('calls run comma-separated --tag', () => {
this.exec('run --tag nightly,staging')
expect(run.start).to.be.calledWith({ tag: 'nightly,staging' })
})
it('does not remove double quotes from --tag', () => {
// I think it is a good idea to lock down this behavior
// to make sure we either preserve it or change it in the future
this.exec('run --tag "nightly"')
expect(run.start).to.be.calledWith({ tag: '"nightly"' })
})
it('calls run comma-separated --spec', () => {
this.exec('run --spec main_spec.js,view_spec.js')
expect(run.start).to.be.calledWith({ spec: 'main_spec.js,view_spec.js' })
})
it('calls run with space-separated --tag', () => {
this.exec('run --tag a b c d e f g')
expect(run.start).to.be.calledWith({ tag: 'a,b,c,d,e,f,g' })
this.exec('run --dev bang --tag foo bar baz -P ./')
expect(run.start).to.be.calledWithMatch({ tag: 'foo,bar,baz' })
})
it('warns with space-separated --tag', (done) => {
sinon.spy(logger, 'warn')
this.exec('run --tag a b c d e f g --dev')
snapshot(logger.warn.getCall(0).args[0])
done()
})
it('calls run with space-separated --tag and --spec', () => {
this.exec('run --tag a b c d e f g --spec h i j k l')
expect(run.start).to.be.calledWith({ tag: 'a,b,c,d,e,f,g', spec: 'h,i,j,k,l' })
this.exec('run --dev bang --tag foo bar baz -P ./ --spec fizz buzz --headed false')
expect(run.start).to.be.calledWithMatch({ tag: 'foo,bar,baz', spec: 'fizz,buzz' })
})
it('removes stray double quotes from --ci-build-id and --group', () => {
this.exec('run --ci-build-id "123" --group "staging"')
expect(run.start).to.be.calledWith({ ciBuildId: '123', group: 'staging' })
})
})
context('cypress open', () => {
+15 -1
View File
@@ -2,7 +2,7 @@ require('../spec_helper')
const os = require('os')
const snapshot = require('../support/snapshot')
const { errors, formErrorText } = require(`${lib}/errors`)
const { errors, getError, formErrorText } = require(`${lib}/errors`)
const util = require(`${lib}/util`)
describe('errors', function () {
@@ -19,6 +19,20 @@ describe('errors', function () {
})
})
context('getError', () => {
it('forms full message and creates Error object', () => {
const errObject = errors.childProcessKilled('exit', 'SIGKILL')
snapshot('child kill error object', errObject)
return getError(errObject).then((e) => {
expect(e).to.be.an('Error')
expect(e).to.have.property('known', true)
snapshot('Error message', e.message)
})
})
})
context('.errors.formErrorText', function () {
it('returns fully formed text message', () => {
expect(missingXvfb).to.be.an('object')
+18
View File
@@ -131,5 +131,23 @@ describe('exec run', function () {
expect(spawn.start).to.be.calledWith(['--run-project', process.cwd(), '--output-path', '/path/to/output'])
})
})
it('spawns with --tag value', function () {
return run.start({ tag: 'nightly' })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--tag', 'nightly',
])
})
})
it('spawns with several --tag words unchanged', function () {
return run.start({ tag: 'nightly, sanity' })
.then(() => {
expect(spawn.start).to.be.calledWith([
'--run-project', process.cwd(), '--tag', 'nightly, sanity',
])
})
})
})
})
+43 -1
View File
@@ -7,6 +7,7 @@ const tty = require('tty')
const path = require('path')
const EE = require('events')
const mockedEnv = require('mocked-env')
const debug = require('debug')('test')
const state = require(`${lib}/tasks/state`)
const xvfb = require(`${lib}/exec/xvfb`)
@@ -14,6 +15,7 @@ const spawn = require(`${lib}/exec/spawn`)
const verify = require(`${lib}/tasks/verify`)
const util = require(`${lib}/util.js`)
const expect = require('chai').expect
const snapshot = require('../../support/snapshot')
const cwd = process.cwd()
@@ -92,6 +94,7 @@ describe('lib/exec/spawn', function () {
return spawn.start('--foo', { foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('/path/to/cypress', [
'--',
'--foo',
'--cwd',
cwd,
@@ -112,11 +115,13 @@ describe('lib/exec/spawn', function () {
// and also less risk that a failed assertion would dump the
// entire ENV object with possible sensitive variables
const args = cp.spawn.firstCall.args.slice(0, 2)
// it is important for "--no-sandbox" to appear before "--" separator
const expectedCliArgs = [
'--no-sandbox',
'--',
'--foo',
'--cwd',
cwd,
'--no-sandbox',
]
expect(args).to.deep.equal(['/path/to/cypress', expectedCliArgs])
@@ -133,6 +138,28 @@ describe('lib/exec/spawn', function () {
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
], {
detached: false,
stdio: ['inherit', 'inherit', 'pipe'],
})
})
})
it('does not pass --no-sandbox when running in dev mode', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(verify, 'needsSandbox').returns(true)
const p = path.resolve('..', 'scripts', 'start.js')
return spawn.start('--foo', { dev: true, foo: 'bar' })
.then(() => {
expect(cp.spawn).to.be.calledWithMatch('node', [
p,
'--',
'--foo',
'--cwd',
cwd,
@@ -171,6 +198,20 @@ describe('lib/exec/spawn', function () {
})
})
context('detects kill signal', function () {
it('exits with error on SIGKILL', function () {
this.spawnedProcess.on.withArgs('exit').yieldsAsync(null, 'SIGKILL')
return spawn.start('--foo')
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e) => {
debug('error message', e.message)
snapshot(e.message)
})
})
})
it('does not start xvfb when its not needed', function () {
this.spawnedProcess.on.withArgs('close').yieldsAsync(0)
@@ -246,6 +287,7 @@ describe('lib/exec/spawn', function () {
.then(() => {
throw new Error('should have hit error handler but did not')
}, (e) => {
debug('error message', e.message)
expect(e.message).to.include(msg)
})
})
+38
View File
@@ -3,6 +3,7 @@ require('../../spec_helper')
const os = require('os')
const path = require('path')
const snapshot = require('../../support/snapshot')
const cp = require('child_process')
const fs = require(`${lib}/fs`)
const util = require(`${lib}/util`)
@@ -63,4 +64,41 @@ describe('lib/tasks/unzip', function () {
return fs.statAsync(installDir)
})
})
// NOTE: hmm, running this test for some reason breaks 4 tests in verify_spec.js with very weird errors
context.skip('on linux', () => {
beforeEach(() => {
os.platform.returns('linux')
})
it('can try unzip first then fall back to node unzip', function () {
sinon.stub(unzip.utils.unzipTools, 'extract').resolves()
const unzipChildProcess = {
on: sinon.stub(),
stdout: {
on () {},
},
stderr: {
on () {},
},
}
unzipChildProcess.on.withArgs('error').yieldsAsync(0)
unzipChildProcess.on.withArgs('close').yieldsAsync(0)
sinon.stub(cp, 'spawn').withArgs('unzip').returns(unzipChildProcess)
const zipFilePath = path.join('test', 'fixture', 'example.zip')
return unzip
.start({
zipFilePath,
installDir,
})
.then(() => {
expect(cp.spawn).to.have.been.calledWith('unzip')
expect(unzip.utils.unzipTools.extract).to.be.calledWith(zipFilePath)
})
})
})
})
+50
View File
@@ -497,4 +497,54 @@ describe('util', () => {
})
})
})
context('parseOpts', () => {
it('passes normal options and strips unknown ones', () => {
const result = util.parseOpts({
unknownOptions: true,
group: 'my group name',
ciBuildId: 'my ci build id',
})
expect(result).to.deep.equal({
group: 'my group name',
ciBuildId: 'my ci build id',
})
})
it('removes leftover double quotes', () => {
const result = util.parseOpts({
group: '"my group name"',
ciBuildId: '"my ci build id"',
})
expect(result).to.deep.equal({
group: 'my group name',
ciBuildId: 'my ci build id',
})
})
it('leaves unbalanced double quotes', () => {
const result = util.parseOpts({
group: 'my group name"',
ciBuildId: '"my ci build id',
})
expect(result).to.deep.equal({
group: 'my group name"',
ciBuildId: '"my ci build id',
})
})
it('works with unspecified options', () => {
const result = util.parseOpts({
// notice that "group" option is missing
ciBuildId: '"my ci build id"',
})
expect(result).to.deep.equal({
ciBuildId: 'my ci build id',
})
})
})
})
+3 -3
View File
@@ -43,9 +43,9 @@ declare module 'cypress' {
*/
reporter: string,
/**
* A String glob pattern of the test files to load.
* A String or Array of string glob pattern of the test files to load.
*/
testFiles: string
testFiles: string | string[]
//
// timeouts
@@ -290,7 +290,7 @@ declare module 'cypress' {
})
```
*/
interface CypressOpenOptions extends CypressCommonOptions {
interface CypressOpenOptions extends CypressCommonOptions {
/**
* Specify a filesystem path to a custom browser
*/
+67 -4
View File
@@ -4,7 +4,7 @@
// Mike Woudenberg <https://github.com/mikewoudenberg>
// Robbert van Markus <https://github.com/rvanmarkus>
// Nicholas Boll <https://github.com/nicholasboll>
// TypeScript Version: 2.8
// TypeScript Version: 2.9
// Updated by the Cypress team: https://www.cypress.io/about/
/// <reference path="./cy-blob-util.d.ts" />
@@ -60,7 +60,7 @@ declare namespace Cypress {
name: "electron" | "chrome" | "canary" | "chromium" | "firefox"
displayName: "Electron" | "Chrome" | "Canary" | "Chromium" | "FireFox"
version: string
majorVersion: string
majorVersion: number
path: string
isHeaded: boolean
isHeadless: boolean
@@ -620,13 +620,60 @@ declare namespace Cypress {
* @see https://on.cypress.io/dblclick
*/
dblclick(options?: Partial<ClickOptions>): Chainable<Subject>
/**
* Double-click a DOM element at specific corner / side.
*
* @param {String} position - The position where the click should be issued.
* The `center` position is the default position.
* @see https://on.cypress.io/dblclick
* @example
* cy.get('button').dblclick('topRight')
*/
dblclick(position: string, options?: Partial<ClickOptions>): Chainable<Subject>
/**
* Double-click a DOM element at specific coordinates
*
* @param {number} x The distance in pixels from the elements left to issue the click.
* @param {number} y The distance in pixels from the elements top to issue the click.
* @see https://on.cypress.io/dblclick
* @example
```
// The click below will be issued inside of the element
// (15px from the left and 40px from the top).
cy.get('button').dblclick(15, 40)
```
*/
dblclick(x: number, y: number, options?: Partial<ClickOptions>): Chainable<Subject>
/**
* Right-click a DOM element.
*
* @see https://on.cypress.io/rightclick
*/
rightclick(options?: Partial<ClickOptions>): Chainable<Subject>
/**
* Right-click a DOM element at specific corner / side.
*
* @param {String} position - The position where the click should be issued.
* The `center` position is the default position.
* @see https://on.cypress.io/click
* @example
* cy.get('button').rightclick('topRight')
*/
rightclick(position: string, options?: Partial<ClickOptions>): Chainable<Subject>
/**
* Right-click a DOM element at specific coordinates
*
* @param {number} x The distance in pixels from the elements left to issue the click.
* @param {number} y The distance in pixels from the elements top to issue the click.
* @see https://on.cypress.io/rightclick
* @example
```
// The click below will be issued inside of the element
// (15px from the left and 40px from the top).
cy.get('button').rightclick(15, 40)
```
*/
rightclick(x: number, y: number, options?: Partial<ClickOptions>): Chainable<Subject>
/**
* Set a debugger and log what the previous command yields.
@@ -819,6 +866,13 @@ declare namespace Cypress {
*/
hash(options?: Partial<Loggable & Timeoutable>): Chainable<string>
/**
* Invoke a function in an array of functions.
* @see https://on.cypress.io/invoke
*/
invoke<T extends (...args: any[]) => any, Subject extends T[]>(index: number): Chainable<ReturnType<T>>
invoke<T extends (...args: any[]) => any, Subject extends T[]>(options: Loggable, index: number): Chainable<ReturnType<T>>
/**
* Invoke a function on the previously yielded subject.
* This isn't possible to strongly type without generic override yet.
@@ -829,6 +883,7 @@ declare namespace Cypress {
* @see https://on.cypress.io/invoke
*/
invoke(functionName: keyof Subject, ...args: any[]): Chainable<Subject> // don't have a way to express return types yet
invoke(options: Loggable, functionName: keyof Subject, ...args: any[]): Chainable<Subject>
/**
* Get a propertys value on the previously yielded subject.
@@ -840,7 +895,15 @@ declare namespace Cypress {
* // Drill into nested properties by using dot notation
* cy.wrap({foo: {bar: {baz: 1}}}).its('foo.bar.baz')
*/
its<K extends keyof Subject>(propertyName: K): Chainable<Subject[K]>
its<K extends keyof Subject>(propertyName: K, options?: Loggable): Chainable<Subject[K]>
/**
* Get a value by index from an array yielded from the previous command.
* @see https://on.cypress.io/its
* @example
* cy.wrap(['a', 'b']).its(1).should('equal', 'b')
*/
its<T, Subject extends T[]>(index: number, options?: Loggable): Chainable<T>
/**
* Get the last DOM element within a set of DOM elements.
+21 -3
View File
@@ -92,11 +92,29 @@ namespace CypressLocalStorageTest {
}
}
cy.wrap({ foo: [1, 2, 3] })
.its('foo')
.each((s: number) => {
namespace CypressItsTests {
cy.wrap({ foo: [1, 2, 3] })
.its('foo')
.each((s: number) => {
s
})
cy.wrap({foo: 'bar'}).its('foo') // $ExpectType Chainable<string>
cy.wrap([1, 2]).its(1) // $ExpectType Chainable<number>
cy.wrap(['foo', 'bar']).its(1) // $ExpectType Chainable<string>
.then((s: string) => {
s
})
}
namespace CypressInvokeTests {
const returnsString = () => 'foo'
const returnsNumber = () => 42
// unfortunately could not define more precise type
// in this case it should have been "number", but so far no luck
cy.wrap([returnsString, returnsNumber]).invoke(1) // $ExpectType Chainable<any>
}
cy.wrap({ foo: ['bar', 'baz'] })
.its('foo')
+13 -3
View File
@@ -1,6 +1,6 @@
{
"name": "cypress",
"version": "3.5.0",
"version": "3.7.0",
"description": "Cypress.io end to end testing tool",
"private": true,
"scripts": {
@@ -17,6 +17,7 @@
"bump": "node ./scripts/binary.js bump",
"check-deps": "node ./scripts/check-deps.js --verbose",
"check-deps-pre": "node ./scripts/check-deps.js --verbose --prescript",
"check-next-dev-version": "node scripts/check-next-dev-version.js",
"check-node-version": "node scripts/check-node-version.js",
"check-terminal": "node scripts/check-terminal.js",
"clean-deps": "npm run all clean-deps && rm -rf node_modules",
@@ -51,11 +52,13 @@
"stop-only-all": "npm run stop-only -- --folder packages",
"test": "echo '⚠️ This root monorepo is only for local development and new contributions. There are no tests.'",
"test-debug-package": "node ./scripts/test-debug-package.js",
"test-jscodeshift": "jest ./scripts/decaff",
"test-mocha": "mocha --reporter spec scripts/spec.js",
"test-mocha-snapshot": "mocha scripts/mocha-snapshot-spec.js",
"test-s3-api": "node -r ./packages/coffee/register -r ./packages/ts/register scripts/binary/s3-api-demo.ts",
"test-scripts": "mocha -r packages/coffee/register -r packages/ts/register --reporter spec 'scripts/unit/**/*spec.js'",
"test-scripts-watch": "npm run test-scripts -- --watch --watch-extensions 'ts,js,coffee'",
"test-unit": "node ./scripts/test-unit",
"watch": "npm run all watch"
},
"husky": {
@@ -74,20 +77,25 @@
"@fellow/eslint-plugin-coffee": "0.4.13",
"@types/bluebird": "3.5.21",
"@types/chai": "4.1.7",
"@types/chai-enzyme": "0.6.7",
"@types/classnames": "2.2.9",
"@types/debug": "4.1.4",
"@types/execa": "0.7.2",
"@types/fs-extra": "8.0.0",
"@types/glob": "7.1.1",
"@types/lodash": "4.14.122",
"@types/markdown-it": "0.0.9",
"@types/mini-css-extract-plugin": "0.8.0",
"@types/mocha": "5.2.7",
"@types/node": "11.12.0",
"@types/node": "12.12.14",
"@types/ramda": "0.25.47",
"@types/react-dom": "16.9.4",
"@types/request-promise": "4.1.42",
"@types/sinon-chai": "3.2.2",
"@typescript-eslint/eslint-plugin": "1.11.0",
"@typescript-eslint/parser": "1.11.0",
"ansi-styles": "3.2.1",
"arg": "4.1.0",
"arg": "4.1.2",
"ascii-table": "0.0.9",
"aws-sdk": "2.447.0",
"babel-eslint": "10.0.1",
@@ -126,6 +134,7 @@
"husky": "2.4.1",
"inquirer": "3.3.0",
"inquirer-confirm": "2.0.3",
"jest": "24.9.0",
"js-codemod": "cpojer/js-codemod#29dafed",
"jscodemods": "cypress-io/jscodemods#01b546e",
"jscodeshift": "0.6.3",
@@ -156,6 +165,7 @@
"stop-only": "3.0.1",
"strip-ansi": "4.0.0",
"terminal-banner": "1.1.0",
"through": "2.3.8",
"ts-node": "8.3.0",
"typescript": "3.5.3",
"vinyl-paths": "2.1.0"
@@ -34,10 +34,11 @@
"commandTimeout": 4000,
"cypressHostUrl": "http://localhost:2020",
"cypressEnv": "development",
"env": {
},
"blacklistHosts": ["www.google-analytics.com", "hotjar.com"],
"env": {},
"blacklistHosts": [
"www.google-analytics.com",
"hotjar.com"
],
"execTimeout": 60000,
"fileServerFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink",
"fixturesFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/fixtures",
@@ -46,9 +47,7 @@
"integrationFolder": "/Users/jennifer/Dev/Projects/cypress-example-kitchensink/cypress/integration",
"isHeadless": false,
"isNewProject": false,
"javascripts": [
],
"javascripts": [],
"morgan": true,
"namespace": "__cypress",
"numTestsKeptInMemory": 50,
@@ -180,6 +179,43 @@
"from": "config",
"value": "http://localhost:8080"
},
"browsers": {
"from": "plugins",
"value": [
{
"name": "chrome",
"displayName": "Chrome",
"family": "chrome",
"version": "50.0.2661.86",
"path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"majorVersion": "50"
},
{
"name": "chromium",
"displayName": "Chromium",
"family": "chrome",
"version": "49.0.2609.0",
"path": "/Users/bmann/Downloads/chrome-mac/Chromium.app/Contents/MacOS/Chromium",
"majorVersion": "49"
},
{
"name": "canary",
"displayName": "Canary",
"family": "chrome",
"version": "48.0",
"path": "/Users/bmann/Downloads/chrome-mac/Canary.app/Contents/MacOS/Canary",
"majorVersion": "48"
},
{
"name": "electron",
"family": "electron",
"displayName": "Electron",
"path": "",
"version": "99.101.1234",
"majorVersion": "99"
}
]
},
"commandTimeout": {
"from": "default",
"value": 4000
@@ -269,7 +305,8 @@
"blacklistHosts": {
"from": "config",
"value": [
"www.google-analytics.com", "hotjar.com"
"www.google-analytics.com",
"hotjar.com"
]
},
"hosts": {
@@ -167,7 +167,7 @@ describe('Project Nav', function () {
it('displays browser icon as spinner', () => {
cy.get('.browsers-list>a').first().find('i')
.should('have.class', 'fa fa-refresh fa-spin')
.should('have.class', 'fas fa-sync-alt fa-spin')
})
it('disables browser dropdown', () => {
@@ -185,7 +185,7 @@ describe('Project Nav', function () {
it('displays browser icon as opened', () => {
cy.get('.browsers-list>a').first().find('i')
.should('have.class', 'fa fa-check-circle-o')
.should('have.class', 'fas fa-check-circle')
})
it('disables browser dropdown', () => {
@@ -77,17 +77,85 @@ describe('Settings', () => {
cy.contains('Your project\'s configuration is displayed')
})
it('displays legend in table', () => {
cy.get('table>tbody>tr').should('have.length', 6)
it('displays browser information which is collapsed by default', () => {
cy.contains('.config-vars', 'browsers')
cy.get('.config-vars').invoke('text')
.should('not.contain', '0:Chrome')
cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
cy.get('.config-vars').invoke('text')
.should('contain', '0:Chrome')
})
it('wraps config line in proper classes', () => {
cy.get('.line').first().within(() => {
cy.contains('animationDistanceThreshold').should('have.class', 'key')
cy.contains(':').should('have.class', 'colon')
cy.contains('5').should('have.class', 'default')
cy.contains(',').should('have.class', 'comma')
})
it('removes the summary list of values once a key is expanded', () => {
cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
cy.get('.config-vars').invoke('text')
.should('not.contain', 'Chrome, Chromium')
cy.get('.config-vars').invoke('text')
.should('contain', '0:Chrome')
})
it('distinguishes between Arrays and Objects when expanded', () => {
cy.get('.config-vars').invoke('text')
.should('not.contain', 'browsers: Array (4)')
cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
cy.get('.config-vars').invoke('text')
.should('contain', 'browsers: Array (4)')
})
it('applies the same color treatment to expanded key values as the root key', () => {
cy.contains('span', 'browsers').parents('div').first().find('span').first().click()
cy.get('.config-vars').as('config-vars')
.contains('span', 'Chrome').parent('span').should('have.class', 'plugins')
cy.get('@config-vars')
.contains('span', 'Chromium').parent('span').should('have.class', 'plugins')
cy.get('@config-vars')
.contains('span', 'Canary').parent('span').should('have.class', 'plugins')
cy.get('@config-vars')
.contains('span', 'Electron').parent('span').should('have.class', 'plugins')
cy.contains('span', 'blacklistHosts').parents('div').first().find('span').first().click()
cy.get('@config-vars')
.contains('span', 'www.google-analytics.com').parent('span').should('have.class', 'config')
cy.get('@config-vars')
.contains('span', 'hotjar.com').parent('span').should('have.class', 'config')
cy.contains('span', 'hosts').parents('div').first().find('span').first().click()
cy.get('@config-vars')
.contains('span', '127.0.0.1').parent('span').should('have.class', 'config')
cy.get('@config-vars')
.contains('span', '127.0.0.2').parent('span').should('have.class', 'config')
cy.get('@config-vars')
.contains('span', 'Electron').parents('div').first().find('span').first().click()
cy.get('@config-vars').contains('span', 'electron').parents('li').eq(1).find('.line .plugins').should('have.length', 6)
})
it('displays string values as quoted strings', () => {
cy.get('.config-vars').invoke('text')
.should('contain', 'baseUrl:"http://localhost:8080"')
})
it('displays undefined and null without quotations', () => {
cy.get('.config-vars').invoke('text')
.should('not.contain', '"undefined"')
.should('not.contain', '"null"')
})
it('does not show the root config label', () => {
cy.get('.config-vars').find('> ol > li > div').should('have.css', 'display', 'none')
})
it('displays legend in table', () => {
cy.get('table>tbody>tr').should('have.length', 6)
})
it('displays "true" values', () => {
@@ -99,26 +167,13 @@ describe('Settings', () => {
})
it('displays "object" values for env and hosts', () => {
cy.get('.nested-obj').eq(0)
.contains('fixturesFolder')
cy.get('.line').contains('www.google-analytics.com, hotjar.com')
cy.get('.nested-obj').eq(1)
.contains('*.foobar.com')
cy.get('.line').contains('*.foobar.com, *.bazqux.com')
})
it('displays "array" values for blacklistHosts', () => {
cy.get('.nested-arr')
.parent()
.should('contain', '[')
.and('contain', ']')
.and('not.contain', '0')
.and('not.contain', '1')
.find('.line .config').should(($lines) => {
expect($lines).to.have.length(2)
expect($lines).to.contain('www.google-analytics.com')
expect($lines).to.contain('hotjar.com')
})
cy.contains('.line', 'blacklistHosts').contains('www.google-analytics.com, hotjar.com')
})
it('opens help link on click', () => {
@@ -447,6 +447,12 @@ describe('Set Up Project', function () {
cy.get('.modal').contains('Log In to Dashboard')
})
it('closes login modal', () => {
cy.get('.modal').contains('Log In to Dashboard')
cy.get('.close').click()
cy.get('.btn').contains('Set up project').click()
})
describe('when login succeeds', function () {
beforeEach(function () {
cy.stub(this.ipc, 'beginAuth').resolves(this.user)
@@ -454,6 +460,7 @@ describe('Set Up Project', function () {
})
it('shows setup', () => {
cy.get('.login-content > .btn').click()
cy.contains('h4', 'Set up project')
})
})
@@ -186,7 +186,7 @@ describe('Specs List', function () {
})
it('updates spec icon', function () {
cy.get('@allSpecs').find('i').should('have.class', 'fa-dot-circle-o')
cy.get('@allSpecs').find('i').should('have.class', 'fa-dot-circle')
cy.get('@allSpecs').find('i').should('not.have.class', 'fa-play')
})
@@ -424,8 +424,8 @@ describe('Specs List', function () {
})
it('updates spec icon', function () {
cy.get('@firstSpec').find('i').should('have.class', 'fa-dot-circle-o')
cy.get('@firstSpec').find('i').should('not.have.class', 'fa-file-code-o')
cy.get('@firstSpec').find('i').should('have.class', 'fa-dot-circle')
cy.get('@firstSpec').find('i').should('not.have.class', 'fa-file-code')
})
it('sets spec as active', () => {
@@ -439,7 +439,7 @@ describe('Specs List', function () {
})
it('updates spec icon', () => {
cy.get('@deepSpec').find('i').should('have.class', 'fa-dot-circle-o')
cy.get('@deepSpec').find('i').should('have.class', 'fa-dot-circle')
})
it('sets spec as active', () => {
@@ -461,8 +461,8 @@ describe('Specs List', function () {
})
it('updates spec icon', function () {
cy.get('@firstSpec').find('i').should('not.have.class', 'fa-dot-circle-o')
cy.get('@secondSpec').find('i').should('have.class', 'fa-dot-circle-o')
cy.get('@firstSpec').find('i').should('not.have.class', 'fa-dot-circle')
cy.get('@secondSpec').find('i').should('have.class', 'fa-dot-circle')
})
it('updates active spec', function () {
+7 -5
View File
@@ -10,23 +10,24 @@
"check-deps": "node ../../scripts/check-deps.js --verbose",
"check-deps-pre": "npm run check-deps -- --prescript",
"clean-deps": "rm -rf node_modules",
"cypress:open": "TZ=America/New_York node ../../scripts/cypress open --project .",
"cypress:run": "TZ=America/New_York node ../../scripts/cypress run --project .",
"cypress:open": "cross-env TZ=America/New_York node ../../scripts/cypress open --project .",
"cypress:run": "cross-env TZ=America/New_York node ../../scripts/cypress run --project .",
"postinstall": "echo '@packages/desktop-gui needs: npm run build'",
"prewatch": "npm run check-deps-pre",
"watch": "npm run build -- --watch --progress"
},
"devDependencies": {
"@babel/polyfill": "^7.7.0",
"@cypress/icons": "0.7.0",
"@cypress/json-schemas": "5.32.2",
"@cypress/json-schemas": "5.33.0",
"@cypress/react-tooltip": "0.5.3",
"@fortawesome/fontawesome-free": "5.11.2",
"bin-up": "1.2.2",
"bluebird": "3.5.3",
"bootstrap-sass": "3.4.1",
"classnames": "2.2.6",
"cross-env": "5.2.1",
"cross-env": "6.0.3",
"fira": "cypress-io/fira#fb63362742eea8cdce0d90825ab9264d77719e3d",
"font-awesome": "4.7.0",
"gravatar": "1.8.0",
"human-interval": "0.1.6",
"lodash": "4.17.15",
@@ -40,6 +41,7 @@
"react": "16.8.6",
"react-bootstrap-modal": "4.2.0",
"react-dom": "16.8.6",
"react-inspector": "^4.0.0",
"react-loader": "2.4.5",
"webpack": "4.35.3",
"webpack-cli": "3.3.2"
@@ -13,14 +13,14 @@ const GlobalError = observer(() => {
return (
<div className='global-error alert alert-danger'>
<p>
<i className='fa fa-warning'></i>{' '}
<i className="fas fa-exclamation-triangle"></i>{' '}
<strong>{appStore.error.name || 'Unexpected Error'}</strong>
</p>
<p dangerouslySetInnerHTML={{
__html: appStore.error.message.split('\n').join('<br />'),
}} />
<button className='btn btn-link close' onClick={remove}>
<i className='fa fa-remove' />
<i className='fas fa-times' />
</button>
</div>
)
+3 -3
View File
@@ -34,8 +34,8 @@ class Default extends Component {
onDrop={this._drop}
>
<span className="fa-stack fa-lg">
<i className="fa fa-folder fa-stack-2x"></i>
<i className="fa fa-plus fa-stack-1x"></i>
<i className="fas fa-folder fa-stack-2x"></i>
<i className="fas fa-plus fa-stack-1x"></i>
</span>
<p>Drag your project here or <a href="#" onClick={this._selectProject}>select manually</a>.</p>
</div>
@@ -51,7 +51,7 @@ class Default extends Component {
return (
<div className='local-install-notice alert alert-info alert-dismissible'>
<p className='text-center'>
<i className='fa fa-info-circle'></i>{' '}
<i className='fas fa-info-circle'></i>{' '}
We recommend versioning Cypress per project and{' '}
<a onClick={this._openHelp} className='helper-docs-link'>
installing it via <span className='mono'>npm</span>
+1 -2
View File
@@ -75,8 +75,7 @@
line-height: 3em;
}
.fa {
// font-size: 50px;
.fas {
color: #d3d6d8;
&.fa-stack-2x {
+7 -7
View File
@@ -25,12 +25,12 @@ export default class Nav extends Component {
<ul className='nav'>
<li>
<a onClick={this._openSupport} href='#'>
<i className='fa fa-question-circle'></i> Support
<i className='fas fa-question-circle'></i> Support
</a>
</li>
<li>
<a onClick={this._openDocs} href='#'>
<i className='fa fa-graduation-cap'></i> Docs
<i className='fas fa-graduation-cap'></i> Docs
</a>
</li>
{this._userStateButton()}
@@ -51,7 +51,7 @@ export default class Nav extends Component {
if (appStore.isGlobalMode && project) {
return (
<Link to={routes.intro()}>
<i className='fa fa-chevron-left'></i> Back
<i className='fas fa-chevron-left'></i> Back
</Link>
)
}
@@ -69,7 +69,7 @@ export default class Nav extends Component {
return (
<li>
<div>
<i className='fa fa-user' /> <i className='fa fa-spinner fa-spin' />
<i className='fas fa-user' /> <i className='fas fa-spinner fa-spin' />
</div>
</li>
)
@@ -79,7 +79,7 @@ export default class Nav extends Component {
return (
<li>
<a onClick={this._showLogin}>
<i className='fa fa-user' /> Log In
<i className='fas fa-user' /> Log In
</a>
</li>
)
@@ -114,7 +114,7 @@ export default class Nav extends Component {
return (
<span>
<i className='fa fa-sign-out'></i>{' '}
<i className='fas fa-sign-out-alt'></i>{' '}
Log Out
</span>
)
@@ -127,7 +127,7 @@ export default class Nav extends Component {
}
_showLogin () {
authStore.setShowingLogin(true)
authStore.openLogin()
}
_openDocs (e) {
+19
View File
@@ -34,6 +34,13 @@
}
}
&.left-nav {
i {
font-size: 11px;
color: #555;
}
}
> li {
> div,
> a {
@@ -41,6 +48,8 @@
color: #444;
padding: 1px 20px 0 20px;
line-height: 35px;
}
> a {
@@ -69,6 +78,12 @@
}
}
}
.browsers.nav {
i {
font-size: 13px;
}
}
}
@@ -121,6 +136,10 @@
}
}
i {
font-size: 12px;
}
> li > div,
> li > a,
> li > span > a {
+13 -2
View File
@@ -19,9 +19,20 @@ class AuthStore {
this.message = message
}
@action setShowingLogin (isShowing) {
@action openLogin (onCloseCb) {
this.onCloseCb = onCloseCb
this.setMessage(null)
this.isShowingLogin = isShowing
this.isShowingLogin = true
}
@action closeLogin () {
if (this.onCloseCb) {
this.onCloseCb(this.isAuthenticated)
}
this.setMessage(null)
this.isShowingLogin = false
}
@action setUser (user) {
+3 -3
View File
@@ -61,7 +61,7 @@ class LoginForm extends Component {
if (message && message.name === 'AUTH_COULD_NOT_LAUNCH_BROWSER') {
return (
<span>
<i className='fa fa-exclamation-triangle'></i>{' '}
<i className='fas fa-exclamation-triangle'></i>{' '}
Could not open browser.
</span>
)
@@ -69,7 +69,7 @@ class LoginForm extends Component {
return (
<span>
<i className='fa fa-spinner fa-spin'></i>{' '}
<i className='fas fa-spinner fa-spin'></i>{' '}
{message && message.browserOpened ? 'Waiting for browser login...' : 'Opening browser...'}
</span>
)
@@ -90,7 +90,7 @@ class LoginForm extends Component {
return (
<div className='alert alert-danger'>
<p>
<i className='fa fa-warning'></i>{' '}
<i className='fas fa-exclamation-triangle'></i>{' '}
<strong>Can't Log In</strong>
</p>
<p>{this._errorMessage(error.message)}</p>
@@ -8,7 +8,7 @@ import authStore from './auth-store'
import ipc from '../lib/ipc'
const close = () => {
authStore.setShowingLogin(false)
authStore.closeLogin()
}
// LoginContent is a separate component so that it pings the api
@@ -68,7 +68,7 @@ class LoginContent extends Component {
return (
<div className='modal-body login'>
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
<h1><i className='fa fa-lock'></i> Log In</h1>
<h1><i className='fas fa-lock'></i> Log In</h1>
<p>Logging in gives you access to the <a onClick={this._openDashboard}>Cypress Dashboard Service</a>. You can set up projects to be recorded and see test data from your project.</p>
<LoginForm onSuccess={() => this.setState({ succeeded: true })} />
</div>
@@ -79,7 +79,7 @@ class LoginContent extends Component {
return (
<div className='modal-body login'>
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
<h1><i className='fa fa-check'></i> Login Successful</h1>
<h1><i className='fas fa-check'></i> Login Successful</h1>
<p>You are now logged in as {authStore.user.name}.</p>
<div className='login-content'>
<button
@@ -97,14 +97,14 @@ class LoginContent extends Component {
return (
<div className='modal-body login login-no-api-server'>
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
<h4><i className='fa fa-wifi'></i> Cannot connect to API server</h4>
<h4><i className='fas fa-wifi'></i> Cannot connect to API server</h4>
<p>Logging in requires connecting to an external API server. We tried but failed to connect to the API server at <em>{this.state.apiUrl}</em></p>
<p>
<button
className='btn btn-default btn-sm'
onClick={this._pingApiServer}
>
<i className='fa fa-refresh'></i>{' '}
<i className='fas fa-sync-alt'></i>{' '}
Try again
</button>
</p>
@@ -21,7 +21,7 @@ export default class TimerDisplay extends Component {
render () {
return (
<span className='env-duration'>
<i className='fa fa-hourglass-half'></i>
<i className='fas fa-hourglass-half'></i>
{this.timerStore.mainDisplay}
</span>
)
+1 -1
View File
@@ -65,7 +65,7 @@ export const getStatusIcon = (status) => {
case 'passed':
return 'check-circle'
case 'running':
return 'refresh fa-spin'
return 'sync-alt fa-spin'
case 'overLimit':
return 'exclamation-triangle'
case 'timedOut':
@@ -35,7 +35,7 @@ export default class Browsers extends Component {
return (
<li className='close-browser'>
<button className='btn btn-xs btn-danger' onClick={this._closeBrowser.bind(this)}>
<i className='fa fa-fw fa-times'></i>
<i className='fas fa-fw fa-times'></i>
Stop
</button>
</li>
@@ -58,19 +58,19 @@ export default class Browsers extends Component {
let prefixText
if (project.browserState === 'opening') {
icon = 'refresh fa-spin'
icon = 'fas fa-sync-alt fa-spin'
prefixText = 'Opening'
} else if (project.browserState === 'opened') {
icon = 'check-circle-o green'
icon = 'fas fa-check-circle green far'
prefixText = 'Running'
} else {
icon = browser.icon
icon = `fab fa-${browser.icon}`
prefixText = ''
}
return (
<span className={browser.name}>
<i className={`fa fa-${icon}`}></i>{' '}
<i className={icon}></i>{' '}
{prefixText}{' '}
{browser.displayName}{' '}
{browser.majorVersion}
@@ -90,7 +90,7 @@ export default class Browsers extends Component {
placement='bottom'
className='browser-info-tooltip cy-tooltip'
>
<i className='fa fa-exclamation-triangle' />
<i className='fas fa-exclamation-triangle' />
</Tooltip>
</span>
)
@@ -106,7 +106,7 @@ export default class Browsers extends Component {
placement='bottom'
className='browser-info-tooltip cy-tooltip'
>
<i className='fa fa-info-circle' />
<i className='fas fa-info-circle' />
</Tooltip>
</span>
)
@@ -9,22 +9,22 @@ export default class ProjectNav extends Component {
return (
<nav className='project-nav navbar navbar-default'>
<ul className='nav'>
<ul className='nav left-nav'>
<li>
<Link to={routes.specs(project)}>
<i className='fa fa-code'></i>{' '}
<i className='fas fa-code'></i>{' '}
Tests
</Link>
</li>
<li>
<Link to={routes.runs(project)}>
<i className='fa fa-database'></i>{' '}
<i className='fas fa-database'></i>{' '}
Runs
</Link>
</li>
<li>
<Link to={routes.settings(project)}>
<i className='fa fa-cog'></i>{' '}
<i className='fas fa-cog'></i>{' '}
Settings
</Link>
</li>
@@ -60,7 +60,7 @@ class ErrorMessage extends Component {
<div className='full-alert-container'>
<div className='full-alert alert alert-danger error'>
<p className='header'>
<i className='fa fa-warning'></i>{' '}
<i className='fas fa-exclamation-triangle'></i>{' '}
<strong>{err.title || 'Can\'t start server'}</strong>
</p>
<span className='alert-content'>
@@ -81,7 +81,7 @@ class ErrorMessage extends Component {
className='btn btn-default btn-sm'
onClick={this.props.onTryAgain}
>
<i className='fa fa-refresh'></i>{' '}
<i className='fas fa-sync-alt'></i>{' '}
Try Again
</button>
</div>
@@ -43,12 +43,12 @@ class OnBoarding extends Component {
<p>
We've added some folders and example tests to your project. Try running the tests in the
<strong onClick={this._openExampleSpec}>
<i className='fa fa-folder-o'></i>{' '}
<i className='far fa-folder'></i>{' '}
{project.integrationExampleName}{' '}
</strong>
folder or add your own test files to
<strong onClick={this._openIntegrationFolder}>
<i className='fa fa-folder-o'></i>{' '}
<i className='far fa-folder'></i>{' '}
cypress/integration
</strong>.
</p>
@@ -56,13 +56,13 @@ class OnBoarding extends Component {
<ul>
<li>
<span>
<i className='fa fa-folder-open-o'></i>{' '}
<i className='far fa-folder-open'></i>{' '}
{project.name}
</span>
<ul>
<li className='app-code'>
<span >
<i className='fa fa-folder-o'></i>{' '}
<i className='far fa-folder'></i>{' '}
...
</span>
</li>
@@ -98,7 +98,7 @@ class OnBoarding extends Component {
return (
<li className={cs(className, 'new-item')} key={file.name}>
<span>
<i className='fa fa-folder-open-o'></i>{' '}
<i className='far fa-folder-open'></i>{' '}
{file.name}
</span>
<ul>
@@ -111,7 +111,7 @@ class OnBoarding extends Component {
return (
<li className={cs(className, 'new-item', { 'is-more': file.more })} key={file.name}>
<span>
<i className='fa fa-file-code-o'></i>{' '}
<i className='far fa-file-code'></i>{' '}
{file.name}
</span>
</li>
@@ -10,14 +10,14 @@ class WarningMessage extends Component {
return (
<div className='alert alert-warning'>
<p className='header'>
<i className='fa fa-warning'></i>{' '}
<i className='fas fa-exclamation-triangle'></i>{' '}
<strong>Warning</strong>
</p>
<div>
<MarkdownRenderer markdown={warningText}/>
</div>
<button className='btn btn-link close' onClick={this.props.onClearWarning}>
<i className='fa fa-remove' />
<i className='fas fa-times' />
</button>
</div>
)
@@ -17,7 +17,7 @@ const ProjectListItem = observer(({ project, onSelect, onRemove }) => (
e.stopPropagation()
onRemove()
}}>
<i className='fa fa-remove' />
<i className='fas fa-times' />
</button>
</li>
))
@@ -46,7 +46,7 @@ class ProjectsList extends Component {
return (
<div className='alert alert-danger'>
<p>
<i className='fa fa-warning'></i>{' '}
<i className='fas fa-exclamation-triangle'></i>{' '}
<strong>Error</strong>
</p>
<p dangerouslySetInnerHTML={{
@@ -29,7 +29,7 @@ const ErrorMessage = observer(({ error }) => {
<div className='runs-list-error'>
<div className='empty'>
<h4>
<i className='fa fa-warning red'></i>{' '}
<i className='fas fa-exclamation-triangle red'></i>{' '}
Runs could not be loaded
</h4>
{errorMessage}
@@ -55,10 +55,10 @@ class PermissionMessage extends Component {
onClick={this._requestAccess}
>
<span>
<i className='fa fa-paper-plane'></i>{' '}
<i className='fas fa-paper-plane'></i>{' '}
Request access
</span>
<i className='fa fa-spinner fa-spin'></i>
<i className='fas fa-spinner fa-spin'></i>
</button>
)
}
@@ -67,7 +67,7 @@ class PermissionMessage extends Component {
return (
<div className='empty'>
<h4>
<i className='fa fa-check passed'></i>{' '}
<i className='fas fa-check passed'></i>{' '}
Request sent
</h4>
<p>
@@ -89,7 +89,7 @@ class PermissionMessage extends Component {
return (
<div className='empty'>
<h4>
<i className='fa fa-exclamation-triangle failed'></i>{' '}
<i className='fas fa-exclamation-triangle failed'></i>{' '}
Request Failed
</h4>
<p>An unexpected error occurred while requesting access:</p>
@@ -106,7 +106,7 @@ class PermissionMessage extends Component {
return (
<div className="empty">
<h4>
<i className='fa fa-lock'></i>{' '}
<i className='fas fa-lock'></i>{' '}
Request access to see the runs
</h4>
<p>This is a private project created by someone else.</p>
@@ -6,6 +6,7 @@ import BootstrapModal from 'react-bootstrap-modal'
import ipc from '../lib/ipc'
import { configFileFormatted } from '../lib/config-file-formatted'
import SetupProject from './setup-project-modal'
import authStore from '../auth/auth-store'
@observer
export default class ProjectNotSetup extends Component {
@@ -53,7 +54,7 @@ export default class ProjectNotSetup extends Component {
className='btn btn-primary'
onClick={this._showSetupProjectModal}
>
<i className='fa fa-wrench'></i>{' '}
<i className='fas fa-wrench'></i>{' '}
Set up project to record
</button>
</div>
@@ -69,7 +70,7 @@ export default class ProjectNotSetup extends Component {
return (
<div className='empty-runs-not-displayed'>
<h4>
<i className='fa fa-warning errored'></i>{' '}
<i className='fas fa-exclamation-triangle errored'></i>{' '}
Runs cannot be displayed
</h4>
<p>We were unable to find an existing project matching the <code>projectId</code> in your {configFileFormatted(this.props.project.configFile)}.</p>
@@ -79,7 +80,7 @@ export default class ProjectNotSetup extends Component {
className='btn btn-warning'
onClick={this._showSetupProjectModal}
>
<i className='fa fa-wrench'></i>{' '}
<i className='fas fa-wrench'></i>{' '}
Set up a new project
</button>
<p>
@@ -92,6 +93,22 @@ export default class ProjectNotSetup extends Component {
_projectSetup () {
if (!this.state.setupProjectModalOpen) return null
if (!this.props.isAuthenticated) {
authStore.openLogin((isAuthenticated) => {
if (!isAuthenticated) {
// auth was canceled, cancel project setup too
this.setState({ setupProjectModalOpen: false })
}
})
return null
}
if (this.props.isShowingLogin) {
// login dialog still open, wait for it to close before proceeding
return null
}
return (
<SetupProject
project={this.props.project}
@@ -18,7 +18,7 @@ const RunDuration = ({ run }) => {
return (
<Tooltip title="Parallelization was disabled for this run." placement="top" className="cy-tooltip">
<span className='env-duration'>
<i className='fa fa-exclamation-triangle orange'></i>
<i className='fas fa-exclamation-triangle orange'></i>
{' '}{durationFormatted(run.totalDuration)}
</span>
</Tooltip>
@@ -27,7 +27,7 @@ const RunDuration = ({ run }) => {
return (
<span className='env-duration'>
<i className='fa fa-hourglass-end'></i>
<i className='fas fa-hourglass-end'></i>
{' '}{durationFormatted(run.totalDuration)}
</span>
)
@@ -57,7 +57,7 @@ export default class RunsListItem extends Component {
<div className='row-column-wrapper'>
<div>
<Tooltip title={_.startCase(run.status)} className='cy-tooltip'>
<i className={`fa ${run.status} fa-${getStatusIcon(run.status)}`}></i>
<i className={`fas ${run.status} fa-${getStatusIcon(run.status)}`}></i>
</Tooltip>
{' '}#{run.buildNumber}
</div>
@@ -106,7 +106,7 @@ export default class RunsListItem extends Component {
</div>
<div className='row-column-wrapper'>
<div>
<i className='fa fa-clock-o'></i>{' '}
<i className='far fa-clock'></i>{' '}
{moment(run.createdAt).fromNow() === '1 secs ago' ? '1 sec ago' : moment(run.createdAt).fromNow()}
</div>
</div>
@@ -122,12 +122,12 @@ export default class RunsListItem extends Component {
this._instancesExist() ?
this._moreThanOneInstance() && this._osLength() > 1 ?
<div>
<i className='fa fa-fw fa-desktop'></i>{' '}
<i className='fas fa-fw fa-desktop'></i>{' '}
{this._osLength()} OSs
</div> :
// or did we only actual run it on one OS
<div>
<i className={`fa fa-fw fa-${osIcon(this.props.run.instances[0].platform.osName)}`}></i>{' '}
<i className={`fa-fw ${this._osIcon()}`}></i>{' '}
{this._osDisplay()}
</div> :
null
@@ -137,12 +137,12 @@ export default class RunsListItem extends Component {
this._instancesExist() ?
this._moreThanOneInstance() && this._browsersLength() > 1 ?
<div className='env-msg'>
<i className='fa fa-fw fa-globe'></i>{' '}
<i className='fas fa-fw fa-globe'></i>{' '}
{this._browsersLength()} browsers
</div> :
// or did we only actual run it on one browser
<div className='env-msg'>
<i className={`fa fa-fw fa-${this._browserIcon()}`}></i>{' '}
<i className={`fa-fw ${this._browserIcon()}`}></i>{' '}
{this._browserDisplay()}
</div> :
null
@@ -153,7 +153,7 @@ export default class RunsListItem extends Component {
{
run.status !== 'running' ?
<div className='result'>
<i className='fa fa-check'></i>{' '}
<i className='fas fa-check'></i>{' '}
<span>
{run.totalPassed || '0'}
</span>
@@ -165,7 +165,7 @@ export default class RunsListItem extends Component {
{
run.status !== 'running' ?
<div className='result'>
<i className='fa fa-times'></i>{' '}
<i className='fas fa-times'></i>{' '}
<span>
{run.totalFailed || '0'}
</span>
@@ -220,19 +220,15 @@ export default class RunsListItem extends Component {
}
_browserIcon () {
return browserIcon(_.get(this.props.run, 'instances[0].platform.browserName', ''))
const icon = browserIcon(_.get(this.props.run, 'instances[0].platform.browserName', ''))
return icon === 'globe' ? `fas fa-${icon}` : `fab fa-${icon}`
}
_osIcon () {
if (!this.props.run.instances) return
const icon = osIcon(this.props.run.instances[0].platform.osName)
return _
.chain(this.props.run.instances)
.map((instance) => {
return `${_.get(instance, 'platform.osName', '')} + ${_.get(instance, 'platform.osVersion', '')}`
})
.uniq()
.value()
return icon === 'desktop' ? `fas fa-${icon}` : `fab fa-${icon}`
}
_getUniqOs () {
+9 -7
View File
@@ -215,12 +215,12 @@ class RunsList extends Component {
disabled={this.runsStore.isLoading}
onClick={this._getRuns}
>
<i aria-hidden="true" className={`fa fa-refresh ${this.runsStore.isLoading ? 'fa-spin' : ''}`}></i>
<i aria-hidden="true" className={`fas fa-sync-alt ${this.runsStore.isLoading ? 'fa-spin' : ''}`}></i>
</button>
</h5>
<div>
<a href="#" className='btn btn-sm see-all-runs' onClick={this._openRuns}>
See all runs <i className='fa fa-external-link'></i>
See all runs <i className='fas fa-external-link-alt'></i>
</a>
</div>
</header>
@@ -250,7 +250,7 @@ class RunsList extends Component {
_noApiServer () {
return (
<div className='empty empty-no-api-server'>
<h4><i className='fa fa-wifi'></i> Cannot connect to API server</h4>
<h4><i className='fas fa-wifi'></i> Cannot connect to API server</h4>
<p>Viewing runs requires connecting to an external API server.</p>
<p>We tried but failed to connect to the API server at <em>{this.state.apiUrl}</em></p>
<p>
@@ -258,7 +258,7 @@ class RunsList extends Component {
className='btn btn-default btn-sm'
onClick={this._pingApiServer}
>
<i className='fa fa-refresh'></i>{' '}
<i className='fas fa-sync-alt'></i>{' '}
Try again
</button>
</p>
@@ -290,6 +290,8 @@ class RunsList extends Component {
_projectNotSetup (isValid = true) {
return (
<ProjectNotSetup
isAuthenticated={authStore.isAuthenticated}
isShowingLogin={authStore.isShowingLogin}
project={this.props.project}
isValid={isValid}
onSetup={this._setProjectDetails}
@@ -330,7 +332,7 @@ class RunsList extends Component {
1. Check {configFileFormatted(this.props.project.configFile)} into source control.
</span>
<a onClick={this._openProjectIdGuide} className='pull-right'>
<i className='fa fa-question-circle'></i>{' '}
<i className='fas fa-question-circle'></i>{' '}
{' '}
Why?
</a>
@@ -345,7 +347,7 @@ class RunsList extends Component {
2. Run this command now, or in CI.
</span>
<a onClick={this._openCiGuide} className='pull-right'>
<i className='fa fa-question-circle'></i>{' '}
<i className='fas fa-question-circle'></i>{' '}
Need help?
</a>
</h5>
@@ -354,7 +356,7 @@ class RunsList extends Component {
</pre>
<hr />
<p className='alert alert-default'>
<i className='fa fa-info-circle'></i>{' '}
<i className='fas fa-info-circle'></i>{' '}
Recorded runs will show up{' '}
<a href='#' onClick={this._openRunGuide}>here</a>{' '}
and on your{' '}
@@ -70,7 +70,7 @@ class SetupProject extends Component {
render () {
if (!authStore.isAuthenticated) {
authStore.setShowingLogin(true)
authStore.openLogin()
return null
}
@@ -98,7 +98,7 @@ class SetupProject extends Component {
>
{
this.state.isSubmitting ?
<span><i className='fa fa-spin fa-refresh'></i>{' '}</span> :
<span><i className='fas fa-spin fa-sync-alt'></i>{' '}</span> :
null
}
<span>Set up project</span>
@@ -152,7 +152,7 @@ class SetupProject extends Component {
Who should own this project?
{' '}
<a onClick={this._openOrgDocs}>
<i className='fa fa-question-circle'></i>
<i className='fas fa-question-circle'></i>
</a>
</label>
</div>
@@ -189,7 +189,7 @@ class SetupProject extends Component {
checked={this.state.owner === 'org'}
onChange={this._updateOwner}
/>
<i className='fa fa-building-o'></i>
<i className='far fa-building'></i>
{' '}An Organization
</label>
</div>
@@ -204,7 +204,7 @@ class SetupProject extends Component {
href='#'
className={cs('btn btn-link', { 'hidden': this.state.owner !== 'org' })}
onClick={this._manageOrgs}>
<i className='fa fa-plus'></i>{' '}
<i className='fas fa-plus'></i>{' '}
Create organization
</a>
</p>
@@ -263,7 +263,7 @@ class SetupProject extends Component {
Who should see the runs and recordings?
{' '}
<a onClick={this._openAccessDocs}>
<i className='fa fa-question-circle'></i>
<i className='fas fa-question-circle'></i>
</a>
</label>
<div className='radio privacy-radio'>
@@ -276,7 +276,7 @@ class SetupProject extends Component {
onChange={this._updateAccess}
/>
<p>
<i className='fa fa-eye'></i>{' '}
<i className='far fa-eye'></i>{' '}
<strong>Public:</strong>{' '}
Anyone has access.
</p>
@@ -292,7 +292,7 @@ class SetupProject extends Component {
onChange={this._updateAccess}
/>
<p>
<i className='fa fa-lock'></i>{' '}
<i className='fas fa-lock'></i>{' '}
<strong>Private:</strong>{' '}
Only invited users have access.
</p>
@@ -1,115 +1,105 @@
import _ from 'lodash'
import cn from 'classnames'
import { observer } from 'mobx-react'
import React from 'react'
import Tooltip from '@cypress/react-tooltip'
import { ObjectInspector, ObjectName } from 'react-inspector'
import { configFileFormatted } from '../lib/config-file-formatted'
import ipc from '../lib/ipc'
const display = (obj) => {
const keys = _.keys(obj)
const lastKey = _.last(keys)
return _.map(obj, (value, key) => {
const hasComma = lastKey !== key
if (value.from == null) {
return displayNestedObj(key, value, hasComma)
}
if (value.isArray) {
return getSpan(key, value, hasComma, true)
}
if (_.isObject(value.value)) {
const realValue = value.value.toJS ? value.value.toJS() : value.value
if (_.isArray(realValue)) {
return displayArray(key, value, hasComma)
const formatData = (data) => {
if (Array.isArray(data)) {
return _.map(data, (v) => {
if (_.isObject(v) && (v.name || v.displayName)) {
return _.defaultTo(v.displayName, v.name)
}
return displayObject(key, value, hasComma)
}
return String(v)
}).join(', ')
}
return getSpan(key, value, hasComma)
})
if (_.isObject(data)) {
return _.defaultTo(_.defaultTo(data.displayName, data.name), String(Object.keys(data).join(', ')))
}
const excludedFromQuotations = ['null', 'undefined']
if (_.isString(data) && !excludedFromQuotations.includes(data)) {
return `"${data}"`
}
return String(data)
}
const ObjectLabel = ({ name, data, expanded, from, isNonenumerable }) => {
const formattedData = formatData(data)
const displayNestedObj = (key, value, hasComma) => (
<span key={key}>
<span className='nested nested-obj'>
<span className='key'>{key}</span>
<span className='colon'>:</span>{' '}
{'{'}
{display(value)}
</span>
<span className='line'>{'}'}{getComma(hasComma)}</span>
<br />
</span>
)
const displayNestedArr = (key, value, hasComma) => (
<span key={key}>
<span className='nested nested-arr'>
<span className='key'>{key}</span>
<span className='colon'>:</span>{' '}
{'['}
{display(value)}
</span>
<span className='line'>{']'}{getComma(hasComma)}</span>
<br />
</span>
)
const displayArray = (key, nestedArr, hasComma) => {
const arr = _.map(nestedArr.value, (value) => {
return { value, from: nestedArr.from, isArray: true }
})
return displayNestedArr(key, arr, hasComma)
}
const displayObject = (key, nestedObj, hasComma) => {
const obj = _.reduce(nestedObj.value, (obj, value, key) => {
return _.extend(obj, {
[key]: { value, from: nestedObj.from },
})
}, {})
return displayNestedObj(key, obj, hasComma)
}
const getSpan = (key, obj, hasComma, isArray) => {
return (
<div key={key} className='line'>
{getKey(key, isArray)}
{getColon(isArray)}
<Tooltip title={obj.from || ''} placement='right' className='cy-tooltip'>
<span className={obj.from}>
{getString(obj.value)}
{`${obj.value}`}
{getString(obj.value)}
</span>
</Tooltip>
{getComma(hasComma)}
</div>
<span className="line" key={name}>
<ObjectName name={name} dimmed={isNonenumerable} />
<span>:</span>
{!expanded && (
<>
<Tooltip title={from} placement='right' className='cy-tooltip'>
<span className={cn(from, 'key-value-pair-value')}>
<span>{formattedData}</span>
</span>
</Tooltip>
</>
)}
{expanded && Array.isArray(data) && (
<span> Array ({data.length})</span>
)}
</span>
)
}
const getKey = (key, isArray) => {
return isArray ? '' : <span className='key'>{key}</span>
ObjectLabel.defaultProps = {
data: 'undefined',
}
const getColon = (isArray) => {
return isArray ? '' : <span className="colon">:{' '}</span>
const createComputeFromValue = (obj) => {
return (name, path) => {
const pathParts = path.split('.')
const pathDepth = pathParts.length
const rootKey = pathDepth <= 2 ? name : pathParts[1]
return obj[rootKey] ? obj[rootKey].from : undefined
}
}
const getString = (val) => {
return _.isString(val) ? '\'' : ''
}
const ConfigDisplay = ({ data: obj }) => {
const computeFromValue = createComputeFromValue(obj)
const renderNode = ({ depth, name, data, isNonenumerable, expanded, path }) => {
if (depth === 0) {
return null
}
const getComma = (hasComma) => {
return hasComma ? <span className='comma'>,</span> : ''
const from = computeFromValue(name, path)
return (
<ObjectLabel
name={name}
data={data}
expanded={expanded}
from={from}
isNonenumerable={isNonenumerable}
/>
)
}
const data = _.reduce(obj, (acc, value, key) => Object.assign(acc, {
[key]: value.value,
}), {})
return (
<div className="config-vars">
<span>{'{'}</span>
<ObjectInspector data={data} expandLevel={1} nodeRenderer={renderNode} />
<span>{'}'}</span>
</div>
)
}
const openHelp = (e) => {
@@ -120,7 +110,7 @@ const openHelp = (e) => {
const Configuration = observer(({ project }) => (
<div>
<a href='#' className='learn-more' onClick={openHelp}>
<i className='fa fa-info-circle'></i> Learn more
<i className='fas fa-info-circle'></i> Learn more
</a>
<p className='text-muted'>Your project's configuration is displayed below. A value can be set from the following sources:</p>
<table className='table config-table'>
@@ -146,16 +136,12 @@ const Configuration = observer(({ project }) => (
<td>set from CLI arguments</td>
</tr>
<tr className='config-keys'>
<td><span className='plugin'>plugin</span></td>
<td><span className='plugins'>plugin</span></td>
<td>set from plugin file</td>
</tr>
</tbody>
</table>
<pre className='config-vars'>
{'{'}
{display(project.resolvedConfig)}
{'}'}
</pre>
<ConfigDisplay data={project.resolvedConfig} />
</div>
))
@@ -12,7 +12,7 @@ const openHelp = (e) => {
const renderLearnMore = () => {
return (
<a href='#' className='learn-more' onClick={openHelp}>
<i className='fa fa-info-circle'></i> Learn more
<i className='fas fa-info-circle'></i> Learn more
</a>
)
}
@@ -15,7 +15,7 @@ const ProjectId = observer(({ project }) => {
return (
<div>
<a href='#' className='learn-more' onClick={openProjectIdHelp}>
<i className='fa fa-info-circle'></i>{' '}
<i className='fas fa-info-circle'></i>{' '}
Learn more
</a>
<p className='text-muted'>This projectId should be in your {configFileFormatted(project.configFile)} and checked into source control.
@@ -24,7 +24,7 @@ const openHelp = (e) => {
const renderLearnMore = () => {
return (
<a href='#' className='learn-more' onClick={openHelp}>
<i className='fa fa-info-circle'></i> Learn more
<i className='fas fa-info-circle'></i> Learn more
</a>
)
}
@@ -64,7 +64,7 @@ const ProxySettings = observer(({ app }) => {
<Tooltip className='cy-tooltip'
title='Cypress will not route requests to these domains through the configured proxy server.'
>
<i className='fa fa-info-circle' />
<i className='fas fa-info-circle' />
</Tooltip>
</th>
<td>
@@ -22,7 +22,7 @@ const openRecordKeyGuide = (e) => {
}
const showLogin = () => {
authStore.setShowingLogin(true)
authStore.openLogin()
}
@observer
@@ -74,7 +74,7 @@ class RecordKey extends Component {
return (
<div>
<a href='#' className='learn-more' onClick={openRecordKeyGuide}>
<i className='fa fa-info-circle'></i>{' '}
<i className='fas fa-info-circle'></i>{' '}
Learn More
</a>
<p className='text-muted'>
@@ -97,7 +97,7 @@ class RecordKey extends Component {
className='btn btn-primary'
onClick={showLogin}
>
<i className='fa fa-user'></i>{' '}
<i className='fas fa-user'></i>{' '}
Log In
</button>
</p>
@@ -107,7 +107,7 @@ class RecordKey extends Component {
if (this.isLoading) {
return (
<p className='loading-record-keys'>
<i className='fa fa-spinner fa-spin'></i>{' '}
<i className='fas fa-spinner fa-spin'></i>{' '}
Loading Keys...
</p>
)
@@ -131,7 +131,7 @@ class RecordKey extends Component {
</p>
<p className='text-muted manage-btn'>
<a href='#' onClick={openDashboardProjectSettings(this.props.project)}>
<i className='fa fa-key'></i> You can change this key in the Dashboard
<i className='fas fa-key'></i> You can change this key in the Dashboard
</a>
</p>
</div>
+22 -27
View File
@@ -68,45 +68,40 @@
font-size: 13px;
border: 1px solid #ddd;
border-radius: 3px;
background-color: #252831;
background: #fff;
color: #eee;
margin: 0;
padding: 10px;
padding: 8px 12px;
font-family: $font-mono;
> span {
color: #000;
}
.envFile:hover, .env:hover, .config:hover, .cli:hover, .plugin:hover, .default:hover {
ol[role="tree"] {
> li > div{
display: none;
}
}
.key-value-pair-value {
margin-left: 2px;
display: inline-block;
line-height: 1;
padding: 2px;
border-bottom: 1px solid transparent
}
.key-value-pair-value:hover {
border-bottom: 1px dotted #777;
}
p {
margin-bottom: 0;
}
.key {
color: #ccc;
}
.comma, .colon, {
color: #ccc;
}
.line {
margin-left: 15px;
color: #eee;
}
.nested {
margin-left: 15px;
.line {
margin-left: 30px;
}
}
}
.envFile, .env, .config, .cli, .plugin, .default {
.envFile, .env, .config, .cli, .plugins, .default {
font-family: $font-mono;
padding: 2px;
}
@@ -130,7 +125,7 @@
color: #A21313;
}
.plugin {
.plugins {
background-color: #f0e7fc;
color: #134aa2;
}
+10 -10
View File
@@ -28,7 +28,7 @@ class SpecsList extends Component {
'show-clear-filter': !!specsStore.filter,
})}>
<label htmlFor='filter'>
<i className='fa fa-search'></i>
<i className='fas fa-search'></i>
</label>
<input
id='filter'
@@ -43,11 +43,11 @@ class SpecsList extends Component {
title='Clear search'
className='browser-info-tooltip cy-tooltip'
>
<a className='clear-filter fa fa-times' onClick={this._clearFilter} />
<a className='clear-filter fas fa-times' onClick={this._clearFilter} />
</Tooltip>
</div>
<a onClick={this._selectSpec.bind(this, allSpecsSpec)} className={cs('all-tests btn btn-default', { active: specsStore.isChosen(allSpecsSpec) })}>
<i className={`fa fa-fw ${this._allSpecsIcon(specsStore.isChosen(allSpecsSpec))}`}></i>{' '}
<i className={`fa-fw ${this._allSpecsIcon(specsStore.isChosen(allSpecsSpec))}`}></i>{' '}
{allSpecsSpec.displayName}
</a>
</header>
@@ -66,7 +66,7 @@ class SpecsList extends Component {
this._clearFilter()
this.filterRef.current.focus()
}} className='btn btn-link'>
<i className='fa fa-times'/> Clear search
<i className='fas fa-times'/> Clear search
</a>
</div>
)
@@ -84,11 +84,11 @@ class SpecsList extends Component {
}
_allSpecsIcon (allSpecsChosen) {
return allSpecsChosen ? 'fa-dot-circle-o green' : 'fa-play'
return allSpecsChosen ? 'far fa-dot-circle green' : 'fas fa-play'
}
_specIcon (isChosen) {
return isChosen ? 'fa-dot-circle-o green' : 'fa-file-code-o'
return isChosen ? 'far fa-dot-circle green' : 'far fa-file'
}
_clearFilter = () => {
@@ -130,8 +130,8 @@ class SpecsList extends Component {
<li key={spec.path} className={`folder level-${nestingLevel} ${isExpanded ? 'folder-expanded' : 'folder-collapsed'}`}>
<div>
<div className="folder-name" onClick={this._selectSpecFolder.bind(this, spec)}>
<i className={`folder-collapse-icon fa fa-fw ${isExpanded ? 'fa-caret-down' : 'fa-caret-right'}`}></i>
<i className={`fa fa-fw ${isExpanded ? 'fa-folder-open-o' : 'fa-folder-o'}`}></i>
<i className={`folder-collapse-icon fas fa-fw ${isExpanded ? 'fa-caret-down' : 'fa-caret-right'}`}></i>
<i className={`far fa-fw ${isExpanded ? 'fa-folder-open' : 'fa-folder'}`}></i>
{nestingLevel === 0 ? `${spec.displayName} tests` : spec.displayName}
</div>
{
@@ -154,7 +154,7 @@ class SpecsList extends Component {
<a href='#' onClick={this._selectSpec.bind(this, spec)} className={cs({ active: specsStore.isChosen(spec) })}>
<div>
<div className="file-name">
<i className={`fa fa-fw ${this._specIcon(specsStore.isChosen(spec))}`}></i>
<i className={`fa-fw ${this._specIcon(specsStore.isChosen(spec))}`}></i>
{spec.displayName}
</div>
</div>
@@ -177,7 +177,7 @@ class SpecsList extends Component {
</code>
</h5>
<a className='helper-docs-link' onClick={this._openHelp}>
<i className='fa fa-question-circle'></i>{' '}
<i className='fas fa-question-circle'></i>{' '}
Need help?
</a>
</div>
+2 -2
View File
@@ -95,7 +95,7 @@ $max-nesting-level: 14;
}
i {
font-size: 10px;
font-size: 8px;
position: relative;
top: -1px;
}
@@ -118,7 +118,7 @@ $max-nesting-level: 14;
align-items: center;
i {
font-size: 13px;
font-size: 12px;
margin-right: 5px;
flex-shrink: 0;
}
+4 -1
View File
@@ -35,7 +35,10 @@
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/utilities";
// Fonts
@import "../../node_modules/font-awesome/scss/font-awesome";
@import "../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss";
@import "../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss";
@import "../../node_modules/@fortawesome/fontawesome-free/scss/brands.scss";
@import "../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss";
@import "../../node_modules/fira/fira";
// Tooltip
@@ -36,7 +36,7 @@ class UpdateBanner extends Component {
<div className='updates-available'>
New updates are available
<strong onClick={() => this._toggleModal(true)}>
<i className='fa fa-download'></i>{' '}
<i className='fas fa-download'></i>{' '}
Update
</strong>
<BootstrapModal
@@ -46,7 +46,7 @@ class UpdateBanner extends Component {
>
<div className='update-modal modal-body os-dialog'>
<BootstrapModal.Dismiss className='btn btn-link close'>x</BootstrapModal.Dismiss>
<h4><i className='fa fa-download'></i> Update Available</h4>
<h4><i className='fas fa-download'></i> Update Available</h4>
<p>
<a href='#' onClick={this._openChangelog}><strong>Version {appStore.newVersion}</strong></a> is now available (currently running <strong>Version {appStore.displayVersion}</strong>)
</p>
@@ -65,7 +65,7 @@ class UpdateBanner extends Component {
<ol>
<li>
<span>
<a href='#' onClick={this._openDownload}><i className='fa fa-download'></i> Download the new version.</a>
<a href='#' onClick={this._openDownload}><i className='fas fa-download'></i> Download the new version.</a>
</span>
</li>
<li>
+1 -1
View File
@@ -4,7 +4,7 @@ import path from 'path'
const config: typeof commonConfig = {
...commonConfig,
entry: {
app: [path.resolve(__dirname, 'src/main')],
app: [require.resolve('@babel/polyfill'), path.resolve(__dirname, 'src/main')],
},
output: {
path: path.resolve(__dirname, 'dist'),
+2
View File
@@ -0,0 +1,2 @@
test/cypress/videos
test/cypress/screenshots
+2 -2
View File
@@ -16,7 +16,7 @@
"@cypress/sinon-chai": "1.1.0",
"@cypress/underscore.inflection": "1.0.1",
"@cypress/unique-selector": "0.4.2",
"@cypress/webpack-preprocessor": "4.1.0",
"@cypress/webpack-preprocessor": "4.1.1",
"@cypress/what-is-circular": "1.0.1",
"angular": "1.7.7",
"backbone": "1.4.0",
@@ -52,7 +52,7 @@
"moment": "2.24.0",
"morgan": "1.9.1",
"npm-install-version": "6.0.2",
"parse-domain": "2.0.0",
"parse-domain": "2.3.4",
"setimmediate": "1.0.5",
"sinon": "7.5.0",
"strict-cookie-parser": "3.1.0",
-338
View File
@@ -1,338 +0,0 @@
## tests in driver/test/cypress/integration/commands/assertions_spec.coffee
_ = require("lodash")
$ = require("jquery")
chai = require("chai")
sinonChai = require("@cypress/sinon-chai")
$dom = require("../dom")
$utils = require("../cypress/utils")
$chaiJquery = require("../cypress/chai_jquery")
## all words between single quotes which are at
## the end of the string
allPropertyWordsBetweenSingleQuotes = /('.*?')$/g
## grab all words between single quotes except
## when the single quote word is the LAST word
allButLastWordsBetweenSingleQuotes = /('.*?')(.+)/g
allBetweenFourStars = /\*\*.*\*\*/
allSingleQuotes = /'/g
allEscapedSingleQuotes = /\\'/g
allQuoteMarkers = /__quote__/g
allWordsBetweenCurlyBraces = /(#{.+?})/g
allQuadStars = /\*\*\*\*/g
assertProto = null
matchProto = null
lengthProto = null
containProto = null
existProto = null
getMessage = null
chaiUtils = null
chai.use(sinonChai)
chai.use (chai, u) ->
chaiUtils = u
$chaiJquery(chai, chaiUtils, {
onInvalid: (method, obj) ->
err = $utils.cypressErr(
$utils.errMessageByPath(
"chai.invalid_jquery_obj", {
assertion: method
subject: $utils.stringifyActual(obj)
}
)
)
throw err
onError: (err, method, obj, negated) ->
switch method
when "visible"
if not negated
## add reason hidden unless we expect the element to be hidden
reason = $dom.getReasonIsHidden(obj)
err.message += "\n\n" + reason
## always rethrow the error!
throw err
})
assertProto = chai.Assertion::assert
matchProto = chai.Assertion::match
lengthProto = chai.Assertion::__methods.length.method
containProto = chai.Assertion::__methods.contain.method
existProto = Object.getOwnPropertyDescriptor(chai.Assertion::, "exist").get
getMessage = chaiUtils.getMessage
removeOrKeepSingleQuotesBetweenStars = (message) ->
## remove any single quotes between our **, preserving escaped quotes
## and if an empty string, put the quotes back
message.replace allBetweenFourStars, (match) ->
match
.replace(allEscapedSingleQuotes, "__quote__") # preserve escaped quotes
.replace(allSingleQuotes, "")
.replace(allQuoteMarkers, "'") ## put escaped quotes back
.replace(allQuadStars, "**''**") ## fix empty strings that end up as ****
replaceArgMessages = (args, str) ->
_.reduce args, (memo, value, index) =>
if _.isString(value)
value = value
.replace(allWordsBetweenCurlyBraces, "**$1**")
.replace(allEscapedSingleQuotes, "__quote__")
.replace(allButLastWordsBetweenSingleQuotes, "**$1**$2")
.replace(allPropertyWordsBetweenSingleQuotes, "**$1**")
memo.push value
else
memo.push value
memo
, []
restoreAsserts = ->
chaiUtils.getMessage = getMessage
chai.Assertion::assert = assertProto
chai.Assertion::match = matchProto
chai.Assertion::__methods.length.method = lengthProto
chai.Assertion::__methods.contain.method = containProto
Object.defineProperty(chai.Assertion::, "exist", {get: existProto})
overrideChaiAsserts = (assertFn) ->
_this = @
chai.Assertion.prototype.assert = createPatchedAssert(assertFn)
chaiUtils.getMessage = (assert, args) ->
obj = assert._obj
## if we are formatting a DOM object
if $dom.isDom(obj)
## replace object with our formatted one
assert._obj = $dom.stringify(obj, "short")
msg = getMessage.call(@, assert, args)
## restore the real obj if we changed it
if obj isnt assert._obj
assert._obj = obj
return msg
chai.Assertion.overwriteMethod "match", (_super) ->
return (regExp) ->
if _.isRegExp(regExp) or $dom.isDom(@_obj)
_super.apply(@, arguments)
else
err = $utils.cypressErr($utils.errMessageByPath("chai.match_invalid_argument", { regExp }))
err.retry = false
throw err
containFn1 = (_super) ->
return (text) ->
obj = @_obj
if not ($dom.isJquery(obj) or $dom.isElement(obj))
return _super.apply(@, arguments)
escText = $utils.escapeQuotes(text)
selector = ":contains('#{escText}'), [type='submit'][value~='#{escText}']"
## the assert checks below only work if $dom.isJquery(obj)
## https://github.com/cypress-io/cypress/issues/3549
if not ($dom.isJquery(obj))
obj = $(obj)
@assert(
obj.is(selector) or !!obj.find(selector).length
'expected #{this} to contain #{exp}'
'expected #{this} not to contain #{exp}'
text
)
containFn2 = (_super) ->
return ->
_super.apply(@, arguments)
chai.Assertion.overwriteChainableMethod("contain", containFn1, containFn2)
chai.Assertion.overwriteChainableMethod "length",
fn1 = (_super) ->
return (length) ->
obj = @_obj
if not ($dom.isJquery(obj) or $dom.isElement(obj))
return _super.apply(@, arguments)
length = $utils.normalizeNumber(length)
## filter out anything not currently in our document
if $dom.isDetached(obj)
obj = @_obj = obj.filter (index, el) ->
$dom.isAttached(el)
node = if obj and obj.length then $dom.stringify(obj, "short") else obj.selector
## if our length assertion fails we need to check to
## ensure that the length argument is a finite number
## because if its not, we need to bail on retrying
try
@assert(
obj.length is length,
"expected '#{node}' to have a length of \#{exp} but got \#{act}",
"expected '#{node}' to not have a length of \#{act}",
length,
obj.length
)
catch e1
e1.node = node
e1.negated = chaiUtils.flag(@, "negate")
e1.type = "length"
if _.isFinite(length)
getLongLengthMessage = (len1, len2) ->
if len1 > len2
"Too many elements found. Found '#{len1}', expected '#{len2}'."
else
"Not enough elements found. Found '#{len1}', expected '#{len2}'."
e1.displayMessage = getLongLengthMessage(obj.length, length)
throw e1
e2 = $utils.cypressErr($utils.errMessageByPath("chai.length_invalid_argument", { length }))
e2.retry = false
throw e2
fn2 = (_super) ->
return ->
_super.apply(@, arguments)
chai.Assertion.overwriteProperty "exist", (_super) ->
return ->
obj = @_obj
if not ($dom.isJquery(obj) or $dom.isElement(obj))
try
_super.apply(@, arguments)
catch e
e.type = "existence"
throw e
else
if not obj.length
@_obj = null
node = if obj and obj.length then $dom.stringify(obj, "short") else obj.selector
try
@assert(
isAttached = $dom.isAttached(obj),
"expected \#{act} to exist in the DOM",
"expected \#{act} not to exist in the DOM",
node,
node
)
catch e1
e1.node = node
e1.negated = chaiUtils.flag(@, "negate")
e1.type = "existence"
getLongExistsMessage = (obj) ->
## if we expected not for an element to exist
if isAttached
"Expected #{node} not to exist in the DOM, but it was continuously found."
else
"Expected to find element: '#{obj.selector}', but never found it."
e1.displayMessage = getLongExistsMessage(obj)
throw e1
createPatchedAssert = (assertFn) ->
return (args...) ->
passed = chaiUtils.test(@, args)
value = chaiUtils.flag(@, "object")
expected = args[3]
customArgs = replaceArgMessages(args, @_obj)
message = chaiUtils.getMessage(@, customArgs)
actual = chaiUtils.getActual(@, customArgs)
message = removeOrKeepSingleQuotesBetweenStars(message)
try
assertProto.apply(@, args)
catch e
err = e
assertFn(passed, message, value, actual, expected, err)
throw err if err
overrideExpect = ->
## only override assertions for this specific
## expect function instance so we do not affect
## the outside world
return (val, message) ->
## make the assertion
return new chai.Assertion(val, message)
overrideAssert = ->
fn = (express, errmsg) ->
chai.assert(express, errmsg)
fns = _.functions(chai.assert)
_.each fns, (name) ->
fn[name] = ->
chai.assert[name].apply(@, arguments)
return fn
setSpecWindowGlobals = (specWindow, assertFn) ->
expect = overrideExpect()
assert = overrideAssert()
specWindow.chai = chai
specWindow.expect = expect
specWindow.assert = assert
return {
chai
expect
assert
}
create = (specWindow, assertFn) ->
# restoreOverrides()
restoreAsserts()
# overrideChai()
overrideChaiAsserts(assertFn)
return setSpecWindowGlobals(specWindow)
module.exports = {
replaceArgMessages
removeOrKeepSingleQuotesBetweenStars
setSpecWindowGlobals
# overrideChai: overrideChai
restoreAsserts
overrideExpect
overrideChaiAsserts
create
}
+418
View File
@@ -0,0 +1,418 @@
// tests in driver/test/cypress/integration/commands/assertions_spec.coffee
const _ = require('lodash')
const $ = require('jquery')
const chai = require('chai')
const sinonChai = require('@cypress/sinon-chai')
const $dom = require('../dom')
const $utils = require('../cypress/utils')
const $chaiJquery = require('../cypress/chai_jquery')
// all words between single quotes
const allPropertyWordsBetweenSingleQuotes = /('.*?')/g
const allBetweenFourStars = /\*\*.*\*\*/
const allNumberStrings = /'([0-9]+)'/g
const allEmptyStrings = /''/g
const allSingleQuotes = /'/g
const allEscapedSingleQuotes = /\\'/g
const allQuoteMarkers = /__quote__/g
const allWordsBetweenCurlyBraces = /(#{.+?})/g
const allQuadStars = /\*\*\*\*/g
const leadingWhitespaces = /\*\*'\s*/g
const trailingWhitespaces = /\s*'\*\*/g
const whitespace = /\s/g
const valueHasLeadingOrTrailingWhitespaces = /\*\*'\s+|\s+'\*\*/g
let assertProto = null
let matchProto = null
let lengthProto = null
let containProto = null
let existProto = null
let getMessage = null
let chaiUtils = null
chai.use(sinonChai)
chai.use((chai, u) => {
chaiUtils = u
$chaiJquery(chai, chaiUtils, {
onInvalid (method, obj) {
const err = $utils.cypressErr(
$utils.errMessageByPath(
'chai.invalid_jquery_obj', {
assertion: method,
subject: $utils.stringifyActual(obj),
}
)
)
throw err
},
onError (err, method, obj, negated) {
switch (method) {
case 'visible':
if (!negated) {
// add reason hidden unless we expect the element to be hidden
const reason = $dom.getReasonIsHidden(obj)
err.message += `\n\n${reason}`
}
break
default:
break
}
// always rethrow the error!
throw err
},
})
assertProto = chai.Assertion.prototype.assert
matchProto = chai.Assertion.prototype.match
lengthProto = chai.Assertion.prototype.__methods.length.method
containProto = chai.Assertion.prototype.__methods.contain.method
existProto = Object.getOwnPropertyDescriptor(chai.Assertion.prototype, 'exist').get;
({ getMessage } = chaiUtils)
// remove any single quotes between our **,
// except escaped quotes, empty strings and number strings.
const removeOrKeepSingleQuotesBetweenStars = (message) => {
return message.replace(allBetweenFourStars, (match) => {
if (valueHasLeadingOrTrailingWhitespaces.test(match)) {
// Above we used \s+, but below we use \s*.
// It's because of the strings like ' love' that have empty spaces on one side only.
match = match
.replace(leadingWhitespaces, (match) => {
return match.replace(`**'`, '**__quote__')
.replace(whitespace, '&nbsp;')
})
.replace(trailingWhitespaces, (match) => {
return match.replace(`'**`, '__quote__**')
.replace(whitespace, '&nbsp;')
})
}
return match
.replace(allEscapedSingleQuotes, '__quote__') // preserve escaped quotes
.replace(allNumberStrings, '__quote__$1__quote__') // preserve number strings (e.g. '42')
.replace(allEmptyStrings, '__quote____quote__') // preserve empty strings (e.g. '')
.replace(allSingleQuotes, '')
.replace(allQuoteMarkers, '\'') // put escaped quotes back
})
}
const replaceArgMessages = (args, str) => {
return _.reduce(args, (memo, value, index) => {
if (_.isString(value)) {
value = value
.replace(allWordsBetweenCurlyBraces, '**$1**')
.replace(allEscapedSingleQuotes, '__quote__')
.replace(allPropertyWordsBetweenSingleQuotes, '**$1**')
// when a value has ** in it, **** are sometimes created after the process above.
// remove them with this.
.replace(allQuadStars, '**')
memo.push(value)
} else {
memo.push(value)
}
return memo
}, [])
}
const restoreAsserts = function () {
chaiUtils.getMessage = getMessage
chai.Assertion.prototype.assert = assertProto
chai.Assertion.prototype.match = matchProto
chai.Assertion.prototype.__methods.length.method = lengthProto
chai.Assertion.prototype.__methods.contain.method = containProto
return Object.defineProperty(chai.Assertion.prototype, 'exist', { get: existProto })
}
const overrideChaiAsserts = function (assertFn) {
chai.Assertion.prototype.assert = createPatchedAssert(assertFn)
chaiUtils.getMessage = function (assert, args) {
const obj = assert._obj
// if we are formatting a DOM object
if ($dom.isDom(obj)) {
// replace object with our formatted one
assert._obj = $dom.stringify(obj, 'short')
}
const msg = getMessage.call(this, assert, args)
// restore the real obj if we changed it
if (obj !== assert._obj) {
assert._obj = obj
}
return msg
}
chai.Assertion.overwriteMethod('match', (_super) => {
return (function (regExp, ...args) {
if (_.isRegExp(regExp) || $dom.isDom(this._obj)) {
return _super.apply(this, [regExp, ...args])
}
const err = $utils.cypressErr($utils.errMessageByPath('chai.match_invalid_argument', { regExp }))
err.retry = false
throw err
})
})
const containFn1 = (_super) => {
return (function (text, ...args) {
let obj = this._obj
if (!($dom.isJquery(obj) || $dom.isElement(obj))) {
return _super.apply(this, [text, ...args])
}
const escText = $utils.escapeQuotes(text)
const selector = `:contains('${escText}'), [type='submit'][value~='${escText}']`
// the assert checks below only work if $dom.isJquery(obj)
// https://github.com/cypress-io/cypress/issues/3549
if (!($dom.isJquery(obj))) {
obj = $(obj)
}
this.assert(
obj.is(selector) || !!obj.find(selector).length,
'expected #{this} to contain #{exp}',
'expected #{this} not to contain #{exp}',
text
)
})
}
const containFn2 = (_super) => {
return (function (...args) {
_super.apply(this, args)
})
}
chai.Assertion.overwriteChainableMethod('contain', containFn1, containFn2)
chai.Assertion.overwriteChainableMethod('length',
(_super) => {
return (function (length, ...args) {
let obj = this._obj
if (!($dom.isJquery(obj) || $dom.isElement(obj))) {
return _super.apply(this, [length, ...args])
}
length = $utils.normalizeNumber(length)
// filter out anything not currently in our document
if ($dom.isDetached(obj)) {
obj = (this._obj = obj.filter((index, el) => {
return $dom.isAttached(el)
}))
}
const node = obj && obj.length ? $dom.stringify(obj, 'short') : obj.selector
// if our length assertion fails we need to check to
// ensure that the length argument is a finite number
// because if its not, we need to bail on retrying
try {
return this.assert(
obj.length === length,
`expected '${node}' to have a length of \#{exp} but got \#{act}`,
`expected '${node}' to not have a length of \#{act}`,
length,
obj.length
)
} catch (e1) {
e1.node = node
e1.negated = chaiUtils.flag(this, 'negate')
e1.type = 'length'
if (_.isFinite(length)) {
const getLongLengthMessage = function (len1, len2) {
if (len1 > len2) {
return `Too many elements found. Found '${len1}', expected '${len2}'.`
}
return `Not enough elements found. Found '${len1}', expected '${len2}'.`
}
e1.displayMessage = getLongLengthMessage(obj.length, length)
throw e1
}
const e2 = $utils.cypressErr($utils.errMessageByPath('chai.length_invalid_argument', { length }))
e2.retry = false
throw e2
}
})
},
(_super) => {
return (function (...args) {
return _super.apply(this, args)
})
})
return chai.Assertion.overwriteProperty('exist', (_super) => {
return (function (...args) {
const obj = this._obj
if (!($dom.isJquery(obj) || $dom.isElement(obj))) {
try {
return _super.apply(this, args)
} catch (e) {
e.type = 'existence'
throw e
}
} else {
let isAttached
if (!obj.length) {
this._obj = null
}
const node = obj && obj.length ? $dom.stringify(obj, 'short') : obj.selector
try {
return this.assert(
(isAttached = $dom.isAttached(obj)),
'expected \#{act} to exist in the DOM',
'expected \#{act} not to exist in the DOM',
node,
node
)
} catch (e1) {
e1.node = node
e1.negated = chaiUtils.flag(this, 'negate')
e1.type = 'existence'
const getLongExistsMessage = function (obj) {
// if we expected not for an element to exist
if (isAttached) {
return `Expected ${node} not to exist in the DOM, but it was continuously found.`
}
return `Expected to find element: '${obj.selector}', but never found it.`
}
e1.displayMessage = getLongExistsMessage(obj)
throw e1
}
}
})
})
}
const createPatchedAssert = (assertFn) => {
return (function (...args) {
let err
const passed = chaiUtils.test(this, args)
const value = chaiUtils.flag(this, 'object')
const expected = args[3]
const customArgs = replaceArgMessages(args, this._obj)
let message = chaiUtils.getMessage(this, customArgs)
const actual = chaiUtils.getActual(this, customArgs)
message = removeOrKeepSingleQuotesBetweenStars(message)
try {
assertProto.apply(this, args)
} catch (e) {
err = e
}
assertFn(passed, message, value, actual, expected, err)
if (err) {
throw err
}
})
}
// only override assertions for this specific
// expect function instance so we do not affect
// the outside world
const overrideExpect = () => {
// make the assertion
return (val, message) => {
return new chai.Assertion(val, message)
}
}
const overrideAssert = function () {
const fn = (express, errmsg) => {
return chai.assert(express, errmsg)
}
const fns = _.functions(chai.assert)
_.each(fns, (name) => {
return fn[name] = function (...args) {
return chai.assert[name].apply(this, args)
}
})
return fn
}
const setSpecWindowGlobals = function (specWindow, assertFn) {
const expect = overrideExpect()
const assert = overrideAssert()
specWindow.chai = chai
specWindow.expect = expect
specWindow.assert = assert
return {
chai,
expect,
assert,
}
}
const create = function (specWindow, assertFn) {
// restoreOverrides()
restoreAsserts()
// overrideChai()
overrideChaiAsserts(assertFn)
return setSpecWindowGlobals(specWindow)
}
module.exports = {
replaceArgMessages,
removeOrKeepSingleQuotesBetweenStars,
setSpecWindowGlobals,
// overrideChai: overrideChai
restoreAsserts,
overrideExpect,
overrideChaiAsserts,
create,
}
})
@@ -5,58 +5,29 @@ const $dom = require('../../../dom')
const $utils = require('../../../cypress/utils')
const $actionability = require('../../actionability')
const formatMoveEventsTable = (events) => {
return {
name: `Mouse Move Events${events ? '' : ' (skipped)'}`,
data: _.map(events, (obj) => {
const key = _.keys(obj)[0]
const val = obj[_.keys(obj)[0]]
if (val.skipped) {
const reason = val.skipped
// no modifiers can be present
// on move events
return {
'Event Name': key,
'Target Element': reason,
'Prevented Default?': null,
'Stopped Propagation?': null,
}
}
// no modifiers can be present
// on move events
return {
'Event Name': key,
'Target Element': val.el,
'Prevented Default?': val.preventedDefault,
'Stopped Propagation?': val.stoppedPropagation,
}
}),
}
}
const formatMouseEvents = (events) => {
return _.map(events, (val, key) => {
// get event type either from the keyname, or from the sole object key name
const eventName = (typeof key === 'string') ? key : val.type
if (val.skipped) {
const reason = val.skipped
return {
'Event Name': key.slice(0, -5),
'Event Type': eventName,
'Target Element': reason,
'Prevented Default?': null,
'Stopped Propagation?': null,
'Modifiers': null,
'Prevented Default': null,
'Stopped Propagation': null,
'Active Modifiers': null,
}
}
return {
'Event Name': key.slice(0, -5),
'Event Type': eventName,
'Target Element': val.el,
'Prevented Default?': val.preventedDefault,
'Stopped Propagation?': val.stoppedPropagation,
'Modifiers': val.modifiers ? val.modifiers : null,
'Prevented Default': val.preventedDefault || null,
'Stopped Propagation': val.stoppedPropagation || null,
'Active Modifiers': val.modifiers || null,
}
})
}
@@ -243,12 +214,12 @@ module.exports = (Commands, Cypress, cy, state, config) => {
onTable (domEvents) {
return {
1: () => {
return formatMoveEventsTable(domEvents.moveEvents.events)
},
2: () => {
return {
name: 'Mouse Click Events',
data: formatMouseEvents(domEvents.clickEvents),
name: 'Mouse Events',
data: _.concat(
formatMouseEvents(domEvents.moveEvents.events),
formatMouseEvents(domEvents.clickEvents),
),
}
},
}
@@ -265,35 +236,28 @@ module.exports = (Commands, Cypress, cy, state, config) => {
defaultOptions: { multiple: true },
positionOrX,
onReady (fromElViewport, forceEl) {
const { clickEvents1, clickEvents2, dblclickProps } = mouse.dblclick(fromElViewport, forceEl)
const { clickEvents1, clickEvents2, dblclick } = mouse.dblclick(fromElViewport, forceEl)
return {
dblclickProps,
dblclick,
clickEvents: [clickEvents1, clickEvents2],
}
},
onTable (domEvents) {
return {
1: () => {
return formatMoveEventsTable(domEvents.moveEvents.events)
},
2: () => {
return {
name: 'Mouse Click Events',
name: 'Mouse Events',
data: _.concat(
formatMouseEvents(domEvents.clickEvents[0], formatMouseEvents),
formatMouseEvents(domEvents.clickEvents[1], formatMouseEvents)
formatMouseEvents(domEvents.moveEvents.events),
formatMouseEvents(domEvents.clickEvents[0]),
formatMouseEvents(domEvents.clickEvents[1]),
formatMouseEvents({
dblclick: domEvents.dblclick,
})
),
}
},
3: () => {
return {
name: 'Mouse Double Click Event',
data: formatMouseEvents({
dblclickProps: domEvents.dblclickProps,
}),
}
},
}
},
})
@@ -316,20 +280,16 @@ module.exports = (Commands, Cypress, cy, state, config) => {
onTable (domEvents) {
return {
1: () => {
return formatMoveEventsTable(domEvents.moveEvents.events)
},
2: () => {
return {
name: 'Mouse Click Events',
data: formatMouseEvents(domEvents.clickEvents, formatMouseEvents),
}
},
3: () => {
return {
name: 'Mouse Right Click Event',
data: formatMouseEvents(domEvents.contextmenuEvent),
name: 'Mouse Events',
data: _.concat(
formatMouseEvents(domEvents.moveEvents.events),
formatMouseEvents(domEvents.clickEvents),
formatMouseEvents(domEvents.contextmenuEvent)
),
}
},
}
},
})
+23 -22
View File
@@ -36,33 +36,39 @@ module.exports = function (Commands, Cypress, cy, state, config) {
const table = {}
const getRow = (id, key, which) => {
const getRow = (id, key, event) => {
if (table[id]) {
return table[id]
}
const obj = table[id] = {}
const modifiers = $Keyboard.modifiersToString(keyboard.getActiveModifiers(state))
if (modifiers) {
obj.modifiers = modifiers
const formatEventDetails = (obj) => {
return `{ ${(Object.keys(obj)
.filter((v) => Boolean(obj[v]))
.map((v) => `${v}: ${obj[v]}`))
.join(', ')
} }`
}
if (key) {
obj.typed = key
if (which) {
obj.which = which
}
const obj = table[id] = {
'Typed': key || null,
'Target Element': event.target,
'Events Fired': '',
'Details': formatEventDetails({ code: event.code, which: event.which }),
'Prevented Default': null,
'Active Modifiers': modifiers || null,
}
return obj
}
updateTable = function (id, key, column, which, value) {
const row = getRow(id, key, which)
updateTable = function (id, key, event, value) {
const row = getRow(id, key, event)
row[column] = value || 'preventedDefault'
row['Events Fired'] += row['Events Fired'] ? `, ${event.type}` : event.type
if (!value) {
row['Prevented Default'] = true
}
}
// transform table object into object with zero based index as keys
@@ -84,13 +90,12 @@ module.exports = function (Commands, Cypress, cy, state, config) {
'Applied To': $dom.getElements(options.$el),
'Options': deltaOptions,
'table': {
// mouse events tables will take up slots 1 and 2 if they're present
// mouse events tables will take up slot 1 if they're present
// this preserves the order of the tables
3: () => {
2: () => {
return {
name: 'Keyboard Events',
data: getTableData(),
columns: ['typed', 'which', 'keydown', 'keypress', 'textInput', 'input', 'keyup', 'change', 'modifiers'],
}
},
},
@@ -271,11 +276,7 @@ module.exports = function (Commands, Cypress, cy, state, config) {
return cy.timeout(totalKeys * options.delay, true, 'type')
},
onEvent (...args) {
if (updateTable) {
return updateTable(...args)
}
},
onEvent: updateTable || _.noop,
// fires only when the 'value'
// of input/text/contenteditable
@@ -138,8 +138,26 @@ module.exports = (Commands, Cypress, cy, state, config) ->
}
.finally(cleanup)
invokeFn = (subject, str, args...) ->
options = {}
invokeItsFn = (subject, str, options, args...) ->
return invokeBaseFn(options or { log: true }, subject, str, args...)
invokeFn = (subject, optionsOrStr, args...) ->
optionsPassed = _.isObject(optionsOrStr) and !_.isFunction(optionsOrStr)
options = null
str = null
if not optionsPassed
str = optionsOrStr
options = { log: true }
else
options = optionsOrStr
if args.length > 0
str = args[0]
args = args.slice(1)
return invokeBaseFn(options, subject, str, args...)
invokeBaseFn = (options, subject, str, args...) ->
## name could be invoke or its!
name = state("current").get("name")
@@ -157,21 +175,34 @@ module.exports = (Commands, Cypress, cy, state, config) ->
traversalErr = null
options._log = Cypress.log
message: message
$el: if $dom.isElement(subject) then subject else null
consoleProps: ->
Subject: subject
if options.log
options._log = Cypress.log
message: message
$el: if $dom.isElement(subject) then subject else null
consoleProps: ->
Subject: subject
if not _.isString(str)
$utils.throwErrByPath("invoke_its.invalid_1st_arg", {
if not str
$utils.throwErrByPath("invoke_its.null_or_undefined_property_name", {
onFail: options._log
args: { cmd: name, identifier: if isCmdIts then "property" else "function" }
})
if not _.isString(str) and not _.isNumber(str)
$utils.throwErrByPath("invoke_its.invalid_prop_name_arg", {
onFail: options._log
args: { cmd: name, identifier: if isCmdIts then "property" else "function" }
})
if not _.isObject(options) or _.isFunction(options)
$utils.throwErrByPath("invoke_its.invalid_options_arg", {
onFail: options._log
args: { cmd: name }
})
if isCmdIts and args.length > 0
$utils.throwErrByPath("invoke_its.invalid_num_of_args", {
onFail: options._log
if isCmdIts and args and args.length > 0
$utils.throwErrByPath("invoke_its.invalid_num_of_args", {
onFail: options._log
args: { cmd: name }
})
@@ -298,7 +329,10 @@ module.exports = (Commands, Cypress, cy, state, config) ->
actualSubject = remoteSubject or subject
paths = str.split(".")
if _.isString(str)
paths = str.split(".")
else
paths = [str]
prop = traverseObjectAtPath(actualSubject, paths)
@@ -449,9 +483,9 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## return values are undefined. prob should rethink
## this and investigate why that is the default behavior
## of child commands
invoke: ->
invokeFn.apply(@, arguments)
invoke: (subject, optionsOrStr, args...) ->
invokeFn.apply(@, [subject, optionsOrStr, args...])
its: ->
invokeFn.apply(@, arguments)
its: (subject, str, options, args...) ->
invokeItsFn.apply(@, [subject, str, options, args...])
})
+6 -10
View File
@@ -18,13 +18,8 @@ mergeDefaults = (obj) ->
## we always want to be able to see and influence cookies
## on our superdomain
{ superDomain } = $Location.create(window.location.href)
# { hostname } = $Location.create(window.location.href)
merge = (o) ->
## we are hostOnly if we dont have an
## explicit domain
# o.hostOnly = !o.domain
## and if the user did not provide a domain
## then we know to set the default to be origin
_.defaults o, {domain: superDomain}
@@ -63,8 +58,8 @@ module.exports = (Commands, Cypress, cy, state, config) ->
}
}
getAndClear = (log, timeout) ->
automateCookies("get:cookies", {}, log, timeout)
getAndClear = (log, timeout, options = {}) ->
automateCookies("get:cookies", options, log, timeout)
.then (resp) =>
## bail early if we got no cookies!
return resp if resp and resp.length is 0
@@ -137,13 +132,14 @@ module.exports = (Commands, Cypress, cy, state, config) ->
obj
})
automateCookies("get:cookies", {}, options._log, options.timeout)
automateCookies("get:cookies", _.pick(options, 'domain'), options._log, options.timeout)
.then (resp) ->
options.cookies = resp
return resp
setCookie: (name, value, options = {}) ->
setCookie: (name, value, userOptions = {}) ->
options = _.clone(userOptions)
_.defaults options, {
name: name
value: value
@@ -256,7 +252,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
obj
})
getAndClear(options._log, options.timeout)
getAndClear(options._log, options.timeout, { domain: options.domain })
.then (resp) ->
options.cookies = resp
@@ -1,477 +0,0 @@
_ = require("lodash")
$ = require("jquery")
Promise = require("bluebird")
$dom = require("../../dom")
$utils = require("../../cypress/utils")
$expr = $.expr[":"]
$contains = $expr.contains
restoreContains = ->
$expr.contains = $contains
module.exports = (Commands, Cypress, cy, state, config) ->
## restore initially when a run starts
restoreContains()
## restore before each test and whenever we stop
Cypress.on("test:before:run", restoreContains)
Cypress.on("stop", restoreContains)
Commands.addAll({
focused: (options = {}) ->
_.defaults(options, {
verify: true
log: true
})
if options.log
options._log = Cypress.log()
log = ($el) ->
return if options.log is false
options._log.set({
$el: $el
consoleProps: ->
ret = if $el
$dom.getElements($el)
else
"--nothing--"
Yielded: ret
Elements: $el?.length ? 0
})
getFocused = ->
focused = cy.getFocused()
log(focused)
return focused
do resolveFocused = (failedByNonAssertion = false) ->
Promise
.try(getFocused)
.then ($el) ->
if options.verify is false
return $el
if not $el
$el = $dom.wrap(null)
$el.selector = "focused"
## pass in a null jquery object for assertions
cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveFocused
})
get: (selector, options = {}) ->
ctx = @
if options is null or Array.isArray(options) or typeof options isnt 'object' then return $utils.throwErrByPath "get.invalid_options", {
args: { options }
}
_.defaults(options, {
retry: true
withinSubject: cy.state("withinSubject")
log: true
command: null
verify: true
})
consoleProps = {}
start = (aliasType) ->
return if options.log is false
options._log ?= Cypress.log
message: selector
referencesAlias: if aliasObj?.alias then {name: aliasObj.alias}
aliasType: aliasType
consoleProps: -> consoleProps
log = (value, aliasType = "dom") ->
return if options.log is false
start(aliasType) if not _.isObject(options._log)
obj = {}
if aliasType is "dom"
_.extend(obj, {
$el: value
numRetries: options._retries
})
obj.consoleProps = ->
key = if aliasObj then "Alias" else "Selector"
consoleProps[key] = selector
switch aliasType
when "dom"
_.extend(consoleProps, {
Yielded: $dom.getElements(value)
Elements: value?.length
})
when "primitive"
_.extend(consoleProps, {
Yielded: value
})
when "route"
_.extend(consoleProps, {
Yielded: value
})
return consoleProps
options._log.set(obj)
## We want to strip everything after the last '.'
## only when it is potentially a number or 'all'
if _.indexOf(selector, ".") == -1 ||
selector.slice(1) in _.keys(cy.state("aliases"))
toSelect = selector
else
allParts = _.split(selector, '.')
toSelect = _.join(_.dropRight(allParts, 1), '.')
if aliasObj = cy.getAlias(toSelect)
{subject, alias, command} = aliasObj
return do resolveAlias = ->
switch
## if this is a DOM element
when $dom.isElement(subject)
replayFrom = false
replay = ->
cy.replayCommandsFrom(command)
## its important to return undefined
## here else we trick cypress into thinking
## we have a promise violation
return undefined
## if we're missing any element
## within our subject then filter out
## anything not currently in the DOM
if $dom.isDetached(subject)
subject = subject.filter (index, el) ->
$dom.isAttached(el)
## if we have nothing left
## just go replay the commands
if not subject.length
return replay()
log(subject)
return cy.verifyUpcomingAssertions(subject, options, {
onFail: (err) ->
## if we are failing because our aliased elements
## are less than what is expected then we know we
## need to requery for them and can thus replay
## the commands leading up to the alias
if err.type is "length" and err.actual < err.expected
replayFrom = true
onRetry: ->
if replayFrom
replay()
else
resolveAlias()
})
## if this is a route command
when command.get("name") is "route"
if !(_.indexOf(selector, ".") == -1 ||
selector.slice(1) in _.keys(cy.state("aliases")))
allParts = _.split(selector, ".")
index = _.last(allParts)
alias = _.join([alias, index], ".")
requests = cy.getRequestsByAlias(alias) ? null
log(requests, "route")
return requests
else
## log as primitive
log(subject, "primitive")
do verifyAssertions = =>
cy.verifyUpcomingAssertions(subject, options, {
ensureExistenceFor: false
onRetry: verifyAssertions
})
start("dom")
setEl = ($el) ->
return if options.log is false
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el?.length
options._log.set({$el: $el})
getElements = ->
## attempt to query for the elements by withinSubject context
## and catch any sizzle errors!
try
$el = cy.$$(selector, options.withinSubject)
## jQuery v3 has removed its deprecated properties like ".selector"
## https://jquery.com/upgrade-guide/3.0/breaking-change-deprecated-context-and-selector-properties-removed
## but our error messages use this property to actually show the missing element
## so let's put it back
$el.selector ?= selector
catch e
e.onFail = -> if options.log is false then e else options._log.error(e)
throw e
## if that didnt find anything and we have a within subject
## and we have been explictly told to filter
## then just attempt to filter out elements from our within subject
if not $el.length and options.withinSubject and options.filter
filtered = options.withinSubject.filter(selector)
## reset $el if this found anything
$el = filtered if filtered.length
## store the $el now in case we fail
setEl($el)
## allow retry to be a function which we ensure
## returns truthy before returning its
if _.isFunction(options.onRetry)
if ret = options.onRetry.call(ctx, $el)
log($el)
return ret
else
log($el)
return $el
do resolveElements = ->
Promise.try(getElements).then ($el) ->
if options.verify is false
return $el
cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements
})
root: (options = {}) ->
_.defaults options, {log: true}
if options.log isnt false
options._log = Cypress.log({message: ""})
log = ($el) ->
options._log.set({$el: $el}) if options.log
return $el
if withinSubject = cy.state("withinSubject")
return log(withinSubject)
cy.now("get", "html", {log: false}).then(log)
})
Commands.addAll({ prevSubject: ["optional", "window", "document", "element"] }, {
contains: (subject, filter, text, options = {}) ->
## nuke our subject if its present but not an element.
## in these cases its either window or document but
## we dont care.
## we'll null out the subject so it will show up as a parent
## command since its behavior is identical to using it
## as a parent command: cy.contains()
if subject and not $dom.isElement(subject)
subject = null
switch
## .contains(filter, text)
when _.isRegExp(text)
text = text
filter = filter
## .contains(text, options)
when _.isObject(text)
options = text
text = filter
filter = ""
## .contains(text)
when _.isUndefined(text)
text = filter
filter = ""
_.defaults options, {log: true}
$utils.throwErrByPath "contains.invalid_argument" if not (_.isString(text) or _.isFinite(text) or _.isRegExp(text))
$utils.throwErrByPath "contains.empty_string" if _.isBlank(text)
getPhrase = (type, negated) ->
switch
when filter and subject
node = $dom.stringify(subject, "short")
"within the element: #{node} and with the selector: '#{filter}' "
when filter
"within the selector: '#{filter}' "
when subject
node = $dom.stringify(subject, "short")
"within the element: #{node} "
else
""
getErr = (err) ->
{type, negated, node} = err
switch type
when "existence"
if negated
"Expected not to find content: '#{text}' #{getPhrase(type, negated)}but continuously found it."
else
"Expected to find content: '#{text}' #{getPhrase(type, negated)}but never did."
if options.log isnt false
consoleProps = {
Content: text
"Applied To": $dom.getElements(subject or cy.state("withinSubject"))
}
options._log = Cypress.log
message: _.compact([filter, text])
type: if subject then "child" else "parent"
consoleProps: -> consoleProps
setEl = ($el) ->
return if options.log is false
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el?.length
options._log.set({$el: $el})
if _.isRegExp(text)
$expr.contains = (elem) ->
## taken from jquery's normal contains method
text.test(elem.textContent or elem.innerText or $.text(elem))
## find elements by the :contains psuedo selector
## and any submit inputs with the attributeContainsWord selector
selector = $dom.getContainsSelector(text, filter)
checkToAutomaticallyRetry = (count, $el) ->
## we should automatically retry querying
## if we did not have any upcoming assertions
## and our $el's length was 0, because that means
## the element didnt exist in the DOM and the user
## did not explicitly request that it does not exist
return if count isnt 0 or ($el and $el.length)
## throw here to cause the .catch to trigger
throw new Error()
resolveElements = ->
getOpts = _.extend(_.clone(options), {
# error: getErr(text, phrase)
withinSubject: subject or cy.state("withinSubject") or cy.$$("body")
filter: true
log: false
# retry: false ## dont retry because we perform our own element validation
verify: false ## dont verify upcoming assertions, we do that ourselves
})
cy.now("get", selector, getOpts).then ($el) ->
if $el and $el.length
$el = $dom.getFirstDeepestElement($el)
setEl($el)
cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements
onFail: (err) ->
switch err.type
when "length"
if err.expected > 1
$utils.throwErrByPath "contains.length_option", { onFail: options._log }
when "existence"
err.displayMessage = getErr(err)
})
Promise
.try(resolveElements)
.finally ->
## always restore contains in case
## we used a regexp!
restoreContains()
})
Commands.addAll({ prevSubject: "element" }, {
within: (subject, options, fn) ->
ctx = @
if _.isUndefined(fn)
fn = options
options = {}
_.defaults options, {log: true}
if options.log
options._log = Cypress.log({
$el: subject
message: ""
})
$utils.throwErrByPath("within.invalid_argument", { onFail: options._log }) if not _.isFunction(fn)
## reference the next command after this
## within. when that command runs we'll
## know to remove withinSubject
next = cy.state("current").get("next")
## backup the current withinSubject
## this prevents a bug where we null out
## withinSubject when there are nested .withins()
## we want the inner within to restore the outer
## once its done
prevWithinSubject = cy.state("withinSubject")
cy.state("withinSubject", subject)
fn.call(ctx, subject)
cleanup = ->
cy.removeListener("command:start", setWithinSubject)
## we need a mechanism to know when we should remove
## our withinSubject so we dont accidentally keep it
## around after the within callback is done executing
## so when each command starts, check to see if this
## is the command which references our 'next' and
## if so, remove the within subject
setWithinSubject = (obj) ->
return if obj isnt next
## okay so what we're doing here is creating a property
## which stores the 'next' command which will reset the
## withinSubject. If two 'within' commands reference the
## exact same 'next' command, then this prevents accidentally
## resetting withinSubject more than once. If they point
## to differnet 'next's then its okay
if next isnt cy.state("nextWithinSubject")
cy.state("withinSubject", prevWithinSubject or null)
cy.state("nextWithinSubject", next)
## regardless nuke this listeners
cleanup()
## if next is defined then we know we'll eventually
## unbind these listeners
if next
cy.on("command:start", setWithinSubject)
else
## remove our listener if we happen to reach the end
## event which will finalize cleanup if there was no next obj
cy.once "command:queue:before:end", ->
cleanup()
cy.state("withinSubject", null)
return subject
})
+621
View File
@@ -0,0 +1,621 @@
const _ = require('lodash')
const $ = require('jquery')
const Promise = require('bluebird')
const $dom = require('../../dom')
const $utils = require('../../cypress/utils')
const $expr = $.expr[':']
const $contains = $expr.contains
const restoreContains = () => {
return $expr.contains = $contains
}
module.exports = (Commands, Cypress, cy) => {
// restore initially when a run starts
restoreContains()
// restore before each test and whenever we stop
Cypress.on('test:before:run', restoreContains)
Cypress.on('stop', restoreContains)
Commands.addAll({
focused (options = {}) {
_.defaults(options, {
verify: true,
log: true,
})
if (options.log) {
options._log = Cypress.log()
}
const log = ($el) => {
if (options.log === false) {
return
}
options._log.set({
$el,
consoleProps () {
const ret = $el ? $dom.getElements($el) : '--nothing--'
return {
Yielded: ret,
Elements: $el != null ? $el.length : 0,
}
},
})
}
const getFocused = () => {
const focused = cy.getFocused()
log(focused)
return focused
}
const resolveFocused = () => {
return Promise
.try(getFocused)
.then(($el) => {
if (options.verify === false) {
return $el
}
if (!$el) {
$el = $dom.wrap(null)
$el.selector = 'focused'
}
// pass in a null jquery object for assertions
return cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveFocused,
})
})
}
return resolveFocused()
},
get (selector, options = {}) {
const ctx = this
if ((options === null) || Array.isArray(options) || (typeof options !== 'object')) {
return $utils.throwErrByPath('get.invalid_options', {
args: { options },
})
}
_.defaults(options, {
retry: true,
withinSubject: cy.state('withinSubject'),
log: true,
command: null,
verify: true,
})
let aliasObj
const consoleProps = {}
const start = (aliasType) => {
if (options.log === false) {
return
}
if (options._log == null) {
options._log = Cypress.log({
message: selector,
referencesAlias: (aliasObj != null && aliasObj.alias) ? { name: aliasObj.alias } : undefined,
aliasType,
consoleProps: () => {
return consoleProps
},
})
}
}
const log = (value, aliasType = 'dom') => {
if (options.log === false) {
return
}
if (!_.isObject(options._log)) {
start(aliasType)
}
const obj = {}
if (aliasType === 'dom') {
_.extend(obj, {
$el: value,
numRetries: options._retries,
})
}
obj.consoleProps = () => {
const key = aliasObj ? 'Alias' : 'Selector'
consoleProps[key] = selector
switch (aliasType) {
case 'dom':
_.extend(consoleProps, {
Yielded: $dom.getElements(value),
Elements: (value != null ? value.length : undefined),
})
break
case 'primitive':
_.extend(consoleProps, {
Yielded: value,
})
break
case 'route':
_.extend(consoleProps, {
Yielded: value,
})
break
default:
break
}
return consoleProps
}
options._log.set(obj)
}
let allParts
let toSelect
// We want to strip everything after the last '.'
// only when it is potentially a number or 'all'
if ((_.indexOf(selector, '.') === -1) ||
(_.keys(cy.state('aliases')).includes(selector.slice(1)))) {
toSelect = selector
} else {
allParts = _.split(selector, '.')
toSelect = _.join(_.dropRight(allParts, 1), '.')
}
aliasObj = cy.getAlias(toSelect)
if (aliasObj) {
let { subject, alias, command } = aliasObj
const resolveAlias = () => {
// if this is a DOM element
if ($dom.isElement(subject)) {
let replayFrom = false
const replay = () => {
cy.replayCommandsFrom(command)
// its important to return undefined
// here else we trick cypress into thinking
// we have a promise violation
return undefined
}
// if we're missing any element
// within our subject then filter out
// anything not currently in the DOM
if ($dom.isDetached(subject)) {
subject = subject.filter((index, el) => $dom.isAttached(el))
// if we have nothing left
// just go replay the commands
if (!subject.length) {
return replay()
}
}
log(subject)
return cy.verifyUpcomingAssertions(subject, options, {
onFail (err) {
// if we are failing because our aliased elements
// are less than what is expected then we know we
// need to requery for them and can thus replay
// the commands leading up to the alias
if ((err.type === 'length') && (err.actual < err.expected)) {
return replayFrom = true
}
},
onRetry () {
if (replayFrom) {
return replay()
}
return resolveAlias()
},
})
}
// if this is a route command
if (command.get('name') === 'route') {
if (!((_.indexOf(selector, '.') === -1) ||
(_.keys(cy.state('aliases')).includes(selector.slice(1))))
) {
allParts = _.split(selector, '.')
const index = _.last(allParts)
alias = _.join([alias, index], '.')
}
const requests = cy.getRequestsByAlias(alias) || null
log(requests, 'route')
return requests
}
// log as primitive
log(subject, 'primitive')
const verifyAssertions = () => {
return cy.verifyUpcomingAssertions(subject, options, {
ensureExistenceFor: false,
onRetry: verifyAssertions,
})
}
return verifyAssertions()
}
return resolveAlias()
}
start('dom')
const setEl = ($el) => {
if (options.log === false) {
return
}
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el != null ? $el.length : undefined
options._log.set({ $el })
}
const getElements = () => {
// attempt to query for the elements by withinSubject context
// and catch any sizzle errors!
let $el
try {
$el = cy.$$(selector, options.withinSubject)
// jQuery v3 has removed its deprecated properties like ".selector"
// https://jquery.com/upgrade-guide/3.0/breaking-change-deprecated-context-and-selector-properties-removed
// but our error messages use this property to actually show the missing element
// so let's put it back
if ($el.selector == null) {
$el.selector = selector
}
} catch (e) {
e.onFail = () => {
if (options.log === false) {
return e
}
options._log.error(e)
}
throw e
}
// if that didnt find anything and we have a within subject
// and we have been explictly told to filter
// then just attempt to filter out elements from our within subject
if (!$el.length && options.withinSubject && options.filter) {
const filtered = options.withinSubject.filter(selector)
// reset $el if this found anything
if (filtered.length) {
$el = filtered
}
}
// store the $el now in case we fail
setEl($el)
// allow retry to be a function which we ensure
// returns truthy before returning its
if (_.isFunction(options.onRetry)) {
const ret = options.onRetry.call(ctx, $el)
if (ret) {
log($el)
return ret
}
} else {
log($el)
return $el
}
}
const resolveElements = () => {
return Promise.try(getElements).then(($el) => {
if (options.verify === false) {
return $el
}
return cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements,
})
})
}
return resolveElements()
},
root (options = {}) {
_.defaults(options, { log: true })
if (options.log !== false) {
options._log = Cypress.log({ message: '' })
}
const log = ($el) => {
if (options.log) {
options._log.set({ $el })
}
return $el
}
const withinSubject = cy.state('withinSubject')
if (withinSubject) {
return log(withinSubject)
}
return cy.now('get', 'html', { log: false }).then(log)
},
})
Commands.addAll({ prevSubject: ['optional', 'window', 'document', 'element'] }, {
contains (subject, filter, text, options = {}) {
// nuke our subject if its present but not an element.
// in these cases its either window or document but
// we dont care.
// we'll null out the subject so it will show up as a parent
// command since its behavior is identical to using it
// as a parent command: cy.contains()
if (subject && !$dom.isElement(subject)) {
subject = null
}
if (_.isRegExp(text)) {
// .contains(filter, text)
// Do nothing
} else if (_.isObject(text)) {
// .contains(text, options)
options = text
text = filter
filter = ''
} else if (_.isUndefined(text)) {
// .contains(text)
text = filter
filter = ''
}
_.defaults(options, { log: true })
if (!(_.isString(text) || _.isFinite(text) || _.isRegExp(text))) {
$utils.throwErrByPath('contains.invalid_argument')
}
if (_.isBlank(text)) {
$utils.throwErrByPath('contains.empty_string')
}
const getPhrase = () => {
if (filter && subject) {
const node = $dom.stringify(subject, 'short')
return `within the element: ${node} and with the selector: '${filter}' `
}
if (filter) {
return `within the selector: '${filter}' `
}
if (subject) {
const node = $dom.stringify(subject, 'short')
return `within the element: ${node} `
}
return ''
}
const getErr = (err) => {
const { type, negated } = err
if (type === 'existence') {
if (negated) {
return `Expected not to find content: '${text}' ${getPhrase()}but continuously found it.`
}
return `Expected to find content: '${text}' ${getPhrase()}but never did.`
}
}
let consoleProps
if (options.log !== false) {
consoleProps = {
Content: text,
'Applied To': $dom.getElements(subject || cy.state('withinSubject')),
}
options._log = Cypress.log({
message: _.compact([filter, text]),
type: subject ? 'child' : 'parent',
consoleProps: () => {
return consoleProps
},
})
}
const setEl = ($el) => {
if (options.log === false) {
return
}
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el != null ? $el.length : undefined
options._log.set({ $el })
}
if (_.isRegExp(text)) {
// taken from jquery's normal contains method
$expr.contains = (elem) => {
return text.test(elem.textContent || elem.innerText || $.text(elem))
}
}
// find elements by the :contains psuedo selector
// and any submit inputs with the attributeContainsWord selector
const selector = $dom.getContainsSelector(text, filter)
const resolveElements = () => {
const getOpts = _.extend(_.clone(options), {
// error: getErr(text, phrase)
withinSubject: subject || cy.state('withinSubject') || cy.$$('body'),
filter: true,
log: false,
// retry: false ## dont retry because we perform our own element validation
verify: false, // dont verify upcoming assertions, we do that ourselves
})
return cy.now('get', selector, getOpts).then(($el) => {
if ($el && $el.length) {
$el = $dom.getFirstDeepestElement($el)
}
setEl($el)
return cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements,
onFail (err) {
switch (err.type) {
case 'length':
if (err.expected > 1) {
return $utils.throwErrByPath('contains.length_option', { onFail: options._log })
}
break
case 'existence':
return err.displayMessage = getErr(err)
default:
break
}
},
})
})
}
return Promise
.try(resolveElements)
// always restore contains in case
// we used a regexp!
.finally(() => {
restoreContains()
})
},
})
Commands.addAll({ prevSubject: 'element' }, {
within (subject, options, fn) {
const ctx = this
if (_.isUndefined(fn)) {
fn = options
options = {}
}
_.defaults(options, { log: true })
if (options.log) {
options._log = Cypress.log({
$el: subject,
message: '',
})
}
if (!_.isFunction(fn)) {
$utils.throwErrByPath('within.invalid_argument', { onFail: options._log })
}
// reference the next command after this
// within. when that command runs we'll
// know to remove withinSubject
const next = cy.state('current').get('next')
// backup the current withinSubject
// this prevents a bug where we null out
// withinSubject when there are nested .withins()
// we want the inner within to restore the outer
// once its done
const prevWithinSubject = cy.state('withinSubject')
cy.state('withinSubject', subject)
fn.call(ctx, subject)
const cleanup = () => cy.removeListener('command:start', setWithinSubject)
// we need a mechanism to know when we should remove
// our withinSubject so we dont accidentally keep it
// around after the within callback is done executing
// so when each command starts, check to see if this
// is the command which references our 'next' and
// if so, remove the within subject
const setWithinSubject = (obj) => {
if (obj !== next) {
return
}
// okay so what we're doing here is creating a property
// which stores the 'next' command which will reset the
// withinSubject. If two 'within' commands reference the
// exact same 'next' command, then this prevents accidentally
// resetting withinSubject more than once. If they point
// to differnet 'next's then its okay
if (next !== cy.state('nextWithinSubject')) {
cy.state('withinSubject', prevWithinSubject || null)
cy.state('nextWithinSubject', next)
}
// regardless nuke this listeners
cleanup()
}
// if next is defined then we know we'll eventually
// unbind these listeners
if (next) {
cy.on('command:start', setWithinSubject)
} else {
// remove our listener if we happen to reach the end
// event which will finalize cleanup if there was no next obj
cy.once('command:queue:before:end', () => {
cleanup()
cy.state('withinSubject', null)
})
}
return subject
},
})
}
@@ -159,7 +159,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
widthAndHeightAreWithinBounds = (width, height) ->
_.every [width, height], (val) ->
val >= 20 and val <= 4000
val >= 0
switch
when _.isString(presetOrWidth) and _.isBlank(presetOrWidth)
+1 -1
View File
@@ -253,7 +253,7 @@ module.exports = (Commands, Cypress, cy, state, config) ->
Cypress.on "test:before:run:async", ->
## reset any state on the backend
Cypress.backend('reset:xhr:server')
Cypress.backend('reset:server:state')
Cypress.on "test:before:run", ->
## reset the existing server
+74 -64
View File
@@ -8,7 +8,6 @@ import * as $dom from '../dom'
import * as $document from '../dom/document'
import * as $elements from '../dom/elements'
import * as $selection from '../dom/selection'
import { HTMLTextLikeElement, HTMLTextLikeInputElement } from '../dom/types'
import $window from '../dom/window'
const debug = Debug('cypress:driver:keyboard')
@@ -36,7 +35,7 @@ interface KeyDetailsPartial extends Partial<KeyDetails> {
}
type SimulatedDefault = (
el: HTMLTextLikeElement,
el: HTMLElement,
key: KeyDetails,
options: any
) => void
@@ -52,6 +51,7 @@ interface KeyDetails {
shiftKeyCode?: number
simulatedDefault?: SimulatedDefault
simulatedDefaultOnly?: boolean
originalSequence?: string
events: {
[key in KeyEventType]?: boolean;
}
@@ -62,6 +62,7 @@ const monthRe = /^\d{4}-(0\d|1[0-2])/
const weekRe = /^\d{4}-W(0[1-9]|[1-4]\d|5[0-3])/
const timeRe = /^([0-1]\d|2[0-3]):[0-5]\d(:[0-5]\d)?(\.[0-9]{1,3})?/
const dateTimeRe = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}/
const numberRe = /^-?(0|[1-9]\d*)(\.\d+)?(e-?(0|[1-9]\d*))?$/i
const charsBetweenCurlyBracesRe = /({.+?})/
const INITIAL_MODIFIERS = {
@@ -153,16 +154,16 @@ const getFormattedKeyString = (details: KeyDetails) => {
let foundKeyString = _.findKey(keyboardMappings, { key: details.key })
if (foundKeyString) {
return `{${foundKeyString}}`
return `{${details.originalSequence}}`
}
foundKeyString = keyToModifierMap[details.key]
if (foundKeyString) {
return `<${foundKeyString}>`
return `{${details.originalSequence}}`
}
return details.key
return details.originalSequence
}
const countNumIndividualKeyStrokes = (keys: KeyDetails[]) => {
@@ -206,6 +207,8 @@ const getKeyDetails = (onKeyNotFound) => {
details.text = details.key
}
details.originalSequence = key
return details
}
@@ -215,10 +218,6 @@ const getKeyDetails = (onKeyNotFound) => {
}
}
const hasModifierBesidesShift = (modifiers: KeyboardModifiers) => {
return _.some(_.omit(modifiers, ['shift']))
}
/**
* @example '{foo}' => 'foo'
*/
@@ -236,7 +235,7 @@ const shouldIgnoreEvent = <
return options[eventName] === false
}
const shouldUpdateValue = (el: HTMLElement, key: KeyDetails) => {
const shouldUpdateValue = (el: HTMLElement, key: KeyDetails, options) => {
if (!key.text) return false
const bounds = $selection.getSelectionBounds(el)
@@ -247,6 +246,29 @@ const shouldUpdateValue = (el: HTMLElement, key: KeyDetails) => {
return false
}
const isNumberInputType = $elements.isInput(el) && $elements.isInputType(el, 'number')
if (isNumberInputType) {
const needsValue = options.prevVal || ''
const needsValueLength = (needsValue && needsValue.length) || 0
const curVal = $elements.getNativeProp(el, 'value')
const bounds = $selection.getSelectionBounds(el)
// We need to see if the number we're about to type is a valid number, since setting a number input
// to an invalid number will not set the value and possibly throw a warning in the console
const potentialValue = $selection.insertSubstring(curVal + needsValue, key.text, [bounds.start + needsValueLength, bounds.end + needsValueLength])
if (!(numberRe.test(potentialValue))) {
debug('skipping inserting value since number input would be invalid', key.text, potentialValue)
options.prevVal = needsValue + key.text
return
}
key.text = (options.prevVal || '') + key.text
options.prevVal = null
}
if (noneSelected) {
const ml = $elements.getNativeProp(el, 'maxLength')
@@ -309,13 +331,15 @@ const validateTyping = (
let isWeek = false
let isDateTime = false
// use 'type' attribute instead of prop since browsers without
// support for attribute input type will have type prop of 'text'
if ($elements.isInput(el)) {
isDate = $dom.isInputType(el, 'date')
isTime = $dom.isInputType(el, 'time')
isMonth = $dom.isInputType(el, 'month')
isWeek = $dom.isInputType(el, 'week')
isDate = $elements.isAttrType(el, 'date')
isTime = $elements.isAttrType(el, 'time')
isMonth = $elements.isAttrType(el, 'month')
isWeek = $elements.isAttrType(el, 'week')
isDateTime =
$dom.isInputType(el, 'datetime') || $dom.isInputType(el, 'datetime-local')
$elements.isAttrType(el, 'datetime') || $elements.isAttrType(el, 'datetime-local')
}
const isFocusable = $elements.isFocusable($el)
@@ -453,11 +477,7 @@ function _getEndIndex (str, substr) {
// Simulated default actions for select few keys.
const simulatedDefaultKeyMap: { [key: string]: SimulatedDefault } = {
Enter: (el, key, options) => {
if ($elements.isContentEditable(el) || $elements.isTextarea(el)) {
key.events.input = $selection.replaceSelectionContents(el, '\n')
} else {
key.events.input = false
}
$selection.replaceSelectionContents(el, '\n')
options.onEnterPressed()
},
@@ -508,27 +528,21 @@ const keyboardMappings: { [key: string]: KeyDetailsPartial } = {
selectAll: {
key: 'selectAll',
simulatedDefault: (el) => {
const doc = $document.getDocumentFromElement(el)
return $selection.selectAll(doc)
$selection.selectAll(el)
},
simulatedDefaultOnly: true,
},
moveToStart: {
key: 'moveToStart',
simulatedDefault: (el) => {
const doc = $document.getDocumentFromElement(el)
return $selection.moveSelectionToStart(doc)
$selection.moveSelectionToStart(el)
},
simulatedDefaultOnly: true,
},
moveToEnd: {
key: 'moveToEnd',
simulatedDefault: (el) => {
const doc = $document.getDocumentFromElement(el)
return $selection.moveSelectionToEnd(doc)
$selection.moveSelectionToEnd(el)
},
simulatedDefaultOnly: true,
},
@@ -695,7 +709,7 @@ export class Keyboard {
debug('setting element value', valToSet, activeEl)
return $elements.setNativeProp(
activeEl as HTMLTextLikeInputElement,
activeEl as $elements.HTMLTextLikeInputElement,
'value',
valToSet
)
@@ -713,30 +727,18 @@ export class Keyboard {
}
this.typeSimulatedKey(activeEl, key, options)
debug('returning null')
return null
}
}
)
const modifierKeys = _.filter(keyDetailsArr, isModifier)
if (options.simulated && !options.delay) {
_.each(typeKeyFns, (fn) => {
return fn()
})
if (options.release !== false) {
_.each(modifierKeys, (key) => {
return this.simulatedKeyup(getActiveEl(doc), key, options)
})
}
options.onAfterType()
return
}
// we will only press each modifier once, so only find unique modifiers
const modifierKeys = _
.chain(keyDetailsArr)
.filter(isModifier)
.uniqBy('key')
.value()
return Promise
.each(typeKeyFns, (fn) => {
@@ -747,6 +749,8 @@ export class Keyboard {
.then(() => {
if (options.release !== false) {
return Promise.map(modifierKeys, (key) => {
options.id = _.uniqueId('char')
return this.simulatedKeyup(getActiveEl(doc), key, options)
})
}
@@ -897,8 +901,7 @@ export class Keyboard {
debug(`dispatched [${eventType}] on ${el}`)
const formattedKeyString = getFormattedKeyString(keyDetails)
debug('format string', formattedKeyString)
options.onEvent(options.id, formattedKeyString, eventType, which, dispatched)
options.onEvent(options.id, formattedKeyString, event, dispatched)
return dispatched
}
@@ -923,10 +926,11 @@ export class Keyboard {
details.text = details.shiftText
}
// If any modifier besides shift is pressed, no text.
if (hasModifierBesidesShift(modifiers)) {
details.text = ''
}
// TODO: Re-think skipping text insert if non-shift modifers
// @see https://github.com/cypress-io/cypress/issues/5622
// if (hasModifierBesidesShift(modifiers)) {
// details.text = ''
// }
return details
}
@@ -954,18 +958,22 @@ export class Keyboard {
const didFlag = this.flagModifier(_key)
if (!didFlag) {
return null
// we've already pressed this modifier, so ignore it and don't fire keydown or keyup
_key.events.keydown = false
}
// don't fire keyup for modifier keys, this will happen after all other keys are typed
_key.events.keyup = false
}
const key = this.getModifierKeyDetails(_key)
if (!key.text) {
key.events.input = false
key.events.keypress = false
key.events.textInput = false
if (key.key !== 'Backspace' && key.key !== 'Delete') {
key.events.input = false
}
}
let elToType
@@ -985,9 +993,12 @@ export class Keyboard {
if (key.key === 'Enter' && $elements.isInput(elToType)) {
key.events.textInput = false
key.events.input = false
}
if ($elements.isReadOnlyInputOrTextarea(elToType)) {
if ($elements.isContentEditable(elToType)) {
key.events.input = false
} else if ($elements.isReadOnlyInputOrTextarea(elToType)) {
key.events.textInput = false
}
@@ -1044,19 +1055,16 @@ export class Keyboard {
this.fireSimulatedEvent(el, 'keyup', key, options)
}
getSimulatedDefaultForKey (key: KeyDetails) {
getSimulatedDefaultForKey (key: KeyDetails, options) {
debug('getSimulatedDefaultForKey', key.key)
if (key.simulatedDefault) return key.simulatedDefault
let nonShiftModifierPressed = hasModifierBesidesShift(this.getActiveModifiers())
debug({ nonShiftModifierPressed, key })
if (!nonShiftModifierPressed && simulatedDefaultKeyMap[key.key]) {
if (simulatedDefaultKeyMap[key.key]) {
return simulatedDefaultKeyMap[key.key]
}
return (el: HTMLElement) => {
if (!shouldUpdateValue(el, key)) {
if (!shouldUpdateValue(el, key, options)) {
debug('skip typing key', false)
key.events.input = false
@@ -1084,7 +1092,7 @@ export class Keyboard {
performSimulatedDefault (el: HTMLElement, key: KeyDetails, options: any) {
debug('performSimulatedDefault', key.key)
const simulatedDefault = this.getSimulatedDefaultForKey(key)
const simulatedDefault = this.getSimulatedDefaultForKey(key, options)
if ($elements.isTextLike(el)) {
if ($elements.isInput(el) || $elements.isTextarea(el)) {
@@ -1099,6 +1107,8 @@ export class Keyboard {
simulatedDefault(el, key, options)
}
debug({ key })
shouldIgnoreEvent('input', key.events) ||
this.fireSimulatedEvent(el, 'input', key, options)
+65 -55
View File
@@ -247,16 +247,16 @@ const create = (state, keyboard, focused) => {
pointerout()
pointerleave()
events.push({ pointerover: pointerover() })
events.push({ type: 'pointerover', ...pointerover() })
pointerenter()
mouseout()
mouseleave()
events.push({ mouseover: mouseover() })
events.push({ type: 'mouseover', ...mouseover() })
mouseenter()
state('mouseLastHoveredEl', $elements.isAttachedEl(el) ? el : null)
state('mouseCoords', { x, y })
events.push({ pointermove: pointermove() })
events.push({ mousemove: mousemove() })
events.push({ type: 'pointermove', ...pointermove() })
events.push({ type: 'mousemove', ...mousemove() })
return events
},
@@ -310,12 +310,12 @@ const create = (state, keyboard, focused) => {
}, mouseEvtOptionsExtend)
// TODO: pointer events should have fractional coordinates, not rounded
let pointerdownProps = sendPointerdown(
let pointerdown = sendPointerdown(
el,
pointerEvtOptions
)
const pointerdownPrevented = pointerdownProps.preventedDefault
const pointerdownPrevented = pointerdown.preventedDefault
const elIsDetached = $elements.isDetachedEl(el)
if (pointerdownPrevented || elIsDetached) {
@@ -326,31 +326,37 @@ const create = (state, keyboard, focused) => {
}
return {
pointerdownProps,
mousedownProps: {
skipped: formatReasonNotFired(reason),
targetEl: el,
events: {
pointerdown,
mousedown: {
skipped: formatReasonNotFired(reason),
},
},
}
}
let mousedownProps = sendMousedown(el, mouseEvtOptions)
let mousedown = sendMousedown(el, mouseEvtOptions)
return {
pointerdownProps,
mousedownProps,
targetEl: el,
events: {
pointerdown,
mousedown,
},
}
},
down (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) {
const $previouslyFocused = focused.getFocused()
const mouseDownEvents = mouse._downEvents(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseDownPhase = mouse._downEvents(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
// el we just send pointerdown
const el = mouseDownEvents.pointerdownProps.el
const el = mouseDownPhase.targetEl
if (mouseDownEvents.pointerdownProps.preventedDefault || mouseDownEvents.mousedownProps.preventedDefault || !$elements.isAttachedEl(el)) {
return mouseDownEvents
if (mouseDownPhase.events.pointerdown.preventedDefault || mouseDownPhase.events.mousedown.preventedDefault || !$elements.isAttachedEl(el)) {
return mouseDownPhase
}
//# retrieve the first focusable $el in our parent chain
@@ -374,10 +380,10 @@ const create = (state, keyboard, focused) => {
if (shouldMoveCursorToEndAfterMousedown($elToFocus[0])) {
debug('moveSelectionToEnd due to click')
$selection.moveSelectionToEnd($dom.getDocumentFromElement($elToFocus[0]), { onlyIfEmptySelection: true })
$selection.moveSelectionToEnd($elToFocus[0], { onlyIfEmptySelection: true })
}
return mouseDownEvents
return mouseDownPhase
},
/**
@@ -410,42 +416,41 @@ const create = (state, keyboard, focused) => {
* el2 = moveToCoordsOrNoop(coords)
* sendMouseup(el2)
* el3 = moveToCoordsOrNoop(coords)
* if (notDetached(el1) && el1 === el2)
* sendClick(el3)
* if (notDetached(el1))
* sendClick(ancestorOf(el1, el2))
*/
click (fromElViewport, forceEl, pointerEvtOptionsExtend = {}, mouseEvtOptionsExtend = {}) {
debug('mouse.click', { fromElViewport, forceEl })
const mouseDownEvents = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseDownPhase = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault
const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
const mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
// Only send click event if the same element received both pointerdown and pointerup, and it's not detached.
const getSkipClickEventAndReason = () => {
const getElementToClick = () => {
// Never skip the click event when force:true
if (forceEl) {
return false
return { elToClick: forceEl }
}
if ($elements.isDetachedEl(mouseDownEvents.pointerdownProps.el)) {
return 'element was detached'
// Only send click event if mousedown element is not detached.
if ($elements.isDetachedEl(mouseDownPhase.targetEl)) {
return { skipClickEventReason: 'element was detached' }
}
if (!mouseUpEvents.pointerupProps.el || mouseDownEvents.pointerdownProps.el !== mouseUpEvents.pointerupProps.el) {
return 'mouseup and mousedown not received by same element'
}
const commonAncestor = mouseUpPhase.targetEl &&
mouseDownPhase.targetEl &&
$elements.getFirstCommonAncestor(mouseUpPhase.targetEl, mouseDownPhase.targetEl)
// No reason to skip the click event
return false
return { elToClick: commonAncestor }
}
const skipClickEvent = getSkipClickEventAndReason()
const { skipClickEventReason, elToClick } = getElementToClick()
const mouseClickEvents = mouse._mouseClickEvents(fromElViewport, mouseDownEvents.pointerdownProps.el, forceEl, skipClickEvent, mouseEvtOptionsExtend)
const mouseClickEvents = mouse._mouseClickEvents(fromElViewport, elToClick, forceEl, skipClickEventReason, mouseEvtOptionsExtend)
return _.extend({}, mouseDownEvents, mouseUpEvents, mouseClickEvents)
return _.extend({}, mouseDownPhase.events, mouseUpPhase.events, mouseClickEvents)
},
/**
@@ -471,29 +476,35 @@ const create = (state, keyboard, focused) => {
const el = forceEl || mouse.moveToCoords(fromElViewport)
let pointerupProps = sendPointerup(el, pointerEvtOptions)
let pointerup = sendPointerup(el, pointerEvtOptions)
if (skipMouseEvent || $elements.isDetachedEl($(el))) {
return {
pointerupProps,
mouseupProps: {
skipped: formatReasonNotFired('Previous event cancelled'),
targetEl: el,
events: {
pointerup,
mouseup: {
skipped: formatReasonNotFired('Previous event cancelled'),
},
},
}
}
let mouseupProps = sendMouseup(el, mouseEvtOptions)
let mouseup = sendMouseup(el, mouseEvtOptions)
return {
pointerupProps,
mouseupProps,
targetEl: el,
events: {
pointerup,
mouseup,
},
}
},
_mouseClickEvents (fromElViewport, el, forceEl, skipClickEvent, mouseEvtOptionsExtend = {}) {
if (skipClickEvent) {
return {
clickProps: {
click: {
skipped: formatReasonNotFired(skipClickEvent),
},
}
@@ -512,9 +523,9 @@ const create = (state, keyboard, focused) => {
detail: 1,
}, mouseEvtOptionsExtend)
let clickProps = sendClick(el, clickEventOptions)
let click = sendClick(el, clickEventOptions)
return { clickProps }
return { click }
},
_contextmenuEvent (fromElViewport, forceEl, mouseEvtOptionsExtend) {
@@ -530,9 +541,9 @@ const create = (state, keyboard, focused) => {
which: 3,
}, mouseEvtOptionsExtend)
let contextmenuProps = sendContextmenu(el, mouseEvtOptions)
let contextmenu = sendContextmenu(el, mouseEvtOptions)
return { contextmenuProps }
return { contextmenu }
},
dblclick (fromElViewport, forceEl, mouseEvtOptionsExtend = {}) {
@@ -553,9 +564,9 @@ const create = (state, keyboard, focused) => {
detail: 2,
}, mouseEvtOptionsExtend)
let dblclickProps = sendDblclick(el, dblclickEvtProps)
let dblclick = sendDblclick(el, dblclickEvtProps)
return { clickEvents1, clickEvents2, dblclickProps }
return { clickEvents1, clickEvents2, dblclick }
},
rightclick (fromElViewport, forceEl) {
@@ -570,15 +581,14 @@ const create = (state, keyboard, focused) => {
which: 3,
}
const mouseDownEvents = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseDownPhase = mouse.down(fromElViewport, forceEl, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const contextmenuEvent = mouse._contextmenuEvent(fromElViewport, forceEl)
const skipMouseupEvent = mouseDownEvents.pointerdownProps.skipped || mouseDownEvents.pointerdownProps.preventedDefault
const skipMouseupEvent = mouseDownPhase.events.pointerdown.skipped || mouseDownPhase.events.pointerdown.preventedDefault
const mouseUpPhase = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const mouseUpEvents = mouse.up(fromElViewport, forceEl, skipMouseupEvent, pointerEvtOptionsExtend, mouseEvtOptionsExtend)
const clickEvents = _.extend({}, mouseDownEvents, mouseUpEvents)
const clickEvents = _.extend({}, mouseDownPhase.events, mouseUpPhase.events)
return _.extend({}, { clickEvents, contextmenuEvent })
},
+1 -1
View File
@@ -19,7 +19,7 @@ const create = () => {
const invoke = (contentWindow, fnOrCode, params = []) => {
if (_.isFunction(fnOrCode)) {
return fnOrCode(...params)
return fnOrCode.apply(contentWindow, params)
}
return contentWindow.eval(fnOrCode)
+5
View File
@@ -193,6 +193,9 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
contentWindow.SVGElement.prototype.blur = ->
focused.interceptBlur(@)
contentWindow.HTMLInputElement.prototype.select = ->
$selection.interceptSelect.call(@)
contentWindow.document.hasFocus = ->
focused.documentHasFocus.call(@)
@@ -484,6 +487,8 @@ create = (specWindow, Cypress, Cookies, state, config, log) ->
## since this failed this means that a
## specific command failed and we should
## highlight it in red or insert a new command
err.name = err.name || 'CypressError'
errors.commandRunningFailed(err)
fail(err, state("runnable"))
@@ -409,12 +409,13 @@ module.exports = {
cy.wrap({ foo: {{value}} }).its('foo.baz').should('not.exist')
"""
invalid_1st_arg: "#{cmd('{{cmd}}')} only accepts a string as the first argument."
invalid_num_of_args:
"""
#{cmd('{{cmd}}')} only accepts a single argument.
If you want to invoke a function with arguments, use cy.invoke().
invalid_prop_name_arg: "#{cmd('{{cmd}}')} only accepts a string or a number as the {{identifier}}Name argument."
null_or_undefined_property_name: "#{cmd('{{cmd}}')} expects the {{identifier}}Name argument to have a value."
invalid_options_arg: "#{cmd('{{cmd}}')} only accepts an object as the options argument."
invalid_num_of_args:
"""
#{cmd('{{cmd}}')} does not accept additional arguments.
If you want to invoke a function with arguments, use cy.invoke().
"""
timed_out:
"""
@@ -1040,7 +1041,7 @@ module.exports = {
viewport:
bad_args: "#{cmd('viewport')} can only accept a string preset or a width and height as numbers."
dimensions_out_of_range: "#{cmd('viewport')} width and height must be between 20px and 4000px."
dimensions_out_of_range: "#{cmd('viewport')} width and height must be at least 0px."
empty_string: "#{cmd('viewport')} cannot be passed an empty string."
invalid_orientation: "#{cmd('viewport')} can only accept '{{all}}' as valid orientations. Your orientation was: '{{orientation}}'"
missing_preset: "#{cmd('viewport')} could not find a preset for: '{{preset}}'. Available presets are: {{presets}}"
-522
View File
@@ -1,522 +0,0 @@
_ = require("lodash")
$ = require("jquery")
clone = require("clone")
$Snapshots = require("../cy/snapshots")
$Events = require("./events")
$dom = require("../dom")
$utils = require("./utils")
## adds class methods for command, route, and agent logging
## including the intermediate $Log interface
CypressErrorRe = /(AssertionError|CypressError)/
groupsOrTableRe = /^(groups|table)$/
parentOrChildRe = /parent|child/
ERROR_PROPS = "message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff".split(" ")
SNAPSHOT_PROPS = "id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight".split(" ")
DISPLAY_PROPS = "id alias aliasType callCount displayName end err event functionName hookName instrument isStubbed message method name numElements numResponses referencesAlias renderProps state testId type url visible".split(" ")
BLACKLIST_PROPS = "snapshots".split(" ")
delay = null
counter = 0
{ HIGHLIGHT_ATTR } = $Snapshots
reduceMemory = (attrs) ->
## mutate attrs by nulling out
## object properties
_.each attrs, (value, key) ->
if _.isObject(value)
attrs[key] = null
toSerializedJSON = (attrs) ->
isDom = $dom.isDom
isWindow = $dom.isWindow
isDocument = $dom.isDocument
isElement = $dom.isElement
stringify = (value, key) ->
return null if key in BLACKLIST_PROPS
switch
when _.isArray(value)
_.map(value, stringify)
when isDom(value)
$dom.stringify(value, "short")
when _.isFunction(value) and groupsOrTableRe.test(key)
value()
when _.isFunction(value)
value.toString()
when _.isObject(value)
## clone to nuke circular references
## and blow away anything that throws
try
_.mapValues(clone(value), stringify)
catch err
null
else
value
_.mapValues(attrs, stringify)
getDisplayProps = (attrs) ->
_.pick(attrs, DISPLAY_PROPS)
getConsoleProps = (attrs) ->
attrs.consoleProps
getSnapshotProps = (attrs) ->
_.pick(attrs, SNAPSHOT_PROPS)
countLogsByTests = (tests = {}) ->
return 0 if _.isEmpty(tests)
_
.chain(tests)
.map (test, key) ->
[].concat(test.agents, test.routes, test.commands)
.flatten()
.compact()
.union([{id: 0}])
.map("id")
.max()
.value()
## TODO: fix this
setCounter = (num) ->
counter = num
setDelay = (val) ->
delay = val ? 4
defaults = (state, config, obj) ->
instrument = obj.instrument ? "command"
## dont set any defaults if this
## is an agent or route because we
## may not even be inside of a command
if instrument is "command"
current = state("current")
## we are logging a command instrument by default
_.defaults(obj, current?.pick("name", "type"))
## force duals to become either parents or childs
## normally this would be handled by the command itself
## but in cases where the command purposely does not log
## then it could still be logged during a failure, which
## is why we normalize its type value
if not parentOrChildRe.test(obj.type)
## does this command have a previously linked command
## by chainer id
obj.type = if current?.hasPreviouslyLinkedCommand() then "child" else "parent"
_.defaults(obj, {
event: false
renderProps: -> {}
consoleProps: ->
## if we don't have a current command just bail
return {} if not current
ret = if $dom.isElement(current.get("subject"))
$dom.getElements(current.get("subject"))
else
current.get("subject")
return { Yielded: ret }
})
# if obj.isCurrent
## stringify the obj.message (if it exists) or current.get("args")
obj.message = $utils.stringify(obj.message ? current?.get("args"))
## allow type to by a dynamic function
## so it can conditionally return either
## parent or child (useful in assertions)
if _.isFunction(obj.type)
obj.type = obj.type(current, state("subject"))
_.defaults(obj, {
id: (counter += 1)
state: "pending"
instrument: "command"
url: state("url")
hookName: state("hookName")
testId: state("runnable")?.id
viewportWidth: state("viewportWidth")
viewportHeight: state("viewportHeight")
referencesAlias: undefined
alias: undefined
aliasType: undefined
message: undefined
renderProps: -> {}
consoleProps: -> {}
})
Log = (cy, state, config, obj) ->
obj = defaults(state, config, obj)
## private attributes of each log
attributes = {}
return {
get: (attr) ->
if attr then attributes[attr] else attributes
unset: (key) ->
@set(key, undefined)
invoke: (key) ->
invoke = =>
## ensure this is a callable function
## and set its default to empty object literal
fn = @get(key)
if _.isFunction(fn)
fn()
else
fn
invoke() ? {}
serializeError: ->
if err = @get("error")
_.reduce ERROR_PROPS, (memo, prop) ->
if _.has(err, prop) or err[prop]
memo[prop] = err[prop]
memo
, {}
else
null
toJSON: ->
_
.chain(attributes)
.omit("error")
.omitBy(_.isFunction)
.extend({
err: @serializeError()
consoleProps: @invoke("consoleProps")
renderProps: @invoke("renderProps")
})
.value()
set: (key, val) ->
if _.isString(key)
obj = {}
obj[key] = val
else
obj = key
if "url" of obj
## always stringify the url property
obj.url = (obj.url ? "").toString()
## convert onConsole to consoleProps
## for backwards compatibility
if oc = obj.onConsole
obj.consoleProps = oc
## if we have an alias automatically
## figure out what type of alias it is
if obj.alias
_.defaults obj, {aliasType: if obj.$el then "dom" else "primitive"}
## dont ever allow existing id's to be mutated
if attributes.id
delete obj.id
_.extend(attributes, obj)
## if we have an consoleProps function
## then re-wrap it
if obj and _.isFunction(obj.consoleProps)
@wrapConsoleProps()
if obj and obj.$el
@setElAttrs()
@fireChangeEvent()
return @
pick: (args...) ->
args.unshift(attributes)
_.pick.apply(_, args)
publicInterface: ->
{
get: _.bind(@get, @)
on: _.bind(@on, @)
off: _.bind(@off, @)
pick: _.bind(@pick, @)
attributes: attributes
}
snapshot: (name, options = {}) ->
## bail early and don't snapshot if we're in headless mode
## or we're not storing tests
if not config("isInteractive") or config("numTestsKeptInMemory") is 0
return @
_.defaults options,
at: null
next: null
snapshot = cy.createSnapshot(name, @get("$el"))
snapshots = @get("snapshots") ? []
## insert at index 'at' or whatever is the next position
snapshots[options.at or snapshots.length] = snapshot
@set("snapshots", snapshots)
if next = options.next
fn = @snapshot
@snapshot = ->
## restore the fn
@snapshot = fn
## call orig fn with next as name
fn.call(@, next)
return @
error: (err) ->
@set({
ended: true
error: err
state: "failed"
})
return @
end: ->
## dont set back to passed
## if we've already ended
return if @get("ended")
@set({
ended: true
state: "passed"
})
return @
getError: (err) ->
## dont log stack traces on cypress errors
## or assertion errors
if CypressErrorRe.test(err.name)
err.toString()
else
err.stack
setElAttrs: ->
$el = @get("$el")
return if not $el
if _.isElement($el)
## wrap the element in jquery
## if its just a plain element
return @set("$el", $($el), {silent: true})
## if we've passed something like
## <window> or <document> here or
## a primitive then unset $el
if not $dom.isJquery($el)
return @unset("$el")
## make sure all $el elements are visible!
obj = {
highlightAttr: HIGHLIGHT_ATTR
numElements: $el.length
visible: $el.length is $el.filter(":visible").length
}
@set(obj, {silent: true})
merge: (log) ->
## merges another logs attributes into
## ours by also removing / adding any properties
## on the original
## 1. calculate which properties to unset
unsets = _.chain(attributes).keys().without(_.keys(log.get())...).value()
_.each unsets, (unset) =>
@unset(unset)
## 2. merge in any other properties
@set(log.get())
_shouldAutoEnd: ->
## must be autoEnd
## and not already ended
## and not an event
## and a command
@get("autoEnd") isnt false and
@get("ended") isnt true and
@get("event") is false and
@get("instrument") is "command"
finish: ->
## end our command since our subject
## has been resolved at this point
## unless its already been 'ended'
## or has been specifically told not to auto resolve
if @_shouldAutoEnd()
@snapshot().end()
wrapConsoleProps: ->
_this = @
consoleProps = attributes.consoleProps
## re-wrap consoleProps to set Command + Error defaults
attributes.consoleProps = ->
key = if _this.get("event") then "Event" else "Command"
consoleObj = {}
consoleObj[key] = _this.get("name")
## merge in the other properties from consoleProps
_.extend consoleObj, consoleProps.apply(@, arguments)
## TODO: right here we need to automatically
## merge in "Yielded + Element" if there is an $el
## and finally add error if one exists
if err = _this.get("error")
_.defaults(consoleObj, {
Error: _this.getError(err)
})
## add note if no snapshot exists on command instruments
if _this.get("instrument") is "command" and not _this.get("snapshots")
consoleObj.Snapshot = "The snapshot is missing. Displaying current state of the DOM."
else
delete consoleObj.Snapshot
return consoleObj
}
create = (Cypress, cy, state, config) ->
counter = 0
logs = {}
## give us the ability to change the delay for firing
## the change event, or default it to 4
delay ?= setDelay(config("logAttrsDelay"))
trigger = (log, event) ->
## bail if we never fired our initial log event
return if not log._hasInitiallyLogged
## bail if we've reset the logs due to a Cypress.abort
return if not logs[log.get("id")]
attrs = log.toJSON()
## only trigger this event if our last stored
## emitted attrs do not match the current toJSON
if not _.isEqual(log._emittedAttrs, attrs)
log._emittedAttrs = attrs
log.emit(event, attrs)
Cypress.action(event, attrs, log)
triggerLog = (log) ->
log._hasInitiallyLogged = true
trigger(log, "command:log:added")
addToLogs = (log) ->
id = log.get("id")
logs[id] = true
logFn = (options = {}) ->
if !_.isObject(options)
$utils.throwErrByPath "log.invalid_argument", {args: { arg: options }}
attributes = {}
log = Log(cy, state, config, options)
## add event emitter interface
$Events.extend(log)
triggerStateChanged = ->
trigger(log, "command:log:changed")
## only fire the log:state:changed event
## as fast as every 4ms
log.fireChangeEvent = _.debounce(triggerStateChanged, 4)
log.set(options)
## if snapshot was passed
## in, go ahead and snapshot
log.snapshot() if log.get("snapshot")
## if end was passed in
## go ahead and end
log.end({silent: true}) if log.get("end")
if err = log.get("error")
log.error(err, {silent: true})
log.wrapConsoleProps()
onBeforeLog = state("onBeforeLog")
## dont trigger log if this function
## explicitly returns false
if _.isFunction(onBeforeLog)
return if onBeforeLog.call(cy, log) is false
## set the log on the command
state("current")?.log(log)
addToLogs(log)
triggerLog(log)
## if not current state then the log is being run
## with no command reference, so just end the log
if not state("current") then log.end({silent: true})
return log
logFn._logs = logs
return logFn
module.exports = {
CypressErrorRe
reduceMemory
toSerializedJSON
getDisplayProps
getConsoleProps
getSnapshotProps
countLogsByTests
setCounter
create
}
+630
View File
@@ -0,0 +1,630 @@
const _ = require('lodash')
const $ = require('jquery')
const clone = require('clone')
const $Snapshots = require('../cy/snapshots')
const $Events = require('./events')
const $dom = require('../dom')
const $utils = require('./utils')
// adds class methods for command, route, and agent logging
// including the intermediate $Log interface
const CypressErrorRe = /(AssertionError|CypressError)/
const groupsOrTableRe = /^(groups|table)$/
const parentOrChildRe = /parent|child/
const ERROR_PROPS = 'message type name stack fileName lineNumber columnNumber host uncaught actual expected showDiff'.split(' ')
const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ')
const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName hookName instrument isStubbed message method name numElements numResponses referencesAlias renderProps state testId type url visible'.split(' ')
const BLACKLIST_PROPS = 'snapshots'.split(' ')
let delay = null
let counter = 0
const { HIGHLIGHT_ATTR } = $Snapshots
// mutate attrs by nulling out
// object properties
const reduceMemory = (attrs) => {
return _.each(attrs, (value, key) => {
if (_.isObject(value)) {
attrs[key] = null
}
})
}
const toSerializedJSON = function (attrs) {
const { isDom } = $dom
const stringify = function (value, key) {
if (BLACKLIST_PROPS.includes(key)) {
return null
}
if (_.isArray(value)) {
return _.map(value, stringify)
}
if (isDom(value)) {
return $dom.stringify(value, 'short')
}
if (!(!_.isFunction(value) || !groupsOrTableRe.test(key))) {
return value()
}
if (_.isFunction(value)) {
return value.toString()
}
if (_.isObject(value)) {
// clone to nuke circular references
// and blow away anything that throws
try {
return _.mapValues(clone(value), stringify)
} catch (err) {
return null
}
}
return value
}
return _.mapValues(attrs, stringify)
}
const getDisplayProps = (attrs) => {
return _.pick(attrs, DISPLAY_PROPS)
}
const getConsoleProps = (attrs) => {
return attrs.consoleProps
}
const getSnapshotProps = (attrs) => {
return _.pick(attrs, SNAPSHOT_PROPS)
}
const countLogsByTests = function (tests = {}) {
if (_.isEmpty(tests)) {
return 0
}
return _
.chain(tests)
.map((test, key) => {
return [].concat(test.agents, test.routes, test.commands)
}).flatten()
.compact()
.union([{ id: 0 }])
.map('id')
.max()
.value()
}
// TODO: fix this
const setCounter = (num) => {
return counter = num
}
const setDelay = (val) => {
return delay = val != null ? val : 4
}
const defaults = function (state, config, obj) {
const instrument = obj.instrument != null ? obj.instrument : 'command'
// dont set any defaults if this
// is an agent or route because we
// may not even be inside of a command
if (instrument === 'command') {
const current = state('current')
// we are logging a command instrument by default
_.defaults(obj, current != null ? current.pick('name', 'type') : undefined)
// force duals to become either parents or childs
// normally this would be handled by the command itself
// but in cases where the command purposely does not log
// then it could still be logged during a failure, which
// is why we normalize its type value
if (!parentOrChildRe.test(obj.type)) {
// does this command have a previously linked command
// by chainer id
obj.type = (current != null ? current.hasPreviouslyLinkedCommand() : undefined) ? 'child' : 'parent'
}
_.defaults(obj, {
event: false,
renderProps () {
return {}
},
consoleProps () {
// if we don't have a current command just bail
if (!current) {
return {}
}
const ret = $dom.isElement(current.get('subject')) ?
$dom.getElements(current.get('subject'))
:
current.get('subject')
return { Yielded: ret }
},
})
// if obj.isCurrent
// stringify the obj.message (if it exists) or current.get("args")
obj.message = $utils.stringify(obj.message != null ? obj.message : (current != null ? current.get('args') : undefined))
// allow type to by a dynamic function
// so it can conditionally return either
// parent or child (useful in assertions)
if (_.isFunction(obj.type)) {
obj.type = obj.type(current, state('subject'))
}
}
const runnable = state('runnable')
return _.defaults(obj, {
id: (counter += 1),
state: 'pending',
instrument: 'command',
url: state('url'),
hookName: state('hookName'),
testId: runnable ? runnable.id : undefined,
viewportWidth: state('viewportWidth'),
viewportHeight: state('viewportHeight'),
referencesAlias: undefined,
alias: undefined,
aliasType: undefined,
message: undefined,
renderProps () {
return {}
},
consoleProps () {
return {}
},
})
}
const Log = function (cy, state, config, obj) {
obj = defaults(state, config, obj)
// private attributes of each log
const attributes = {}
return {
get (attr) {
if (attr) {
return attributes[attr]
}
return attributes
},
unset (key) {
return this.set(key, undefined)
},
invoke (key) {
const invoke = () => {
// ensure this is a callable function
// and set its default to empty object literal
const fn = this.get(key)
if (_.isFunction(fn)) {
return fn()
}
return fn
}
return invoke() || {}
},
serializeError () {
let err = this.get('error')
if (err) {
return _.reduce(ERROR_PROPS, (memo, prop) => {
if (_.has(err, prop) || err[prop]) {
memo[prop] = err[prop]
}
return memo
}, {})
}
return null
},
toJSON () {
return _
.chain(attributes)
.omit('error')
.omitBy(_.isFunction)
.extend({
err: this.serializeError(),
consoleProps: this.invoke('consoleProps'),
renderProps: this.invoke('renderProps'),
})
.value()
},
set (key, val) {
if (_.isString(key)) {
obj = {}
obj[key] = val
} else {
obj = key
}
if ('url' in obj) {
// always stringify the url property
obj.url = (obj.url != null ? obj.url : '').toString()
}
// convert onConsole to consoleProps
// for backwards compatibility
if (obj.onConsole) {
obj.consoleProps = obj.onConsole
}
// if we have an alias automatically
// figure out what type of alias it is
if (obj.alias) {
_.defaults(obj, { aliasType: obj.$el ? 'dom' : 'primitive' })
}
// dont ever allow existing id's to be mutated
if (attributes.id) {
delete obj.id
}
_.extend(attributes, obj)
// if we have an consoleProps function
// then re-wrap it
if (obj && _.isFunction(obj.consoleProps)) {
this.wrapConsoleProps()
}
if (obj && obj.$el) {
this.setElAttrs()
}
this.fireChangeEvent()
return this
},
pick (...args) {
return _.pick(attributes, args)
},
publicInterface () {
return {
get: _.bind(this.get, this),
on: _.bind(this.on, this),
off: _.bind(this.off, this),
pick: _.bind(this.pick, this),
attributes,
}
},
snapshot (name, options = {}) {
// bail early and don't snapshot if we're in headless mode
// or we're not storing tests
if (!config('isInteractive') || (config('numTestsKeptInMemory') === 0)) {
return this
}
_.defaults(options, {
at: null,
next: null,
})
const snapshot = cy.createSnapshot(name, this.get('$el'))
const snapshots = this.get('snapshots') || []
// insert at index 'at' or whatever is the next position
snapshots[options.at || snapshots.length] = snapshot
this.set('snapshots', snapshots)
if (options.next) {
const fn = this.snapshot
this.snapshot = function () {
// restore the fn
this.snapshot = fn
// call orig fn with next as name
return fn.call(this, options.next)
}
}
return this
},
error (err) {
this.set({
ended: true,
error: err,
state: 'failed',
})
return this
},
end () {
// dont set back to passed
// if we've already ended
if (this.get('ended')) {
return
}
this.set({
ended: true,
state: 'passed',
})
return this
},
getError (err) {
// dont log stack traces on cypress errors
// or assertion errors
if (CypressErrorRe.test(err.name)) {
return err.toString()
}
return err.stack
},
setElAttrs () {
const $el = this.get('$el')
if (!$el) {
return
}
if (_.isElement($el)) {
// wrap the element in jquery
// if its just a plain element
return this.set('$el', $($el), { silent: true })
}
// if we've passed something like
// <window> or <document> here or
// a primitive then unset $el
if (!$dom.isJquery($el)) {
return this.unset('$el')
}
// make sure all $el elements are visible!
obj = {
highlightAttr: HIGHLIGHT_ATTR,
numElements: $el.length,
visible: $el.length === $el.filter(':visible').length,
}
return this.set(obj, { silent: true })
},
merge (log) {
// merges another logs attributes into
// ours by also removing / adding any properties
// on the original
// 1. calculate which properties to unset
const unsets = _.chain(attributes).keys().without(..._.keys(log.get())).value()
_.each(unsets, (unset) => {
return this.unset(unset)
})
// 2. merge in any other properties
return this.set(log.get())
},
_shouldAutoEnd () {
// must be autoEnd
// and not already ended
// and not an event
// and a command
return (this.get('autoEnd') !== false) &&
(this.get('ended') !== true) &&
(this.get('event') === false) &&
(this.get('instrument') === 'command')
},
finish () {
// end our command since our subject
// has been resolved at this point
// unless its already been 'ended'
// or has been specifically told not to auto resolve
if (this._shouldAutoEnd()) {
return this.snapshot().end()
}
},
wrapConsoleProps () {
const _this = this
const { consoleProps } = attributes
attributes.consoleProps = function (...args) {
const key = _this.get('event') ? 'Event' : 'Command'
const consoleObj = {}
consoleObj[key] = _this.get('name')
// merge in the other properties from consoleProps
_.extend(consoleObj, consoleProps.apply(this, args))
// TODO: right here we need to automatically
// merge in "Yielded + Element" if there is an $el
// and finally add error if one exists
if (_this.get('error')) {
_.defaults(consoleObj, {
Error: _this.getError(_this.get('error')),
})
}
// add note if no snapshot exists on command instruments
if ((_this.get('instrument') === 'command') && !_this.get('snapshots')) {
consoleObj.Snapshot = 'The snapshot is missing. Displaying current state of the DOM.'
} else {
delete consoleObj.Snapshot
}
return consoleObj
}
},
}
}
const create = function (Cypress, cy, state, config) {
counter = 0
const logs = {}
// give us the ability to change the delay for firing
// the change event, or default it to 4
if (delay == null) {
delay = setDelay(config('logAttrsDelay'))
}
const trigger = function (log, event) {
// bail if we never fired our initial log event
if (!log._hasInitiallyLogged) {
return
}
// bail if we've reset the logs due to a Cypress.abort
if (!logs[log.get('id')]) {
return
}
const attrs = log.toJSON()
// only trigger this event if our last stored
// emitted attrs do not match the current toJSON
if (!_.isEqual(log._emittedAttrs, attrs)) {
log._emittedAttrs = attrs
log.emit(event, attrs)
return Cypress.action(event, attrs, log)
}
}
const triggerLog = function (log) {
log._hasInitiallyLogged = true
return trigger(log, 'command:log:added')
}
const addToLogs = function (log) {
const id = log.get('id')
logs[id] = true
}
const logFn = function (options = {}) {
if (!_.isObject(options)) {
$utils.throwErrByPath('log.invalid_argument', { args: { arg: options } })
}
const log = Log(cy, state, config, options)
// add event emitter interface
$Events.extend(log)
const triggerStateChanged = () => {
return trigger(log, 'command:log:changed')
}
// only fire the log:state:changed event
// as fast as every 4ms
log.fireChangeEvent = _.debounce(triggerStateChanged, 4)
log.set(options)
// if snapshot was passed
// in, go ahead and snapshot
if (log.get('snapshot')) {
log.snapshot()
}
// if end was passed in
// go ahead and end
if (log.get('end')) {
log.end({ silent: true })
}
if (log.get('error')) {
log.error(log.get('error'), { silent: true })
}
log.wrapConsoleProps()
const onBeforeLog = state('onBeforeLog')
// dont trigger log if this function
// explicitly returns false
if (_.isFunction(onBeforeLog)) {
if (onBeforeLog.call(cy, log) === false) {
return
}
}
// set the log on the command
const current = state('current')
if (current) {
current.log(log)
}
addToLogs(log)
triggerLog(log)
// if not current state then the log is being run
// with no command reference, so just end the log
if (!current) {
log.end({ silent: true })
}
return log
}
logFn._logs = logs
return logFn
}
module.exports = {
CypressErrorRe,
reduceMemory,
toSerializedJSON,
getDisplayProps,
getConsoleProps,
getSnapshotProps,
countLogsByTests,
setCounter,
create,
}
+1 -1
View File
@@ -84,7 +84,7 @@ module.exports = {
## because the browser has a cached
## dynamic stack getter that will
## not be evaluated later
stack = err.stack
stack = err.stack or ''
## preserve message
## and toString
+8 -1
View File
@@ -203,7 +203,7 @@ const getBottomRightCoordinates = (rect) => {
const getElementCoordinatesByPositionRelativeToXY = ($el, x, y) => {
const positionProps = getElementPositioning($el)
const { fromElViewport, fromElWindow } = positionProps
const { fromElViewport, fromElWindow, fromAutWindow } = positionProps
fromElViewport.left += x
fromElViewport.top += y
@@ -211,8 +211,12 @@ const getElementCoordinatesByPositionRelativeToXY = ($el, x, y) => {
fromElWindow.left += x
fromElWindow.top += y
fromAutWindow.left += x
fromAutWindow.top += y
const viewportTargetCoords = getTopLeftCoordinates(fromElViewport)
const windowTargetCoords = getTopLeftCoordinates(fromElWindow)
const autWindowTargetCoords = getTopLeftCoordinates(fromAutWindow)
fromElViewport.x = viewportTargetCoords.x
fromElViewport.y = viewportTargetCoords.y
@@ -220,6 +224,9 @@ const getElementCoordinatesByPositionRelativeToXY = ($el, x, y) => {
fromElWindow.x = windowTargetCoords.x
fromElWindow.y = windowTargetCoords.y
fromAutWindow.x = autWindowTargetCoords.x
fromAutWindow.y = autWindowTargetCoords.y
return positionProps
}
+7
View File
@@ -505,6 +505,12 @@ const isInputType = function (el: JQueryOrEl<HTMLElement>, type) {
return elType === type
}
const isAttrType = function (el: HTMLInputElement, type: string) {
const elType = (el.getAttribute('type') || '').toLowerCase()
return elType === type
}
const isScrollOrAuto = (prop) => {
return prop === 'scroll' || prop === 'auto'
}
@@ -1079,6 +1085,7 @@ export {
isIframe,
isTextarea,
isInputType,
isAttrType,
isFocused,
isFocusedOrInFocused,
isInputAllowingImplicitFormSubmission,
+177 -131
View File
@@ -1,8 +1,11 @@
import _ from 'lodash'
import * as $dom from '../dom'
import * as $document from './document'
import * as $elements from './elements'
const debug = require('debug')('cypress:driver:selection')
const INTERNAL_STATE = '__Cypress_state__'
const _getSelectionBoundsFromTextarea = (el) => {
return {
start: $elements.getNativeProp(el, 'selectionStart'),
@@ -18,12 +21,18 @@ const _getSelectionBoundsFromInput = function (el) {
}
}
const doc = $document.getDocumentFromElement(el)
const range = _getSelectionRange(doc)
const internalState = el[INTERNAL_STATE]
if (internalState) {
return {
start: internalState.start,
end: internalState.end,
}
}
return {
start: range.startOffset,
end: range.endOffset,
start: 0,
end: 0,
}
}
@@ -58,9 +67,11 @@ const _getSelectionBoundsFromContentEditable = function (el) {
}
// TODO get ACTUAL caret position in contenteditable, not line
const _replaceSelectionContentsWithExecCommand = function (doc, text) {
const _replaceSelectionContentsContentEditable = function (el, text) {
const doc = $document.getDocumentFromElement(el)
// NOTE: insertText will also handle '\n', and render newlines
return $elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text)
$elements.callNativeMethod(doc, 'execCommand', 'insertText', true, text)
}
// Keeping around native implementation
@@ -102,7 +113,7 @@ const _replaceSelectionContentsWithExecCommand = function (doc, text) {
// # startNode.nodeValue = updatedValue
// el.normalize()
const _insertSubstring = (curText, newText, [start, end]) => {
const insertSubstring = (curText, newText, [start, end]) => {
return curText.substring(0, start) + newText + curText.substring(end)
}
@@ -128,31 +139,39 @@ const getHostContenteditable = function (el) {
return curEl
}
/**
*
* @param {HTMLElement} el
* @returns {Selection}
*/
const _getSelectionByEl = function (el) {
const doc = $document.getDocumentFromElement(el)
return doc.getSelection()!
}
const deleteSelectionContents = function (el: HTMLElement) {
const deleteSelectionContents = function (el) {
if ($elements.isContentEditable(el)) {
const doc = $document.getDocumentFromElement(el)
$elements.callNativeMethod(doc, 'execCommand', 'delete', false, null)
return false
return
}
return replaceSelectionContents(el, '')
}
const setSelectionRange = function (el, start, end) {
$elements.callNativeMethod(el, 'setSelectionRange', start, end)
if ($elements.canSetSelectionRangeElement(el)) {
$elements.callNativeMethod(el, 'setSelectionRange', start, end)
return
}
// NOTE: Some input elements have mobile implementations
// and thus may not always have a cursor, so calling setSelectionRange will throw.
// we are assuming desktop here, so we store our own internal state.
el[INTERNAL_STATE] = {
start,
end,
}
}
// Whether or not the selection contains any text
@@ -162,59 +181,98 @@ const isSelectionCollapsed = function (selection: Selection) {
return !selection.toString()
}
/**
* @returns {boolean} whether or not input events are needed
*/
const deleteRightOfCursor = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
if (start === $elements.getNativeProp(el, 'value').length) {
// nothing to delete, nothing to right of selection
return false
}
if (start === end) {
setSelectionRange(el, start, end + 1)
}
return deleteSelectionContents(el)
deleteSelectionContents(el)
// successful delete, needs input events
return true
}
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'forward',
'character'
)
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'forward',
'character',
)
}
if ($elements.getNativeProp(selection, 'isCollapsed')) {
// there's nothing to delete
return false
}
deleteSelectionContents(el)
// successful delete, does not need input events
return false
}
deleteSelectionContents(el)
return false
}
/**
* @returns {boolean} whether or not input events are needed
*/
const deleteLeftOfCursor = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
debug('delete left of cursor input/textarea', start, end)
if (start === end) {
if (start === 0) {
// there's nothing to delete, nothing before cursor
return false
}
setSelectionRange(el, start - 1, end)
}
return deleteSelectionContents(el)
deleteSelectionContents(el)
// successful delete
return true
}
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
// there is no 'backwardDelete' command for execCommand, so use the Selection API
const selection = _getSelectionByEl(el)
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'backward',
'character'
)
if (isSelectionCollapsed(selection)) {
$elements.callNativeMethod(
selection,
'modify',
'extend',
'backward',
'character'
)
}
deleteSelectionContents(el)
return false
}
deleteSelectionContents(el)
return false
}
@@ -223,7 +281,7 @@ const _collapseInputOrTextArea = (el, toIndex) => {
}
const moveCursorLeft = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
if (start !== end) {
@@ -237,17 +295,17 @@ const moveCursorLeft = function (el) {
return setSelectionRange(el, start - 1, start - 1)
}
// if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return $elements.callNativeMethod(
selection,
'modify',
'move',
'backward',
'character'
)
// }
if (selection.isCollapsed) {
return $elements.callNativeMethod(selection, 'modify', 'move', 'backward', 'character')
}
selection.collapseToStart()
return
}
}
// Keeping around native implementation
@@ -263,7 +321,7 @@ const moveCursorLeft = function (el) {
// range.setEnd(range.startContainer, newOffset)
const moveCursorRight = function (el) {
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
if (start !== end) {
@@ -275,15 +333,11 @@ const moveCursorRight = function (el) {
return setSelectionRange(el, start + 1, end + 1)
}
const selection = _getSelectionByEl(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return $elements.callNativeMethod(
selection,
'modify',
'move',
'forward',
'character'
)
return $elements.callNativeMethod(selection, 'modify', 'move', 'forward', 'character')
}
}
const moveCursorUp = (el) => {
@@ -314,16 +368,15 @@ const _moveCursorUpOrDown = function (el, up) {
return
}
if ($elements.isTextarea(el) || $elements.isContentEditable(el)) {
const isTextarea = $elements.isTextarea(el)
if (isTextarea || $elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return $elements.callNativeMethod(
selection,
'modify',
return $elements.callNativeMethod(selection, 'modify',
'move',
up ? 'backward' : 'forward',
'line'
)
'line')
}
}
@@ -335,8 +388,12 @@ const moveCursorToLineEnd = (el) => {
return _moveCursorToLineStartOrEnd(el, false)
}
const _moveCursorToLineStartOrEnd = function (el, toStart) {
if ($elements.isContentEditable(el) || $elements.isInput(el) || $elements.isTextarea(el)) {
const _moveCursorToLineStartOrEnd = function (el: HTMLElement, toStart) {
const isInput = $elements.isInput(el)
const isTextarea = $elements.isTextarea(el)
const isInputOrTextArea = isInput || isTextarea
if ($elements.isContentEditable(el) || isInputOrTextArea) {
const selection = _getSelectionByEl(el)
// the selection.modify API is non-standard, may work differently in other browsers, and is not in IE11.
@@ -345,34 +402,34 @@ const _moveCursorToLineStartOrEnd = function (el, toStart) {
}
}
const isCollapsed = (el: HTMLElement) => {
if ($elements.canSetSelectionRangeElement(el)) {
const isCollapsed = function (el) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
const { start, end } = getSelectionBounds(el)
return start === end
}
const doc = $document.getDocumentFromElement(el)
if ($elements.isContentEditable(el)) {
const selection = _getSelectionByEl(el)
return _getSelectionRange(doc).collapsed
return selection.isCollapsed
}
return false
}
const selectAll = function (doc) {
const el = _getActive(doc)
if ($elements.canSetSelectionRangeElement(el)) {
const selectAll = function (el: HTMLElement) {
if ($elements.isTextarea(el) || $elements.isInput(el)) {
setSelectionRange(el, 0, $elements.getNativeProp(el, 'value').length)
return
}
return $elements.callNativeMethod(
doc,
'execCommand',
'selectAll',
false,
null
)
if ($elements.isContentEditable(el)) {
const doc = $document.getDocumentFromElement(el)
return $elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
}
}
// Keeping around native implementation
// for same reasons as listed below
@@ -403,14 +460,14 @@ const getSelectionBounds = function (el) {
}
}
const _moveSelectionTo = function (toStart: boolean, doc: Document, options = {}) {
const _moveSelectionTo = function (toStart: boolean, el: HTMLElement, options = {}) {
const opts = _.defaults({}, options, {
onlyIfEmptySelection: false,
})
const el = _getActive(doc)
const doc = $document.getDocumentFromElement(el)
if ($elements.canSetSelectionRangeElement(el)) {
if ($elements.isInput(el) || $elements.isTextarea(el)) {
if (opts.onlyIfEmptySelection) {
const { start, end } = getSelectionBounds(el)
@@ -430,48 +487,52 @@ const _moveSelectionTo = function (toStart: boolean, doc: Document, options = {}
return
}
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
const selection = doc.getSelection()
if ($elements.isContentEditable(el)) {
$elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null)
const selection = doc.getSelection()
if (!selection) {
return
}
// collapsing the range doesn't work on input/textareas, since the range contains more than the input element
// However, IE can always* set selection range, so only modern browsers (with the selection API) will need this
const direction = toStart ? 'backward' : 'forward'
selection.modify('move', direction, 'line')
if (!selection) {
return
}
// collapsing the range doesn't work on input/textareas, since the range contains more than the input element
// However, IE can always* set selection range, so only modern browsers (with the selection API) will need this
const direction = toStart ? 'backward' : 'forward'
selection.modify('move', direction, 'line')
return false
}
const moveSelectionToEnd = _.curry(_moveSelectionTo)(false)
const moveSelectionToStart = _.curry(_moveSelectionTo)(true)
const replaceSelectionContents = function (el: HTMLElement, key: string) {
if ($elements.canSetSelectionRangeElement(el)) {
// if ($elements.isRead)
const replaceSelectionContents = function (el, key) {
if ($elements.isContentEditable(el)) {
_replaceSelectionContentsContentEditable(el, key)
return
}
if ($elements.isInput(el) || $elements.isTextarea(el)) {
const { start, end } = getSelectionBounds(el)
const value = $elements.getNativeProp(el, 'value') || ''
const updatedValue = _insertSubstring(value, key, [start, end])
if (value === updatedValue) {
return false
}
const updatedValue = insertSubstring(value, key, [start, end])
debug(`inserting at selection ${JSON.stringify({ start, end })}`, 'rewriting value to ', updatedValue)
$elements.setNativeProp(el, 'value', updatedValue)
setSelectionRange(el, start + key.length, start + key.length)
return true
return
}
const doc = $document.getDocumentFromElement(el)
_replaceSelectionContentsWithExecCommand(doc, key)
return false
}
const getCaretPosition = function (el) {
@@ -489,28 +550,12 @@ const getCaretPosition = function (el) {
return null
}
const _getActive = function (doc) {
// TODO: remove this state access
// eslint-disable-next-line
const activeEl = $elements.getNativeProp(doc, 'activeElement')
return activeEl
}
const focusCursor = function (el, doc) {
const elToFocus = $elements.getFirstFocusableEl($dom.wrap(el)).get(0)
const prevFocused = _getActive(doc)
elToFocus.focus()
if ($elements.isInput(elToFocus) || $elements.isTextarea(elToFocus)) {
moveSelectionToEnd(doc)
const interceptSelect = function () {
if ($elements.isInput(this) && !$elements.canSetSelectionRangeElement(this)) {
setSelectionRange(this, 0, $elements.getNativeProp(this, 'value').length)
}
if ($elements.isContentEditable(elToFocus) && prevFocused !== elToFocus) {
moveSelectionToEnd(doc)
}
return $elements.callNativeMethod(this, 'select')
}
// Selection API implementation of insert newline.
@@ -606,5 +651,6 @@ export {
moveCursorToLineEnd,
replaceSelectionContents,
isCollapsed,
focusCursor,
insertSubstring,
interceptSelect,
}
+108 -1
View File
@@ -61,6 +61,17 @@ const isHidden = (el, name = 'isHidden()') => {
return true // is hidden
}
// when an element is scaled to 0 in one axis
// it is not visible to users.
// So, it is hidden.
if (elIsHiddenByTransform($el)) {
return true
}
if (elIsBackface($el)) {
return true
}
// we do some calculations taking into account the parents
// to see if its hidden by a parent
if (elIsHiddenByAncestors($el)) {
@@ -112,10 +123,86 @@ const elHasVisibilityHidden = ($el) => {
return $el.css('visibility') === 'hidden'
}
const numberRegex = /-?[0-9]+(?:\.[0-9]+)?/g
// This is a simplified version of backface culling.
// https://en.wikipedia.org/wiki/Back-face_culling
//
// We defined view normal vector, (0, 0, -1), - eye to screen.
// and default normal vector, (0, 0, 1)
// When dot product of them are >= 0, item is visible.
const elIsBackface = ($el) => {
const el = $el[0]
const style = getComputedStyle(el)
const backface = style.getPropertyValue('backface-visibility')
const backfaceInvisible = backface === 'hidden'
const transform = style.getPropertyValue('transform')
if (!backfaceInvisible || !transform.startsWith('matrix3d')) {
return false
}
const m3d = transform.substring(8).match(numberRegex)
const defaultNormal = [0, 0, -1]
const elNormal = findNormal(m3d)
// Simplified dot product.
// [0] and [1] are always 0
const dot = defaultNormal[2] * elNormal[2]
return dot >= 0
}
const findNormal = (m) => {
const length = Math.sqrt(+m[8] * +m[8] + +m[9] * +m[9] + +m[10] * +m[10])
return [+m[8] / length, +m[9] / length, +m[10] / length]
}
const elHasVisibilityCollapse = ($el) => {
return $el.css('visibility') === 'collapse'
}
// This function checks 2 things that can happen: scale and rotate
const elIsHiddenByTransform = ($el) => {
// We need to see the final calculation of the element.
const el = $el[0]
const style = window.getComputedStyle(el)
const transform = style.getPropertyValue('transform')
// Test scaleZ(0)
// width or height of getBoundingClientRect aren't 0 when scaleZ(0).
// But it is invisible.
// Experiment -> https://codepen.io/sainthkh/pen/LYYQGpm
// That's why we're checking transfomation matrix here.
//
// To understand how this part works,
// you need to understand tranformation matrix first.
// Matrix is hard to explain with only text. So, check these articles.
//
// https://www.useragentman.com/blog/2011/01/07/css3-matrix-transform-for-the-mathematically-challenged/
// https://en.wikipedia.org/wiki/Rotation_matrix#In_three_dimensions
//
if (transform.startsWith('matrix3d')) {
const m3d = transform.substring(8).match(numberRegex)
// Z Axis values
if (+m3d[2] === 0 && +m3d[6] === 0 && +m3d[10] === 0) {
return true
}
}
// Other cases
if (transform !== 'none') {
const { width, height } = el.getBoundingClientRect()
if (width === 0 || height === 0) {
return true
}
}
return false
}
const elHasDisplayNone = ($el) => {
return $el.css('display') === 'none'
}
@@ -272,6 +359,14 @@ const elIsHiddenByAncestors = function ($el, $origEl = $el) {
return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl)
}
if (elIsHiddenByTransform($parent)) {
return true
}
if (elIsBackface($parent)) {
return true
}
// continue to recursively walk up the chain until we reach body or html
return elIsHiddenByAncestors($parent, $origEl)
}
@@ -388,6 +483,14 @@ const getReasonIsHidden = function ($el) {
return `This element '${node}' is not visible because it has an effective width and height of: '${width} x ${height}' pixels.`
}
if (elIsHiddenByTransform($el)) {
return `This element '${node}' is not visible because it is hidden by transform.`
}
if (elIsBackface($el)) {
return `This element '${node}' is not visible because it is rotated and its backface is hidden.`
}
if ($parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())) {
parentNode = $elements.stringify($parent, 'short')
width = elOffsetWidth($parent)
@@ -402,11 +505,15 @@ const getReasonIsHidden = function ($el) {
// show the long element here
const covered = $elements.stringify(elAtCenterPoint($el))
return `\
if (covered) {
return `\
This element '${node}' is not visible because it has CSS property: 'position: fixed' and its being covered by another element:
${covered}\
`
}
return `This element '${node}' is not visible because its ancestor has 'position: fixed' CSS property and it is overflowed by other elements. How about scrolling to the element with cy.scrollIntoView()?`
}
} else {
if (elIsOutOfBoundsOfAncestorsOverflow($el)) {
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<script>
window.addEventListener('beforeunload', function (e) {
e.preventDefault();
e.returnValue = 'Dialog';
return;
})
</script>
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<script>
window.onbeforeunload = function (e) {
e.preventDefault();
e.returnValue = 'Dialog';
return;
}
</script>

Some files were not shown because too many files have changed in this diff Show More