mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-27 10:19:26 -05:00
Merge branch 'develop' into v4.0-release
This commit is contained in:
+1
-2
@@ -4,6 +4,5 @@
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@cypress/dev/general"
|
||||
],
|
||||
"rules": {}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 code’s 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)?
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -53,7 +53,7 @@ npm install cypress --save-dev
|
||||
- [](https://circleci.com/gh/cypress-io/cypress/tree/develop) - `develop` branch
|
||||
- [](https://circleci.com/gh/cypress-io/cypress/tree/master) - `master` branch
|
||||
|
||||
Please see our [Contributing Guideline](/CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps.
|
||||
Please see our [Contributing Guideline](./CONTRIBUTING.md) which explains repo organization, linting, testing, and other steps.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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'] = `
|
||||
[33m⚠[39m 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'] = `
|
||||
[33m⚠[39m 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'] = `
|
||||
[33m⚠[39m 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
|
||||
`
|
||||
|
||||
@@ -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 [36mexit[39m event with signal [36mSIGKILL[39m",
|
||||
"solution": "Please search Cypress documentation for possible solutions:\n\n [34mhttps://on.cypress.io[39m\n\nCheck if there is a GitHub issue describing this crash:\n\n [34mhttps://github.com/cypress-io/cypress/issues[39m\n\nConsider opening a new issue."
|
||||
}
|
||||
|
||||
exports['Error message'] = `
|
||||
The Test Runner unexpectedly exited via a [36mexit[39m event with signal [36mSIGKILL[39m
|
||||
|
||||
Please search Cypress documentation for possible solutions:
|
||||
|
||||
[34mhttps://on.cypress.io[39m
|
||||
|
||||
Check if there is a GitHub issue describing this crash:
|
||||
|
||||
[34mhttps://github.com/cypress-io/cypress/issues[39m
|
||||
|
||||
Consider opening a new issue.
|
||||
|
||||
----------
|
||||
|
||||
Platform: test platform (Foo-OsVersion)
|
||||
Cypress Version: 1.2.3
|
||||
`
|
||||
|
||||
@@ -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'] = [
|
||||
|
||||
@@ -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 [36mexit[39m event with signal [36mSIGKILL[39m
|
||||
|
||||
Please search Cypress documentation for possible solutions:
|
||||
|
||||
[34mhttps://on.cypress.io[39m
|
||||
|
||||
Check if there is a GitHub issue describing this crash:
|
||||
|
||||
[34mhttps://github.com/cypress-io/cypress/issues[39m
|
||||
|
||||
Consider opening a new issue.
|
||||
|
||||
----------
|
||||
|
||||
Platform: darwin (Foo-OsVersion)
|
||||
Cypress Version: 0.0.0
|
||||
`
|
||||
+93
-127
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Vendored
+3
-3
@@ -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
|
||||
*/
|
||||
|
||||
Vendored
+67
-4
@@ -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 element’s left to issue the click.
|
||||
* @param {number} y The distance in pixels from the element’s top to issue the click.
|
||||
* @see https://on.cypress.io/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 element’s left to issue the click.
|
||||
* @param {number} y The distance in pixels from the element’s top to issue the click.
|
||||
* @see https://on.cypress.io/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 property’s 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.
|
||||
|
||||
@@ -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
@@ -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 () {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
line-height: 3em;
|
||||
}
|
||||
|
||||
.fa {
|
||||
// font-size: 50px;
|
||||
.fas {
|
||||
color: #d3d6d8;
|
||||
|
||||
&.fa-stack-2x {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
test/cypress/videos
|
||||
test/cypress/screenshots
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, ' ')
|
||||
})
|
||||
.replace(trailingWhitespaces, (match) => {
|
||||
return match.replace(`'**`, '__quote__**')
|
||||
.replace(whitespace, ' ')
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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...])
|
||||
})
|
||||
|
||||
@@ -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
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 })
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user